OSS 대형 클러스터 운영 기술: ReplicatedMergeTree와 Distributed 엔진
🦣

OSS 대형 클러스터 운영 기술: ReplicatedMergeTree와 Distributed 엔진

ClickHouse 분류
Core Architecture
Type
Research
작성자

Ken

ClickHouse는 실시간 분석 분야에서 독보적인 성능을 자랑하는 데이터베이스입니다. 단일 서버에서도 초당 수억 행을 처리하는 놀라운 속도 덕분에 많은 기업들이 ClickHouse OSS를 선택합니다. 하지만 비즈니스가 성장하고 데이터가 페타바이트 규모로 확장되면, 단일 서버의 한계를 넘어 클러스터로 확장해야 하는 시점이 옵니다. 바로 이때 ReplicatedMergeTree와 Distributed 엔진이 등장합니다.

이 글에서는 ClickHouse OSS 대규모 클러스터 운영에 필수적인 두 엔진의 아키텍처를 깊이 있게 살펴보고, 실제 운영에서 마주치는 고민들, 그리고 왜 많은 대형 고객들이 ClickHouse Cloud로의 전환을 고려하게 되는지까지 솔직하게 이야기해보겠습니다.

  • 1부: Shared-Nothing 아키텍처의 이해
  • ClickHouse OSS의 설계 철학
  • 핵심 개념 정리
  • 2부: 코디네이션 시스템 — Keeper
  • ZooKeeper의 한계와 Keeper의 등장
  • Keeper 설정 예시
  • 3부: ReplicatedMergeTree — 데이터 복제의 핵심
  • 복제 메커니즘
  • 테이블 생성
  • 매크로 설정
  • 복제 상태 모니터링
  • 4부: Distributed 엔진 — 분산 쿼리의 관문
  • 역할과 동작 방식
  • 클러스터 정의
  • Distributed 테이블 생성
  • 샤딩 키 설계
  • 5부: 실제 운영의 고민들
  • 고민 1: INSERT 전략의 딜레마
  • 고민 2: 노드 추가의 복잡성
  • 고민 3: Keeper 부하 관리
  • 고민 4: JOIN의 함정
  • 고민 5: 운영 overhead
  • 6부: 왜 SharedMergeTree가 필요했는가
  • Shared-Nothing에서 Shared-Everything으로
  • 이 아키텍처가 해결하는 문제들
  • 실제 마이그레이션 사례: Poizon
  • 7부: 언제 ClickHouse Cloud를 고려해야 하는가
  • OSS가 여전히 좋은 선택인 경우
  • Cloud 전환을 고려해야 하는 시그널
  • BYOC(Bring Your Own Cloud) 옵션
  • 결론: 복잡성을 어디서 감당할 것인가
  • 참고 자료

1부: Shared-Nothing 아키텍처의 이해

ClickHouse OSS의 설계 철학

ClickHouse OSS는 전통적인 Shared-Nothing 아키텍처를 기반으로 합니다. 각 서버가 자체 스토리지에 데이터를 보유하고, 독립적으로 쿼리를 처리하는 구조입니다. 이 설계는 단일 서버의 성능을 극대화하는 데 최적화되어 있으며, ClickHouse가 수직 확장(Scale-up)을 선호하는 이유이기도 합니다.

하지만 데이터가 단일 서버의 용량을 초과하거나, 더 높은 처리량이 필요하거나, 고가용성이 요구되면 수평 확장(Scale-out)이 불가피합니다. 이때 필요한 것이 **샤딩(Sharding)**과 **복제(Replication)**입니다.

핵심 개념 정리

  • *샤드(Shard)**는 전체 데이터의 일부분을 담당하는 서버(또는 서버 그룹)입니다. 데이터를 여러 샤드에 분산하면 각 샤드가 병렬로 쿼리를 처리할 수 있어 처리량이 늘어납니다.
  • *레플리카(Replica)**는 동일한 데이터의 복제본입니다. 한 샤드 내에 여러 레플리카를 두면, 하드웨어 장애 시에도 데이터 손실 없이 서비스를 지속할 수 있습니다.

일반적인 프로덕션 구성은 "N샤드 × M레플리카" 형태입니다. 예를 들어 "3샤드 × 2레플리카"는 6대의 서버로 구성되며, 데이터가 3등분되어 각각 2개의 복제본을 가집니다.

