Spring Bootマイクロサービスにおける自己修復とカオスエンジニアリング活用事例:サーキットブレーカーの効果とは

Published on: | Last updated:

落ちるサービスより、「勝手に復活するサービス」を育てたい

マイクロサービスって、ちゃんと動いてるときは気持ちいいんだけど、一個どこかでコケると一気に地獄になるやつじゃないですか。

だから最近、自分は Spring Boot でサービス作るとき、「機能が動くか」より前に「壊れても自力で立ち直れるか」を先に考えるようになってて、そのときにけっこうしっくりきた組み合わせが、Resilience4j のサーキットブレーカー + Chaos Monkey でのカオスエンジニアリングなんですよね。

ざっくり言うと、「外部APIとかが変な動きしても、サービス全体を巻き込まずに済ませる仕掛けを入れておく」「わざと壊しておいて、本当に耐えられるかを先に確認しておく」っていう話です。

ざっくり「勝手に復活するマイクロサービス」の流れ。
ざっくり「勝手に復活するマイクロサービス」の流れ。

結論だけ先に言うと:マイクロサービスやってて「なんか最近よく落ちるな」と思ってるなら、ログを読む前に、サーキットブレーカーとカオスエンジニアリングを一回ちゃんと触っておくと、後でだいぶ楽になります。

なんでそこまで「レジリエンス」がうるさいのか

分散システムのめんどくささ:マイクロサービスって、サービスを小さく分けてデプロイしやすくして、チームごとに独立させて…みたいなきれいな話がよく出てくるけど、実際やると「依存サービスが増えただけ」とも言えるんですよね。

たとえば:

  • 外部の決済 API がちょっと重くなる
  • 在庫システムの DB が一瞬スパイクする
  • 通知サービスのキューが詰まり始める

これのどれか一つが遅くなっただけで、フロントのレスポンスが一気に悪くなったり、タイムアウト連鎖したりして、「どこが悪いのかよく分からないけど全体的に遅い」状態になるやつ。

「たまたま大丈夫」は信用しない:台湾のプロジェクトでも、日本の SI 案件でも、最初のリリースのときって、負荷も少ないし外部 API もまだ優しいから「案外いけるじゃん」とか思うんですよ。

でもセール期間とか、momo や楽天のキャンペーンと重なったり、ちょっとだけトラフィックが増えた瞬間に、今まで気づいてなかった依存関係の弱さが一気に出てくる。

「ちゃんと作ったから大丈夫」じゃなくて、「壊れる前提で安全装置を入れてあるか」がマイクロサービスではだいたい勝ち筋。

その安全装置として分かりやすいのが、サーキットブレーカーカオスエンジニアリングです。

サーキットブレーカーって、結局なにしてるやつ?

イメージ:家のブレーカーとほぼ同じで、「危なそうだから一旦スイッチ切っとくね」ができる仕組みです。

たとえば、Spring Boot のサービスから外部の決済 API を叩いているとして:

  • 普段は普通にリクエストを投げる
  • 失敗が一定割合を超えたら、「この API 今やばそうだな」と判断してスイッチを切る
  • スイッチが切れている間は、そもそも外部 API を呼びに行かずに、フォールバック処理に流す
  • しばらくしてから、ちょっとだけ試しにまた呼んでみて、大丈夫そうならスイッチを戻す

これをライブラリでやってくれるのが Resilience4j です。

Resilience4j を選ぶ理由:昔からある Hystrix とかも名前はよく出てくるけど、最近だと Java 17 とか Spring Boot 3 とか、環境が新しくなってきてるので、軽量でモジュール分割もされている Resilience4j の方が使いやすいって話をよく聞きます。

しかも Spring Boot Actuator と組み合わせると、サーキットブレーカーの状態や失敗率をメトリクスとして拾えるので、Grafana でダッシュボード作るのも楽。

コードの雰囲気:よくあるのは、決済サービスにこんな感じでアノテーションを貼るパターン。

@Service
public class PaymentService {

    @CircuitBreaker(name = "paymentGateway", fallbackMethod = "paymentFallback")
    public PaymentResponse processPayment(PaymentRequest request) {
        // 外部の決済ゲートウェイ呼び出し
        return paymentGatewayClient.process(request);
    }

    public PaymentResponse paymentFallback(PaymentRequest request, Throwable t) {
        // リトライ用キューに積んでおく
        queuePaymentForRetry(request);
        return new PaymentResponse("Payment queued for retry");
    }
}

実際のプロジェクトだと、ここにタイムアウトやリトライ、レートリミットも一緒に絡めることが多いです。

具体的にどこにサーキットブレーカーを仕込むか

典型的な EC の注文マイクロサービスを想像すると、ざっくりこんな依存がありますよね:

  • 決済ゲートウェイ
  • 在庫システム
  • メール / Push 通知サービス

