代表的な面接トピック

データエンジニアリング面接:Apache Iceberg スキーマを安全に進化させるには?

データ難しい
Offer.cc 編集チーム公開日 更新日

質問

複数のエンジンによって書き込まれ、18か月分の履歴を含むIcebergファクトテーブルがあります。古いクエリ、並行書き込み、ストリーミング読み取り、ロールバックの安全性を維持しながら、customer_nameをfirst_nameとlast_nameに分割し、日次パーティショニングを月次パーティショニングに変更する必要があります。スキーマ進化、パーティション進化、ロールアウト順序、検証、およびリカバリについて説明してください。

プロンプトとコンテキスト

あるApache Icebergファクトテーブルには、Sparkバッチジョブ、Flinkストリーム、Trinoクエリが同時に実行されている状態で18か月分の注文データが保存されています。チームはcustomer_namefirst_namelast_nameに分割し、パーティショニングを日単位から月単位に変更したいと考えています。すべての履歴ファイルを書き直すことはできず、古いクエリは機能し続ける必要があり、ストリーミング書き込みを停止することはできず、失敗したロールアウトは可逆的(ロールバック可能)でなければなりません。

この面接では、候補者がスキーマ互換性、フィールドID、パーティションレイアウト、スナップショットコミット、およびリーダー/ライターのアップグレードを明確に区別できているかを評価します。Icebergは安定したフィールドIDと、独立したスキーマおよびパーティションの進化を仕様として備えています。優れた回答とは、「テーブルを変更してバックフィルする」と言うのではなく、これらの保証を行き届いた段階的移行計画に落とし込むことです。

面接官が評価しているポイント

名前とフィールドIDの違い、スキーマ進化と履歴バックフィルの違い、新しいパーティション仕様と古いファイルの物理的移動の違いを理解しているかを確認します。候補者は、スナップショットのアトミック性、エンジンの互換性、ロールバックの証拠、さらにはライターが移行と競合(レース)した場合に何が起こるかを説明できる必要があります。

明確化のための質問

  • どのSpark、Flink、Trino、カタログ、Icebergのバージョンがテーブルを読み書きしていますか?
  • customer_nameは、言語、単一の名前(モノニム)、プライバシー制限を越えて確実にパースできますか?
  • 古いクライアントは位置(ポジション)、SELECT *、またはシリアライズされたスキーマに依存していますか?
  • リーダーはパーティション仕様が混在していても、日付述語のプッシュダウン(predicate pushdown)を適用できますか?
  • ストリーミングのチェックポイントとスキーマバージョンはどのように連携していますか?
  • カタログはアトミックコミット、スナップショット保持、IDによるロールバックをサポートしていますか?

30秒の回答

「エンジンの互換性とカラムの使用状況を棚卸しし、IcebergのフィールドIDを使用して新しいカラムを追加し、観察期間中は古いカラムを保持します。パーティションの進化は独立しており、古いファイルが読み取り可能な状態を維持したまま、新しいファイルには月次変換を使用します。互換性のあるリーダー、ライター、コンシューマーの順にロールアウトし、クエリ、チェックポイント、並行コミットを検証し、ロールバック用に以前のスナップショットを保持します。履歴のバックフィルはバージョン管理されたジョブであり、暗黙のスキーマ変更ではありません。」

ステップバイステップの解決策

ステップ 1: 互換性マトリックスと変更契約を定義する

スキーマ、フィールドID、現在のパーティション仕様、スナップショット保持期間、ライターのバージョン、リーダーのバージョンを記録します。パース、名前変更、物理的な書き換えがひとつの不透明なコミットにならないよう、カラムのセマンティクスとパーティションレイアウトに対して別々の契約を維持します。

分割はまず追加的なものとして扱います。first_namelast_nameを追加し、customer_nameを保持し、空白、モノニム、多言語の名前、パース失敗に対するルールを定義します。古いカラムに新しい意味を暗黙的に割り当ててはなりません。すべてのコンシューマーが移行を完了した後にのみ削除を計画します。

ステップ 2: カラムの位置ではなくフィールドIDを使用する

Icebergは安定したIDによってフィールドをバインドします。変更レコードは次のようになります:

text
old: id=7 customer_name:string
new: id=21 first_name:string, id=22 last_name:string
old id=7 remains until consumers migrate

削除後に異なる意味でID 7を再利用してはなりません。ネストされたstruct、map、listの子IDも確認してください。CIは変更前後のIDマップを比較する必要があります。名前だけの変更は安全な進化の証明にはなりません。

ステップ 3: オンライン書き込みからバックフィルを分離する

まずバッチおよびストリーミングライターに新しいカラムの値を設定させ、パース品質メトリクスを公開します。履歴ファイルのバックフィルは後からパーティションごとに実行します。固定されたスナップショットまたはウォーターマークを読み取り、入力スナップショット、コードバージョン、行数、パース失敗、チェックサムをマニフェストに記録します。並行するIcebergコミットが競合した場合は、最新のスナップショットを再読み込みして再試行します。他のライターのコミットを決して上書きしてはなりません。

履歴の名前を確実にパースできない場合は、アイデンティティデータを捏造するのではなく、nullに加えてname_parse_statusを保持します。バックフィルは独立したバージョン管理された計算処理であり、スキーマコマンド自体の要件ではありません。

ステップ 4: パーティション仕様を個別に進化させる