2부: 코디네이션 시스템 — Keeper

복제가 제대로 동작하려면 "어떤 데이터가 어디에 있는지", "누가 어떤 작업을 수행해야 하는지"를 조율하는 중앙 코디네이터가 필요합니다. ClickHouse는 이를 위해 ZooKeeper 또는 자체 개발한 ClickHouse Keeper를 사용합니다.

ZooKeeper의 한계와 Keeper의 등장

초기에는 Apache ZooKeeper가 유일한 선택지였습니다. 하지만 대규모 운영 환경에서 ZooKeeper는 여러 문제를 드러냈습니다.

첫째, 리소스 비효율입니다. Java 기반이라 JVM 튜닝이 필요하고, 메모리 사용량이 높습니다. 한 실제 사례에서 동일 환경에서 ZooKeeper의 메모리 사용량이 Keeper 대비 4.5배, I/O 사용량이 8배 높았다는 보고가 있습니다.

둘째, 운영 복잡성입니다. zxid 오버플로우(20억 트랜잭션마다 재시작 필요), Full GC로 인한 서비스 중단, 압축되지 않는 로그 등의 문제가 있습니다.

셋째, 확장성 제한입니다. 클러스터가 커질수록 ZooKeeper에 대한 부하가 급증합니다. 실제로 Bonree라는 기업은 5샤드마다 별도의 ZooKeeper 클러스터(3노드)를 추가해야 했고, 결국 15노드의 ZooKeeper를 운영하는 상황에 이르렀습니다.

ClickHouse Keeper는 이러한 문제들을 해결하기 위해 개발되었습니다. C++로 작성되어 최대 46배 적은 메모리를 사용하고, Raft 합의 알고리즘으로 더 빠른 복구가 가능하며, ClickHouse 바이너리에 내장되어 별도 설치가 불필요합니다.

Keeper 설정 예시

프로덕션 환경에서는 최소 3대의 전용 Keeper 노드를 권장합니다. Keeper가 불안정하면 전체 클러스터의 쓰기가 중단될 수 있으므로, 이 컴포넌트의 안정성은 매우 중요합니다.

3부: ReplicatedMergeTree — 데이터 복제의 핵심

복제 메커니즘

ReplicatedMergeTree는 테이블 레벨에서 복제를 수행합니다. 같은 Keeper 경로를 공유하는 테이블들이 복제 그룹을 형성하고, 한 레플리카에 데이터가 INSERT되면 다른 레플리카들이 이를 감지하고 자동으로 동기화합니다.

핵심적인 점은 Keeper는 메타데이터만 저장한다는 것입니다. 실제 데이터는 레플리카 간에 직접 전송됩니다. 이 구조 덕분에 Keeper의 부하는 상대적으로 낮게 유지되지만, 레플리카 간 네트워크 대역폭과 레이턴시가 복제 성능에 직접적인 영향을 미칩니다.

테이블 생성

CREATE TABLE events ON CLUSTER 'my_cluster'
(
    timestamp DateTime,
    user_id UInt64,
    event_type LowCardinality(String),
    properties String
)
ENGINE = ReplicatedMergeTree(
    '/clickhouse/{cluster}/tables/{shard}/events',
    '{replica}'
)
PARTITION BY toYYYYMM(timestamp)
ORDER BY (user_id, timestamp)

ZooKeeper 경로 설계는 매우 중요합니다. {shard} 매크로를 포함해 샤드별로 다른 경로를 사용해야 합니다. 같은 경로를 가진 테이블들이 데이터를 공유하므로, 실수로 경로를 재사용하면 데이터 손실이 발생할 수 있습니다.

매크로 설정

각 서버의 설정 파일에 매크로를 정의합니다.

<!-- clickhouse-01 (Shard 1, Replica 1) -->
<macros>
    <cluster>my_cluster</cluster>
    <shard>01</shard>
    <replica>clickhouse-01</replica>
</macros>

<!-- clickhouse-02 (Shard 1, Replica 2) -->
<macros>
    <cluster>my_cluster</cluster>
    <shard>01</shard>
    <replica>clickhouse-02</replica>
</macros>

복제 상태 모니터링

SELECT
    database, table, replica_name,
    is_leader, is_readonly,
    queue_size, inserts_in_queue,
    absolute_delay
