문서 저장소와 증분 맵리듀스의 결합으로 데이터를 쿼리할 수 있는 강력한 방법을 제공했기 때문에 Ziniki 인프라 시스템은 Couchbase 위에 통합 계층을 구축했습니다. 이 블로그에서는 Ziniki의 창립자이자 아키텍트인 Gareth Powell이 Couchbase에서 맵리듀스 보기를 사용한 자신의 경험을 설명합니다. 


저는 SQL에 대한 배경 지식이 풍부하고 MongoDB를 어느 정도 접해본 경험이 있습니다. SQL의 대부분이 조인과 "SELECT" 문을 기반으로 하기 때문에, 다른 프로젝트에서 잠깐 사용했던 MongoDB와 비슷한 "찾기" 기능이 Couchbase에도 있을 것이라고 순진하게 생각했었죠. 처음에는 그렇지 않다는 사실에 놀랐습니다.

문제

공교롭게도 이 프로젝트에 Couchbase를 사용하기로 결정한 이유는 점진적 맵리듀스 기술을 사용하여 백그라운드에서 복잡한 인덱스를 생성할 수 있는 기능 때문이었습니다. 다음 섹션에서 이에 대해 좀 더 자세히 설명하겠지만, 그 전에 먼저 제가 구축하려고 했던 것이 무엇인지 설명하겠습니다.

저는 Couchbase 위에 미들웨어 계층을 구축하고 있습니다. 이 계층에는 사용자, 자격 증명, 신원, 개인 데이터, 보안 요구 사항 등에 대한 지식이 있습니다. 또한 애플리케이션을 인식하며 개별 사용자의 데이터에 대한 애플리케이션의 액세스를 제어하는 로직과 규칙을 가지고 있습니다.

이 모든 작업을 수행하려면 애플리케이션의 데이터 도메인을 설명하는 '데이터 정의 계층'이 필요합니다. 저는 이 목적으로 XML을 사용하고 있지만, UML 다이어그램이나 시각적 디자인 도구부터 JSON에 이르기까지 모든 것이 허용될 수 있습니다. 중요한 것은 모든 정의에 하나의 명확한 의미가 있고 '중단 문제'와 같은 까다로운 문제가 혼재되어 있지 않아야 한다는 것입니다. 또한 이 표현은 본질적으로 언어 중립적이며 관련될 수 있는 모든 프로그래밍 언어 또는 자산 클래스에 대한 정의를 생성하는 데 사용할 수 있다는 점도 중요합니다. 현재로서는 선호하는 객체 지향 프로그래밍 언어의 클래스 정의 집합과 동일하다고 생각할 수 있습니다.

데이터 모델의 추상적인 특성 때문에 데이터를 기반으로 키를 직접 코딩하는 대신 Couchbase에 저장된 문서의 키로 전역 고유 키(UUID)를 사용하기로 결정했습니다. 이를 통해 키를 정확히 한 번만 생성할 수 있었습니다. 이 키는 개체를 고유하게 식별하며 개체가 아무리 많이 변경되더라도 그 개체의 주요 신원이 됩니다.

제 데이터 모델의 또 다른 측면은 데이터가 '덩어리'가 될 수 있다고 가정한다는 것입니다. 즉, 복합 객체를 고도로 상호 연결된 객체의 그룹으로 정의하는 것이 매우 일반적이며, 이 모든 객체는 덩어리의 보안 측면을 처리하는 하나의 대표자를 통해 기본 객체 그래프로 연결된다는 것입니다.

카우치베이스 스토리지 메커니즘

카우치베이스는 매우 강력하고 다양한 방식으로 사용될 수 있는 '조회수'라는 개념을 정의합니다. 그러나 이 블로그 게시물은 대부분 사용해서는 안 되는 용도로 사용하지 않도록 주의를 환기하는 내용입니다.

