分布式系统与消息的投递

截取转载自https://draveness.me/message-delivery

消息投递语义

网络请求由于超时的问题,消息的发送者只能通过重试的方式对消息进行重发,但是这就可能会导致消息的重复发送与处理,然而如果超时后不重新发送消息也可能导致消息的丢失,所以如何在不可靠的通信方式中,保证消息不重不漏是非常关键的。
我们一般都会认为,消息的投递语义有三种,分别是最多一次(At-Most Once)、最少一次(At-Least Once)以及正好一次(Exactly Once),我们分别会介绍这三种消息投递语义究竟是如何工作的。

  • 最多一次
    最多一次其实非常容易保证的,UDP 这种传输层的协议其实保证的就是最多一次消息投递,消息的发送者只会尝试发送该消息一次,并不会关心该消息是否得到了远程节点的响应。
    无论该请求是否发送给了接受者,发送者都不会重新发送这条消息;这其实就是最最基本的消息投递语义,然而消息可能由于网络或者节点的故障出现丢失。

  • 最少一次
    为了解决最多一次时的消息丢失问题,消息的发送者需要在网络出现超时重新发送相同的消息,也就是引入超时重试的机制,在发送者发出消息会监听消息的响应,如果超过了一定时间也没有得到响应就会重新发送该消息,直到得到确定的响应结果。
    对于最少一次的投递语义,我们不仅需要引入超时重试机制,还需要关心每一次请求的响应,只有这样才能确保消息不会丢失,但是却可能会造成消息的重复,这就是最少一次在解决消息丢失后引入的新问题。


    image.png
  • 正好一次
    虽然最少一次解决了最多一次的消息丢失问题,但是由于重试却带来了另一个问题 消息重复,也就是接受者可能会多次收到同一条消息;从理论上来说,在分布式系统中想要解决消息重复的问题是不可能的,很多消息服务提供了正好一次的 QoS 其实是在接收端进行了去重。


    image.png

    消息去重需要生产者生产消息时加入去重的 key,消费者可以通过唯一的 key 来判断当前消息是否是重复消息,从消息发送者的角度来看,实现正好一次的投递是不可能的,但是从整体来看,我们可以通过唯一 key 或者重入幂等的方式对消息进行『去重』。

消息的重复是不可能避免的,除非我们允许消息的丢失,然而相比于丢失消息,重复发送消息其实是一种更能让人接受的处理方式,因为一旦消息丢失就无法找回,但是消息重复却可以通过其他方法来避免副作用。

投递顺序

由于一些网络的问题,消息在投递时可能会出现顺序不一致性的情况,在网络条件非常不稳定时,我们就可能会遇到接收方处理消息的顺序和生产者投递的不一致;想要满足绝对的顺序投递,其实在生产者和消费者的单线程运行时是相对比较好解决的,但是在市面上比较主流的消息队列中,都不会对消息的顺序进行保证,在这种大前提下,消费者就需要对顺序不一致的消息进行处理,常见的两种方式就是使用序列号或者状态机。

  • 序列号
    在投递消息时加入序列号其实与 TCP 中的序列号非常类似,我们需要在数据之外增加消息的序列号,对于消费者就可以根据每一条消息附带的序列号选择如何处理顺序不一致的消息,对于不同的业务来说,常见的处理方式就是用阻塞的方式保证序列号的递增或者忽略部分『过期』的消息。

  • 状态机
    使用序列号确实能够保证消息状态的一致,但是却需要在消息投递时额外增加字段,这样消费者才能在投递出现问题时进行处理,除了这种方式之外,我们也可以通过状态机的方式保证数据的一致性,每一个资源都有相应的状态迁移事件,这些事件其实就是一个个消息(或操作),它们能够修改资源的状态:


    image.png

    在状态机中我们可以规定,状态的迁移方向,所有资源的状态只能按照我们规定好的线路进行改变,在这时只要对生产者投递的消息状态做一定的约束,例如:资源一旦 completed 就不会变成 failed,因为这两个状态都是业务逻辑中定义的最终状态,所以处于最终状态的资源都不会继续接受其他的消息。

假设我们有如下的两条消息 active 和 complete,它们分别会改变当前资源的状态,如果一个处于 pending 状态的资源先收到了 active 再收到 complete,那么状态就会从 pending 迁移到 active 再到 completed;但是如果资源先收到 complete 后收到 active,那么当前资源的状态会直接从 pending 跳跃到 completed,对于另一条消息就会直接忽略;从总体来看,虽然消息投递的顺序是乱序的,但是资源最终还是通过状态机达到了我们想要的正确状态,不会出现不一致的问题。

协议

  • AMQP 协议
    AMQP 协议的全称是 Advanced Message Queuing Protocol,它是一个用于面向消息中间件的开放标准,协议中定义了队列、路由、可用性以及安全性等方面的内容。
amqp-protoco

该协议目前能够为通用的消息队列架构提供一系列的标准,将发布订阅、队列、事务以及流数据等功能抽象成了用于解决消息投递以及相关问题的标准,StormMQ、RabbitMQ 都是 AMQP 协议的一个实现。

在所有实现 AMQP 协议的消息中间中,RabbitMQ 其实是最出名的一个实现,在分布式系统中,它经常用于存储和转发消息,当生产者短时间内创建了大量的消息,就会通过消息中间件对消息转储,消费者会按照当前的资源对消息进行消费。

producer-and-consume

RabbitMQ 在消息投递的过程中保证存储在 RabbitMQ 中的全部消息不会丢失、推送者和订阅者需要通过信号的方式确认消息的投递,它支持最多一次和最少一次的投递语义,当我们选择最少一次时,需要幂等或者重入机制保证消息重复不会出现问题。

  • MQTT 协议
    另一个用于处理发布订阅功能的常见协议就是 MQTT 了,它建立在 TCP/IP 协议之上,能够在硬件性能底下或者网络状态糟糕的情况下完成发布与订阅的功能;与 AMQP 不同,MQTT 协议支持三种不同的服务质量级别(QoS),也就是投递语义,最多一次、最少一次和正好一次。

从理论上来看,在分布式系统中实现正好一次的投递语义是不可能的,这里实现的正好一次其实是协议层做了重试和去重机制,消费者在处理 MQTT 消息时就不需要关系消息是否重复这种问题了。

你可能感兴趣的:(分布式系统与消息的投递)