Slide 1

Slide 1 text

ReproでのicebergのStreaming Writeの検証と 実運⽤にむけた取り組み joker1007 (Repro株式会社) Apache Iceberg Meetup 1

Slide 2

Slide 2 text

⾃⼰紹介 • 橋⽴友宏 (@joker1007) • Repro株式会社 チーフアーキテクト • 元々はRailsエンジニアだったが、最近は Javaばかり書いている • ⽇本酒とクラフトビールが好き 2

Slide 3

Slide 3 text

Asakusaから来ました 活動中のRubyコミュニティで⽇本最古 (⾃分は本当に浅草に住んでます) 3

Slide 4

Slide 4 text

最近の仕事 チーフアーキテクトとして、サービス全体の中長期的な技術選定、設計アドバイス、全体アー キテクチャのデザインなどをしている。 4

Slide 5

Slide 5 text

Reproの事業 マーケティングオートメーションサービスを提供している。 マーケティングオートメーションとは、 • ユーザーの⾏動を分析・分類し • 複数のチャネルで適切なキャンペーンを配信することで • 顧客とエンドユーザーのコミュニケーションを⽀援する SaaS事業だけでなく、サービスグロースの総合的な⽀援も提供しています。 5

Slide 6

Slide 6 text

背景 Reproでは⼤体200億件ぐらいのレコードを持つテーブルから1000万件前後を取得するワーク ロードと、普通にWebリクエストで1件単位で取得するワークロードが混在している。 出来る限り短いリードタイムで利⽤可能になっていて欲しい。(⽬安として⼤体5分以内) この⽬的のために、ReproではCassandraをバックエンドとし、テーブルのパーティション キー及びクラスタリングキーを⼯夫してbucketingなどのテクニックを活⽤することで対応し てきた。 ⼤規模な読み込みワークロードではTrinoを利⽤し、その他のリクエストでは普通に Cassandra Clientから読み取りを⾏っている。 また、Kafkaを利⽤したストリームアプリケーションとRocksDBを利⽤して各ノードローカ ルで処理することで、⾼速にエンドユーザーを分析し振り分ける処理などもある。 6

Slide 7

Slide 7 text

課題 Cassandraはバルクロードに向いた構造とは⾔い難く、現状Trinoの並列読み込みとテーブル のbucketingにより機能として動作はしているがコスパが悪い状態。 得意とは⾔えないユースケースなので将来的なスケーラビリティにも不安がある。 しかし、開発メンバーが運⽤できる基盤にも限りがあるので、できる限り運⽤負荷は上げたく ない。 そこで、S3に配置可能なOpen Table Formatが以前から気になっていた。 7

Slide 8

Slide 8 text

前回 OTF StudyでHudiについて検証した結果を発表させてもらった 本番のトラフィック量でHudiを検証して⾒えてきた課題 この時はTrinoのHudi対応が余り積極的ではなかった点、Sparkとかなり処理が結合していて Compactionタイミングや負荷のコントロールがやり辛かった点、などを考慮して実運⽤を⽬ 指すところまでは踏み切らなかった。 8

Slide 9

Slide 9 text

IcebergとHudiとの⽐較 icebergの⽅が明らかにシンプルで仕組みが分かり易い。 特にMerge On Readの仕組みとConcurrency Control。 HudiはAvroを利⽤したレコードタイプのフォーマットとカラムナを組み合わせておりクエリ が複雑になる。 ⼀⽅でIcebergはmanifestとDeleteレコードの組み合わせで実現しており基本的に Parquet(Orc)で完結する。 Hudiはより低レイテンシの書き込みに価値を置いている分、書き込みもクエリも仕組みが複 雑になる印象。 9

Slide 10

Slide 10 text

検証により知りたかったこと Reproのデータ量及び書き込みのワークロードに対して、以下の要素がそれぞれどうなるの か。 • icebergの書き込みとcommitにかかるノードのリソースコストがどの程度になる のか • commitとcompactionがどれぐらいの実⾏頻度ならバランスが取れるのか • compactionにどれぐらいの負荷がかかるのか Hudiの検証で得た知識として、この⼿のストリーミング書き込みでは断⽚化したsmall fileの 増加を抑えるcompactionが⾮常に重要かつ負荷がかかることが分かっていたので、ここに注 ⽬して検証を実施。 10

Slide 11

