RabbitMQ VS Apache Kafka (七)—— RabbitMQ消息路由原语与路由保证

释放双眼,带上耳机,听听看~!

路由保证

RabbitMQ和Kafka都提供给了持续的消息路由保证,两者都提供了最多一次和最少一次的路由保证,此外,Kafka还提供了有限应用场景下的仅一次可靠性路由保证。首先,我们看下这三种路由保证的具体含义:

  • 最多一次:一个消息最多被路由不超过一次,消息可能丢失。

  • 最少一次:消息永远不会丢失,但同一个消息可能会被路由到一个消费者多次。

  • 仅一次:消息路由的理想模式,一个消息只被路由一次。

说消息路由可能也不是特别准确,消息处理反倒能真实反映实际的业务场景,毕竟,我们的关注点是消息能否被消费者正常处理,不论消息是通过最多一次、最少一次、或者仅一次的方式进行路由。但使用消息处理来描述又可能将问题复杂化了,比如,在有些业务场景中(消息在处理过程中,消息者出现服务宕机,那么我们就需要将消息重新路由给新的消费者),我们的确需要将消息路由两次以确保消息可以被正确处理,那么,此时如果我们使用仅一次消息处理就不是特别准确了。

第二点,消息处理可能会带来部分失败的业务痛点。通常情况,消息处理并非原子操作,一个完整的消息处理场景可能包含多个步骤,除与应用本身和消息平台的交互之外,中间还夹杂着应用本身的业务逻辑,中间的业务逻辑部分处理可能会出现失败的情况,那么这种失败就需要应用自身来进行处理。当然,我们也可以通过事务来控制整个消息处理流程,要么全成功,要么全失败,但事实上这是不现实的,毕竟消息处理的步骤中可能涉及多个系统,当我们需要在消息路由,应用,缓存和数据库之间进行通信时,我们能保证仅一次处理么?很显然,答案是否。

所以,仅一次仅适用于消息处理的输出仍是消息平台本身的业务场景,并由消息平台进行事务控制。在这样的一个有限的应用场景中,我们可以通过事务确保消息的处理要么全部成功,要么全部失败,这正是Kafka Stream所提供的。如果消息处理的输出是幂等的,在基于仅一次路由保证的前提,我们可以无需使用事务控制。

端到端通信

RabbitMQ和Kafka均未提供端到端的通信。对于RabbitMQ来说,一个消息可以路由到多个队列中,所以,端到端的通信并没有实际意义。同样的对于Kafka来说,多个消费组可以从同一个主题中读取消息,端到端的通信同样也没有实际含义。

责任链

本质上来讲,生产者并不知道消息是否已被处理,他们只知道消息已被消息系统接收并安全保存,并由消息系统负责路由指定消费者。这里,生产者、消息系统和消费者之间就存在一个责任链的关系,从生产者开始到消费者结束。在责任传递的过程中,责任链中的当前节点必须负责将责任正确的传递到下一节点,不能出错。这也意味着作为消息者一方,当开发人员收到消息之后需注意代码质量与规范,避免出现消息丢失或者误用的情况发生。

消息顺序

关于消息有序性,之前章节已有讨论,简单来讲,RabbitMQ和Kafka均有FIFO的顺序保证,只不过RabbitMQ提供的是队列层架的有序性,而Kafka提供的则是主题分区层级的有序性。

RabbitMQ 路由保证

RabbitMQ的路由保证主要通过以下方式实现:

  • 消息持久化:通过消息存储,确保消息不丢失

  • 消息确认:实现RabbitMQ与生产者、消费者之间的通信

持久化原语

队列镜像

队列可以实现跨节点(服务器)间的镜像(复制)功能,每一个队列都有一个主队列的概念,其他服务器节点上的队列作为镜像。假设有三个服务器节点,如果主队列所在的节点宕掉,那么剩余的某个镜像队列则会提升为主队列,如果镜像队列所在的服务节点宕掉,那么会创建新的镜像队列来代替丢失的镜像,并维持与主队列的复制关系。

持久化队列

RabbitMQ包含两种类型的消息队列:持久化队列和非持久化队列,持久化队列会将队列持久化到服务器磁盘上,当节点重启时,队列状态会被保留。当节点启动时,持久化队列会被重新声明。

持久化消息

即便一个队列是持久化的,也不意味着当节点重启时队列中的消息仍然保留,除非生产者明确声明消费者是持久化的。
注意,在RabbitMQ中,持久化消息越多性能越低。假设,你面对的是一个实时事件流业务场景,实际应用中可以容忍丢失若干或者短时间内的流信息,那么你就可以考虑不实现队列镜像,并且将所有消息设置为非持久化。当然,如果你无法容忍当节点宕机之后信息丢失,那么就需要考虑使用持久化队列和镜像,并且设置消息的持久化属性。

消息确认

生产者确认

消息在产生的过程中可能出现丢失或者重复的情况,这实际依赖于生产者的行为。

  • 简单发送:生产者不使用生产者确认,只进行简单的消息发送并且不执行任何后续动作,这种情况下,消息不会重复,但很容易出现丢失情况。

  • 生产者确认:当生产者打开代理通道时,设置需要生产者确认,那么代理将会针对每个消息产生相应消息:

basic.ack:肯定确认,消息收到,消息责任节点归属RabbitMQ,消息开始由RabbitMQ负责。

basic.nack:否定确认,消息未收到,消息未被处理,消息责任点仍旧归属生产者,生产者可以执行重发操作。

