RabbitMQ/AMQP - MicroService架构中的最佳实践队列/主题设计

Fri*_*itz 52 esb amqp rabbitmq spring-amqp

我们正在考虑为我们的微服务基础设施(编排)引入基于AMQP的方法.我们提供多种服务,比如客户服务,用户服务,文章服务等.我们计划将RabbitMQ作为我们的中央消息系统.

我正在寻找有关主题/队列等系统设计的最佳实践.一种选择是为我们系统中可能发生的每个事件创建一个消息队列,例如:

user-service.user.deleted
user-service.user.updated
user-service.user.created
...
Run Code Online (Sandbox Code Playgroud)

我认为创建数百个消息队列不是正确的方法,不是吗?

我想使用Spring和这些不错的注释,例如:

  @RabbitListener(queues="user-service.user.deleted")
  public void handleEvent(UserDeletedEvent event){...
Run Code Online (Sandbox Code Playgroud)

将"用户服务通知"作为一个队列,然后将所有通知发送到该队列是不是更好?我仍然想将听众只注册到所有事件的子集,那么如何解决呢?

我的第二个问题:如果我想要侦听之前未创建的队列,我将在RabbitMQ中获得异常.我知道我可以使用AmqpAdmin"声明"一个队列,但是我是否应该为每个微服务中的数百个队列执行此操作,因为到目前为止总是会发生队列创建?

Der*_*ley 36

我通常发现最好按对象类型/交换类型组合进行交换.

在您的用户事件示例中,您可以根据系统需要执行许多不同的操作.

在一个场景中,如您所列,每个事件进行交换可能是有意义的.你可以创建以下交换

| exchange     | type   |
|-----------------------|
| user.deleted | fanout |
| user.created | fanout |
| user.updated | fanout |

这将适合任何听众的广播事件的" pub/sub "模式,而不关心什么是听力.

使用此设置,绑定到任何这些交换的任何队列都将接收发布到交换的所有消息.这对于pub/sub和其他一些场景非常有用,但它可能不是你想要的,因为如果没有创建新的交换,队列和绑定,你将无法过滤特定消费者的消息.

在另一种情况下,您可能会发现由于事件太多而创建的交换太多.您可能还希望将用户事件和用户命令的交换组合在一起.这可以通过直接或主题交换来完成:

| exchange     | type   |
|-----------------------|
| user         | topic  |

使用这样的设置,您可以使用路由键将特定消息发布到特定队列.例如,您可以将其user.event.created作为路由密钥发布,并使其具有针对特定使用者的特定队列的路由.

| exchange     | type   | routing key        | queue              |
|-----------------------------------------------------------------|
| user         | topic  | user.event.created | user-created-queue |
| user         | topic  | user.event.updated | user-updated-queue |
| user         | topic  | user.event.deleted | user-deleted-queue |
| user         | topic  | user.cmd.create    | user-create-queue  |

在这种情况下,最终会使用单个交换和路由密钥将消息分发到适当的队列.请注意,我还在此处包含了"创建命令"路由键和队列.这说明了如何组合模式.

我仍然想将听众只注册到所有事件的子集,那么如何解决呢?

通过使用扇出交换,您可以为要侦听的特定事件创建队列和绑定.每个消费者都会创建自己的队列和绑定.

通过使用主题交换,您可以设置路由键以将特定消息发送到所需的队列,包括具有绑定的所有事件user.events.#.

如果您需要特定的消息转发给特定的消费者,您可以通过路由和绑定来完成此操作.

最终,在不了解每个系统需求的具体情况下,使用哪种交换类型和配置没有正确或错误的答案.您可以将任何交换类型用于任何目的.每个应用都有权衡,这就是为什么需要仔细检查每个应用程序以了解哪一个是正确的.

至于宣布你的队列.每个消息使用者都应该在尝试附加到它之前声明它需要的队列和绑定.这可以在应用程序实例启动时完成,也可以等到需要队列.再次,这取决于您的应用程序需要什么.

我知道我提供的答案是相当模糊和充满选择,而不是真正的答案.但是,没有具体的答案.它是所有模糊逻辑,特定场景和查看系统需求.

FWIW,我写了一本小电子书,从讲述故事的独特视角来介绍这些主题.它解决了你的许多问题,尽管有时是间接的.


小智 26

德里克的建议很好,除了他如何命名他的队列.队列不应仅仅模仿路由密钥的名称.路由键是消息的元素,队列不应该关心它.这就是绑定的用途.

队列名称应以连接到队列的使用者所做的命名.这个队列的操作意图是什么.假设您想在创建帐户时向用户发送电子邮件(当使用Derick的答案发送带有路由键user.event.created的消息时).您可以使用您认为合适的样式创建队列名称sendNewUserEmail(或沿着这些行的某些内容).这意味着很容易查看并确切知道该队列的作用.

为什么这很重要?好吧,现在你有了另一个路由密钥user.cmd.create.假设当另一个用户为其他人(例如,团队成员)创建帐户时发送此事件.您仍然希望向该用户发送电子邮件,因此您创建绑定以将这些消息发送到sendNewUserEmail队列.

如果队列是在绑定后命名的,则可能会导致混淆,尤其是在路由键发生更改的情况下.保持队列名称分离和自我描述.

  • 好点!回顾上面我的回答,我喜欢你将队列名称作为要执行的操作或者对此队列中的消息应该发生什么的意图. (14认同)

小智 15

在回答"一次交换,还是很多?"之前 题.我其实想问另一个问题:我们真的需要为这种情况进行自定义交换吗?

不同类型的对象事件非常自然地匹配要发布的不同类型的消息,但有时并不是必需的.如果我们将所有3种类型的事件抽象为"写"事件,其子类型是"创建","更新"和"删除",该怎么办?

| object | event   | sub-type |
|-----------------------------|
| user   | write   | created  |
| user   | write   | updated  |
| user   | write   | deleted  |
Run Code Online (Sandbox Code Playgroud)

解决方案1

支持这一点的最简单的解决方案是我们只能设计一个"user.write"队列,并通过全局默认交换将所有用户写入事件消息直接发布到此队列.直接发布到队列时,最大的限制是假设只有一个应用程序订阅此类消息.订阅此队列的一个应用程序的多个实例也没问题.

| queue      | app  |
|-------------------|
| user.write | app1 |
Run Code Online (Sandbox Code Playgroud)

解决方案2

当有第二个应用程序(具有不同的处理逻辑)想要订阅发布到队列的任何消息时,最简单的解决方案无法工作.当有多个应用订阅时,我们至少需要一个"扇出"类型的交换,绑定到多个队列.因此,消息被发布到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 |
Run Code Online (Sandbox Code Playgroud)

如果每个订户关心并且想要处理"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 |
Run Code Online (Sandbox Code Playgroud)

解决方案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 |
Run Code Online (Sandbox Code Playgroud)

回到"一个交换,还是很多?"的问题.到目前为止,所有解决方案仅使用一次交换.工作正常,没有错.那么,什么时候我们需要多次交流?如果"主题"交换具有太多绑定,则会略微降低性能.如果"主题交换"上过多绑定的性能差异确实成为问题,当然您可以使用更多"直接"交换来减少"主题"交换绑定的数量以获得更好的性能.但是,在这里,我想更多地关注"一个交换"解决方案的功能限制.

解决方案5

我们可能自然会考虑多个交换的一个案例是针对不同的群体或事件维度.例如,除了上面记得的创建,更新和删除事件之外,如果我们有另一组事件:登录和注销 - 一组描述"用户行为"而不是"数据写入"的事件.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.*          |
Run Code Online (Sandbox Code Playgroud)

其他方案

在其他情况下,我们可能需要针对一种对象类型进行多次交换.例如,如果要在交换机上设置不同的权限(例如,只允许将一种对象类型的选定事件从外部应用程序发布到一个交换机,而另一个交换机接受来自内部应用程序的任何事件).对于另一个实例,如果要使用后缀为版本号的不同交换,则支持同一组事件的不同版本的路由策略.对于另一个实例,您可能希望为交换到交换绑定定义一些"内部交换",这可以以分层方式管理路由规则.

总而言之,"最终的解决方案取决于您的系统需求",但是上面的所有解决方案示例以及背景考虑因素,我希望它至少可以在正确的方向上进行思考.

我还创建了一篇博客文章,将这个问题背景,解决方案和其他相关注意事项放在一起.