본문으로 건너뛰기
clickhouse검색
운영과 트러블슈팅

Too many parts와 머지 지연 트러블슈팅 (증상, 원인, 해결)

INSERT 가 "Too many parts" 로 거절되거나 느려질 때 파트 수와 머지 상태를 확인하는 쿼리, 작은 INSERT 와 과한 파티션이라는 두 원인, 배치와 비동기 INSERT 로 고치는 방법을 정리합니다.

Too many parts머지INSERT파티션async_insert2026-10 기준, 약 11분

증상

ClickHouse 를 운영하면서 가장 먼저 만나는 에러입니다. 애플리케이션 로그에 이런 메시지가 찍힙니다.

DB::Exception: Too many parts (3000 with average size of 1.24 KiB) in table 'logs.events (uuid)'.
Merges are processing significantly slower than inserts. (TOO_MANY_PARTS)

거절까지 가지 않더라도 INSERT 응답이 수 초씩 늦어지면 같은 문제의 앞 단계입니다. 파트 수가 parts_to_delay_insert 를 넘으면 ClickHouse 가 INSERT 를 일부러 늦추고(최대 max_delay_to_insert 초), parts_to_throw_insert 를 넘으면 위 에러로 거절합니다. 기본값은 지연 1000 개, 거절 3000 개입니다 (예전 버전은 150 개와 300 개였고, 지금 서버의 값은 system.merge_tree_settings 에서 확인합니다).

메시지의 "average size of 1.24 KiB" 가 핵심 단서입니다. 평균 파트 크기가 KB 단위라면 한 번에 몇 줄씩 INSERT 하고 있다는 뜻입니다.

원인

MergeTree 는 INSERT 한 번마다 파티션당 파트를 하나 만들고, 백그라운드 머지가 이를 합칩니다. 자세한 동작은 MergeTree 엔진에 있습니다. 파트가 쌓이는 이유는 결국 "만드는 속도가 합치는 속도보다 빠르다" 하나이고, 그 안에서 흔한 경우는 다음과 같습니다.

원인어떻게 알아보나
한 번에 몇 줄씩 자주 INSERT평균 파트 크기가 KB 단위, system.part_log 의 NewPart 이벤트가 초당 수십 건
INSERT 하나가 여러 파티션을 건드림PARTITION BY 가 날짜가 아닌 고카디널리티 열(고객 ID, 시간 단위)
max_insert_threads 가 커서 INSERT 하나가 파트를 여러 개 생성part_log 에 같은 query_id 로 파트가 N 개
머지 풀이 꽉 찼거나 큰 머지가 풀을 독점system.merges 에 오래 걸리는 머지, BackgroundMergesAndMutationsPoolTask 가 풀 크기에 붙음
뮤테이션이 머지를 막음system.mutations 에 is_done = 0 이 쌓임
디스크가 느리거나 가득 참system.merges 의 progress 가 거의 안 움직임, system.disks 의 free_space
테이블과 MV 가 너무 많아 머지 스레드를 나눠 씀테이블 수백 개에 각각 작은 INSERT
Kafka 엔진 블록이 너무 작음kafka_max_block_size, kafka_flush_interval_ms 가 작음

확인 쿼리

먼저 어느 테이블, 어느 파티션에 파트가 몰렸는지 봅니다.

SELECT database, table, partition,
       count() AS parts,
       formatReadableSize(sum(bytes_on_disk)) AS size,
       formatReadableSize(avg(bytes_on_disk)) AS avg_part
FROM system.parts
WHERE active
GROUP BY database, table, partition
ORDER BY parts DESC
LIMIT 20;

지금 돌고 있는 머지와 풀 상태입니다.

SELECT database, table, elapsed, progress, num_parts,
       formatReadableSize(total_size_bytes_compressed) AS size, merge_type
FROM system.merges
ORDER BY elapsed DESC;

SELECT metric, value FROM system.metrics
WHERE metric IN ('BackgroundMergesAndMutationsPoolTask', 'BackgroundMergesAndMutationsPoolSize');

SELECT metric, value FROM system.asynchronous_metrics
WHERE metric IN ('MaxPartCountForPartition', 'NumberOfTables');

INSERT 패턴을 보려면 system.part_log 가 가장 정확합니다.

SELECT toStartOfMinute(event_time) AS m,
       countIf(event_type = 'NewPart') AS new_parts,
       countIf(event_type = 'MergeParts') AS merges,
       formatReadableSize(avgIf(size_in_bytes, event_type = 'NewPart')) AS avg_new
FROM system.part_log
WHERE table = 'events' AND event_time > now() - INTERVAL 1 HOUR
GROUP BY m ORDER BY m;

분당 새 파트가 수백 개인데 머지는 수십 개라면 머지가 따라가지 못하는 상황이 숫자로 보입니다. 거절과 지연 자체는 system.events 의 RejectedInserts, DelayedInserts 로 셉니다.

팁 파트 수를 보는 임계는 "파티션당 활성 파트 300 개 이상이면 비정상" 정도로 두고 경보를 걸어 두면 거절되기 전에 잡을 수 있습니다. 모니터링에 경보 예가 있습니다.

해결

1. INSERT 를 묶습니다 (가장 중요)

