微服务架构中的最佳实践队列/主题设计
我们正在考虑为我们的微服务基础设施(编排)引入基于AMQP的方法。 我们有几种服务,比如说客户服务,用户服务,文章服务等。我们计划引入RabbitMQ作为我们的中央消息系统。
我正在寻找有关主题/队列等系统设计的最佳实践。一种选择是为我们的系统中可能发生的每一个事件创建一个消息队列,例如:
user-service.user.deleted
user-service.user.updated
user-service.user.created
...
我认为这不是创建数百个消息队列的正确方法,不是吗?
我想使用Spring和这些漂亮的注释,例如:
@RabbitListener(queues="user-service.user.deleted")
public void handleEvent(UserDeletedEvent event){...
将“user-service-notifications”这样的东西当作一个队列,然后将所有通知发送到该队列是不是更好? 我仍然想将注册听众只注册到所有事件的一个子集,那么如何解决?
我的第二个问题:如果我想听一个之前没有创建的队列,我会在RabbitMQ中遇到异常。 我知道我可以用AmqpAdmin“申报”一个队列,但是我应该在每一个微服务中为我的数百个队列执行此操作,因为它总是会发生该队列尚未创建到目前为止?
我通常发现最好是按对象类型/交换类型组合进行交换。
在你的用户事件的例子中,你可以根据你的系统需要做许多不同的事情。
在一种情况下,按照您列出的每个事件进行交换可能是有意义的。 您可以创建以下交易所
| exchange | type | |-----------------------| | user.deleted | fanout | | user.created | fanout | | user.updated | fanout |
这将适合广播事件的“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 |
在这种情况下,您最终得到一个交换,并且使用路由键将消息分发到相应的队列。 注意我还在这里包含了一个“create command”路由键和队列。 这说明了如何通过组合模式。
我仍然想将注册听众只注册到所有事件的一个子集,那么如何解决?
通过使用扇出交换,您可以为要收听的特定事件创建队列和绑定。 每个消费者将创建它自己的队列和绑定。
通过使用主题交换,您可以设置路由键以将特定消息发送到所需的队列,包括具有绑定的所有事件,如user.events.#
。
如果你需要特定的消息去特定的消费者,你可以通过路由和绑定来完成。
最终,在不知道每个系统需求的具体情况的情况下,没有正确或错误的答案来决定使用哪种交换类型和配置。 你可以使用任何交换类型几乎任何目的。 每个人都有权衡,因此每个应用程序都需要仔细检查以了解哪一个是正确的。
至于宣布你的队列。 每个消息使用者在尝试附加它之前都应该声明队列并进行绑定。 这可以在应用程序实例启动时完成,或者您可以等到需要队列。 再次,这取决于你的应用程序需要什么。
我知道我所提供的答案相当含糊而充满选择,而非真正的答案。 尽管如此,还没有确切的答案。 这都是模糊逻辑,具体情况并考虑系统需求。
FWIW,我写了一本小小的电子书,以讲述故事的一个非常独特的角度来讲述这些话题。 它解决了你有的许多问题,尽管有时是间接的。
Derick的建议很好,除了他如何命名他的队列。 队列不应仅仅模仿路由密钥的名称。 路由密钥是消息的元素,队列不应该在意这一点。 这是绑定的目的。
队列名称应该根据消费者附加到队列中的内容来命名。 这个队列的操作意图是什么。 假设你想在创建帐户时发送一封电子邮件给用户(当使用上面的Derick的答案发送带有路由键user.event.created的消息时)。 你可以创建一个队列名称sendNewUserEmail(或者按照你认为合适的风格)。 这意味着很容易检查和确切知道该队列的功能。
为什么这很重要? 那么,现在你有另一个路由键,user.cmd.create。 假设其他用户为其他人(例如团队成员)创建帐户时发送此事件。 您仍然希望向该用户发送电子邮件,以便创建绑定以将这些消息发送到sendNewUserEmail队列。
如果队列是在绑定后进行命名的,则会导致混淆,尤其是在路由键发生更改时。 保持队列名称解耦和自描述。
在回答“一次交换,还是多次?”之前 题。 我其实还想问另外一个问题:我们是否真的需要为这种情况进行定制交换?
不同类型的对象事件如此自然以匹配待发布的不同类型的消息,但有时并不是必须的。 如果我们将所有3种类型的事件抽象为一个“写”事件,其子类型是“创建”,“更新”和“删除”呢?
| object | event | sub-type |
|-----------------------------|
| user | write | created |
| user | write | updated |
| user | write | deleted |
解决方案1
支持这个最简单的解决方案是我们只能设计一个“user.write”队列,并通过全局默认交换直接发布所有用户写入事件消息到这个队列。 当直接发布到队列中时,最大的限制是它假定只有一个应用程序订阅了这种类型的消息。 一个应用订阅这个队列的多个实例也很好。
| queue | app |
|-------------------|
| user.write | app1 |
解决方案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 |
如果每个用户都关心并想要处理所有“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 |
回到“一个交换,还是很多?”的问题。 到目前为止,所有解决方案只使用一个交换机。 工作正常,没有错。 那么,我们什么时候需要多次交流? 如果“主题”交换的绑定太多,则性能会有所下降。 如果过多的“话题交换”绑定的性能差异真的成为问题,那么当然可以使用更多的“直接”交换来减少“话题”交换绑定的数量以获得更好的性能。 但是,在这里我想更多地关注“一次交换”解决方案的功能限制。
解决方案5
我们可能会考虑多个交换的一种情况是针对不同的组或事件的维度。 例如,除了上面提到的创建,更新和删除的事件,如果我们有另一组事件:登录和注销 - 描述“用户行为”而不是“数据写入”的一组事件。 因为不同的事件组可能需要完全不同的路由策略以及路由关键字和队列命名约定,所以自然而然地有一个单独的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.* |
其他解决方案
还有其他一些情况,我们可能需要针对一种对象类型进行多次交换。 例如,如果您希望在交易所上设置不同的权限(例如,只允许一个对象类型的选定事件从外部应用程序发布到一个交易所,而另一个交易所接受来自内部应用程序的任何事件)。 另一个例子是,如果你想使用带有版本号后缀的不同交换来支持同一组事件的不同版本的路由策略。 对于另一个实例,您可能想要为交换到交换绑定定义一些“内部交换”,这可以以分层的方式管理路由规则。
总而言之,“最终的解决方案取决于您的系统需求”,但是通过上面的所有解决方案示例以及背景考虑,我希望至少可以在正确的方向上进行一次思考。
我还创建了一篇博文,综合了这个问题背景,解决方案和其他相关考虑。
链接地址: http://www.djcxy.com/p/5843.html上一篇: Best Practice Queue/Topic Design in a MicroService Architecture