Couchbase의 모든 것은 "규모에 맞게" 효과적으로 작동하도록 설계되었습니다. 이는 특정 의미 집합(예: 관계형 이론)을 중심으로 정의된 다음 대규모로 효과적으로 작동할 수 있을 때까지 (분석 데이터베이스의 스타 스키마처럼) 구두쇠로 고정되어 있는 대부분의 다른 시스템과는 다릅니다. 그 결과, Couchbase가 제공하는 보증은 대규모로 제공할 수 있는 최소한의 집합에 불과합니다. 모든 확장 가능한 시스템과 마찬가지로 모든 것이 분리되어 있습니다. 저처럼 대규모로 확장 가능한 시스템을 제공할 계획이라면 이러한 제한에 대해 불평하는 것은 무의미합니다. 조만간 이러한 한계에 직면하게 될 것이며, 그 결과를 어떻게 처리하느냐의 문제일 뿐입니다. 제 조언은 가능한 한 많은 부분을 Couchbase에 맡기고 애플리케이션에 가장 적합한 액세스 메커니즘을 선택하라는 것입니다. 기본적으로 Couchbase에는 두 가지 액세스 메커니즘이 있습니다.

키/값 저장소는 다음과 같습니다. 본질적으로 동기식입니다: 를 사용하면 단일 작업 집합에서 특정 키에 단일 값을 할당하는 것이 성공적이었는지, 고유한지 또는 다른 방식으로 원자적인지 확인할 수 있습니다. 이렇게 하면 키의 고유성 제약 조건이 적용되는 경우에만 수행하는 모든 작업이 성공하도록 할 수 있습니다.

보기를 사용하여 문서를 쿼리하는 방법은 다음과 같습니다. 비동기 그리고 결국 일관성을 유지합니다: 즉, 변경을 요청한 시점으로부터 (아마도 멀리 떨어진) 어느 시점에서 조회수가 업데이트되지만, 궁극적으로 시스템에서 아무것도 하지 않으면 '따라잡히게' 되고 (결국) 그렇게 되면 시스템은 100%로 일관성을 유지하게 됩니다.

이 두 메커니즘은 서로 다른 의미를 제공합니다. 예를 들어 키/값 저장소에서는 키가 고유한 값을 가져야 하지만 뷰는 그렇지 않습니다. 뷰의 매핑 기능은 동일한 값을 가진 키를 원하는 만큼 만들 수 있으며, 뷰는 단순한 키/값 저장소에서는 지원하지 않는 풍부한 다중 부분 키도 지원합니다.

마지막으로, 뷰에 대한 입력이 키/값 저장소에 있는 문서 집합이라는 점에서 이 둘은 상호 작용합니다.

보기 정의

카우치베이스의 뷰는 한 쌍의 맵 및 축소 함수를 사용하여 정의됩니다. 축소 함수는 선택 사항이며 단순히 뷰의 여러 행을 단일 행으로 "축소"할 수 있도록 하기 위해 존재합니다. 이 글에서는 주로 뷰를 사용하여 Couchbase에서 개체의 "인덱스"를 만드는 데 관심이 있으므로 "축소" 방법에 대해서는 더 이상 설명하지 않겠지만, 데이터 분석과 같은 흥미로운 작업을 하고 싶다면 문서에서 살펴보는 것이 좋습니다.

카우치베이스의 맵과 축소 함수는 자바스크립트로 정의되며 필요할 때 논리적으로 "호출"됩니다. 디버깅을 위해 이러한 함수를 래핑하는 간단한 환경을 Chrome 또는 Rhino에서 정의할 수 있으며, 이를 통해 Couchbase 외부에서 "의미론적으로" 어떻게 작동하는지 확인할 수 있습니다. 그러나 이것은 Couchbase 내부에서 뷰 생성을 구현하는 데 직접 사용되는 메커니즘이 아니라 대규모로 최고의 성능을 달성하기 위해 업데이트를 지연시키고 그룹화하여 일괄 처리합니다. 또한 업데이트는 서버 노드별로 세분화되며 어떤 순서로든 발생할 수 있습니다.

