Skip to Content
BlogClickHouse Keeper는 큐가 아니다

ClickHouse Keeper는 큐가 아니다

#ClickHouse#Keeper#ReplicatedMergeTree#Kafka

ClickHouse에 이벤트를 보내는 중 서버가 꺼졌다고 가정해 봅니다.

Keeper 세 대가 모두 살아 있다면 수신 중이던 이벤트도 어딘가에 남아 있을까요?

남아 있지 않습니다.

Keeper는 ClickHouse의 복제와 분산 작업을 조정하지만, 클라이언트가 보낸 이벤트 본문을 저장하는 메시지 큐가 아닙니다.

이 차이를 놓치면 Keeper를 고가용성으로 만들고도 ingest 중단 때 데이터를 잃을 수 있습니다.

Keeper가 맡는 일

ClickHouse Keeper 는 ZooKeeper와 호환되는 클라이언트 프로토콜과 데이터 모델을 제공하는 분산 조정 서비스입니다.

Raft 합의를 사용해 복제 상태와 분산 작업의 순서를 맞춥니다.

ReplicatedMergeTree에서는 각 replica가 같은 테이블의 파트를 갖도록 조정합니다.

하지만 파트의 실제 바이트는 replica끼리 직접 주고받습니다.

Keeper에는 작은 znode 형태의 메타데이터가 들어갑니다.

Keeper에 저장하는 것역할
replication log어떤 파트와 작업이 생겼는지 기록
replica queue해당 replica가 아직 수행하지 않은 fetch·merge 작업
part와 block 번호파트 존재와 순서를 조정
dedup checksum최근 INSERT 블록의 중복 여부 판단
distributed DDL queueON CLUSTER DDL의 실행 순서와 완료 상태
leader election과 lockmerge·mutation 같은 분산 작업의 소유권 조정

반대로 Keeper에 없는 것은 다음과 같습니다.

  • INSERT로 보낸 원본 row
  • 아직 flush되지 않은 비동기 INSERT 버퍼
  • replica 디스크에 저장된 part 파일
  • SELECT 결과와 query cache
  • Collector가 전송하지 못한 telemetry batch
데이터 바이트파트 바이트 직접 fetch파트 등록·블록번호·로그복제 지시(포인터)Keeper 안 탐Client / OTelCollectorCH replica-1파트를 자기 디스크에 씀CH replica-2SELECT 쿼리Keeper 앙상블3 or 5 노드
INSERT 데이터 경로 — Keeper는 메타데이터만, 파트 바이트는 replica 직송

클라이언트는 ClickHouse replica에 데이터 바이트를 보냅니다.

첫 replica가 part를 만들고 Keeper에 등록하면 다른 replica는 해야 할 작업을 알게 됩니다.

다른 replica는 Keeper에서 데이터 본문을 읽는 것이 아니라 원본 replica에서 part를 fetch합니다.

Keeper의 복제 로그에 part 이름이 남아 있어도 그 part를 가진 replica가 모두 사라지면 본문을 복원할 수 없습니다.

메타데이터는 데이터의 위치를 가리킬 뿐, 데이터 자체를 대신하지 않습니다.

이름에 queue가 붙어도 메시지 큐는 아니다

Keeper에는 task_queue, replica의 queue, replication log가 있습니다.

이름만 보면 Kafka 같은 durable queue로 보이기 쉽습니다.

둘의 계약은 전혀 다릅니다.

Kafka 같은 이벤트 로그ClickHouse Keeper
저장 대상producer가 보낸 메시지 본문파트 참조, 체크섬, 작업 지시
보존retention 동안 다시 읽을 수 있음조정에 필요한 작은 상태
replay소비자가 offset부터 재처리이벤트 본문 replay 불가
ClickHouse 경로의 위치ClickHouse 앞단ClickHouse 복제 조정 계층
ClickHouse 중단 흡수retention 범위에서 가능불가능

ClickHouse가 잠시 멈췄을 때 이벤트를 보관하려면 큐가 앞단에 있어야 합니다.

Kafka를 쓸 수도 있고, OTel Collector의 영속 sending_queue처럼 더 작은 장치를 쓸 수도 있습니다.

어느 쪽이든 성공적으로 ClickHouse에 전달하기 전까지 이벤트 본문을 소유해야 합니다.

Keeper 노드를 세 대에서 다섯 대로 늘리는 일은 이 문제를 해결하지 않습니다.

정족수를 잃으면 무엇이 멈추나

Keeper가 쓰기를 진행하려면 과반이 필요합니다.

필요한 수는 floor(N / 2) + 1입니다.

Keeper 수필요한 과반견디는 동시 소실
110
220
321
431
532

4대는 3대보다 한 대를 더 쓰면서도 견디는 소실 수가 같습니다.

그래서 보통 3대 또는 5대처럼 홀수로 구성합니다.

3대 중 2대를 잃으면 Keeper 정족수가 사라집니다.

이때 “ClickHouse 전체가 죽는다”라고 단정하면 실제 장애를 잘못 읽게 됩니다.

정족수 상실로 주로 멈추는 것은 새 조정 상태가 필요한 작업입니다.

멈추는 작업계속될 수 있는 작업
신규 part 등록과 INSERT이미 로컬에 있는 part의 SELECT
replica 동기화와 복제 큐 진행Keeper가 필요하지 않은 진행 중 로컬 조회
ON CLUSTER DDL기존 데이터 파일 읽기
mutation·merge 조정 일부로컬 테이블과 상태에 따른 일부 읽기