FROM system.replicas
WHERE absolute_delay > 300 OR is_readonly = 1;

absolute_delay가 증가하면 복제 지연이 발생하고 있다는 신호입니다. is_readonly=1이면 해당 레플리카가 Keeper와 연결이 끊어진 상태입니다.

4부: Distributed 엔진 — 분산 쿼리의 관문

역할과 동작 방식

Distributed 테이블은 데이터를 저장하지 않는 특수 테이블입니다. 대신 여러 샤드의 로컬 테이블들을 하나의 통합된 뷰로 제공합니다.

SELECT 쿼리는 모든 샤드로 분산되어 병렬 실행되고, 결과가 코디네이터 노드에서 병합됩니다.

INSERT는 샤딩 키에 따라 적절한 샤드로 라우팅됩니다.

클러스터 정의

internal_replication=true는 반드시 설정해야 합니다. 이 설정이 없으면 Distributed 테이블이 모든 레플리카에 직접 데이터를 쓰려고 시도해 중복 데이터가 발생합니다. true로 설정하면 각 샤드의 하나의 레플리카에만 쓰고, 나머지는 ReplicatedMergeTree가 복제를 처리합니다.

Distributed 테이블 생성

CREATE TABLE events_distributed ON CLUSTER 'my_cluster'
AS events
ENGINE = Distributed('my_cluster', currentDatabase(), events, cityHash64(user_id))

샤딩 키 설계

샤딩 키는 쿼리 패턴에 따라 신중히 선택해야 합니다.

rand(): 데이터를 균등하게 분산하지만, 특정 키로 필터링하는 쿼리가 모든 샤드를 스캔해야 합니다.

cityHash64(user_id): 같은 사용자의 데이터가 같은 샤드에 모이므로, 사용자별 쿼리가 단일 샤드에서 처리됩니다. JOIN 시 GLOBAL 없이 로컬 조인이 가능합니다.

5부: 실제 운영의 고민들

여기까지가 "교과서적인" 설명입니다. 하지만 실제로 대규모 클러스터를 운영해본 사람이라면, 이론과 현실 사이의 간극이 얼마나 큰지 알고 있을 것입니다.

고민 1: INSERT 전략의 딜레마

Distributed 테이블로 INSERT하면 편리하지만, 데이터가 먼저 연결된 노드에 저장된 후 재분배됩니다. 기본 비동기 모드에서는 로컬 디스크에 임시 저장 후 백그라운드로 전송되는데, 이 과정에서 문제가 생기면 system.distribution_queue에 수백만 개의 미처리 파일이 쌓일 수 있습니다.

로컬 테이블에 직접 INSERT하면 성능은 좋지만, 클라이언트가 샤딩 로직을 구현해야 하고, 토폴로지 변경 시 클라이언트도 수정해야 합니다.

-- Distribution 큐 상태 확인 (이 숫자가 계속 증가하면 문제)
SELECT database, table, data_files, broken_data_files
FROM system.distribution_queue;

고민 2: 노드 추가의 복잡성

"단순히 서버 추가하면 되는 거 아닌가요?"

아닙니다. ClickHouse OSS에서 샤드를 추가하면 다음과 같은 작업이 필요합니다.

  1. 새 서버에 ClickHouse와 Keeper 연결 설정
  2. 모든 노드의 remote_servers.xml 업데이트 및 재시작 또는 reload
  3. 새 노드에서 모든 테이블을 ON CLUSTER로 생성
  4. Distributed 테이블 재생성 또는 갱신

더 큰 문제는 기존 데이터입니다. 새 샤드가 추가되어도 기존 데이터는 자동으로 재분배되지 않습니다. 기존 데이터는 여전히 이전 샤드들에만 존재하고, 새 INSERT만 새 샤드로 갈 수 있습니다. 데이터를 균등하게 재분배하려면 별도의 마이그레이션 작업이 필요합니다.

Contentsquare의 엔지니어링 팀은 이 문제를 해결하기 위해 여러 방법을 검토했습니다. clickhouse-copier를 사용한 클러스터 간 마이그레이션은 프로덕션 성능에 영향을 미쳤고, 같은 클러스터 내 리샤딩도 운영 중인 서버에 부하를 주었습니다. 결국 Kafka를 중간 매개체로 사용하는 복잡한 솔루션을 자체 개발해야 했습니다.

