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

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

カタログは名前一つをメタデータファイル一つにつなぐ原子的なポインタだ

TT Labで続きを見る

一言でいうと

カタログは「テーブル名 → 現在のmetadata.jsonのパス」という1か所を持っています。その1か所を、読み取った値がそのままのときだけ書き換える条件付き置換によって、コミットの原子性と同時書き込みの裁定を担います。名前・場所・ファイルは、それぞれ別の層です。

なぜカタログが必要なのか

前のモジュールで見たように、Icebergテーブルの状態は1つのmetadata.jsonで決まります。ところが、metadataファイルはコミットのたびに新しくできます。そうなると、誰かが「今のものはどのファイルか」を教える必要があり、2つのライターが同時にコミットしたときには、一方だけを受け付ける必要があります。

初期には、ファイルシステムがこの役を担っていました。仕様のファイルシステムテーブルの方式は、v<V>.metadata.jsonという決まった名前への名前の変更(rename)を試みて、先に成功した側が勝つようにします。HDFSのようにrenameがアトミックな環境では通用します。しかし仕様は今では、この方式を非推奨予定と記し、オブジェクトストレージとローカルファイルシステムでは安全ではないと警告しています。S3にはアトミックなrenameがないからです。

どう動くのか: 条件付き置換

そこで、標準はメタストアテーブル方式になりました。ポインターをDBやサービスに置き、check-and-putで書き換えます。

  1. 現在のmetadata(バージョンV)を読みます。
  2. それをもとに、新しいmetadata<V+1>-<uuid>.metadata.jsonを書きます。
  3. カタログに「ポインターがまだVならV+1に書き換えてください」と要求します。
  4. 成功すればコミット完了です。失敗したなら、誰かが先に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のパスをつなぐだけです。そのため、次のことが成り立ちます。

現場での姿

カタログを移すときは、HiveメタストアからRESTカタログへ移す場合も、データは1バイトも動きません。テーブルごとに現在のmetadataのパスを読み、新しいカタログに登録し、古いカタログからはPURGEなしで削除します。移している間に、2つのカタログが同じテーブルに書き込まないようにすることが核心です。

誤って消したテーブルの場合です。DROP TABLEのあとに容量が減らなくて戸惑う人もいれば、逆にすべて失われたと諦める人もいます。PURGEがなかったなら、metadataファイルのパス1つで復元できます。

実務で本当に大切なこと

次のラボですること

SparkがJDBCカタログ(SQLite)にテーブルを作り、pyicebergで同じカタログを開いてそのテーブルを確認し、新しいテーブルを作ったあと、Sparkが2つのエンジンのテーブルを結合します。コミットの前後で、カタログの1行にある現在と以前のパスがどう動くかを書き留め、テーブル名を変更して場所がそのままであることを確認し、PURGEなしで削除したあとに残ったファイルを数え、register_tableで1つのmetadataファイルからテーブルを復元します。