中间件的功能

典型的消息中间件主要包含如下几个功能:

  • 消息接收

  • 消息分发

  • 消息存储

  • 消息读取

概念模型

抽象的消息中间件模型包含如下几个角色:

  1. 发送者和接收者客户端(Sender/Receiver Client);
  2. 代理服务器(Broker Server),它们是与客户端代码直接交互的服务端代码;
  3. 消息交换机(Exchanger),接收到的消息一般需要通过消息交换机(Exchanger)分发到具体的消息队列中;
  4. 消息队列,一般是一块内存数据结构或持久化数据。

如下图所示:

image-20230709105627284

选型标准

大致有以下几点

1、性能

性能主要有两个方面需要考虑:吞吐量(Throughput)和响应时间(Latency)。 不同的消息队列中间件的吞吐量和响应时间相差甚远,对于同一种中间件,不同的配置方式也会影响性能。

配置主要有以下几种:

  • 是否需要确认机制,即写入队列后,或从队列读取后,是否需要进行确认。确认机制对响应时间的影响往往很大。
  • 能否批处理,即消息能否批量读取或者写入。批量操作可以大大减少应用程序与消息中间件的交互次数和消息传递量,大大提高吞吐量。
  • 能否进行分区(Partition)。将某一主题消息队列进行分区,同一主题消息可以有多台机器并行处理。这不仅仅能影响消息中间件的吞吐量,还决定着消息中间件是否具备良好的可伸缩性(Scalability)。
  • 是否需要进行持久化。将消息进行持久化往往会同时影响吞吐量和响应时间。

2、可靠性

可靠性主要包含:可用性、持久化、确认机制等。高可用性的消息中间件应该具备如下特征:

  • 消息中间件代理服务器(Broker)具有主从备份。即当一台代理服务宕机之后,备用服务器能接管相关的服务。
  • 消息中间件中缓存的消息是否有备份、并持久化。高可用、高一致性以及网络分裂不可兼得,大部分中间件在面临网络分裂情况下都很难保证高可用和一致性,而且可用性和一致性之间也不可兼得。

高可靠的消息中间件应该确保从发送者接收到的消息不会丢失。中间件代理服务器的宕机并不是小概率事件,所以大部分消息中间件都提供持久化,将消息写入磁盘中。但仍有两个问题需要考虑:

  • 磁盘损坏问题。长时间来看,磁盘出问题的概率仍然存在。
  • 性能问题。与操作内存相比,磁盘I/O的操作性能要慢几个数量级。频繁持久化不仅会增加响应时间,也会降低吞吐量。

解决方案:多机确认,定期持久化。即消息被缓存在多台机器的内存中,只有每台机器都确认收到消息,才跟发送者确认(很多消息中间件都会提供相应的配置选项,让用户设置最少需要多少台机器接收到消息)。

确认机制本质上是通讯的握手机制(Handshaking)。如果没有该机制,消息在传输过程中丢失将不会被发现。当然如果没有接收到消息中间件确认完成的指令,应用程序需要决定如何处理。典型的做法有两个:

  1. 多次重试。
  2. 暂存到本地磁盘或其它持久化媒介。

3、投递策略(Delivery policies)

投递策略指的是一个消息会被发送几次。主要包含三种策略:

  1. 最多一次(At most Once )
  2. 最少一次(At least Once)
  3. 仅有一次(Exactly Once)。

在实际应用中,只考虑消息中间件的投递策略并不能保证业务的投递策略,因为接收者在确认收到消息和处理完消息并持久化之间存在一个时间窗口。

例如,即使消息中间件保证仅有一次(Exactly Once),如果接收者先确认消息,在持久化之前宕机,则该消息并未被处理。从应用的角度,这就是最多一次(At most Once)。反之,接收者先处理消息并完成持久化,但在确认之前宕机,消息就要被再次发送,这就是最少一次(At least Once)。

面临的挑战

消费者是分布式队列编程中真正的数据处理方,数据处理方最常见的挑战包括:有序性、串行化(Serializability)、频次控制、完整性和一致性等。

1、有序性

如下图,假定分布式队列保证请求严格有序,请求ri2和ri1都是针对同一数据记录的不同状态,ri2的状态比ri1的状态新。T1、T2、T3和T4代表各个操作发生的时间,并且 T1 < T2 < T3 < T4(”<“代表早于)。

采用多消费者架构,这两条记录被两个消费者(Consumer1和Consumer2)处理后更新到数据库里面。Consumer1虽然先读取ri1但是却后写入数据库,这就导致,新的状态被老的状态覆盖,所以多消费者不保证数据的有序性。

image-20230709111658723

2、串行化

很多场景下,串行化是数据处理的一个基本需求,这是保证数据完整性、可恢复性、事务原子性等的基础。对于分布式队列编程架构,要在在多台消费者实现串行化非常复杂。

3、频次控制

有时候,消费者的消费频次需要被控制,可能的原因包括:

  • 费用问题。如果每次消费所引起的操作都需要收费,而同一个请求消息在队列中保存多份,不进行频次控制,就会导致无谓的浪费。
  • 性能问题。每次消费可能会引起对其他服务的调用,被调用服务希望对调用量有所控制,对同一个请求消息的多次访问就需要有所控制。

4、完整性和一致性

完整性和一致性是所有多线程和多进程的代码都面临的问题。在多线程或者多进程的系统中考虑完整性和一致性往往会大大地增加代码的复杂度和系统出错的概率。

参考

《美团博客》