Apache Pulsar

Pulsar의 물리적 아키텍처, Physical Architecture ("Apache Pulsar in Action" 챕터 2.1)

joohyukkim 2023. 7. 2. 00:02
 본 포스트는 “Manning Publications”에서 출판하고 현재 StreamNative 에서 무료 배포 중인 책 “Apache Pulsar in Action”의 내용을 정리한 내용입니다. 본 포스트는 비상업적이며 학습 목적으로 작성되었습니다. 전문적인 내용은 반드시 원서를 참조하시기 바랍니다. 모든 저작권은 해당 출판사와 저자에게 있습니다.

링크: 
1. 원서(출판사) : https://www.manning.com/books/apache-pulsar-in-action
2. 원서(배포본) : https://streamnative.io/ebooks/get-your-free-copy-of-mannings-apache-pulsar-in-action

 

2.1 Pulsar’s physical architecture

다른 메시징 시스템들은 클러스터를 관리 및 배포 관점에서 가장 높은 레벨(또는 레이어) 고려하며, 이는 각 클러스터를 독립된 시스템으로 관리하고 설정하는 것을 필요로 합니다. 다행히도, 펄서는 펄서 인스턴스라는 더 높은 수준의 추상화를 제공하며, 펄서 인스턴스는 하나 이상의 펄서 클러스터로 구성되며, 이 클러스터들은 단일 유닛으로 함께 작동하고 단일 위치에서 관리될 수 있습니다. Figure 2.1에서 보여지는 것과 같습니다.
펄서 인스턴스를 사용하는 가장 큰 이유 중 하나는 지리적 복제를 가능하게 하는 것입니다. 실제로, 같은 인스턴스 내의 클러스터 사이에서만 데이터를 복제하도록 설정될 수 있습니다.

펄서 인스턴스는 configuration store 로 알려진 펄서 인스턴스 전역 ZooKeeper 클러스터를 사용하여 geo-replication 및 테넌트(tenant) 레벨 보안 정책과 같은 여러 개의(multiple) 클러스터에 적용되는 정보를 관리하며 이또한 단 한 곳에서 관리할 수 있습니다. Configuration Store 에 탄력성(resiliency)을 제공하기 위해 인스턴스의 ZooKeeper 클러스터 내의 각 노드는 region 장애 발생 시에도 가용성을 보장할 수 있도록 여러 region 에 배치되어야 합니다.

Geo-replication이 활성화 되어있는 경우에도 구성 저장소로 사용되는 펄서 인스턴스의 ZooKeeper 클러스터는 각 Pulsar 클러스터의 작동에 필수 요소입니다. Geo-replication이 활성화 상태일때 configuration-store 가 다운되는 경우 해당 클러스터로 발행되는 메시지는 로컬 버퍼(buffered)에서 머무르다 해당 ZooKeeper 클러스터가 다시 동작할 때 다른 지역으로 전달됩니다.지역으로 전달됩니다.

2.1.1 Pulsar’s layered architecture (계층 구조)

아래 Figure 2.2 에서 볼 수 있듯이, 각 Pulsar 클러스터는 여러 펄서 메시지 브로커 인스턴스들로 이루어진 stateless한 서빙 레이어, 여러 북키퍼 인스턴스 “bookie”들로 이루어진 stateful한 스토리지 레이어, 그리고 여러 펄서 프록시로 구성된 옵셔널(optional) 라우팅 레이어로 구성됩니다. Kubernetes 환경에서 호스팅되는 경우, 이러한 분리된 아키텍처는 DevOps 팀이 브로커, 북키퍼 그리고 프록시 인스턴스의 수를 동적으로 스케일하며 시기에 따라 요구되는 최대(Peak) 성능과 낮은 성능을 모두 충족시키며 비용 절감을 가능케합니다.


