[ClickHouse Deep Dive] 3편: 극한의 CPU 성능을 쥐어짜는 쿼리 엔진의 비밀 (SIMD, 멀티코어, JIT 컴파일)
1편과 2편에서는 ClickHouse의 스토리지 구조와 필요한 데이터 블록만 칼같이 골라내는 Data Pruning(가지치기) 기술을 알아보았습니다. 하지만 인덱스를 통해 최소한의 데이터만 메모리에 올렸다고 해도, 그 데이터가 수억, 수십억 건에 달한다면 연산 엔진의 효율성이 전체 쿼리 속도를 결정짓게 됩니다.
ClickHouse는 성능을 위해서라면 가능한 락을 걸지 않는(Latch-free) 구조와 함께, 현대 CPU의 하드웨어 잠재력을 극한까지 끌어쓰도록 설계되었습니다. 이번 3편에서는 ClickHouse의 핵심 심장부인 Query Processing Layer가 어떻게 SIMD 벡터화, 하이퍼 병렬 파이프라인, JIT 컴파일을 활용해 압도적인 연산 속도를 만들어내는지 그 비밀을 파헤쳐 봅니다.
1. SIMD와 벡터화 연산 (Vectorized Execution)
전통적인 RDBMS(OLTP 시스템)는 쿼리를 처리할 때 **'화산 모델(Volcano Model)'**이라 불리는 행 기반(Row-oriented) 방식을 주로 사용합니다. 루프를 돌며 한 번에 한 행(Row)씩 메서드를 호출하여 데이터를 처리하는 방식입니다. 이 방식은 메모리 아키텍처 관점에서 CPU 캐시 미스(Cache Miss)를 자주 유발하고, 현대 CPU의 강력한 병렬 연산 기능을 활용하지 못한다는 단점이 있습니다.
반면 ClickHouse는 데이터 청크를 한 번에 처리하는 **벡터화 방식(Vectorized Execution)**을 채택했습니다. 데이터를 행 단위가 아니라 동일한 데이터 타입을 가진 열(Column) 단위의 **Chunk(덩어리)**로 묶어 메모리에 나란히 배열합니다. 데이터가 메모리상에 연속적으로 밀집해 있기 때문에 CPU 캐시 적재 효율이 극대화됩니다.
이 구조 덕분에 ClickHouse는 SIMD(Single Instruction, Multiple Data) 기술을 완벽하게 활용합니다. SIMD는 CPU가 하나의 명령어로 여러 개의 데이터를 동시에 처리하는 하드웨어 가속 기술입니다.
[일반 연산 (Non-SIMD)] : 데이터 A1 + B1 연산 ➡️ 데이터 A2 + B2 연산 ➡️ 데이터 A3 + B3 연산...
[SIMD 벡터 연산] : 하나의 명령어로 [A1, A2, A3, A4] + [B1, B2, B3, B4]를 한 번에 연산!
ClickHouse는 WHERE 절의 필터링 조건이나 대량의 산술 연산을 수행할 때 이 SIMD 명령어를 사용하여 일반적인 C++ 루프보다 수십 배 빠른 속도로 데이터를 정제합니다. 내부적으로 벡터화는 두 가지 방식으로 구현됩니다.
- 수동 벡터화 (Manual Intrinsics): 프로그래머가 직접 AVX-512, AVX2, SSE 같은 CPU 전용 명령어를 호출하는 저수준 코드를 작성하는 방식입니다. 구현은 매우 어렵지만 상상할 수 있는 최대의 속도를 냅니다.
- 자동 벡터화 (Compiler Auto-Vectorization): Clang/LLVM 컴파일러가 소스 코드의 루프를 분석하여 최적의 SIMD 코드로 자동 변환해 주는 방식입니다.
재미있는 점은, ClickHouse가 컴파일될 때 동일한 연산 코드라도 일반 CPU 버전(Non-vectorized), AVX2 특화 버전, AVX-512 특화 버전 등 여러 개의 'SIMD 연산 커널(특화 함수)'을 동시에 만들어둔다는 것입니다. 그리고 프로그램이 실행되는 런타임 시점에 cpuid 명령을 통해 현재 서버의 CPU가 지원하는 가장 최신의, 가장 빠른 하드웨어 커널을 동적으로 선택하여 실행합니다. 덕분에 15년이 지난 구형 서버부터 최신 하이엔드 서버까지 각 하드웨어의 한계 성능을 알아서 뽑아내게 됩니다.
2. 멀티코어 병렬 파이프라인 (Multi-Core Parallelization)
ClickHouse에 쿼리가 유입되면 SQL > Logical Plan > Physical Plan > Operator Graph 구조의 실행 그래프로 변환됩니다. (예: SCAN > FILTER > GROUP BY > SORT > OUTPUT)
여기서 각 단계를 담당하는 컴포넌트를 연산자(Operator)라고 부르며, 이 연산자 그래프는 컴파일 타임에 서버의 CPU 코어 수(Maximum Worker Threads)와 소스 테이블의 크기를 기반으로 여러 개의 독립적인 **Execution Lane(실행 레인)**으로 분할됩니다.
Lane 1 (Core 1) ──> [SCAN] ──> [FILTER] ──> [LOCAL AGGREGATION] ──┐
Lane 2 (Core 2) ──> [SCAN] ──> [FILTER] ──> [LOCAL AGGREGATION] ──┼─> [GroupStateMerge] (Pipeline Breaker)
Lane 3 (Core 3) ──> [SCAN] ──> [FILTER] ──> [LOCAL AGGREGATION] ──┘
CPU 캐시 지역성(Locality) 극대화
중앙에서 명령을 내리는 무거운 스케줄러가 존재하는 다른 DBMS와 달리, ClickHouse의 워커 스레드들은 스스로 연산자 그래프를 순회하며 자신이 일할 작업을 직접 찾는 독립적인 구조를 취합니다.
특히 같은 스레드가 같은 데이터 Lane을 우선적으로 처리하도록 유도하는데, 이는 CPU 캐시 효율을 극도로 끌어올리기 위함입니다. 예를 들어 1번 레인의 FILTER 작업을 수행한 코어가 연이어 AGGREGATION 작업까지 처리하게 되면, 이미 CPU 코어 캐시에 적재되어 있는 데이터 Chunk 메모리를 그대로 재사용할 수 있게 됩니다. 이로 인해 NUMA(Non-Uniform Memory Access) 아키텍처 환경에서 코어 간 크로스 노드 메모리 접근 오버헤드가 최소화되고 메모리 대역폭 효율이 극대화됩니다.
두 가지 차원의 병렬 처리
- 수평적 병렬 처리: 위 구조처럼 데이터 범위를 여러 레인으로 쪼개어 서로 다른 코어가 겹치지 않는 데이터를 동시에 처리합니다.
- 수직적 병렬 처리 (파이프라인): 한 레인 안에서도 데이터 파이프라인 구조를 활용해
FILTER연산자가 입력 데이터를 읽어 Chunk를 생성하는 동시에, 후속AGGREGATION연산자가 완성된 Chunk를 바로 받아 집계를 처리하는 공장 라인 같은 구조가 동시에 돌아갑니다.
연산 중에 하드웨어 자원의 불균형이나 데이터 치우침(Skew)이 발생하면 Repartition Exchange Operator가 데이터 청크를 다시 균등하게 재분배하여 특정 코어에 병목이 누적되는 것을 방지합니다. 또한, 실행 도중 메모리가 부족해지면 쿼리를 중단하는 대신, 연산자 그래프가 비동기적으로 External Aggregation Operator(디스크 사용형 연산자)로 동적 교체되어 쿼리를 끝까지 완수해 냅니다.
3. JIT 컴파일과 하이퍼 최적화 해시 테이블
ClickHouse의 또 다른 치트키는 LLVM 기반의 JIT(Just-In-Time) 쿼리 컴파일 기술입니다. 쿼리 최적화가 실행 계획을 바꾸는 작업이라면, 쿼리 컴파일은 **'실행 코드 그 자체를 기계어 수준으로 갈아끼우는 작업'**입니다. 성능 이득이 확실한 무거운 연산 표현식이 들어오면 런타임에 최적화된 C++ 바이너리 코드를 직접 생성해 냅니다.
가상 함수 호출(Virtual Call)의 제거
전통적인 엔진에서 복수 개의 집계 함수(SUM(x), COUNT(), MIN(y))를 실행하면, 루프를 돌며 매 행마다 "이게 어떤 함수인지" 판별하는 가상 함수 호출 오버헤드가 발생합니다.
하지만 ClickHouse가 JIT 컴파일을 수행하면, 해당 표현식만을 위한 전용 최적화 단일 루프 함수를 기계어 레벨로 융합(Fusion)해 버립니다.
// JIT 컴파일을 통해 런타임에 동적으로 생성되는 코드 개념 구조
for each row:
sum_state += row.x;
count_state += 1;
if (row.y < min_state) min_state = row.y;
가상 함수 호출이 완벽히 제거되고, 분기문이 없는 스트레이트 라인 코드가 생성되므로 CPU는 연산 결과를 레지스터와 캐시에 유지한 채 엄청난 속도로 루프를 돌파합니다. 심지어 x = x + 0 이나 y = y * 1 같은 무의미한 SQL 연산식은 컴파일 단계에서 피프홀(Peephole) 최적화를 통해 기계어 코드에서 흔적도 없이 지워버립니다.
데이터 특성에 맞춘 30여 가지의 해시 테이블 (Hash Tables)
대용량 대시보드 조회나 통계 연산의 핵심은 GROUP BY와 JOIN입니다. ClickHouse는 이 연산들의 속도를 극대화하기 위해 단 하나의 범용 해시 테이블을 쓰지 않고, 해시 함수, 메모리 할당 정책, 셀 타입 등을 조합하여 30개가 넘는 특화된 해시 테이블 변형 구조를 런타임에 인스턴스화하여 사용합니다.
- Two-level Hash Table: 데이터 그룹화 키가 너무 많을 때는 해시 값의 첫 1바이트를 기준으로 해시 테이블을 256개의 하위 테이블로 쪼개어 관리(샤딩)합니다. 단일 테이블 락 병목을 없애고 멀티코어 병렬 처리에 유리한 구조를 만듭니다.
- Lookup Tables: 그룹화 키의 종류가 아주 적을 때(예: Boolean 또는 소형 Enum)는 해싱 연산 자체를 아예 생략하고, 키 값을 버킷 인덱스로 직접 사용하여 CPU 연산의 극단을 추구합니다.
- CPU Prefetch: 키의 해시값을 계산한 직후, CPU에게 "곧 이 메모리 주소의 해시 셀을 읽을 거야"라고 미리 넌지시 알려주는 하드웨어 명령어(
__builtin_prefetch)를 코드에 심어둡니다. CPU가 데이터를 필요로 하기 전에 메모리에서 캐시로 미리 데이터를 가져오게 하여 메모리 지연(Memory Latency) 시간을 기적적으로 상쇄합니다.
4. 완벽한 격리를 위한 워크로드 관리 (Workload Isolation)
이토록 무시무시하게 CPU와 메모리를 쥐어짜는 엔진이기 때문에, 무거운 배치 쿼리 하나가 유입되면 시스템 전체 자원을 독점하여 다른 서비스의 가용성을 해칠 위험이 있습니다. ClickHouse는 이를 제어하기 위해 서버, 사용자, 쿼리라는 3가지 레벨에서 바이트 단위의 워크로드 격리(Workload Isolation) 기능을 제공합니다.
가장 대표적인 기술이 바로 메모리 초과 할당(Memory Overcommit) 제어입니다. 시스템에 다른 연산들이 수행되지 않아 여유 메모리가 넉넉하다면, 특정 대형 쿼리가 사전에 설정된 자원 보장 한도를 초과하여 메모리를 자유롭게 가져다 쓸 수 있도록 허용합니다. 그러다 다른 유저의 쿼리가 유입되어 메모리가 필요해지면 철저하게 우선순위에 따라 자원 권한을 재조정합니다.
또한, 대규모 JOIN이나 AGGREGATION 도중 메모리 임계치 한도에 도달하면 "Memory limit exceeded(메모리 제한 초과)" 오류를 뿜으며 쿼리를 강제 종료(Fail)시키는 것이 아니라, 앞서 언급한 **디스크 기반 외부 정렬/집계 알고리즘(External Algorithms)으로 매끄럽게 폴백(Fallback)**되어 안정성과 격리성을 동시에 확보합니다.
결론: ClickHouse Deep Dive 시리즈를 마무리하며
총 3편의 시리즈를 통해 오픈소스 최강의 OLAP 데이터베이스인 ClickHouse의 심장부를 들여다보았습니다.
- 1편에서는 분산 인프라에 최적화된 평면 구조의 MergeTree 아키텍처를 보았고,
- 2편에서는 수십억 건의 데이터 스캔을 칼같이 차단하는 희소 인덱스와 Data Pruning 기술을 살펴봤습니다.
- 마지막 3편에서는 하드웨어 한계까지 자원을 몰아붙이는 SIMD 벡터화와 JIT 컴파일 연산 엔진의 사상을 파헤쳤습니다.
ClickHouse의 압도적인 쿼리 성능은 대단한 마법 하나로 이루어진 것이 아닙니다. **"디스크 쓰기는 순차적으로 단순하게 배치 처리하고, 읽을 때는 인덱스로 가볍게 필터링하며, 메모리에 올라온 덩어리는 현대 CPU 아키텍처 특성을 고려해 가장 빠른 기계어로 조진다"**는 지극히 상식적이고 정교한 엔지니어링 철학들의 집합체입니다.
대용량 인프라 로그 플랫폼이나 실시간 지표 분석 시스템 가동을 고민하고 있다면, 하드웨어 사상을 가장 완벽하게 이해하고 구현된 ClickHouse가 훌륭한 해답이자 강력한 무기가 되어줄 것입니다.
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 Tech Blog: Hash Tables in ClickHouse and Zero-Cost Abstractions.
- ClickHouse Official Documentation: Query Execution & Vectorization