Ken
ClickHouse OSS를 운영하다 보면 예상치 못한 복제 문제를 마주하게 됩니다. 특히 Multi-IDC 환경에서 대용량 데이터를 처리할 때, 소규모 테이블은 정상적으로 동작하는데 대규모 테이블에서만 데이터 불일치가 발생하는 현상을 경험할 수 있습니다. 이 글에서는 실제 프로덕션 환경에서 발생한 복제 불일치 문제의 근본 원인을 분석해본 글입니다.
- 문제 상황
- 근본 원인 분석
- ClickHouse 복제 메커니즘의 이해
- ReplicatedMergeTree 복제 흐름
- 대용량 테이블에서만 문제가 발생하는 이유
- 테이블 크기에 따른 복제 지연 비교
- Multi-IDC 환경의 추가적인 복잡성
- 문제 진단 방법
- system.replication_queue 테이블 분석
- system.replicated_fetches 테이블로 진행 상황 모니터링
- system.replicas 테이블로 전체 상태 확인
- 진단 플로우차트
- 해결 방안
- Option 1: insert_quorum 활성화 (On-premise 환경)
- Option 2: 복제 성능 튜닝
- Option 3: ClickHouse Cloud로 마이그레이션 (권장)
- 해결 방안 비교
- 마이그레이션 시 고려사항
- 결론
- 참고 자료
문제 상황
한 기업에서 두 개의 IDC에 걸친 stretched 구성으로 ClickHouse 클러스터를 운영하고 있었습니다. Hadoop에서 ClickHouse로 대용량 테이블을 복사하는 과정에서 다음과 같은 문제가 발생했습니다.
파티션 레벨에서 row count가 일치하지 않는 현상이 나타났으며, 흥미롭게도 소규모 테이블은 정상적으로 복제되었지만 대규모 테이블에서만 이 문제가 반복되었습니다. 임시 해결책으로 작은 파티션 단위로 나눠서 ingestion하는 workaround를 적용했지만, 근본적인 해결책은 아니었습니다.
근본 원인 분석
ClickHouse 복제 메커니즘의 이해
ClickHouse의 ReplicatedMergeTree 엔진은 비동기 복제(Asynchronous Replication) 방식을 사용합니다. 이 방식의 핵심 특성을 이해하는 것이 문제 해결의 첫 걸음입니다.
기본적으로 insert_quorum 설정값은 0으로 되어 있습니다. 이는 INSERT 쿼리가 실행될 때 해당 쿼리를 처리하는 레플리카만 즉시 데이터를 저장하고, 다른 레플리카들은 replication_queue를 통해 비동기적으로 데이터를 가져온다는 의미입니다.
ReplicatedMergeTree 복제 흐름
아래 다이어그램은 기본 설정(insert_quorum=0)에서의 복제 흐름을 보여줍니다.
대용량 테이블에서만 문제가 발생하는 이유
대용량 ingestion 시에는 생성되는 part의 크기가 크고 개수도 많아집니다. 이로 인해 다른 레플리카로의 데이터 전송(fetch) 작업이 오래 걸리게 됩니다. 복제 큐에 작업이 쌓이는 동안 레플리카 간 데이터 불일치 상태가 오랫동안 지속되며, 특히 IDC 간 네트워크 지연이 있는 stretched 구성에서는 이 문제가 더욱 심화됩니다.
반면 작은 테이블의 경우 part 크기가 작고 복제가 빠르게 완료되기 때문에 일시적인 불일치가 눈에 띄지 않습니다. 쿼리 시점에 이미 복제가 완료되어 있을 가능성이 높기 때문입니다.
테이블 크기에 따른 복제 지연 비교
Multi-IDC 환경의 추가적인 복잡성
두 개의 IDC에 걸친 stretched 구성은 추가적인 도전 과제를 안고 있습니다. ZooKeeper(또는 ClickHouse Keeper)는 네트워크 지연에 매우 민감하여, IDC 간 지연이 커지면 coordination 성능이 저하됩니다. ClickHouse 복제 자체는 비동기이지만, 메타데이터 동기화는 Keeper를 통해 이루어지므로 높은 지연 환경에서는 전체적인 복제 성능에 영향을 미칩니다.
문제 진단 방법
system.replication_queue 테이블 분석
복제 상태를 확인하는 가장 기본적인 방법은 system.replication_queue 테이블을 조회하는 것입니다. 이 테이블에서는 복제 대기 중인 작업들을 확인할 수 있으며, 특히 type이 'GET_PART'인 작업들이 다른 레플리카로부터 part를 가져오는 작업입니다.
SELECT
database,
table,
type,
count() AS task_count,
max(num_tries) AS max_tries,
countIf(last_exception != '') AS error_count,
countIf(is_currently_executing) AS executing_count
FROM system.replication_queue
GROUP BY database, table, type
ORDER BY task_count DESC;
위 쿼리를 실행하면 각 테이블별로 복제 큐에 쌓인 작업 수와 에러 발생 현황을 한눈에 파악할 수 있습니다. max_tries 값이 높거나 error_count가 0이 아니라면 복제 과정에서 문제가 있음을 의미합니다.
system.replicated_fetches 테이블로 진행 상황 모니터링
현재 진행 중인 fetch 작업을 실시간으로 확인하려면 system.replicated_fetches 테이블을 조회합니다.
SELECT
database,
table,
result_part_name,
round(progress * 100, 2) AS progress_percent,
formatReadableSize(total_size_bytes_compressed) AS total_size,
formatReadableSize(bytes_read_compressed) AS bytes_read,
round(elapsed, 1) AS elapsed_seconds
FROM system.replicated_fetches
ORDER BY elapsed DESC;
이 쿼리를 통해 어떤 part가 얼마나 오래 걸리고 있는지, 진행률은 어느 정도인지 파악할 수 있습니다. 네트워크 문제나 디스크 I/O 병목이 있다면 여기서 확인됩니다.
system.replicas 테이블로 전체 상태 확인
클러스터 전체의 복제 건강 상태를 확인하는 종합적인 쿼리입니다.
SELECT
database,
table,
is_leader,
is_readonly,
is_session_expired,
queue_size,
inserts_in_queue,
merges_in_queue,
log_max_index - log_pointer AS replication_lag
FROM system.replicas
WHERE
is_readonly
OR is_session_expired
OR queue_size > 20
OR inserts_in_queue > 10
OR (log_max_index - log_pointer) > 10;
이 쿼리가 결과를 반환하지 않는다면 복제 상태가 양호한 것입니다. is_readonly가 true이거나 replication_lag이 크다면 즉각적인 조치가 필요합니다.
진단 플로우차트
해결 방안
Option 1: insert_quorum 활성화 (On-premise 환경)
가장 직접적인 해결책은 쿼럼 기반 쓰기를 활성화하는 것입니다.
SET insert_quorum = 2; -- 레플리카 수에 맞게 조정
SET insert_quorum_timeout = 60000; -- 밀리초 단위, 필요에 따라 조정
이 설정을 활성화하면 INSERT 쿼리는 지정된 수의 레플리카가 모두 데이터를 저장한 후에야 성공을 반환합니다. 모든 레플리카가 데이터를 가지게 되므로 일관성이 보장되며, 복제 지연으로 인한 불일치 문제가 원천적으로 해결됩니다.
다만 이 방식에는 트레이드오프가 있습니다. Ingestion 시간이 증가하며, 레플리카 중 하나라도 응답하지 않으면 INSERT가 실패할 수 있습니다. 특히 IDC 간 지연이 큰 환경에서는 성능 저하가 심할 수 있습니다.
읽기 작업에서도 일관성을 보장하려면 select_sequential_consistency 설정을 함께 사용할 수 있습니다.
SET select_sequential_consistency = 1;
이 설정은 insert_quorum으로 쓰여진 데이터만 읽도록 보장합니다.
Option 2: 복제 성능 튜닝
insert_quorum을 사용하지 않고 비동기 복제의 성능을 개선하는 방법도 있습니다.
background_fetches_pool_size 설정을 늘려 동시에 더 많은 fetch 작업이 진행되도록 할 수 있습니다. 기본값은 8이며, 필요에 따라 16이나 24로 늘릴 수 있습니다. 또한 max_replicated_fetches_network_bandwidth 설정으로 네트워크 대역폭 사용을 조절할 수 있으며, 복제 관련 HTTP 타임아웃 설정을 조정하여 네트워크 불안정 상황에 대응할 수 있습니다.
그러나 이 방법은 복제 속도를 개선할 뿐, 일시적인 불일치 자체를 방지하지는 못합니다.
Option 3: ClickHouse Cloud로 마이그레이션 (권장)
근본적인 해결책은 ClickHouse Cloud의 SharedMergeTree 엔진을 사용하는 것입니다. SharedMergeTree는 기존 ReplicatedMergeTree와는 완전히 다른 아키텍처를 가지고 있습니다.
SharedMergeTree에서 모든 데이터는 S3, GCS, Azure Blob Storage 같은 공유 Object Storage에 저장됩니다. 컴퓨트 노드들은 동일한 저장소의 데이터에 직접 접근하므로, 레플리카 간 part 복제라는 개념 자체가 없습니다. 메타데이터만 ClickHouse Keeper를 통해 동기화되며, 이는 매우 빠르게 이루어집니다.
이 아키텍처의 핵심적인 장점은 insert_quorum이나 insert_quorum_parallel 같은 설정이 불필요하다는 것입니다. SharedMergeTree에서 모든 INSERT는 기본적으로 쿼럼 insert입니다. 데이터가 공유 저장소에 쓰여지면 모든 컴퓨트 노드가 즉시 접근할 수 있기 때문입니다.
이로 인해 복제 지연으로 인한 데이터 불일치 문제가 원천적으로 해결되며, system.replication_queue나 system.replicated_fetches 테이블 자체가 존재하지 않습니다. 레플리카 간 데이터 전송이 없으므로 네트워크 대역폭 문제도 해소됩니다.
해결 방안 비교
마이그레이션 시 고려사항
On-premise 환경에서 ClickHouse Cloud로 마이그레이션을 검토할 때는 몇 가지 사항을 고려해야 합니다.
데이터 이전의 경우 ClickPipes나 S3를 통한 벌크 로딩을 활용할 수 있습니다. 쿼리 호환성 측면에서 대부분의 쿼리가 수정 없이 동작하지만, 시스템 테이블 관련 쿼리는 일부 수정이 필요할 수 있습니다. 비용 측면에서 인프라 관리 비용, 운영 인력 비용, 복제 문제 해결에 소요되는 시간 등을 종합적으로 고려했을 때 클라우드 전환이 유리한 경우가 많습니다.
결론
ClickHouse에서 대규모 테이블의 복제 불일치 문제는 비동기 복제의 본질적인 특성에서 비롯됩니다. insert_quorum 설정으로 단기적인 해결이 가능하지만, 성능 트레이드오프가 있습니다.
근본적인 해결을 원한다면 SharedMergeTree 기반의 ClickHouse Cloud 아키텍처가 최선의 선택입니다. Shared storage 기반 아키텍처는 복제 지연이라는 개념 자체를 제거하여, 대용량 데이터 처리에서도 일관된 성능과 데이터 정합성을 보장합니다.
데이터 일관성 문제로 고민하고 있다면, 클러스터 복잡도를 높이는 해결책보다는 아키텍처 자체를 단순화하는 방향을 고려해 보시기 바랍니다.
참고 자료
- ClickHouse Replication Queue System Table
- ClickHouse Replicated Fetches System Table
- SharedMergeTree Table Engine
- ClickHouse Cloud Architecture
- Altinity Knowledge Base - Replication Queue