Slide 1

Slide 1 text

Chapter4. Optimizing the Performance of Iceberg Tables 後編 株式会社アシスト 島尻 龍一 2024/08/05 Apache Iceberg: The Definitive Guide 輪読会

Slide 2

Slide 2 text

自己紹介 ● 名前:島尻龍一 ● 所属:株式会社アシスト データイノベーションセンター ○ 2019 年新卒入社 ○ ETL 製品のフィールドエンジニア ⇒ Snowflake 担当 ○ お客様への Snowflake 提案、基盤構築の伴走支援 ● 趣味:かな書道 ● Xアカウント:@rshimajiri_kka Iceberg は初心者です。よろしくお願いします!

Slide 3

Slide 3 text

今日発表すること ● 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 のブルームフィルター ● まとめ

Slide 4

Slide 4 text

今日発表すること ● 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 のブルームフィルター ● まとめ

Slide 5

Slide 5 text

ソートの課題 ソートはパフォーマンス改善に効果的だけどまだ課題が、、、 ● 新しいデータが取り込まれると、次の Compaction ジョブまでソートされていない状態になってしまう。 ● ソートされても、1 つの Datafile 内のフィールドに複数の値が含まれている可能性がある。 Lionsのデータを取りたいときに Packersのレコードをスキャンするのは非効率的 アメフトの各チームの選手に関するデータセット 「Apache Iceberg: The Definitive Guide」4章より

Slide 6

Slide 6 text

パーティショニング(Partitioning)とは 単にフィールドに基づいてソートするだけでなく、対象フィールド個別の値(distinct values)をそれぞれの Datafile に書き出す。 先ほどの例であれば、「team」というフィールドに基づき、値が「Lions」の Datafile たちを 1 つのパーティ ションとして、値が「Packers」の Datafile たちをまた別の 1 つのパーティションとして扱うということ。 Lions パーティション Datafile Datafile Datafile Datafile Packers パーティション Datafile Datafile Datafile Datafile

Slide 7

Slide 7 text

従来のパーティショニングにおける問題点 従来、特定のフィールド値に基づいてパーティショニングをす るには、別途フィールドを追加して管理する必要があった。 問題点: ● レコード挿入のたびに値の変換が必要 ● 以下のような理由で結局フルスキャンのクエリになって しまうことが起こりうる ○ ユーザーがパーティションに使用する列を認識し ておらず、パーティション列を条件に加えない ○ 誤った形式でパーティション列のフィルタリング をしてしまう(例: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';

Slide 8

Slide 8 text

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」のような追加のフィルタリングを しなくても、パーティショニングの効果を得られる

Slide 9

Slide 9 text

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 列の変換 値の切り捨て カーディナリティが高い場合に有効 ※ハッシュ関数を使って、  指定した数のバケットに  レコードを分散させる 特定の郵便番号を探すとき、テーブルをフルスキャンする のではなく、対象の郵便番号を含むバケットをスキャン するだけでよくなる

Slide 10

Slide 10 text

(脱線)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 で割った余りを取る。

Slide 11

Slide 11 text

Partition Evolution 従来のパーティショニングのもう一つの課題として、「パーティショニング方法を変更するとテーブル全体を書 き換える必要がある」というものがあった。※ファイルの物理的な構造に依存していたため Icebergでは、Metadata file が過去のパーティション構成(partition scheme)も追跡することができ、 Partition Evolution を可能にしている。 つまり、2 つの Datafile が異なるパーティション構成になっていても、エンジンに Metadata の情報を認識させ て、パーティション構成 A とパーティション構成 B を分けてプラン作成を行い、最後に全体的なスキャン計画を 作成することができる。  

Slide 12

Slide 12 text

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);

Slide 13

Slide 13 text

今日発表すること ● 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 のブルームフィルター ● まとめ

Slide 14

Slide 14 text

行レベルの更新をどう処理するか Iceberg のファイルは immutable なため、変更ができない。 新しいデータが追加されるときは新しい Datafile を追加すればよいことは分かるが、 既存の行を更新/削除するときにどうすればよいのか、というお話。 行レベルの更新方式には以下の 3 つのアプローチがある。 更新方式 Readの速度 Writeの速度 ベストプラクティス Copy-on-write 最速 最遅 Merge-on-read (position deletes) 速い 速い (読み込みコストを最小化す るために) 定期的なCompactionを行う Merge-on-read (equality deletes) 最遅 最速 (読み込みコストを最小化す るために) より頻繁にCompactionを行う

