简单地来说,这个模式能够使多个并行的consumer处理同一个信道中收到的消息,从而使系统能够并发地处理大量消息来优化系统的吞吐量,提高系统的可扩展性和可用性,平衡负载。
Context and Problem
一般来说,一个运行在云端的应用通常都需要处理大量的请求。相比于同步的方式来处理每一个请求来说,一个更加通用的技术是实现一个消息系统来实现异步地处理各个请求。另外,这种策略也能够使得业务逻辑不会在某个请求正在被处理时阻塞。
在系统运行过程中,有很多种原因会导致系统所处理的请求数量发生巨大变化。突然出现的用户活动或者是对多个系统使用者的消息汇聚都会引起一些意料之外(大)的负载。系统也许会在高峰时间段处理巨量的消息但在其他时间段只处理较少的消息,并且处理不同消息的逻辑可能差异非常大。由于这几个原因,如果在系统中只使用一个消息consumer(就是用来处理消息的应用,下同),那么这个consumer在高峰期就很有可能被海量的消息淹没,或者整个消息传递系统负载过高直接陷入瘫痪。为了解决这个问题,可以在系统中启动多个consumer实例,但这种情况下又必须协调好这些consumer实例,要确保每条消息只被其中之一拿到并处理;另外,也要做好负载均衡,避免其中某个consumer成为整个系统的瓶颈。
Solution
解决上面说的问题的方法就是用消息队列来作为用户程序(即消息的生产者)和消息消费者之间的通信媒介。用户程序将调用请求封装成一个消息放到消息队列中,consumer从消息队列中取出消息并进行处理。这个方法就能使同一个consumer池(包含了一堆consumer实例)中的consumer能够处理来自任意用户程序的请求。
这种解决方案有以下这些好处:
1.这种设计使那些负载水平固定的系统能够处理不同压力的用户请求。消息队列相当与在用户程序与consumer之间做了一个buffer,有助于减少压力波动时对用户程序和consumer稳定性、响应能力产生的影响。
2.这种设计能有助于提高系统可靠性。假设用户程序直接与consumer相连而不是使用这种模式(引入消息队列),并且不对消费者做任何监管,那么当消费者出现问题时请求就很有可能丢失或失败。然而在这种模式下用户程序并不是把消息发送给某个具体的consumer,而是一堆consumer在消息队列端竞争消息,另外,当消息失败时也不会造成用户程序的阻塞。
3.采用这种模式后就不需要在多个consumer之间或是用户程序与consumer之间做太多复杂的协调工作,消息队列本身可以保证消息至少被传递一次。
4.这种设计具有很好的易扩展性。consumer的数量可以根据请求压力的不同随意增减。
5.如果所使用的消息队列支持事务读(每个消息会且仅会被取到一次),那么这种设计就能提高系统的弹性。如果一个consumer读取并处理一个消息是作为一个事务性操作的一个步骤,当它抢到一个消息之后的执行过程中出现步骤,消息队列可以保证这条消息还会留存在队列中并被其他的consumer再次获取重复。
Issues and Considerations
在考虑如何实现这个模式时需要注意一下几点:
1.消息顺序:在多个consumer抢消息的情景下,consumer获得消息的顺序是无法保证的,并且也没有必要与消息创建的顺序相关联。在系统设计中要把消息处理过程设计成幂等的,这样就能消除消息处理顺序对处理结果的影响。
2.系统弹性:如果系统需要监管consumer服务并在其失败时进行重启,那么将操作设计成具有幂等性就更有必要了,这能将消息重复处理情况出现时对系统的影响降到最小。
3.检测“有毒的”消息:一个畸形的消息或者访问那些无法正常使用资源的任务极有可能会造成consumer的崩溃。对此,系统需要避免这种消息重新回流到消息系统中,并且捕获这个消息/任务的异常信息以便日后分析修复。
4.结果处理:consumer处理消息的逻辑与生成消息的业务逻辑是完全解耦的,并且不是以一种直接的方式进行通信的。如果consumer的处理结果必须发回给源消息对应的生产者,那么必须用一个两边都能访问到的容器来存储结果,并且系统要提供一个标志用于表明数据处理完成防止有一方访问不完整的数据。
5.消息系统的扩展性:在一个大规模数据处理情景下,单个消息队列很容易被大量的消息淹没从而成为整个系统性能的瓶颈。此时,可以考虑对整个消息系统(分布式的)进行分区,消息的producer只在自己所在的分区中发送消息;或者说可以将多个消息队列组合在一起,在它们之上加一层负载均衡。
6.确保消息系统的可靠性:一个可靠的消息系统需要保证当一个应用将一条消息插入到消息系统后,这条消息就绝不会丢失(哪怕处理失败),这是“保证每条消息至少被投递一次”这一原则的前提条件。