ClickHouse Keeper 의 발전 과정
🕍

ClickHouse Keeper 의 발전 과정

ClickHouse 분류
Core Architecture
Type
Research
작성자

Ken

ClickHouse를 분산 환경에서 운영할 때 가장 중요한 구성요소 중 하나가 바로 분산 합의 시스템입니다. 이 글에서는 ClickHouse Keeper가 무엇인지, 왜 필요한지, 그리고 실제 운영에서 마주치는 주요 문제들과 그 해결 방안에 대해 깊이 있게 다루어 보겠습니다.

  • Keeper의 사용 목적
  • 역사: ZooKeeper에서 Keeper로
  • ZooKeeper 시대 (2016-2021)
  • Keeper 개발 시작 (2021년 2월)
  • 프로덕션 준비 완료 (2022년 4월)
  • Raft vs ZAB: 합의 알고리즘의 선택
  • 성능 벤치마크
  • 주요 문제 상황들
  • FINAL 키워드와 Keeper 리소스의 관계
  • ACID 미지원과 대응 노력
  • 결론

Keeper의 사용 목적

ClickHouse Keeper는 분산 ClickHouse 클러스터에서 메타데이터 저장소분산 조율(coordination) 기능을 제공하는 핵심 컴포넌트입니다. 단일 노드 ClickHouse 배포에서는 필요하지 않지만, ReplicatedMergeTree 테이블을 사용하거나 클러스터 확장을 계획한다면 반드시 필요합니다.

Keeper가 담당하는 주요 역할은 다음과 같습니다.

복제 메타데이터 관리: ReplicatedMergeTree 테이블의 파트 정보, 복제 상태, 머지 작업 큐 등을 저장합니다. 각 복제본이 어떤 데이터 파트를 보유하고 있는지, 어떤 머지 작업이 대기 중인지 등의 정보가 Keeper에 기록됩니다.

분산 DDL 실행: ON CLUSTER 구문을 사용한 DDL 명령어가 모든 노드에 일관되게 적용되도록 조율합니다. 예를 들어 CREATE TABLE ... ON CLUSTER를 실행하면, Keeper가 모든 노드에서 해당 테이블이 생성되도록 보장합니다.

중복 제거(Deduplication): INSERT 작업의 중복을 방지하기 위한 deduplication log를 관리합니다. 네트워크 장애로 인한 재시도 시에도 동일한 데이터가 중복 삽입되지 않도록 합니다.

리더 선출: 복제된 테이블에서 머지 작업을 수행할 리더를 선출하고, 분산 작업의 조율을 담당합니다.

접근 제어: 클러스터 전체의 접근 제어 설정을 동기화하고 관리합니다.

역사: ZooKeeper에서 Keeper로

ClickHouse Keeper의 탄생 배경을 이해하려면, 먼저 ZooKeeper와의 관계를 살펴봐야 합니다.

ZooKeeper 시대 (2016-2021)

ClickHouse가 처음 오픈소스로 공개되었을 때, 분산 조율을 위해 Apache ZooKeeper를 사용했습니다. ZooKeeper는 이미 검증된 분산 합의 시스템으로, Kafka, HBase 등 많은 분산 시스템에서 활용되고 있었습니다. ZooKeeper는 신뢰성 높은 합의 메커니즘과 간단하면서도 강력한 API를 제공했으며, 합리적인 성능을 보여주었습니다.

그러나 ClickHouse 클러스터의 규모가 커지면서 여러 문제점들이 드러나기 시작했습니다.

Java 의존성 문제: ZooKeeper는 Java로 작성되어 있어, C++로 작성된 ClickHouse 코드베이스와 잘 어울리지 않았습니다. JVM 힙 사이즈 설정, 가비지 컬렉션 튜닝 등 추가적인 운영 부담이 발생했습니다.

리소스 효율성: ZooKeeper는 상당한 메모리와 I/O 리소스를 소비했습니다. 특히 대규모 클러스터에서 Full GC가 발생하면 서비스 중단이나 성능 저하가 발생했습니다.

ZXID 오버플로우: ZooKeeper는 트랜잭션 ID(ZXID)가 20억 건에 도달하면 강제 재시작이 필요했습니다.

운영 복잡성: 별도의 ZooKeeper 앙상블을 구축하고 관리해야 했으며, ClickHouse 노드와 별개의 인프라를 운영해야 했습니다.

Keeper 개발 시작 (2021년 2월)

2021년 ClickHouse 로드맵에서 "ZooKeeper 대안 제공"이 주요 과제로 선정되었습니다. ClickHouse 팀은 기존 문제점들을 해결하면서도 ZooKeeper와 완벽하게 호환되는 새로운 시스템을 설계했습니다.

처음에는 ClickHouse 서버에 임베디드 서비스로 구현되었으며, 같은 해에 독립 실행(standalone) 모드가 추가되었습니다. Jepsen 테스트도 도입되어 6시간마다 다양한 워크플로우와 장애 시나리오를 통해 합의 메커니즘의 정확성을 검증합니다.

프로덕션 준비 완료 (2022년 4월)

