分布式系统场景下的消息队列常见问题

通常分布式消息队列的常见问题有:重复消费、消息丟失、消息堆积等

1. 重复消费问题

大多数的消息队列都不能保证消息的"Exactly Only Once",且并不是说恰好一次就是好的,原因如下:

  • 发送消息阶段:分布式环境下的超时问题,同步的调用超时,并不能保证消息没发送或发送成功;此时就无法保证消息只会发送一次。要处理这类问题,就需要使用分布式事务处理,如Kafka的事务消息,但代价是吞吐量的大幅降低;
  • 消息订阅阶段:消费者节点的变动、扩缩容、下线、网络抖动等会触发消息服务的Rebalance,在这个过程中消息会有可能产生重复;
  • 消费阶段:消费者的宕机,在消费完成后,未能及时的commit确认消费。这种情况在,业务上往往会更希望消息重新消费;

排查解决思路:

  1. 生产端:判断生产端是否重复发送了,可以基于队列的offset判断:
    1. 如果相同的消息offset相同,则认为是同一条消息,则生产端正常;
    2. 如果相同的消息offset不同,则看消息的内容是否都相同,如果相同则是生产端产生了重复消息;
  2. 消费者的Rebalance导致:服务节点的重启、长FGC等等
    1. 消费者的节点数发生变化(集群发布、节点重启、断连、FGC等)都可能触发Rebalance;
    2. Rebalance过程中,可能存在少量的重复消费情况;
  3. 消费者处理的问题:
    1. 消费者在处理完消息后,在commit消息之前,宕机了,就可能触发消息的重复消费;

消息的重复消费问题,不能依赖生产者和消息队列,消息队列要想做到恰好一次的代价很高;"At Least Once"往往是更为通用、更灵活的方式,并且业务上的可用性也是更高的:

  • 消息队列保证消息的最少一次的投递和消费;
  • 消费者自行保证业务的幂等;
  • 消费者做好消息id的并发消费控制,通常每个消息都有一个唯一id,可以以此来拒绝并发处理;

2. 消息丟失

要保证消息可靠传输,要从消息链路每个地方保证,可能存在消息丟失的场景:

  1. 生产者发送消息到消息队列时丢失;
  2. 消息队列未能持久化消息,宕机丢失;
  3. 消息消费时,未能消费成功,MQ清除消息或者宕机丢失;

三个方面去考虑:

  1. 保证生产端可靠投递(同步投递
    • 采用同步方式发送消息,确保收到MQ的成功响应,保证投递成功;
  2. 保证MQ端的消息副本同步、持久化;(TP模式
    • MQ在收到消息后,保证消息同步到各个MQ副本上,再响应生产端;副本同步成功已经可以保证消息不丢失了;
    • 如果要完全保证,还需要持久化消息,但是会拉低性能;
  3. 消费端保证消费完成,从MQ中Commit指定消息(同步Commit
    • 当消费端,消费完成,应当响应MQ,去进行对应消息的消费Commit;

3. 消息堆积

没办法,要消费,就只能提高消费端的消费能力;

  1. 增加消费者线程、进程;
  2. 优化业务消费逻辑;

4. 消费限流

目前主流的消息队列都是PULL的方式拉取消息;那么如果要实现消费的限流,就需要控制PULL的频率批次大小

但这种限流方式,只能作用于单机限流,无法做到整个集群的分布式限流;

如果期望实现集群限流,有集成Sentinel的解决方案;

5. 消费顺序

如果要做到顺序消费,首先消息进入队列时,就需要保证顺序,而绝大多数分布式消息队列在同一个队列中的消息,能够保证顺序,通常也是利用这一特性来实现顺序消费:

  1. 单线程单队列消费:将所有消息都由同一个线程来消费,这样就可以保证消息的顺序性。但是这样会影响系统的吞吐量和并发性能。
  2. 按特定业务逻辑顺序消费:有些场景不需要全局顺序,只需要保证某个企业、某个用户的消费顺序,那就可以按照对应的key,再次分区,保证相同的key路由到相同的分区或消费者。这样保证顺序,提高并发度;
  3. 结合业务特点,保证处理消息的顺序性;比如先入库,再顺序消费数据库消息;这样的好处是:
    1. 可靠性高:消息持久化后可靠性高;
    2. 灵活性高:持久化后的消息,再次消费时可以自由的定制逻辑;甚至批处理;
    3. 延时增大:写入再读取势必增加延时;