全部に一律でサーキットブレーカーを入れる…というより、「落ちたときの痛さ」で優先度をつける方がやりやすいです。

決済ゲートウェイ:ここが一番クリティカル。失敗が続くなら、いったんキューに積んで「支払い処理中」として扱う、みたいなフォールバックが現実的。

在庫システム:在庫チェックに失敗したら、保守的に「在庫なし」と判断するか、一時的に新規注文を制限するか。B2B か B2C かでも戦略変わります。

通知サービス:ここは最悪後からまとめて送ればいいので、「通知だけキューに積んで、レスポンスは先に返す」というフォールバックが多いです。

つまり、サーキットブレーカーは「どこで諦めて、どうやって後処理に回すか」を決めるスイッチなんですよね。

でも、サーキットブレーカー「入れただけ」だと案外危ない

設定ファイルにそれっぽく threshold 書いて、アノテーション貼って、「はい完了!」ってやりがちなんですけど、ここからが本番で。

実運用だと、こんなパターンが普通に出てきます:

  • 失敗率のしきい値が厳しすぎて、ちょっとしたネットワークの揺れでブレーカーが開きまくる
  • 逆に緩すぎて、ブレーカーが開く前にスレッドプールが溶ける
  • フォールバックの中で、また別の重い処理をやってしまって、意味がなくなる

ここで必要になってくるのがカオスエンジニアリングです。

カオスエンジニアリングって、Netflix だけの話じゃない

カオスエンジニアリングって聞くと、「Netflix みたいな巨大サービスがやるやつでしょ?」って空気がまだちょっとあるんですけど、小さめの Spring Boot マイクロサービスでも、むしろやった方がコスパいいなと思ってて。

Spring 系だとChaos Monkey for Spring Bootがそのまま使えるので、「ちょっと試しに壊してみる」がかなり気軽にできます。

普通の状態/遅延を注入した状態/完全に落とした状態を並べて見ると、設計の穴が見えやすい。
普通の状態/遅延を注入した状態/完全に落とした状態を並べて見ると、設計の穴が見えやすい。

Chaos Monkey でよくやる遊び:

  • 支払いクライアントにランダムな遅延を入れる(ネットワークぐずりの再現)
  • 特定の Bean をランダムに kill して、例外を投げさせる
  • 一定割合で強制的に失敗させる

これを、JMeter や k6 みたいなツールで負荷をかけながら回すと、サーキットブレーカーの設定が現実に合っているかどうかがだいたい見えてきます。

実際にカオスを入れてみると、どこがバレるのか

1. レイテンシーに弱すぎ問題:Chaos Monkey で 1〜3 秒くらいのランダム遅延を入れると、「タイムアウト設定が妙に長くて、スレッドが溜まりまくる」「逆に短すぎて、すぐに失敗扱いになってしまう」とかがすぐ見つかります。

ここで Spring Boot Actuator のメトリクス(サーキットブレーカーの状態や、外部呼び出しの成功/失敗率)を Prometheus 経由で拾って、Grafana で時系列を見ると、「どのレイテンシーでどんな挙動になってるか」が視覚的に分かりやすいです。

2. リトライストーム:外部 API が落ちているときに、リトライロジックが雑だと、「落ちている先に向かって、全サービスが全力でリトライを撃ち続ける」という地獄モードに入りがちです。

カオスを入れてみると:

  • サーキットブレーカーが開くまでの間に、どれくらい無駄なリトライが飛んでいるか
  • フォールバック側のキューや DB に、どれくらいの速度でタスクが溜まっていくか

このあたりの挙動がかなりはっきりします。

3. 想定してなかったフォールバックのバグ:「とりあえずキューに積んでおこう」とか「一旦保留にして後で処理」とか、フォールバック側の実装って、つい雑に書きがちなんですよね。

Chaos Monkey で依存先を完全に落としてみると、

  • フォールバックで NPE が出て落ちる
  • エラー文言がユーザーにそのまま見えてしまう
  • 想定外の例外型が返ってきて、ハンドリングできていない

みたいなところがきれいにあぶり出されます。

レジリエンス設計で見ておきたい「表じゃない指標」

マイクロサービスの話をするときって、どうしても「平均レスポンスタイム」「エラー率」みたいな表面的な数字に寄りがちなんですけど、レジリエンスを見るなら、もう少し違うメトリクスが効いてきます。

サーキットブレーカーの状態遷移:「Closed / Open / Half-Open」がどれくらいの頻度で切り替わっているか。

たとえば:

  • 一日に何回 Open になっているか
  • Half-Open に戻したとき、どれくらいの割合で成功しているか
  • 特定の時間帯だけ異常に Open が多くなっていないか

フォールバック率全体のリクエストのうち、何%がフォールバック経由になっているか。

これが高すぎると、「一応サービスは生きてるけど、裏側はずっと火事」みたいな状態になっている可能性があります。

