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执行该命令

相关推荐
安的列斯凯奇6 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
架构文摘JGWZ6 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC6 小时前
Swift语言的网络编程
开发语言·后端·golang
邓熙榆6 小时前
Haskell语言的正则表达式
开发语言·后端·golang
专职9 小时前
spring boot中实现手动分页
java·spring boot·后端
Ciderw9 小时前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·
m0_7482463510 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
m0_7482304410 小时前
创建一个Spring Boot项目
java·spring boot·后端
卿着飞翔10 小时前
Java面试题2025-Mysql
java·spring boot·后端
C++小厨神10 小时前
C#语言的学习路线
开发语言·后端·golang