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

レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する

ロックなしで同時に書く — 楽観的並行性が守るものと守らないもの

TT Labで続きを見る

一言でいうと

Icebergのライターは、ロックなしでそれぞれ新しいmetadataを作ったあと、カタログで条件付き置換を試み、負けたら新しい状態を読み直して載せ直します。appendのように、いつ載せ直してもよい変更は自動で解決し、上書きのように、その間の変更と重なる可能性があるものは、検証に引っかかって止まります。しかし、皆さんのコードが以前に読んだ値で計算した結果は、誰も検証してくれません。

なぜロックを使わないのか

1つのテーブルに書き込むジョブは、たいてい複数あります。ストリーミングのロードジョブが数分おきにappendし、夜間バッチが昨日のパーティションを直し、メンテナンス処理が小さなファイルをコンパクションします。テーブル全体にロックをかけると、最も遅いジョブがすべてを止めてしまいます。しかもオブジェクトストレージの上には、信頼できる分散ロックがありません。

仕様の楽観的同時実行制御の節は、別の道を選びます。ライターは、自分のコミットまでcurrentが変わらないと仮定してmetadataを作ったあと、基になったバージョンから新しいバージョンへ、ポインターを書き換えます。基にしたスナップショットがもうcurrentでなければ、新しいcurrentを基にやり直します。読み取る側は、自分が開いたスナップショットを最後まで見るので、ロックする必要がありません。

どう動くのか: 前提と動作

Reliabilityドキュメントは、コミットを前提と動作で説明します。衝突が起きると、ライターは前提が現在の状態でも成り立つかを確認し、成り立てば動作を再適用してコミットします。仕様のコミット競合の解決の節が、操作ごとにその前提を定めています。

操作 載せ直す前に確認すること
append なし。いつでも載せ直せる
replace(コンパクションなど) 消そうとしたファイルが、まだテーブルにあるか
delete(特定のファイル) 消そうとしたファイルが、まだテーブルにあるか
スキーマ・仕様の変更 その間にスキーマが変わっていないか

appendがいつでも載せ直せる理由は、「新しいファイルを加える」という動作が、その間に何が入ってきても、やはり正しいからです。ドキュメントは、リトライのコストも抑えてあると記しています。appendは、新しいマニフェストを1回書いておき、試行のたびに書き直しません。

リトライの回数と間隔は、テーブルプロパティです。commit.retry.num-retriesがデフォルト4、commit.retry.min-wait-msが100、commit.retry.max-wait-msが60000、全体の制限のcommit.retry.total-timeout-msが30分です。リトライを使い切ると、コミット失敗の例外が上がります。

分離レベル: 何を衝突とみなすか

仕様は、「どんな条件を検証するかが分離レベル」と記しています。SparkのDELETE・UPDATE・MERGEは、テーブルプロパティwrite.delete.isolation-level(updateとmergeも同じ名前)で選び、デフォルトはserializableです。serializableは、自分が読んで変更した範囲に、その間に誰かが追加したか削除したものがあれば失敗し、snapshotは削除したものだけを衝突とみなします。Spark書き込みオプションのDataFrame上書きにも、同じ2段階があります。緩いほど失敗は減りますが、その間に入ってきた行を知らないまま上書きしてしまうことがあります。

ライブラリが守ってくれないもの

カウンターを考えてみましょう。2人のワーカーがhits = 10を読んで、それぞれ+5して15で上書きします。先に進んだ側が勝ちます。負けた側は条件付き置換で負け、リトライは「自分が上書きしようとした条件(name = hits)に合うファイルが、その間に入ってきた」という検証に引っかかって止まります。ここまでは、ライブラリがやってくれます。

問題はその先です。例外を捕まえて、以前に計算した15をそのまま書き直すと、今度はコミットが成功します。テーブルは15になり、片方の+5が消えます。これが、更新の喪失です。ライブラリはファイルレベルの前提を検証するだけで、皆さんの値がどの読み取りから来たかは知りません。正しいリトライは、新しく読み、計算し直し、その読み取りを前提にもう一度コミットすることです。

現場での姿

コンパクションとロードがぶつかる場合です。コンパクション(replace)は、消そうとしたファイルがまだあれば、ロードが割り込んでも載せ直されます。逆に、MERGEが同じファイルを変更していたら、コンパクションが失敗するので、次の周期でやり直せば済みます。

失敗したコミットの痕跡もあります。コミットに負けたライターがすでに書いたデータファイルは、どのスナップショットにも入らないまま残ります。テーブルには影響がありませんが、容量を占めます。このようなファイルを片づけるのが、孤立ファイルの整理です。

リトライ設定を0にした場合です。1つのテーブルに書き込むジョブがちょうど1つだと信じてリトライを切ると、たまに動くメンテナンス処理とぶつかる日に、ジョブが理由もなく失敗します。

実務で本当に大切なこと

次のラボですること

pyicebergで、2人のライターを1つのプロセスの中の2つのTableオブジェクトとして作り、同じmetadataを読ませてから順番にappendして、負けた側が自動で載せ直されて、スナップショットが1本の線でつながることを確認します。リトライを0にして同じ競合をもう一度起こし、例外の名前と、負けた側が残した孤立ファイルを探します。最後に、カウンター1つを使って上書きを競合させて検証例外を確認し、新しく読んで計算し直して、カウンターがちょうど20になるようにします。