Slide 11 text

検証内容 • 現⾏のcassandraへの書き込みと同様にKafkaからレコードを受け取ってiceberg テーブルの書き込みを⾏う。 • コミット及びcompaction処理がReproの機能・運⽤⾯で現実的な頻度の実⾏で処 理が可能かどうかを検証し、productionで必要になるノード数を⾒積る。 • trinoでクエリをする時の簡単なクエリパターンの実験・検討を⾏い挙動を確認す る。 11

Slide 12

Slide 12 text

データの傾向 レコードの内容はエンドユーザーに関するメタデータを保持するuser_id+キーバリューとい うシンプルな構造のレコードであり、Updateも頻繁に発⽣する。 同⼀の内容であれば処理をスキップする仕組みは既に前段に構築済み。 そのため、本当に書き込みを必要とするスループットはそこまで⾼くない。 ⼤体秒間6000件程度がカバーできれば現状は対応可能。 12

Slide 13

Slide 13 text

書き込み⽅式 EMR-7.10を利⽤してFlinkでS3にデータを書き込む。 書き込みにFlinkを利⽤するのはEquality Deletionを利⽤するため。 テーブルの管理にはAWS Glue Data Catalogを利⽤し、spark-sqlを利⽤したDDLで構築す る。 Glue Data Catalogを利⽤するのは、Catalog管理の運⽤コストを削減するためと、Glueが持 つIcebergのテーブル最適化の機能を利⽤したいため。 13

Slide 14

Slide 14 text

メンテナンスタスク メンテナンスタスクは以下の様なやり⽅で実施。 • expire_snapshot: Glueに任せる • rewrite_data_files: Sparkで実⾏する • rewrite_manifests: Sparkで実⾏する • delete_orphan_files: Glueに任せる 全部Glueの機能で完結すれば楽だったのだが、rewrite_data_filesの負荷が⾼過ぎてGlueの 機能ではコンピューティングリソースが不⾜し失敗する。 14

Slide 15

Slide 15 text

データ規模 • 全体のデータ量: 2TB程 • レコード数: 200億件超 • 更新頻度: 5000件 / sec bucketingの結果、バラつきはあるが⼤体1パーティションが平均1GBぐらいになる様にして 検証。(バケットサイズ、ファイルサイズは実運⽤時には要調整) 15

Slide 16

Slide 16 text

DDL spark-sqlで実⾏する。 CREATE TABLE IF NOT EXISTS glue.testdb_production.test_tables ( app_id bigint NOT NULL, user_id bigint NOT NULL, key string NOT NULL, value string ) USING iceberg PARTITIONED BY (bucket(32, app_id), bucket(8, key)) TBLPROPERTIES ( 'write.object-storage.enabled'='true', 'write.delete.mode'='merge-on-read', 'write.update.mode'='merge-on-read', 'write.merge.mode'='merge-on-read', 'history.expire.max-snapshot-age-ms'='86400000' ) LOCATION 's3://repro-experimental-store/production/testdb_production/test_tables'; ALTER TABLE glue.testdb_production.test_tables WRITE ORDERED BY insight_id, key; ALTER TABLE glue.testdb_production.test_tables SET IDENTIFIER FIELDS insight_id, user_id, key; 16

Slide 17

Slide 17 text

パラメーターについて Flinkで⾼頻度に書き込むことでsnapshotが⼤量に増えるためmax-snapshot-age-msを短く 設定 merge-on-readとobject storage向けの最適化(prefixをばらけさせることで書き込みスルー プットのキャップを回避する)を有効化。 17

Slide 18

Slide 18 text

パーティショニング設定 クエリのワークロードは基本的にapp_idごとに閉じた形で⾏われる。 値を直接利⽤せずにbucketingしているのは、パーティションに利⽤するには数が多過ぎ、ま たデータ量のばらつきも⾮常に⼤きいため。 Icebergの良い点として、manifest構造の中にパーティション定義を持っていて、後から変更 可能になっているので調整がしやすい。 18

Slide 19

Slide 19 text

EMRクラスタサイズ 以下のクラスタサイズ、設定内容は試⾏錯誤の結果落ち着いた値。 書き込みクラスタサイズ: r8g.2xlarge * 3 テーブルメンテナンス⽤クラスタサイズ: r8g.2xlarge * 20 (メンテナンスタスク実⾏時のみ必 要) 19

