Open2
CQRS/ES
Fire and Forget vs Request-Response
- メッセージの処理が冪等か冪等でないかを見極め、冪等である場合は Fire and Forget(+ at least oneceの配信保障)を採用する
- メッセージが冪等ではない場合、コンテキストによってメッセージを拒否する必要があるため、request-response型の通信の方がシステムとして一貫した動作を実現しやすい。
- 例えば、ECサイトで在庫以上の注文を行えないという制約がある場合、在庫サービスがメッセージを拒否した場合、注文サービスは注文を拒否する必要がある。オンラインのサービスだと、一旦注文を受け付けたうえで後で処理結果を伝えるというのはUXが悪い。
- メッセージの処理が冪等ではない場合は、メッセージの重複を取り除く仕組みも必要。
参考資料
-
Approach to Event-Driven Architecture
-
When should I use events, and when should I prefer synchronous API requests? (非同期のメッセージと同期メッセージをどう使い分けるか?)
- イベントソースが受信側の処理の実行結果に依存してイベントソースの機能を果たす必要がある場合は、同期的APIの利用は適切。既存のビジネスロジックを再利用する場合によくこのケースが発生する。
-
This guidance has been very focused on one-way event notifications. Can I implement bi-directional messaging in an event-driven architecture? (このガイドラインは単一方向のイベント通知にフォーカスしているが、イベント駆動アーキテクチャにおいて双方向メッセージングを実装することは可能か?)
- イベント駆動アーキテクチャはイベント通知メッセージにフォーカスしたアーキテクチャで、このアーキテクチャで語られるメッセージは基本的に単方向でfire-and-forgetする。コマンドメッセージとドキュメントメッセージは呼び出し側に何らかの結果を返す必要がある。
- 双方向メッセージングは同期的な方法と非同期的な方法があるが、どちらも基本的にはイベント駆動メッセージングとは異なる。イベント駆動メッセージングは、応答を返さないというのが重要な特徴である。
- 双方向メッセージングはメッセージの生産者と購読者に結合を生む。生産者側が購読者の応答を期待し、障害の処理を行わなければならない。
- しかし、これは双方向メッセージングがよくない、ということを言っているわけではなく、実際にはアーキテクチャに回復性とスケーラビリティをもたらすこともある。単純に双方向メッセージングはイベント駆動メッセージングとは違うよ、というだけの話だ。
-
When should I use events, and when should I prefer synchronous API requests? (非同期のメッセージと同期メッセージをどう使い分けるか?)
-
Asynchronous commands are dangerous
- コマンドは本質的に同期的な操作であり、非同期的な操作であるかのように考えることは危険
- コマンドは拒否される可能性があり、ほとんど拒否されないことを前提にした設計や、非同期に受信することにしたメッセージをすぐに受信できるという前提は誤り
- 非同期に処理し、トランザクションIDをすぐに返すような設計の場合、すくなくともコマンドバスに問題なくコマンドが配信されたことを保証するべき
- 全てのコマンドが非同期に処理できることを前提とすることは、システムの設計全てを怠慢にし、トランザクションの境界を気にせずにサービスを分割したりして、分散モノリスの苦しみを味わうことになる
-
The Tao of Microservices / Chapter3 Messages
-
request-response
- サービスAとサービスBが分かれている場合に、サービスAがサービスBの処理結果に依存したトランザクションを持っている場合に用いる
- ナイーブに実装すると、通信プロトコルやポート番号をクライアントプログラムが直接知っているような実装になるが、マイクロサービスの原則としてはこれを隠ぺいすること
-
fire-and-forget
- マイクロサービスのメッセージパターンの中で最も純粋なパターンで、他のメッセージパターンはこれに制約を課すもの。
- メッセージの受信側が実行するタスクは冪等でなければならない
- 冪等なタスクはインスタンスを増やしてスケールアウトすることが出来る
-
request-response
Idempotent Consumer パターン
冪等ではないメッセージは重複実行されないように、メッセージの重複を取り除く必要がある
重複を取り除く方法
- RDBMSを用いる場合
メッセージのIDを管理するテーブルを使って、テーブルの一意キー制約を使って、同一IDのメッセージ処理をロックする。
Slickで書くとこんな感じ。
db.run {
(for {
_ <- (TableQuery[ID] += message.id) // 一意キー制約のあるテーブルにトランザクションを張ってinsert
res <- DBIO.from(processSomething(message))
} yield res).transactionally
}
-
NoSQLを用いる場合
集約を保存するドキュメントに処理を行ったメッセージのIDを保存し、重複したメッセージIDを処理しないように制御する -
Actor Modelを用いる場合
NoSQLの場合とほぼ同じだが、全てのIDをメモリに持つ
重複を取り除く方法はそれぞれスケーラビリティや性能、IDを保持するために使用する技術といったものが異なり、一長一短あるので、プロジェクトの特性に合わせて選択する。