はじめに
前回の記事では Docker Compose を使ったローカル環境のセットアップを行いました。
今回は Iceberg の内部でデータ更新がどのように処理されるかを見ていきます。
本記事では、
- Iceberg が持つ ACID 特性とその意味
- データ更新時にファイルがどの順番で書き換えられるか
を説明します。
ACID 特性
Iceberg は ACID トランザクションをサポートしています。
ACID とは以下の 4 つの特性の頭文字です。
Atomicity(原子性):
トランザクションは「全て成功」か「全て失敗」のどちらかになります。
途中で失敗した場合、テーブルの状態は変化しません。
Consistency(一貫性):
トランザクションの前後でテーブルは常に整合性のとれた状態を保ちます。
Isolation(独立性):
複数のトランザクションが同時に実行されても、互いに干渉しません。
Iceberg は OCC(楽観的同時実行制御) を採用しています。
書き込み時に他の書き込みとの競合を検知し、競合した場合はリトライまたはエラーとなります。
読み取りは書き込みをブロックしないため、書き込み中でも一貫したデータを参照できます。
Durability(永続性):
一度コミットされたデータは、障害が発生しても失われません。
データ更新フロー
Iceberg の特徴は、データを 上書きせず、常に新しいファイルを追記する 点です。
更新はファイルの書き込みとメタデータの付け替えによって実現されます。
INSERT を例に、更新フローを順番に見ていきます。
[更新前の状態]
カタログ
└── メタデータファイル (v1)
└── マニフェストリスト (snapshot-1)
└── マニフェストファイル (manifest-1)
└── データファイル (data-1.parquet)
Step 1:データファイルの書き込み
まず、新しいレコードを含むデータファイルがオブジェクトストレージに書き込まれます。
この時点では、テーブルの状態はまだ変わりません。
新規作成: data-2.parquet
Step 2:マニフェストファイルの作成
新しいデータファイルを参照するマニフェストファイルが作成されます。
マニフェストファイルには、データファイルのパスに加えて統計情報(レコード数・最小値・最大値)も記録されます。
新規作成: manifest-2
└── data-2.parquet(パス・統計情報)
Step 3:マニフェストリストの作成
新しいスナップショットとして、マニフェストリストが作成されます。
マニフェストリストは、このスナップショットを構成する全マニフェストファイルへのポインタを持ちます。
新規作成: snapshot-2(マニフェストリスト)
├── manifest-1(既存)
└── manifest-2(新規)
Step 4:メタデータファイルの作成
新しいメタデータファイルが作成されます。
スキーマ情報やパーティション定義に加え、「現在のスナップショット = snapshot-2」という情報が記録されます。
新規作成: メタデータファイル (v2)
├── スキーマ情報
├── パーティション定義
├── snapshot-1(過去)
└── snapshot-2(現在)← current
Step 5:カタログの更新(アトミックコミット)
最後に、カタログが新しいメタデータファイル(v2)を指すよう アトミックに 更新されます。
この操作が成功した瞬間にコミット完了となり、他のクライアントから新しいデータが見えるようになります。
カタログ: メタデータファイル v1 → v2(アトミック更新)
更新後の状態
[更新後の状態]
カタログ
└── メタデータファイル (v2)
├── snapshot-1(過去スナップショット)
│ └── manifest-1
│ └── data-1.parquet
└── snapshot-2(現在スナップショット)← current
├── manifest-1
│ └── data-1.parquet
└── manifest-2
└── data-2.parquet
失敗した場合
Step 5 のカタログ更新が完了する前に失敗した場合、カタログは依然として v1 を指したままです。
新しく書き込まれたファイル群(data-2.parquet、manifest-2、snapshot-2、v2)はストレージ上に残りますが、テーブルからは参照されないため、テーブルの状態は変化しません。
これにより Atomicity(原子性) が保証されます。
なお、参照されなくなったファイルはメンテナンス処理で定期的に削除します。
競合検知の仕組み
OCC では、競合の検知は Step 5 のカタログ更新のタイミング で行われます。
Step 1〜4 の間、書き込み処理はロックを取得せず並行して進みます。
複数の書き込みが同時に進行しても互いを意識しません。
Step 5 でカタログを更新する際、Iceberg は「自分が読み取った時点のバージョンと、現在のカタログのバージョンが一致している場合のみ更新する」という条件付き更新を行います。
「書き込みを始めた後、他の誰かが先にカタログを書き換えていたら失敗する」というイメージです。
2 つの書き込みが同時に進行した場合の例を見てみます。
Writer A: v1 を読み取り → データ書き込み → メタデータ作成 → v2 でコミット試行 → 成功
カタログ: v1 → v2
Writer B: v1 を読み取り → データ書き込み → メタデータ作成 → v2' でコミット試行
カタログ: v2(≠ v1)→ 競合を検知
Writer A が先にコミットするとカタログは v2 に変わります。
Writer B がコミットを試みると「カタログが v1 であること」という条件を満たさないため、競合が検知されます。
競合後の動作
競合が検知された場合、Iceberg はまず 競合の内容を評価 します。
リトライ可能な競合:
操作対象のパーティションやファイルが重複していない場合、競合は解消できると判断されます。
Iceberg は最新のメタデータを再読み込みしてコミットを自動的にリトライします。
例えば、Writer A と Writer B が異なるパーティションに書き込んでいた場合がこれに該当します。
リトライ不可能な競合:
同じパーティションやファイルへの変更が衝突する場合、自動解消はできません。
この場合はエラーとなり、アプリケーション側でのハンドリングが必要です。
過去データが参照できる理由
更新フローの通り、Iceberg は既存のファイルを上書きしません。
古いスナップショット(snapshot-1)の情報がメタデータファイルに残っているため、過去時点のデータをそのまま参照できます。
これがタイムトラベルの仕組みです。
おわりに
Iceberg の更新フローは「新規ファイルの追記」と「メタデータの付け替え」によって実現されています。
カタログへのアトミックな書き込みがコミットの確定点となることで、ACID 特性が保証されています。
次回はスナップショットとタイムトラベルについて詳しく説明していきます。
乞うご期待!