消息幂等
重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这个消费过程就是消费幂等的。
在互联网应用中,尤其在网络不稳定的情况下,消息很有可能会出现重复发送或重复消费。如果重复的消息可能会影响业务处理,那么就应该对消息做幂等处理。
- 发送时重复: producer发送给broker时,Broker已经持久化完毕,出现网络分区导致broker与producer应答失败.导致producer以为发送失败,producer重试再次发送两条messageId和内容相同的消息
- 消费时重复: 消息投递到consumer之后,consumer消费完毕之后,发送ack给broker时发生网络分区,导致broker没有接受到consumer的应答.因为rocketMQ的至少一次原则,当broker网络恢复之后可能会再次投递消息
- rebalance时重复: 当consumer group数量发生变化,订阅的topic和queue数量发生变化,此时consumer也可能会收到曾经被消费过的消息
幂等通用解决方案
- 幂等令牌:是生产者和消费者两者中的既定协议,通常指具备唯一业务标识的字符串。例如,订单号、流水号。一般由Producer随着消息一同发送来的。
- 唯一性处理:服务端通过采用一定的算法策略,保证同一个业务逻辑不会被重复执行成功多次。例如,对同一笔订单的多次支付操作,只会成功一次。
解决方案
对于常见的系统,幂等性操作的通用性解决方案是:
- 首先通过缓存去重。在缓存中如果已经存在了某幂等令牌,则说明本次操作是重复性操作;若缓存没有命中,则进入下一步。
- 在唯一性处理之前,先在数据库中查询幂等令牌作为索引的数据是否存在。若存在,则说明本次操作为重复性操作;若不存在,则进入下一步。
- 在同一事务中完成三项操作:唯一性处理后,将幂等令牌写入到缓存,并将幂等令牌作为唯一索引的数据写入到DB中。
第 1 步已经判断过是否是重复性操作了,为什么第 2 步还要再次判断?能够进入第 2 步,说明已经不是重复操作了,第 2 次判断是否重复?
当然不重复。一般缓存中的数据是具有有效期的。缓存中数据的有效期一旦过期,就是发生缓存穿透,使请求直接就到达了DBMS。
消息堆积与延迟
消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度,MQ中未处理的消息会越来越多(进的多出的少),这部分消息就被称为堆积消息。消息出现堆积进而会造成消息的消费延迟。
以下场景需要重点关注消息堆积和消费延迟问题:
- 业务系统上下游能力不匹配造成的持续堆积,且无法自行恢复。
- 业务系统对消息的消费实时性要求较高,即使是短暂的堆积造成的消费延迟也无法接受。
consumer使用Pull模型消费消息主要分为几个阶段
- 消息拉取: 从broker中拉取消息缓存到本地缓冲队列.一般Broker和consumer在同一个网络环境下吞吐量会比较高,一般不会成为消息延迟的瓶颈 一个单线程单分区的低规格主机(Consumer,4C8G),其可达到几万的TPS。如果是多个分区多个线程,则可以轻松达到几十万的TPS
- 消息消费: 将本地缓冲队列的提交到消费线程中.此时真正对消息进行业务处理,处理完成之后获取一个结果.此时consumer的能力依赖于 消费耗时 和 消费并行度. 因为处理消息的耗时较长,此时consumer的消费吞吐量不高,导致缓冲队列的数据达到上限停止从broker进行Pull数据导致了broker中的消息积压
- 消息耗时: 主要是有业务逻辑决定的,一般是I/O密集型操作会导致耗时增加,比如访问数据库 HTTP请求 远程RPC调用
- 消费并发度: 消费者端的消费并发度由单节点线程数和节点数量共同决定,其值为单节点线程数*节点数量 通常需要优先调整单节点的线程数,若单机硬件资源达到了上限,则需要通过横向扩展来提高消费并发度。
单节点线程数,即单个Consumer所包含的线程数量
节点数量,即Consumer Group所包含的Consumer数量
对于普通消息、延时消息及事务消息,并发度计算都是单节点线程数*节点数量。但对于顺序消息则是不同的。顺序消息的消费并发度等于Topic的Queue分区数量。
1 )全局顺序消息:该类型消息的Topic只有一个Queue分区。其可以保证该Topic的所有消息被顺序消费。为了保证这个全局顺序性,Consumer Group中在同一时刻只能有一个Consumer的一个线程进行消费。所以其并发度为 1 。
2 )分区顺序消息:该类型消息的Topic有多个Queue分区。其仅可以保证该Topic的每个Queue分区中的消息被顺序消费,不能保证整个Topic中消息的顺序消费。为了保证这个分区顺序性,每个Queue分区中的消息在Consumer Group中的同一时刻只能有一个Consumer的一个线程进行消费。即,在同一时刻最多会出现多个Queue分蘖有多个Consumer的多个线程并行消费。所以其并发度为Topic的分区数量。
消息延迟如何避免
为了避免在业务使用时出现非预期的消息堆积和消费延迟问题,需要在前期设计阶段对整个业务逻辑进行完善的排查和梳理。其中最重要的就是梳理消息的消费耗时和设置消息消费的并发度。
梳理消息的消费耗时
通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。梳理消息的消费耗时需要关注以下信息:
- 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。
- 消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避。
- 消费逻辑中的复杂耗时的操作是否可以做异步化处理。如果可以,是否会造成逻辑错乱。
设置消费并发度
对于消息消费并发度的计算,可以通过以下两步实施:
- 逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。
- 根据上下游链路的流量峰值计算出需要设置的节点数
节点数 = 流量峰值 / 单个节点消息吞吐量
消息清理
消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。
commitlog文件存在一个过期时间,默认为 72 小时,即三天。除了用户手动清理外,在以下情况下也会被自动清理,无论文件中的消息是否被消费过:
- 文件过期,且到达清理时间点(默认为凌晨 4 点)后,自动清理过期文件
- 文件过期,且磁盘空间占用率已达过期清理警戒线(默认75%)后,无论是否达到清理时间点,都会自动清理过期文件
- 磁盘占用率达到清理警戒线(默认85%)后,开始按照设定好的规则清理文件,无论是否过期。默认会从最老的文件开始清理
- 磁盘占用率达到系统危险警戒线(默认90%)后,Broker将拒绝消息写入
需要注意以下几点:
1 )对于RocketMQ系统来说,删除一个1G大小的文件,是一个压力巨大的IO操作。在删除过程中,系统性能会骤然下降。所以,其默认清理时间点为凌晨 4 点,访问量最小的时间。也正因如果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除commitlog文件的情况。
2 )官方建议RocketMQ服务的Linux文件系统采用ext4。因为对于文件删除操作,ext4要比ext3性能更好