TT Lab
はじめる
学ぶ 学習パス コース

ClickHouse — 列指向分析 DB を中身から

パートを積み、マージし、上限にぶつける

TT Labで続きを見る

目標

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のエラーコードで確認します。

ステップ

  1. データベースpartsとテーブルparts.eventsを作成してください。列はid UInt64, grp UInt8, v UInt32、エンジンはMergeTree、ORDER BY idです。そのあとSYSTEM STOP MERGES parts.eventsで、このテーブルのマージを止めてください。
  2. /opt/lab/fixtures/parts/batches.sql(INSERT文10個)を1回だけ実行したあと、アクティブなパートの数と名前をactive_parts・namesとして書き込んでください(保存先: /root/ch/parts/parts.json)。
  3. 同じ列のテーブル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)。
  4. SYSTEM START MERGES parts.eventsのあと、OPTIMIZE TABLE parts.events FINALでパートを1つにマージし、そのパートの名前・レベルと、part_logのこのテーブルのMergePartsイベントの数を、part・level・merge_eventsとして書き込んでください(保存先: /root/ch/parts/merge.json)。
  5. 同じ列でSETTINGS parts_to_throw_insert = 5を指定したテーブルparts.guardedを作成してマージを止めたあと、idの1–6を1行ずつ6回のINSERTで入れてください。出たエラー(stderr)を集めてください(保存先: /root/ch/parts/toomany.txt)。
  6. parts.guardedのマージを再び有効にしてOPTIMIZE ... FINALでまとめたあと、拒否されたidの6をもう一度入れて、テーブルがidの1–6の6行になるようにしてください。
  7. 同じ列のテーブル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)。
  8. partsデータベースの現在あるテーブルごとに、part_logのNewPartイベントの数を数え、{"표이름": 수, ...}の形で書き込んでください(プレースホルダーはテーブル名と件数です。events・big・guarded・async_evを含めます。保存先: /root/ch/parts/summary.json)。

参考

テーブルを作成してマージを止める

データベース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つの文です。パートの数を決めたのは、行数ではなく挿入の方式です。