代表的な面接トピック

データエンジニアリング面接:レイクハウスにおけるスモールファイル問題をどのように診断し修正するか?

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

質問

20 TiBのIcebergイベントテーブルが1日あたり200 GiBを受信していますが、約80,000個のParquetファイルを生成しており、クエリプランニング時間が上昇し続けています。原因を特定し、新たなスモールファイルの発生を抑え、安全に既存のバックログをコンパクションするにはどうすればよいですか?

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

オブジェクトストレージ上のApache Icebergイベントテーブルには20 TiBのアクティブデータがあります。ストリーミングジョブは1分ごとのマイクロバッチをコミットしています。1日あたり約200 GiBを追加していますが、中央値サイズがわずか3 MiBのParquetデータファイルを約80,000個作成しています。クエリのp95は最近20秒から95秒に増加し、プランニングフェーズは4秒から32秒に増加しました。ビジネス要件として、ほぼリアルタイムの取り込みを維持し、7日間のタイムトラベルを保持し、ストレージから直接テーブルオブジェクトを削除してはなりません。

スモールファイルが性能低下の主な要因であることを証明し、書き込みおよびパーティショニングの原因を見つけ、増加を食い止め、既存のファイルを安全にコンパクションする方法を説明してください。並行性制御、リソースの予算設定、ロールバック、受け入れ基準も含めてください。

すべての容量、ファイル数、レイテンシ、スループットの値は面接用の前提条件であり、普遍的なベンチマークではありません。この質問は、データエンジニアリング、アナリティクスプラットフォーム、レイクハウスインフラストラクチャ、データプラットフォームSREの役割に適しています。その核となる能力はデータレイアウトとテーブルメンテナンスであるため、カテゴリはdataです。単なるSpark構成やオブジェクトストレージ運用の質問ではありません。

面接官が評価していること

第一に、候補者が証拠の連鎖を構築できるか?ファイル数が多いことだけでは因果関係は成立しません。アクティブファイル数、サイズ分布、パーティションの偏り(スキュー)、マニフェスト読み取り時間、タスク起動、ファイルオープンオーバーヘッドを、クエリのプランニングとスキャンに個別に結び付ける必要があります。

第二に、候補者がバックログの修正と予防を区別できるか?コンパクションは既存のファイルを処理します。マイクロバッチの頻度、ライター数、データ分散、過度に細かいパーティショニングが変更されないままであれば、テーブルは再び断片化します。

第三に、候補者がテーブルフォーマットのトランザクション境界を尊重しているか?Icebergのコンパクションはデータファイルを書き換え、新しいスナップショットをコミットします。古いスナップショットは依然として古いファイルを参照できるため、メタデータの裏でオブジェクトを削除するのは安全ではありません。

第四に、候補者が境界のあるトレードオフを行えるか?ビンパッキング(Bin packing)は主にファイルサイズを変更します。ソートやZ-orderingはクラスタリングも変更しプルーニングを改善できますが、シャッフル、ソート、一時ストレージのコストが増加します。

第五に、候補者が運用計画を定量化できるか?優れた回答は、1日に書き換えられるバイト数、目標ファイル数、ジョブウィンドウ、競合リスクを見積もり、正確性とパフォーマンスの両方の指標を使用してロールアウトを拡大するかどうかを決定します。