リトライキューの滞留時間:フォールバックでキューに積んだタスクが、平均どれくらいで処理されているか。

ここが長くなりすぎると、ユーザー体験としては「いつまでも処理中」のままになるので、SLA 的な観点でも危険ゾーンに入ります。

この辺を Spring Boot Actuator + Prometheus + Grafana みたいな王道セットで可視化しておくと、カオスエンジニアリングの結果も分析しやすいです。

「速いかどうか」だけじゃなくて、「壊れ方」と「戻り方」を見る指標が大事。
「速いかどうか」だけじゃなくて、「壊れ方」と「戻り方」を見る指標が大事。

Resilience4j × Chaos Monkey をざっくり比較してみる

役割の違いを、一回テーブルで並べた方がイメージしやすいかもしれません。

項目 Resilience4j(サーキットブレーカー) Chaos Monkey for Spring Boot
ざっくり役割 壊れたときの「安全装置」 わざと壊して「弱点をあぶり出す」
使うタイミング 本番・ステージング問わず常時 検証環境 or 本番でも慎重に一部だけ
メイン対象 外部 API 呼び出し、DB、メッセージキュー Spring Bean、メソッドレベル、レイテンシー
得られるもの 連鎖障害のブロック、フォールバック経路 設計の穴、しきい値設定のまずさ、バグ
開発チームへの効果 「とりあえず落とさない」安心感 「どこが本当に弱いか」の共通認識

じゃあ、自分のプロジェクトでどう始める?

一気に全部やろうとするとだいたい詰むので、個人的にはこんな順番が楽かなと思ってます。

1. 一番怖い依存先だけにサーキットブレーカーを入れる

まずは決済ゲートウェイとか、外部 SaaS(送信制限が厳しいメールサービスなど)みたいな、「止まるとビジネスインパクトが大きいけど、自分ではコントロールしづらい」依存先だけに絞る。

2. フォールバックの UX を決める

技術的な話より前に、「ユーザーにどう見せるか」を決めておくと、フォールバックの実装がブレにくいです。

  • 決済なら「処理中」として保留にするのか
  • 在庫なら「一時的に購入不可」にするのか
  • 通知なら「後でまとめて送る」だけでいいのか

3. Chaos Monkey をステージングにだけ入れて、軽く壊してみる

いきなり本番でやるのはさすがに怖いので、まずはステージングで:

  • レイテンシーを入れてみる
  • 特定の Bean を kill してみる

くらいから始めると、サーキットブレーカーの設定の「雑さ」がすぐ見えてきます。

4. メトリクスを一枚のダッシュボードにまとめる

Prometheus + Grafana でも、Datadog でもいいので、

  • 外部呼び出しの成功率・失敗率
  • サーキットブレーカーの状態
  • フォールバック率

あたりを一画面で見えるようにしておくと、カオスを入れたときの挙動が一気に理解しやすくなります。

サーキットブレーカーとカオスエンジニアリングを「ちゃんと一回やった後」は、リリースの気持ちがだいぶ変わる。
サーキットブレーカーとカオスエンジニアリングを「ちゃんと一回やった後」は、リリースの気持ちがだいぶ変わる。

正直なところ、自分が一番楽になったポイント

インシデントのときの会話が変わる:前までは障害が起きると、

  • 「どこが悪いのかよく分からない」
  • 「とりあえず全部のログ見よう」

みたいな空気になりがちだったのが、サーキットブレーカーとカオスエンジニアリングを一周回した後だと、

  • 「今どのブレーカーが Open になってる?」
  • 「フォールバック率が急に上がったのはどこ?」

みたいな、少しだけ構造化された会話になるんですよね。

「落ちないコード」から「壊れても戻ってくるシステム」へ:コード単体をどれだけきれいに書いても、外の世界は好き勝手に壊れるので、結局は「壊れ方」と「戻り方」を設計しておいた方が精神衛生がいい。

サーキットブレーカーとカオスエンジニアリングは、そのためのわかりやすい入口、みたいな感じがしています。

テストで一度も壊してないシステムを、本番に出すのはけっこうギャンブル寄りの行為。

最後に、どこから一歩目を踏み出すか

もし今、Spring Boot でマイクロサービスをやっていて、

  • 外部 API の調子にいつも振り回されている
  • 障害のたびに、どこから見ればいいか迷う

みたいな状態なら、まずは一番怖い依存先にだけ Resilience4j を入れてみて、その上で Chaos Monkey をステージングに仕込んで軽く遊んでみる、くらいからで十分です。

今すぐ全部完璧にやる必要はないので、今日は「一番クリティカルな外部サービスにだけサーキットブレーカーをつけてみる」ところまで、軽く手を動かしてみてください。

そこから先で、「あ、ここも壊れうるな」と思った場所が増えてきたら、そのときまた一緒に悩めばいいと思います。

Related to this topic:

Comments