Spring Boot高トラフィック対応にKafka連携で遅延ゼロを実現する方法

高トラフィックでもSpring Boot×Kafka連携で安定・高速な処理を実現するヒント

  1. Kafkaリスナーの同時実行数を3以上に設定する

    1秒あたりのメッセージ処理量が最大3倍に増え、ピーク時の遅延や滞留を抑えやすい

  2. プロデューサー/コンシューマーのackモードをRECORDに明示する

    1件ごと確実に受信確認できるため、障害発生時もメッセージ損失リスクを最小化

  3. トピックごとにパーティション数を2以上へ拡張して運用

    複数ノードで分散処理される分、サービス全体のスループットと可用性が向上

  4. Kafka Streamsなどストリーム処理APIを1つ導入

    リアルタイムでフィルタや変換ができ、バッチ遅延ゼロの業務ロジック反映が可能

解決Java Spring Boot高トラフィック課題に着手する方法

なんというか、バックエンドで高トラフィックを相手にしたことがある人、そしてマイクロサービスがあっちこっちでデータをぶん投げ合う現場で四苦八苦した経験のある人は、それなりにスケーラビリティとか非同期処理といった――もう昔からつきまとう悩みどころに直面して、正直うんざりしたこともあるんじゃないかな。自分たちの場合も例に漏れず、リクエスト数がぐいぐい膨らむのにつれて、バックエンド側の負荷…これがまた目に見えて増してくるわけです。ここぞという時に助け舟になってくれたのが、オープンソースのストリーム処理プラットフォーム――**Apache Kafka**だったんですよね(こういう派手な救世主的な存在、本当にありがたかった)。今回は、KafkaがJava Spring Boot製の僕らのバックエンドシステムでどう役立ったか、そのへん含めて話そうと思います。Kafkaはガチなスケーラビリティ問題だけじゃなくて、全体としてのフォールトトレランス(要は壊れてもヘッチャラ感)、それから信頼性とかパフォーマンス向上にも実際一役買ってくれてさ。ではまあ、どんな道筋でここまで至ったか細かく振り返ってみますね。

## 問題:オーバーロードするバックエンド
えっと、自分たちは**Java Spring Boot**を使ってマイクロサービスベースでバックエンドを組み上げていました。それぞれのサービス同士はHTTPリクエスト・レスポンスでやり取りしていて、「最初のうちは」システム全体も穏やか~に動いていた記憶があります。ただし、お決まりと言えばお決まりですが――ユーザーやリクエスト数がズンズン増えてきてしまうと、だんだん無視できないパフォーマンスボトルネックが顔を出し始めまして…ちょっとヒヤッとしましたよ、本当に。

見逃しがちなSpring Bootバックエンドの4大問題とは

正直なところ、我々がぶつかっていた具体的な問題は複数ありまして、例えばこんな感じかな。1. **高レイテンシ**──各サービス間が同期型のHTTP通信で繋がれていたため、トラフィックの山場なんかには、やたらと応答遅延が目立ったんですよね。2. **トラフィック集中時への対応力不足**、という点もあって、大量リクエストを同時に捌くこと自体うまくいかず、結果的に遅延・タイムアウト・さらに場合によっては失敗処理まで起きていました。3. **サービス同士の結合度が妙に高い**状況にも悩まされていてさ……要するに同期通信のせいで、一方のサービス障害が他にもあっさり飛び火しちゃう、と。4. **レジリエンス設計の欠如**だよね。何らか障害や一時ダウンが発生した際、自動リトライなど最低限欲しい機構すら持っていませんでした。

……まあ、この現状では困るので、それぞれのサービスをもっと疎結合状態で組み直しつつ(しかも複雑化だけはできれば避けたかった)、全体としてスループット上げたり信頼性保てるようなソリューションが本当に求められていたわけです。ま、いいか。

見逃しがちなSpring Bootバックエンドの4大問題とは

Kafkaはどんな場面で役立つストリーム処理技術?

