消息投递过程中需要注意哪些问题?

191 阅读6分钟

消息投递过程中需要注意哪些问题?

可靠传输

可靠传输即意味着消息不丢。这需要做到两点——持久化消息、投递确认机制。

接下来分析整个投递链路可能会发生消息丢失的时机:

sequenceDiagram

autonumber

producer ->> producer: producer可能会宕机,故需要对消息进行刷盘落地

producer->>broker: 消息可能会因为网络延迟、丢包等问题,导致消息丢失,若超时未收到ack则重新投递
broker ->>broker:同理broker也需要对消息进行刷盘落地
broker ->> producer:消息落地之后,再通过ack机制来告知producer消息投递成功

broker->>consumer: 消息可能会因为网络延迟、丢包等问题,导致消息丢失,若超时未收到ack则重新投递
consumer ->>consumer:消息处理
consumer ->> broker:无论消息是否处理成功,最后通过ack机制来告知broker消息投递成功

  1. producer可能会宕机,故需要对消息进行刷盘落地
  2. producer将消息投递至broker时,消息可能会因为网络延迟、丢包等问题,导致消息丢失,若超时未收到ack则重新投递。

    此处实际上会涉及到消息投递时的一致性语义——最多一次、最少一次以及精确一次。以上策略本质上是对消息重复以及消息可靠的权衡。

  3. 同理broker也需要对消息进行刷盘落地,防止因宕机而导致消息丢失。

    此处根据不同的消息队列,或者应用场景也可以选择内存存储,进而提升吞吐能力

  4. 消息落地之后,再通过ack机制来告知producer消息投递成功

    进一步地,如果为了高可用而部署了主从,则还需要保证消息成功落地到指定个数的从节点才能向producer返回ack。

  5. 消息可能会因为网络延迟、丢包等问题,导致消息丢失,若超时未收到ack则重新投递。
  6. 消息处理
  7. 无论消息是否处理成功,最后通过ack机制来告知broker消息投递成功

    对处理异常的消息,并不属于MQ的问题,故不应该返回一个nack,而是是通过记录日志,之后人工介入处理。

消息幂等/重复

幂等指的是在消息重复的情况下,消息消费多次但不多次影响业务数据

首先来看看消息投递时的一致性语义——最多一次、最少一次以及精确一次。

  • 最多一次:即在进行消息投递时,不进行重试(换句话说,不存在ack机制)。
    • 对于发送端来说,可能会因网络问题而导致消息丢失。
    • 对于接收端来说,可能根本没接收到发送端的消息,也可能未能及时将消息持久化而在宕机时丢失消息。
  • 最少一次:即在进行消息投递时,根据ack机制进行重试。
    • 对于发送端来说,当出现网络丢包时,那么重新发送消息是没有问题的。但是如果出现网络延迟,那么重新发送消息就可能导致重复消息投递至接收端。
    • 对于接收端来说,如果在ack的过程中,出现网络丢包或者网络延迟,都会导致发送端重新发送消息,进而出现重复消息。如果没有幂等处理,那么接收端会持有重复的消息
  • 精确一次:即在进行消息投递时,进行akc机制进行重试,但除此之外保证接收端只会消费一次消息。
    • 正常流程和最少一次方案没有区别,但是额外地需要让接收端对消息进行幂等处理

一般来说,由于最多一次的可靠性太低,不会采用。多数的队列都对producer到broker最少一次消费提供了支持。kafka则是对producer到broker精确一次提供了支持。

对于broker到consumer来说,想要做到消息幂等/精确一次,一般都需要手动进行去重。一般的做法就是根据消息ID进行判断。

顺序消费

顺序消费需要保证顺序生产、顺序存储、顺序消费。

对于生产者来说:

  • 对于多实例来说,由于并行发送消息,故消息顺序是不可控的。
  • 对于单实例、多生产线程来说,同样会出现消息乱序的问题。但是一般来说,生产线程一般都属于单线程,生产者并不像消费者一样,需要通过多线程来提升吞吐量。
    • 还需要注意的是,尽管是单线程,但还需要满足同步发送才能保证消息的有序性。否则异步发送还是可能出现乱序问题

对于broker来说,则需要保证接收到的消息都是按ack的顺序保存到队列当中。

  • 局部有序:单主题多队列,其中每条队列都保证消息有序性
    • 若想实现局部有序性,则需要在producer发送消息时,手动指定同一业务类型的消息发送到统一的队列上。
  • 全局有序:单主题单队列,该单一队列保证消息有序性

在云原生时代下,动态缩扩容是一种常见的能力,而对队列进行扩容也是一种常见的需求。当producer是通过哈希、取模将消息发送到指定队列,且此时触发动态缩扩容的话,那么同一类型的消息就不一定会投递到同一队列


对于消费者来说:

  • 对于多实例来说,即消费者组消费队列,由于并行消费消息,故消息的顺序消费是不可控的。
  • 对于单实例、单消费线程来说:是简单有效保证顺序消费的方案,但是吞吐量不足
  • 对于单实例、多消费线程来说:需要额外实现与消费线程一一对应的内存队列,使得从broker接收而来的消息,转交到内存队列中,消费线程再从内存队列中取出消息进行消费。

对于需要保证消息有序性的业务消息来说,一般都有一个状态字段。业务需要根据这个有限状态机来进行处理。队列存储这种消息的方式有两种:

  • 队列可承载各种状态的消息,此时由生产者维护消息状态流转的正确性,消费者只需要顺序从broker队列取出消费即可保证顺序消费。
  • 队列仅可承载一种状态的消息,而对于这类消息,由于每种队列都对应有一种消费者,故可以看作是并行消费不同状态的消息。那么此时就需要额外的机制来保证顺序消费,从而符合状态流转关系。一种方式就是在我们的消费者服务中进行状态判断处理,另一种方式则是直接通过数据库进行状态的乐观更新处理(需要进行的判断条件主要是两个——状态和递增的版本号)。

消息堆积

消息堆积进而带来的问题将会导致消息过期丢失、磁盘空间不足、海量消息等待消费等问题。

排除机器故障问题,解决方案主要有两种:

  1. 如果对消息顺序不作要求,那么可以直接对consumer进行扩容。
  2. 如果对消息顺序有要求,即consumer数量不可变。那么我们首先将先有的consumer暂停,然后新建队列,再额外通过一个程序将原队列的消息迁移到新队列,随后再重启原先的consumer慢慢对所有队列的消息进行消费,消费完毕再释放新建的队列和程序资源。