前書き
この記事は、以前お世話になったエネルギー会社で開発したシステムの経験を記録したものです。
コレオグラフィからメディエイター、そしてDBボトルネックまで
この記事では、イベント駆動アーキテクチャ(Event-Driven Architecture: EDA)の基本概念を押さえたうえで、AWS上での実装例を通して「コレオグラフィがハマる領域」と「メディエイター(オーケストレーション)が必要になる領域」を具体的に比較します。さらに、実装して運用し始めて初めて見えてきた現実的な課題――特に並列イベント処理とDBのボトルネックの衝突――について、実際にとった対策とその結果を説明します。
結論だけ言えば、EDAは「正常系を高速に流す疎結合の仕組み」として非常に強力ですが、業務フローの複雑さや運用要件が乗った瞬間に設計のフェーズが変わり、最後はDBをどう扱うかが勝負になります。
1. イベント駆動アーキテクチャとは何か
イベント駆動アーキテクチャとは、「システム内で起きた出来事(イベント)」を中心に、処理連携や状態変化を組み立てるアーキテクチャです。従来の同期型アーキテクチャでは、あるサービスAが別サービスBのAPIを直接呼び出し、結果を待って次の処理へ進む形が一般的でした。これは構造が直感的である一方、サービス同士が強く結びつきやすく、「AがBを知っている」「Bの応答速度がAの処理速度を決める」といった依存関係がシステム全体に波及します。
EDAでは発想が逆です。サービスAで何か重要な状態変化が起きたら、それを「イベント」という形で外部に発行します。イベントは「起きた事実の宣言」であり、発行側は「誰が受け取るか」「その後どう使われるか」を知りません。イベントを受け取りたい側のサービスは、イベントを購読(Subscribe)し、届いたイベントに基づいて自律的に処理を実行します。これにより連携は非同期化し、サービス間の依存は“API呼び出し”から“事実の通知”へと置き換わります。
基本構造を図にするとこうなります。
この図のポイントは、A(発行者)がC/D/E(購読者)を一切知らないことです。依存が一方向になり、イベントの受け手が増えても発行者の変更が不要になります。結果としてシステムは拡張に強くなり、後付けで要求が増える現実に耐えやすい構造になります。
2. EDAで頻出する用語と考え方
EDAの議論では、まず「イベント」と「コマンド」の区別が極めて重要です。コマンド(Command)は「こうしてほしい」という要求であり、成功するかどうかは実行時に決まります。たとえば「注文を作成してくれ」「支払いを実行してくれ」といった未来向きの命令です。一方、イベント(Event)は「こうなった」という過去の確定事実で、成功・失敗の結果がすでに内包された不変の記録です。「注文が作成された」「支払いが完了した」という形になります。
最後に残るのは「イベント配送の性質」です。現実の多くのブローカー(SQSやKafkaなど)は「少なくとも1回配送(At-least-once)」を採ります。つまり同じイベントが複数回届く可能性がある。したがってEDAのコンシューマー側は、必ず冪等性(同じイベントを何回処理してもシステムが壊れない性質)を持たせる必要があります。
加えて、イベントの前後関係(順序保証)やイベントスキーマの変更(互換性)も運用設計として避けて通れません。このへんは後半の「踏んだ課題」で実例として扱います。
3. EDAのメリットとデメリット
EDAの最大のメリットは「疎結合による拡張性」です。イベントを発行する側は受け手を知らないため、新しい処理を後から追加するのが非常に楽になります。たとえば既存の「注文作成イベント」に対して、後から「通知」「分析」「不正検知」「推薦更新」などの処理を足したくなっても、イベント発行側のコードを変更せずに済むことが多い。これは“機能が増え続ける現実”に対して構造的に強い性質です。
また、処理が非同期であるためピーク負荷の吸収や負荷平準化がやりやすい点も重要です。同期APIのチェーンは下流が詰まった瞬間に全体が連鎖的に遅くなりますが、EDAではブローカーがバッファとして働き、下流の回復を待てます。
一方デメリットも明確です。第一に、非同期ゆえに最終的整合性(Eventual Consistency)が前提になります。つまりある瞬間に各サービスの状態が揃っていないことが普通に起きる。これを受け入れられない設計やUIは破綻します。第二に、イベントが時間差で複数サービスへ波及するため、障害解析やデバッグが難しくなります。因果関係を追うための分散トレーシングや相関IDなどの仕組みがないと、どこで何が起きたか見失いやすい。第三に、重複配送・順序・スキーマ変更など、分散システム特有の運用課題が必ず発生します。
EDAは「変化に強い疎結合」を売りにする代わりに「整合性・可観測性・運用成熟度」を要求してくるアーキテクチャだと言えます。
4. AWSでの最小構成:APIGW + SQS + Lambda
AWS上でEDAを素直に組むなら、最小形として Amazon API Gateway(入口) + SQS(ブローカー) + Lambda(コンシューマー) が非常に使い勝手が良い構成です。
API Gatewayが外部からの入力(コマンド)を受け取り、それをSQSに積み、Lambda群が並列で購読・処理します。スケールも運用もAWS側に寄せられ、MVP〜中規模の現場で最もボスキャラを減らせる形です。
ここまでが「EDAの基本セット」です。
次の2章で、このセットを使った コレオグラフィ型の実例 と、そこから発展した メディエイター型の実例 を対比します。
5. コレオグラフィ型の実例:日次指針データから料金算出
5.1 なぜコレオグラフィが合うのか
日次指針データの取り込み→料金計算→現在料金の更新、という流れは、処理の目的が明確で、並列で走っても問題の少ない“計算・集計系”のワークロードです。この手の処理では、全体の進行役(中央の司令塔)を置くよりも、イベントをトリガーに各コンシューマーが独立に反応していく「コレオグラフィ型(自律分散)」が最もシンプルで壊れにくいです。
流れは単純です。指針データが入ったという事実(メッセージ)がSQSに積まれ、それを複数のLambdaがそれぞれの責務で処理していきます。後から分析や通知を追加したくなっても、SQSを購読するLambdaを1個増やすだけで済みます。
5.2 実際に踏んだ課題と解決策
ただし、現実には「並列に処理する」という性質ゆえに、コレオグラフィ特有の課題が確実に出ます。ここが“教科書では薄いけど現場では濃い”部分です。
まず重複配送と冪等性です。SQS/Lambdaの組み合わせはAt-least-once配送なので、同一メッセージが再配送される可能性をゼロにできません。そこで各処理の単位ごとにUUID(処理ID)を割り振り、処理開始時に「そのIDが既に処理済みか」を確認する軽量ガードを入れました。これにより二重計算や二重更新の事故を防ぎます。重要なのは、冪等性を“ブローカー側”ではなく“各コンシューマーが自分の責務の中で持つ”というスタイルに寄せることです。
次に異常系の扱いです。イベント駆動では、例外をフロー内のif分岐で全部吸収しようとすると、処理の因果が散らばり一気に崩れます。だから「フローは正常系中心に細く長く」「異常系は隔離して運用と再実行へ送る」という割り切りを採りました。具体的には、再実行で回復可能な異常はDLQに落とし、回復不能もしくは判断が必要なものはログ+Slack/Backlog通知で運用プロセスに渡します。EDAは“例外をコードで潰すための仕組み”ではなく、“例外の隔離と回復のための仕組み”にした方が長期的に健全だと感じました。
最後に順序です。順序が強く求められる処理を無理にEDAへ載せようとすると、FIFOキューや順序制御のための追加ロジックが必要になり、並列化メリットが消え、コストと複雑性だけが増えます。そこで順序が本質要件のものは、無理にFIFOを使って延命せず、EDA実装を諦めて同期処理やバッチ処理に寄せました。これは「EDAを採る/採らないを判断できる」ことが設計上の重要スキルだという学びでもあります。
6. メディエイター型の実例:問い合わせをCamundaで業務割当
6.1 なぜメディエイターが必要になるのか
一方で、お客からの問い合わせ対応のような業務フローは性質が違います。
緊急保安、ガス配送、営業などの作業は、問い合わせ内容によって分岐し、並列になったり、待ち合わせが発生したり、途中で失敗したら補償や再割当が必要になったりします。さらに「今この問い合わせはどこまで進んでいるか」を運用側が常に把握している必要があります。こうしたフローをコレオグラフィに任せると、因果と状態が各サービスに分散しすぎて、運用の可視性が崩壊します。
そこで採るのがメディエイタートポロジー(Mediator Topology)です。
メディエイターはイベントの単なる受け手ではなく、業務フローの進行役(オーケストレーター)として振る舞います。今回はCamundaなどのワークフローエンジンをメディエイターとして置き、UIからの問い合わせ登録を起点に、必要な作業を順序・条件に従って割り振り、結果を受け取りながらフローを進めます。
Camundaが「次に誰を動かすか」「どこで待つか」「失敗したらどう戻すか」を決めることで、業務フローの状態と責任が中心に集約され、運用しやすい形になります。
6.2 実際に踏んだ課題と解決策
メディエイター型で現場が詰まるのは、例外フロー、リトライ、補償(Saga)、進捗の可視化といった“フロー制御そのもの”の部分です。これを自作し始めると、あっという間に「ワークフローエンジンを自作しただけ」の地獄になります。そこで、これらの機能はCamundaに委譲し、自分たちで実装しない方針を徹底しました。
メディエイターは“買うべき複雑さ”の塊だという認識が重要です。
さらに重要だったのが運用設計です。
業務フロー型システムでは、運用は“後から決めるもの”ではなく、仕様そのものです。どの状態で誰が何を判断するのか、どの例外で誰に通知されるのか、どこまで自動化しどこから手動介入なのか――こうした設計をユーザー(現場)と繰り返しすり合わせ、今後の業務がどう変わるかをレクチャーしながら合意を作りました。
ワークフローエンジンを導入しても、運用の握りがないと成功しない、というのが実戦の結論です。
7. 最後にやってみてわかったこと:DBボトルネックとの殴り合い
EDAを現実のスループットで回し始めると、イベントプロセッサーの並列度が高まるにつれて、読み書き問わずDBMSアクセスがボトルネックになります。これはほぼ例外なく起こります。
特にRDBMSは同時接続、ロック、I/Oがネックになりやすく、並列イベント処理の圧を直接受けると、イベント処理のタイムアウトや遅延に直結します。これはEDAにおける致命的なデメリットになり得ます。
ただし、RDBMSはスキーママイグレーションや整合性、監査を含めた運用保守上の価値が高く、メインDBとして外せません。結局「RDBMSを使い続けながら、イベント並列の負荷をどう受け流すか」が実戦の課題になります。
7.1 読み込みの対策
まずRDS Proxyを入れて接続数上限の問題を緩和しました。Connection Refusedは吸収できるものの、最終的なパフォーマンスはRDSインスタンスのサイズに依存するため、並列度が上がるとスケールアップが必要になりコストが上がりがちでした。
次に、RDSから計算に必要なデータをDynamoDBにコピーする方式を試しました。DynamoDBは同時接続の性能が圧倒的に高く、読み込みのスループットは改善しました。ただし並列度が上がるほど必要な通信帯域・キャパシティを事前に購入する必要があり、結局高コスト化しやすい。またマスターデータをDynamoDBに移すと運用保守の複雑性が増え、RDSにマスタを置き続ける場合は同期コストが跳ね上がります。
最終的に最も効いたのは、RDSから読み込んだデータをLambdaインスタンスの/tmpにキャッシュする方式でした。Lambdaは一度立ち上がったウォームインスタンスが再利用されるため、キャッシュヒット率が上がるほどRDSアクセスが減り、並列イベント処理の圧を効果的に逃がせます。読み込みに関しては「DBを速くする」より「DBを読まない回数を減らす」方が勝ち筋でした。
7.2 書き込みの対策
書き込み先は、RDSや並列耐性が高くない外部API以外では要件を満たせませんでした。つまり「書き込み先は動かせない」前提を受け入れたうえで対策する必要がありました。
並列コンシューマーが直接RDS/APIへ書き込みに行くと同時接続問題が確実に出るため、書き込みだけは出口で直列化・制御する設計に切り替えました。
具体的には、並列処理が終わった結果を一旦SQSに集約し、出口にWriter Lambdaを置いてメッセージを順序制御・バルク化して書き込みます。
記録先がRDSの場合、Writer Lambdaは1インスタンスで200メッセージをまとめて取得し、RDSへバルクインサートしました。これにより接続数を絞り、I/O効率を高められました。
記録先がAPIの場合は、APIが耐えられる並列数に合わせてWriter Lambdaの並列度を制御し、1メッセージずつ取得してAPI Callしました。ここは“システム側の都合で並列を上げない”割り切りが必要でした。
要するに、並列化のメリットは計算で最大化し、DB/APIアクセスは薄く・少なく・まとめて寄せるのが現実解でした。
8. まとめ
イベント駆動アーキテクチャは、イベントという「確定事実」を中心に非同期・疎結合で連携することで、拡張性とスケーラビリティを得るアーキテクチャです。
計算・集計系のように中央制御が不要なワークロードではコレオグラフィ型が最もシンプルに機能し、重複は冪等性ガードとDLQで吸収し、順序要件が強い領域には踏み込まない判断が重要になります。
一方、業務フローが複雑で運用の可視化や補償が価値の中心になる領域では、メディエイター型(Camunda等のワークフローエンジン)に寄せてフロー制御を集中管理し、例外・リトライ・補償・可視化を自作しない方が長期TCOは確実に下がります。運用設計は仕様そのものなので、現場との握り込みが成功の前提です。
そして実戦最大の壁はDBです。EDAで並列処理を強めるほどDBアクセスがボトルネックになるため、読み込みは「回数を減らす」方向(/tmpキャッシュが最強)へ、書き込みは「出口で直列化・バルク化する」方向へ寄せるのが現実的な落としどころでした。
EDAは万能ではありませんが、 **“向く領域で、向く形で、向かない領域を見極めて使う”**と、現場で爆発的に効くアーキテクチャです。