その時、なんというか、自然と**Kafka**って選択肢が頭をよぎったんだよね。
## Kafkaって結局なに?
うーん、ちょっと雑に説明するけど──Apache Kafkaは膨大なリアルタイムデータのストリーム処理ができる、分散型のストリーミング基盤なんだ。いやほんと、一口に言えばさ。これがね、アプリケーション同士が“記録(レコード)”の流れを瞬時にやり取りしたり蓄積したりって仕組みのために緻密に作られてて…まあ設計思想も独特で奥深いっちゃ深いわけ。構造としては、とりあえず**プロデューサー**(送り手)、**コンシューマー**(受け手)、それから“話題”とか呼ばれる**トピック**という三本柱が中核になっていて──マイクロサービス間をゆるく繋ぎつつ壊れない通信や信頼度高めたメッセージ流通を狙う場合にも最適だったりする。

じゃあKafkaの根っこの動きをざっくりまとめてみるね:
- **プロデューサー**:データ(メッセージ)をKafka上のトピック宛てに投げ込む役目。
- **コンシューマー**:逆にそのトピックからひたすらメッセージを読んでいく係かな。
- **ブローカー**:複数台ある「ブローカー」上でKafka自体が動作していて、それぞれでデータ分散&保存管理…実際、この辺が要とも言える。

だからね、大量データを非同期的につるっと捌かなくちゃいけない現場とか、“イベントソーシング”だとか“リアルタイムパイプラインの構築”、あとは“ログ集約”とか、とにかく俊敏さや規模感重視されるタスクになった途端、この仕組みの妙味が活きてくるんだよ…。ま、いいか。

サービス同士をKafkaでどうやって疎結合化できる?

Kafkaを導入したことで――まあ正直、サービス同士の距離がグッと遠のいた感がある。なにより**疎結合**、これ一択じゃないかってくらい重要になったんだよね。以前なら同期HTTP通信にすっかり頼ってたんだけど、今ではイベントドリブン構成への変更によって「直接他サービスにリクエスト送らなくても平気」という状況。かわりにKafkaトピックへ淡々とイベント投げちゃえば事足りる。この切り替え例を下に挙げておく。

@Autowired
private KafkaTemplate
 kafkaTemplate;

public void sendMessageToKafka(String message) {
kafkaTemplate.send("notification-topic", message);
}

そういうことだから、昔みたいに他サービスから応答帰ってくるまでボーッと処理止めとく必要も消滅した。ま、「こっちで適当にイベント送り出しておけばいいや」ぐらい気軽なもの。そのイベントが欲しい側は例によってトピック購読しておいて、非同期的に中身取得して食べれば済む話(まあ、それぞれ事情はあるけど)。この辺が功を奏して**依存性もサラっと低減**しつつ、結果として全体的な障害耐性まで底上げされたような気配さえ漂う。

しかもねぇ、高トラフィック対応も前より上手く回せてる印象なんだ。実際のところKafkaなし時代は完全同期型だったから、一つ二つサービスで混雑発生→その余波でシステム全部が巻き込まれてガタ落ち…なんて目も当てられない流れ、普通に起こり得た。でも今?このやり方なら少なくとも負荷集中の連鎖爆発には強くなった、と自分でも思う。不意打ちにも割と動じなくて済むので案外助かったりするんだよね。ま、いいか。

サービス同士をKafkaでどうやって疎結合化できる?

ピークトラフィックでも処理遅延を防ぐ具体的な手法

Kafkaはね、こう…各サービスのあいだに一種のバッファみたいな感じで挟まるんだけど、それがトラフィック急増時にわりと良いクッション役を果たしてくれるんだよな。もし大量リクエストが一気に来ちゃったとしても、慌てて全部リアルタイムで処理する代わりに、Kafkaキューへメッセージをぽんぽん放り込めばOK。その後はコンシューマ側サービスが並行的にじわじわっと(とはいえ速度も出せるけど)取り出して消化するという寸法だよ。うーん、要するにどこか一カ所のサービスだけが過剰にしんどくならなくて済むし、ときには膨大なデータ量でもちゃんと捌ける。このしくみ、本当にありがたい。

しかもKafkaはさ、ブローカーを水平方向っていうのかな、とにかく何台も並べて規模拡大できるから、トラフィック山盛りになった時も「え…パフォーマンス終わった?」って心配なしで耐えてくれる。思えばその柔軟さが最大の持ち味なんじゃない?

