ClickHouse — A Columnar Analytics Database from the Inside
Pile up parts, merge them, and hit the limit
한국어 원문으로 표시합니다.
목표
INSERT 가 파트를 몇 개 만드는지, 병합이 그것을 어떻게 줄이는지, 파트가 너무 많으면 무엇이 막히는지를 system.parts·system.part_log·system.query_log 로 확인한다. 비동기 INSERT 로 여러 INSERT 를 파트 하나로 모은다.
왜 중요한가
ClickHouse 운영 장애의 단골은 "Too many parts" 이고, 원인은 넣는 방식이다. 넣는 쪽 코드를 고치려면 "이 INSERT 가 파트를 몇 개 만들었나" 를 셀 줄 알아야 한다. 병합은 백그라운드에서 제멋대로 일어나므로 이 실습은 병합을 멈추고 시작한다. 채점기는 여러분이 적은 숫자를 믿지 않는다 — part_log 의 사건을 지금 있는 표의 uuid 로 걸러 세고, TOO_MANY_PARTS 는 query_log 의 오류 코드로 확인한다.
단계
- 데이터베이스
parts와 표parts.events를 만드세요 — 열id UInt64, grp UInt8, v UInt32, 엔진MergeTree,ORDER BY id. 그리고SYSTEM STOP MERGES parts.events로 이 표의 병합을 멈추세요. /opt/lab/fixtures/parts/batches.sql(INSERT 문 10개)을 한 번 실행한 뒤, 활성 파트 수와 이름을 /root/ch/parts/parts.json 에active_parts·names로 적으세요.- 같은 열의 표
parts.big을 새로 만들어numbers(1000000)에서 100만 행을 INSERT 한 번으로 넣되SETTINGS min_insert_block_size_rows = 250000을 붙이세요. part_log 에서 그 INSERT 가 만든 새 파트들의query_id·개수·행 수 목록을 /root/ch/parts/blocks.json 에query_id·parts·rows로 적으세요. SYSTEM START MERGES parts.events뒤OPTIMIZE TABLE parts.events FINAL로 파트를 하나로 합치고, 그 파트의 이름·수준과 part_log 의 이 표MergeParts사건 수를 /root/ch/parts/merge.json 에part·level·merge_events로 적으세요.- 같은 열에
SETTINGS parts_to_throw_insert = 5인 표parts.guarded를 만들고 병합을 멈춘 뒤,id1–6 을 한 행씩 INSERT 여섯 번으로 넣으세요. 나온 오류(stderr)를 /root/ch/parts/toomany.txt 에 모으세요. parts.guarded의 병합을 다시 켜고OPTIMIZE ... FINAL로 합친 뒤, 거절됐던id6 을 다시 넣어 표가id1–6 여섯 행이 되게 하세요.- 같은 열의 표
parts.async_ev를 새로 만들고, 두 행짜리INSERT ... VALUES를 5번 따로 보내되SETTINGS async_insert = 1, wait_for_async_insert = 0, async_insert_use_adaptive_busy_timeout = 0, async_insert_busy_timeout_max_ms = 600000을 붙이세요.SYSTEM FLUSH ASYNC INSERT QUEUE로 버퍼를 비운 뒤, query_log 에서 표를 만든 뒤의 INSERT 수·part_log 의 새 파트 수·그 파트를 만든 query_id 를 /root/ch/parts/async.json 에insert_queries·new_parts·flush_query_id로 적으세요. parts데이터베이스의 지금 있는 표마다 part_log 의NewPart사건 수를 세어 /root/ch/parts/summary.json 에{"표이름": 수, ...}로 적으세요(events·big·guarded·async_ev포함).
참고
- 파트 보기:
SELECT name, rows, level FROM system.parts WHERE database = 'parts' AND table = '...' AND active. - part_log 는 1초마다 비워집니다. 방금 일이 안 보이면
SYSTEM FLUSH LOGS. 지금 표의 기록만 보려면table_uuid = (SELECT uuid FROM system.tables WHERE database = 'parts' AND name = '...'). SYSTEM STOP MERGES는 서버를 다시 켜면 풀립니다. 병합이 멈춘 표에 OPTIMIZE 를 치면Cancelled merging parts로 거절됩니다 — 먼저SYSTEM START MERGES.- 흔한 실수: 2단계를 두 번 돌리는 것. TRUNCATE 해도 part_log 기록은 남으므로, 다시 하려면
DROP TABLE parts.events뒤 1단계부터. 7단계에서 한 문장에 10행을 넣거나, 기다리는 모드(기본)로 하나씩 보내면 파트가 하나로 모이지 않습니다. - 7단계의 기다리지 않는 모드는 오류가 클라이언트에 돌아오지 않아 운영에서는 권하지 않습니다(문서 권장:
wait_for_async_insert = 1). 여기서는 시간에 기대지 않고 모으려고 씁니다. - 공식 문서: Table parts · Part merges · system.part_log · parts_to_* settings · Asynchronous inserts · Selecting an insert strategy · OPTIMIZE
표를 만들고 병합을 멈춘다
데이터베이스 parts 와 표 parts.events 를 만드세요. 열은 id UInt64, grp UInt8, v UInt32 순서, 엔진 MergeTree, ORDER BY id 입니다. 이어서 SYSTEM STOP MERGES parts.events 로 이 표의 백그라운드 병합을 멈추세요.
병합을 멈추지 않으면 다음 단계에서 만든 작은 파트들을 서버가 몇 초 안에 합쳐 버려, 파트가 생긴 모습을 볼 수 없습니다. SYSTEM STOP MERGES 는 표 이름을 받으면 그 표만 멈춥니다.
INSERT 10번 → 파트 10개
/opt/lab/fixtures/parts/batches.sql(INSERT 문 10개, 각 1000행)을 한 번 실행하고, 활성 파트 수와 이름 목록을 /root/ch/parts/parts.json 에 active_parts·names 로 적으세요.
--queries-file 로 돌리면 문장마다 따로 INSERT 가 됩니다. 이름의 가운데 두 숫자는 블록 번호의 처음과 끝, 마지막 숫자는 병합 수준입니다. groupArray 로 이름을 배열로 모아 JSONEachRow 로 받으면 그대로 저장할 수 있습니다.
INSERT 한 번이 파트 여러 개가 될 때
같은 열의 표 parts.big 을 새로 만들어 numbers(1000000) 에서 100만 행을 INSERT ... SELECT 한 번으로 넣되 SETTINGS min_insert_block_size_rows = 250000 을 붙이세요. part_log 에서 이 표의 NewPart 사건을 읽어, 그것을 만든 query_id, 새 파트 개수, 파트마다의 행 수 목록을 /root/ch/parts/blocks.json 에 query_id·parts·rows 로 적으세요.
INSERT ... SELECT 는 읽은 블록을 min_insert_block_size_rows 만큼 모일 때까지 붙인 뒤 파트로 씁니다. 기본값이 약 100만 행이라 그대로 두면 파트 하나로 끝납니다. part_log 의 query_id 는 여러분이 보낸 INSERT 의 id 입니다. 표를 다시 만들었다면 table_uuid 로 걸러야 옛 기록이 섞이지 않습니다.
병합을 켜고 하나로 합친다
SYSTEM START MERGES parts.events 뒤 OPTIMIZE TABLE parts.events FINAL 로 파트를 하나로 합치세요. 합친 파트의 이름·수준(system.parts 의 name·level)과, part_log 에서 이 표의 MergeParts 사건 수를 /root/ch/parts/merge.json 에 part·level·merge_events 로 적으세요.
병합을 멈춘 채 OPTIMIZE 를 치면 Cancelled merging parts 로 거절됩니다. 병합을 켜는 순간 백그라운드가 먼저 합칠 수도 있고, FINAL 은 이미 한 파트여도 한 번 더 씁니다 — 그래서 수준이 1일 수도 2일 수도 있습니다. 어느 경로였는지는 part_log 의 merged_from 을 보면 압니다.
문턱을 낮춰 TOO_MANY_PARTS 를 부른다
같은 열·ORDER BY id 에 SETTINGS parts_to_throw_insert = 5 인 표 parts.guarded 를 만들고 SYSTEM STOP MERGES parts.guarded 로 병합을 멈추세요. id 1–6 을 한 행씩 INSERT 여섯 번으로 넣고, 나온 오류 출력(stderr)을 /root/ch/parts/toomany.txt 에 모으세요.
문턱은 파티션 하나의 활성 파트 수에 걸립니다. 한 행짜리 INSERT 도 파트를 하나 만듭니다. clickhouse-client 의 오류는 stderr 로 나오므로 2>> 로 모읍니다. 채점기는 파일만 믿지 않고 query_log 에 오류 코드 252 로 거절된 INSERT 가 실제로 있는지 봅니다.
파트를 줄여 풀어 준다
parts.guarded 의 병합을 SYSTEM START MERGES 로 다시 켜고 OPTIMIZE TABLE parts.guarded FINAL 로 합친 뒤, 거절됐던 id 6 을 다시 넣으세요. 표에 id 1–6 이 한 번씩, 여섯 행이 있어야 합니다.
문턱을 올리는 것은 증상을 늦출 뿐입니다. 파트 수가 문턱 아래로 내려가면 같은 INSERT 가 들어갑니다. id 6 을 두 번 넣지 않도록 조심하세요 — MergeTree 는 중복을 막지 않습니다.
비동기 INSERT 5번 → 파트 하나
같은 열의 표 parts.async_ev 를 새로 만들고, 두 행짜리 INSERT ... VALUES 를 5번 따로 보내세요. 각 INSERT 에 SETTINGS async_insert = 1, wait_for_async_insert = 0, async_insert_use_adaptive_busy_timeout = 0, async_insert_busy_timeout_max_ms = 600000 을 붙입니다. SYSTEM FLUSH ASYNC INSERT QUEUE 로 버퍼를 비운 뒤, query_log 에서 이 표를 CREATE 한 뒤 끝난 INSERT 수(query_kind = 'Insert'), part_log 의 새 파트 수, 그 파트를 만든 query_id 를 /root/ch/parts/async.json 에 insert_queries·new_parts·flush_query_id 로 적으세요.
기다리지 않는 모드에서는 INSERT 가 버퍼에 넣자마자 돌아오므로 SELECT 로는 아직 0행입니다. 적응형 타이머를 끄고 상한을 크게 두면 시간이 지나 저절로 비워지지 않습니다. 파트를 만든 쿼리는 여러분의 INSERT 가 아니라 버퍼를 비운 쿼리(query_log 의 query_kind 가 AsyncInsertFlush)입니다.
넣는 방식별 파트 수를 한 표로
parts 데이터베이스에 지금 있는 표마다 part_log 의 NewPart 사건 수를 세어 /root/ch/parts/summary.json 에 {"표이름": 수, ...} 모양으로 적으세요. events·big·guarded·async_ev 네 표가 들어 있어야 합니다.
system.tables 와 system.part_log 를 uuid 로 이으면 지금 있는 표의 기록만 셉니다. 네 숫자를 나란히 놓고 보세요 — 문장 10개, 블록으로 잘린 한 문장, 한 행씩, 버퍼로 모은 5문장. 파트 수를 정한 것은 행 수가 아니라 넣는 방식입니다.