回答前の明確化のための質問

  • 「小さい」を定義する目標サイズは何か? write.target-file-size-bytesを読み、クエリ選択性、パーティションごとの1日あたりのボリューム、エンジンのテストを使用してしきい値を選択します。すべてのテーブルに当てはまる単一の固定サイズはありません。
  • 80,000個のファイルは現在のスナップショットでアクティブなのか、それとも過去のスナップショット全体でカウントされているのか? クエリパスにはfilesから始め、保持期間とストレージコストにはall_filesとスナップショット参照を使用します。
  • レイテンシはプランニングにあるのかスキャンにあるのか? プランニングの割合の増加はマニフェストとファイルタスクを示唆します。スキャンスループットの低下は、スキュー、削除ファイル(delete files)、圧縮、カラム統計、ダウンストリームリソースの確認も必要とします。
  • パーティション仕様と書き込み分散はどうなっているか? 高カーディナリティまたは過度に細かい時間パーティションは、目標サイズのファイルを蓄積できない可能性があります。過剰な並列ライターがそれぞれ部分的に埋まったファイルをコミットする可能性があります。
  • テーブルはcopy-on-writeまたはmerge-on-readを使用しているか? Merge-on-readは位置または等価削除ファイルを蓄積する可能性があるため、データファイルのみのコンパクションでは不十分な場合があります。
  • どのパーティションが依然として遅延データを受信しているか? クローズされたコールドパーティションを優先します。ホットパーティションには、より小さなファイルグループ、制御された並行性、競合リトライが必要です。
  • 7日間の保持とは何を意味するか? スナップショットのクエリ可能性、ブランチまたはタグの保持、オブジェクトストレージのライフサイクルを個別に確認します。ディレクトリの削除でこれら3つすべてを代替することはできません。

30秒の回答フレームワーク

「現在のIcebergスナップショットからアクティブファイル、サイズパーセンタイル、パーティションをプロファイリングし、プランニングとスキャンを分離します。1日あたり200 GiBで、理想的な512 MiBのファイル約400個に対して80,000個のファイルが存在することは、マイクロバッチ、ライター、パーティションの粒度が最初の容疑者であることを示しています。

バッチを拡大し、パーティションキーで分散し、ライター数を制御することで、新たな断片化を食い止めます。その後、プルーニングテストで正当化された場合にのみソートを行い、制限された並行性で1つのコールドパーティションをビンパッキングします。コンパクションは新しいスナップショットをコミットします。古いファイルは7日間の保持に従い、直接削除されることはありません。データ照合、ファイルパーセンタイル、プランニングとクエリのp95、ラグ、競合によって、ロールアウトを拡大するかどうかを決定します。」

ステップバイステップの詳細解説

ステップ 1: 現在のスナップショットからファイルをプロファイリングする

オブジェクトストレージディレクトリを再帰的にリストするのではなく、Icebergメタデータをクエリします。ディレクトリには過去のスナップショットからのみ参照されるファイルや孤立したオブジェクトが含まれる可能性があるため、現在のクエリが計画するものを表していません。次のSQLはSparkおよびIcebergカタログの例です。カタログ名とパーセンタイル関数は実際のエンジンに合わせて調整してください。

sql
SELECT
  partition,
  COUNT(*) AS active_files,
  SUM(file_size_in_bytes) AS active_bytes,
  percentile_approx(file_size_in_bytes, array(0.5, 0.9, 0.99)) AS size_percentiles
FROM lakehouse.analytics.events.files
GROUP BY partition
ORDER BY active_files DESC;

現在のスナップショットID、データファイル数と削除ファイル数、マニフェスト数、パーティションごとのファイルサイズパーセンタイル、および候補しきい値を下回るファイルの割合を記録します。平均値はロングテールを隠してしまいます。パーティションに少数の大きなファイルと数万の1〜3 MiBのファイルが存在する可能性があります。最低限、p50、p90、p99、およびヒストグラムを検査します。

クエリp95を、カタログとマニフェストの解決、ファイルプランニング、タスクスケジューリング、最初のバイトまでの時間、スキャンに分解します。ファイル数とプランニング時間が共に増加し、同じ合計バイト数を持つテストパーティションがコンパクション後に大幅に高速に計画される場合、因果関係の根拠が強くなります。プランニングは安定しているがスキャンが遅い場合は、代わりに選択性、カラム統計、削除ファイル、スキュー、コンピュートリソースを調査します。

ステップ 2: 書き込みパスにおける再生成の原因を見つける

このシナリオでは、1日に1,440回の1分バッチが生成されます。80,000ファイルは、1バッチあたり平均約56ファイルです。各ライターまたはライターとパーティションの組み合わせが受け取るデータが少なすぎる場合、512 MiBのターゲットを設定しても3 MiBの出力を完全なファイルに変換することはできません。write.target-file-size-bytesは目標値であり、すべてのファイルがそこに達することを保証するものではありません。