除了肯定确认与否定确认,还有一种确认类型:*basic.return:返回确认。有时候,生产者可能不仅需要知道消息已被RabbitMQ接收,并且还需要知道消息被路由到了哪几个队列中。比如说,当生产者发布消息到一个主题交换器中,但却没有满足消息路由键值的绑定关系到订阅队列,在这种情况下,RabbitMQ消息代理可能就会丢弃消息,在绝大部分场景中,这么处理都是OK的,但在一些其他应用场景中,生产者需要知道消息是否被丢弃,是否需要采取一定的应对措施。生产者可以设置强制返回标识,当消息被丢弃时,生产者就会收到一个返回确认通知:basic.return

生产者也可以选择每发送一个消息就等待确认信息,当然这会很大程度上影响性能。因此,生产者可以通过信息流的方式持续发送消息,当达到一定的阈值之后,暂停消息发送直至所有的确认信息返回。为了提高RabbitMQ批量确认性能,我们还可以使用multiple标识,每一个发送的消息都会携带一个序列号,当确认消息返回时,会同时包含所收到消息的序列号,生产者就可以根据确认消息的序列号确认哪些消息已被正确收到。

因此,通过消息确认我们可以避免消息丢失的场景发生:

  • 重发(当收到否定确认)

  • 持久化消息到其他地方(当收到否定确认或者返回确认)

事务:RabbitMQ中一般很少使用事务,主要原因如下:

  • 不清晰的路由保证,如果消息被路由到多个队列,或者使用mandatory标识,那么事务的原子性就无法保证。

  • 性能较差

  • 连接或通道异常:除了要考虑消息确认,生产者还需要考虑连接失败或者RabbitMQ消息代理失败的情况,两种情况都会导致消息通道的丢失。消息通道的丢失会导致无法接收到任意消息确认。在这一点上,我们需要做一个风险决策,要么选择信息丢失,要么选择消息重复。

如果是代理失败,有可能此时消息还在OS缓存中,或者正在被解析,那么这种情况就会导致信息丢失。或者消息已经被持久化到队列中,只是发送消息确认之前,代理宕掉,那么这种情况下,消息是被成功路由的。同样的场景也适用于连接失败,比如消息传输过程中连接失败,或者消息已被持久化到队列中但确认信息尚未收到连接中断。

对于连接中断的异常状况,生产者是无法知道的,所以我们的可以采取如下措施:

  • 不重发,结果可能会带来消息丢失

  • 重发,消息可能重复

消费者确认

针对消息确认,消费者也有两种模式

  • 无确认模式

  • 主动确认模式

无确认模式:又称自动确认模式,这种模式非常危险,首先,在这种模式下,只要消息路由到应用中去,消息就会从队列中立即删除,这样做的结果是消息丢失,原因在于:

  • 消息送达之前连接中断

  • 应用宕掉,但消息仍旧在内部缓存中

  • 消息处理失败

第二,我们失去了反向控制消息路由速率的方式。在主动确认模式下,我们可以通过预取阈值(QoS)的方式来限制未确认消息的数量。在没有这个特性限制下,RabbitMQ发送消息的速率可能远大于消费者可以处理的速度,这就可能会导致缓存溢出或者内存异常等问题。

主动确认模式:在这种模式下,消费者必须主动确认每一个收到的消息。消费者可以选择每收到一个消息就发送一个确认信息或者使用multiple标识一次确认多个消息,批量确认消息可以显著提高效率。确认信息会包含一个自增的Delivery Tag标识,消费确认信息主要包含:

  • basic.ack: 肯定确认,RabbitMQ将从队列中删除该消息

  • basic.nack:否定确认,消费者必须设置一个标识告诉RabbitMQ是否需要将消息重新入列。重新入列的含义是表示消息被重新放到队首,然后被重新路由给消费者。

  • basic.reject: 与basic.nack类似,区别在于不支持multiple标识。

在语义上来讲,basic.ack与requeue标识为false的basic.nack是一样的,两种消息确认都会执行将消息都队列中删除。

接下来的问题是,什么时候发送确认消息?如果消息处理很快,那么你也许会想在消息处理完毕时(无论成功与失败)立即发送。但是,如果使用RabbitMQ作为工作队列,发送确认消息可能耗时较长,那么这就可能是一个大问题,如果通道关闭,那么未被确认的消息可能会被重新入列,从而导致消息被重复路由。

连接或代理失败

如果连接中断或者代理失败都会导致通道中断,进而导致所有未确认消息重新入列和路由,好处是避免了消息丢失,坏处是消息重复。

消费者拥有未确认消息的时间越长,消息被重新路由的风险越大。当消息被重新路由之后,RabbitMQ会将重新路由标识设置为真,所以当消费者重新读到这条消息时,可以判断出消息已经被处理了。

幂等性

幂等性和消息无损路由意味着你需要构建一些消息重复检测机制或者其他幂等模型,如果消息重复性检测的代价较大,你可以使用一种生产者策略,在重新发布消息时,生产者可以定义一个自定义消息头,当消费者通过解析自定义消息头和重新路由标识来确认消息是否重复。

结论

RabbitMQ提供了非常强大的可靠的,持久化的消息路由保证,但使用不好可能会适得其反。以下几点需格外注意:

  • 如果你需要强至少一次路由保证,你可以使用队列镜像,持久化队列,持久化消息,生产者确认,强制标识和消费者众多那个确认方式
  • 如果你可以接收至少一次的路由模式(模式可能导致消息重复),你可能需要增加删重或者幂等机制。
  • 如果你不关系消息丢失,相反的,你更关注低延迟和高伸缩性,那么很简单,不使用队列镜像、不使用持久化消息以及生产者确认。当然,我还是建议你使用消费者主动确认,因为你可以通过预取阈值来控制消息路由速率,在这种情况你可以使用multiple标识来批量确认。

给TA打赏
共{{data.count}}人
人已打赏
安全运维

MongoDB数据建模小案例:多列数据结构

2021-12-11 11:36:11

安全运维

Ubuntu上NFS的安装配置

2021-12-19 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索