古いファイルが日次仕様を維持する一方で、新しいデータには月次変換を使用するパーティション仕様を追加します。プランナーは各ファイルの仕様を識別し、適切にプルーニング(不要なファイルの除外)を行う必要があります。

text
spec-0: day(ts)      -> existing files
spec-1: month(ts)    -> new files

シャドウテーブルでスキャン量、プルーニング、および小さなファイルの挙動をテストします。コンシューマーはディレクトリ名からオブジェクトストレージのパスを構築するのではなく、カタログ経由でメタデータを読み取る必要があります。

ステップ 5: リーダーとライターのリリースを段階化する

まず両方のカラムを許容するリーダーをデプロイし、次に新しいカラムに書き込むライターをデプロイし、その後に新しいフィールドを必要とするコンシューマーをデプロイします。両方のスナップショットに対してストリーミングチェックポイントのリカバリを検証します。名前変更、ネストされた型、型の昇格、変換、混在した仕様に対するエンジン固有のサポートを記録します。サポートが不足している場合は、ビューまたはエンジンのアップグレードを使用します。

各変更を小さなスナップショットにし、ライター、カタログ、IDマップ、パーティション仕様を記録します。同じ観察ウィンドウ内でカラムの削除、型の変更、大規模な書き換えを組み合わせることは避けてください。

ステップ 6: コミットをゲートで検証しロールバックを確保する

ID、型、仕様に関する構造チェック、行数、null率、パース失敗、集計、日付スライスに関するデータチェック、そして新旧SQL、チェックポイントリカバリ、コミット競合、プルーニングに関する動作チェックを実行します。レビュー用にパースできなかったサンプルを保持します。

リリース前にprevious_snapshot_idnew_snapshot_idを保存します。ゲートの検証に失敗した場合は、カタログを古いスナップショットに戻し、新しいファイルと証拠を保持し、新しいカラムを必要とするコンシューマーを一時停止し、固定された入力から影響を受けるパーティションを再実行します。

模範解答

「まず、フィールドIDに対するSpark、Flink、Trino、カタログのサポートを棚卸しし、次にcustomer_nameを保持しながら2つの新しいカラムを追加します。決定論的パーサーとname_parse_statusで品質を定量化します。履歴バックフィルには固定スナップショット、コードバージョン、マニフェストを使用します。新しいファイルはmonth(ts)を使用し、古いファイルはday(ts)を使用します。テーブルメタデータは両方を処理します。」

「互換性のあるリーダー、ライター、コンシューマーの順にアップグレードします。リリース前に、ID、null、集計、新旧クエリ、ストリームリカバリ、並行コミットを検証します。前のスナップショットを保持し、いずれかのハードゲートが失敗した場合はカタログポインタをロールバックします。」

よくある間違い

  • カラムの位置を識別子として使用する → 古いバイトが誤って読み取られる → 安定したフィールドIDを検証する。
  • 名前変更を新しいセマンティックカラムとして扱う → 古い値が新しい意味を取得してしまう → 追加し、非推奨にし、その後削除する。
  • 仕様変更後にすべての古いファイルを移動する → 膨大なコミットと困難なロールバックが発生する → 仕様を共存させ、選択的に書き換える。
  • 新しいカラム専用のリーダーを最初にアップグレードする → 古いライターがnullを出力する → 互換性のあるリーダー、ライター、コンシューマーの順にする。
  • スキーマコマンドのみをテストする → クエリやストリームリカバリが失敗する → 構造、データ、動作のゲートを実行する。
  • 本番のスナップショットに直接バックフィルする → クリーンなリカバリポイントが存在しなくなる → バージョン管理された出力とスナップショットIDを使用する。

フォローアップの質問と回答

フォローアップ 1: 削除されたフィールドIDを絶対に再利用してはならないのはなぜですか?

古いデータファイルには依然として元のIDが含まれています。これを再利用すると、リーダーは古いバイトを新しいセマンティックフィールドとして解釈してしまいます。そのため、新しいIDを割り当て、コンシューマーと履歴リプレイが移行するまで古いフィールドを保持します。

フォローアップ 2: 新旧のパーティション仕様は安全に共存できますか?

はい、メタデータが各ファイルの仕様を記録し、実際のエンジンが変換と述語を正しく適用していれば共存可能です。ディレクトリ名から動作を推測するのではなく、本番環境と同等のクエリを使用してプルーニング、タイムゾーンの動作、および小さなファイルの数を確認してください。

フォローアップ 3: 並行コミットの競合からどのように回復しますか?

最新のスナップショットを読み取り、入力が引き続き有効であることを確認し、影響を受けるパーティションのみを再計算して、再度送信します。他のライターのスナップショットに対して古いメタデータファイルを強制的に適用してはなりません。競合が繰り返される場合は、並行性を下げるか、スライスを小さくする必要があります。

フォローアップ 4: 古いカラムはいつ削除できますか?

リーダー、ライター、リプレイ、監査、エクスポートの移行が完了し、観察期間が安定し、保持されているスナップショットがロールバックウィンドウをカバーしている時点です。削除を送信する前に、隠れたコンシューマーを棚卸ししてください。

フォローアップ 5: パースできない名前はどう処理すべきですか?

nullに加えて理由と元の値への制御された参照を書き込み、言語およびフォーマットごとに監視し、アイデンティティフィールドに推測を入れないようにします。以降のバージョンまたは人間のレビューワークフローで修正します。

公開情報ソース

関連する質問