DB Study

11장. 모니터링과 트러블슈팅

ClickHouse는 자기 내부 상태를 전부 SQL로 보여줍니다 — system 테이블이라는 창을 통해서. 앞 10개 장의 아키텍처 지식을 "증상 → 진단 → 원인 격리 → 대응"의 운영 무기로 바꿔봅시다.

중급 ⏱ 약 28분 🎬 애니메이션 2개 선수 지식: 4~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.zookeeperKeeper 상태: 노드, 자식 수, 경로 탐색8장

11.2 케이스 스터디 — "Too many parts"를 단계별로 격리하기

가장 흔한 운영 사고인 INSERT 거부를 예로, 증상 → system 테이블 진단 → 원인 격리 → 대응의 사고 흐름을 따라가 봅시다.

🚨 증상: INSERT 거부 DB::Exception: Too many parts (3000) ① system.parts — 어느 테이블? level 0 비율은? active_parts=3200, unmerged(level=0)=2900 → events 테이블 ② 파티션 분포 — uniq(partition)? 1,000 초과? → 과도한 파티셔닝 (5장) ③ system.merges / mutations 머지 항상 바쁨? mutation is_done=0? 파티션 12개 — 정상 ✓ (용의선상 제외) level 0가 대부분 + 머지는 한가 → 소량 빈번 INSERT! ④ 원인별 대응 소량 INSERT → 배칭·async insert (9장) · 머지 부족 → background_pool_size ↑ (6장) 파티셔닝 과다 → 키 재설계 (5장) · mutation 점유 → KILL MUTATION (10장)

이 플로우의 각 단계에 해당하는 실제 쿼리입니다.

-- 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 = 340s (증가 추세!) 레플리카 = 복제 로그 큐의 소비자 지연 = 큐 처리가 못 따라감 큐 분해 — 무엇이 쌓였나? inserts_in_queue = 1,840 📦 merges_in_queue = 12 inserts_in_queue 높음 → 파트 다운로드 병목 네트워크 대역폭 / 디스크 I/O / Keeper 상태 확인 merges_in_queue 높음 → 머지 스레드 부족 background_pool_size 증가 검토 ✅ 네트워크 병목 해소 후 absolute_delay 340s → 2s로 수렴 교훈: 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 등)로 실제 적용값을 확인하세요.

📊
ClickHouse 24.10+ 내장 대시보드

브라우저에서 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장)
🎓
11개 장의 마무리

축하합니다! 파트(4장) → 파티션(5장) → 머지(6장) → 인덱스(7장) → 분산(8장) → INSERT(9장) → 안티패턴(10장)의 아키텍처 지식이 있으면, 이 장의 모든 진단 쿼리가 "외울 것"이 아니라 "당연한 것"이 됩니다. 트러블슈팅의 절반은 아키텍처 이해입니다.

✍️ 이해도 체크

system.parts에서 특정 테이블의 level = 0 파트 비율이 매우 높게 나타났습니다. 이것이 의미하는 것은?
✅ level 0은 INSERT 직후 한 번도 머지되지 않은 파트입니다(6장). 이 비율이 높다는 것은 파트 생성 속도가 머지 속도를 초과한다는 신호로, 방치하면 "Too many parts"로 이어집니다. 원인은 소량 빈번 INSERT, 머지 스레드 부족, 디스크 I/O 병목, mutation 점유 순으로 확인하세요.
레플리카 지연 진단 시 system.replicasinserts_in_queue가 높을 때 우선 확인해야 할 것은?
✅ 복제는 압축된 파트 자체를 전송하는 물리적 복제입니다(8장). inserts_in_queue가 쌓인다는 것은 받아와야 할 파트가 밀렸다는 뜻이므로 네트워크/다운로드 병목을 먼저 봅니다. 반대로 merges_in_queue가 높으면 머지 스레드 부족이므로 background_pool_size 증가를 검토합니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch11_monitoring_troubleshooting.md — 문제별 전체 쿼리 모음, VLDB 2024 논문 등 참고 자료 목록이 담겨 있습니다.