클릭하우스는 쿼리를 어떻게 병렬로 실행하나요?

클릭하우스는 쿼리를 어떻게 병렬로 실행하나요?

클릭하우스는 '빠름'을 위해 설계된 데이터베이스라, 쿼리를 가용한 모든 CPU 코어를 활용해 고병렬로 실행합니다. 데이터를 여러 처리 레인(processing lane)으로 나누고 max_threads 설정으로 레인 수를 조절하죠. 대규모 워크로드에서 병렬 처리를 조정·모니터링하는 방법까지 함께 살펴볼게요.

집계 쿼리를 병렬화하는 과정

테이블의 프라이머리 키에 필터가 있는 집계 쿼리가 실행된다고 해 볼게요. 클릭하우스는 먼저 프라이머리 인덱스를 메모리로 로드해, 처리해야 할 그래뉼과 안전하게 건너뛸 수 있는 그래뉼을 식별합니다.

선택된 데이터는 그다음 n개의 병렬 처리 레인에 분산돼요. 각 레인은 데이터를 블록 단위로 스트리밍하며 최종 결과로 처리하죠.

nmax_threads 설정이 결정해요. 기본값은 클릭하우스가 사용 가능한 단일 CPU의 코어(스레드) 수와 같아요. 코어가 4개인 머신에서 8개로 늘면 병렬 처리 레인이 늘어나 처리량이 대략 두 배가 되지만, 메모리 사용도 그만큼 늘어나요. 레인 분산이 효율적일수록 CPU 활용이 높아지고 총 쿼리 시간이 줄어듭니다.

샤딩된 테이블 처리

테이블 데이터가 여러 서버에 샤드로 분산돼 있으면 각 서버가 자신의 샤드를 병렬로 처리해요. 서버 내부에서는 앞에서 본 것처럼 병렬 처리 레인으로 로컬 데이터를 다루고요. 처음 쿼리를 받은 서버가 모든 샤드의 부분 결과를 모아 최종 전역 결과로 합칩니다. 샤드 간 부하 분산은 특히 고처리량 환경에서 병렬 처리의 수평 확장을 가능하게 해요.

클릭하우스 클라우드에서는 이 병렬 처리를 병렬 레플리카(parallel replicas) 로 달성합니다. 각 레플리카가 데이터의 일부를 병렬로 처리해 최종 결과에 기여하는 방식으로, 샤드와 유사하게 동작하죠.

병렬 처리 모니터링

쿼리가 CPU 자원을 충분히 쓰는지 확인하려면 몇 가지 도구를 쓸 수 있어요.

집계 쿼리 동안 send_logs_level='trace'를 설정하면 클릭하우스 서버가 trace 레벨 로그를 돌려줍니다.

SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
SETTINGS send_logs_level='trace';

로그에서 클릭하우스가 3,609개의 그래뉼(로그에선 marks로 표시)을 3개 데이터 범위에서 읽고, 59개 CPU 코어로 이 작업을 59개의 병렬 처리 스트림에 분산한다는 걸 볼 수 있어요.

또는 EXPLAIN PIPELINE 절로 물리 연산자 계획(쿼리 파이프라인)을 살펴볼 수 있습니다. 물리 계획은 아래에서 위로 읽어요. 각 연산자가 × 59로 표시되면 그 단계가 겹치지 않는 데이터 영역에 걸쳐 59개 처리 레인에서 동시에 실행된다는 뜻이죠.

EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple;

클릭하우스 내장 웹 UI( /play 엔드포인트)는 이 물리 계획을 그래픽으로 렌더링해 줘요. 각 행이 병렬 처리 레인 하나를 나타내며 필터링·집계·최종 처리 단계를 거쳐 데이터를 블록 단위로 스트리밍하죠.

레인 간 부하 분산

물리 계획의 Resize 연산자는 데이터 블록 스트림을 레인 간에 재분배해 균등하게 유지해요. 데이터 범위마다 쿼리 술어와 일치하는 행 수가 다르면 어떤 레인은 과부하되고 다른 레인은 놀 수 있는데, 작업을 다시 나눠 빠른 레인이 느린 레인을 돕게 하면 전체 쿼리 런타임이 최적화됩니다.

max_threads가 항상 지켜지진 않아요

max_threads의 기본값은 서버가 사용 가능한 CPU 코어 수예요. SELECT getSetting('max_threads')로 확인할 수 있죠.

다만 max_threads 값은 처리 대상으로 선택된 데이터 양에 따라 무시될 수 있어요. 작은 데이터셋을 다루는 쿼리에서는 클릭하우스가 의도적으로 동시성을 줄입니다. 예를 들어 max_threads가 59여도 town = 'LONDON' 조건으로 282개 그래뉼 정도만 처리하는 쿼리라면 30개 동시 스트림만 쓸 수 있어요.

즉, max_threads의 'max'는 상한을 뜻하지 실제로 쓰는 스레드 수를 보장하지 않아요. "충분한 데이터"의 기준은 두 설정이 정합니다. 각 처리 레인이 다뤄야 할 최소 행 수(기본 163,840)와 최소 바이트 수(기본 2,097,152)가 그것이죠.

  • 공유-낫싱 클러스터: merge_tree_min_rows_for_concurrent_read, merge_tree_min_bytes_for_concurrent_read
  • 공유 스토리지 클러스터(클라우드 등): merge_tree_min_rows_for_concurrent_read_for_remote_filesystem, merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem

이 설정들은 프로덕션에서 수정하지 않는 걸 권장해요. 성능 실험용으로만 쓰세요. 데모처럼 아래처럼 재정의하면 실제로 max_threads를 온전히 반영해 스캔하는 걸 볼 수 있습니다.

EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON'
SETTINGS
  max_threads = 59,
  merge_tree_min_read_task_size = 0,
  merge_tree_min_rows_for_concurrent_read_for_remote_filesystem = 0, 
  merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem = 0;

핵심 요점

  • 클릭하우스는 max_threads와 연결된 처리 레인으로 쿼리를 병렬화해요.
  • 실제 레인 수는 처리 대상 데이터의 크기에 따라 달라져요.
  • EXPLAIN PIPELINE과 trace 로그로 레인 사용량을 분석할 수 있어요.

출처: How ClickHouse executes a query in parallel

더 알아보기

  • 스파스 프라이머리 인덱스 — 처리할 그래뉼을 고르는 인덱스 역할
  • 테이블 데이터 파트 — 병렬 스캔의 대상이 되는 파트 구조
  • 파트 병합 — 파트를 관리하는 백그라운드 병합