根本原因は多くの場合、複合的なものです。マイクロバッチが頻繁すぎる、各バッチに対してアップストリームの並列度が過剰である、書き込み前にテーブルのパーティションキーによって行がクラスタ化されていない、時間・テナント・ユーザーディメンションでテーブルが過剰にパーティショニングされている、ホットキーがスキューを引き起こしている、リトライによってコミットが増加している、またはmerge-on-readの更新によって削除ファイルの負債が発生しているなどが挙げられます。

「小さい」の定義をパーティションごとに解釈します。1日に40 MiBしか受信しない正当な低ボリュームパーティションは、512 MiBのファイルを埋めることはできません。コンパクションの頻度を永遠に増やすのではなく、より小さな目標を受け入れるか、パーティション仕様を粗くするか、バケットや隠蔽パーティショニングを使用します。

ステップ 3: 同様の断片化の生成を停止する

カナリアリリースで書き込み側の最小限の変更を行います。鮮度SLAの範囲内で、1分ごとのコミットをより大きなトリガーバッチに結合します。Icebergパーティションキーによって行をハッシュまたはレンジ分散します。クラスタの最大並列度ではなく、バッチあたりのバイト数からライター数を選択します。高カーディナリティのカラムで直接パーティショニングすることは避けてください。

実際の圧縮率、行の幅、クエリの選択性、パーティションごとの1日のボリュームに対して、目標ファイルサイズをテストします。このシナリオでは、Icebergのデフォルトが適切な出発点となるため、候補として512 MiB(536,870,912バイト)を使用しています。128 MiB、256 MiB、またはそれ以上の値を排除するものではありません。パーティションが目標値よりもはるかに小さい場合は、パーティション仕様を進化させます。十分なデータが存在するにもかかわらず各ライターが受け取る行数が少ない場合は、分散と並列度を修正します。

同様の負荷を持つ2つのパーティションでA/B比較を実行します。同じデータボリュームで、ファイル数、サイズ分布、コミットレイテンシ、ストリーム処理ラグ、障害復旧を比較します。バックログのコンパクションは、新しいファイルの生成率が大幅に低下した後に初めて持続可能になります。

ステップ 4: メリットと競合リスクによってコンパクションを制限する

最初のパスでは、遅延データおよびビジネス修正ウィンドウが閉じているコールドパーティションを1つ選択し、少なくとも現在書き込み中の時間は除外します。パーティションがまだ変更される可能性があるかどうかと、スナップショットが7日間保持されるかどうかは別のタイムラインです。当面の目標はメタデータとファイルオープンのオーバーヘッドの削減であるため、ビンパッキングから開始します。一般的なフィルタカラムがファイル間で大幅に重複し、ベンチマークが追加のシャッフルを正当化する場合にのみ、ソートまたはZ-orderingにアップグレードします。

sql
CALL lakehouse.system.rewrite_data_files(
  table => 'analytics.events',
  strategy => 'binpack',
  options => map(
    'target-file-size-bytes', '536870912',
    'min-input-files', '5',
    'max-concurrent-file-group-rewrites', '3',
    'partial-progress.enabled', 'true'
  ),
  where => 'event_date = DATE ''2026-07-10'''
);

whereプレディケートは、一致する行を含む可能性のあるファイルを選択します。これをパーティション境界に合わせ、実行前に候補バイト数を検査します。ファイルグループは作業の各単位を制限します。制御された並行性により、オブジェクトストレージ、シャッフル、クエリクラスタが同時に飽和するのを防ぎます。部分的な進捗(partial progress)によりグループが個別にコミットされ、競合のリトライコストが削減されますが、複数のスナップショットが作成され、グループレベルの監視とロールバックが必要になります。

ホットパーティションを避けられない場合は、時間範囲とファイルグループを縮小し、スケジューリングを冪等にし、データファイルの競合とリトライ可能なメタデータコミットの競合を区別します。重複するテーブル範囲を持つ2つのコンパクションジョブを同時に実行しないでください。