클라이언트가 아직 사용되지 않은 토픽에 접근하면, 해당 토픽의 소유권을 획득하기에 가장 적합한 브로커를 선택하는 프로세스가 트리거됩니다. 브로커가 토픽의 소유권을 획득한 이후에는 해당 토픽에 대한 모든 요청을 처리하는 책임을 지게 됩니다. 그리고 해당 토픽에 데이터를 발행(publish)하거나 소비(consume)하려는 모든 클라이언트는 해당 토픽을 소유한 해당 브로커와 연결해야합니다. 따라서, 특정 토픽에 데이터를 발행하려면 해당 토픽을 소유한 브로커를 알아야 하고 그 브로커에 연결해야 합니다. 하지만 브로커 할당(assignment) 정보는 오직 ZooKeeper 메타데이터에 존재하며, 로드밸런싱,브로커 장애 등으로 인해 변경될 가능성이 있습니다. 따라서, 브로커와 직접 연결한다고 해서 의도한 브로커와 통신하게된다는 가정은 할 수 없습니다. 이것이 바로 Pulsar 프록시가 만들어진 이유입니다. Pulsar 프록시는 클러스터 내의 모든 브로커들을 위한 중간 매개체 역할을 수행하기 위해 만들어졌습니다.

THE PULSAR PROXY

Pulsar 클러스터를 Kubernetes와 같은 사설 및 가상 네트워크 환경에서 호스팅하고 있으며, Pulsar 브로커들에게 인바운드 연결을 제공하려는 경우 사설 IP 주소를 공용 IP 주소로 변환해야 합니다. 이를 위해 전통적인 부하 분산 기술과 기법인 물리적 로드 밸런서, 가상 IP 주소 또는 DNS 기반의 로드 밸런싱 등을 사용할 수 있지만, 이는 클라이언트의 장애 조치 및 장애 복구 기능을 제공하기에는 최적의 방법이 아닐 수 있습니다.

전통적인 로드 밸런서 접근 방식은 특정 토픽에 할당된 브로커를 알지 못하며 대신 클러스터 내에서 요청을 임의의 브로커로 전달하기 때문에 효율적이지 않습니다. 만약 브로커가 자신이 서비스하지 않는 토픽에 대한 요청을 받으면 해당 요청을 적절한 브로커로 자동으로 재전달하게 됩니다. 그러나 이는 시간 측면에서 상당한 손실을 야기하게 됩니다. 이러한 이유로 펄서 브로커를 대상으로 지능적인 로드 밸런서 Pulsar 역할을 수행하는 Pulsar 프록시를 사용하는 것을 권장합니다.

Pulsar 프록시를 사용할 경우, 모든 클라이언트 연결은 브로커로 직접 연결하는 대신 먼저 프록시를 통과하게 됩니다. 프록시는 Pulsar의 내장된 서비스 디스커버리 메커니즘을 사용하여 접근 대상 토픽을 호스팅하는 브로커를 확인하고 클라이언트의 요청을 자동으로 해당 브로커로 라우팅합니다. 프록시는 라우팅한 정보를 메모리에 캐시하여 이후의 조회(lookup) 프로세스를 더욱 효율적으로 진행합니다. 성능과 장애 대응(Failover) 목적으로 전통적인 로드 밸런서 뒤에 1개 보다 여러 개의 Pulsar 프록시를 실행하는 것이 권장됩니다. 브로커와 달리 Pulsar 프록시는 모든 요청을 처리할 수 있으므로 문제없이 로드 밸런싱될 수 있습니다.

2.1.2 Stateless serving layer

Pulsar의 다중 계층 구조는 데이터(메시지)가 브로커와 별도로 저장되며 어떤 브로커든 언제든지 모든 토픽의 데이터를 제공할 수 있도록 보장합니다. 이러한 동작은 브로커와 해당 토픽 데이터를 함께 할당하고 관리하는 다른 메시징 시스템과 달리 펄서 클러스터가 언제든지 토픽의 소유권을 클러스터 내의 어떤 브로커에게든 자유롭게 할당할 수 있도록 합니다. 이처럼 펄서 브로커 자체에는 클라이언트 요청을 처리하는 데 필요한 정보가 저장되어 있지 않기 때문에 서빙 레이어를 "무상태(stateless)"라는 표현을 사용합니다.

