RocketMQ broker停写功能源码分析

这里是weihubeats ,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党

背景

在我们要平滑升级broker的时候,无损升级的最佳实践应该是

  1. 新broker启动
  2. 旧broker停写
  3. 旧broker消息消费完成后下线(包括延时消息)

所以我们本次就是来分析如何完成broker的停写

源码入口

其实通过查看源码,我们发现有两种方式可以停写broker

  1. 通过mqadmin运维工具命令行的方式
  1. 通过MQAdminExt管理工具类

两种方式没有区别,都是调用DefaultMQAdminExtwipeWritePermOfBroker方法

源码分析

client

我们这里直接进去到最底层的实现类代码

java 复制代码
public int wipeWritePermOfBroker(final String namesrvAddr, String brokerName,
        final long timeoutMillis) throws RemotingCommandException,
        RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException {
        WipeWritePermOfBrokerRequestHeader requestHeader = new WipeWritePermOfBrokerRequestHeader();
        requestHeader.setBrokerName(brokerName);

        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.WIPE_WRITE_PERM_OF_BROKER, requestHeader);
        RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMillis);
        assert response != null;
        switch (response.getCode()) {
            case ResponseCode.SUCCESS: {
                WipeWritePermOfBrokerResponseHeader responseHeader =
                    (WipeWritePermOfBrokerResponseHeader) response.decodeCommandCustomHeader(WipeWritePermOfBrokerResponseHeader.class);
                return responseHeader.getWipeTopicCount();
            }
            default:
                break;
        }

        throw new MQClientException(response.getCode(), response.getRemark());
    }

客户端的代码我们没什么好看的,我们还是通过请求状态码RequestCode.WIPE_WRITE_PERM_OF_BROKER查看实际的Nameserver的处理逻辑

NameServer

  • DefaultRequestProcessor的方法 wipeWritePermOfBroker

可以看到实际的代码逻辑在这一行

java 复制代码
int wipeTopicCnt = this.namesrvController.getRouteInfoManager().wipeWritePermOfBrokerByLock(requestHeader.getBrokerName());

我们进入到这个方法看看

可以看到实际的逻辑还在更下层,我们再进去看看

java 复制代码
private int operateWritePermOfBroker(final String brokerName, final int requestCode) {
        int topicCnt = 0;

        for (Entry<String, Map<String, QueueData>> entry : this.topicQueueTable.entrySet()) {
            Map<String, QueueData> qdMap = entry.getValue();

            final QueueData qd = qdMap.get(brokerName);
            if (qd == null) {
                continue;
            }
            int perm = qd.getPerm();
            switch (requestCode) {
                case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
                    perm &= ~PermName.PERM_WRITE;
                    break;
                case RequestCode.ADD_WRITE_PERM_OF_BROKER:
                    perm = PermName.PERM_READ | PermName.PERM_WRITE;
                    break;
            }
            qd.setPerm(perm);
            topicCnt++;
        }
        return topicCnt;
    }

这里的topicQueueTable我们可以看看他实际的数据结构

可以看到这里他的perm7

如果我们去broker的 config的topics.json查看相关的配置信息

可以看到刚好可以对得上

这里我们解释一下perm的权限问题

  • PERM_PRIORITY = 0x1 << INDEX_PERM_PRIORITY 8 优先级队列
  • PERM_READ = 0x1 << INDEX_PERM_READ 4 读权限
  • PERM_WRITE = 0x1 << INDEX_PERM_WRITE 2 写权限
  • PERM_INHERIT = 0x1 << INDEX_PERM_INHERIT 1 继承权限

0x1表示16进制的1,0x1 << 3 表示将二进制数 0001 向左移动 3 位,结果为 1000,对应十进制数 8。其他的类似

这里最基本的权限只有这三个,也就是读、写、继承。但是是否拥有读写权限是通过如下几个方法进行位运算

java 复制代码
public static boolean isWriteable(final int perm) {
        return (perm & PERM_WRITE) == PERM_WRITE;
    }

    public static boolean isInherited(final int perm) {
        return (perm & PERM_INHERIT) == PERM_INHERIT;
    }

    public static boolean isValid(final String perm) {
        return isValid(Integer.parseInt(perm));
    }

然后我们继续源码分析 这里对请求状态码做了区分,分析让queue权限变成可写还是可读 我们先看看perm &= ~PermName.PERM_WRITE;

我们原先的权限是7(可继承、读、写),经过运算就变成5(可继承、可读)了

这里我们是执行停写,如果要恢复写入,就执行PermName.PERM_READ | PermName.PERM_WRITE;,变为6(可读、可写、不可继承)

所以可以看到这里是先更新了NameServer的路由信息

值得注意的是这里有一个问题,就是实际这里并不会去更新brokertopics.json配置信息,只是更新了NameServer的内存里面的topicQueueTable,所以也就是如果broker重启或者NameServer重启还是会恢复写入。 这里为什么不去更新brokertopics.json,个人觉得可能是一个bug或者是为了逻辑的简单

消费者更新Topic路由信息

至于消费者是如何更新获取到NameServer更新的topicQueueTable,我们可以去随便看看消费者的代码

我们可以看到消费者在启动的时候就启动了一个定时任务 MQClientInstance.this.updateTopicRouteInfoFromNameServer();

这里定时会去Nameserver拉取最新的Topic路由信息,所以我们更新Nameserver的路由信息后这里就会拉去到。更新时间我们可以看看

默认30s,也就是说我们在停写命令发出后,最长30s内还是会有消息写入

值得注意的是NameServer之间并不会互相通信,所以如果我们是集群模还需要循环遍历所有NameServer执行该命令

相关推荐
AronTing几秒前
12- Java虚拟线程(Project Loom)深度解析:原理、实战与性能调优
java·后端·面试
豆浆Whisky23 分钟前
深入剖析Go Channel:从底层原理到高阶避坑指南|Go语言进阶(5)
后端·go
写bug写bug27 分钟前
Lua语言教程:从基础到进阶
后端·lua
normaling29 分钟前
XML配置文件
后端
shepherd11141 分钟前
从零搭建高可用Kafka集群与EFAK监控平台:全流程实战总结
分布式·后端·kafka
有诺千金1 小时前
深入理解 Spring Boot 的@AutoConfiguration注解
java·spring boot·后端
代码吐槽菌1 小时前
基于SpringBoot的律师事务所案件管理系统【附源码】
java·数据库·spring boot·后端·毕业设计
不爱学英文的码字机器1 小时前
Rust 的征服:从系统编程到全栈开发的 IT 新宠
开发语言·后端·rust
flzjkl1 小时前
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
java·后端
Victor3561 小时前
Dubbo(49)如何排查Dubbo的集群容错问题?
后端