고민 3: Keeper 부하 관리

클러스터가 커질수록 Keeper에 대한 부하가 급증합니다. 테이블 수, 파티션 수, INSERT 빈도가 모두 Keeper 부하에 영향을 미칩니다.

Bonree의 사례에서, 파트 수가 2만 개를 넘어가자 INSERT 응답 시간이 밀리초에서 수십 초로 증가했습니다. 결국 5샤드마다 별도의 ZooKeeper 클러스터를 두는 복잡한 아키텍처를 구성했지만, 15노드의 ZooKeeper를 관리하는 것은 그 자체로 큰 운영 부담이었습니다.

고민 4: JOIN의 함정

분산 환경에서 JOIN은 예상치 못한 성능 문제를 일으킵니다.

-- 이 쿼리가 왜 느릴까?
SELECT e.*, u.name
FROM events_distributed e
JOIN users_distributed u ON e.user_id = u.user_id
WHERE e.timestamp > now() - INTERVAL 1 DAY

기본적으로 JOIN의 오른쪽 테이블(users_distributed)이 모두 코디네이터로 전송된 후 조인이 수행됩니다. 수백만 명의 사용자 테이블이라면 네트워크와 메모리 병목이 발생합니다.

해결책으로 GLOBAL JOIN을 사용하거나, 같은 샤딩 키로 데이터를 co-locate하거나, Dictionary를 활용할 수 있지만, 모두 추가적인 설계와 구현 노력이 필요합니다.

고민 5: 운영 overhead

버전 업그레이드, OS 패치, 보안 설정, 백업, 모니터링, 알림 설정... 단일 서버도 만만치 않은 이 작업들이 노드 수만큼 곱해집니다. 대기업의 복잡한 보안/컴플라이언스 요구사항까지 더해지면, ClickHouse 클러스터 관리에만 전담 인력이 필요해집니다.

한 금융 서비스 기업 사례에서, ClickHouse 클러스터 운영을 위해 인프라, 네트워크, 스토리지, 보안 팀과 지속적으로 협업해야 했고, 방화벽 변경 하나에도 상당한 시간과 비용이 들었습니다.

6부: 왜 SharedMergeTree가 필요했는가

ClickHouse Inc.는 이러한 OSS 운영의 한계를 잘 알고 있었습니다. 그래서 ClickHouse Cloud를 위해 완전히 새로운 테이블 엔진을 개발했습니다: SharedMergeTree.

Shared-Nothing에서 Shared-Everything으로

SharedMergeTree의 핵심 혁신은 스토리지와 컴퓨트의 완전한 분리입니다.

ReplicatedMergeTree에서는 각 서버가 데이터와 메타데이터를 로컬에 저장합니다. 레플리카 간 데이터 동기화가 필요하고, 새 노드가 추가되면 데이터를 복제해야 합니다.

SharedMergeTree에서는 모든 데이터가 **오브젝트 스토리지(S3/GCS/Azure Blob)**에 저장됩니다. 컴퓨트 노드는 상태가 없는(stateless) 워커로, 필요할 때 오브젝트 스토리지에서 데이터를 읽어옵니다. 메타데이터는 Keeper에 저장되고, 레플리카 간 직접 통신이 불필요합니다.

이 아키텍처가 해결하는 문제들

노드 추가가 즉각적입니다. 새 컴퓨트 노드는 데이터 복제 없이 즉시 쿼리를 처리할 수 있습니다. 데이터는 이미 공유 스토리지에 있으니까요.

샤딩이 필요 없습니다. 오브젝트 스토리지는 사실상 무한대의 용량을 제공하므로, 데이터를 분할할 이유가 없습니다. Distributed 테이블, 샤딩 키 설계, 리샤딩 걱정이 모두 사라집니다.

고가용성이 기본입니다. 오브젝트 스토리지 자체가 고가용성을 제공하므로, 별도의 레플리카 관리가 불필요합니다.

수평 확장이 간단합니다. 쿼리 부하가 증가하면 컴퓨트 노드만 추가하면 됩니다. Parallel Replicas 기능으로 단일 쿼리도 여러 노드에서 병렬 처리할 수 있습니다.

실제 마이그레이션 사례: Poizon