Slide 20

Slide 20 text

Sparkパラメーター rewrite_data_filesの実施に多⼤なメモリが必要になるため以下の項⽬を調整。 • spark.driver.memory • spark.executor.memory • spark.memory.fraction • GCパラメーター (効果の程は微妙) 20

Slide 21

Slide 21 text

Flinkパラメーター • jobmanager.memory.process.size • taskmanager.memory.managed.fraction • taskmanager.memory.process.size 同じくメモリサイズの割り当てを調整。 こちらはそこまで負荷にはならなかった。 ただKafkaにレコードが⼤量に溜まっていて台数が多いケースだとjobmanagerのメモリが不 ⾜するケースがあった。 21

Slide 22

Slide 22 text

初期構築 Flinkで書き込みを⾏う前に、既存のデータを元に初期データを投⼊し本番と同等のサイズの テーブルを構築する。 既存のデータはcassandraに蓄積されているため、これを変換してTrino経由でicebergテー ブルに投⼊する。 INSERT INTO iceberg.testdb_production.test_tables (app_id, user_id, key, value) SELECT app_id, user_id, key, value FROM cassandra.repro.test_tables; 先述のデータ規模を投⼊するのにr8g.2xlarge 10台のtrinoクラスタで2時間半ぐらいの所要時 間で完了する。 22

Slide 23

Slide 23 text

Flinkによる書き込み設定 confluentのschema registryを利⽤したKafkaトピックからのストリーム書き込みを⾏うた め、以下の準備をする。 # confluent registrtyに対応したflink sql connectorのjarファイルをDL wget https://repo.maven.apache.org/maven2/org/apache/flink/flink-sql-avro-confluent-registry/1.20.0/flink-sql-avro-confluent-registry-1.20.0.jar # flinkセッション起動 flink-yarn-session -Dparallelism.default=2 -d # flink SQLの実行 flink-sql-client -j flink-sql-avro-confluent-registry-1.20.0.jar -f insert.sql 23

Slide 24

Slide 24 text

flink-sqlでチェックポイント間隔を調整 SET state.backend.type = 'rocksdb'; SET execution.checkpointing.storage = 'filesystem'; SET execution.checkpointing.dir = 's3://repro-experimental-store/production/flink-checkpoints'; SET execution.checkpointing.savepoint-dir = 's3://repro-experimental-store/production/flink-checkpoints'; SET execution.checkpointing.num-retained = '1'; SET execution.checkpointing.interval = '15min'; SET execution.checkpointing.timeout = '10min'; SET execution.checkpointing.min-pause = '1min'; クエリ⾃体は単純にKafkaからconfluet-schema-registryを利⽤してデータを取得、upsert で書き込む単純なクエリを利⽤した。 execution.checkpointing.interval がicebergのcommit間隔になる。今回は調整の結果15 分とした。 24

Slide 25

Slide 25 text

commit間隔とファイル数の関係 1タスクで15分に1回コミットなので、15分に凡そ1パーティションに1ファイルづつparquet ファイルが増えていく。 24時間で 4(1hで4回) * 24h * 2(data file & delete file) = 96ファイル程増えることになる。 並列数がもっと必要であれば、その分作成されるファイル数も増える。 これを定期的なcompactionで解消できるかどうかを検証した。 25

Slide 26

Slide 26 text

メンテナンスタスクの実⾏ expire_snapshotは12時間に1回実⾏、delete_orphan_filesは24時間に1回実⾏し48時間よ り前のファイルを削除する⽤に設定した。 compactionはEMRのcommand-runnerステップを利⽤して24時間に1回spark-sqlコマンド を利⽤して以下のクエリを実⾏する。 CALL glue.system.rewrite_data_files( table => 'testdb_production.test_tables', strategy => 'sort', options => map( 'min-input-files', '50', 'partial-progress.enabled', 'true', 'partial-progress.max-commits', '20', 'remove-dangling-deletes', 'true', 'rewrite-job-order', 'files-desc')); 26

Slide 27

Slide 27 text

compaction実⾏ペースと実際の処理時間 24時間で1パーティションごとに最低でも96ファイル + deletesファイル分の⼩さいファイル が⽣成されるので、それをcompactionにより⼤きなparquetファイルに結合する。 このファイル増加ペースであれば、⼤体r8g.2xlarge * 20台で1時間〜2時間ぐらいの処理時間 でタスクが完了する。 24時間に1度、2時間の所要時間の実⾏ペースで十分追い付けると分かった。 27

