[ClickHouse Deep Dive] 1편: 대용량 로그 분석을 위한 ClickHouse 아키텍처 및 저장 구조
최근 대규모 인프라와 서비스에서 발생하는 페타바이트급 대용량 로그 및 이벤트 데이터를 실시간으로 분석하기 위해 ClickHouse를 도입하는 기업이 늘고 있습니다. ClickHouse는 극단적인 속도를 자랑하는 오픈소스 컬럼 지향(Column-oriented) OLAP 데이터베이스 관리 시스템입니다.
이번 글에서는 ClickHouse 엔진의 전반적인 아키텍처를 살펴보고, 데이터가 디스크에 실제로 어떤 메커니즘으로 삽입되고 저장되는지 Storage Layer의 내부 동작을 깊이 있게 파헤쳐 보겠습니다.
1. ClickHouse 메인 아키텍처 개요
ClickHouse 엔진은 크게 세 개의 핵심 레이어(Layer)와 이를 보조하는 컴포넌트들로 구성되어 있습니다.
- Query Processing Layer: 쿼리 실행 계획을 수립하고, 멀티코어 병렬 처리 및 CPU 특화 명령어를 통해 쿼리를 초고속으로 연산하는 레이어입니다.
- Storage Layer: 테이블 엔진의 데이터 저장 포맷과 물리적 위치를 정의하고 관리하는 레이어입니다.
- Integration Layer: 외부 시스템(Kafka, S3, MySQL 등)과의 연결 및 데이터 교환을 담당하는 레이어입니다.
이 외에도 사용자가 데이터베이스에 접근하여 명령을 내릴 수 있도록 통로 역할을 하는 Access Layer가 존재합니다. Access Layer는 데이터베이스 서버와 애플리케이션 사이의 연결을 관리하는 부분이며, 사용자 세션 관리를 수행합니다. 구체적으로는 권한 확인 및 상태 유지 등의 역할을 담당하며, 통신 방식으로는 HTTP, 비네이티브 TCP, MySQL 프로토콜 등 다양한 방식을 지원하는 것이 특징입니다.
또한 Thread pools, Caching, Role-based Access Control(RBAC), Backups, Monitoring 등의 기능들이 각각 독립적인 컴포넌트로 분리되어 견고하게 구현되어 있습니다.
2. Storage Layer와 테이블 엔진
Storage Layer는 데이터를 어떻게 저장하고 활용할 것인지를 결정하는 다양한 테이블 엔진을 포함하고 있습니다. ClickHouse의 테이블 엔진은 크게 세 가지 계열로 분류할 수 있습니다.
① MergeTree 계열 엔진 (기본 스토리지 포맷)
ClickHouse의 가장 핵심이 되는 엔진으로, 디스크 기반의 영구 저장을 지원하며 정렬된 파트를 백그라운드에서 병합하는 방식을 사용합니다. 뒤에서 자세히 다룰 LSM-Tree(로그 구조 병합 트리)의 아이디어에서 영감을 받아 설계되었습니다.
기본적인 MergeTree 외에도 중복을 제거하는 ReplacingMergeTree, 백그라운드에서 누적 합계를 계산하는 SummingMergeTree, 미리 정의된 집계 상태를 저장하는 AggregatingMergeTree 등이 존재하여 목적에 맞게 선택할 수 있습니다.
② 특수 목적(Special-purpose) 엔진
쿼리 속도 향상, 캐싱, 분산 처리를 위해 특수하게 설계된 엔진입니다.
- Dictionary Engine: 인메모리 Key-Value 캐시 역할을 수행합니다. 외부 DB나 내부 테이블을 주기적으로 쿼리하여 결과를 메모리에 저장하며, 약간의 만료된 데이터(stale data)를 허용하는 대신 매우 빠른 조회 속도를 보장합니다.
- Memory Engine: 완전한 메모리 기반의 임시 테이블입니다. 세션 중간 결과를 저장하는 용도로 쓰이며 매우 빠르지만, 서버 재시작 시 데이터가 사라집니다.
- Distributed Engine: 여러 노드에 분산되어 있는 동일한 스키마의 테이블들을 샤딩하고, 쿼리를 병렬로 실행할 수 있게 돕는 엔진입니다.
③ Virtual 엔진
외부 데이터 소스와 별도의 ETL 과정 없이 직접 연결하여 가상 테이블 형태로 양방향 데이터 교환을 가능하게 하는 엔진입니다. 연동 대상에 따라 아래와 같은 엔진들이 제공됩니다.
| 연동 대상 | 엔진 종류 | 설명 |
|---|---|---|
| RDBMS | MySQL, PostgreSQL |
외부 RDB를 ClickHouse에서 로컬 테이블처럼 조회 |
| Message Queue | Kafka, RabbitMQ |
실시간 스트리밍 데이터를 실시간으로 Ingestion |
| Key/Value Store | Redis |
KV 저장소 기반의 빠른 데이터 조회 |
| Data Lake | Iceberg, DeltaLake, Hudi |
오브젝트 스토리지 기반의 데이터 레이크 연동 |
| Cloud Storage | S3, GCS |
클라우드 파일 저장소의 데이터를 직접 조회 |
3. MergeTree의 On-Disk 포맷과 데이터 삽입 메커니즘
이제 ClickHouse의 기본 저장 구조인 MergeTree 엔진이 디스크 상에서 데이터를 어떻게 관리하는지 물리적 구조를 살펴보겠습니다.
불변(Immutable)의 단위, Part
MergeTree 테이블의 데이터는 여러 개의 불변한 **'part'**들로 구성됩니다. 하나의 part는 한 번 디스크에 쓰이면 절대로 수정되지 않습니다. 새로운 데이터가 들어오면 기존 파일에 내용을 추가하거나 변경하는 대신, 완전히 새로운 part 하나를 독립적으로 생성합니다.
즉, 사용자가 INSERT 문을 실행할 때마다 하나의 물리적인 저장 블록(part)이 만들어집니다. 한 번에 1,000행을 삽입하든 1억 행을 삽입하든 상관없이, 단일 insert batch 단위로 하나의 part가 생성되는 구조입니다.
각 part는 내부적으로 데이터 스키마, 압축 정보, 인덱스 정보, 정렬 키 정보 등 데이터를 읽는 데 필요한 모든 메타데이터를 자체적으로 포함하고 있습니다(Self-contained 특성). 중앙 카탈로그를 거칠 필요가 없기 때문에 병렬 읽기, 데이터 복제 및 이동이 매우 자유롭다는 장점을 가집니다.
백그라운드 머지(Merge Job)
데이터 삽입이 빈번하게 일어나면 디스크 상에 수천 개의 작은 part 파일들이 생겨나게 됩니다. part의 개수가 너무 많아지면 데이터를 읽을 때 파일 스캔 오버헤드가 급증하여 전체적인 쿼리 성능이 떨어집니다.
이를 방지하기 위해 ClickHouse는 백그라운드 스레드를 통해 주기적으로 작은 part들을 하나로 합쳐주는 Merge Job을 수행합니다. 병합 결과물은 하나의 큰 part로 재탄생하며, 기본적으로 목표 크기는 약 150GB 내외로 관리됩니다. (이 상한선은 max_bytes_to_merge_at_max_space_in_pool 설정을 통해 조정할 수 있습니다.)
각 part 내부는 Primary Key 순서로 정렬되어 있기 때문에, 병합 시에는 효율적인 k-way merge sort 알고리즘이 사용됩니다. 병합이 완료되면 기존의 작은 part들은 즉시 지워지지 않고 inactive 상태로 표시됩니다. 현재 해당 part를 읽고 있는 다른 쿼리가 있을 수 있기 때문입니다. 참조 카운트(refcount)가 0이 되는 순간, 디스크에서 안전하게 물리적으로 삭제되며, 이를 통해 Snapshot Isolation(MVCC)을 자연스럽게 구현합니다.
4. 효율적인 데이터 삽입을 위한 두 가지 모드
앞서 설명했듯 빈번한 INSERT는 무수한 파트를 생성하여 시스템에 심각한 병목을 유발합니다. ClickHouse는 이 문제를 제어하기 위해 두 가지 삽입 방식을 제공합니다.
① 동기식 삽입 (Synchronous Insert)
ClickHouse의 기본 모드입니다. 각 INSERT 요청이 들어올 때마다 클라이언트의 호출에 맞춰 즉시 새로운 part를 생성합니다. 따라서 클라이언트 애플리케이션이 아주 작은 단위로 잦은 INSERT를 발생시키면 CPU 및 I/O 부하가 폭발적으로 상승합니다. ClickHouse 공식 문서에서도 안정적인 운영을 위해 최소 2만 행 정도씩 데이터를 애플리케이션 단에서 모아서 한 번에 INSERT 할 것을 강력히 권장하고 있습니다.
② 비동기식 삽입 (Asynchronous Insert)
수천 개의 서버에서 실시간으로 몇 행씩 로그를 던지는 모니터링/Observability 환경을 위해 고안된 모드입니다. 개별 INSERT 크기는 매우 작지만 시스템 지연(Latency)은 짧아야 할 때 유용하게 작동합니다.
비동기 모드가 활성화되면 ClickHouse는 여러 클라이언트의 요청을 즉시 디스크에 쓰지 않고 **메모리 버퍼(Buffer)**에 먼저 쌓습니다. 이후 아래 설정된 조건 중 하나라도 만족되는 순간 취합된 데이터를 하나의 큰 part로 묶어 디스크에 Flush 합니다.
SET async_insert = 1;
SET async_insert_max_data_size = '1M'; -- 버퍼 크기가 1MB에 도달했을 때
SET async_insert_busy_timeout_ms = 1000; -- 혹은 타임아웃(1초)이 지났을 때
주의할 점: 데이터가 메모리 버퍼에 머무는 동안 서버에 장애가 발생하면 해당 데이터가 손실될 가능성이 존재하므로, 아키텍처 설계 시 데이터의 중요도에 따라 선택해야 합니다.
5. ClickHouse MergeTree vs 전통적 LSM-Tree 구조 비교
ClickHouse의 MergeTree는 append-only 방식으로 디스크의 순차 쓰기(Sequential Write) 성능을 극대화한다는 점에서 RocksDB, Cassandra, MongoDB WiredTiger 등에서 사용하는 LSM-Tree와 유사해 보이지만, 결정적인 구조적 차이점을 가지고 있습니다.
전통적인 LSM-Tree는 데이터 유실 방지를 위한 WAL(Write-Ahead Log)에 먼저 기록한 뒤, 메모리 트리 구조인 MemTable에 데이터를 정렬하여 유지합니다. 이후 MemTable이 가득 차면 불변의 SSTable 파일로 디스크에 Flush하며, 이 파일들을 층(Level) 구조로 배치하여 계층적 컴팩션(Compaction)을 수행합니다. 또한, 데이터를 지울 때도 즉시 지우지 않고 'Tombstone(삭제 마커)'을 남겼다가 컴팩션 시점에 물리적으로 제거합니다.
반면 ClickHouse의 MergeTree는 복잡한 계층(Level) 개념을 없애고 모든 part를 평면 구조로 관리합니다. 크기나 생성 시점에 구애받지 않고 유연하게 백그라운드 병합을 수행하므로 스케줄링이 단순하고 빠릅니다. 또한, 별도의 WAL을 유지하지 않고 데이터를 곧바로 part 형태로 디스크에 다이렉트로 기록하며, **"파일 쓰기 성공 = 데이터 커밋 성공"**으로 간주하여 오버헤드를 극단적으로 줄였습니다.
삭제 처리 역시 전통적인 Tombstone 방식 대신, 비동기 큐를 통해 파트를 통째로 재작성하는 Mutation 방식이나, 압축 비트맵 플래그를 활용해 SELECT 시 필터링하고 추후 merge 때 물리적으로 털어내는 Lightweight Delete 방식을 채택하고 있습니다.
| 항목 | ClickHouse MergeTree | 일반적인 LSM-Tree |
|---|---|---|
| 구조적 계층 | 모든 part가 동등함 (비계층적 평면 구조) | 계층적 구조 (Level 0, Level 1...) |
| 병합(Merge) 기준 | 정책에 따라 유연하게 part끼리 병합 | 같은 레벨 단위의 계층적 컴팩션 수행 |
| 쓰기 경로 (Write Path) | WAL 없이 직접 디스크에 part 기록 | WAL 기록 ➡️ MemTable 적재 ➡️ 디스크 Flush |
| 삭제 처리 방식 | Mutation 기반 재작성 또는 비트맵 필터링 | Tombstone(삭제 마커) 삽입 후 컴팩션 시 삭제 |
| 트레이드 오프 | 구조가 단순하며 읽기/쓰기가 극도로 빠르나, 크래시 발생 시 아주 미미한 손실 가능성 감수 | WAL 및 버전을 명확히 관리하여 높은 안정성을 보장하나 구조적 오버헤드가 있음 |
마치며
이번 글에서는 ClickHouse의 기본 뼈대가 되는 아키텍처와 함께 MergeTree 엔진의 물리적인 데이터 삽입 메커니즘을 전통적인 LSM-Tree 구조와 비교하며 알아보았습니다. 대용량 로그 분석 시스템을 설계할 때 ClickHouse가 왜 압도적인 성능 우위를 가질 수 있는지 스토리지 레이어의 설계 사상에서 그 힌트를 찾을 수 있었습니다.
다음 2편에서는 디스크에 저장된 이 part 파일들의 더 세부적인 내부 물리 파일 구조(*.bin, *.mrk3)와 함께, 수십억 건의 데이터 중 필요한 데이터만 칼같이 골라내는 **Data Pruning(Primary Key 및 Skipping Index)**의 비밀에 대해 자세히 살펴보겠습니다.
References (참고 문헌)
- Schulze, A., Krotov, Artem., & ClickHouse Team. (2024). ClickHouse: Lightning Fast Analytics for Everyone. In Proceedings of the 2024 International Conference on Management of Data (SIGMOD '24).
- ClickHouse Official Documentation: The MergeTree Engine Family