$30 off During Our Annual Pro Sale. View Details »

Apache Iceberg The Definitive Guide 輪読会 - 4章 Op...

Apache Iceberg The Definitive Guide 輪読会 - 4章 Optimizing the Performance of Iceberg Tables 後半

SnowVillageで開催している「Apache Iceberg: The Definitive Guide 輪読会」の4章後半の発表スライドです。

Ryuichi Shimajiri

August 05, 2024
Tweet

Other Decks in Technology

Transcript

  1. 自己紹介 • 名前:島尻龍一 • 所属:株式会社アシスト データイノベーションセンター ◦ 2019 年新卒入社 ◦

    ETL 製品のフィールドエンジニア ⇒ Snowflake 担当 ◦ お客様への Snowflake 提案、基盤構築の伴走支援 • 趣味:かな書道 • Xアカウント:@rshimajiri_kka Iceberg は初心者です。よろしくお願いします!
  2. 今日発表すること • Partitioning ◦ Hidden Partitioning ◦ Partition Evolution •

    Copy-on-Write vs. Merge-on-Read ◦ Copy-on-Write(COW) ◦ Merge-on-Read(MOR) ◦ COW と MOR の設定 • その他考慮事項 ◦ メトリクスの収集 ◦ マニフェストの書き換え ◦ ストレージ最適化 ◦ Write Distribution モード ◦ オブジェクトストレージの考慮事項 ◦ Datafile のブルームフィルター • まとめ
  3. 今日発表すること • Partitioning ◦ Hidden Partitioning ◦ Partition Evolution •

    Copy-on-Write vs. Merge-on-Read ◦ Copy-on-Write(COW) ◦ Merge-on-Read(MOR) ◦ COW と MOR の設定 • その他考慮事項 ◦ メトリクスの収集 ◦ マニフェストの書き換え ◦ ストレージ最適化 ◦ Write Distribution モード ◦ オブジェクトストレージの考慮事項 ◦ Datafile のブルームフィルター • まとめ
  4. ソートの課題 ソートはパフォーマンス改善に効果的だけどまだ課題が、、、 • 新しいデータが取り込まれると、次の Compaction ジョブまでソートされていない状態になってしまう。 • ソートされても、1 つの Datafile

    内のフィールドに複数の値が含まれている可能性がある。 Lionsのデータを取りたいときに Packersのレコードをスキャンするのは非効率的 アメフトの各チームの選手に関するデータセット 「Apache Iceberg: The Definitive Guide」4章より
  5. パーティショニング(Partitioning)とは 単にフィールドに基づいてソートするだけでなく、対象フィールド個別の値(distinct values)をそれぞれの Datafile に書き出す。 先ほどの例であれば、「team」というフィールドに基づき、値が「Lions」の Datafile たちを 1 つのパーティ

    ションとして、値が「Packers」の Datafile たちをまた別の 1 つのパーティションとして扱うということ。 Lions パーティション Datafile Datafile Datafile Datafile Packers パーティション Datafile Datafile Datafile Datafile
  6. 従来のパーティショニングにおける問題点 従来、特定のフィールド値に基づいてパーティショニングをす るには、別途フィールドを追加して管理する必要があった。 問題点: • レコード挿入のたびに値の変換が必要 • 以下のような理由で結局フルスキャンのクエリになって しまうことが起こりうる ◦

    ユーザーがパーティションに使用する列を認識し ておらず、パーティション列を条件に加えない ◦ 誤った形式でパーティション列のフィルタリング をしてしまう(例:yyyy-mm-ddが正しいのに yyyymmddで指定) 従来のパーティショニング (「月」でパーティショニングしたい場合) CREATE TABLE MyTable (...) PARTITIONED BY month; -- レコード挿入のたびに値を手動で変換 INSERT INTO MyTable (SELECT MONTH(time) AS month, ... FROM data_source); -- 本当はこうクエリしてほしいけど、、、 SELECT * FROM MYTABLE WHERE time BETWEEN '2022-07-01 00:00:00' AND '2022-07-31 00:00:00' AND month = 7; -- ユーザーはフルスキャンのクエリをしてしまう SELECT * FROM MYTABLE WHERE time BETWEEN '2022-07-01 00:00:00' AND '2022-07-31 00:00:00';
  7. Hidden Partitioning Iceberg では、Hidden Partitioning という方法で、 パーティショニングを行う。 • 追加列を作成するのではなく、組み込みの変換を使 用することができる。

    → Datafile 容量の節約にもなる • 実行エンジンは Metadata file から元の列の変換を 認識できるため、ユーザーがクエリするときには単 純に元の列でフィルタリングするだけでパーティ ショニングの効果を得ることができる。 Hidden Partitioning の例 -- 列の追加は不要で、PARTITIONED BY句で直接変換を指定 CREATE TABLE catalog.MyTable (...) PARTITIONED BY months(time) USING iceberg; -- ユーザーがクエリするときは変換列の指定が不要 SELECT * FROM MYTABLE WHERE time BETWEEN '2022-07-01 00:00:00' AND '2022-07-31 00:00:00'; 「AND month = 7」のような追加のフィルタリングを しなくても、パーティショニングの効果を得られる
  8. Hidden Partitioning 使用できる組み込みの変換: • year • month • day •

    hour • truncate • bucket 参考:https://iceberg.apache.org/spec/#partition-transforms truncateの例 bucketの例 -- 人の名前の最初の文字に基づいてパーティショニング CREATE TABLE catalog.MyTable (...) PARTITIONED BY truncate(name, 1) USING iceberg; -- 郵便番号に基づいてパーティショニング CREATE TABLE catalog.voters (...) PARTITIONED BY bucket(24, zip) USING iceberg; timestamp 列の変換 値の切り捨て カーディナリティが高い場合に有効 ※ハッシュ関数を使って、  指定した数のバケットに  レコードを分散させる 特定の郵便番号を探すとき、テーブルをフルスキャンする のではなく、対象の郵便番号を含むバケットをスキャン するだけでよくなる
  9. (脱線)bucket について ソースの値の 32 bit ハッシュが使われる。 実装は Murmur3 という高速な非暗号化ハッシュ関数を使用。 ※シード値は「0」(固定させることに意味があると理解)

    疑似コード def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N https://iceberg.apache.org/spec/#partition-transforms N は bucket の数。 ハッシュ関数の結果を正の値にして N で割った余りを取る。
  10. Partition Evolution 従来のパーティショニングのもう一つの課題として、「パーティショニング方法を変更するとテーブル全体を書 き換える必要がある」というものがあった。※ファイルの物理的な構造に依存していたため Icebergでは、Metadata file が過去のパーティション構成(partition scheme)も追跡することができ、 Partition Evolution

    を可能にしている。 つまり、2 つの Datafile が異なるパーティション構成になっていても、エンジンに Metadata の情報を認識させ て、パーティション構成 A とパーティション構成 B を分けてプラン作成を行い、最後に全体的なスキャン計画を 作成することができる。  
  11. Partition Evolution • Iceberg の日付関連のパーティションでは、粒度の細か いもの(例:月ごと)に変更しても、粒度の粗いもの (例:年ごと)のパーティションルールを変更しなくて も良い。 • bucket

    や truncate をパーティション構成として使用 していて、そのフィールドでパーティショニングする必 要がなくなった場合、パーティショニング構成を削除す ることができる。 注意: 古いデータを古いパーティショニング構成のまま保持したい場 合は、Compaction ジョブによって新しいパーティショニング 構成で書き換えられないように、適切なフィルタを使用する。 Partition Evolution の例 -- 会員が登録された「年」に基づいてパーティショニング CREATE TABLE catalog.members (...) PARTITIONED BY years(registration_ts) USING iceberg; -- 月別のパーティショニングを追加 ALTER TABLE catalog.members ADD PARTITION FIELD months(registration_ts) 数年後、会員数の増加ペースが早くなったため、 月別のパーティショニング構成に変更 → 月別のパーティショニングは、今後テーブルに書き込まれる   新しいデータにのみ適用される。   ※既存データの書き換えは不要 パーティション削除の例 ALTER TABLE catalog.members DROP PARTITION FIELD bucket(24, id);
  12. 今日発表すること • Partitioning ◦ Hidden Partitioning ◦ Partition Evolution •

    Copy-on-Write vs. Merge-on-Read ◦ Copy-on-Write(COW) ◦ Merge-on-Read(MOR) ◦ COW と MOR の設定 • その他考慮事項 ◦ メトリクスの収集 ◦ マニフェストの書き換え ◦ ストレージ最適化 ◦ Write Distribution モード ◦ オブジェクトストレージの考慮事項 ◦ Datafile のブルームフィルター • まとめ
  13. 行レベルの更新をどう処理するか Iceberg のファイルは immutable なため、変更ができない。 新しいデータが追加されるときは新しい Datafile を追加すればよいことは分かるが、 既存の行を更新/削除するときにどうすればよいのか、というお話。 行レベルの更新方式には以下の

    3 つのアプローチがある。 更新方式 Readの速度 Writeの速度 ベストプラクティス Copy-on-write 最速 最遅 Merge-on-read (position deletes) 速い 速い (読み込みコストを最小化す るために) 定期的なCompactionを行う Merge-on-read (equality deletes) 最遅 最速 (読み込みコストを最小化す るために) より頻繁にCompactionを行う
  14. Copy-on-Write(COW) COW では、Datafile 内の 1 行でも更新/削除されると、そのDatafileは書き換えられ、新しいスナップショット に新しい Datafile が作成される。Read は速いけど、Write

    は遅い。(デフォルトの方式) 向いていないケース: ワークロードが(そこそこの頻度で)定期的な行レベル更新を行う場合 「Apache Iceberg: The Definitive Guide」4章より
  15. Merge-on-Read (MOR) MOR では、Datafile 全体を書き換える代わりに、既存ファイルの中で更新/削除対象となるレコードを Delete file にキャプチャして、読み込み時に Delete file

    を使って Datafile をフィルタする。 (レコード更新時は、更新レコードのみを含む新しい Datafile が作成される。) →Write は速いけど、Read は遅い。  ※Read コストを最小限に抑えるためには定期的な Compaction ジョブが必要。 「Apache Iceberg: The Definitive Guide」4章より Delete file で 更新レコードを追跡 更新レコードのみを含む 新しい Datafile が作成される 元の Datafile は 書き換え不要
  16. Merge-on-Read (MOR) Delete file には「positional」と「equality」の 2 種類がある。 削除すべき行 Filepath Position

    001.parquet 0 001.parquet 5 006.parquet 5 positionalのイメージ 削除すべき行 Team State Yellow NY Green MA equalityのイメージ 行をスキップする場所がかなり特定されるため、Readのコストは大幅 に減る。一方 Delete file 作成時に対象行の位置を知るために Datafile を一度読み込む必要があるため、Write のコストはかかる。 対象値を追跡するために Datafile を読み込む必要はないので Write の コストはかからない。一方で、対象行の場所を知るための情報がない ため、Read のコストははるかに高くなる。 →積極的な Compaction 計画が必要。
  17. COWとMORの設定 テーブルが COW と MOR のどちらを使って行レベルの更新を処理するかは、以下の要素によって決まる。 • テーブルのプロパティ • Iceberg

    への書き込みに使用するエンジンが MOR をサポートしているか テーブル作成時の設定例 CREATE TABLE catalog.people ( id int, first_name string, last_name string ) TBLPROPERTIES ( 'write.delete.mode'='copy-on-write', 'write.update.mode'='merge-on-read', 'write.merge.mode'='merge-on-read' ) USING iceberg; テーブル作成後の設定変更例 ALTER TABLE catalog.people SET TBLPROPERTIES ( 'write.delete.mode'='merge-on-read', 'write.update.mode'='copy-on-write', 'write.merge.mode'='copy-on-write' );
  18. 今日発表すること • Partitioning ◦ Hidden Partitioning ◦ Partition Evolution •

    Copy-on-Write vs. Merge-on-Read ◦ Copy-on-Write(COW) ◦ Merge-on-Read(MOR) ◦ COW と MOR の設定 • その他考慮事項 ◦ メトリクスの収集 ◦ マニフェストの書き換え ◦ ストレージ最適化 ◦ Write Distribution モード ◦ オブジェクトストレージの考慮事項 ◦ Datafile のブルームフィルター • まとめ
  19. メトリクスの収集 テーブル内の各フィールドのメトリクスは Manifest file によって追跡される。 ・値、NULL 値、一意の値のそれぞれの数 ・上限値と下限値 フィールド数が増えると、メトリクスがメタデータ読み込みの上で負担になってくる。 →テーブルプロパティにて、メトリクス追跡の有無を各列で微調整することができる。

    メトリクス追跡の有無を各列で微調整する例 ALTER TABLE catalog.db.students SET TBLPROPERTIES ( 'write.metadata.metrics.column.col1'='none', 'write.metadata.metrics.column.col2'='full', 'write.metadata.metrics.column.col3'='counts', 'write.metadata.metrics.column.col4'='truncate(16)', ); none:メトリクスを収集しない counts:カウント(値、一意の値、null値)のみ収集 truncate(XX):値を XX の文字数で切り捨てて、         それに基づいて上限/下限を決定 full:カウントと上限/下限はフルの値に基づく ※デフォルトは「truncate(16)」
  20. マニフェストの書き換え Datafileが最適なサイズ&最適にソート済みであっても、Datafile が複数のスナップショットにまたがって書き 込まれていると、マニフェストが多くの Datafile をリストしてしまい、ファイル操作が増えてパフォーマンス問 題が発生することがある。 この問題を解消するのが「rewrite_manifests」プロシージャ。 これを実行すると Manifest

    file の総数が少なくなるように Manifest file を書き換えてくれる。 参考:https://iceberg.apache.org/docs/1.5.1/spark-procedures/#rewrite_manifests マニフェスト書き換えを実行する例 Sparkのキャッシュを無効化する場合 CALL catalog.system.rewrite_manifests('MyTable') CALL catalog.system.rewrite_manifests('MyTable', false)
  21. ストレージ最適化 スナップショットに有効期限を設ける 時間が経つにつれて多くのスナップショットが作成さ れるので、古い不要なスナップショットとその Datafileを削除することで、ストレージの最適化を行う ことができる。 例)最新の 50 個のスナップショットを保持しながら、現在の日付から 90

    日以上経過したスナップショットを削除 例)孤立したファイル(削除対象ファイル)をリストで返す CALL catalog.system.expire_snapshots('test.employees', date_sub(current_date(), 90), 50) CALL catalog.system.remove_orphan_files(table => 'test.employees', dry_run => true) 孤立したファイルを削除する 時間経過に伴い、Metadata file からの参照を失った Datafile が生まれることがある。その不要なファイル を削除することができる。
  22. Write Distribution モード エンジンが並列処理で書き込みを行う際、データをタスク間でどのように配分するかが重要になる。 (例えばパーティション A に属する 10 個のレコードが 10

    個のタスクに分散している場合、そのパーティションは 1 レコードずつ 10 個のファイルになってしまうが、それは理想的ではない。) Iceberg では 3 つの Write Distribution モードが用意されている。 • none:特別な配分は行わない。書き込み時間はもっとも速い。     →事前にソート済みのデータ、もしくは 1 パーティションへの書き込みの時には最適。(だけど、そんなことは稀) • hash:パーティションキーによってハッシュ分散される。 • range:パーティションキーまたはソート順によって範囲分散される。 Write Distribution モードの設定例 ALTER TABLE catalog.MyTable SET TBLPROPERTIES ( 'write.distribution-mode'='hash', 'write.delete.distribution-mode'='none', 'write.update.distribution-mode'='range', 'write.merge.distribution-mode'='hash', );
  23. オブジェクトストレージの考慮事項 オブジェクトストレージでは、同じプレフィックスを持つファイルへのリクエスト数に制限があることが多い。 例えば、/prefix1/fileA.txt と /prefix1/fileB.txt にアクセスしたい場合、異なるファイルであるにもかかわら ず、両方にアクセスすると prefix1 の制限にダブルカウントされてしまう。 →ファイルの多いパーティションで問題になる。

    Iceberg はファイルの物理的なレイアウトに依存しないため、同じパーティションに対して多くのプレフィック スに渡ってファイルを書き込むことができる。 例)同じパーティション内のファイルを   複数のプレフィックスに分散させるコマンド例 ALTER TABLE catalog.MyTable SET TBLPROPERTIES ( 'write.object-storage.enabled'= true ); s3://bucket/database/table/field=value1/datafile1.parquet s3://bucket/database/table/field=value1/datafile2.parquet s3://bucket/database/table/field=value1/datafile3.parquet s3://bucket/4809098/database/table/field=value1/datafile1.parquet s3://bucket/5840329/database/table/field=value1/datafile2.parquet s3://bucket/2342344/database/table/field=value1/datafile3.parquet ハッシュ値
  24. Datafile のブルームフィルター ブルームフィルター: ある値がデータセットの中に存在するかどうかを知るための方法。 これにより、不要なデータのスキャンを避けるという実装を省スペースで実現できる。 ブルームフィルターの設定例 ALTER TABLE catalog.MyTable SET

    TBLPROPERTIES ( 'write.parquet.bloom-filter-enabled.column.col1'= true, 'write.parquet.bloom-filter-max-bytes'= 1048576 ); ブルームフィルターが、明らかに必要なデータが存在しないことを示しているDatafile をスキップすることで、Datafile の読み込みをさらに高速化することができる
  25. (脱線)ブルームフィルターとは 初期状態(要素数=10個) 要素「A」を挿入 要素「B」を挿入 [0、0、0、0、0、0、0、0、0、0] hash1("A") -> 2 hash2("A") ->

    5 [0, 0, 1, 0, 0, 1, 0, 0, 0, 0] hash1("B") -> 3 hash2("B") -> 7 [0, 0, 1, 1, 0, 1, 0, 1, 0, 0] 要素「B」の検索 →Bはおそらく存在する 要素「C」の検索 →Cは必ず存在しない hash1("B") -> 3 hash2("B") -> 7 hash1("C") -> 1 hash2("C") -> 6 参考:https://zenn.dev/seita/articles/2a3f53f030ea93    https://www.linkedin.com/pulse/bloom-filter-snowflake-minzhen-yang-rl4mc
  26. 今日発表すること • Partitioning ◦ Hidden Partitioning ◦ Partition Evolution •

    Copy-on-Write vs. Merge-on-Read ◦ Copy-on-Write(COW) ◦ Merge-on-Read(MOR) ◦ COW と MOR の設定 • その他考慮事項 ◦ メトリクスの収集 ◦ マニフェストの書き換え ◦ ストレージ最適化 ◦ Write Distribution モード ◦ オブジェクトストレージの考慮事項 ◦ Datafile のブルームフィルター • まとめ
  27. 4章後半まとめ Iceberg には、Compaction やソート以外にも、以下のような様々なパフォーマンス最適化の手段が用意されて いる。 • Partitioning          →組み込み変換の選択 • COR

    と MOR の使い分け     → Read と Write のどちらの性能に課題があるか • メトリクスの収集        →フィールド数が多いときの微調整 • マニフェストの書き換え     → Datafile が複数のスナップショットにまたがっているときに • ストレージ最適化        → ストレージ容量の節約 • Write Distribution モード    →エンジン内でデータをタスク間でどのように配分するか • write.object-storage.enabled  →同一プレフィックスのリクエスト数の制限に引っかかるときに • ブルームフィルター       →Datafile の読み込みをさらに高速化 ユースケースに応じた手段の選択が重要。