ClickHouse 의 INSERT 는 "크게, 드물게" 가 원칙입니다. 한 번에 1만 행 이상 또는 1초에 한 번 정도로 묶으면 대부분 끝납니다. 애플리케이션에서 버퍼링이 어렵다면 서버 쪽에서 묶어 주는 비동기 INSERT 를 씁니다.

-- 세션 또는 사용자 프로필에
SET async_insert = 1, wait_for_async_insert = 1;
-- 모이는 조건: 크기(async_insert_max_data_size), 시간(async_insert_busy_timeout_ms), 쿼리 수

wait_for_async_insert = 1 이면 버퍼가 디스크에 쓰인 뒤 응답하므로 데이터 유실 걱정이 없고, 0 이면 바로 응답하되 실패를 클라이언트가 모릅니다. 서버 전체 버퍼는 async_insert_threads 와 async_insert_max_data_size 로 조절합니다. 자세한 비교는 INSERT 모범 사례에 있습니다.

2. 파티션 키를 다시 봅니다

파티션은 데이터 삭제와 TTL 단위이지 성능 장치가 아닙니다. 하루에 수 GB 미만이면 월별, 그보다 적으면 파티션 없이 가는 것이 보통 맞습니다. 고객 ID 같은 열로 파티션을 나눴다면 INSERT 하나가 수백 파트를 만들고 머지도 파티션 안에서만 일어나 영원히 합쳐지지 않습니다. 설계 기준은 정렬 키, 기본 키, 파티션에 있습니다.

-- INSERT 하나가 몇 개 파티션에 걸치는지
SELECT query_id, count() AS parts, uniq(partition_id) AS partitions
FROM system.part_log
WHERE event_type = 'NewPart' AND event_time > now() - INTERVAL 10 MINUTE
GROUP BY query_id ORDER BY parts DESC LIMIT 10;

3. INSERT 하나가 파트를 여러 개 만들지 않게

max_insert_threads 가 1 보다 크면 INSERT SELECT 가 스레드별로 파트를 만듭니다. 작은 적재에서는 max_insert_threads = 1 로 두거나 min_insert_block_size_rows, min_insert_block_size_bytes 를 키워 블록을 합칩니다. Kafka 엔진은 kafka_max_block_size 를 수십만 행으로, kafka_flush_interval_ms 를 수 초로 올립니다. Kafka와 스트리밍 수집에 설정 표가 있습니다.

4. 머지가 돌 수 있게 길을 터 줍니다

  • 뮤테이션이 쌓였으면 끝나길 기다리거나 KILL MUTATION 으로 정리합니다 (삭제와 갱신).
  • 머지 풀을 키웁니다: background_pool_size(기본 16), background_merges_mutations_concurrency_ratio(기본 2). 코어 수 이상으로 올려도 디스크가 병목이면 소용없습니다.
  • 150 GB 를 넘는 머지는 기본으로 하지 않습니다(max_bytes_to_merge_at_max_space_in_pool). 파트가 이미 크다면 파트가 수십 개 있어도 정상이니 세지 않습니다.
  • 디스크 여유가 적으면 머지가 공간을 못 잡아 멈춥니다. 머지에는 합칠 파트 크기만큼의 임시 공간이 필요합니다.

5. 급할 때 임시 조치

-- 파티션 하나를 강제로 합침 (디스크와 CPU 를 많이 씀, 업무 시간은 피함)
OPTIMIZE TABLE logs.events PARTITION '202610' FINAL;

-- 한도를 잠시 올림 (근본 해결이 아니라 시간을 사는 것)
ALTER TABLE logs.events MODIFY SETTING parts_to_throw_insert = 6000;

OPTIMIZE ... FINAL 은 전체 테이블에 돌리면 수 TB 를 다시 쓰는 작업이 되니 반드시 파티션 단위로, 야간에 돌립니다. 한도를 올리는 것은 거절만 미루는 것이라 그 사이에 INSERT 를 묶는 쪽을 고쳐야 합니다.

주의 parts_to_throw_insert 를 크게 올리고 잊어버리면 파트가 수만 개까지 쌓여 서버 재시작이 수십 분 걸리고(파트 로딩), 쿼리마다 수만 개 파일을 열어 느려집니다. 임시 조치는 티켓에 남기고 되돌립니다.

재발 방지

  1. 적재 경로마다 "INSERT 한 번의 행 수" 를 지표로 남깁니다. system.part_log 의 평균 새 파트 크기가 1 MB 아래면 경보.
  2. 새 테이블을 만들 때 파티션 키 검토를 리뷰 항목에 넣습니다. 기본은 toYYYYMM(ts) 또는 파티션 없음.
  3. 애플리케이션이 직접 쓰는 테이블에는 async_insert 를 사용자 프로필로 켜 둡니다.
  4. MaxPartCountForPartition 과 RejectedInserts 를 대시보드에 올립니다.
  5. 머티리얼라이즈드 뷰가 많은 테이블은 MV 대상 테이블의 파트 수도 같이 봅니다. 원본 INSERT 하나가 MV 개수만큼 파트를 더 만듭니다 (머티리얼라이즈드 뷰).

공식 문서

이 문서에서