프로덕션에서 ClickHouse 운영하기: 20억 행 규모의 수집, 병합, 비용
ClickHouse를 프로덕션에서 운영한 경험: 스키마 설계, 쿼리 최적화, 그리고 1억 개 이상의 디바이스 이벤트에서 서브초 단위 분석을 달성하는 방법.
tracio.ai의 분석 계층을 구축하기 시작했을 때, 우리는 특정 워크로드를 감당할 수 있는 데이터베이스가 필요했다. 초당 50,000건의 디바이스 식별 이벤트를 수집하고, 20억 개 이상의 행을 저장하며, 분석 쿼리에 1초 이내로 응답해야 했다. 우리는 PostgreSQL(이 규모의 집계에는 너무 느림), Elasticsearch(시계열 분석에는 너무 비쌈), 그리고 ClickHouse를 평가했다. ClickHouse가 압도적으로 승리했다.
ClickHouse를 선택한 이유
ClickHouse는 실시간 분석을 위해 설계된 컬럼 지향 OLAP 데이터베이스다. 우리 워크로드에서 가장 큰 장점은 각 쿼리에 필요한 컬럼만 읽는다는 점이다. 사기 분석가가 "지난 7일간 국가별 사기 비율을 보여줘"라고 요청하면, ClickHouse는 이벤트 테이블의 나머지 40개 이상의 컬럼을 무시하고 country, timestamp, risk_score 컬럼만 읽는다. 20억 행 테이블에서 이는 I/O를 95% 줄인다.
ClickHouse는 데이터도 매우 잘 압축한다. 우리의 20억 행 이벤트 테이블은 디스크에서 340GB를 차지한다 — 압축하지 않으면 행당 1.2KB인 데 비해 압축 시 행당 약 170바이트다. 7:1의 압축률은 더 많은 데이터가 메모리에 들어간다는 뜻이며, 이는 곧 더 빠른 쿼리로 직결된다.
스키마 설계
우리의 기본 테이블은 식별 이벤트당 한 행을 저장한다:
이 테이블은 MergeTree 엔진을 사용하며 (workspace_id, toDate(timestamp), visitor_hash) 기준으로 정렬된다. 이 정렬은 매우 중요하다 — 워크스페이스와 날짜 범위로 필터링된 쿼리가 최소한의 데이터만 읽는다는 뜻이기 때문이다. visitor_hash 컬럼은 보조 인덱스 없이도 visitor ID로 빠른 조회를 가능하게 한다.
country, device_type, browser_family, os_family 컬럼은 서로 다른 값이 10,000개 미만이므로 LowCardinality(String)를 선택했다. ClickHouse는 LowCardinality 컬럼을 딕셔너리 인코딩된 정수로 저장하여, 일반 문자열 대비 저장 공간을 80% 줄이고 GROUP BY 연산을 가속한다.
샤딩 전략
우리는 workspace_id의 해시를 사용해 이벤트 테이블을 6개 노드에 샤딩한다. 이렇게 하면 특정 고객의 모든 이벤트가 동일한 샤드에 위치하므로, 대부분의 쿼리(workspace_id로 필터링됨)가 단일 샤드에 도달한다. 크로스 샤드 쿼리는 내부 분석에만 필요하다.
각 샤드는 고가용성을 위해 2개의 복제본을 가진다. 복제는 ClickHouse의 내장 ReplicatedMergeTree 엔진과 ZooKeeper 조정을 사용한다. 페일오버는 자동이다 — 샤드가 다운되면 클라이언트 측 변경 없이 쿼리가 복제본으로 라우팅된다.
수집 파이프라인
이벤트는 Kafka 토픽에서, 삽입을 배치 처리하는 커스텀 Go 서비스를 거쳐 ClickHouse로 흐른다. 우리는 500ms마다 10,000행 단위로 배치 삽입한다 — 이는 수집 지연(서브초)과 삽입 효율(ClickHouse는 큰 배치에서 가장 좋은 성능을 낸다) 사이의 균형을 맞춘다.
수집 서비스는 백프레셔를 우아하게 처리한다. ClickHouse가 삽입을 받아들이는 속도가 느려지면(병합 중이거나 쿼리 부하가 높을 때), 서비스는 최대 100만 개의 이벤트를 메모리에 버퍼링하고 Kafka 컨슈머에 백프레셔를 가한다. 18개월간의 프로덕션 운영에서 우리는 단 한 건의 이벤트도 잃지 않았다.
쿼리 최적화
Materialized View
일반적인 대시보드 쿼리에는 데이터를 사전 집계하는 materialized view를 사용한다. 예를 들어 우리의 사기 비율 대시보드는 fraud_detected 카운트를 워크스페이스, 국가, 시간 단위로 집계하는 materialized view에서 읽는다. 이 뷰는 해당 쿼리에서 스캔하는 데이터를 20억 행에서 500만 행으로 줄인다.
Projection 정렬
ClickHouse의 projection을 사용하면 데이터를 중복하지 않고도 테이블에 대한 대체 정렬 순서를 정의할 수 있다. 우리는 방문자 타임라인 쿼리를 위해 (workspace_id, visitor_hash, timestamp)로 정렬된 projection을 추가했다. projection이 없으면 이런 쿼리는 전체 날짜 범위를 스캔했다. 있으면 대상 방문자를 포함하는 블록만 읽는다.
근사 함수
정확한 카운트가 중요하지 않은 대시보드 쿼리에는 ClickHouse의 근사 함수를 사용한다: 고유 카운트에는 uniqCombined(오차 2%, uniqExact보다 10배 빠름), 백분위수 계산에는 quantileTDigest를 쓴다. 사기 분석 대시보드는 근사 함수만 사용하며, 덕분에 모든 대시보드 쿼리가 200ms 이내로 유지된다.
성능 수치
다음은 우리의 20억 행 프로덕션 클러스터에서 대표적인 쿼리 벤치마크다:
지난 7일간 국가별 사기 비율: 120ms. 방문자 타임라인(이벤트 50건): 8ms. 지난 30일간 일별 고유 방문자: 340ms. 지난 24시간 위험 점수 분포: 95ms. 지난 30일간 이벤트 수 기준 상위 100개 디바이스: 210ms.
이 수치들은 애플리케이션 서버에서 ClickHouse 클러스터까지의 네트워크 왕복 시간을 포함한다. 순수 쿼리 실행 시간은 일반적으로 30~50% 더 낮다.
운영 교훈
교훈 1: 병합 지연을 모니터링하라
ClickHouse의 MergeTree 엔진은 작은 데이터 파트를 지속적으로 더 큰 파트로 병합한다. 병합이 뒤처지면(높은 삽입 속도나 디스크 I/O 경합 때문에) 쿼리가 더 많은 파트를 스캔해야 하므로 성능이 저하된다. 우리는 파티션당 파트 수를 모니터링하고 300개를 초과하면 알림을 보낸다.
교훈 2: 대규모 ALTER TABLE 작업을 피하라
ClickHouse에서 20억 행 테이블에 컬럼을 추가하는 것은 즉시 완료된다(메타데이터만 변경되기 때문). 그러나 컬럼 타입을 변경하려면 모든 데이터 파트를 다시 써야 하며 — 이 작업은 우리 클러스터에서 6시간이 걸렸다. 이제 우리는 스키마를 append-only로 취급한다: 새 컬럼은 자유롭게 추가하되, 타입 변경은 마이그레이션 테이블을 거친다.
교훈 3: TTL은 신중하게
ClickHouse는 TTL을 통한 자동 데이터 만료를 지원한다. 우리는 이벤트 테이블에 90일 TTL을 설정했다. 함정: TTL 삭제는 병합 중에 일어나므로, 삭제된 데이터가 TTL 만료 후 몇 시간 또는 며칠 동안 남아 있을 수 있다. 규정 준수가 중요한 삭제의 경우, 우리는 명시적인 ALTER TABLE DELETE 쿼리를 스케줄에 따라 실행한다.
비용
우리의 6개 노드 ClickHouse 클러스터(각 노드: 32 vCPU, 128GB RAM, 2TB NVMe)는 베어메탈 호스팅에서 월 약 8,400달러의 비용이 든다. 이 클러스터는 90일 보존 기간으로 20억 행을 저장하고, 초당 5만 건의 삽입과 200개의 동시 대시보드 쿼리를 처리한다. 저장 이벤트당 비용은 0.0000042달러로 — 매니지드 클라우드 데이터베이스의 동등한 분석보다 몇 자릿수나 저렴하다.