マイクロサービスインフラストラクチャ(振り付け)にAMQPベースのアプローチを導入することを考えています。カスタマーサービス、ユーザーサービス、記事サービスなど、いくつかのサービスがあります。RabbitMQを中央メッセージングシステムとして導入する予定です。
トピック/キューなどに関するシステム設計のベストプラクティスを探しています。1つのオプションは、システムで発生する可能性のあるすべてのイベントごとにメッセージキューを作成することです。
user-service.user.deleted
user-service.user.updated
user-service.user.created
...
数百のメッセージキューを作成するのは適切な方法ではないと思います。
SpringとこれらのNiceアノテーションを使用したいので、例えば:
@RabbitListener(queues="user-service.user.deleted")
public void handleEvent(UserDeletedEvent event){...
「user-service-notifications」のようなものをone queueにして、すべての通知をそのキューに送信する方が良いと思いませんか?すべてのイベントのサブセットのみにリスナーを登録したいのですが、どうすれば解決できますか?
2番目の質問:以前に作成されていないキューでリッスンする場合、RabbitMQで例外が発生します。 AmqpAdminでキューを「宣言」できることはわかっていますが、キューがこれまで作成されなかったことが常に発生する可能性があるため、すべてのマイクロサービスで数百のキューごとにこれを行う必要がありますか?
私は一般的に、オブジェクトタイプ/交換タイプの組み合わせでグループ化された交換を持っていることが最善であると思います。
ユーザーイベントの例では、システムに必要なものに応じて、さまざまなことを行うことができます。
あるシナリオでは、リストしたようにイベントごとに交換を行うのが理にかなっているかもしれません。次の交換を作成できます
|交換|タイプ| | --------------------- | user.deleted |ファンアウト| | user.created |ファンアウト| | user.updated |ファンアウト|
これは、「---(pub/sub 」イベントをリスナーにブロードキャストするパターンに適合し、何を聞いているかを気にしません。
この設定では、これらの交換のいずれかにバインドしたキューは、交換に発行されたすべてのメッセージを受信します。これはpub/subやその他のシナリオには適していますが、新しい交換、キュー、バインドを作成せずに特定のコンシューマーのメッセージをフィルター処理できないため、常に望んでいるものではない場合があります。
別のシナリオでは、イベントが多すぎるため、作成されている交換が多すぎることがわかります。ユーザーイベントとユーザーコマンドの交換を組み合わせることもできます。これは、直接またはトピックの交換で行うことができます。
|交換|タイプ| | --------------------- |ユーザー|トピック|
このような設定では、ルーティングキーを使用して特定のメッセージを特定のキューに公開できます。たとえば、user.event.created
をルーティングキーとして公開し、特定のコンシューマーの特定のキューにルーティングさせることができます。
|交換|タイプ|ルーティングキー|キュー| | ------------------------------------------ ----------------------- | |ユーザー|トピック| user.event.created |ユーザー作成キュー| |ユーザー|トピック| user.event.updated |ユーザー更新キュー| |ユーザー|トピック| user.event.deleted |ユーザー削除キュー| |ユーザー|トピック| user.cmd.create |ユーザー作成キュー|
このシナリオでは、単一の交換で終わることになり、ルーティングキーを使用してメッセージを適切なキューに配信します。ここに「作成コマンド」ルーティングキーとキューも含まれていることに注意してください。これは、パターンを組み合わせる方法を示しています。
すべてのイベントのサブセットのみにリスナーを登録したいのですが、どうすれば解決できますか?
ファンアウト交換を使用すると、リッスンする特定のイベントのキューとバインディングを作成できます。各コンシューマーは、独自のキューとバインディングを作成します。
トピック交換を使用すると、ルーティングキーを設定して、user.events.#
などのバインディングを持つallイベントを含む特定のメッセージを必要なキューに送信できます。
特定のメッセージを特定のコンシューマに送信する必要がある場合は、---(ルーティングとバインディングを使用してこれを行います 。
最終的に、各システムのニーズの詳細を知らなくても、使用する交換タイプと構成について正しい答えも間違った答えもありません。ほぼすべての目的に任意の交換タイプを使用できます。それぞれにトレードオフがあります。そのため、どのアプリケーションが正しいかを理解するために、各アプリケーションを綿密に調べる必要があります。
キューの宣言に関して。各メッセージコンシューマは、必要なキューとバインディングを宣言してからアタッチを試行する必要があります。これは、アプリケーションインスタンスの起動時に行うことも、キューが必要になるまで待つこともできます。繰り返しますが、これはアプリケーションに必要なものに依存します。
私が提供している答えは、本当の答えではなく、曖昧で選択肢が多いことを知っています。ただし、具体的な確かな答えはありません。それはすべてファジーロジック、特定のシナリオ、およびシステムニーズの調査です。
FWIW、私は これらのトピックをカバーする小さな電子書籍 物語を伝えるというかなりユニークな観点から書きました。間接的なこともありますが、あなたが持っている多くの質問に対処します。
Derickのアドバイスは、キューに名前を付ける方法を除き、問題ありません。キューは、単にルーティングキーの名前を模倣するべきではありません。ルーティングキーはメッセージの要素であり、キューはそれを気にするべきではありません。それがバインディングの目的です。
キュー名は、キューにアタッチされたコンシューマが実行する名前にちなんで命名する必要があります。このキューの操作の意図は何ですか。アカウントの作成時にユーザーにメールを送信するとします(ルーティングキーがuser.event.createdのメッセージが上記のDerickの回答を使用して送信される場合)。キュー名sendNewUserEmail(または適切なスタイルでこれらの行に沿ったもの)を作成します。これは、そのキューが何をするのかを簡単に確認して知ることができることを意味します。
何でこれが大切ですか?さて、これで別のルーティングキーuser.cmd.createができました。このイベントは、別のユーザーが他のユーザー(チームのメンバーなど)のアカウントを作成したときに送信されるとします。そのユーザーにもメールを送信したいので、バインディングを作成してそれらのメッセージをsendNewUserEmailキューに送信します。
キューの名前がバインディングに基づいて付けられている場合、特にルーティングキーが変更された場合、混乱を引き起こす可能性があります。キュー名を切り離し、わかりやすいものにしてください。
「1つの交換、または多くの?」に答える前に質問。私は実際に別の質問をしたいと思います。この場合、カスタム交換さえ本当に必要ですか?
さまざまなタイプのオブジェクトイベントは、発行されるさまざまなタイプのメッセージと一致するように非常に自然ですが、実際には必ずしも必要ではありません。サブタイプが「作成」、「更新」、「削除」の3つのタイプのイベントすべてを「書き込み」イベントとして抽象化するとどうなりますか?
| object | event | sub-type |
|-----------------------------|
| user | write | created |
| user | write | updated |
| user | write | deleted |
ソリューション1
これをサポートする最も簡単なソリューションは、「user.write」キューのみを設計し、すべてのユーザー書き込みイベントメッセージをグローバルデフォルト交換を介してこのキューに直接発行できることです。キューに直接公開する場合の最大の制限は、このタイプのメッセージをサブスクライブするアプリは1つだけであると想定していることです。このキューにサブスクライブしている1つのアプリの複数のインスタンスも問題ありません。
| queue | app |
|-------------------|
| user.write | app1 |
ソリューション2
キューにパブリッシュされたメッセージをサブスクライブする2番目のアプリ(異なる処理ロジックを持っている)がある場合、最も単純なソリューションは機能しません。複数のアプリがサブスクライブしている場合、少なくとも1つの「ファンアウト」タイプの交換と複数のキューへのバインディングが必要です。そのため、メッセージはexcahngeにパブリッシュされ、交換はメッセージを各キューに複製します。各キューは、それぞれ異なるアプリの処理ジョブを表します。
| queue | subscriber |
|-------------------------------|
| user.write.app1 | app1 |
| user.write.app2 | app2 |
| exchange | type | binding_queue |
|---------------------------------------|
| user.write | fanout | user.write.app1 |
| user.write | fanout | user.write.app2 |
この2番目のソリューションは、各サブスクライバーが「user.write」イベントのすべてのサブタイプを処理したい場合、または少なくともこれらすべてのサブタイプイベントを各サブスクライバーに公開する場合は問題ありません。たとえば、サブスクライバーアプリが単にトランザクションログを保持するためのものである場合、または、サブスクライバーはuser.createdのみを処理しますが、user.updatedまたはuser.deletedがいつ発生するかを通知しても構いません。一部のサブスクライバーが組織の外部から来ている場合、エレガントではなくなり、特定のサブタイプイベントについてのみ通知したい場合があります。たとえば、app2がuser.createdのみを処理し、user.updatedまたはuser.deletedの知識がまったくない場合。
ソリューション3
上記の問題を解決するには、「user.write」から「user.created」の概念を抽出する必要があります。 「トピック」タイプの交換が役立ちます。メッセージを公開するとき、user.created/user.updated/user.deletedをルーティングキーとして使用して、「user.write.app1」キューのバインディングキーを「user。*」に、バインディングキーを「user.created.app2」キューは「user.created」です。
| queue | subscriber |
|---------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| exchange | type | binding_queue | binding_key |
|-------------------------------------------------------|
| user.write | topic | user.write.app1 | user.* |
| user.write | topic | user.created.app2 | user.created |
ソリューション4
「トピック」交換タイプは、イベントサブタイプがさらに増える可能性がある場合に備えて、より柔軟です。ただし、イベントの正確な数が明確にわかっている場合は、代わりに「直接」交換タイプを使用してパフォーマンスを向上させることもできます。
| queue | subscriber |
|---------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| exchange | type | binding_queue | binding_key |
|--------------------------------------------------------|
| user.write | direct | user.write.app1 | user.created |
| user.write | direct | user.write.app1 | user.updated |
| user.write | direct | user.write.app1 | user.deleted |
| user.write | direct | user.created.app2 | user.created |
「1回の交換か、それとも多数ですか?」という質問に戻ります。これまでのところ、すべてのソリューションは1つの交換のみを使用しています。問題なく動作します。それでは、いつ複数の交換が必要になるのでしょうか? 「トピック」交換のバインディングが多すぎる場合、パフォーマンスがわずかに低下します。 「トピック交換」のバインディングが多すぎる場合のパフォーマンスの違いが実際に問題になる場合は、「ダイレクト」交換を増やして「トピック」交換バインディングの数を減らしてパフォーマンスを向上させることができます。しかし、ここでは、「ワンエクスチェンジ」ソリューションの機能制限にもっと焦点を当てたいと思います。
ソリューション5
複数の交換が本来考えられるケースの1つは、イベントの異なるグループまたはディメンションに対するものです。たとえば、上記の作成、更新、および削除されたイベントに加えて、別のイベントグループがある場合:ログインとログアウト-「データ書き込み」ではなく「ユーザーの行動」を説明するイベントのグループ。 Cozの異なるグループのイベントには、完全に異なるルーティング戦略とルーティングキーとキューの命名規則が必要な場合があります。そのため、個別のuser.behavior交換が必要になります。
| queue | subscriber |
|----------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| user.behavior.app3 | app3 |
| exchange | type | binding_queue | binding_key |
|--------------------------------------------------------------|
| user.write | topic | user.write.app1 | user.* |
| user.write | topic | user.created.app2 | user.created |
| user.behavior | topic | user.behavior.app3 | user.* |
その他のソリューション
1つのオブジェクトタイプに対して複数の交換が必要になる場合があります。たとえば、エクスチェンジに異なる権限を設定する場合(たとえば、1つのオブジェクトタイプの選択されたイベントのみが外部アプリから1つのエクスチェンジに公開され、他のエクスチェンジは内部アプリからのイベントを受け入れます)。別の例として、同じイベントグループのルーティング戦略の異なるバージョンをサポートするために、バージョン番号をサフィックスとして付けた異なる交換を使用する場合。別の別のインスタンスでは、交換ルールへのバインディングのためのいくつかの「内部交換」を定義することができます。これは、階層化された方法でルーティングルールを管理できます。
要約すると、やはり「最終的な解決策はシステムのニーズに依存します」が、上記のすべての解決策の例と背景の考慮事項を踏まえて、少なくとも正しい方向に1つの思考を得ることができればと思います。
ブログ投稿 も作成し、この問題の背景、解決策、およびその他の関連する考慮事項をまとめました。