SELECT가 항상 성공한다는 뜻은 아닙니다.

분산 쿼리가 접근해야 할 replica까지 함께 죽었거나, 쿼리 경로가 Keeper 상태를 새로 요구하거나, 읽으려는 part가 로컬에 없다면 조회도 실패할 수 있습니다.

정확한 표현은 Keeper 정족수를 잃어도 이미 로컬에 있는 part를 읽는 조회는 계속될 수 있다입니다.

장애를 어떻게 확인할까

애플리케이션의 TABLE_IS_READ_ONLY 오류만으로 원인을 확정하지 않습니다.

먼저 복제 테이블 상태를 봅니다.

SELECT database, table, replica_name, is_leader, is_readonly, is_session_expired, queue_size, absolute_delay, total_replicas, active_replicas, zookeeper_exception FROM system.replicas ORDER BY database, table;

is_readonly = 1is_session_expired = 1이 여러 replica에서 함께 보이면 Keeper 연결과 정족수를 의심할 수 있습니다.

zookeeper_exception은 세션 만료, 연결 실패, 경로 문제를 구분하는 단서입니다.

그다음 Keeper 멤버 상태를 확인합니다.

kubectl get pods -n clickhouse -o wide kubectl get pvc -n clickhouse

포트는 배포 방식에 따라 다릅니다.

독립형 ClickHouse Keeper 예제에서 흔히 보는 클라이언트 포트와 Altinity ClickHouseKeeperInstallation의 설정을 섞으면 진단 명령이 엉뚱한 곳을 봅니다.

CHK 구성에서는 클라이언트 포트 2181, Raft 포트 9444를 사용하는 예가 일반적이고, 독립형 Keeper 문서에서는 9181을 자주 봅니다.

실제 Service와 설정을 먼저 확인합니다.

kubectl get svc -n clickhouse kubectl get endpointslice -n clickhouse

포트 번호를 기억에 의존하지 않고 배포된 리소스에서 확인하는 편이 안전합니다.

Keeper 저장소도 영속적이어야 한다

Keeper가 이벤트 본문을 저장하지 않는다고 해서 Keeper 데이터가 중요하지 않은 것은 아닙니다.

Raft log와 snapshot에는 복제 상태와 조정 이력이 들어 있습니다.

데이터 노드가 로컬 NVMe를 쓰더라도 Keeper는 EBS gp3 같은 영속 볼륨에 두는 이유입니다.

Keeper 파드가 다른 노드로 옮겨도 기존 볼륨을 재부착할 수 있어야 정족수 복구가 단순해집니다.

세 멤버를 같은 노드나 같은 AZ에 몰아두면 숫자만 3일 뿐 장애 도메인은 하나입니다.

확인할 것은 세 가지입니다.

  1. 서로 다른 노드에 배치됐는가?
  2. AZ 장애를 견뎌야 한다면 서로 다른 AZ에 배치됐는가?
  3. 각 멤버의 Raft 데이터 볼륨이 삭제 정책에서 보호되는가?

Kubernetes PDB는 계획된 eviction을 제한합니다.

노드 급사나 AZ 장애를 막지는 못합니다.

Keeper의 배치와 볼륨 보존을 PDB 하나로 대신할 수 없습니다.

작은 INSERT가 Keeper에도 부담을 만든다

MergeTree는 INSERT마다 작은 part를 계속 만들면 파일 수와 merge 비용이 늘어납니다.

복제 구성에서는 part마다 Keeper 엔트리도 만들어집니다.

ClickHouse의 비동기 INSERT 설명 도 작은 part가 파일 작업, write amplification, Keeper 요청을 함께 늘린다고 설명합니다.

따라서 배칭은 데이터 디스크만을 위한 튜닝이 아닙니다.

Keeper의 조정 부하도 줄입니다.

다음 지표를 함께 봅니다.

SELECT metric, value FROM system.metrics WHERE metric ILIKE '%ZooKeeper%' OR metric ILIKE '%Keeper%' ORDER BY metric;
SELECT database, table, count() AS active_parts, sum(rows) AS rows, formatReadableSize(sum(bytes_on_disk)) AS bytes FROM system.parts WHERE active GROUP BY database, table ORDER BY active_parts DESC;

Keeper 요청이 늘고 active part도 빠르게 증가한다면 replica 수보다 INSERT shape를 먼저 확인할 수 있습니다.

경계를 한 줄로 정리하면

Keeper는 어떤 part가 있고 누가 무엇을 해야 하는지 기억합니다.

Kafka나 영속 Collector 큐는 아직 ClickHouse에 저장하지 못한 이벤트 본문을 기억합니다.

둘 다 장애 대응에 필요할 수 있지만 서로 대체하지 않습니다.

Keeper 정족수가 살아 있다는 사실은 ingest 데이터가 어딘가에 durable하게 대기한다는 뜻이 아닙니다.

반대로 Keeper 정족수를 잃었다고 이미 저장된 모든 SELECT가 즉시 사라지는 것도 아닙니다.

어떤 계층이 어떤 바이트를 소유하는지부터 그리면 장애의 경계가 꽤 선명해집니다.

자료 확인 기준: 2026-09-15. Keeper 포트와 시스템 테이블 컬럼은 실제 배포 버전과 operator가 렌더한 설정에서 다시 확인해야 합니다.