来源:JAVA日知录
1、升级背景
Apache RocketMQ是阿里巴巴于2012年开发的分布式消息中间件,专为处理万亿级超大规模消息而设计。其出色的性能特点包括高吞吐量、低延迟、海量消息处理能力以及顺序消息传递功能。经过多次考验,尤其是在双十一这种高流量的情况下,RocketMQ已经成为Apache基金会的顶级项目,并在教育业务领域得到广泛应用。
然而,最近RocketMQ曝出了一个远程代码执行漏洞:由于CVE-2023-33246的修复不完善,攻击者在未授权访问Apache RocketMQ NameServer的情况下仍然可以以系统用户身份执行命令,该漏洞的编号是CVE-2023-37582。这意味着存在潜在的风险,攻击者有可能利用此漏洞入侵系统。
复现截图如下,攻击成功将会在受害者服务器写入/tmp/pwned文件

漏洞影响的版本范围包括:4.0.0 <= Apache RocketMQ <= 4.9.6,以及 5.0.0 <= Apache RocketMQ <= 5.1.1。如果您使用的RocketMQ版本处于这个范围内,强烈建议您升级到4.9.7版本或5.1.2版本及以上。
不幸的事,漏洞公告刚发布不久,我们所在的业务线服务器就中招了。挖矿病毒还在集团内网络互通机房传播,除了进行安全隔离和防护,修复RocketMQ的漏洞成为信息安全管理部门要求的红线范围之内的紧急任务。
2、问题描述
根据漏洞情况,影响的版本范围为4.0.0至4.9.6以及5.0.0至5.1.1。在我们的中心化产线自建集群中,有多达25套集群受到影响,还有更多的交付项目可能也受到影响。尽管我们之前提供了RocketMQ停服升级方案,但是由于业务使用规模庞大,场景复杂,而且夜间还有大量的生产消费活动,升级要在无感知的情况下进行。
因此,我们需要深入研究和实践下文中的升级方案,同时整理出详细的方案文档,以便为其他产线提供支持和参考。

3、升级思路
为了解决这个问题,我们提出了以下升级思路:
首先,我们计划对Broker节点进行扩容,将两台高版本的Broker服务器加入到集群中。然后,我们会关闭低版本Broker的写权限,等待其消息过期后再将其移除。最后,我们将滚动升级NameServer,以实现在线迁移而无需停机。
升级方案图如下:

4、方案验证
生产环境做任何变更之前,必须有在测试或开发环境,进行充分的场景验证,本次升级验证也不例外,在此重点需要验证低版本客户端对高版本RocketMQ兼容性问题。
验证场景一:服务端Namesrv、Broke高低版本集群兼容性问题
高版本的Broker是否能向低版本的NameServer注册路由 低版本的Broker是否能向高版本的NameServer注册路由
经验证,通过接口模拟,消息能在高低版本正常生产消费,符合预期

验证场景二:服务端低版本Broke设置为只读后,是否还有消息生产,且低版本已生产消息是否能继续消费

其中:brokerPermission=2
表示只写,brokerPermission=4
表示只读,brokerPermission=6
表示读写,通过将低版本broke-b设置为只读后,新消息便不再低版本生产,但能正常消费,符合预期

验证场景三:低版本Broker主节点设为只读并关闭后,低版本Broker从节点是否有消息生产消费