ステップ 5: 目標ファイル数、I/O、およびウィンドウを見積もる

200 GiBを512 MiBで割ると、理想的な目標ファイル数は約400個になります。パーティション境界、圧縮、端数の残りにより、実際の数はこれより多少多くなります。この見積もりは桁違いのエラーを検出するためのものであり、正確に400個の出力を保証するものではありません。

1つの完全な日次パーティションをコンパクションすると、約200 GiBを読み取り、約200 GiBを書き込みます。つまり、約400 GiBのデータI/Oに加え、シャッフル、一時ストレージ、メタデータ、リトライが発生します。ベンチマークで入力バイト数ベースで持続的なエンドツーエンドスループットが100 MiB/sと測定された場合、理想的な所要時間は次のようになります。

text
200 GiB * 1024 MiB/GiB / 100 MiB/s = 2,048 s ≈ 34.1 min

スキュー、並行クエリ、リトライのためのマージンを追加します。候補バイト数、並行ファイルグループ数、オブジェクトストレージリクエストレート、一時ディスクの制限とともに、60〜90分のウィンドウが妥当なシナリオ予算です。200 GiBが到着する間にコンパクションが1日あたり150 GiBしか処理できない場合、バックログは増加せざるを得ません。持続可能なスループットを向上させるか、まず新規ファイルの作成を削減してください。

ステップ 6: スナップショットの保持と物理クリーンアップを分離する

コンパクション後、新しいスナップショットは大きなファイルを参照します。古いスナップショットを既に読み取っているクエリは引き続き完了でき、7日間のタイムトラベルには依然として古いファイルが必要です。元のParquetオブジェクトを直接削除すると、これら両方の動作が破壊されます。

expire_snapshotsを実行する前に、新しいスナップショットを検証し、完全なビジネスサイクルを観察します。7日間のウィンドウ、必要なブランチまたはタグ、および最小スナップショット数を保持します。スナップショットの失効は、保持されているスナップショットによって不要になったファイルのみを削除します。孤立ファイル(orphan files)は別のクラスであり、テーブルメタデータから一切参照されていません。remove_orphan_filesを個別に実行し、dry_runから開始し、保守的なolder_thanを選択し、削除前にパススキーム、オーソリティ、および最長インフライト書き込みを検証します。

スナップショットの失効はコンパクションではなく、孤立ファイルのクリーンアップは古いスナップショット参照の失効の代わりにはなりません。3つすべてに独立したスケジュール、権限、監査ログを付与してください。

ステップ 7: カナリア、検証、および停止条件の定義

コンパクション前のスナップショットと候補パーティションを固定します。行数、個別のビジネスキー数、重要な金額やイベントの集計、イベント時間の最小値と最大値、およびnull数を記録します。処理後に同じ論理範囲を再計算します。合計行数だけでは1行の損失が1行の重複によって相殺される可能性があるため、重要なテーブルにはバケット化されたチェックサムやビジネスキーのサンプルを追加する必要があります。

パフォーマンスについては、アクティブファイル数、p50/p90/p99サイズ、マニフェスト数、プランニングp50/p95、エンドツーエンドクエリp95、スキャンバイト数、書き換えバイト数、リソースコストを比較します。運用指標には、スモールファイル生成率、コンパクションラグ、失敗したファイルグループ、コミット競合、スナップショット数、再利用可能なバイト数が含まれます。

正確性に不一致がある場合、プランニング時間が改善しない場合、コストが取り込みSLAに侵入する場合、またはコンパクションスループットが新しい断片化率を下回ったままの場合は、展開を停止します。テーブルスナップショットはメタデータポインタをロールバックできますが、既に完了したスナップショット失効と物理削除をスナップショット自体で元に戻すことはできません。

質の高い模範回答

