基于MetaQ实现实时计算系统的稳定性方案

110 阅读7分钟

前言

使用消息队列一般是在解耦,异步和削峰填谷等场景,以此来实现系统的可扩展性和可用性。但是在引入消息队列的同时也引入了一些新的问题,比如消息的一致性。

如何解决消息丢失

1)生产者不丢消息

生产者发送消息的流程:

  • Broker启动时,向NameServer注册信息
  • 客户端调用producer发送消息时,会先从NameServer获取该topic的路由信息。消息头code为GET_ROUTEINFO_BY_TOPIC
  • 从NameServer返回的路由信息,包括topic包含的队列列表和broker列表
  • Producer端根据查询策略,选出其中一个队列,用于后续存储消息
  • 每条消息会生成一个唯一id,添加到消息的属性中。属性的key为UNIQ_KEY
  • 对消息做一些特殊处理,比如:超过4M会对消息进行压缩
  • producer向Broker发送rpc请求,将消息保存到broker端。消息头的code为SEND_MESSAGE或SEND_MESSAGE_V2(配置文件设置了特殊标志)

事务消息的基本流程:

image.png 如图其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

1.事务消息发送及提交:

(1) producer向broker发送 half 消息。
(2) broker响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行 Commit 或者 Rollback( Commit 操作生成消息索引,消息对消费者可见)

2.补偿流程:

(1) 对没有 Commit/Rollback 的事务消息( pending 状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback

2)broker不丢消息

broker刷盘的流程:

  • producer发送给broker的消息保存在MappedFile中,然后通过刷盘机制同步到磁盘中
  • 刷盘分为同步刷盘和异步刷盘
  • 异步刷盘后台线程按一定时间间隔执行
  • 同步刷盘也是生产者-消费者模型。broker保存消息到MappedFile后,创建GroupCommitRequest请求放入列表,并阻塞等待。后台线程从列表中获取请求并刷新磁盘,成功刷盘后通知等待线程。

broker异步刷盘策略存在数据丢失的风险,当消息尚在pagecache时,机器宕机,刷盘间隔内的消息就丢失了;但是同步刷盘,又损失了不少性能。

即便落了磁盘,就能保证数据不丢失吗?Broker通过主从模式来保证高可用,Broker支持Master和Slave同步复制、Master和Slave异步复制模式,生产者的消息都是发送给Master,但是消费既可以从Master消费,也可以从Slave消费。同步复制模式可以保证即使Master宕机,消息肯定在Slave中有备份,保证了消息不会丢失。

3)消费者不丢消息

Consumer保证消息成功消费的关键在于offset确认的时机,不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。

如何解决重复消费

1)生产者重复发送

生产者在进行发送消息的过程中,如果发送失败了,可以将消息进行重发。重发又分为同步重发和异步重发。同步重发如果持续失败,那么重发几次呢?间隔时长呢?如果由异步重发,如何保证生产者消息的顺序性?

如果有严格时序性要求,在重试失败的情况下,应执行“快速失败”策略,阻塞一切流程,由技术介入,排查问题,通过修改数据或者代码的方式,将进程恢复。

如果在时序性要求不严格的情况下,可以继续异步重发,或者将数据持久化存储,提供回查接口。

2)消费者重复消费

消费者在接收到消息后,先不确认offset,而是先处理业务逻辑,在完成后进行提交offset。消费者提交offset也不是同步提交,在间隔时间之内,如果消费者宕机,这期间的offset是不会被确认的,依然会被重新消费。针对这种情况,我们有必要将处理完成后的消息唯一标识进行持久化缓存,即使offset丢失提交,再次消费也会被过滤掉。但是此时又引入了一个新的问题:唯一标识不能用msgId,而是应该用业务id,因为我们不能完全相信生产者不会将同一条业务消息重发两次。另一种解决重复消费的方式,就是实现幂等,即同一条消息无论执行几次,其结果与执行一次相同。而实现幂等的一个常用方案就是唯一标识过滤。

如何解决消息有序性

1)消息顺序发送

metaq无法保证topic维度的全局有序,只能实现分区内有序。很多情况下,我们是所说的全局有序,其实也是某业务场景内消息的全局有序,比如下单或支付流水下消息的有序。所以可以将业务唯一key作为消息发送的路由,将相同场景内的消息路由到同一个分区内,即可以实现该场景下全局有序(本质还是单分区有序)。

这种方式引入了一个新的问题,消费方为了有序,采用单进程和单线程的方式进行消费,如果此种场景下,数据量非常大,实效性要求高,单线程处理可能会有延迟。处理的方式只能是尽可能的将分区的数据在进行细分,只要分区足够细,就可以降低数据量,提高计算效率。在强一致性的前提下,只能牺牲一定的效率。如果可以允许弱一致性,可以有其他方案进行讨论。

在保证强一致性的前提下,生产者发送消息时,必须保证消息的顺序性。在发送失败的情况下,应该阻塞流程,进行同步重试。如果重试失败,则应选择快速失败的策略进行处理。

2)消息顺序存储

3)消息顺序消费

metaq对消费者提供了两种策略:并发消费和顺序消费。

并发消费,是采用多线程处理;顺序消费,是保证同一时刻只有一个线程在消费处理,保证了有序性,牺牲了吞吐量。

最佳实践

producer最佳实践

1、每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。

2、消息发送成功或者失败,要打印消息日志,务必要打印 sendresult 和 key 字段。

3、对于消息不可丢失应用,务必要有消息重发机制。例如:消息发送失败,存储到数据库,能有定时程序尝试重发或者人工触发重发。

4、某些应用如果不关注消息是否发送成功,请直接使用sendOneWay方法发送消息。

consumer最佳实践

1、消费过程要做到幂等(即消费端去重)

2、尽量使用批量方式消费方式,可以很大程度上提高消费吞吐量。

3、优化每条消息消费过程