经验证,关闭broke-b主节点后,通过命令指定向broke-b从节点生产消息,结果会往高版本broke-c主节点产生消息,因此证明低版本主设为只读并关闭后,从节点不会提升为主,且不参与生产,符合预期
验证场景四:RocketMQ客户端与服务端兼容性验证
RocketMQ的客户端API其实比较单一,无非就是消息发送、批量发送,消息消费,验证的要点:
低版本的客户端是否可以像高版本broke生产消费 高版本的客户端是否可以像低版本broke生产消费
通过结合业务实际场景验证,加上本次升级是从4.7.1升级至4.9.7,升级幅度不大,高低版本客户端是能兼容的,验证符合预期,其他产线升级,一定得考虑这一点,顺便补充下,可以通过consumerConnection参数查看客户端链接版本版本、语言类型信息;
5、实施方案
在完成方案验证后,我们进入具体的实施步骤阶段。这一阶段需要确保有清晰的割接和回滚方案。由于服务器资源有限,我们只能在原有的服务器资源上进行扩容。
在进行实际升级前,必须确保系统的剩余内存资源大于原有Broker集群JVM内存的一倍以上,再加上操作系统预留的资源。否则,同服务器升级可能会引发问题。不停机升级过程相对复杂,需要理解原理并且仔细核查配置,方能顺利进行。
详细升级步骤如下:
5.1 高版本broke节点扩容至低版本rocketmq集群中
1) 高版本集群搭建
注意事项:jvm 参数需要调整,和原先集群保持一致即可;调用链日志文件
[root@node1 third_party]# cd /data/third_party
[root@node1 third_party]# wget https://dist.apache.org/repos/dist/release/rocketmq/4.9.7/rocketmq-all-4.9.7-bin-release.zip
[root@node1 third_party]# unzip rocketmq-all-4.9.7-bin-release.zip
2)拷贝调用链日志文件(若未采集则不需要拷贝)
[root@node1 third_party]# cd /data/third_party/rocketmq-all-4.7.1-bin-release/conf/
[root@node1 conf]# cp logback_broker.xml logback_namesrv.xml logback_tools.xml /data/third_party/rocketmq-all-4.9.7-bin-release/conf/
3)高版本手动创建数据目录
[root@node1 third_party]# cd /data/third_party/rocketmq-all-4.9.7-bin-release
[root@node1 third_party]# mkdir master/store/{commitlog,config,consumequeue,index} -p
[root@node1 third_party]# mkdir slave/store/{commitlog,config,consumequeue,index} -p
4)低版本4.7.1broke配置文件拷贝至高版本4.9.7
node1和node2服务器,分别将将低版本4.7.1配置文件拷贝至高版本4.9.7,并更名为broker-c-m.properties、broker-d-s.properties、broker-c-s.properties、broker-d-m.properties,下面已broker-c-m.properties为例,其余三个配置文件修改雷同

broke-a-m.properties ---> broker-c-m.properties更改配置文件如下:
brokerName=broker-c
listenPort=30911 #新增30911端口
storePathRootDir=/data/third_party/rocketmq-all-4.9.7-bin-release/master/store
storePathCommitLog=/data/third_party/rocketmq-all-4.9.7-bin-release/master/store/commitlog
storePathConsumeQueue=/data/third_party/rocketmq-all-4.9.7-bin-release/master/store/consumequeue
storePathIndex=/data/third_party/rocketmq-all-4.9.7-bin-release/master/store/index
storeCheckpoint=/data/third_party/rocketmq-all-4.9.7-bin-release/master/store/checkpoint
5)低版本4.7.1 topic配置、subscriptionGroup配置拷贝至高版本4.9.7
topics.json文件由TopicConfigManager类解析并存储;存储每个topic的读写队列数、权限、是否顺序等信息。
consumerOffset.json文件由ConsumerOffsetManager类解析并存储;
delayOffset.json文件由ScheduleMessageService类解析并存储;
存储对于延迟主题SCHEDULE_TOPIC_XXXX的每个consumequeue队列的消费进度;
存储每个消费者Consumer在每个topic上对于该topic的consumequeue队列的消费进度;
subscriptionGroup.json文件由SubscriptionGroupManager类解析并存储,存储每个消费者Consumer的订阅信息。
本次升级会等待消息消费完再进行升级,所以只同步 topics.json、subscriptionGroup.json文件
[root@node1 config]# cd /data/third_party/rocketmq-all-4.7.1-bin-release/master/store/config
[root@node1 config]# cp topics.json subscriptionGroup.json /data/third_party/rocketmq-all-4.9.7-bin-release/master/store/config
6)启动高版本broke-c、broke-d主从节点
[root@node1 bin] nohup sh mqbroker -c /data/third_party/rocketmq-all-4.9.7-bin-release/conf/2m-2s-async/broker-c-m.properties &
[root@node1 bin] nohup sh mqbroker -c /data/third_party/rocketmq-all-4.9.7-bin-release/conf/2m-2s-async/broker-d-s.properties &
[root@node2 bin] nohup sh mqbroker -c /data/third_party/rocketmq-all-4.9.7-bin-release/conf/2m-2s-async/broker-c-s.properties &
[root@node2 bin] nohup sh mqbroker -c /data/third_party/rocketmq-all-4.9.7-bin-release/conf/2m-2s-async/broker-d-m.properties &
5.2 夜间停止低版本broke写权限
brokerPermission=2 表示只写,brokerPermission=4 表示只读,brokerPermission=6 表示读写
# 摘除broke-a主节点写入
[root@node1 bin]# sh ./mqadmin updateBrokerConfig -b node1_IP:10911 -n xx.xxx.7.21:9876 -k brokerPermission -v 4
# 摘除broke-b主节点写入
[root@node1 bin]# sh ./mqadmin updateBrokerConfig -b node2_IP:20911 -n 10.107.7.21:9876 -k brokerPermission -v 4
#观察流量变化命令
#将 Broker 设置为只读权限后,观察该节点的流量变化,直到写入流量(InTPS)掉为 0 表示写入流量已摘除,或通过控制台观看5分钟趋势
5.3 待broke主节点消息完成后进行关闭操作
将broker的写权限关闭后,非顺序消息不会立马拒绝,而是需要等客户端路由信息更新后,不会在往该broker上发送消息,故这个过程需要等待。通过 rocketmq-console 查看该broker的写入TPS,当写入TPS降为0后,再使用 kill pid 关闭 rocketmq 进程。同时观察业务是否正常,同时在控制台观察消息是否在broke-c和broke-d写入情况;

