三 Raft在CMQ中的应用
早期我们在rabbitmq的基础上搭建了一套可扩展消息中间件CRMQ1.0,由于rabbitmq的GM同步算法在性能等方面存在瓶颈,所以自研了基于raft算法的内部版本CRMQ2.0和腾讯云CMQ,在保证强一致高可靠的前提下,性能和可用性都有显著提升。实现上采用了生产Confirm + 消费Ack机制保证消息不丢失,Confirm和Ack机制均通过raft来保证。生产的消息通过Raft转为Entry同步到大多数节点并提交,完成后各节点状态机应用该Entry,将消息内容写入磁盘,之后由Leader节点回复客户端Confirm,表示消息生产成功。消费时客户端从Leader节点拉取消息,消费完成后通过Ack命令通知服务端消息已消费可删除,Ack请求经Raft同步后,各节点应用该请求,之后消息被删除不会再投递。下面介绍详细过程:
生产流程:
1)生产者将生产消息的请求发往Leader的Raft模块。
2)Raft模块完成Entry的创建和同步。
3)大多数节点上持久化并返回成功后Entry标记为Committed。
4)所有节点的State Machine应用该日志,取出实际的生产请求,将消息内容写入磁盘,更新ApplyIndex。该步骤不需要刷盘。
5)Leader回复客户端Confirm,通知生产成功。
6)如果此后机器重启,通过raft日志恢复生产消息,保证了已Confirm的消息不丢失。
消费流程:
1)消费者从Leader节点拉取消息。
2)Leader收到后从磁盘加载未删除的消息投递给客户端。
3)客户端处理完成后Ack消息,通知服务器删除消息。
4)Ack请求经Raft同步后标记为Committed。
5)各节点状态机应用该日志,将消息对应的bit置位,将其设置为已删除并更新ApplyIndex。
6)通知客户端删除成功。
7)如果机器重启,通过Raft日志恢复Ack请求,保证了已删除的消息不会再投递。
快照管理:
快照管理与业务紧密相关,不同系统快照制作的成本差异很大,CMQ中快照的内容十分轻量,一次快照的耗时在毫秒级,平均5min创建一次,各节点独立完成。实现上内存中维护了一份动态的快照,制作快照时首先拷贝出动态快照的副本,之后处理流继续更新动态快照,用拷贝出的副本创建快照文件,不影响实际的处理流。快照具体内容包括:
1)term:快照对应Entry的term (参照算法)
2)index:快照对应Entry的 index (参照算法)
3)node_info:Entry时的集群配置信息。
4)topic info:每个队列一项。CMQ中同一队列生产的消息顺序写入,分片存储,因此只需记录最后一个分片的状态(分片文件名,文件偏移量)。
5)queue info:每个队列一项。CMQ中采用bitmap记录消息的删除情况,在内存中维护,在制作快照时dump到快照文件。
可靠性:业界统一的衡量标准为RPO(Recovery Point Objective),反映故障时数据恢复完整性的指标。由于只有提交的日志才会被应用到状态机,且raft日志在写入时会强制刷盘,所以故障重启后通过快照+raft日志即可恢复,不会丢失数据,RPO=0。不过,如2.7节所述,Leader故障时可能会产生重复数据,需要通过幂等性保证或去重机制来解决该问题。
可用性:业界统一的衡量标准为RTO(Recovery Time Objective),反映故障时业务恢复及时性的指标。follower故障对系统没有影响(RTO=0),leader故障时其他节点通过自发选出新leader,而且CMQ中前端具备自动重连功能,当连接断开后会自动寻找新leader,系统不可用时间大大降低。目前CMQ中配置的选举超时时间为2s~4s,在不考虑选举冲突的前提下,RTO上限为4s。
在CMQ中,Leader通过与Follower的心跳判断自己是否已网络分区,当检测到分区时(大多数节点上次心跳回复时间距现在超过2s),主动断开前端连接,前端发现后会自动寻找新Leader。这段时间内客户端请求会超时,在连上新Leader后,客户端重试之前超时的任务,后续请求恢复正常。
四 Raft算法性能优化
Raft算法的性能瓶颈主要有两方面:
1) 每次日志写入后都需要刷盘才能返回成功,而刷盘是一个比较耗时的操作。
2) 由于算法限制,所有的请求都由Leader处理,不能做到所有节点皆可提供服务。
针对以上两个问题,我们做了以下优化:
1)Batch Processing:在请求量较大时,并不是每一条日志写入都刷盘,还是累积一定量的日志后集中刷盘,从而减少刷盘次数。对应的,在同步到Follower时也采用批量同步的方式,Follower接收后将日志批量写盘。
2)Multi-Raft: 进程中同时运行多个raft实例,机器之间组建多raft 组,客户端请求路由到不同的group上,从而实现多主读写,提高并发性能。通过将leader分布在不同机器上,提高了系统的整体利用率。
3)Async-rpc: 在日志同步过程中采用同步rpc方式,在一端处理时另一端只能等待,性能较差。我们采用异步的方式使得leader端发送和Follower端处理并发进行。发送过程中leader端维持一个发送窗口,当待确认的rpc数达到上限停止发送,窗口值上限:
在与同属于高可靠(多副本同步刷盘)的Rabbitmq性能对比中,相同压测场景下CMQ速度可以达到RabbitMQ的四倍左右。
以下为TS60机器1KB消息大小时性能数据:
测试中CMQ采用单Raft组方式以保证测试公平性。监控显示CPU、内存和网卡均未达到瓶颈,系统瓶颈在磁盘IO,iostat显示w_await远大于svctm。主要原因在于刷盘耗时,造成写操作排队等待。
实际生产环境CMQ中我们将raft组和磁盘进行绑定,实现raft组之间磁盘的隔离,一方面保证了磁盘的顺序读写,另一方面充分利用机器的cpu 、内存、网卡等资源。
五 通用Raft库
CMQ中完整实现了Raft算法并解决了很多细节难点。考虑到分布式系统设计的复杂性,如果开发者只专注于业务相关部分,将可以显著降低开发难度,提高系统的质量,所以我们将CMQ中的raft部分以库的方式独立出来,使用者用它即可搭建一套强一致高可用分布式系统。目前该库已经完成基线版本开发并在部门落地使用,验证完成后会陆续开放给更多业务使用。
六 总结
消息中间件通常分为高可靠版本和高性能版本两种。CMQ是一款金融级的高可靠分布式消息中间件,通过raft保证了消息的可靠不丢失。同时在性能和可用性方面相比竞品都有显著提高。此外,我们自研的高性能版本的消息中间件ckafka也已在腾讯云上线,完美兼容kafka0.09~0.10版本客户端,关于CKafka的具体技术介绍请关注后续技术文章。
Raft算法强调了Leader的地位,选举和日志同步都是围绕Leader展开。由Leader负责处理所有请求保证了系统的强一致性;Leader选举和日志同步算法保证了数据的可靠不丢失;此外上述步骤只需要大多数正常互联即可,从而极大提高了系统的可用性,少量机器故障不受影响。不过,所有请求由Leader处理并没有充分利用从节点的资源,目前google的Spanner已支持从从节点读取,后续我们也会在这方面作更进一步的研究。Raft算法易于理解和工程化,相信未来会应用在越来越多的分布式系统中。