あともうひとつ強調したい点あるよ。Kafkaってフォールトトレランス - つまり耐障害性 - がけっこう堅牢で頼もしい。仮にだけどコンシューマサービス側が落ちたりエラー吐いたとしても、その間はKafka上でトピック内メッセージずっと保持され続ける仕組み。なので復旧して動き出した瞬間から未処理分を漏れなく回収・消化できるし、「もしサーバーダウン中データ飛ばしちゃったらどうしよう」みたいな恐怖は基本いらない…うっかり忘れてしまいそうになるくらい自然だな。

それと細かい話だけど、Kafkaではアクノリッジメントやオフセット管理って仕掛けを駆使したリトライ(再試行)機構まで実装されているんだ。ふう…地味だけど着実な安心材料になる、この辺意外と侮れないポイントだったりするね。ま、いいか。

Kafkaの再試行機構で可用性と信頼性をどう確保するか

メッセージの処理がうまくいかなかったときも、全体のシステムに悪影響を及ぼさずコンシューマ側で自動的にリトライができる――なんだか地味だけど、安心感はあるよね。で、Kafka コンシューマで手動アクノリッジメントを使って信頼性を高めたい場合には、例えばこういう書き方になる(たぶん現場でもよく見るやつ):

typescript
@KafkaListener(topics = "notification-topic", groupId = "notification-group")
public void listen(ConsumerRecord<string string> record, Acknowledgment acknowledgment) {
try {
// メッセージ受け取ったら、その内容を適切に処理して
processMessage(record.value());
// 無事終わったら自分で ack を呼ぶ感じ
acknowledgment.acknowledge();
} catch (Exception e) {
// もし失敗しちゃっても焦る必要なし
// 設定によって勝手に再試行になるしな…
}
}
<pre><code>


こういう仕組みのおかげでさ、Kafka の導入以降は昔ながらのモノリシックな通信方式からイベントドリブンな設計へ自然と移行した感じだ。これまでありがちなリクエスト・レスポンスじゃなくて、「User Created」や「Order Completed」とかイベント単位でサービス同士の状態変化を伝え合うようになった。何となく便利そう…いや実際助かってる。このやり方なら各サービスがそれぞれ該当するトピックを監視していて、自律的かつ非同期にイベント対応できちゃうんだよな。おまけに、それぞれ別個にスケール可能だから柔軟性も出る。

例えばユーザーが注文すると、order サービスから次みたいなイベントが生じることになる――

public class OrderPlacedEvent {
private String orderId;
private String userId;
private double amount;

// ゲッター・セッター類などご自由に
}

その後 inventory サービスとか payment サービス、それから notification サービスなどがこのイベント情報を受け取り、それぞれ勝手(?)に責任範囲内の仕事(たとえば在庫減算だったり決済だったり通知送信だったり)を進めていく流れ。・・・まあ、一見遠回りっぽい気もするけど、不思議と煩雑にならない。不安?慣れると割と快適だと思う。ま、いいか。

Kafkaの再試行機構で可用性と信頼性をどう確保するか

イベント駆動型アーキテクチャ移行の現実的な進め方

Kafkaって、何て言うんだろう、システムのアーキテクチャをわざと頑丈めにしてるからさ、レジリエンスとか監視能力?あたりが昔よりもグッと良くなってる感じがある。仮にどっかのサービスが突然落ちたとしても…まあよくある話だけど、Kafka側ではメッセージを失わせないための工夫をいろいろしていてさ。いや、本当にトピック内部でしっかり保存され続けているから、その故障したやつが復帰した後も途切れずバックログ(溜まった分)の処理をまた始められるという仕掛けなのだ。実はこの辺の「分散」特性のおかげでレプリケーション――つまり複数コピーみたいなこと――も可能になるので、ほんと頼れる…まあ、便利すぎるなと思う瞬間、多いよね。

マルチサービスへのイベント伝搬をシンプルに実装するコツ