等低版本broke彻底没有消费后,则关闭:
node1节点
[root@node1 bin]# kill -9 `ps -eo pid,lstart,etime,cmd | grep 4.7.1 |grep a-m |grep mq |awk -F" " '{print $1}'`
[root@node1 bin]# kill -9 `ps -eo pid,lstart,etime,cmd | grep 4.7.1 |grep b-s |grep mq |awk -F" " '{print $1}'`
node2节点
[root@node2 bin]# kill -9 `ps -eo pid,lstart,etime,cmd | grep 4.7.1 |grep a-s |grep mq |awk -F" " '{print $1}'`
[root@node2 bin]# kill -9 `ps -eo pid,lstart,etime,cmd | grep 4.7.1 |grep b-m |grep mq |awk -F" " '{print $1}'`
5. 4 滚动升级Namesrv
由于业务代码直接连接Namesrv集群地址,考虑到namesrv是无状态特性,新老版本namesrv可以共存,所以本次Namesrv采取滚动升级,确保升级过程中有一个能正常提供服务和注册,即:

node1节点:
1)关闭node1节点低版本namesrv节点
[root@node1 bin]# kill -9 `ps -ef |grep 4.7.1| grep namesrv |grep -v grep |awk -F" " '{print $2}'`
2)启动node1节点高版本namesrv节点
[root@node1 bin]# cd /data/third_party/rocketmq-all-4.9.7-bin-release/bin
[root@node1 bin]# nohup sh mqnamesrv &
3)等待业务验证
4)关闭node2节点低版本namesrv节点
[root@node2 bin]# kill -9 `ps -ef |grep 4.7.1| grep namesrv |grep -v grep |awk -F" " '{print $2}'`
5)启动node2节点高版本namesrv节点
[root@node2 bin]# cd /data/third_party/rocketmq-all-4.9.7-bin-release/bin
[root@node2 bin]# nohup sh mqnamesrv &
至此整个集群升级完毕,业务所有功能进行回归验证工作;尽管上面的方案非常详细,并且已经过测试环境验证,但线上操作一定要仔细检查,确保万无一失,最后预祝大家顺利升级!!
6、个人建议
跨版本升级可能存在风险,特别是从3.x升级到4.x,或从4.x升级到5.x。在进行这样的升级之前,建议详细了解高低版本的特性,并结合业务特点进行充分验证。 如果业务场景简单,没有不停机升级的必要,也可以考虑停服升级。升级过程的中断时间应该根据新集群的重启时间来估算,业务方需要根据风险评估是否接受这种中断。 在生产环境中,避免使用root账号部署应用,这样可以减少漏洞引发的攻击风险。另外,如果有条件,可以考虑使用公有云产品,商业产品一般会更加安全和稳定。