Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Kafka Streamsで作る10万rpsを支えるイベント駆動マイクロサービス
Search
Tomohiro Hashidate
November 20, 2023
Technology
5.2k
7
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Kafka Streamsで作る10万rpsを支えるイベント駆動マイクロサービス
CNDT2023 プレイベント 登壇資料
Tomohiro Hashidate
November 20, 2023
More Decks by Tomohiro Hashidate
See All by Tomohiro Hashidate
Ruby::Boxでできること、Refinementsでできること
joker1007
3
420
Do Ruby::Box dream of Modular Monolith?
joker1007
1
990
ReproでのicebergのStreaming Writeの検証と実運用にむけた取り組み
joker1007
0
760
マイクロサービスへの5年間 ぶっちゃけ何をしてどうなったか
joker1007
23
10k
Quarkusで作るInteractive Stream Application
joker1007
0
290
今改めてServiceクラスについて考える 〜あるRails開発者の10年〜
joker1007
25
23k
rubygem開発で鍛える設計力
joker1007
5
1.4k
実践Kafka Streams 〜イベント駆動型アーキテクチャを添えて〜
joker1007
3
1.4k
本番のトラフィック量でHudiを検証して見えてきた課題
joker1007
2
1.3k
Other Decks in Technology
See All in Technology
打造你的 AI 工作流:Agent Skill + MCP 實戰工作坊
appleboy
0
370
組織における AI-DLC 実践
askul
0
290
BPaaSで進むAIオペレーションの現在地 AI実装が効く領域とスケーラビリティの選定と実装
kentarofujii
0
250
AI駆動開発におけるQAエンジニアの役割事例 〜AI駆動開発の現場から〜
kobayashiyorimitsu
0
280
AIエージェントとPhysical AIが拓く製造業の変革(ハノーバーメッセリキャップ)
iotcomjpadmin
0
200
『AIに負けない』より『AIと遊ぶ』」〜ワクワクが最強のテスト・QA学習戦略_公開用
odan611
1
360
AWS Blocks を触ってみた/first-tach-aws-blocks
fossamagna
2
130
Hatena Engineer Seminar 37 jj1uzh
jj1uzh
0
410
Microsoft のサポートとフィードバック総まとめ
murachiakira
PRO
0
140
Multi-Agent並列開発を 安全に回すための技術 / Technology for Safely Multi-Agent Parallel Development
tooppoo
0
250
なぜ人は自分のプロジェクトを 「なんちゃってアジャイル」と 自嘲するのか
kozotaira
0
240
どうして今サーバーサイドKotlinを選択したのか
nealle
0
190
Featured
See All Featured
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Reality Check: Gamification 10 Years Later
codingconduct
0
2.2k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
410
XXLCSS - How to scale CSS and keep your sanity
sugarenia
250
1.3M
Making the Leap to Tech Lead
cromwellryan
135
9.9k
Why You Should Never Use an ORM
jnunemaker
PRO
61
9.9k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
180
First, design no harm
axbom
PRO
2
1.2k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
72
40k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.2k
Speed Design
sergeychernyshev
33
1.9k
Transcript
Kafka Streams で作る10 万rps を支えるイベント駆動 マイクロサービス CNDT 2023 Pre Repro
株式会社 橋立友宏 (@joker1007)
自己紹介 橋立友宏 (@joker1007) Repro inc. チーフアーキテクト 人生における大事なことは ジョジョから学んだ 日本酒とクラフトビールが好き Asakusa.rb
メンバー
Repro のプロダクト Repro はマーケティングソリューションを提供する会社で、 マーケティングオートメーションのための同名のサービスを提供している。 つまり、デジタルマーケティングを支援するツールが主なプロダクト。
マーケティングの基本要素 あくまで私見ですが、マーケティングとは 1. 適切な顧客および顧客の集団に 2. 適切なタイミングで 3. 適切なコンテンツ or クリエイティブを提供する
デジタルマーケティングに求められるもの システムに求められる特性に言い換えると 1. エンドユーザーの状況を素早く反映できる 行動ログやプロフィール情報、サービス利用ステータスなど 2. 任意のタイミングでユーザーの集合を抽出できる ( ユーザーセグメンテーション) 3.
柔軟な配信チャネルに対応している これらを大量のユーザー規模で提供する。 弊社では延べ数で数億ユーザーを越える。 スケーラビリティとサービス追加の容易さが重要。
ストリームベースへの転換 元々は、fluentd でS3 やBigQuery にデータを転送して一定間隔のバッチでBigQuery や Presto のクエリを実行し、ユーザーセグメントを定期更新していた。 しかし、このままでは一定以上の迅速さでユーザーの情報を反映できない。 スケーラビリティと情報反映のレイテンシ短縮のためにストリームベースのアーキテ
クチャに転換。 データパイプラインの基盤としてKafka を採用し、システムを組み直した。
Kafka とは 分散ストリームバッファを提供するミドルウェア。 キューとは異なり、一定期間もしくは永続的にメッセージを保持するストレージとし ての側面もある。 クライアントがどこまでメッセージを処理したかは、クライアントごとにconsumer group という単位で管理・記録し、メッセージ自体には影響を与えない。
現在のアーキテクチャの簡易的な図
Fire and Forget による疎結合化 メッセージバスでサービス間を連携する時に大事なことは、サービス同士がお互いの 存在すら知る必要が無い、という状態を維持すること。 イベントやメッセージを送信したら、後は受け取る側の責任で発信者は感知しない。 この形をFire and Forget
と表現する。 以下の様なメリットがある。 サービス間の依存関係を無くし、特定箇所が全体の可用性に与える影響を小さく できる あるサービスの応答性が、他のサービスに影響を与えない 一つ一つのサービスは自分のやることだけに関心を持てばいいので、小さく認知 負荷の低いサービスを構築しやすい
サービス追加の容易さ Kafka の特性により、発行済みのイベントやメッセージは一定期間Kafka のtopic 上に維 持される。また、キューやファンアウト式のメッセージパッシングと異なり、発信者 やメッセージバス自体が各サービスのことを事前に知らなくて良い。結果として、 同じイベントを元にして駆動できるサービスであれば、後から容易に追加可能 必要なデータの形状が異なるなら加工用のパイプラインを追加することもそこま で難しくない
といった利点があった。 Repro のプロダクトにおいては、配信チャネルの追加を容易にし、それに伴う認知負 荷上昇を抑える狙いがある。
スキーマによるサービス間の連携 疎結合化を目指すとはいえ、サービスを協調させるためには規約が必要。 そのためには、スキーマフルなデータ構造が必須。 弊社では現状Avro フォーマットを利用している。 また、スキーマの集中管理を行うためのスキーマレジストリを活用する。 各サービスにスキーマ情報を持たせなくて良くなる。 スキーマの変更パターンから後方互換性や前方互換性について検証した上で安全 にスキーマを変更できる。 see.
https://docs.confluent.io/platform/current/schema-registry/index.html
イベント駆動マイクロサービスのトレードオフ メリットは実感しているが、もちろんトレードオフとしてマイナス面もある。 Kafka の可用性とスケーラビリティに大きく依存している クラスタを簡単に止められない Kafka 自体はスケーラブルだがtopic のパーティション数を後から変えるのが 困難 エラーハンドリングが難しい
ローカル開発環境で全体を動かすのが難しい 同期的に別サービスの終了を待ち受ける必要があると複雑さが激増する 根本的に新しい機能を追加する場合は、パイプラインの広い範囲で修正が必要な 場合もある マイナス面を減らすための工夫が継続的な課題。
例: 同期的なワークフロー制御 既存のマイクロサービスを活用しつつ、バッチ取り込みなどの同期的に待ち受けが必 要な処理を実装したいケースがあった。 弊社ではAWS を利用しているので、Step Function をワークフローのメディエーターと して利用した。 結果イベントのpolling
とタイムアウトを組み合わせ、エラーハンドリングと通知は Step Function でコントロールする。 ワンタイムで必要になる処理はFargate やLambda を使うことでインフラ管理コストを 削減している。
例: ローカル開発環境の難しさへの対策 Kafka と多数のサービスで全体が構成されているため、ローカルの開発環境で全体を動 かすことが難しくなる。 開発用のステージング環境には全てのコンポーネントが揃っているため、AWS のVPC に対してVPN で透過的に接続可能にし、ローカルで修正中のコンポーネントを簡単に 差し込める様にした。
全てのコンポーネントには対応できないが、Consumer が主体となるコンポーネント の検証が容易になった。 ( ローカルの開発環境は、Consumer Group の所属ノードの一つになる)
ストリームプロセッシングの詳細 ここからは、実際にストリームアプリケーションを書くことに焦点を当てる。
Kafka Streams 概要 Java 向けのストリームプロセッサを書くためのフレームワーク。 Apache Kafka プロジェクトの中でメンテされており、Kafka Broker 以外に追加で必
要なものが無いのが特徴。 DSL とローレベルなProcessor API を組み合わせて、ストリームプロセッサが書ける。 基本的な動きとしては、Kafka のtopic からデータを取得し、レコード単位で加工した り集計処理を行なって、結果を再度Kafka のtopic に書き出すという動作を組み合わせ て処理を組み上げていく。
ストリームアプリケー ションのTopology Kafka Streams ではアプリケーション 内の処理一つ一つをノードとした DAG としてアプリケーションを表現 する。 この処理グラフをTopology
と呼ぶ。
ストリームアプリケーション開発の実践 Kafka Streams の細かい解説をすると時間が足りないので、 今回は実践的な開発に役立つ構成要素や考え方を中心に話をする。
ストリームアプリケーションを書く上で大事なこと 大量のデータを1 件単位で処理することになるので、とにかく処理のレイテンシに気を 配る必要がある。 ネットワーク通信は可能な限り避けるべき。 処理内容に依るが1 レコード処理するのに1ms は遅過ぎる。 ノードを分けて分散処理できるとはいえ、処理スループットに直結する。 処理レイテンシはしっかりモニタリングして気を配ることが大事。
re-partition の回避 Kafka Streams で状態を利用した処理、つまりあるキーでグルーピングして集計した り、レコードを結合してデータエンリッチを行いたい場合、同一のパーティションに レコードが届いている必要がある。 もし、これが異なるキーでいくつも必要になると、その度にキーを振り直して再度re- partition topic
にデータを送り直す必要がある。 DSL ではこれを自動で行ってくれる機能があるが、キーによるパーティションを意識せ ずに多用すると、ネットワーク負荷とストレージ消費量の増大、レイテンシの増加に よりパフォーマンスの低下に繋がる。 つまり、パーティションキーの設計が重要。
StateStore ストリームアプリケーションにおいて集計を行うためには、以前のレコードの処理結 果の蓄積( 状態) を保持しておく必要がある。 Redis などの外部ストアに蓄積することは可能だが、前述した様にネットワーク通信の オーバーヘッドはストリームアプリケーションにおいて致命的になる。 Kafka Streams
ではStateStore という仕組みで各ノードのローカルなストアに状態を 保持する。 実態はバイト順でソートされたキーバリューストアで、in-memory ストアとRocksDB をバックエンドにしたpersistent ストアがある。 DSL によって提供されるcount 処理やレコード同士のjoin の仕組みの裏側もStateStore で実装されている。
StateStore のイメージ図
Processor API Processor API というローレベルのAPI を利用することでStateStore を直接操作するこ とができる。 任意のデータをStateStore に書き込むことができるし、バイト順にソートされること
を利用してRange 探索を行うこともできる。 DSL では利用できない1:N のjoin を実現したり、レコードキーと違う値をキーにして値 を書き込むことや、レコードごとに異なるタイムウインドウで集計処理を実装するこ とも可能。 また、通常のJava のコードとして表現できることは実現できるので、Processor API を 処理の終端として利用し、外部のデータストアに書き込む処理を行ったりもする。 例えば、集計後や加工後のデータをCassandra に書き込んだりできる。
Processor API の簡単なサンプル public class WordCountProcessor implements Processor<String, String, String,
String> { private KeyValueStore<String, Integer> kvStore; @Override public void init(final ProcessorContext<String, String> context) { kvStore = context.getStateStore("Counts"); } @Override public void process(final Record<String, String> record) { final String[] words = record.value().toLowerCase(Locale.getDefault()).split("\\W+"); for (final String word : words) { final Integer oldValue = kvStore.get(word); if (oldValue == null) { kvStore.put(word, 1); } else { kvStore.put(word, oldValue + 1); } } } }
1:N のjoin の実装例とか載せたかったんですが、 スライドで表現するには長くなってしまうので、 気になる方は懇親時間に質問していただければと思います。
保持しているデータの永続化 各ノードのローカルにデータを持つなら、ノードやディスクが壊れた時はどうするの かという疑問が出てくる。 persistent なStateStore はデフォルトでKafka のtopic と関連付けられており、 StateStore に書かれたものは一定間隔でtopic
にflush される。 Kafka のtopic に書き込まれてしまえば、ブローカーのレプリケーションで耐久性が担 保される。 もしノードが壊れた場合は、別ノードに処理が移り、担当ノードはKafka topic から データ取得しローカルのStateStore を自動的に復元する。
レストア処理のイメージ
レストア処理のイメージ
レストア処理のイメージ
RocksDB のパフォーマンスチューニング 大体どんなDB でも同じだが、処理量が大きくなるとメモリの割り当て量を増やすこと が重要になる。 RocksDB はLSM ツリーを基盤にしたKVS である。 memtable
というメモリ上のテーブルにデータを書いて、一定期間でディスクにflush する。 memtable へのメモリ割り当てを増やしたりスロット数を調整して書き込みパフォー マンスをチューニングし、Block キャッシュに使えるメモリを増やして読み込みパ フォーマンスのチューニングを行う。 特にKafka Streams ではデフォルトの割り当てはかなり控え目になっており、処理が多 くなるとディスクに負荷がかかりがち。
RockdDB のメモリ割り当ての設定例 private static final long TOTAL_OFF_HEAP_MEMORY = 14L *
1024 * 1024 * 1024; private static final long TOTAL_MEMTABLE_MEMORY = 2L * 1024 * 1024 * 1024; private static final org.rocksdb.Cache cache = new org.rocksdb.LRUCache(TOTAL_OFF_HEAP_MEMORY, -1, false, 0.1); private static final org.rocksdb.WriteBufferManager writeBufferManager = new org.rocksdb.WriteBufferManager(TOTAL_MEMTABLE_MEMORY, cache); private static final long MEM_TABLE_SIZE = 180 * 1024L * 1024L; @Override public void setConfig( final String storeName, final Options options, final Map<String, Object> configs) { BlockBasedTableConfig tableConfig = (BlockBasedTableConfig) options.tableFormatConfig(); tableConfig.setBlockCache(cache); tableConfig.setCacheIndexAndFilterBlocks(true); options.setWriteBufferManager(writeBufferManager); options.setWriteBufferSize(getMemtableSize()); options.setMaxWriteBufferNumber(4); options.setMinWriteBufferNumberToMerge(2); options.setTableFormatConfig(tableConfig); options.setTargetFileSizeBase(256L * 1024 * 1024); options.setLevel0FileNumCompactionTrigger(10); }
AWS におけるノード選択 前述した様にノードやディスク破壊に対してはKafka Broker からのレストアが可能な ので、AWS でKafka Streams を運用する場合は、高速なエフェメラルストレージと相性 が良い。
例えば、i4i シリーズやr7gd などの高速なNVMe ストレージが装備されているインスタ ンスだ。 これらのインスタンスは非常に高いIOPS を出せるストレージを低コストで利用できる し、ノードが無くなってもデータ本体はKafka Broker に保持できるという点でKafka Streams と相性が良い。
StateStore の問題点 StateStore には大きな難点が一つある。 それはデータレストア中は、そのパーティションのパイプラインの処理が停止すると いうこと。 もし集計結果を大量に保持しなければいけないなら、データロスト時のレストアにも それなりに時間を要してしまう。 その間に蓄積したデータは全て処理が遅延してしまう。 現時点で万能の解決策が存在しないため、状況に合わせた工夫が必要になるかもしれ
ない。 弊社ではStateStore とCassandra を多段に積み重ねて、StateStore をキャッシュ的に利 用する構成にしレストアにかかる時間を短くしている箇所がある。
チューニングポイントとして重要な設定 max.fetch.bytes, max.partition.fetch.bytes: consumer が一度に取得するデー タ量 max.poll.records: 1 回のpoll で処理する最大のレコード数
num.stream.threads: 1 ノード上の処理スレッドの数。 cache.max.bytes.buffering: StateStore に対するアプリケーションレイヤーでの キャッシュメモリ量 num.standby.replicas: StateStore のスタンバイレプリカの数
運用時に注視すべきメトリック consumer_lag: consume 済みのレコードから最新のレコードまでのレコード数 process_latency: 1 つの処理が完了するまでの時間 e2e_latency: あるアプリケーションの一連の処理が完了するまでの時間 commit_latency:
consumer が処理済みレコードをcommit するのにかかった時 間 もちろん各種システムメトリックも必要。
今後の展望 トレーサビリティの拡充 Debezium を利用したRDB とKafka トピックの同期 Apache Hudi へのストリーム変換により、バッチラインとの協調を強化
Repro 株式会社はエンジニアを募集しています 特にこういった基盤を支えるSRE を強く求めています https://company.repro.io/recruit/