Slide 15

Slide 15 text

Copy-on-Write(COW) COW では、Datafile 内の 1 行でも更新/削除されると、そのDatafileは書き換えられ、新しいスナップショット に新しい Datafile が作成される。Read は速いけど、Write は遅い。(デフォルトの方式) 向いていないケース: ワークロードが(そこそこの頻度で)定期的な行レベル更新を行う場合 「Apache Iceberg: The Definitive Guide」4章より

Slide 16

Slide 16 text

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 は 書き換え不要

Slide 17

Slide 17 text

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 計画が必要。

Slide 18

Slide 18 text

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' );

Slide 19

Slide 19 text

今日発表すること ● 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 のブルームフィルター ● まとめ

Slide 20

Slide 20 text

メトリクスの収集 テーブル内の各フィールドのメトリクスは 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)」

Slide 21

Slide 21 text

マニフェストの書き換え 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)

Slide 22

Slide 22 text

マニフェストの書き換え(イメージ) https://blog.dynofu.me/post,iceberg,spark,parquet/2023/06/14/iceberg-metadata-deepdive.html

Slide 23

Slide 23 text

ストレージ最適化 スナップショットに有効期限を設ける 時間が経つにつれて多くのスナップショットが作成さ れるので、古い不要なスナップショットとその 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 が生まれることがある。その不要なファイル を削除することができる。

Slide 24

Slide 24 text

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', );

Slide 25

Slide 25 text

Write Distribution モード(none) 参考:https://www.youtube.com/watch?v=4bOCDP-rhuM    Dremio「Managing Data Files In Apache Iceberg」

Slide 26

Slide 26 text

Write Distribution モード(hash) 参考:https://www.youtube.com/watch?v=4bOCDP-rhuM    Dremio「Managing Data Files In Apache Iceberg」

Slide 27

Slide 27 text

Write Distribution モード(range) 参考:https://www.youtube.com/watch?v=4bOCDP-rhuM    Dremio「Managing Data Files In Apache Iceberg」

Slide 28

Slide 28 text

オブジェクトストレージの考慮事項 オブジェクトストレージでは、同じプレフィックスを持つファイルへのリクエスト数に制限があることが多い。 例えば、/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 ハッシュ値

Slide 29

Slide 29 text

Datafile のブルームフィルター ブルームフィルター: ある値がデータセットの中に存在するかどうかを知るための方法。 これにより、不要なデータのスキャンを避けるという実装を省スペースで実現できる。 ブルームフィルターの設定例 ALTER TABLE catalog.MyTable SET TBLPROPERTIES ( 'write.parquet.bloom-filter-enabled.column.col1'= true, 'write.parquet.bloom-filter-max-bytes'= 1048576 ); ブルームフィルターが、明らかに必要なデータが存在しないことを示しているDatafile をスキップすることで、Datafile の読み込みをさらに高速化することができる

Slide 30

Slide 30 text

(脱線)ブルームフィルターとは ある要素が該当の集合に含まれているかどうかを判定する確率的データ構造。 ● ハッシュテーブルなどの他のデータ構造よりもスペース効率が良い ● 偽陰性はない(「この要素は存在しない」と言ったとき、その要素は必ず存在しない) ● 偽陽性はある(「この要素は存在する」と言ったとき、本当はその要素が存在しない可能性がある) 参考:『大規模データセットのためのアルゴリズムとデータ構造』(https://book.mynavi.jp/ec/products/detail/id=143918)

Slide 31

Slide 31 text

(脱線)ブルームフィルターとは 初期状態(要素数=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

Slide 32

Slide 32 text

今日発表すること ● 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 のブルームフィルター ● まとめ

Slide 33

Slide 33 text

4章後半まとめ Iceberg には、Compaction やソート以外にも、以下のような様々なパフォーマンス最適化の手段が用意されて いる。 ● Partitioning          →組み込み変換の選択 ● COR と MOR の使い分け     → Read と Write のどちらの性能に課題があるか ● メトリクスの収集        →フィールド数が多いときの微調整 ● マニフェストの書き換え     → Datafile が複数のスナップショットにまたがっているときに ● ストレージ最適化        → ストレージ容量の節約 ● Write Distribution モード    →エンジン内でデータをタスク間でどのように配分するか ● write.object-storage.enabled  →同一プレフィックスのリクエスト数の制限に引っかかるときに ● ブルームフィルター       →Datafile の読み込みをさらに高速化 ユースケースに応じた手段の選択が重要。