TT Lab
はじめる
学ぶ 学習パス コース

マイクロサービスアーキテクチャ

命令ではなく事実を送る

TT Labで続きを見る

一言でいうと

同期呼び出しは「これをやって」で、イベントは「こういうことがあった」です。後者は、相手が今生きていなくてもかまいません。

なぜ必要なのか

注文が作られたら、在庫を減らし、ポイントを計上し、通知を送り、分析イベントを溜める必要があるとしましょう。これを同期呼び出しでつなぐと、注文APIの応答時間は4つのサービスの応答時間の合計になり、可用性は4つのサービスの可用性の積になります。それぞれ99.9%なら、合わせて99.6%です。そして、通知サービスが落ちると、注文を受け付けられません。通知のせいで売上が止まる設計です。

イベントに変えると、注文サービスは「注文が作成された」という事実を1つだけ発行して終わりです。残りの4つは、それぞれの速度でそれを消費します。通知サービスが10分間落ちていても、注文は受け付け続けられ、復旧したら溜まったイベントを処理します。

どう動くのか

ここで重要なのは名前です。SendNotificationはコマンドで、OrderCreatedは事実です。コマンドをキューに送ると、それは単なる非同期RPCです。送信者が受信者を知っていて、受信者が増えると送信者のコードが変わります。事実を発行すれば、送信者は誰が聞いているかを知りません。新しいコンシューマーが加わっても、発行者はそのままです。これが、結合を実際に減らす地点です。

イベントのペイロードには、コンシューマーが自分の仕事を終えるのに必要な情報を、十分に入れます。コンシューマーが発行者のDBを逆に問い合わせると、結合が復活します。ただし、入れすぎるとイベントがスキーマになり、スキーマはそのまま契約なので、変えにくくなります。実務での妥協は、識別子と一緒に、変わらないはずの中核のフィールドだけを入れることです。

代償も明確です。結果整合性です。注文は作成されたのに、在庫はまだ減っていない可能性がある時間の窓が生じます。その窓が200msなのか30秒なのか、その間にユーザーが何を見るのかを、プロダクトの観点で決めなければなりません。「まもなく反映されます」という画面の文言が、実はアーキテクチャの決定なのです。

イベント名が設計を決める

同じ内容を入れても、名前によってシステムが変わります。表で見ると明確です。

名前 種類 送信者が知っていること コンシューマーが増えると
SendNotification コマンド 誰が処理するかを知っている 送信者のコードが変わる
ReserveStock コマンド 誰が処理するかを知っている 送信者のコードが変わる
OrderCreated 事実 誰も知らない 送信者はそのまま
PaymentApproved 事実 誰も知らない 送信者はそのまま

過去形の動詞を使えば、自然に事実になります。名前の規則1つが、アーキテクチャを守ります。

コマンドが必要な場面もあります。「今これをやれ」が、ちょうど1つのコンシューマーに 届かなければならないときです。そのときは、キュー(ポイントツーポイント)を使い、事実はトピック(パブリッシュ・サブスクライブ)で 送ります。両方を同じチャネルに混ぜると、すぐに区別が失われます。

発行が失敗する場面

最も危険な瞬間は、DBはコミットされたのに、イベントの発行が失敗することです。 注文は作られたのに、誰も知りません。逆に、イベントを先に送ってDBが失敗すると、 存在しない注文のイベントが出回ります。

❌ 위험한 순서
   BEGIN; INSERT order; COMMIT;
   publish(OrderCreated)      ← 여기서 죽으면 이벤트가 영영 안 나간다

✅ 아웃박스 패턴
   BEGIN;
     INSERT order;
     INSERT outbox(topic, payload);   ← 같은 트랜잭션 안
   COMMIT;
   (별도 프로세스가 outbox 를 읽어 발행하고 표시한다)

1つのトランザクションの中で両方がコミットされるので、「注文はあるのにイベントがない」状態が、原理的に 不可能になります。その代わり、発行が少し遅くなり、同じイベントが2回出ていく 可能性があります(発行の後、マークの前に落ちた場合)。そのため、コンシューマーは常に冪等でなければなりません。

スキーマは契約

イベントを発行した瞬間、それは公開APIになります。誰が聞いているかわからないので、むやみに 変更できません。2つの規則で、たいてい持ちこたえられます。

コンシューマー側にも規則があります。知らないフィールドは無視します。厳格なデシリアライズ (additionalProperties: false)を使うと、発行者がフィールドを1つ追加するたびに、コンシューマーが すべて壊れます。

現場での姿

非同期に変えると、デバッグが難しくなります。同期呼び出しはスタックトレース1つで済みますが、イベントは発行の時点と消費の時点が離れています。そのため、イベントに相関IDを必ず載せる必要があります。これなしでイベントアーキテクチャを運用するのは、目を閉じて運転するのと同じです。

もう1つ。イベントは、順序が保証されないことがあります。同じ注文のOrderCreatedとOrderPaidが、逆転して届くと、コンシューマーが壊れます。パーティションキーを集約のID(例: 注文ID)にすれば、同じ集約の中では順序が保証されます。グローバルな順序は、あきらめるのが普通です。

次のクイズで確認すること

このモジュールは概念だけを扱います。イベントを実際に安全に発行する問題は、すぐ次のモジュールのアウトボックスのラボで、手を動かして扱います。