「私はコンパクションの実行から始めることはしません。まずボトルネックを証明します。オブジェクトストレージディレクトリには過去のファイルと孤立ファイルが混在しているため、現在のIcebergスナップショットのfilesメタデータテーブルを使用して、パーティションごとのアクティブファイル、合計バイト数、p50/p90/p99サイズを測定します。次に、カタログとマニフェストの解決、タスクプランニング、ファイルオープン、スキャンを分離します。このシナリオでは、1日200 GiBで中央値3 MiBのファイルが80,000個作成されています。512 MiBの候補目標は理想的なファイル約400個を意味するため、書き込みレイアウトが有力な手掛かりですが、それでも同じバイト数を持つコンパクション済みパーティションのプランニングが高速になることを確認します。

次に、再生成を修正します。1日に1,440回の1分バッチがあり、1バッチあたり約56ファイルあります。ライターの並列度、パーティションのカーディナリティ、書き込み前の分散、スキュー、リトライ、merge-on-readの削除ファイルを調査します。鮮度SLAの範囲内で、コミットバッチを拡大し、パーティションキーでハッシュまたはレンジ分散し、バッチバイト数からライター数を決定します。512 MiBの値は単なる出発点です。それを埋めることができない低ボリュームパーティションには、より粗いパーティションまたはより小さな目標が必要です。

バックログについては、rewrite_data_filesビンパッキング、パーティションプレディケート、制限されたファイルグループ並行性、候補バイト上限を使用して、1つのコールドパーティションをカナリア処理します。ソートはシャッフルと一時ストレージを追加するため、一般的なフィルタテストで明確な価値が示された場合にのみ、ソートまたはZ-orderingを使用します。ホットパーティションを処理する必要がある場合は、より小さなファイルグループ、部分的な進捗、制限された競合リトライを使用します。コンパクションの重複を防ぎます。

200 GiBと512 MiBでは、理想的な出力は約400ファイルです。1回のパスで約200 GiBを読み取り、200 GiBを書き込みます。入力バイトベースで100 MiB/sの測定されたエンドツーエンドレートでは、理想的な実行時間は34.1分です。60〜90分を確保し、日次キャパシティが日次入力を上回っていることを証明します。

コンパクションは新しいスナップショットをアトミックにコミットします。既存のクエリと7日間のタイムトラベルは古いファイルを参照し続けるため、オブジェクトを直接削除することは決してありません。新しいスナップショットで行数、ビジネス集計、バケット化チェックサム、クエリp95を検証した後、7日間のポリシーの下でスナップショットを失効させます。孤立ファイルのクリーンアップは、ドライランを先行させ、保守的な遅延を持たせた別のジョブとして維持します。ロールアウトダッシュボードでは、正確性、新規スモールファイル率、ファイルパーセンタイル、プランニングp95、コンパクションラグ、競合、GiBあたりのメリットを追跡します。主要な項目に回帰が見られた場合は展開を停止します。」

よくある間違い

  • ファイル数が多いことが原因の証明になると思い込む → 過去のファイルは現在のクエリファイルではない → 現在のスナップショットをプロファイリングし、プランニングとスキャンを分離する。
  • 1回コンパクションを実行して終了する → マイクロバッチ、ライター、細かいパーティションが断片を生成し続ける → バックログを解消する前に新規断片化率を下げる。
  • 目標サイズを絶対的な保証として扱う → ライターは受け取った行しか出力できない → 目標値、バッチバイト数、分散、パーティションごとのボリュームを合わせて調整する。
  • 20 TiBテーブル全体を書き換える → コストと競合範囲が過大になる → メリットで制限されたバッチでコールドパーティションを処理する。
  • デフォルトでソートまたはZ-orderを選択する → どちらもシャッフルと一時ストレージを追加する → ビンパッキングから開始し、プルーニングベンチマークでクラスタリングを正当化する。
  • 古いParquetオブジェクトを直接削除する → 保持されているスナップショットや並行クエリがそれらを参照している可能性がある → スナップショット失効と、ドライランを行う個別の孤立ファイルクリーンアップを使用する。
  • 合計行数のみを検証する → 欠損と重複が相殺される可能性がある → ビジネス集計、キー数、バケット化チェックサム、サンプルを追加する。
  • 持続可能なスループットを考慮せずに実行時間を見積もる → 日次処理が日次入力を下回るとバックログが増加する → 読み取り、書き込み、シャッフル、リトライ、コンパクションラグを予算化する。
  • coalesce(1)を普遍的な修正策として使用する → ライターが1つになると並列処理が破壊されボトルネックになる → パーティションボリュームと目標バイト数からライター数を計算する。

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