この仕組みがあるおかげで、仮にどこか1台のブローカーがダメになったとしても…まあ、大ごとにはならない。他のレプリカが自動で後を引き受けてくれるので、高可用性とか耐障害性はちゃんと守られる。とはいえ、運用面もやっぱり重要だなぁ。Kafkaの監視や管理については、Kafka ManagerとPrometheusをがっつり導入していて、Kafkaブローカーやトピックの状態なんかも逐一リアルタイムで監視できる体制にしたんだよね。この辺のおかげで、「あれ?これヤバイ?」って小さな兆候にも早めに気付いて手当できるので、結局システム本体への影響をかなり抑え込むことに成功した気がする。

## Spring BootにおけるKafkaセットアップ

Spring Kafkaライブラリの存在にはちょっと救われたというか、Spring Boot上でKafkaを扱う設定プロセス、本当に思いのほか簡単だった。その連携方法について、下記ざっとまとめてみた。

## 依存関係の追加

`pom.xml`には必要となる依存パッケージをサクッと記述(実際コピペした)。

xml
<dependency>
<groupid>org.springframework.kafka</groupid>
<artifactid>spring-kafka</artifactid>
</dependency>


## Kafkaプロデューサーとコンシューマーの設定

今回はプロデューサーとコンシューマー、それぞれ用に構成クラスを書き起こした感じ。

@Configuration
public class KafkaConfig {

@Bean
public KafkaTemplate
 kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}

@Bean
public ConsumerFactory consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfig());
}

@Bean
public ConcurrentMessageListenerContainer messageListenerContainer() {
return new ConcurrentMessageListenerContainer<>(consumerFactory(), new ContainerProperties("notification-topic"));
}
}

## メッセージ送信および受信

サービス層では通知メッセージの送信・受信メソッドもしっかり設計してある。まあ、ごく普通だけど...慣れてない人は最初つまづくポイントかな、とふと思う。

@Service
public class NotificationService {

@Autowired
private KafkaTemplate kafkaTemplate;

public void sendNotification(String message) {
kafkaTemplate.send("notification-topic", message);
}
}

受信(つまりコンシューム)については、お決まりと言えばそれまでなんだけど `@KafkaListener` アノテーションで完結。

@Service
public class NotificationConsumer {

@KafkaListener(topics = "notification-topic", groupId = "notification-group")
public void consume(String message) {
System.out.println("Consumed message: " + message);
}

マルチサービスへのイベント伝搬をシンプルに実装するコツ

可観測性と冗長化設計で障害発生時もメッセージ損失ゼロへ

Kafkaって、水平スケーラビリティのおかげで、大きく負荷を色んなコンシューマたちに割り振れるようになったんだよね。これが実際のところ、全体のパフォーマンスへの悪影響をかなり抑えているという感じがする。ま、それはそれで助かる…と言いたいけどさ(笑)。あと、Kafkaにはデータ永続化機能とかメッセージ再試行っていう仕組みも標準搭載されていて、例えば一時的なサービス停止があったとしてもデータが飛んじゃうリスクはまずない。その辺の耐障害性も、わりと安心できるポイントではあるよ。うーん、しかもちゃんと監視ツールさえ使えばね—Kafkaの動作状況をずっとチェックしながら、不調が出てもリアルタイムに手を打てたりする。そのお陰で予期せぬトラブルにもそこそこ柔軟に立ち向かえる気分になる、ってのが正直なところかな。ま、いいか。

今すぐ始めるSpring Boot×Kafka連携のセットアップ例

結局として、うーん……Kafkaがバックエンドの仕組みを、同期型で脆弱だったものから、耐障害性とスケーラビリティのある設計にガラッと変えてしまったって感覚だね。ま、いいか。Spring BootマイクロサービスにKafkaをドッキングしたことで、とんでもないトラフィック量にも一応対応できるようになったし、安心してメッセージをやり取りできるってわけか。それだけじゃなくて、今まで諦めてたレベルでサービス同士の結合度もグッと下がった印象(まあ全部バラ色とは言い切れないけど)。もしマイクロサービス環境で「もうちょっと規模広げたいな」と感じたら、Kafkaの選択肢は案外馬鹿にならない気がする。…そのパワフルさは、大規模な分散システム作る時によくぶち当たる悩み――ほら、不意打ちみたいな障害とかデータ流通問題――あたりへの現実的な対策になる可能性が高いかなと思う。

Related to this topic:

Comments