这里是weihubeats ,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党
背景
在我们要平滑升级broker的时候,无损升级的最佳实践应该是
- 新broker启动
- 旧broker停写
- 旧broker消息消费完成后下线(包括延时消息)
所以我们本次就是来分析如何完成broker
的停写
源码入口
其实通过查看源码,我们发现有两种方式可以停写broker
- 通过mqadmin运维工具命令行的方式

- 通过MQAdminExt管理工具类

两种方式没有区别,都是调用DefaultMQAdminExt
的wipeWritePermOfBroker
方法

源码分析
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
我们可以看看他实际的数据结构

可以看到这里他的perm
是7
如果我们去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
的路由信息
值得注意的是这里有一个问题,就是实际这里并不会去更新broker
的 topics.json
配置信息,只是更新了NameServer
的内存里面的topicQueueTable
,所以也就是如果broker
重启或者NameServer
重启还是会恢复写入。 这里为什么不去更新broker
的topics.json
,个人觉得可能是一个bug或者是为了逻辑的简单
消费者更新Topic路由信息
至于消费者是如何更新获取到NameServer
更新的topicQueueTable
,我们可以去随便看看消费者的代码
我们可以看到消费者在启动的时候就启动了一个定时任务 MQClientInstance.this.updateTopicRouteInfoFromNameServer();

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

默认30s,也就是说我们在停写命令发出后,最长30s内还是会有消息写入
值得注意的是NameServer
之间并不会互相通信,所以如果我们是集群模还需要循环遍历所有NameServer
执行该命令