パートを積み、マージし、上限にぶつける
目標
INSERTがパートをいくつ作るか、マージがそれをどう減らすか、パートが多すぎると何が止まるかを、system.parts・system.part_log・system.query_logで確認します。非同期INSERTで、複数のINSERTを1つのパートにまとめます。
なぜ重要なのか
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個)を1回だけ実行したあと、アクティブなパートの数と名前をactive_parts・namesとして書き込んでください(保存先: /root/ch/parts/parts.json)。- 同じ列のテーブル
parts.bigを新しく作成し、numbers(1000000)から100万行を1回のINSERTで入れてください。その際SETTINGS min_insert_block_size_rows = 250000を付けます。part_logから、そのINSERTが作った新しいパートのquery_id・個数・行数の一覧を、query_id・parts・rowsとして書き込んでください(保存先: /root/ch/parts/blocks.json)。 SYSTEM START MERGES parts.eventsのあと、OPTIMIZE TABLE parts.events FINALでパートを1つにマージし、そのパートの名前・レベルと、part_logのこのテーブルのMergePartsイベントの数を、part・level・merge_eventsとして書き込んでください(保存先: /root/ch/parts/merge.json)。- 同じ列で
SETTINGS parts_to_throw_insert = 5を指定したテーブルparts.guardedを作成してマージを止めたあと、idの1–6を1行ずつ6回のINSERTで入れてください。出たエラー(stderr)を集めてください(保存先: /root/ch/parts/toomany.txt)。 parts.guardedのマージを再び有効にしてOPTIMIZE ... FINALでまとめたあと、拒否されたidの6をもう一度入れて、テーブルがidの1–6の6行になるようにしてください。- 同じ列のテーブル
parts.async_evを新しく作成し、2行の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を、insert_queries・new_parts・flush_query_idとして書き込んでください(保存先: /root/ch/parts/async.json)。 partsデータベースの現在あるテーブルごとに、part_logのNewPartイベントの数を数え、{"표이름": 수, ...}の形で書き込んでください(プレースホルダーはテーブル名と件数です。events・big・guarded・async_evを含めます。保存先: /root/ch/parts/summary.json)。
参考
- パートの確認:
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を2回実行することです。TRUNCATEしてもpart_logの記録は残るので、やり直すなら
DROP TABLE parts.eventsのあと、ステップ1から始めます。ステップ7で1つの文に10行を入れたり、待機するモード(デフォルト)で1件ずつ送ったりすると、パートが1つにまとまりません。 - ステップ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行)を1回だけ実行し、アクティブなパートの数と名前の一覧をactive_parts・namesとして書き込んでください(保存先: /root/ch/parts/parts.json)。
--queries-fileで実行すると、文ごとに別々のINSERTになります。名前の真ん中の2つの数字はブロック番号の最初と最後で、最後の数字はマージレベルです。groupArrayで名前を配列にまとめてJSONEachRowで受け取れば、そのまま保存できます。
1回のINSERTが複数のパートになるとき
同じ列のテーブルparts.bigを新しく作成し、numbers(1000000)から100万行を1回のINSERT ... SELECTで入れてください。その際SETTINGS min_insert_block_size_rows = 250000を付けます。part_logからこのテーブルのNewPartイベントを読み、それを作ったquery_id、新しいパートの個数、パートごとの行数の一覧を、query_id・parts・rowsとして書き込んでください(保存先: /root/ch/parts/blocks.json)。
INSERT ... SELECTは、読んだブロックをmin_insert_block_size_rowsの分だけ集まるまでつなげてから、パートとして書きます。デフォルトは約100万行なので、そのままなら1つのパートで終わります。part_logのquery_idは、自分が送ったINSERTのidです。テーブルを作り直した場合は、table_uuidで絞り込まないと、古い記録が混ざります。
マージを有効にして1つにまとめる
SYSTEM START MERGES parts.eventsのあと、OPTIMIZE TABLE parts.events FINALでパートを1つにマージしてください。まとめたパートの名前とレベル(system.partsのname・level)と、part_logのこのテーブルのMergePartsイベントの数を、part・level・merge_eventsとして書き込んでください(保存先: /root/ch/parts/merge.json)。
マージを止めたままOPTIMIZEを実行すると、Cancelled merging partsで拒否されます。マージを有効にした瞬間に、バックグラウンドが先にまとめてしまうことがあり、FINALはすでに1つのパートでももう1回書き直します。そのため、レベルが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を1行ずつ6回のINSERTで入れ、出たエラー出力(stderr)を集めてください(保存先: /root/ch/parts/toomany.txt)。
しきい値はパーティション1つのアクティブなパートの数にかかります。1行だけのINSERTでも、パートが1つできます。clickhouse-clientのエラーはstderrに出るので、2>>で集めます。採点ツールはファイルだけを信用せず、query_logにエラーコード252で拒否されたINSERTが実際にあるかを確認します。
パートを減らして解除する
parts.guardedのマージをSYSTEM START MERGESで再び有効にし、OPTIMIZE TABLE parts.guarded FINALでまとめたあと、拒否されたidの6をもう一度入れてください。テーブルにidの1–6が1つずつ、6行あるようにします。
しきい値を上げるのは、症状を遅らせるだけです。パートの数がしきい値を下回れば、同じINSERTが入ります。idの6を2回入れないよう注意してください。MergeTreeは重複を防ぎません。
非同期INSERT 5回で、パート1つ
同じ列のテーブルparts.async_evを新しく作成し、2行の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を、insert_queries・new_parts・flush_query_idとして書き込んでください(保存先: /root/ch/parts/async.json)。
待機しないモードでは、INSERTがバッファに入れた時点で戻るので、SELECTではまだ0行です。適応型タイマーを無効にして上限を大きくしておくと、時間がたっても自然にはフラッシュされません。パートを作ったクエリは、自分のINSERTではなく、バッファを空にしたクエリ(query_logのquery_kindがAsyncInsertFlush)です。
挿入方式ごとのパート数を1つの表にまとめる
partsデータベースに現在あるテーブルごとに、part_logのNewPartイベントの数を数えて、{"표이름": 수, ...}の形で書き込んでください(プレースホルダーはテーブル名と件数です)。events・big・guarded・async_evの4つのテーブルが含まれている必要があります(保存先: /root/ch/parts/summary.json)。
system.tablesとsystem.part_logをuuidでつなぐと、現在あるテーブルの記録だけを数えられます。4つの数字を並べて見てください。10個の文、ブロックで切られた1つの文、1行ずつ、バッファでまとめた5つの文です。パートの数を決めたのは、行数ではなく挿入の方式です。