Skip to content
hello tis
Go back

[26.10.02] ClickHouse Merge 증가 원인 분석하기

어제 부하테스트를 마친 뒤에도 ClickHouse의 초당 병합 행 수가 3~4시간 동안 늘었다가 떨어지기를 반복했습니다. 원인을 보기 전에 ClickHouse가 데이터를 읽고 쓰는 방식부터 정리했습니다.

ClickHouse 동작 과정

INSERT가 part를 쓰고 병합하는 순서

  1. INSERT 한 번마다 행을 ORDER BY 순서로 정렬하고 압축해 level 0 part 디렉토리 하나로 쓴다.
  2. part 안의 행은 8,192행씩 granule로 나뉜다. primary.idx에는 granule마다 첫 행의 키를, .mrk2에는 granule이 .bin의 어느 압축 블록에서 시작하는지를 기록한다.
  3. 백그라운드 스레드가 같은 파티션에서 블록 번호가 이어진 part를 골라 큰 part 하나로 다시 쓴다. level은 병합한 part 중 가장 높은 level에 1을 더한 값이다.
  4. 병합 전 part는 비활성 상태가 되고, 기본 480초 뒤 삭제된다.

INSERT 한 번마다 primary.idx, 컬럼별 bin, mrk2 파일을 가진 level 0 part 디렉토리가 하나씩 생긴다. part 3개가 병합돼 level 1 part 202610_1_3_1이 되고, 이 part가 새 level 0 part들과 다시 병합돼 level 2 part가 된다. 병합 전 part는 비활성 상태가 된 뒤 삭제된다

SELECT가 데이터를 읽는 순서

  1. 파티션 키의 최솟값과 최댓값으로 조건에 맞지 않는 part를 뺀다.
  2. primary.idx로 조건에 맞는 granule을 고른다.
  3. .mrk2로 granule이 .bin의 어느 압축 블록에서 시작하는지 찾고, 필요한 컬럼의 블록만 풀어 읽는다.
  4. 읽은 블록을 코어별로 집계하고, 마지막에 결과를 합친다.

part 하나의 내부 구조. 데이터는 ORDER BY 순서로 정렬돼 8,192행씩 granule로 나뉜다. primary.idx에는 granule마다 첫 행의 키가 하나씩 기록된다. 컬럼마다 bin 파일에 압축 블록으로 저장되고, mrk2 파일에는 granule마다 bin 파일의 어느 블록에서 시작하는지가 기록된다

ClickHouse가 집계 쿼리를 빠르게 처리하는 방식

쿼리에 쓴 컬럼 파일만 읽는다

같은 SELECT country, sum(amount) 쿼리에서 MySQL, PostgreSQL 같은 행 저장은 행 단위로 모든 컬럼을 읽고, ClickHouse의 컬럼 저장은 country와 amount 컬럼 파일만 읽는다

정렬 키 인덱스로 읽을 granule을 고른다

primary.idx에는 8,192행 granule마다 첫 행의 키가 기록된다. buy, KR 조건은 buy, JP와 buy, US 사이에 있어 granule 1만 읽고 나머지 granule은 건너뛴다

블록 단위로 여러 코어에서 집계한다

컬럼을 블록 4개로 나눠 코어 4개가 하나씩 집계하고, 코어별 부분 집계를 합쳐 결과를 만든다. 블록 안의 값은 SIMD로 한 번에 계산한다

SIMD(Single Instruction, Multiple Data)는 CPU가 명령 하나로 값 여러 개를 동시에 계산하는 방식입니다. 컬럼 파일을 풀면 같은 타입의 값이 연속으로 나와 SIMD로 바로 계산할 수 있습니다.

MergeTree가 INSERT를 저장하고 병합하는 방식

part 이름에 병합 level이 기록된다

부하가 없을 때도 병합 행 수가 3~4시간 주기로 늘었다

부하테스트 종료 후 10월 1일 16시 17분부터 10월 2일 12시 43분까지 초당 병합 행 수. 0에서 약 200만 행까지 3~4시간에 걸쳐 일정하게 늘었다가 0 근처로 떨어지는 모양이 4번 반복된다

병합 행 수는 0에서 3~4시간 동안 1분 rate 최대 256만 행/초까지 늘었다가 0 근처로 떨어졌습니다.

병합한 테이블은 loadtest.events가 아니라 ClickHouse가 자체 지표를 쌓는 system.asynchronous_metric_log였습니다.

테이블을 잘못 설계하면 생기는 문제

문제정의확인 방법
part 과다파티션의 활성 part가 1,000개를 넘으면 INSERT가 지연되고, 3,000개를 넘으면 실패한다system.parts에서 파티션별 활성 part 수
잘게 나눈 파티션INSERT 한 번에 파티션 수만큼 part가 생기고, 파티션끼리는 병합되지 않아 작은 part가 남는다system.parts에서 파티션 수와 파티션별 크기
ORDER BY 키 순서필터 컬럼이 키 앞쪽에 없으면 조건에 맞는 행이 흩어진 granule을 모두 읽는다EXPLAIN indexes = 1에서 선택된 granule 수

part가 병합보다 빨리 늘면 INSERT가 지연되거나 실패한다

INSERT마다 파티션의 활성 part 수가 1씩 늘고, 병합하면 줄어든다. 활성 part가 1,000개를 넘으면 INSERT가 지연되고, 3,000개를 넘으면 INSERT가 실패한다

파티션을 잘게 나누면 작은 part가 병합되지 않고 남는다

월 단위 파티션은 한 파티션 안의 작은 part 8개가 큰 part 하나로 합쳐진다. 시간 단위 파티션은 시간마다 작은 part가 2개씩 있지만 파티션끼리는 합쳐지지 않아 작은 part가 남는다

ORDER BY 키 순서가 필터와 맞지 않으면 granule을 모두 읽는다

WHERE event_type = purchase 조건에서 ORDER BY (event_type, user_id)는 purchase 행이 연속된 granule 4개에 모여 그 구간만 읽고, ORDER BY (user_id, event_type)는 purchase 행이 모든 granule에 흩어져 모든 granule을 읽는다


Share this post:

Previous Post
[26.10.05] 사내 버셀을 Spring으로 통합하기
Next Post
[26.10.01] ClickHouse로 적재 구조 단순화하기