이것은 특히 표준 SQL 메커니즘과 비교할 때 인덱스를 정의하는 데 매우 강력한 메커니즘임이 분명합니다. 예를 들어 문서에 배열 객체인 속성이 있는 경우 객체의 길이(또는 실제로는 합)를 계산하여 속성 중 하나에 넣을 수 있습니다.

제가 보기에 이 접근 방식의 한 가지 문제는 함수가 '샌드박스'에서 작동한다는 점, 즉 맵 함수에 제공되는 각 문서가 해당 컨텍스트에서 제외된다는 점입니다. 데이터가 뭉쳐 있는 경우에는 문제가 될 수 있지만, 확장 가능하고 엄격하게 공유되지 않는 세계관에서는 이 접근 방식이 완벽하게 합리적입니다. 문서가 서로 다른 서버에 해시되어 있기 때문에 일괄 JavaScript 처리 중에 문서를 검색하는 것은 상대적으로 비용이 많이 들고 비효율적입니다.

보기 액세스

이러한 JavaScript 함수를 사용하여 뷰를 정의한 후에는 HTTP를 직접 사용하거나 여러 클라이언트 바인딩 중 하나를 통해 키 범위별로 뷰에 쉽게 액세스할 수 있습니다. 제 프로젝트에서는 Java 클라이언트 라이브러리를 사용하여 사용 중인 보기를 연 다음 정의 XML 파일에 제공된 추상적 정의를 기반으로 쿼리를 실행합니다. "최종적인" 일관성 문제 데이터에 인덱스를 정의한 후, 저는 자연스럽게 돌아가서 저장된 데이터를 검색하기 위해 인덱스에 액세스하려고 했습니다. 키/값 저장소의 키로 의미상 관련이 없는 UUID를 사용하기로 했기 때문에, "자연스러운" 키를 사용해 거기에서 무엇이든 찾을 수 있는 즉각적인 기회를 포기하고 인덱스 메커니즘을 사용해 이러한 "자연스러운" 키를 생성하기로 결정했습니다. 하지만 곧 뷰 의미론과 잘 맞지 않는 사용 사례가 있다는 것을 알게 되었습니다.

자격 증명

가장 먼저 직면한 문제는 자격 증명 문제였습니다. Couchbase는 일정 활동량 또는 일정 시간 중 먼저 도달하는 시점 이후에 뷰를 업데이트합니다. 실제 사용 사례에는 이 정도면 충분하겠지만, 저희는 반복 가능한 자동화된 스크립트를 기반으로 테스트했습니다. 제가 작성한 첫 번째 스크립트는 사용자가 시스템에 등록한 후 즉시 돌아서서 로그인하는 시뮬레이션이었습니다. 저는 보기 메커니즘을 사용하여 '고유한' 사용자 자격 증명(로그인 메커니즘 및 로그인 ID)을 추출하고 이를 다시 자격 증명 UUID에 매핑했습니다. 하지만 자격 증명을 만든 후 뷰에서 이 정보를 가져올 때 아직 인덱스에 반영되지 않았습니다. 뷰에서 "오래된" 옵션을 사용해 보았지만 로그인 작업의 경우 비용이 많이 들 수 있습니다(일반적으로 쿼리를 수행하는 데 약 2.5초가 소요됨).

생성된 아티팩트

또 다른 관련 문제는 사용자 요청을 처리하는 동안 생성되는 아티팩트와 관련된 것이었습니다. 이러한 아티팩트는 이전 사용자 상호 작용을 '기억'하여 시스템이 적절하게 응답할 수 있도록 했습니다. 각각의 경우 아티팩트에는 사용자, 수행 중인 작업, 수행 중인 객체를 반영하는 고유한 '자연스러운' 키가 있었습니다. 뷰를 사용하여 이를 추적한 다음 나중에 동일한 사용자가 동일한 객체에 대해 동일한 작업을 수행할 때 다시 로드했습니다.

