乐趣区

关于云原生-cloud-native:年终盘点-七年零故障支撑-双11-的消息中间件-RocketMQ怎么做到的

作者 | 愈安
起源 |阿里巴巴云原生公众号

2020 年双十一交易峰值达到 58.3W 笔 / 秒,消息中间件 RocketMQ 持续数年 0 故障丝般顺滑地完满反对了整个团体大促的各类业务安稳。往年双十一大促中,消息中间件 RocketMQ 产生了以下几个方面的变动:

  • 云原生化实际。实现运维层面的云原生化革新,实现 Kubernetes 化。
  • 性能优化。音讯过滤优化交易集群性能晋升 30%。
  • 全新的生产模型。对于提早敏感业务提供新的生产模式,升高因公布、重启等场景下导致的生产提早。

云原生化实际

1. 背景

Kubernetes 作为目前云原生化技术栈实际中重要的一环,其生态曾经逐渐建设并日益丰盛。目前,服务于团体外部的 RocketMQ 集群领有微小的规模以及各种历史因素,因而在运维方面存在相当一部分痛点,咱们心愿可能通过云原生技术栈来尝试找到对应解决方案,并同时实现降本提效,达到无人值守的自动化运维。

消息中间件早在 2016 年,通过外部团队提供的中间件部署平台实现了容器化和自动化公布,整体的运维比 2016 年前曾经有了很大的进步,然而作为一个有状态的服务,在运维层面依然存在较多的问题。

中间件部署平台帮咱们实现了资源的申请,容器的创立、初始化、镜像装置等一系列的根底工作,然而因为中间件各个产品都有本人不同的部署逻辑,所以在利用的公布上,就是各利用本人的定制化了。中间件部署平台的开发也不齐全理解团体内 RocketMQ 的部署过程是怎么的。

因而在 2016 年的时候,部署平台须要咱们去亲自实现消息中间件的利用公布代码。尽管部署平台大大晋升了咱们的运维效率,甚至还能实现一键公布,然而这样的计划也有不少的问题。比拟显著的就是,当咱们的公布逻辑有变动的时候,还须要去批改部署平台对应的代码,须要部署平台降级来反对咱们,用最近比拟风行的一个说法,就是相当不云原生。

同样在故障机替换、集群缩容等操作中,存在局部人工参加的工作,如切流,沉积数据的确认等。咱们尝试过在部署平台中集成更多消息中间件本人的运维逻辑,不过在其余团队的工程里写本人的业务代码,的确也是一个不太敌对的实现计划,因而咱们心愿通过 Kubernetes 来实现消息中间件本人的 operator。咱们同样心愿利用云化后云盘的多正本能力来升高咱们的机器老本并升高主备运维的复杂程度。

通过一段时间的跟进与探讨,最终再次由外部团队承当了建设云原生利用运维平台的工作,并依靠于中间件部署平台的教训,借助云原生技术栈,实现对有状态利用自动化运维的冲破。

2. 实现

整体的实现计划如上图所示,通过自定义的 CRD 对消息中间件的业务模型进行形象,将原有的在中间件部署平台的业务公布部署逻辑下沉到消息中间件本人的 operator 中,托管在外部 Kubernetes 平台上。该平台负责所有的容器生产、初始化以及团体内所有线上环境的基线部署,屏蔽掉 IaaS 层的所有细节。

Operator 承当了所有的新建集群、扩容、缩容、迁徙的全副逻辑,包含每个 pod 对应的 brokerName 主动生成、配置文件,依据集群不同性能而配置的各种开关,元数据的同步复制等等。同时之前一些人工的相干操作,比方切流时候的流量察看,下线前的沉积数据察看等也全副集成到了 operator 中。当咱们有需要从新批改各种运维逻辑的时候,也再也不必去依赖通用的具体实现,批改本人的 operator 即可。

最初线上的理论部署状况去掉了图中的所有的 replica 备机。在 Kubernetes 的理念中,一个集群中每个实例的状态是统一的,没有依赖关系,而如果依照消息中间件原有的主备成对部署的计划,主备之间是有严格的对应关系,并且在高低线公布过程中有严格的程序要求,这种部署模式在 Kubernetes 的体系下是并不提倡的。若仍然采纳以上老的架构形式,会导致实例管制的复杂性和不可控性,同时咱们也心愿能更多的遵循 Kubernetes 的运维理念。

