作为后端开发人员,有一天你需要回答这个问题:
我需要构建一个使用分布式队列的异步应用程序,我应该使用哪个代理? 作为工程师,我们的本能是列出我们了解或希望熟悉的工具(如果它是一种新的和已知的技术),然后开始使用它。不幸的是,在那个时刻,我们错过了第一个最重要的问题,这个问题需要在所有其他问题之前得到答案:我们的现有和有时未来的用例/需求是什么,什么工具能最好地解决它们? 这是我们在设计一个重要功能时的起点,当时工程师的本能占了上风。我们的第一个问题不是最重要的问题,从那时开始,我们选择正确工具的过程变得不太有效。我们的团队开了几次会,讨论了我们对分布式队列的需求,不同技术的不同限制和特性(来自不同的范例)让关注点远离了我们最重要的需求,也远离了决策和共识的实现。 在那个时候,我们决定回到基本问题,问:我们试图解决的用例是什么,没有让步的余地在哪些领域?一如既往,让我们从需求开始。
在选择消息代理或事件代理时,有很多事情要考虑:高可用性、容错性、多租户、多云区域支持、能够支持高吞吐量和低延迟等等,列举不胜枚举。
大多数情况下,当阅读有关事件代理或消息代理的主要特性时,我们都是以大多数公司或产品从未完全使用或需要的最复杂用例为例的。
作为工程师,生活中有一句常用的话语:
上帝在细节中,但魔鬼隐藏在细节之间。
在选择事件代理与消息代理两个范式之间,"魔鬼"隐藏在更多的低层技术考虑因素中,比如:消息的消耗或生成确认方法、去重、消息的优先级、消费者线程模型、消息的消耗方法、消息的分发/扩散支持、毒药药处理等等。
(1) 事件代理
存储一系列事件。通常,事件会按到达事件代理的顺序附加到日志(队列或主题)上。主题或队列中的事件是不可变的,其顺序不能更改。
当事件发布到队列或主题时,代理识别主题或队列的订阅者,并使事件可供多种类型的订阅者使用。
生产者和消费者不需要彼此熟悉。
事件潜在地可以存储数天或数周,因为它们一旦被成功消耗,就不会从队列/主题中删除。
(2) 消息代理
用于服务或组件之间的通信。它通过异步方式在应用程序之间传输由生产者接收的消息。
它通常支持队列的概念,其中消息通常存储一段时间。队列中消息的目的是在消费者可用于处理消息并在成功消耗后删除消息。
不能保证队列中消息处理的顺序,并且可以更改。
通常,在处理短命令或面向任务的处理时,我们会倾向于使用消息代理。
例如,假设你在一家电子商务公司工作,想要将新产品添加到公司的网站。这可能意味着多个服务需要知道并以异步方式处理此请求。
上图显示了RabbitMQ扇出消息分发的使用,其中每个服务都有自己的队列连接到扇出交换机。
产品服务发送包含新产品信息的消息到交换机,交换机将消息发送到所有连接的队列。
在从队列成功消耗消息后,它将被删除,因为涉及的服务不需要保留或重新处理消息。
在处理当前或历史事件时,通常涉及大量数据,需要以单个或批量方式处理这些数据,我们会倾向于使用事件代理。
例如,假设你在一个娱乐评级网站工作,你想为用户添加一个新功能,用来显示电影的编剧和导演。这些信息虽然历史存储,但不对负责提供这些数据的服务可用。
上图显示了使用Kafka作为事件代理,它能够从数据仓库中提取数亿部电影,以为每个服务存储的电影信息附加所需的信息。
Kafka可以在相对短的时间内接受大量的数据,而消费者可以有一个独立的消费者组来单独处理电影主题流。
正如我之前提到的,选择合适的范式时有很多事情要考虑。
我想讨论一些关键的差异,这些差异通常可能成就或破坏您对技术的决策。
在这一部分,我将比较迄今为止最流行的两种技术:Kafka(事件代理)和RabbitMQ(消息代理),它们分别代表了这两个范式,我对它们都有实际经验。
我强烈鼓励您在技术选择过程中考虑以下几点。
Kafka消费者的工作方式是通过轮询一个主题中按顺序分区划分的消息的块,每个消费者负责从一个或多个分区中消费消息,其中分区用作消费者的并行机制(隐式线程模型)。
这意味着通常负责管理主题的生产者会隐式知道可以订阅主题的消费者实例的最大数量。
消费者负责处理消息处理的成功和失败情况。由于消息是从分区中批量轮询的,所以消息处理顺序在分区级别是有保证的。
RabbitMQ消费者从队列中接收消息的方式是通过代理将消息推送给它们。
每条消息都以一种独立的方式进行处理,消费者可以采用显式线程模型,而不需要生产者知道消费者实例的数量。
成功的消息处理是消费者的责任,而处理失败主要由消息代理完成。
消息分发由代理进行管理。
如延迟消息和消息优先级等功能是开箱即用的,因为消息处理顺序在队列中通常是不保证的。
Kafka处理消息处理错误的方式是将处理错误的责任委托给消费者。
如果某条消息被处理了几次但失败(毒药药),消费者应用程序需要跟踪处理尝试的数量,然后生成一条消息到一个单独的DLQ(死信队列)主题,以便以后检查/重新运行。
就错误处理而言,消费者是承担所有责任的一方。
这意味着如果您希望具有重试/DLQ功能,您需要提供重试机制,并在发送消息到DLQ主题时充当生产者,这在某些极端情况下可能导致消息丢失。
RabbitMQ处理消息处理错误的方式是跟踪处理消息失败。一旦一条消息被视为毒药药,它将被路由到一个DLQ交换机。
这允许重新排队消息或将其路由到专用DLQ以进行检查。
通过这种方式,RabbitMQ提供了保证未成功处理的消息不会丢失。
Kafka处理消费者确认的方式是由消费者提交从主题分区中轮询的消息的偏移量。
开箱即用,Kafka客户端会自动提交偏移量,无论消息是否成功处理,这可能导致消息丢失,如下图所示。
通过消费者代码负责手动提交获取的消息的偏移量,包括处理消息消费失败的情况,可以更改此行为。
RabbitMQ处理消费者确认的方式是消费者以每条消息的方式进行“确认”或“否认”,允许由消息代理处理重试策略/DLQ,如果需要,可以由消息代理进行管理。
开箱即用,RabbitMQ客户端自动进行确认,无论消息是否成功处理,可以通过消费者端的配置手动控制确认,允许消息在失败/超时时重新推送给消费者。
RabbitMQ和Kafka在大多数情况下都提供至少一次消息/事件处理的保证,这意味着消费者应该是幂等的,以处理同一消息/事件的多次处理。
对我们来说,最重要的部分是编制我们解决方案的技术标准清单,并为我们作为团队和产品不能缺少的要求分配“不可接受”。
回到基础精神,我使用了一个普通的表格来编制和比较不同的标准,并提到了一些需要注意的地方。记住,“细节藏在细节之中”。
这真的帮助我们组织并集中精力关注对我们至关重要的内容以及我们无法缺少的内容。
例如,我们的“不可接受”要求之一是,如果在处理过程中发生错误,我们不能承受丢失消息。
正如您可能从上面的部分中记得的,当使用需要DLQ的Kafka时,消费者也是DLQ的生产者。这意味着在消费者发生故障的某些情况下,消息将不会被发送到DLQ主题,可能导致消息丢失。
在这一点上,正如您可能已经猜到的那样,我们决定选择消息代理。
我们的功能包括面向命令/任务的处理用例,消息代理满足了我们的产品/数据容量要求,也满足了我们团队的需求。
消息和事件流生态系统包括许多解决方案,每个解决方案都有许多不同的方面需要考虑和熟悉。
重要的是我们要睁大眼睛进入每个生态系统,并对这些不同的范式有清晰的理解。它们将对我们工程师的日常生活(有时是夜间生活)产生重大影响。