Share
Embed
Transcript
34 slides
-
実験データ データ構造 カラム名 タイプ partition_id INT id BIGINT key STRING
value STRING updated_at TIMESTAMP -
Config (初期検証時) hoodie.index.type=SIMPLE hoodie.datasource.write.hive_style_partitioning=true hoodie.datasource.hive_sync.jdbcurl=jdbc:hive2://localhost:10000/ hoodie.datasource.hive_sync.database=default hoodie.datasource.hive_sync.table=output_table hoodie.datasource.hive_sync.partition_fields=partition_id hoodie.datasource.hive_sync.enable=true hoodie.parquet.compression.codec=zstd
hoodie.datasource.compaction.async.enable=true hoodie.cleaner.commits.retained=5 # ...省略... hoodie.streamer.source.kafka.enable.commit.offset=true hoodie.archive.merge.enable=true hoodie.clustering.inline=true hoodie.clustering.inline.max.commits=4 hoodie.clustering.plan.strategy.sort.columns=key -
実行コマンド spark-submit \ --conf spark.streaming.kafka.allowNonConsecutiveOffsets=true \ --class org.apache.hudi.utilities.streamer.HoodieStreamer /usr/lib/hudi/hudi-utilities-bundle.jar \
--props s3://repro-config/hudi-kafka-source.properties \ # 上記properties --schemaprovider-class org.apache.hudi.utilities.schema.SchemaRegistryProvider \ --source-class org.apache.hudi.utilities.sources.AvroKafkaSource \ --source-ordering-field updated_at \ --target-base-path s3://repro-batch-store/experiment_data\ --table-type MERGE_ON_READ \ --target-table UserProfiles \ --enable-sync \ --continuous \ --op UPSERT -
重要な設定項目 その1 hoodie.index.type : index方式の決定。初期検証時はSIMPLEだった。性能に大 きく影響するので詳細は後述する。 hoodie.datasource.write.partitionpath.field : hudiテーブルのパーティショ ンキーを指定する。
hoodie.datasource.compaction.async.enable : HoodieStreamerによる書き込 み時に非同期Compactionを実行する。同じ書き込みプロセス内で実行されるが書 き込み自体はブロックしない。 hoodie.cleaner.commits.retained : cleanプロセスで削除せずに維持する直近の コミット数。commitの数であってdeltacommitの数ではないことに注意。 -
重要な設定項目 その2 hoodie.archive.merge.enable : hudiは一定以上commitが蓄積されると metadataを一つのファイルにまとめてアーカイブする。その時アーカイブファイ ルのマージを有効にするかどうか。S3の様な追記をサポートしないファイルシス テムだと小さいファイルが大量に出来る可能性があるのでそれをマージできるよ うにする。 hoodie.streamer.source.kafka.enable.commit.offset
: Kafkaからデータを受 け取る時にConsumer Groupとしてgroup.idを登録して受信状況をcommitする。 この設定を有効にしておくと、Kafka側のconsumer lagの監視で遅延状況が監視 できる様になる。 hoodie.clustering.inline : 書き込み処理と同じプロセスで同期的にclustering 処理を行う。これは書き込み処理をブロックするが別途clusteringジョブを運用す る必要がなくなる。 -
初期検証の結果 結論から言えば、全然パフォーマンスが追いつかなかったどころか書き込みが完了し ないレベルだった。 書き込み開始当初は問題無いのだが、一定以上ファイルが溜まると劇的にパフォーマ ンスが劣化し、deltacommitすら完了しない状態になった。 Sparkのコンソールから時間がかかっている処理を特定し周辺の処理のソースコードを 確認した所、一番の問題はindexからのlookupだった。 自分が検証した範囲ではSparkのSIMPLE indexで大規模なデータレイクを構築するの はかなり難しいのではという印象だ。
という訳で、一番大きな問題であったインデックス選択について詳しく見ていく。 -
そもそもSIMPLE indexとは 公式のリファレンスによると以下の様に書かれている。 SIMPLE (default for Spark engines): This is
the standard index type for the Spark engine. It executes an efficient join of incoming records with keys retrieved from the table stored on disk. It requires keys to be partition-level unique so it can function correctly. see. https://hudi.apache.org/docs/indexing -
SIMPLE indexの動き方 つまり、入ってきたレコードのキーをディスク上に実際に保存されているテーブルファ イルから取得したキーの一覧とJOINして、ファイルを特定する。 GLOBALでないSIMPLEインデックスなら特定パーティション内でしか検索は発生しな いとはいえ、対象となるファイル数やキーの数を考えると、結構な数の組み合わせが 発生する。 実際、投入されたレコード1件ごとにこの処理が行われるため、処理時間の増大や処理 中のメモリの肥大化に繋がり一定以上ベースのテーブルのデータ量が多くなると、ま ともに処理が継続できなくなった。
今回利用したデータの傾向として、パーティション数の増大及びレコード数の偏った パーティションが多く発生するなどがあったことが、ファイル数の増大に繋がり悪影 響を及ぼしていると考えられる。 -
SIMPLE indexを利用していた理由 公式リファレンスのIndex Strategyの項目における"Workload 3: Random updates/deletes to a dimension
table"に該当したため。 初手から余りAdvancedな設定に手を出すべきではないと考え一旦リファレンスに則る ことにした。 結果的にはどうにもならなかったので、再度検討することになった。 -
BUCKET indexの採用 最終的にBUCKET indexを採用することにした。 BUCKET: Utilizes bucket hashing to identify
the file group that houses the records, which proves to be particularly advantageous on a large scale. To select the type of bucket engine—that is, the method by which buckets are created—use the hoodie.index.bucket.engine configuration option. これはレコードのキーをハッシュ関数にかけてバケット(つまりfile group)を特定し、 それをレコードの所在と判断する方式だ。O(1)で即レコードを特定できるので大幅な 書き込み対象lookupの高速化が狙える。 -
BUCKET indexの詳細 BUCKET indexには二つの方式があり、それぞれSIMPLEとCONSISTENT_HASHINGに なる。 SIMPLE(default): This index employs a
fixed number of buckets for file groups within each partition, which do not have the capacity to decrease or increase in size. It is applicable to both COW and MOR tables. Due to the unchangeable number of buckets and the design principle of mapping each bucket to a single file group, this indexing method may not be ideal for partitions with significant data skew. CONSISTENT_HASHING: This index accommodates a dynamic number of buckets, with the capability for bucket resizing to ensure each bucket is sized appropriately. This addresses the issue of data skew in partitions with a high volume of data by allowing these partitions to be dynamically resized. As a -
SIMPLEは全パーティション一律で固定サイズのバケット数を割り当てる。名前の通り 単純で管理コストが不要なのがメリットだが、データの偏りがあると極端に小さい ファイルが大量に発生したり、逆に極端に大きいサイズのファイルが発生する可能性 がある。 CONSISTENT_HASHINGはハッシュ関数をかけて出力される64 bitの整数値空間をバ ケット数の幅で区分けしてバケットを区分けに対応させる形で割り当てる。この方式 はパーティションごとにバケットサイズを可変にできるのが利点で、SIMPLEバケット では対応できなかったデータ量の偏りに対処できるが、パーティションごとに consistent_hashing_metadataというものを作成する必要があり、そのファイルを読
み込んでバケット数を取得するコストが発生する。 また、バケットサイズを変更するには専用のclusteringジョブを動かす必要があり、そ れなりに処理時間・負荷がかかる。 -
チューニング後の重要なコンフィグ差分 hoodie.index.type=BUCKET hoodie.index.bucket.engine=CONSISTENT_HASHING hoodie.bucket.index.hash.field=user_id hoodie.bucket.index.num.buckets=2 hoodie.bucket.index.min.num.buckets=1 hoodie.bucket.index.max.num.buckets=1024 # CONSISTENT_HASHING利用時はmetadataテーブルを利用できない hoodie.metadata.enable=false
hoodie.storage.layout.type=BUCKET hoodie.compact.inline.max.delta.commits=10 hoodie.compaction.strategy=org.apache.hudi.table.action.compact.strategy.UnBoundedCompactionStrategy hoodie.streamer.kafka.source.maxEvents=40000000 hoodie.clustering.plan.strategy.max.num.groups=10000 -
コンフィグ詳細 その1 hoodie.index.type=BUCKET , hoodie.index.bucket.engine=CONSISTENT_HASHING は CONSISTENT_HASHING方式でBUCKET indexを利用する設定になる。 現時点での制約としてmetadataテーブルと同時に有効にすることができないらしいの で、metadataテーブルを明示的にオフにする。
hoodie.bucket.index.num.buckets はSIMPLE方式のバケッティングの時の固定バ ケット数の設定とCONSISTENT_HASHING方式における初期バケット数の両方の設定 値になる。 hoodie.bucket.index.[min,max].num.buckets はCONSISTENT_HASHINGのバケッ ト区分を再割り当てした時の最小のバケット数と最大のバケット数になる。データ量 の偏りに合わせて設定するのが良いだろう。 (上記の設定のmax値が1024になっているが、これは引っかからない様に非常に大きく した値なので今回利用したデータではこんなに大きな値は必要無かった。 ) -
コンフィグ詳細 その2 hoodie.compact.inline.max.delta.commits=10 はいくつのdeltacommitごとに compactionをスケジュールするかを設定する。 hoodie.compaction.strategy=org.apache.hudi.table.action.compact.strategy.U nBoundedCompactionStrategy はcompactionの実行戦略を設定する。デフォルトでは compactionの実行時間の見通しを立て易くするために、一度の処理で一定のファイル サイズを越える量のcompactionを行わない様にフィルタされる。
UnBoundedCompactionStrategy は常に全てのファイルを対象にcompactionを行う様 に設定を変更する。設定を変更している理由は、一部のパーティションのcompaction だけ実行されてない状態になると、trinoからのクエリ時に読み取れる情報に差が出て しまうからだ。 -
コンフィグ詳細 その3 # Hoodie Streamerジョブが一度の書き込みサイクルでKafkaから取得するレコードの上限 hoodie.streamer.kafka.source.maxEvents=40000000 1回のdeltacommitの処理においてKafkaトピックから取得するレコードの上限を設定 する。デフォルトは5000000。今回はかなり値を増やしている。 HoodieStreamerを利用したレコード書き込みでは、1回ごとに consistent_hashing_metadataを全て再読み込みする挙動になる。今回利用したデー
タの様にパーティションが数千を越える規模になると読み込みだけで1分以上の時間が かかってしまった。1回のdeltacommitでのデータ量を少なくしてもこの時間は短縮で きない。そのためdeltacommitで書き込むデータ量を増やして書き込み効率を上げる 狙いがあった。 -
コンフィグ詳細その4 hoodie.clustering.plan.strategy.max.num.groups=10000 hoodie.clustering.plan.strategy.max.num.groups は一度のclusteringジョブで処 理できるfile groupの上限を設定する。デフォルトの値は30。 今回のデータの様にパーティションが大量に存在し、デフォルトのバケット数が2であ る状態だと、凡そ数千 * 2のfile
groupが存在することになる。デフォルトの設定値で ある30では、何度clustering jobを実行しても到底全パーティションまでclustering処 理が実行できない。そのため大幅に設定値を上げている。 -
検証結果 上記の設定で大体1回のdeltacommitのパフォーマンスは2000万レコード / 3minだっ た。 しかし、compactionの実行を全ファイル対象にすると凡そ15分ほど実行に時間がかか る。この処理時間は実行インターバルを短くしても余り短縮できない。 全体のスループットを平均して1秒ごとに均すと、 75000rpsぐらいのペースでレコー ドを書き込むことができる。但し、リアルタイムクエリが可能であってもcompaction
中の15分はデータが更新できない時間帯が存在する。 書き込みペースとしては十分なパフォーマンスが出せるという結果が得られたが、リ アルタイムクエリが可能なクエリエンジンに制限がある状況では、実際に利用可能な データが得られるまでのリードタイムは最悪の場合45分程度とかなり長くなってしま う。 -
EMRFSのコネクションリークっぽい挙動 continuousモードで書き込みを続けているとEMRFSによるS3接続のコネクションが CLOSE_WAIT状態で残り続けてTCPのコネクション数もしくはfdが枯渇する問題があっ た。 問題になるのはSparkのDriverノードだけなので、そのノードで定期的に ss state close-wait --kill などを実行しておけば何とかなるが、地味に面倒な問題だった。
sysctlでtcpのkeepaliveを変更しても効果が無かったので、コネクションがどこかで リークしている可能性がある。S3AFileSystemを使えば解消するかもしれない。 -
結論 更新可能なparquetデータレイクが構築可能であるという性質は非常に有用で、全体的 な書き込みスループットもチューニング次第で要求に耐えるところまで到達できるこ とが分かった。またストレージがS3だけで完結できる点も嬉しい。 一方で、compaction、clusteringの実行をどうコントロールするかはかなり考える必 要があるし、動きが怪しい箇所もまだ見受けられる。Spark以外にFlinkでの書き込み も検証したのだが、書き込みに大きな影響を及ぼすバグを踏んで検証が中断してし まった。 という訳で、将来的には有用な選択肢と言えるが、現状すぐに導入するにはそれなり にハードルがあるという結論になった。
今回検証に用いたデータは、継続的に大量のパーティションに対してランダムに書き 込みがあるという点で、hudiのユースケースとしてはかなりハードなものだったの で、ある程度の課題が発生するのは予想通りでもあった。