11장. 모니터링과 트러블슈팅
ClickHouse는 자기 내부 상태를 전부 SQL로 보여줍니다 — system 테이블이라는 창을 통해서. 앞 10개 장의 아키텍처 지식을 "증상 → 진단 → 원인 격리 → 대응"의 운영 무기로 바꿔봅시다.
① 핵심 system 테이블 10개와 각각이 보여주는 것 ② "Too many parts"를 system 테이블로 단계 진단하는 플로우 ③ 쿼리 느림·레플리카 지연·mutation 멈춤·디스크 부족의 문제별 진단 가이드 ④ 상시 모니터링 쿼리와 24.10+ 내장 대시보드
11.1 System 테이블 맵 — 무엇을 어디서 보는가
대부분의 ClickHouse 문제는 system 테이블 쿼리만으로 진단할 수 있습니다. 각 테이블이 앞 장들의 개념과 어떻게 연결되는지 봅시다.
| System 테이블 | 관찰 대상 | 관련 장 |
|---|---|---|
system.parts | 파트 현황: 활성 파트 수, 크기, 레벨, 파티션 분포 | 4, 5장 |
system.merges | 진행 중인 머지: 진행률, 소요 시간, 파트 수 | 6장 |
system.mutations | 진행 중인 mutation: 상태, 에러, 대기열 | 10장 |
system.replicas | 레플리카 상태: lag, 큐 크기, 리더 여부 | 8장 |
system.query_log | 쿼리 이력: 성능, 메모리, 읽기량, 에러 | 2, 3장 |
system.part_log | 파트 생명주기: INSERT, 머지, 삭제 이벤트 | 4, 6장 |
system.metrics | 실시간 메트릭: 스레드, 커넥션, 메모리 | 3장 |
system.asynchronous_metrics | 주기적 메트릭: 디스크, OS 수준 통계 | — |
system.events | 누적 카운터: 읽기/쓰기 바이트, 쿼리 수 | — |
system.zookeeper | Keeper 상태: 노드, 자식 수, 경로 탐색 | 8장 |
11.2 케이스 스터디 — "Too many parts"를 단계별로 격리하기
가장 흔한 운영 사고인 INSERT 거부를 예로, 증상 → system 테이블 진단 → 원인 격리 → 대응의 사고 흐름을 따라가 봅시다.
증상 발생: "Too many parts" — 활성 파트가 parts_to_throw_insert(기본 3000)를 넘어 INSERT가 거부됐습니다. 당황하지 말고 system 테이블로 갑니다.
① system.parts에서 문제 테이블을 식별합니다. active_parts 수와 함께 level=0 파트 비율을 확인하세요 — 이것이 다음 분기의 핵심 단서입니다.
②③ 두 갈래로 진단합니다: 파티션 분포(uniq(partition)이 1,000을 훌쩍 넘으면 과도한 파티셔닝)와 머지·mutation 상태(system.merges가 항상 바쁜지, system.mutations에 is_done=0이 쌓였는지).
이 사례에서는 파티션은 12개로 정상, 대신 level 0 파트가 2,900개인데 머지는 한가 — 머지가 밀린 게 아니라 파트 생성 자체가 폭주하는 소량 빈번 INSERT가 원인으로 격리됩니다.
④ 원인별 대응 매핑: 소량 INSERT → 배칭/async insert, 머지 처리량 부족 → background_pool_size 증가, 파티셔닝 과다 → 키 재설계(새 테이블 필요), mutation 점유 → KILL MUTATION 후 대안 사용. 디스크 I/O 병목이면 스토리지 업그레이드.
이 플로우의 각 단계에 해당하는 실제 쿼리입니다.
-- 1단계: 어느 테이블이 문제인지 확인
SELECT database, `table`,
count() AS active_parts,
uniq(partition) AS partitions,
countIf(level = 0) AS unmerged_parts
FROM system.parts
WHERE active
GROUP BY database, `table`
HAVING active_parts > 300
ORDER BY active_parts DESC;
-- 2단계: 파티션별 분포 (과도한 파티셔닝 여부)
SELECT partition, count() AS parts, min(level) AS min_level
FROM system.parts
WHERE database = 'mydb' AND `table` = 'mytable' AND active
GROUP BY partition ORDER BY parts DESC LIMIT 20;
-- 3단계: 머지가 밀리고 있는지 확인
SELECT count() AS running_merges,
sum(num_parts) AS merging_parts,
formatReadableSize(sum(total_size_bytes_compressed)) AS total_size
FROM system.merges;
11.3 문제별 진단 가이드
문제: 쿼리 성능 저하
-- 느린 쿼리 Top 10
SELECT query_id, query_duration_ms, read_rows,
formatReadableSize(read_bytes) AS read_size,
formatReadableSize(peak_memory_usage) AS peak_mem,
query
FROM system.query_log
WHERE type = 'QueryFinish' AND query_kind = 'Select'
AND event_date >= today() - 1
ORDER BY query_duration_ms DESC LIMIT 10;
-- 인덱스 skip 효과 확인: Granules X/Y 비율이 핵심
EXPLAIN indexes = 1 SELECT ... FROM mytable WHERE ...;
| 증상 | 진단 | 대응 |
|---|---|---|
| 전체 스캔 (Granules: Y/Y) | EXPLAIN에서 PrimaryKey 활용 없음 | ORDER BY 키 재설계 (7장) |
| 파트가 너무 많음 | Parts: 수백 | 파티셔닝 재검토, 머지 대기 |
| 메모리 부족 | peak_memory_usage 과다 | max_memory_usage 조정, 쿼리 최적화 |
| 대량 데이터 읽기 | read_bytes 과다 | 필요 컬럼만 SELECT, PREWHERE 활용 |
문제: 레플리카 지연 (Replication Lag)
8장에서 배운 복제 구조를 떠올리면 진단이 자연스럽습니다 — 레플리카는 복제 로그의 큐를 처리하는 소비자이므로, 큐에 무엇이 쌓였는지가 병목의 정체를 알려줍니다.
system.replicas에서 absolute_delay(최신 레플리카 대비 지연 초)가 증가 추세로 감지됩니다. 레플리카가 쓰기를 따라잡지 못하고 있다는 뜻입니다.
같은 테이블의 큐 컬럼을 분해합니다: queue_size가 inserts_in_queue(다운로드해야 할 파트)와 merges_in_queue(수행해야 할 머지)로 나뉩니다.
구성에 따라 원인이 갈립니다 — inserts가 높으면 다른 레플리카에서 파트를 받아오는 네트워크/다운로드 병목, merges가 높으면 머지 스레드 부족(background_pool_size 증가 검토). Keeper 연결 문제라면 system.zookeeper도 확인합니다.
병목을 해소하면 delay가 수렴합니다. 핵심 교훈: lag 수치 자체가 아니라 큐의 구성이 원인을 말해준다는 것.
문제: Mutation이 끝나지 않음 / 디스크 공간 부족
-- 멈춘 mutation 확인: parts_to_do가 줄지 않으면 진행 정지
SELECT database, `table`, mutation_id, command,
is_done, parts_to_do, latest_fail_reason
FROM system.mutations
WHERE is_done = 0
ORDER BY create_time ASC;
-- 문제 mutation 강제 취소
KILL MUTATION WHERE mutation_id = 'mutation_0000000001';
-- 테이블별 디스크 사용량 (inactive 파트 포함)
SELECT database, `table`,
formatReadableSize(sum(bytes_on_disk)) AS total_size,
countIf(active = 0) AS inactive_parts,
formatReadableSize(sumIf(bytes_on_disk, active = 0)) AS inactive_size
FROM system.parts
GROUP BY database, `table`
ORDER BY sum(bytes_on_disk) DESC LIMIT 20;
inactive 파트가 과도하면 old_parts_lifetime(기본 8분)이 아직 안 지났거나 머지가 진행 중인 것입니다. 머지 중에는 원본 + 결과 파트가 동시에 존재하므로 일시적으로 2배 공간이 필요합니다(6장). 조금 기다리면 회수되는 공간인지 먼저 확인하세요.
11.4 상시 모니터링 — 사고가 나기 전에 보는 지표
-- 파트 건강 상태: level0_pct가 높으면 머지가 밀리고 있다는 신호
SELECT database, `table`,
count() AS active_parts,
countIf(level = 0) AS level0_parts,
round(countIf(level = 0) / count() * 100, 1) AS level0_pct,
max(modification_time) AS last_insert
FROM system.parts
WHERE active
GROUP BY database, `table`
HAVING active_parts > 10
ORDER BY level0_pct DESC;
-- 시간대별 쿼리 성능 추이 (p95 추적)
SELECT
toStartOfHour(event_time) AS hour,
count() AS queries,
round(avg(query_duration_ms)) AS avg_ms,
round(quantile(0.95)(query_duration_ms)) AS p95_ms
FROM system.query_log
WHERE type = 'QueryFinish' AND query_kind = 'Select'
AND event_date >= today() - 1
GROUP BY hour ORDER BY hour;
실시간 스레드·머지·복제 활동은 system.metrics에서 Query, Merge, PartMutation, GlobalThreadActive, ReplicatedSend/Fetch 같은 메트릭으로 확인합니다.
2장에서 배운 특성: 설정 파일에 오류가 있으면 서버는 이전 설정을 유지하고 경고만 남깁니다. 변경 후에는 SELECT name, value, changed FROM system.settings WHERE name IN ('max_threads', 'async_insert', ...)와 system.merge_tree_settings(parts_to_throw_insert, old_parts_lifetime 등)로 실제 적용값을 확인하세요.
브라우저에서 http://clickhouse-server:8123/dashboard로 내장 모니터링 대시보드에 접근할 수 있습니다. /merges 핸들러는 파트 머지와 write amplification을 시각적으로 보여줍니다(6장 참조).
11.5 트러블슈팅 플로우차트 — 전체 지도
문제 발생
│
├── INSERT 실패?
│ ├── "Too many parts" → §11.2 진단 플로우
│ └── 타임아웃 → 배치 크기·네트워크 확인
│
├── 쿼리가 느리다?
│ ├── EXPLAIN indexes=1 → Granule skip 확인 (7장)
│ ├── system.query_log → read_bytes, peak_memory 확인
│ └── 파트 수 확인 → 머지 밀림 여부 (6장)
│
├── 레플리카 지연? → system.replicas 큐 분해 (8장)
│
├── 디스크 부족? → inactive 파트 + 머지 중 임시 공간 (6장)
│
└── Mutation 멈춤? → system.mutations → KILL MUTATION (10장)
축하합니다! 파트(4장) → 파티션(5장) → 머지(6장) → 인덱스(7장) → 분산(8장) → INSERT(9장) → 안티패턴(10장)의 아키텍처 지식이 있으면, 이 장의 모든 진단 쿼리가 "외울 것"이 아니라 "당연한 것"이 됩니다. 트러블슈팅의 절반은 아키텍처 이해입니다.
✍️ 이해도 체크
system.parts에서 특정 테이블의 level = 0 파트 비율이 매우 높게 나타났습니다. 이것이 의미하는 것은?system.replicas의 inserts_in_queue가 높을 때 우선 확인해야 할 것은?이 장의 원문 문서: chapters/ch11_monitoring_troubleshooting.md — 문제별 전체 쿼리 모음, VLDB 2024 논문 등 참고 자료 목록이 담겨 있습니다.