2022년 4월, ClickHouse Keeper가 프로덕션 준비 완료(production-ready)로 선언되었습니다. ClickHouse Cloud는 2022년 5월 첫 프라이빗 프리뷰 출시 때부터 Keeper를 사용해왔으며, 현재 수천 개의 서비스를 멀티테넌트 환경에서 운영하고 있습니다.

Raft vs ZAB: 합의 알고리즘의 선택

ZooKeeper는 ZAB(ZooKeeper Atomic Broadcast) 프로토콜을 사용하는 반면, ClickHouse Keeper는 Raft 알고리즘을 선택했습니다. ZAB가 2008년부터 개발되어 더 오래된 검증된 옵션이었음에도 Raft를 선택한 이유는 다음과 같습니다.

Raft는 상대적으로 단순하고 이해하기 쉬운 알고리즘입니다. 또한 2021년 Keeper 프로젝트 시작 시점에 가볍고 통합하기 쉬운 C++ 라이브러리(NuRaft)가 이미 존재했습니다. 본질적으로 모든 합의 알고리즘은 동형(isomorphic)이므로, Raft로도 ZAB와 동일한 일관성 보장을 제공할 수 있습니다.

성능 벤치마크

ClickHouse 팀이 개발한 벤치마크 스위트를 통해 동일한 인프라에서 Keeper와 ZooKeeper를 비교한 결과, Keeper는 동일한 데이터 볼륨에서 ZooKeeper 대비 최대 46배 적은 메모리를 사용하면서도 비슷한 성능을 유지했습니다. Bonree社의 실제 운영 환경 테스트에서도 ZooKeeper 대비 메모리 사용량 4.5배, I/O 사용량 8배 감소를 확인했습니다.

주요 문제 상황들

FINAL 키워드와 Keeper 리소스의 관계

ClickHouse를 사용하다 보면 "FINAL 키워드를 사용할 때 왜 Keeper 리소스가 증가하는가?"라는 의문을 가질 수 있습니다. 이를 이해하려면 먼저 ReplacingMergeTree와 FINAL의 동작 방식을 알아야 합니다.

ReplacingMergeTree의 중복 제거 메커니즘

ReplacingMergeTree는 ORDER BY 컬럼을 기준으로 중복 행을 제거하는 테이블 엔진입니다. 하지만 중복 제거는 데이터 삽입 시점이 아닌, 백그라운드 머지(merge) 시점에 발생합니다. 즉, 머지가 완료되기 전까지는 쿼리 결과에 중복 행이 포함될 수 있습니다.

FINAL의 동작 원리

FINAL 키워드를 사용하면 ClickHouse는 쿼리 시점에 강제로 중복 제거를 수행합니다. 이 과정에서 다음과 같은 일이 발생합니다.

첫째, 모든 데이터 파트를 읽어서 메모리에 로드합니다. 둘째, ORDER BY 컬럼을 기준으로 중복을 식별하고 최신 버전만 유지합니다. 셋째, 파티션 간 데이터도 병합하여 중복을 제거합니다(기본 설정 시).

Keeper 리소스 증가의 원인

실제로 FINAL 키워드 자체가 직접적으로 Keeper 리소스를 증가시키는 것은 아닙니다. 다만, 다음과 같은 간접적인 영향이 있을 수 있습니다.

대량의 INSERT가 발생하는 환경에서 FINAL 쿼리가 자주 실행되면, 백그라운드 머지와 경쟁하게 됩니다. 머지 작업은 Keeper에 메타데이터 업데이트를 요청하므로, 시스템 전체의 부하가 증가합니다.

또한 OPTIMIZE TABLE FINAL을 사용하면 모든 파트를 하나로 병합하고, 이 정보가 Keeper에 기록됩니다. ReplicatedMergeTree의 경우, alter_sync 설정에 따라 모든 복제본에서 머지 완료를 기다리므로 Keeper 통신이 증가합니다.

권장 최적화 방안

-- 파티션 단위로만 FINAL 처리하여 Keeper 부하 감소
SELECT * FROM events_replacing FINAL
SETTINGS do_not_merge_across_partitions_select_final = 1;

-- 또는 파티션을 명시하여 최적화
SELECT * FROM events_replacing FINAL
WHERE toYYYYMM(event_time) = 202401;

do_not_merge_across_partitions_select_final=1 설정을 사용하면 FINAL이 파티션 간 병합을 수행하지 않아 성능이 크게 향상됩니다. Altinity의 테스트에서는 이 설정으로 쿼리 시간이 9초에서 1.25초로 약 7배 감소했습니다.

ACID 미지원과 대응 노력

ClickHouse는 OLAP(Online Analytical Processing) 워크로드에 최적화된 데이터베이스로, 전통적인 OLTP 데이터베이스처럼 완전한 ACID 트랜잭션을 지원하지 않습니다. 그러나 이를 보완하기 위한 다양한 노력이 진행되고 있습니다.

현재 ClickHouse의 ACID 지원 수준