자동화된 테스트 스크립트가 실행되는 속도에서는 첫 번째 요청에서 생성된 개체가 인덱스에 도달하기도 전에 두 번째 요청을 발행하는 등 최종 일관성과 관련하여 동일한 문제가 발생했습니다.

중첩된 문서

세 번째 사례는 같은 '덩어리'에 있는 문서가 있는 경우였습니다. 사용자별로 '보안'되고 있는 주요 문서를 찾은 후, 같은 '주변'에 있는 다른 문서로 이동하고 싶었습니다. 찾고자 하는 개체의 유형을 설명하는 보기를 정의하고 원래 개체 ID를 포함시켰습니다. 이 인덱스에서 제가 가지고 있는 개체 ID와 찾고자 하는 특성을 검색하면 덩어리에 있는 모든 문서를 복구할 수 있을 거라고 생각했습니다.

다시 말하지만, 테스트에서 문서를 생성한 다음 액세스를 시도하는 속도가 너무 빨라서 죽을 지경이었습니다. 개체를 생성한 다음 돌아서서 뷰에 액세스하려고 시도했지만 여전히 비어 있는 것을 발견했습니다. 몇 초 후 UI를 통해 문제를 진단하려고 시도했을 때 개체는 있지만 인덱스는 아직 새로 고쳐지지 않았습니다.

첫 번째 해결책: 인덱스 복제

Couchbase를 실험하면서 키/값 저장소와 보기 메커니즘의 차이점을 알게 되었습니다. 이 문제를 해결하기 위한 첫 번째 시도는 단순히 Couchbase가 뷰에서 수행하던 모든 작업을 키/값 저장소에서 '실시간'으로 복제하는 것이었습니다. 모든 데이터 정의가 추상적인 형태로 되어 있었기 때문에 뷰 정의를 생성하고 있었고, 이를 확장하여 키/값 저장소에 항목을 저장하는 동일한 코드를 Java로 생성하는 것이 비교적 쉬웠기 때문에 생각만큼 큰 부담은 아니었습니다.

이렇게 해서 문제가 해결되었지만 여러 가지 이유로 마음에 들지 않았습니다. 가장 분명한 이유는 비효율적인 중복으로 인해 Couchbase 선택에 의문이 들었기 때문입니다. 하지만 더 중요한 것은 코드에서 발생하는 다양한 사례의 수가 제가 문제를 혼동하고 있음을 시사했습니다. 가장 중요한 두 가지 분기점은 "고유" 인덱스와 "비고유" 인덱스의 차이와 보안 컨텍스트를 고려해야 하는 인덱스와 그렇지 않은 인덱스의 차이였습니다.

두 번째 해결책: 사물의 개별적인 특성 인식

Couchbase의 친절한 직원들은 제가 겪었던 자격 증명 문제와 매우 유사한 문제를 자세히 설명한 Couchbase 문서에서 "조회" 패턴을 알려주었습니다.

조회 패턴은 키/값 저장소 내에서 인디렉션을 사용하여 기본적으로 여러 개의 키를 가진 단일 객체를 갖는 방법을 설명합니다. 하나의 고유 키(제 경우에는 UUID)가 있고 다른 모든 보조 키가 그 키를 가리킵니다. 동일한 키를 가진 여러 행을 지원할 수 있는 뷰를 원하는 경우와 특정 키를 가진 하나의 행만 원하는 경우를 구분하기 위해 인덱스 정의를 다시 작업할 수 있었습니다. 이를 위해 데이터 개체의 필드에서 고유 키를 구성하는 방법을 지정한 다음, 이를 개체의 UUID를 가리키는 조회 키로 사용했습니다.

이를 통해 위에서 사용한 처음 두 가지 문제를 해결했습니다. 자격 증명의 경우, '자격 증명 메커니즘'(기본, OpenId, OAuth 등)과 해당 메커니즘을 고유 키로 사용하는 사용자의 고유 로그인 ID를 사용할 수 있었고, 아티팩트의 경우 사용자 ID, 작업 및 개체 UUID의 조합을 사용할 수 있었습니다. 각각의 경우, 저는 이것이 보조 키라는 사실과 인덱싱되는 객체 유형을 자동으로 추가했습니다.