브로커의 무상태성은 요구에 따라 동적으로 스케일 업하거나 다운할 수 있을 뿐만 아니라 여러 브로커의 장애에 대해 클러스터 내구성을 제공합니다. 마지막으로, Pulsar는 내부적으로 보유한 로드 쉐딩 메커니즘을 사용하여 메시지 트래픽의 불규칙한 변화에 따라 모든 활성 브로커 간에 부하를 재분배합니다.

Bundles

토픽을 특정 브로커에 할당하는 작업은 번들(bundle) 레벨로 이루어집니다. Figure 2.3와 같이, Pulsar 클러스터의 모든 토픽은 각각의 번들에 할당되고, 각 번들은 다른 브로커에 할당됩니다. 이를 통해 네임스페이스 내의 모든 토픽이 모든 브로커에 고르게 분산되도록 보장할 수 있습니다.

생성되는 번들의 개수는 브로커 구성 파일 내의 defaultNumberOfNamespaceBundles 속성으로 제어되며, 기본값은 4입니다. Pulsar 관리자 API를 사용하여 네임스페이스를 생성할 때 해당 네임스페이스 수준에서 다른 값을 전달하여 이 설정을 덮어쓸 수 있습니다. 일반적으로 번들의 개수는 브로커 개수의 배수로 설정하여 균일한 분포를 유지하는 것이 좋습니다. 예를 들어, 세 개의 브로커와 네 개의 번들이 있다면, 한 브로커에 두 개의 번들이 할당되고 나머지 브로커들은 각각 한 개씩 할당됩니다.

LOAD BALANCING

메시지 트래픽은 처음에는 가능한 한 균등하게 호라성 브로커에 분산되지만, 시간이 지남에 따라 여러 가지 요인들로 인해 부하가 불균형해질 수 있습니다. 메시지 트래픽 패턴의 변화로 인해 한 브로커가 많은 트래픽을 처리하는 여러 토픽을 담당하고, 다른 브로커는 전혀 활용되지 않을 수 있습니다. 브로커 구성 파일의 다음 속성들에 의해 정의한 임계값을 초과하는 경우, 기존 번들은 두 개의 새로운 번들로 분할되며 그 중 하나가 새로운 브로커에 적재됩니다.

  • loadBalancerNamespaceBundleMaxTopics
  • loadBalancerNamespaceBundleMaxSessions
  • loadBalancerNamespaceBundleMaxMsgRate
  • loadBalancerNamespaceBundleMaxBandwidthMbytes

이러한 메커니즘을 통해 일부 번들이 다른 번들에 비해 더 많은 부하를 겪고 있는 상황을 인식하고, 이러한 과부하가 발생한 번들을 두 개로 분할하고, 그 중 하나를 클러스터 내 다른 브로커에게 적재함으로써 문제를 해결합니다.

LOAD SHEDDING

추가로, Pulsar 브로커에는 특정 브로커가 과부하 상태인지 감지하고 자동으로 부하를 분산하거나 일부 번들을 다른 클러스터 내 브로커에게 해제하거나 양도(offload)하는 메커니즘이 있습니다. 특정 브로커의 리소스 사용량이 설정 파일의 loadBalancerBrokerOverloadedThresholdPercentage 속성에 정의된 임계치를 초과하는 경우, 브로커는 하나 이상의 번들을 새로운 브로커에게 양도(offload)합니다. 이 속성은 브로커가 사용할 수 있는 총 CPU, 네트워크 용량 또는 메모리의 백분율 최대치를 설정합니다. 이러한 자원 중 어느 하나라도 이 임계치를 초과하면 양도(offload) 작업이 트리거됩니다.