云化后的 ECS 应用的是高速云盘,底层将对数据做了多备份,因而数据的可用性失去了保障。并且高速云盘在性能上齐全满足 MQ 同步刷盘,因而,此时就能够把之前的异步刷盘改为同步,保障音讯写入时的不失落问题。云原生模式下,所有的实例环境均是一致性的,依靠容器技术和 Kubernetes 的技术,可实现任何实例挂掉(蕴含宕机引起的挂掉),都能主动自愈,疾速复原。

解决了数据的可靠性和服务的可用性后,整个云原生化后的架构能够变得更加简略,只有 broker 的概念,再无主备之分。

3. 大促验证

上图是 Kubernetes 上线后双十一大促当天的发送 RT 统计,可见大促期间的发送 RT 较为安稳,整体合乎预期,云原生化实际实现了关键性的里程碑。

性能优化

1. 背景

RocketMQ 至今曾经间断七年 0 故障反对团体的双十一大促。自从 RocketMQ 诞生以来,为了可能齐全承载包含团体业务中台交易音讯等外围链路在内的各类要害业务,复用了原有的下层协定逻辑,使得各类业务方齐全无感知的切换到 RocketMQ 上,并同时充沛享受了更为稳固和弱小的 RocketMQ 消息中间件的各类个性。

以后,申请订阅业务中台的外围交易音讯的业务方始终都在一直继续减少,并且随着各类业务复杂度晋升,业务方的音讯订阅配置也变得更加简单繁琐,从而使得交易集群的进行过滤的计算逻辑也变得更为简单。这些业务方局部沿用旧的协定逻辑(Header 过滤),局部应用 RocketMQ 特有的 SQL 过滤。

2. 次要老本

目前团体外部 RocketMQ 的大促机器老本绝大部分都是交易音讯相干的集群,在双十一零点峰值期间,交易集群的峰值和交易峰值成正比,叠加每年新增的简单订阅带来了额定 CPU 过滤计算逻辑,交易集群都是大促中机器老本增长最大的中央。

3. 优化过程