フォローアップ 1: なぜ128 MiBではなく512 MiBの目標を使用するのですか?

Icebergのデフォルト目標である512 MiB(536,870,912バイト)は、このシナリオの大規模テーブルに対する実験的な出発点であり、普遍的な最適値ではありません。クエリの選択性、プランニングコスト、タスク並列度、圧縮、およびパーティションごとの日次ボリュームに対して、128、256、512 MiBまたはそれ以上の候補をベンチマークします。選択性の高いクエリには小さなファイルが好まれる場合があり、スループット重視のスキャンや非常に大きなパーティションには大きなファイルが好まれる場合があります。

フォローアップ 2: 目標を増やした後もファイルが数MiBにしかならないのはなぜですか?

目標はライターが到達しようとする出力サイズを指示するものであり、異なるタスクによって保持されているデータをマージするものではありません。1分間のバッチが数十のライターに分割されている場合、または1つのライターが多くの低ボリュームパーティションにアクセスしている場合、タスクがコミットした時点でファイルがクローズされます。目標を増やすだけでなく、バッチサイズ、書き込み分散、並列度、パーティション設計を変更してください。

フォローアップ 3: ビンパッキング、ソート、Z-orderingをどのように選びますか?

ファイル数を減らしオープンオーバーヘッドを下げることを目指す場合はビンパッキングを選択します。クエリが頻繁に1つのカラムまたは階層キーでフィルタリングし、ファイル統計によって範囲をプルーニングできる場合はソートをテストします。フィルタが複数のディメンションの変化する組み合わせを頻繁にカバーする場合にのみZ-orderingを評価します。後者2つのベンチマークには、追加のシャッフル、一時ストレージ、書き込み増幅、および継続的なメンテナンスを含めてください。

フォローアップ 4: コンパクションがストリーミング書き込みと競合した場合はどうしますか?

まずアクティブなパーティションを除外します。それが不可能な場合は、より小さなファイルグループを使用し、並行性を制限し、部分的な進捗を有効にし、コミットの競合に対して制限付きのリトライを適用します。スケジューラは重複するテーブル範囲に対して相互排他を強制する必要があります。部分的な進捗は、1つの競合グループによって完全な再実行が強制されるのを防ぎますが、スナップショットと部分成功の状態が追加されるため、すべてのグループの結果を記録してください。

フォローアップ 5: コンパクション後すぐにオブジェクトストレージの使用量が減らないのはなぜですか?

コンパクションは新しいファイルを参照するスナップショットをコミットします。過去のスナップショット、ブランチ、またはタグは、並行読み取りやタイムトラベルのために依然として古いファイルを参照しています。7日間の保持が満たされた後、スナップショット失効によって、保持されているスナップショットに不要なファイルを回収できます。テーブルメタデータから一切参照されていないファイルには、個別の孤立ファイルクリーンアッププロセスが必要です。

フォローアップ 6: 改善がキャッシュや追加リソースによるものではなく、スモールファイルの削減によるものであることをどのように証明しますか?

同じエンジン設定を使用し、キャッシュ状態を一貫してコールドまたは一貫してウォームにします。同じ論理スナップショット範囲で再現可能なクエリを実行し、前後のプランニング時間、タスク数、ファイルオープン数、スキャンバイト数、実行時間を記録します。コンパクションされていない同様の負荷のパーティションを対照群として保持し、複数回の実行にわたってパーセンタイルを比較します。ファイルレイアウトが変更され、それに伴ってプランニングまたはオープンオーバーヘッドが一貫して減少した場合にのみ、因果関係の証拠が強くなります。

公開情報ソース

関連する質問