선택된 번들은 그대로 유지되면서 다른 브로커에 할당됩니다. 이는 로드 쉐딩 (load shedding) 프로세스가 로드 밸런싱 프로세스와 다른 문제를 해결하기 때문입니다. 로드 밸런싱에서는, 하나의 bundle이 다른 bundle보다 훨씬 더 많은 트래픽을 처리하기 때문에 bundle 간 토픽의 분포를 조정하며 이 부하를 모든 번들에 고르게 분산시키려고 시도합니다.

반면에 로드 쉐딩은 브로커들 사이에서 서비스에 필요한 자원의 수에 기반하여 번들을 재분배하는 역할을 합니다. 각 브로커에 동일한 수의 번들이 할당될 수 있지만, 번들 간의 부하가 균형되지 않으면 각 브로커가 처리하는 메시지 트래픽은 크게 다를 수 있습니다.

이 점을 설명하기 위해, 브로커가 3개이고 총 60개의 번들이 있으며 각 브로커가 20개의 번들을 처리하는 시나리오를 고려해 보겠습니다. 또한, 이 중 20개의 번들이 현재 전체 메시지 트래픽의 90%를 처리하고 있습니다. 이제 만약 이러한 번들 대부분이 동일한 브로커에 할당된다면, 해당 브로커의 CPU, 네트워크 및 메모리 자원이 소진될 수 있습니다. 따라서 이러한 번들 중 일부를 다른 브로커로 양도(offload)하면 문제를 완화시킬 수 있습니다. 번들 자체를 분할하는 경우에는 메시지 트래픽의 약 절반만 분산되며, 기존 브로커는 여전히 45%의 메시지 트래픽을 처리하게 됩니다.

DATA ACCESS PATTERNS***

일반적으로 스트리밍 시스템에서는 세 가지 I/O 패턴이 있습니다. 쓰기(write): 새로운 데이터가 시스템에 기록되는 경우; 테일링 읽기(tailing reads): 소비자가 발행된 메시지가 즉시 읽히는 가장 최근의 메시지를 읽는 경우; 캐치업 읽기(catch-up-reads): 소비자가 토픽의 시작부터 많은 수의 메시지를 읽어야 할 때, 예를 들어 새로운 소비자가 최신 메시지보다 훨씬 이전의 지점에서 데이터에 접근해야 하는 경우; 이렇게 세가지가 있습니다.

일반적으로 스트리밍 시스템에서는 세 가지 I/O 패턴이 있습니다. 쓰기(write): 새로운 데이터가 시스템에 기록되는 경우; 테일링 읽기(tailing reads): 소비자가 메시지가 발행된 직후에 가장 최신 메시지를 읽는 경우; 캐치업 읽기(catch-up reads): 소비자가 토픽의 시작부터 많은 수의 메시지를 읽는, 예를 들어 새로운 소비자가 최신 메시지보다 훨씬 이전의 지점에서 데이터에 접근해야 하는 경우; 이렇게 세 가지 패턴이 있습니다.

Untitled

저장 계층(storage-layer)에서 데이터에 액세스하는 캐치업 일기 (catch-up reads)를 살펴보면 더 흥미롭습니다. 클라이언트가 Pulsar에서 메시지를 소비할 때, 해당 메시지는 Figure 2.4 에 나와 있는 단계들을 거치게됩니다. 캐치업 읽기의 가장 일반적인 예는 소비자가 오랜 시간동안 오프라인 상태였다가 다시 소비(Consumption)를 시작하는 경우이며 참고로 토픽을 새로운 브로커에 재할당하는 토픽 재할당(topic-reassignment)가 같은 경우처럼 컨슈머가 브로커의 인메모리 캐시에서 직접 서비스를 받지 않는다면, 시나리오 또한 catch-up read로 간주합니다.

2.1.3 Stream storage layer

