RocketMQ基于Dledger模式,平滑升级操作

前置条件

该方案并非所有case通用,其它部署模型仅供参考。

  • 集群模式:多主多从(比如,2主4从)
  • 支持主从切换:Dledger,不支持也可参考,满足上一条要求
  • 发送端与消费端在进行broker升级的过程中(broker重启),要求客户端不允许出现errror日志

场景

MQ集群节点需要进行轮替式升级,或者修改broker的配置进行重启。broker重启时,客户端不允许出现ERROR级别的报错日志。

broker重启,可能导致客户端报错,大概有以下情况:

1. producer发送消息失败(路由信息未及时更新,已停止的broker会连接失败)

2. consumer更新偏移失败(定时调度,每5s向broker master持久化消费位点,此时连接失败)

第1条,错误日志在业务系统中直接就打印了。

第2条,属于消费端日志,默认消费端是在家目录下有个logs/rocketmq_client.log单独记录日志。但是,这个可以设置使用业务系统的slf4j记录日志,日志就可以被业务系统采集,然后我们的业务系统做了相关告警,如果有ERROR级别的日志(某些ERROR日志其实不会对业务造成任何影响),就会告警。为了避免broker master重启过程中,出现告警被业务系统感知到(没有影响业务的告警是不需要的),需要保证操作的平滑性、稳定性。心跳失败是info级别日志。

假如,我现在要把线上的MQ集群从4.7.1升级到4.8.0,操作如下,主要说明broker,忽略name server,那个好操作。

操作步骤

集群模型如下:

依次升级broker1的主从节点、broker2的主从节点。

1.先关闭broker master1的写权限,禁止producer发送消息到broker1。此时需要保证,broker2可以承担全部压力。因为关闭写权限后,此时所有producer的流量会全部切到broker2上(包括原broker1上面的),这就体现了资源冗余的重要性。

sh mqadmin updatebrokerconfig -n 'nameserver:9876' -k brokerPermission -v 4 -b broker1master:10911

2.通过控制台/命令行或者监控平台,取决于你有什么工具,什么都没有就用命令行clusterList命令,观察broker1 master的inTps和outTps都为0,然后把读权限也去掉(确保该节点所有消息消费完结,无积压)。

sh mqadmin updatebrokerconfig -n 'nameserver:9876' -k brokerPermission -v 1 -b broker1master:10911

此时消费端会报一些warn级别的日志,禁止拉取,但是不影响,因为所有消息已经消费完毕。

3.查看从节点slave1_1和slave1_2的inTps都为0,确保无消息同步进行,然后,分别启停slave1_1和slave1_2进行升级。

4.停止 master1节点,确保这一步与第2步间隔时间至少2分钟(为什么间隔时间这么久,后面说明),停止后,其中一个从节点会自动选举为主节点,producer与consumer正常连接新的主节点进行消息的发送和消费。然后,启动最后一个新broker(就刚才停掉这个),做为从节点(如果不是进行版本升级,只是重启的话,先把这个节点的配置项brokerPermission的值改为6再启动(因为前几步把这个节点的读写权限关了,所以这个配置项现在是1)。

升级的时候,从节点切换为新的主节点的时候,如果消费端报这个warn日志,不用关心,这是消费端负载均衡,队列分配的时候,一个实现的不足的地方,目前还没有修复,正常现象。详细原因,后续有时间会单独说明这个问题:

这样的话,broker1整个重启/升级过程,对业务侧客户端完全无影响,只是会报少量warn级别日志,ERROR级别的错误日志不会出现。

5.确认broker1没有问题,重复步骤1-4,操作broker2相关节点,直到整个集群升级完成。

关闭master的读权限,建议间隔至少2分钟再停止

在前面看到,第2步关闭读权限后,要过至少2分钟才停止broker,这是要避免消费端出现连接异常的ERROR报错:持久化消费偏移失败。以停止broker1的场景下面逐个分析:

心跳的定时发送?看一下代码:在MQClientInstance类的startScheduledTask()方法里,注意我添加的中文注释:

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    // 默认是30秒
                    // 遍历缓存的broker地址,然后判断topic路由信息(但是topicRouteInfo一定会有)里是否有该broker,不存在就剔除
                    // 没有read perm是pub info表里没有该topic的队列数据,没有write perm是sub info里没有该topic的队列数据,但是topicRouteInfo一定会有
                    MQClientInstance.this.cleanOfflineBroker();
                    // 尽管 发送心跳的时候,broker地址表里保存的是所有broker的地址,如果没有消费端实例,只会向master节点发送心跳,否则向所有broker发送心跳
                    // 每次发送心跳的时候broker端都会创建retry topic。所以retry topic即使删除了,只要消费端运行,一个心跳后便又创建了,
                    // 同时也意味着创建一个消费组,只有消费端启动之后才会创建重试topic
                    MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
                } catch (Exception e) {
                    log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
                }
            }
        }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

这个心跳不会影响到消费偏移的更新,主要是关注我上面的注释,没有写权限,订阅信息就没该topic队列数据

 

有个定时任务会每隔30s更新topic路由信息(不贴代码了,太多了)。更新路由信息的时候,更新缓存的订阅信息(这个地方与更新偏移有关,先说明下):

                            // Update sub info,没有读权限的话,就没有队列信息
                            {
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }

默认每隔5s定时持久化消费偏移。只要保证持久化偏移的时候,没有broker1的地址,停止broker1就不用担心了。

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    // 默认每隔5秒钟上报偏移
                    // 如果上报偏移的 时候,已停止 broker的地址还缓存呢,就该报错了
                    MQClientInstance.this.persistAllConsumerOffset();
                } catch (Exception e) {
                    log.error("ScheduledTask persistAllConsumerOffset exception", e);
                }
            }
        }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

(代码太多,不粘贴了)简单点说,更新偏移会更新本地消息队列的所有偏移信息,而订阅的消息队列信息来自于前面topic路由信息更新的时候。没有读权限,就不会有broker1的队列信息,更新偏移就不会去更新这个broker上的消费偏移。这个过程的最大时间是:30+5=35s,这是客户端的时间间隔(最大),看下broker端,因为broker端关闭读权限后,最大要多久更新这个topic的路由信息:30s,

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {//brokerConfig.isForceRegister()默认值是true
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }// brokerConfig.getRegisterNameServerPeriod() 默认30s,在10-60s,之间,30s注册一次broker->name server,即每30s上报一次topic信息
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);

30s上报一次topic路由信息到name server,然后消费端才能开始感知到。

所以总时间为65s。要保证消费端一定不会有影响,在读权限关闭后最少需要65s才能停止broker。当然,实际中,这些调度交叉的(一些信息的更新除了调度还有其它场景),一二十秒也有可能。我建议2分钟的原因是,好记,无需关心细节,2分钟绝对安全。

猜你喜欢

转载自blog.csdn.net/x763795151/article/details/112385106