2 minute read

ZooKeeper나 etcd와 같은 프로젝트는 “분산 키-값 저장소” 또는 “코디네이션 및 구성 서비스”로 설명된다. 이러한 서비스의 API는 주어진 키의 값을 읽고 쓸 수 있고, 키를 반복할 수 있는 기능을 갖춘 것이 마치 데이터베이스와 매우 유사하다. 그런데 이것이 데이터베이스라면 왜 합의 알고리즘을 구현해야 할까? 다른 종류의 데이터베이스와 차이점은 무엇일까?

ZooKeeper와 같은 코디네이터 서비스가 어떻게 사용되는지 간략하게 살펴보자. 애플리케이션 개발자는 ZooKeeper가 범용 데이터베이스로는 적합하지 않기 때문에 직접 사용할 일이 거의 없다. 다만, 다른 프로젝트를 통해 간접적으로 사용하게 될 가능성이 더 높다. 예를 들어 HBase, Hadoop YARN, OpenStack Nova, Kafka는 모두 백그라운드에서 실행되는 ZooKeeper에 의존하고 있다. 이 프로젝트들이 ZooKeeper를 사용하는 이유는 무엇일까?

ZooKeeper와 etcd는 메모리에 모두 들어갈 수 있는 소량의 데이터를 저장하도록 설계되었기 때문에(내구성을 위해 여전히 디스크에 쓰기는 한다) 애플리케이션의 모든 데이터를 여기에 저장하고 싶지는 않을 것이다. 이 소량의 데이터는 내결함성 전체 순서 브로드캐스트 알고리즘을 사용하여 모든 노드에 복제된다.

전체 순서 브로드캐스트는 데이터베이스 복제에 필요하다. 각 메시지가 데이터베이스에 대한 쓰기를 나타내는 경우, 동일한 순서로 동일한 쓰기를 적용하면 복제본이 서로 일관성을 유지할 수 있다.

ZooKeeper는 Google의 Chubby lock 서비스를 모델로 하여 전체 순서 브로드캐스트(합의)뿐만 아니라 분산 시스템을 구축할 때 유용한 기능도 구현한다:

선형화 가능한 원자 연산

원자적 비교-설정(compare-and-set) 연산을 사용하면 여러 노드가 동시에 동일한 연산을 수행하려고 시도하면 그중 하나만 성공하는 잠금을 구현할 수 있다. 합의 프로토콜은 노드가 실패하거나 네트워크가 어느 시점에서 중단되더라도 연산이 원자적이고 선형화될 수 있도록 보장한다. 탈중앙화된 잠금은 일반적으로 만료 시간이 있는 임대(lease)로 구현되며, 클라이언트가 실패할 경우 결국 해제된다.

전체 작업 순서 지정

일부 리소스가 잠금 또는 임대로 보호되는 경우 프로세스 일시 중지 시 클라이언트가 서로 충돌하는 것을 방지하기 위해 펜싱 토큰이 필요하다. 펜싱 토큰은 잠금을 획득할 때마다 단조롭게 증가하는 숫자다. ZooKeeper는 모든 작업을 완전히 정렬하고 각 작업에 단조롭게 증가하는 트랜잭션 ID(zxid)와 버전 번호(cversion)를 부여함으로써 이를 제공한다.

장애 감지

클라이언트는 ZooKeeper 서버에서 수명이 긴 세션을 유지하며, 클라이언트와 서버는 주기적으로 하트비트를 교환하여 상대 노드가 여전히 살아 있는지 확인한다. 연결이 일시적으로 중단되거나 ZooKeeper 노드에 장애가 발생하더라도 세션은 활성 상태로 유지된다. 그러나 세션 타임아웃보다 더 긴 시간 동안 하트비트가 중단되면 ZooKeeper는 세션이 죽은 것으로 선언한다. 세션이 보유한 모든 잠금은 세션 시간 초과 시 자동으로 해제되도록 구성할 수 있다.

변경 알림

한 클라이언트는 다른 클라이언트가 생성한 잠금과 값을 읽을 수 있을 뿐만 아니라, 변경 사항이 있는지 감시할 수도 있다. 따라서 클라이언트는 다른 클라이언트가 언제 클러스터에 합류하는지(ZooKeeper에 기록한 값을 기반으로) 또는 다른 클라이언트가 실패하는지(세션 시간이 초과되어 임시 노드가 사라지는지) 알 수 있다. 알림을 구독하면 클라이언트는 변경 사항을 확인하기 위해 자주 폴링할 필요가 없다.

이러한 기능 중 선형화 가능한 원자 연산만 실제로 합의가 필요하다. 하지만 이러한 기능들의 조합이 ZooKeeper와 같은 시스템을 분산 조정에 매우 유용하게 만든다.