因为历史起因,大部分的业务方次要还是应用 Header 过滤,外部实现其实是 aviator 表达式(https://github.com/killme2008/aviatorscript)。仔细观察交易音讯集群的业务方过滤表达式,能够发现绝大部分都指定相似 MessageType == xxxx 这样的条件。翻看 aviator 的源码能够发现这样的条件最终会调用 Java 的字符串比拟 String.compareTo()。

因为交易音讯包含大量不同业务的 MessageType,光是有记录的起码有几千个,随着交易业务流程复杂化,MessageType 的增长更是繁多。随着交易峰值的进步,交易音讯峰值反比增长,叠加这部分更加简单的过滤,持续增长的未来,交易集群的老本极可能和交易峰值指数增长,因而信心对这部分进行优化。

原有的过滤流程如下,每个交易音讯须要一一匹配不同 group 的订阅关系表达式,如果合乎表达式,则选取对应的 group 的机器进行投递。如下图所示:

对此流程进行优化的思路须要肯定的灵感,在这里借助数据库索引的思路:原有流程能够把所有订阅方的过滤表达式看作数据库的记录,每次音讯过滤就相当于一个带有特定条件的数据库查问,把所有匹配查问(音讯)的记录(过滤表达式)选取进去作为后果。为了放慢查问后果,能够抉择 MessageType 作为一个索引字段进行索引化,每次查问变为先匹配 MessageType 主索引,而后把匹配上主索引的记录再进行其它条件 (如下图的 sellerId 和 testA) 匹配,优化流程如下图所示:

以上优化流程确定后,要关注的技术点有两个:

  • 技术点 1:如何抽取每个表达式中的 MessageType 字段?
  • 技术点 2:如何对 MessageType 字段进行索引化?

对于技术点 1,须要针对 aviator 的编译流程进行 hook,深刻 aviator 源码后,能够发现 aviator 的编译是典型的 Recursive descent:http://en.wikipedia.org/wiki/Recursive_descent_parser,同时须要思考到提取后父表达式的短路问题。

在编译过程中针对 messageType==XXX 这种类型进行提取后,把原有的 message==XXX 转变为 true/false 两种状况,而后针对 true、false 进行表达式的短路即可得出表达式优化提取后的状况。例如:

表达式:messageType=='200-trade-paid-done' && buyerId==123456
提取为两个子表达式:子表达式 1(messageType==200-trade-paid-done):buyerId==123456 
子表达式 2(messageType!=200-trade-paid-done):false
  • 具体到 aviator 的实现里,表达式编译会把每个 token 构建一个 List,相似如下图所示(为不便了解,绿色方框的是 token,其它框示意表达式的具体条件组合):

提取了 messageType,有两种状况:

  • 状况一:messageType == ‘200-trade-paid-done’,则把之前 token 的地位合并成 true,而后进行表达式短路计算,最初优化成 buyerId==123456,具体如下:

  • 状况二:messageType != ‘200-trade-paid-done’,则把之前 token 的地位合并成 false,表达式短路计算后,最初优化成 false,具体如下:

这样就实现 messageType 的提取。这里可能有人就有一个疑难,为什么要思考到下面的状况二,messageType != ‘200-trade-paid-done’,这是因为必须要思考到多个条件的时候,比方:

(messageType==’200-trade-paid-done’ && buyerId==123456) || (messageType==’200-trade-success’ && buyerId==3333)

就必须思考到不等于的状况了。同理,如果思考到多个表达式嵌套,须要逐渐进行短路计算。但整体逻辑是相似的,这里就不再赘述。

说完技术点 1,咱们持续关注技术点 2,思考到高效过滤,间接应用 HashMap 构造进行索引化即可,即把 messageType 的值作为 HashMap 的 key,把提取后的子表达式作为 HashMap 的 value,这样每次过滤间接通过一次 hash 计算即可过滤掉绝大部分不适宜的表达式,大大提高了过滤效率。

4. 优化成果

该优化最次要升高了 CPU 计算逻辑,依据优化前后的性能状况比照,咱们发现不同的交易集群中的订阅方订阅表达式复杂度越高,优化成果越好,这个是合乎咱们的预期的,其中最大的 CPU 优化有 32% 的晋升,大大降低了本年度 RocketMQ 的部署机器老本。

全新的生产模型 —— POP 生产

1. 背景

RocketMQ 的 PULL 生产对于机器异样 hang 时并不非常敌对。如果遇到客户端机器 hang 住,但处于半死不活的状态,与 broker 的心跳没有断掉的时候,客户端 rebalance 仍然会调配生产队列到 hang 机器上,并且 hang 机器生产速度很慢甚至无奈生产的时候,这样会导致生产沉积。另外相似还有服务端 Broker 公布时,也会因为客户端屡次 rebalance 导致生产提早影响等无奈防止的问题。如下图所示:

当 Pull Client 2 产生 hang 机器的时候,它所调配到的三个 Broker 上的 Q2 都呈现重大的红色沉积。对于此,咱们减少了一种新的生产模型——POP 生产,可能解决此类稳定性问题。如下图所示:

POP 生产中,三个客户端并不需要 rebalance 去调配生产队列,取而代之的是,它们都会应用 POP 申请所有的 broker 获取音讯进行生产。broker 外部会把本身的三个队列的音讯依据肯定的算法调配给申请的 POP Client。即便 Pop Client 2 呈现 hang,但外部队列的音讯也会让 Pop Client1 和 Pop Client2 进行生产。这样就 hang 机器造成的防止了生产沉积。

2. 实现

POP 生产和原来 PULL 生产比照,最大的一点就是弱化了队列这个概念,PULL 生产须要客户端通过 rebalance 把 broker 的队列调配好,从而去生产调配到本人专属的队列,新的 POP 生产中,客户端的机器会间接到每个 broker 的队列进行申请生产,broker 会把音讯调配返回给期待的机器。随后客户端生产完结后返回对应的 Ack 后果告诉 broker,broker 再标记音讯生产后果,如果超时没响应或者生产失败,再会进行重试。

POP 生产的架构图如上图所示。Broker 对于每次 POP 的申请,都会有以下三个操作:

  • 对应的队列进行加锁,而后从 store 层获取该队列的音讯。
  • 而后写入 CK 音讯,表明获取的音讯要被 POP 生产。
  • 最初提交以后位点,并开释锁。

CK 音讯实际上是记录了 POP 音讯具体位点的定时音讯,当客户端超时没响应的时候,CK 音讯就会从新被 broker 生产,而后把 CK 音讯的位点的音讯写入重试队列。如果 broker 收到客户端的生产后果的 Ack,删除对应的 CK 音讯,而后依据具体后果判断是否须要重试。

从整体流程可见,POP 生产并不需要 reblance,能够防止 rebalance 带来的生产延时,同时客户端能够生产 broker 的所有队列,这样就能够防止机器 hang 而导致沉积的问题。

退出移动版