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
410
Do Ruby::Box dream of Modular Monolith?
joker1007
1
850
ReproでのicebergのStreaming Writeの検証と実運用にむけた取り組み
joker1007
0
740
マイクロサービスへの5年間 ぶっちゃけ何をしてどうなったか
joker1007
23
10k
Quarkusで作るInteractive Stream Application
joker1007
0
280
今改めてServiceクラスについて考える 〜あるRails開発者の10年〜
joker1007
25
22k
rubygem開発で鍛える設計力
joker1007
5
1.4k
実践Kafka Streams 〜イベント駆動型アーキテクチャを添えて〜
joker1007
3
1.4k
本番のトラフィック量でHudiを検証して見えてきた課題
joker1007
2
1.3k
Other Decks in Technology
See All in Technology
2026 TECHFRESH 畢業分享會 - 開發日常大解密!從領域驅動到企業級上線
line_developers_tw
PRO
0
750
地球に⽣きるAI —GeoAIと「中間領域」— / AI Living on Earth — GeoAI and the “Intermediate Layer” —
ykiyota
0
270
脆弱性対応、どこで線を引くか
rymiyamoto
0
360
非エンジニアがClaudeと挑んだ「1ヶ月間プロダクト30本ノック」
askokc
0
280
Dario Amodi『Policy on the AI Exponential』を理解する
nagatsu
0
210
新しいVibe Codingと”自走”について
watany
5
290
AI駆動開発を通して感じた、 AI時代のデザイナーの役割変化
whisaiyo
0
220
Disciplined Vibes: Scaling AI-Assisted Engineering
sheharyar
0
130
AIの性能が向上しても未解決な組織の重大問題は何か?/An Unsolved Organizational Problem in the Age of AI
moriyuya
3
610
2026TECHFRESH畢業分享會 - Lightning Talk - 資料也要 CI/CD? 用 Airbyte 自動化資料同步
line_developers_tw
PRO
0
740
2026TECHFRESH畢業分享會 - 葬送的通靈師:化系統與用戶雜訊成行動訊號
line_developers_tw
PRO
0
750
あなたの AI ワークスペースに、 専門コーダーを連れてくる - Amazon Quick Desktop 最新情報
kawaji_scratch
1
130
Featured
See All Featured
A better future with KSS
kneath
240
18k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
220
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.4k
Code Reviewing Like a Champion
maltzj
528
40k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
140
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
1
180
16th Malabo Montpellier Forum Presentation
akademiya2063
PRO
0
140
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
201
75k
Music & Morning Musume
bryan
47
7.2k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.2k
Navigating Team Friction
lara
192
16k
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/