Slide 28

Slide 28 text

クエリ⽅法 icebergを利⽤しつつ現状のcassandraに対するクエリと同様に数分以内のリードタイムで データを利⽤可能にするために⼯夫が必要だった。 • TrinoのKafka connectorとiceberg connectorをUNIONしたviewを構成し、その viewに対してクエリを⾏うことで最新のデータだけをKafkaから取得可能にする ⽅法を考案。 • 検証実験では、現在時刻から30分以内のtimestampに限定してKafka connector でクエリを⾏いそれ以外のデータはicebergからクエリを⾏う様にする。 28

Slide 29

Slide 29 text

Kafkaと組み合わせたクエリデータの規模と負荷 Kafkaから読み込むデータ量は直近30分に限定すると、約2.7GBで2700万レコードに相当す る。 Kafka上のデータはicebergと同様のパーショニングは⾏えないが、直近30分程度のデータ量 であれば無駄を承知の上でレコードを読んでもそれなりの負荷で済む。 10台前後のtrinoクラスタがあれば数秒で読むことが出来るしKafkaクラスタにかかる負荷も 許容範囲の⼩さいものだった。 但し、単発では問題ないレベルという状態だったのでそのまま実運⽤可能かは要確認。 29

Slide 30

Slide 30 text

GlueカタログとViewについて Trinoのiceberg catalogをGlue Catalogに設定して構成していた場合、icebergテーブルが所 属しているcatalog及びdatabaseに対してviewを⽣成するとGlue Data Catalog側に永続化 されることが分かった。 そのためクラスタの停⽌や⼊れ替えを伴ってもviewを再作成する⼿間はかからないことも確 認できた。 将来的にはApache Flussを活⽤できたりすると効率が良さそうと考えているが、現状では Trinoからのクエリがサポートされていないしバックエンドのicebergサポートも計画中とい う感じなので、今後の展開に注⽬していきたい。 30

Slide 31

Slide 31 text

総評 • Flinkによる書き込みは動きがシンプルなので動作⾃体は⾮常に軽い。 • 15分単位のupsert書き込みを継続している状態で、compactionの実⾏が遅れると 割と顕著にパフォーマンスに影響を与えることが分かった。compactionの定期実 ⾏はやはり重要。 • compactionはかなり処理負荷が⾼くメモリもCPUリソースもかなり必要になる。 • 特に今のicebergで普及しているテーブル仕様のバージョン(v2)とsparkの実装で は、⾮常に⼤量の⼩さなファイル(特にdeleteファイル)が存在すると、 compactionに膨⼤なメモリが必要になるため、compactionが長期に渡って実 ⾏されないとテーブルのメンテナンス⾃体が困難になる可能性があって危険。 現時点でcompactionの所要時間をそれなりに抑えて安定して実⾏できる様にするにはかなり ⼤きめのsparkクラスタがいる。 31

Slide 32

Slide 32 text

現在、本番導⼊に向けて作業中 • [x] 必要なインフラのコード化 • [x] メトリック取得とアラートモニタの整備 • [ ] 実際に利⽤する新しいクエリの構築 32

Slide 33

Slide 33 text

実装済みのモニタリング・監視 • 各種AWSリソース、アラート定義のterraform化 • flinkがちゃんと動作していることを監視し、マシンリソースと書き込みペースの メトリックをdatadogで取得できる様にする。 • EventBridgeとStepFunctionで定期的にcompactionのためのEMRを動かすデプロ イスクリプトと権限設定。 ◦ compactionの失敗時にアラート • icebergのテーブルメタデータによるモニタリング ◦ パーティションごとのファイル数やファイルサイズ合計をメタテーブルから 取得、datadogに送信するシェルスクリプトを書いて書き込みクラスタの systemd timerで定期実⾏。 33

Slide 34

Slide 34 text

Next Step • 実際に利⽤しているクエリの置き換え • Kafkaと直接組み合わせるのではなく、Flussの様な仕組みを簡易的に⾃作できな いか検証。 ◦ ストリーミングアプリケーション上にApache Arrowでデータを溜めてtrinoか らクエリ可能にするアイデア。 • V3フォーマットのサポート状況を定期的に確認 34