Pulsar는 모든 메시지 소비자에게 메시지 전달을 보장합니다. 메시지가 Pulsar 브로커에 성공적으로 도달하면, 해당 메시지가 의도한 대상으로 전달될 것을 확신할 수 있습니다. 이러한 보장을 제공하기 위해 모든 ack되지 않은(non-acknowedged) 메시지는 소비자(consumer)에게 전달되고 ack될 때까지 영속화되어야 합니다. 앞서 언급한 대로, Pulsar는 영속적인 메시지 저장을 위해 Apache BookKeeper를 분산 선행 로그 (WAL) 시스템으로 활용합니다. BookKeeper 를 통해 제공하는 것이 시퀀스로 구성된 원장(ledger)에서 로그 항목 스트림의 영속적인 저장 기능입니다.

LOGICAL STORAGE ARCHITECTURE

Pulsar의 토픽은 메시지가 수신된 순서대로 순차적으로 저장되는 무한한 메시지 스트림으로 생각할 수 있습니다. 새로운 메시지는 스트림의 끝에 추가되며, 컨슈머는 이전에 설명한 데이터 액세스 패턴에 따라 스트림 상단부터 메시지를 읽습니다. 이런 간소화된 관점을 통해 컨슈머가 토픽 내에서의 위치를 이해하기 쉽지만, 실제로는 저장 장치의 공간 제약 때문에 이러한 추상화는 현실에서 존재하기 어려우며 결국 이 추상적인 무한한 스트림 개념은 공간 제약이 있는 물리적인 시스템에서 구현되어야 합니다.

Apache Pulsar는 스트림 저장소를 구현하는 데 있어서 Kafka와 같은 전통적인 메시지 시스템과 근본적으로 다른 접근 방식을 취합니다. Kafka에서는 각 스트림을 여러 개의 레플리카로 분리하고, 각 레플리카는 브로커의 로컬 디스크에만 저장됩니다. 이 접근 방식의 장점은 모든 쓰기 작업이 순차적으로 이루어져 데이터 접근에 필요한 디스크 헤드 이동량을 줄일 수 있다는 것입니다. 그러나 Kafka의 이러한 접근 방식의 단점은 단일 브로커가 파티션 데이터를 저장하기 위해 충분한 저장 용량을 가지고 있어야 한다는 것이며, 이에 대해서는 1장에서 논의하였습니다.

그렇다면 Apache Pulsar의 접근 방식은 어떻게 다를까요? 먼저, 각 토픽은 파티션의 모음이 아닌 연속적인 세그먼트의 모음으로 모델링됩니다. 각 세그먼트는 구성 가능한 수의 메시지를 포함할 수 있으며, 기본값은 50,000입니다. 세그먼트가 가득 차면 새로운 세그먼트가 생성되어 새로운 메시지를 보관합니다. 따라서 Pulsar 토픽은 각 세그먼트가 메시지의 일부를 보관하는 무한한 세그먼트 목록으로 생각할 수 있습니다. Figure 2.5는 Pulsar 스트림 저장소 계층의 논리적 아키텍처와 해당 계층이 어떻게 기반 물리적 구현에 매핑되는지를 보여줍니다.


Pulsar 토픽은 Pulsar 내에서 특정 토픽을 고유하게 식별하는 주소 지정 가능한 엔드포인트입니다. 이는 클라이언트가 연결하려는 리소스를 고유하게 식별하기 위해 사용되며, URL과 유사한 개념입니다. 토픽 이름은 Pulsar 브로커에 의해 데이터의 디코딩되어 저장 위치를 결정하기 위해 사용됩니다.

Pulsar는 BookKeeper의 원장(ledger) 위에 관리 원장(managed ledger)이라는 추가적인 추상화 계층을 추가합니다. 이 관리 원장은 토픽에 발행된 데이터를 보관하는 원장의 ID를 유지합니다. Figure 2.5에서 볼 수 있듯이, 데이터가 처음으로 토픽 A에 발행되었을 때, ledger-20에 기록되었습니다. 토픽에 50,000개의 레코드가 발행된 후, 해당 원장은 닫히고 그 자리를 대신할 다른 원장(ledger-245)이 생성됩니다. 이 과정은 50,000개의 레코드마다 반복되어 들어오는 데이터를 저장하며, 관리 원장은 이러한 원장 ID의 고유한 시퀀스를 ZooKeeper 내부에 유지합니다.

