데이터 엔지니어링 분야를 이것저것 공부하다보면 ( Hadoop, spark, kafka ...) 계속 나오는 Zookeeper 라는 것이 있다.
뭔진 모르는데 계속 나오니 그런가보다 하는게 있었는데 이번 기회에 정리해보자
Zookeeper는 아파치재단에서 개발한 오픈소스 분산 코디네이션 서비스
분산 시스템 환경에서 설정 정보 유지, 이름 지정, 분산 동기화, 그룹 서비스 등을 제공하여 분산 애플리케이션의 복잡한 관리를 쉽게 만들어주는 관제탑 같은 도구.
카프카는 메세지를 주고 받는 Pub/Sub 외에는 아무것도 하지않는 분산 처리 플랫폼이다.
따라서 브로커 상태를 저장하지 않기 때문에 상태 관리를 위해서만 주키퍼를 사용한다.
분산 환경의 여러 서버 간 협력을 위한 '사육사' 역할로, 계층적 데이터 저장소(znode)를 통해 데이터를 관리, 데이터의 일관성과 고가용성을 보장, Kafka나 Hadoop과 같은 빅데이터 생태계에서 핵심적인 역할을 수행한다.
분산 시스템에서의 고민거리
- 분산된 시스템 같의 정보를 어떻게 공유할 것인가
- 클러스터에 있는 서버들의 상태를 어떻게 체크할 것인가
- 분산된 서버들 간에 동기화를 위해 잠금(Lock)을 어떻게 처리할 것인가
분산 시스템에서 시스템 간의 정보 공유, 상태 체크, 서버들 간의 동기화를 위한 락 등을 처리해주는 서비스
이외에도 2PC 라고 하는 분산 트랜잭션 프로토콜에도 트랜잭션 코디네이터가 존재하는데,
그건 트랜잭션 코디네이터고 Zookeeper 는 일반적인 분산 조율/리더선출/락 코디네이터다.
ZooKeeper = 분산 시스템 조율(메타데이터/락/리더선출)
2PC = 분산 트랜잭션 합의(커밋을 원자적으로 맞추기)

주키퍼도 분산 시스템의 일부분이기 때문에 동작을 멈춘다면 분산 시스템이 멈출수도 있다. 그래서 안정성을 확보하기 위해 클러스터로 구축한다.
클러스터는 '홀수' 로 구축한다. 어떤 서버에 문제가 생겼을 경우 과반수 이상의 데이터를 기준으로 일관성을 맞추기 때문이다.
살아있는 노드가 과반수 이상이라면 지속적인 서비스를 제공한다. (위의 그림에서 Server는 주키퍼, Client는 카프카가 된다.) 서버 여러 대를 앙상블(클러스터)로 구성하고, 분산 어플리케이션들이 각각 클라이언트가 되어 주키퍼 서버들과 커넥션을 맺은 후 상태 정보 등을 주고 받는다.
상태 정보들은 주키퍼의 지노드(znode)라고 불리는 곳에 Key-Value 형태로 저장하며, 지노드에 저장된 것을 이용하여 분산 어플리케이션들은 서로 데이터를 주고받게 된다. (znode를 일반 컴퓨터의 파일이나 폴더 개념으로 생각하면 쉬움)
지노드(znode)는 우리가 알고 있는 일반적인 디렉토리와 비슷한 형태로서 자식노드를 가지고 있는 계층형 구조로 구성되어 있다. znode에는 크게 데이터(Data), 자식 노드에 대한 참조(Children), 그리고 상태 정보를 나타내는 메타데이터(Stat)가 담긴다.

Persistent Node : 영구 저장소
Ephermeral Node : Client가 종료되면 사라진다.
Sequence Node: 생성 시 뒤에 숫자가 붙는다.
메타데이터 Stat

상황: Kafka나 HBase 같은 시스템에서 새로운 브로커나 리전 서버가 추가됨.
znode 내용: 각 서버는 자신의 IP와 포트 번호를 /servers/server-1 같은 znode에 저장함.
결과: 클라이언트는 이 znode를 조회하여 "아, 지금 1번 서버가 살아있으니 저기로 데이터를 보내야겠구나"라고 결정합니다.
상황: 데이터 처리 방식(예: 배치 크기, 타임아웃 값)을 변경해야 함.
znode 내용: /config/app-settings 노드에 새로운 설정 데이터(JSON/XML 등)를 업데이트함.
결과: 해당 노드를 감시(Watch)하던 모든 서버가 변경을 감지하고, "이제부터는 데이터를 A 방식이 아닌 B 방식으로 처리하자"라고 동작 방향을 바꿉니다.
상황: 기존 마스터 서버가 다운됨.
znode 내용: /master라는 임시 노드(Ephemeral)를 먼저 생성하는 서버가 승리.
결과: 노드 생성에 성공한 서버는 리더가 되어 "내가 대장이니 나에게 데이터를 보내라"고 선언하고, 나머지 서버들은 팔로워로서 대기합니다.
ZooKeeper는 요청을 RequestProcessor 체인으로 흘려보내는데, 이 체인은 쓰기 요청에 대해 트랜잭션을 준비/로그/적용하고, 읽기 요청도 처리(검증·응답). 특히 FinalRequestProcessor는 트랜잭션 적용도 하고 쿼리도 서비스한다고 명시되어 있다.
Request Processor에서 처리한 요청을 트랜잭션을 생성하여 모든 서버에게 전파
[Leader-Propose] -> [Follower-Accept] -> [Leader-Commit] 단계로 구성된다.
정확히는 리더가 proposal을 보내고, 팔로워들이 이를 영속화(로그 기록)한 뒤 ACK, 리더가 과반수(quorum)의 ACK을 받으면 commit을 브로드캐스트하고 적용되는 식이야.
ZooKeeper의 상태는 각 서버가 전체 데이터 트리(DataTree)를 메모리에 복제(replica)로 들고 있고, 장애 복구를 위해 트랜잭션 로그 + 스냅샷을 디스크에 저장한다.