중국의 명품 이커머스 플랫폼 Poizon은 ClickHouse OSS에서 ClickHouse Cloud(Enterprise Edition)로 전환한 대표적인 사례입니다.

이전 상황: 2022년 이후 trace 데이터가 일일 수백 TB에서 수 PB로 30배 증가. 자체 호스팅 분산 아키텍처의 비용 압박과 운영 복잡성 증가. 대형 쇼핑 시즌(광군제, 618)에 트래픽 급증 대응 어려움.

전환 후 결과: 초당 2천만 행 쓰기 속도 달성. 인프라 비용 60% 절감. 분 단위 수평 확장으로 트래픽 급증 대응. 샤드/Distributed 테이블 관리 부담 제거.

핵심 포인트는 단순히 "관리형 서비스로 이전"한 것이 아니라, 아키텍처 자체가 바뀌면서 이전에는 불가능했거나 매우 어려웠던 것들이 가능해졌다는 점입니다.

7부: 언제 ClickHouse Cloud를 고려해야 하는가

모든 상황에서 Cloud가 정답은 아닙니다. 다음은 의사결정을 위한 가이드라인입니다.

OSS가 여전히 좋은 선택인 경우

  • 데이터 규모가 단일 서버나 소규모 클러스터(2-4샤드)로 충분한 경우
  • 워크로드가 예측 가능하고 안정적인 경우
  • ClickHouse 전문 인력이 이미 있는 경우
  • 데이터 소버린티/컴플라이언스 요구로 퍼블릭 클라우드 사용이 제한되는 경우
  • 비용 구조에서 인건비 비중이 낮은 경우

Cloud 전환을 고려해야 하는 시그널

  • 샤드 추가/리샤딩 작업이 분기마다 발생하거나 더 빈번해지는 경우
  • Keeper/ZooKeeper 관련 장애가 반복되는 경우
  • 운영 팀이 ClickHouse 클러스터 관리에 과도한 시간을 쓰는 경우
  • 트래픽 변동이 심해 오버프로비저닝이 불가피한 경우
  • 새로운 기능(예: Lightweight Updates, compute-compute separation)이 필요한 경우

BYOC(Bring Your Own Cloud) 옵션

데이터가 자체 VPC를 벗어나면 안 되지만 운영 부담은 줄이고 싶다면, ClickHouse의 BYOC 옵션을 고려할 수 있습니다. 고객의 AWS/GCP/Azure 계정 내에서 ClickHouse Cloud를 운영하되, 관리는 ClickHouse Inc.가 담당합니다.

결론: 복잡성을 어디서 감당할 것인가

ClickHouse OSS의 ReplicatedMergeTree와 Distributed 엔진은 강력한 도구입니다. 제대로 이해하고 운영하면 페타바이트급 데이터를 처리하는 대규모 분석 시스템을 구축할 수 있습니다.

하지만 "제대로 운영한다"는 것의 비용을 솔직하게 인정해야 합니다. Keeper 클러스터 관리, 샤딩 전략 설계, 노드 추가 시 마이그레이션, 장애 대응, 버전 업그레이드... 이 모든 것이 쌓이면 상당한 인력과 시간이 필요합니다.

SharedMergeTree와 ClickHouse Cloud가 등장한 이유는 바로 여기에 있습니다. Shared-Nothing 아키텍처의 본질적인 복잡성을 Shared-Everything 아키텍처로 해결하고, 그 운영을 서비스 제공자가 대신 처리해주는 것입니다.

어떤 선택이 맞는지는 조직의 상황에 따라 다릅니다. 중요한 것은 각 선택지의 실제 비용과 트레이드오프를 명확히 이해하고, 장기적 관점에서 결정을 내리는 것입니다.

데이터 인프라는 비즈니스의 기반입니다. 그 기반을 직접 쌓고 관리할 것인지, 검증된 파트너에게 맡기고 비즈니스 가치 창출에 집중할 것인지—이것이 오늘날 데이터 엔지니어와 아키텍트가 답해야 할 핵심 질문입니다.

참고 자료

  • ClickHouse 공식 문서 - Table shards and replicas
  • SharedMergeTree 소개
  • ClickHouse Cloud boosts performance with SharedMergeTree
  • Poizon의 ClickHouse Cloud 마이그레이션 사례
  • Contentsquare의 클러스터 확장 경험
  • Altinity Knowledge Base