開発ノート:レガシーシステムをReactive Streamsで再生させたプロジェクトの舞台裏

プロジェクト背景と課題認識
当社が受注したのは、金融業向け顧客管理システムのマイグレーション案件でした。既存のモノリシックシステムはJava EE 6で10年以上稼働しており、要求件数が増えるたびにスレッドプールが飽和して応答遅延が常態化していました。特に夜間バッチ処理がピーク時に並行実行されると、画面応答が30秒を超えることもあり、ユーザーから「費用対効果が合わない」との指摘が出ていました。さらに、モノリスのままリニューアルを進めると、開発会社選び方も限定されて予算オーバーの懸念が強まりました。
システムの問題点を整理すると、以下のようになります。
-
スレッドブロッキング:従来の同期I/Oモデルでスレッドが待機
-
スケールしづらい設計:水平スケール時のコスト増大
-
保守性低下:巨大なコードベースのため、相場以上の改修費用が発生
-
リリース負荷:一部を修正しても全体デプロイが必要
読者の皆さんも、「開発会社に見積もりを依頼したら、莫大な予算(費用相場)を提示された」という経験があるかもしれません。本案件では「発注→開発→運用」で無駄なく予算を最適化することが求められました。また、レガシー部分の全面書き換えではなく、部分的なリアクティブ化を行うことでコストを抑制し、短期間で効果を出す方針を採用しました。
本記事では、非同期処理とReactive Streams導入の具体的手順、そしてそこで得られた教訓をストーリー仕立てでお伝えします。
非同期処理への移行アプローチ
まずは既存の同期I/O部分を段階的に非同期化するステップから着手しました。具体的には、Java標準のCompletableFuture
を活用し、データベースアクセスや外部API呼び出しを非同期へ置き換えます。
-
スレッドプールの見直し
-
既存Tomcatの
maxThreads
を調整し、同期処理時の待機時間を可視化 -
デフォルトスレッド数では追い付かないことが判明し、非同期化の必要性を確信
-
-
非同期I/Oライブラリ導入
-
R2DBC
(Reactive Relational Database Connectivity)を採用し、DBアクセスをイベント駆動へ -
外部REST API呼び出しは
WebClient
に切り替え
-
-
段階的リリース
-
まずはユーザー一覧検索機能を非同期化し、負荷テストでレスポンスタイム改善を検証
-
成功後、他の機能へ順次展開
-
-
モニタリングとアラート設計
-
Prometheus+Grafanaで非同期処理のレイテンシとエラー率を可視化
-
特定APIの平均応答時間が500msを超えた場合にアラート
-
上記アプローチにより、初期段階で約30%のスループット改善を確認できました。これにより、従来よりスレッド数を減らしても同等以上の処理性能を得られ、インフラコストを圧縮できたのです。この成功体験が、リアクティブプログラミング全体適用へのモメンタムとなりました。
リアクティブプログラミングの導入プロセス
非同期処理の延長線上として、Spring WebFlux+Project Reactorを採用し、システム全体をリアクティブ化しました。主なステップは次の通りです。
-
コントローラの書き換え
-
既存の
@RestController
を@RestController
+Flux
/Mono
戻り値へ変更 -
従来の
@Transactional
は非推奨となったため、@Transactional
付きメソッドを分割
-
-
ドメインサービスのリアクティブ化
-
ブロッキングI/Oを行うレガシーメソッドは
Schedulers.boundedElastic()
で隔離 -
最終的に全I/Oを非ブロッキング化し、純粋なリアクティブパイプラインを構築
-
-
エラー処理設計
-
onErrorResume
/onErrorMap
を駆使し、全体フローを途切れさせずにフォールバック
-
-
テスト戦略
-
Reactor Testの
StepVerifier
でFlux/Monoの挙動を網羅的に検証 -
全体E2Eテストも同期API時代と同様に実行可能
-
導入後は、ピーク時の同時接続数が従来比2倍まで耐えられるようになり、レイテンシは50%削減。これにより、運用中の追加ハードウェア投資を一切行わずにシステムスケールが達成できました。
このリアクティブ化を進める中で得られた教訓は、「開発会社選び方では、Reactive Streamsに関する実績を必ず確認すること」です。流行の技術とはいえ、社内にノウハウがないまま発注すると、高額な追加費用やプロジェクト遅延のリスクが高まります。
運用コスト削減と予算管理
リアクティブ化で得たパフォーマンス向上は、直接的に運用コストの削減につながりました。以下のポイントで予算と費用を最適化しています。
-
インフラ費用の抑制
-
従来のインスタンス3台構成から2台構成へダウンサイジングし、年間約20%のクラウド費用削減
-
-
運用負荷の軽減
-
スレッドダンプ解析、GC監視の頻度が減り、SREの稼働時間を削減
-
-
ライセンスコストの見直し
-
商用モニタリングツールからOSSベースのPrometheus/Grafanaへ移行し、年間ライセンス費用50万円を削減
-
-
予算計画の透明化
-
リアルタイムダッシュボードを経営陣に共有し、開発→運用→保守のコスト構造を可視化
-
追加開発の際はROIを明示し、承認プロセスを簡略化
-
また、予算編成時には「開発→テスト→運用」までのライフサイクルコストを含む見積もりを作成し、相場を超える部分があれば見積り段階でベンダーと交渉しました。この取り組みにより、契約後の追加見積もりや発注変更が激減し、プロジェクトマネージャーは開発に集中できる環境を実現しました。
リアルタイムモニタリングとアラート設定
導入したReactive Streams化システムは、非同期処理によりスループットが向上しましたが、運用時の可視化が不可欠です。そこで以下のモニタリング項目を中心にダッシュボードを構築しました。
-
スループット(RPS):1秒あたりのリクエスト処理数を可視化し、ピーク時の耐性を把握
-
レイテンシ分布:P50/P95/P99の応答時間をトラッキング
-
エラー率:ステータスコード5xxの割合を監視し、しきい値超過で即時通知
-
バックプレッシャー状況:Reactive Streamsの
onBackpressureDrop
やonBackpressureBuffer
の発生回数 -
リソース使用率:CPU/メモリ/スレッドプールの稼働率
これらをPrometheus+Grafanaで可視化し、Slack連携のアラートを設定。
-
しきい値設定:
-
レイテンシP95が800msを超えた場合にWarning
-
エラー率が2%を超えた場合にCritical
-
-
アラートチャンネル分割:
-
開発チームにはWarningのみ通知
-
SRE部門にはCritical以上を通知
-
-
自動復旧フロー:
-
監視ツールからAWS Lambdaが起動し、サービス再起動やスケールアウトを実施
-
これにより、システム全体の安定性と可用性を維持しつつ、予算内での運用コストを最適化できました。
セルフヒーリング機構の実装
高負荷時には一部マイクロサービスが遅延するケースがありました。運用チームの負担を減らすため、セルフヒーリング(自己修復)機能を以下のように組み込みました。
-
Circuit Breaker(回路遮断機構)
-
Resilience4jを導入し、エラー率やタイムアウトが一定を超えた場合に自動的に外部呼び出しを遮断
-
-
Bulkhead(隔離)
-
重要な処理を専用スレッドプールに割り当て、他処理の影響を受けないように分離
-
-
Fallback メカニズム
-
メイン処理が失敗した際、キャッシュ済みデータやデフォルトレスポンスを返却
-
-
自動リトライ
-
一時的エラーに対して指数バックオフで最大3回までリトライ
-
具体的には、顧客照会APIが過負荷時に遅延した際、自動的にCircuit Breakerが作動し、デフォルトの「現在メンテナンス中です」メッセージを返却。その後、一定時間が経過するとService Discoveryを通じて正常ノードへトラフィックを誘導します。これにより、ユーザー体験を損なわずにシステム全体の健全性を維持しました。
DDD適用とマイクロサービス分割
反応性パイプラインの次は、ドメイン駆動設計(DDD)を活かしたマイクロサービス分割を行いました。
-
バウンデッドコンテキストの特定
-
顧客管理、取引管理、レポート生成といったドメインごとに独立チームを編成
-
各チームは開発会社を選定する際、専門性の高いベンダーに発注
-
-
契約テスト(Consumer-Driven Contracts)
-
Pactを利用し、チーム間インターフェース品質を担保
-
-
データ分散管理
-
各マイクロサービスがOwn DBを持ち、整合性はEventual Consistencyで実現
-
Kafkaを中心にイベント駆動アーキテクチャを構築
-
-
インテグレーションパターン
-
Sagaパターンで分散トランザクションを実現
-
OrchestrationとChoreographyを使い分け
-
分割に際しては「小規模プロジェクト vs 大規模プロジェクトの基礎知識比較」を意識し、小さなバウンデッドコンテキストから着手。これにより、予算や開発期間の見積もりを細分化しやすくなり、費用相場を超えない範囲でリリースを継続的に行えました。
データ同期と整合性の維持
マイクロサービス分割後の課題は、複数DB間でのデータ整合性。以下の施策で対応しました。
-
Change Data Capture(CDC)
-
DebeziumでDBの変更イベントをKafkaに流し、他サービスが購読
-
-
Read Model の構築
-
Queryサイド用にElasticsearchやRedisを構築し、高速な検索・集計を実現
-
-
スキーマ管理
-
Avro+Schema Registryでスキーマの互換性を管理
-
-
データスナップショット
-
定期的にバッチで全スナップショットを取得し、万が一のロールバックに備える
-
これにより、各コンポーネントは最新のデータをリアルタイムに取得でき、整合性と可用性を両立できました。
キャパシティプランニングとコスト管理
本番運用フェーズでは定期的にキャパシティプランニングを実施。特に以下の指標を重視しました。
指標 | 目標値 | コメント |
---|---|---|
Peak RPS | 1500 | 従来比2倍 |
P95 レイテンシ | 700ms | 非同期化で50%改善 |
エラー率 | <= 1% | Circuit Breakerで制御 |
スケールアウト時間 | <= 2分 | Canaryリリース込み |
インスタンス台数 | 2→1 | リサイズで年間契約費を20%削減 |
この計画に基づき、AWS LambdaやKubernetes HPAを活用して自動スケールを実現。インスタンス台数削減により、クラウド費用を大幅に圧縮できました。
ROI分析と成果報告
最後に、経営層・発注元へROIを示すための分析を実施しました。
-
改善前後のKPI比較
-
ユーザー応答時間:平均1.2秒 → 0.6秒
-
バッチ完了時間:60分 → 25分
-
-
費用対効果試算
-
インフラコスト削減:年間300万円
-
開発・保守コスト:Reactive化で初期費用200万円、年間保守費100万円
-
純削減額:年間200万円以上
-
-
定量効果+定性効果
-
顧客満足度アンケートで「業務効率化に貢献」との回答が80%
-
システムダウンタイムゼロを6ヶ月間達成
-
これらの数値をもとに、開発会社選び方のポイントや予算管理のノウハウを振り返り、次期プロジェクトへの課題と改善案をまとめて報告書を提出しました。