レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
カタログは名前一つをメタデータファイル一つにつなぐ原子的なポインタだ
一言でいうと
カタログは「テーブル名 → 現在のmetadata.jsonのパス」という1か所を持っています。その1か所を、読み取った値がそのままのときだけ書き換える条件付き置換によって、コミットの原子性と同時書き込みの裁定を担います。名前・場所・ファイルは、それぞれ別の層です。
なぜカタログが必要なのか
前のモジュールで見たように、Icebergテーブルの状態は1つのmetadata.jsonで決まります。ところが、metadataファイルはコミットのたびに新しくできます。そうなると、誰かが「今のものはどのファイルか」を教える必要があり、2つのライターが同時にコミットしたときには、一方だけを受け付ける必要があります。
初期には、ファイルシステムがこの役を担っていました。仕様のファイルシステムテーブルの方式は、v<V>.metadata.jsonという決まった名前への名前の変更(rename)を試みて、先に成功した側が勝つようにします。HDFSのようにrenameがアトミックな環境では通用します。しかし仕様は今では、この方式を非推奨予定と記し、オブジェクトストレージとローカルファイルシステムでは安全ではないと警告しています。S3にはアトミックなrenameがないからです。
どう動くのか: 条件付き置換
そこで、標準はメタストアテーブル方式になりました。ポインターをDBやサービスに置き、check-and-putで書き換えます。
- 現在のmetadata(バージョンV)を読みます。
- それをもとに、新しいmetadata
<V+1>-<uuid>.metadata.jsonを書きます。 - カタログに「ポインターがまだVならV+1に書き換えてください」と要求します。
- 成功すればコミット完了です。失敗したなら、誰かが先にV+1を作ったということなので、ステップ1からやり直します。
この契約を守る実装が、複数あります。
| カタログ | ポインターがある場所 | 条件付き置換 |
|---|---|---|
| JDBC | DBテーブルiceberg_tablesの1行 |
metadata_locationが読み取った値と同じときだけUPDATE |
| REST | カタログサーバー | サーバーが要求条件を検査してコミット |
| Hiveメタストア | HMSのテーブルプロパティmetadata_location |
メタストアのロックとともに置換 |
JDBCカタログのドキュメントは、接続するDBがアトミックなトランザクションをサポートしていてはじめて、アトミックなコミットと直列化可能な読み取りが保証されると記しています。このコースのラボ環境は、SQLiteファイル1つにこのテーブルを置き、Spark(JavaのJdbcCatalog)とpyiceberg(SqlCatalog)が同じファイルを開きます。サーバーを起動しなくても、2つのエンジンが1つのカタログを共有できます。
RESTカタログ仕様は、コミットを要求条件(requirements)と変更(updates)の2つの部分に分けます。assert-ref-snapshot-idのような要求条件をサーバーが先に検査し、満たされなければ409 CommitFailedExceptionを返し、クライアントは再試行できます。サーバーがコミットを受け付けるので、クライアントがストレージの認証情報を直接持つ必要が減り、複数のテーブルを一度にアトミックにコミットする経路もあります。
Hiveメタストアは、既存のHiveエコシステムとつなぎやすい方式です。Hiveドキュメントの例のように、ポインターはHMSのテーブルプロパティmetadata_locationに入っています。ただしHMSは、IcebergではなくHiveのために作られたサービスなので運用する部品が増え、同じドキュメントは、複数のテーブルにまたがるINSERTがアトミックではないと記しています。
名前・場所・ファイルは別の層
カタログの1行は、名前とmetadataのパスをつなぐだけです。そのため、次のことが成り立ちます。
- 名前の変更は、その行の名前だけを書き換えます。テーブルの場所とファイルはそのままです。名前が変わったテーブルが古い名前のディレクトリを使い続けるのは、正常です。
- PURGEなしのDROP TABLEは、その行だけを消します。metadataとデータファイルはディスクに残ります。
- register_tableは、すでにあるmetadata.jsonに新しい名前を付けます。metadataファイルがスキーマ・スナップショット・ファイル一覧をすべて持っているので、パス1つでテーブルが復元できます。同じドキュメントは、1つのmetadataを2つのカタログに同時に登録すると更新が失われ、テーブルが破損することがあると警告しています。ポインターが2つあれば、裁定役も2つになるからです。
現場での姿
カタログを移すときは、HiveメタストアからRESTカタログへ移す場合も、データは1バイトも動きません。テーブルごとに現在のmetadataのパスを読み、新しいカタログに登録し、古いカタログからはPURGEなしで削除します。移している間に、2つのカタログが同じテーブルに書き込まないようにすることが核心です。
誤って消したテーブルの場合です。DROP TABLEのあとに容量が減らなくて戸惑う人もいれば、逆にすべて失われたと諦める人もいます。PURGEがなかったなら、metadataファイルのパス1つで復元できます。
実務で本当に大切なこと
- カタログの仕事は、条件付き置換です。アトミックに書き換えられないストレージの上では、テーブルが壊れます。
- エンジンが複数あっても、カタログは1つでなければなりません。同じテーブルを2つのカタログが指すと、コミットが互いを上書きします。
- 名前はカタログにしかありません。名前を変えても、ファイルは移動しません。
- PURGEは元に戻せません。PURGEなしのDROPは、metadataのパスさえあれば復元できます。
次のラボですること
SparkがJDBCカタログ(SQLite)にテーブルを作り、pyicebergで同じカタログを開いてそのテーブルを確認し、新しいテーブルを作ったあと、Sparkが2つのエンジンのテーブルを結合します。コミットの前後で、カタログの1行にある現在と以前のパスがどう動くかを書き留め、テーブル名を変更して場所がそのままであることを確認し、PURGEなしで削除したあとに残ったファイルを数え、register_tableで1つのmetadataファイルからテーブルを復元します。