ZooKeeper는 기본적으로 복수 개의 ZooKeeper 서버의 집합인 Ensemble로 구성된다. Ensemble은 leader-follower 구조를 사용하며, Leader가 Follower에게 동기화를 위한 명령을 내리게 된다.
합의체가 의사를 진행시키거나 의결을 하는 데 필요한 최소한도의 인원수
주키퍼 앙상블을 이루고 있는 모든 서버 중 과반수 서버로 이루어진 그룹을 말한다.
Leader가 새로운 트랜잭션을 수행하기 위해서는 자신을 포함하여 과반수 이상의 서버의 합의를 얻어야 한다. 과반수의 합의를 위해 필요한 서버들을 Quorum이라고 한다. Ensemble을 구성하는 서버의 수가 5개라면, Quorum은 3개의 서버로 구성이 된다.

Leader에게 Request 전달
새로운 트랜잭션 요청이 Follower에게 도착하였을 경우, Follower는 Leader에게 요청을 전달한다.
Propose
Propose는 Leader가 Quorum을 구성하는 서버들에게 트랙잭션을 수행해도 되는지 여부를 요청하는 과정을 의미한다.
Ack
Quorum을 구성하는 서버들은 Leader로 부터 Propose 요청을 받으면, 트랙잭션을 수행해도 된다는 Ack 응답을 Leader에게 전송한다.
Commit
모든 Quorum으로 부터 Ack를 받으면, Leader는 트랙잭션을 처리하라는 Commit 명령을 broadcast 형태로 모든 Follower에 전파한다. ZooKeeper에서는 Commit 명령을 전달할 때, ZAB(ZooKeeper Atomic Broadcast) 알고리즘을 사용한다. Atomic Broadcast는 broacast 방식 중 하나로, 멀티 프로세스 시스템에서 모든 프로세스에게 동일한 순서로 메시지가 전달된다는 것을 의미한다.
쿼럼은 한 서버에서 일어난 변경이 앙상블 내의 다른 서버로 전파되어 복제될 때, 복제가 이용가능한 수준까지 이루어졌다는 최소 기준이다.
6대로 구성한 경우
두 서버에 문제가 생겼으면 4대가 동작하는데 과반수 이상이기 때문에 지속적으로 동작한다.
세 서버에 문제가 생겼으면 3대가 동작하는데 과반수 이상이 아니므로 동작을 멈춘다.
5대로 구성한 경우
두 서버에 문제가 생겼으면 3대가 동작하는데 과반수 이상이기 때문에 지속적으로 동작한다.
세 서버에 문제가 생겼으면 2대가 동작하는데 과반수 이상이 아니므로 동작을 멈춘다.
쿼럼이 5개 중 2개일 때 시나리오
사용자가 주키퍼에게 쓰기 작업을 요청한다. 주키퍼가 쿼럼에게 쓰기 작업을 복제한다.
1번과 2번 서버가 znode의 쓰기 작업을 완료했다고 리더에게 응답한다.
주키퍼 서비스가 클라이언트에게 알린다.
1번과 2번 서버에 문제가 발생했다고 가정한다. 1번과 2번 서버에 적용된 내용이 사라진다. 문제가 발생하더라도 과반수 이상이 살아있기 때문에 계속 동작한다.
아직 살아있는 서버 2개로 새로운 쿼럼을 구성한다.
주키퍼 서비스는 정상적으로 동작하지만 자기들끼리 쿼럼(2)을 만족한다고 판단하여 서로 다른 데이터를 업데이트 , 일관성이 깨진다.
결국 시스템 내에 서로 다른 데이터가 공존하는 스플릿 브레인(Split-brain) 현상이 발생
쿼럼이 5개 중 3개일 때 시나리오
사용자가 주키퍼에게 쓰기 작업을 요청한다. 주키퍼가 쿼럼에게 쓰기 작업을 복제한다.
쓰기 작업을 3대로 복제한다.
3대에 장애가 발생한 경우 서비스를 중단하고, 2대에 장애가 발생한 경우 지속한다.
쿼럼으로 구성되었던 서버 1대와 쿼럼에 포함되지 않았던 2대로 새로운 쿼럼을 구성한다.
쿼럼으로 구성되었던 서버 1대의 작업을 다른 서버들에게 복제한다.
쓰기 요청을 유실하지 않고 계속 사용할 수 있다.