이후 컨슈머가 토픽 A에서 데이터를 읽으려고 하는경우 , 관리 원장(managed ledger)은 BookKeeper 내에서 데이터의 위치를 찾아 컨슈머에게 반환하는 데 사용됩니다. 컨슈머가 가장 오래된 메시지부터 캐치업 읽기를 수행하는 경우, 먼저 ledger-20의 모든 데이터를 가져오고, 그 다음에 ledger-245를 가져오는 식으로 진행됩니다. 이러한 원장들의 순회(traversal)는 가장 오래된 메시지부터 가장 최근의 메시지까지 사용자 입장에서는 단순하게(transparently) 이루어지면서 마치 단일, 순차적인 데이터 스트림(single sequential stream of data)을 다루는 것과 같은 효과(illusion)를 만들어냅니다. 이를 가능케 할 뿐만 아니라 BookKeeper 원장들의 순서를 유지하면서 메시지가 발행된 순서대로 읽히도록 하는 것이 관리 원장입니다.

BOOKKEEPER PHYSICAL ARCHITECTURE

BookKeeper에서 각 원장을 구성하는 것(원장의 단위)을 엔트리(entry)라고합니다. 이러한 엔트리에는 수신 메시지의 실제 원시 바이트와 함께 엔트리를 추적하고 액세스하는 데 사용되는 중요한 메타데이터가 포함되어 있습니다. 가장 중요한 메타데이터는 해당 엔트리가 속한 원장의 ID로, 이는 로컬 ZooKeeper 인스턴스에 저장되어 컨슈머가 나중에 메시지를 읽을 때 빠르게 BookKeeper에서 메시지를 검색할 수 있습니다. 로그 엔트리의 스트림(stream of log entries)은 그림 2.6에 나와 있는 원장이라고 하는 추가 전용 데이터 구조에 저장됩니다.

원장은 append-only 특성을 가지며, 엔트리들은 원장에 순차적으로 작성되고 작성된 후에는 수정할 수 없습니다. 실제적인 관점에서는 다음과 같은 의미를 갖습니다.

  • Pulsar 브로커는 먼저 원장을 생성한 후, 엔트리들을 원장에 추가하고 최종적으로 원장을 닫습니다. 다른 상호작용은 허용되지 않습니다.
  • 원장이 정상적으로 닫힌 경우 또는 프로세스 충돌로 인해 닫힌 경우, 해당 원장은 읽기 전용 모드로만 열 수 있습니다.
  • 마지막으로, 원장의 엔트리들이 더 이상 필요하지 않은 경우 전체 원장을 시스템에서 삭제할 수 있습니다.

북키퍼(BookKeeper) 서버들은 원장(구체적으로는 원장의 일부인 조각들)을 저장하는데 책임을 지는 개별 서버들로 "북키(bookie)"로 알려져 있습니다. 원장에 엔트리가 작성될 때, 해당 엔트리들은 앙상블(ensemble)이라고 불리는 북키 노드의 특정 그룹에 작성됩니다. 앙상블의 크기는 Pulsar 토픽에서 지정한 복제 팩터(R, replication factor)와 동일하며, 데이터 손실을 방지하기 위해 엔트리의 정확히 R개의 사본이 디스크에 저장됩니다.

북키(bookie)들은 로그 구조화 방식(log-structured way)으로 데이터를 관리하며, 이를 위해 다음 세 가지 종류의 파일을 사용합니다: 저널(journal), 엔트리 로그(entry log), 그리고 인덱스 파일(index file). 저널 파일은 모든 BookKeeper 트랜잭션 로그를 보관합니다. 원장에 대한 업데이트가 수행되기 전에, 북키는 해당 업데이트를 기술하는 트랜잭션이 디스크에 기록되도록 보장하여 데이터 손실을 방지합니다.