단일 INSERT 문에 대해서는 조건부 ACID가 지원됩니다. 삽입된 행들이 단일 블록으로 패킹되어 삽입될 경우, Atomicity, Consistency, Isolation, Durability가 보장됩니다.

Atomicity 측면에서 INSERT는 전체가 성공하거나 전체가 실패합니다. Consistency의 경우 테이블 제약 조건이 위반되지 않으면 모든 행이 삽입됩니다. Isolation 관점에서 동시 클라이언트는 INSERT 시도 전 또는 성공 후의 일관된 스냅샷을 관찰합니다. Durability는 성공한 INSERT가 파일시스템에 기록된 후에야 클라이언트에 응답한다는 것을 의미합니다.

제한 사항

여러 가지 중요한 제한 사항이 존재합니다. 분산 테이블(Distributed)로의 INSERT는 전체적으로 트랜잭션이 아닙니다. Buffer 테이블로의 삽입은 원자적이지도, 격리되지도, 일관되지도, 영속적이지도 않습니다. async_insert가 활성화되고 wait_for_async_insert가 0으로 설정되면 원자성이 보장되지 않습니다. 다중 문(multi-statement) 트랜잭션과 롤백도 기본적으로 지원되지 않습니다.

실험적 트랜잭션 기능

ClickHouse는 실험적으로 다중 테이블, Materialized View, 다중 SELECT에 걸친 완전한 트랜잭션 기능을 개발 중입니다. 이 기능은 Keeper를 통해 트랜잭션을 추적합니다.

<!-- 실험적 트랜잭션 활성화 -->
<clickhouse>
    <allow_experimental_transactions>1</allow_experimental_transactions>
</clickhouse>
-- 실험적 트랜잭션 사용 예시
BEGIN TRANSACTION;
INSERT INTO table1 VALUES (1, 'data');
INSERT INTO table2 VALUES (2, 'more_data');
COMMIT;

현재 MergeTree 엔진에서만 구현되어 있으며, 트랜잭션 중 예외가 발생하면 커밋이 불가능합니다. 중첩 트랜잭션은 지원되지 않습니다.

ACID 대응을 위한 실용적 패턴들

실제 운영 환경에서 ACID 부재를 보완하기 위해 다양한 패턴이 사용됩니다.

첫째, ReplacingMergeTree와 버전 컬럼을 활용합니다. 최신 버전의 행만 유지하여 업데이트를 시뮬레이션할 수 있습니다.

CREATE TABLE users (
    user_id UInt64,
    name String,
    email String,
    version UInt64,
    is_deleted UInt8
) ENGINE = ReplacingMergeTree(version, is_deleted)
ORDER BY user_id;

둘째, 멱등성(Idempotency) 설계를 통해 동일한 INSERT가 여러 번 실행되어도 결과가 동일하도록 설계합니다.

셋째, 외부 트랜잭션 관리로 Kafka, PostgreSQL 등 외부 시스템에서 트랜잭션을 관리하고, ClickHouse는 최종 결과만 저장합니다.

넷째, Apache Iceberg 통합을 활용합니다. ClickHouse는 Iceberg 테이블 포맷을 지원하며, Iceberg는 완전한 ACID 트랜잭션을 제공합니다.

-- Iceberg 카탈로그를 통한 테이블 접근
SELECT * FROM iceberg_catalog.database.table;

향후 발전 방향

ClickHouse 팀은 트랜잭션 지원에 대한 수요를 인식하고 있으며, 다음 단계를 계획 중입니다. 커뮤니티의 피드백을 수집하여 실제 사용 사례를 파악하고 있으며, MergeTree 엔진에서의 실험적 트랜잭션 기능을 점진적으로 확장할 예정입니다. Multi-group Raft 도입을 통해 여러 Raft 인스턴스를 사용하여 확장성을 개선하는 것도 로드맵에 포함되어 있습니다.

결론

ClickHouse Keeper는 ZooKeeper의 한계를 극복하고 ClickHouse 생태계에 최적화된 분산 합의 시스템입니다. C++로 작성되어 리소스 효율성이 뛰어나고, Raft 알고리즘을 통해 더 강력한 일관성 보장을 제공합니다.

FINAL 키워드 사용 시 발생할 수 있는 성능 문제는 적절한 설정과 파티셔닝 전략으로 완화할 수 있으며, ACID 트랜잭션의 부재는 다양한 설계 패턴과 외부 도구를 통해 보완할 수 있습니다.

2022년 프로덕션 준비 완료 이후 Keeper는 ClickHouse Cloud에서 대규모로 검증되었으며, 새로운 설치에서는 ZooKeeper 대신 Keeper 사용이 권장됩니다. 향후 Multi-group Raft와 트랜잭션 기능 확장을 통해 더욱 강력한 분산 데이터베이스 플랫폼으로 발전할 것으로 기대됩니다.

참고 자료

  • ClickHouse Keeper 공식 문서
  • ClickHouse Keeper: A ZooKeeper alternative written in C++
  • Why is ClickHouse Keeper recommended over ZooKeeper?
  • Transactional (ACID) support
  • ReplacingMergeTree Explained