세 번째 문제는 성격이 달라서 다른 해결책이 필요했지만, 역시 뷰를 사용하는 것은 잘못된 선택이었습니다. 이 경우 고려해야 할 고유 개체 ID 집합은 이미 메모리에 있는 실제 개체 정의에 포함되어 있었습니다. 올바른 접근 방식은 뷰에서 개체를 조회하는 대신 키/값 저장소에서 UUID를 사용하여 이러한 각 개체를 읽고 어떤 개체가 적절한 특성을 가지고 있는지 확인하는 것이었습니다.

이 접근 방식이 수천 또는 수백만 개의 포함된 개체로 확장될지는 확실하지 않지만, 현재로서는 그럴 필요가 있는지도 확실하지 않습니다. 하지만 이 경우 다른 (하이브리드) 접근 방식이 가능합니다. 예를 들어, 키/값 저장소에 기록될 때 포함된 각 객체를 분석하고 기준에 일치하는 경우 적절한 특성화된 하위 항목 세트를 추가하는 것이 가능할 것입니다. 객체 간의 크기, 수, 관계의 균형을 맞추는 것은 제가 직면하고 있는 완전히 별개의 과제이며 언젠가 다른 블로그 게시물의 내용이 될 수도 있는 문제입니다.

주요 요점

주요 요점은 실제로 달성하고자 하는 것이 무엇인지 이해하고 애플리케이션 요구 사항을 해결하는 데 적합한 Couchbase 액세스 메커니즘과 일치하는지 확인하는 것이 중요하다는 것입니다.

제 경우에는 보기가 멋지고 강력한 기능이라 거부하기 어려워서 과도하게 사용하려고 했습니다. 하지만 사실 뷰는 앱의 모든 데이터 액세스 요구 사항에 적합하지 않습니다.

또 한 가지 명심해야 할 것은 인프라 소프트웨어의 필요성에 공감하고 확장성을 위해서는 공유되지 않는 데이터를 보유하는 것이 중요하다는 점을 인식하는 것입니다. 이를 거부하거나 피하려고만 하면 고통만 초래할 뿐입니다.

그렇다면 데이터에 사용할 Couchbase 액세스 패턴을 어떻게 선택해야 할까요? 이러한 경험을 바탕으로 개발자가 어떤 데이터 액세스 패턴을 선택할지 결정할 때 사용할 수 있는 몇 가지 지침을 적어보았습니다. 전체 목록은 아니지만, 다음은 고려해야 할 4가지 일반적인 패턴입니다:

1. 개체의 키가 있는 경우 이를 사용하여 Couchbase 키/값 저장소에서 직접 개체를 가져옵니다.

2. 고유한 보조 키가 없는데 고유한 보조 키가 있는 개체를 찾고 있다면 보조 인덱스에서 키를 읽어 키를 찾은 다음 해당 키를 사용하여 Couchbase 키/값 스토어에서 개체를 가져옵니다.

3. 이미 개체가 있고 다른 개체에 대한 참조가 포함되어 있는 경우 뷰에 작성된 관계를 기반으로 개체를 찾지 말고 해당 참조를 직접 사용하세요.

4. 마지막으로, 전체 데이터베이스에서 특정 조건과 일치하는 모든 개체를 검색하려는 경우, 그리고 작업의 의미가 내재적인 순서 종속성이 없는 경우 정의한 보기에 액세스하세요. Couchbase의 보기는 결국 일관성이 있다는 점을 기억하세요.

작성자

게시자 Gareth Powell, Ziniki 창립자 겸 아키텍트

Gareth Powell은 Ziniki의 창립자이자 아키텍트입니다. Gareth Powell이 Couchbase에서 맵리듀스 뷰를 사용한 자신의 경험을 설명합니다.

댓글 하나

댓글 남기기