엔트리 로그 파일은 BookKeeper에 기록된 실제 데이터를 포함합니다. 각 원장에서의 엔트리들은 집계되어 순차적으로 기록되며, 그들의 오프셋은 빠른 조회를 위해 원장 캐시에 포인터로 유지됩니다. 각 원장마다 인덱스 파일이 생성되며, 이 파일은 엔트리 로그 파일에 저장된 데이터의 오프셋을 기록하는 여러 인덱스를 포함합니다. 이 인덱스 파일은 전통적인 관계형 데이터베이스의 인덱스 파일을 모델링한 것으로, 원장을 사용하는 클라이언트에게 빠른 조회 기능을 제공합니다. 클라이언트가 Pulsar에 메시지를 게시하면, 해당 메시지는 그림 2.7에 나와 있는 단계를 거쳐 BookKeeper 원장에 디스크에 영속화됩니다.

북키(bookie)들은 분산된 디스크 장치들의 여러 파일에 엔트리 데이터를 분산하여, 읽기 작업이 진행 중인 쓰기 작업의 지연 시간과 독립적으로 처리함으로써 동시에 수천 개의 읽기 및 쓰기 작업을 다룰 수 있게 합니다.

2.1.4 Metadata storage

마지막으로, 각 클러스터에는 Pulsar에서 테넌트, 네임스페이스 및 토픽과 관련된 클러스터별 구성 정보를 저장하기 위해 사용하는 별도의 로컬 ZooKeeper 앙상블이 있습니다. 이 앙상블은 보안 및 데이터 보존 정책을 포함하여 클러스터에 특화된 설정 정보를 저장하는 데 사용됩니다. 이는 이전에 언급한 관리 원장 정보와 별개로 관리됩니다.

ZOOKEEPER BASICS

공식 Apache 웹사이트에 따르면, "ZooKeeper는 설정 정보, 네이밍, 분산 동기화 및 그룹 서비스 제공을 위한 중앙 집중식 서비스"라고 합니다(https://zookeeper.apache.org). 간단히 말해 “분산 데이터 소스”라고 할 수 있습니다. ZooKeeper는 분산 시스템인 Pulsar나 BookKeeper와 같은 시스템에서 중요한 역할을 하는 분산된 정보 저장소를 제공합니다.

Apache ZooKeeper는 거의 모든 분산 시스템이 해결해야 하는 핵심적인 문제인 합의(consensus, 또는 agreement)를 달성하는 것을 해결합니다. 분산 시스템의 프로세스들은 현재 구성 값 및 토픽 소유권과 같은 여러 정보에 대해 합의가 이루어져야 합니다. 이는 분산 시스템에 특히 문제가 되는데, 여러 개의 동일한 시스템 컴포넌트가 동시(concurrently)에 실행되고 이들 사이에 정보를 조율할 수 있는 실질적인 방법이 없기 때문입니다. 전통적인 데이터베이스는 선택지가 아닙니다. 왜냐하면 모든 호출 서비스가 테이블의 동일한 락을 기다리는 동안 블록되어 분산 컴퓨팅의 모든 이점을 사실상 상쇄하기 때문입니다.

합의 기반의 구현을 활용할 수 있다면 활용있다면, 분산 시스템들은 CAS(compare-and-swap) 연산을 통해 분산 락을 구현하고 프로세스의 동작들을 더 효과적으로 조율(coordinate)할 수 있습니다. CAS 연산은 ZooKeeper에서 검색한 데이터를 예상했던 데이터과 비교하고, 오직 그 값이 동일한 경우에만 해당 데이터을 업데이트합니다. 이를 통해 시스템이 최신 정보를 기반으로 작동하는 것을 보장할 수 있습니다. 예를 들어, BookKeeper 원장(ledger)이 열려(open)있는 상태에 있는지 확인한 이후 데이터를 기록하는 경우가 있을 수 있습니다. 다른 프로세스가 해당 원장을 닫은 경우, 이는 ZooKeeper 데이터에 반영되어 프로세스는 쓰기 작업을 진행하지 않아야 함을 알 수 있습니다. 반대로, 프로세스가 원장을 닫는 경우, 이 정보는 ZooKeeper로 전송되어 다른 서비스에 전파되어 쓰기를 시도하기 전에 해당 서비스가 원장이 닫혀 있음을 알 수 있게 합니다.

ZooKeeper 서비스 자체는 클라이언트가 정보를 저장하기 위해 간단한 데이터 파일(znode)을 조작할 수 있도록 파일 시스템과 유사한 API를 제공합니다. 각각의 znode들은 파일 시스템과 유사한 계층 구조를 형성합니다. 다음 섹션에서는 ZooKeeper 내에 유지되는 메타데이터와 해당 메타데이터가 누구에 의해 어떻게 사용되는지 살펴보겠습니다. 이를 확인하기 위해 Pulsar와 함께 배포되는 zookeeper-shell 도구를 사용하여 모든 znode를 나열하는 것이 가장 좋은 방법입니다. 다음 목록에 나와 있는 대로 수행할 수 있습니다.

리스팅 2.1을 통해 알 수 있듯이, ZooKeeper 내에는 Apache Pulsar와 BookKeeper를 위해 총 11개의 다른 znode가 생성됩니다. 이러한 정보들은 내용과 사용 방식에 따라 네 가지 범주 중 하나에 속합니다.

CONFIGURATION DATA

첫 번째 카테고리는 테넌트, 네임스페이스, 스키마 등에 대한 설정 데이터입니다. 이 모든 정보는 낮은 빈도로 변경되는 정보로, 사용자가 새로운 클러스터, 테넌트, 네임스페이스, 스키마를 생성하거나 업데이트할 때 Pulsar 관리 API를 통해만 업데이트되며 이 정보에는 보안 정책, 메시지 보존 정책, 복제 정책, 스키마 등이 포함됩니다. 이 정보는 다음 znode 에 저장됩니다. /admin/schemas.

METADATA STORAGE

모든 토픽의 관리 원장(managed-ledger) 정보는 /managed-ledgers znode에 저장되며, /ledgers znode는 BookKeeper가 클러스터 내의 모든 북키(bookie)에 저장된 원장들을 추적하는 데 사용됩니다.

리스팅 2.2에서 확인할 수 있듯이, pulsar-managed-ledger-admin라는 또 다른 도구를 사용하여 Pulsar가 BookKeeper로부터 데이터를 읽고 쓰는 데 사용하는 관리 원장 정보에 쉽게 접근할 수 있습니다

DYNAMIC COORDINATION BETWEEN SERVICES

나머지 znode들은 시스템 간 분산 조정(distributed coordination)을 위해 모두 사용됩니다. 이 중 /bookies는 BookKeeper 클러스터에 등록된 북키 목록을 유지하고, /namespace는 프록시 서비스에서 특정 토픽을 소유한 브로커를 결정하는 데 사용됩니다. 다음 리스팅에서 볼 수 있듯이, /namespace znode 계층 구조는 각 네임스페이스의 번들 ID를 저장하는 데 사용됩니다.

앞서 논의한 대로, 프록시는 토픽 이름을 해싱하여 번들 이름을 결정합니다. 이 경우에는 0x40000000_0x80000000입니다. 그런 다음 프록시는 /namespace/{테넌트}/{네임스페이스}/{번들-ID} znode를 조회하여 토픽을 "소유"하는 브로커의 URL을 가져옵니다.

이렇게 함으로써, Pulsar 클러스터 내에서 ZooKeeper의 역할과 클러스터에 동적으로 추가된 노드가 쉽게 액세스하여 클러스터 구성 정보를 신속하게 확인하고 클라이언트 요청을 처리할 수 있는 서비스를 제공하는 방법에 대해 더 깊은 이해를 얻을 수 있었기를 바랍니다. 새로 추가된 브로커가 /managed-ledgers znode의 데이터를 참조하여 토픽에서 데이터를 서비스하는 능력이 이에 대한 한 가지 예입니다.