Rocketmq源码分析03:NameServer 消息处理

·  阅读 571

注:本系列源码分析基于RocketMq 4.8.0,gitee仓库链接:gitee.com/funcy/rocke….

上一篇文章中,我们分析NameServer的启动流程,最终NameServer启动了一个netty服务,本文我们将来分析这个netty服务是如何处理请求的。

1. 处理业务请求的ChannelHandler:serverHandler

NamesrvController启动后,就可以处理Broker/Producer/Consumer的请求消息了,处理该类型消息的ChannelHandlerserverHandler,也就是NettyRemotingServer.NettyServerHandlerNettyServerHandlerNettyRemotingServer的内部类),代码如下:

class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) 
            throws Exception {
        processMessageReceived(ctx, msg);
    }
}

继续跟进NettyRemotingAbstract#processMessageReceived方法:

public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) 
        throws Exception {
    final RemotingCommand cmd = msg;
    if (cmd != null) {
        switch (cmd.getType()) {
            case REQUEST_COMMAND:
                processRequestCommand(ctx, cmd);
                break;
            case RESPONSE_COMMAND:
                processResponseCommand(ctx, cmd);
                break;
            default:
                break;
        }
    }
}

这里我们直接跟进REQUEST_COMMAND命令的处理方法NettyRemotingAbstract#processRequestCommand

public void processRequestCommand(final ChannelHandlerContext ctx, 
        final RemotingCommand cmd) {
    final Pair<NettyRequestProcessor, ExecutorService> matched 
        = this.processorTable.get(cmd.getCode());
    final Pair<NettyRequestProcessor, ExecutorService> pair = 
        null == matched ? this.defaultRequestProcessor : matched;
    final int opaque = cmd.getOpaque();

    if (pair != null) {
        Runnable run = new Runnable() {
            @Override
            public void run() {
                try {
                    doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);
                    ...

                    if (pair.getObject1() instanceof AsyncNettyRequestProcessor) {
                        AsyncNettyRequestProcessor processor = 
                                (AsyncNettyRequestProcessor)pair.getObject1();
                        processor.asyncProcessRequest(ctx, cmd, callback);
                    } else {
                        NettyRequestProcessor processor = pair.getObject1();
                        // 处理请求
                        RemotingCommand response = processor.processRequest(ctx, cmd);
                        callback.callback(response);
                    }
                } catch (Throwable e) {
                    ...
                }
            }
        };

        ...

        try {
            // 异步处理
            final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
            pair.getObject2().submit(requestTask);
        } catch (RejectedExecutionException e) {
            ...
        }
    } else {
        ...
    }
}

这个方法主要流程为,先获取Pair对象,然后将处理操作封装为Runnable对象,接着把Runnable对象提交到线程池中。

这个Pair对象是啥呢?还记得我们在NamesrvController#initialize方法中创建的remotingExecutor吗,它最终注册到为NettyRemotingServerdefaultRequestProcessor属性:

@Override
public void registerDefaultProcessor(NettyRequestProcessor processor, ExecutorService executor) {
    this.defaultRequestProcessor 
            = new Pair<NettyRequestProcessor, ExecutorService>(processor, executor);
}

这里获取的Pair对象正是defaultRequestProcessorpair.getObject2()得到的线程池正是remotingExecutorpair.getObject1()得到的processorDefaultRequestProcessor.

这里我们就明白了,remotingExecutor线程池就是用来处理远程请求的。

远程命令的处理逻辑在Runnable#run方法中:

public void run() {
    try {
        doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);
        ...
        // 处理请求
        if (pair.getObject1() instanceof AsyncNettyRequestProcessor) {
            AsyncNettyRequestProcessor processor = 
                    (AsyncNettyRequestProcessor)pair.getObject1();
            processor.asyncProcessRequest(ctx, cmd, callback);
        } else {
            NettyRequestProcessor processor = pair.getObject1();
            // 处理请求
            RemotingCommand response = processor.processRequest(ctx, cmd);
            callback.callback(response);
        }
    } catch (Throwable e) {
        ...
    }
}

代码中区分了同步与异步请求两种方式,实际上最终都会进入到DefaultRequestProcessor#processRequest方法中:

public RemotingCommand processRequest(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {

    ...

    switch (request.getCode()) {
        ...
        // 查询dataVersion
        case RequestCode.QUERY_DATA_VERSION:
            return queryBrokerTopicConfig(ctx, request);
        // 注册broker的消息
        case RequestCode.REGISTER_BROKER:
            Version brokerVersion = MQVersion.value2Version(request.getVersion());
            if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
                return this.registerBrokerWithFilterServer(ctx, request);
            } else {
                return this.registerBroker(ctx, request);
            }
        // broker断开连接时,取消broker的注册消息
        case RequestCode.UNREGISTER_BROKER:
            return this.unregisterBroker(ctx, request);
        // 根据 topic 获取路由信息
        case RequestCode.GET_ROUTEINFO_BY_TOPIC:
            return this.getRouteInfoByTopic(ctx, request);
        // 省略其他消息的处理    
        ...
        default:
            break;
    }
    return null;
}

这个方法就是用来处理网络请求的,处理的请求消息会比较多,这里我们仅关注以下类型的消息:

  • 获取broker版本信息:请求codeQUERY_DATA_VERSION,用来查询broker的版本信息
  • broker注册:请求codeREGISTER_BROKERbroker启动时,会将自己的信息注册到nameServer
  • broker注销:请求codeUNREGISTER_BROKERbroker停止前,会发消息告诉nameServer自己将要关闭
  • 获取topic路由信息:根据 topic 获取路由信息,其实就是topic对应的brokermessageQueue信息

接下来我们就分析来介绍下这几种消息的实现。

2 broker的注册与注销消息

broker启动时,会向NamerServer发送注册消息;在broker关闭前,会向NameServer发送关闭消息。

我们先来看注册消息,处理broker注册消息的方法为DefaultRequestProcessor#registerBrokerWithFilterServer,我们直接看重要代码:

public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, 
        RemotingCommand request) throws RemotingCommandException {
    ...

    // 处理注册
    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
        requestHeader.getClusterName(),
        requestHeader.getBrokerAddr(),
        requestHeader.getBrokerName(),
        requestHeader.getBrokerId(),
        requestHeader.getHaServerAddr(),
        registerBrokerBody.getTopicConfigSerializeWrapper(),
        registerBrokerBody.getFilterServerList(),
        ctx.channel());
    ...
}

这里我们去除了不必要的代码,仅保留了注册的方法,这里调用的是RouteInfoManager#registerBroker方法,在分析这个方法前,我们先来了解下RouteInfoManager的基本信息,它的几个重要成员变量如下:

public class RouteInfoManager {
    /** topic -> List<QueueData> */
    private final HashMap<String, List<QueueData>> topicQueueTable;
    /** brokerName -> BrokerData */
    private final HashMap<String, BrokerData> brokerAddrTable;
    /** clusterName -> brokerName */
    private final HashMap<String, Set<String>> clusterAddrTable;
    /** brokerAddr -> BrokerLiveInfo */
    private final HashMap<String, BrokerLiveInfo> brokerLiveTable;

    // 省略其他方法
    ...
}

前面提到过NameServer是一个非常简单的Topic路由注册中心,这个HashMap就是NameServer实现注册中心的关键!

  1. topicQueueTable:存放保存topicQueue的关系,value类型为List,表明一个topic可以有多个queueQueueData的成员变量如下:

    public class QueueData implements Comparable<QueueData> {
     // 所在的 borker 的名称   
     private String brokerName;
     // 读写数
     private int readQueueNums;
     private int writeQueueNums;
     private int perm;
     private int topicSynFlag;
     ...
    }
    
  2. brokerAddrTable:记录broker的具体信息,keybroker名称,valuebroker具体信息,BrokerData的成员变量如下:

    public class BrokerData implements Comparable<BrokerData> {
     // 所在集群的名称   
     private String cluster;
     // broker名称
     private String brokerName;
     // borkerId对应的服务器地址,一个brokerName可以有多个broker服务器
     private HashMap<Long, String> brokerAddrs;
    }
    
  3. clusterAddrTable:集群信息,保存集群名称对应的brokerName

  4. brokerLiveTable:存活的broker信息,keybroker地址,value为具体的broker服务器,BrokerLiveInfo的成员变量如下:

    class BrokerLiveInfo {
     // 上一次心跳更新时间
     private long lastUpdateTimestamp;
     private DataVersion dataVersion;
     // 表示网络连接的channel,由netty提供
     private Channel channel;
     // 高可用的服务地址
     private String haServerAddr;
     ...
    }
    

了解完成这些后,再回过头来看RouteInfoManager#registerBroker方法,我们就会发现所谓的注册就是往以上几个HashMapput数据的操作:

public RegisterBrokerResult registerBroker(
    final String clusterName,
    final String brokerAddr,
    final String brokerName,
    final long brokerId,
    final String haServerAddr,
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List<String> filterServerList,
    final Channel channel) {
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        try {
            this.lock.writeLock().lockInterruptibly();
            // 根据 clusterName 获取 brokerNames
            Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
            if (null == brokerNames) {
                brokerNames = new HashSet<String>();
                // 注意put操作,操作的是 clusterAddrTable
                this.clusterAddrTable.put(clusterName, brokerNames);
            }
            brokerNames.add(brokerName);

            boolean registerFirst = false;
            // 根据 brokerName 获取指定的 brokerData
            BrokerData brokerData = this.brokerAddrTable.get(brokerName);
            if (null == brokerData) {
                registerFirst = true;
                brokerData = new BrokerData(clusterName, 
                    brokerName, new HashMap<Long, String>());
                // put操作,操作的是 brokerAddrTable    
                this.brokerAddrTable.put(brokerName, brokerData);
            }
            Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
            // 如果是由从切换为主,需要删除原来的从节点记录
            Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
            while (it.hasNext()) {
                Entry<Long, String> item = it.next();
                if (null != brokerAddr && brokerAddr.equals(item.getValue()) 
                        && brokerId != item.getKey()) {
                    it.remove();
                }
            }

            String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
            registerFirst = registerFirst || (null == oldAddr);

            if (null != topicConfigWrapper
                && MixAll.MASTER_ID == brokerId) {
                if (this.isBrokerTopicConfigChanged(brokerAddr, 
                        topicConfigWrapper.getDataVersion()) || registerFirst) {
                    ConcurrentMap<String, TopicConfig> tcTable =
                        topicConfigWrapper.getTopicConfigTable();
                    if (tcTable != null) {
                        for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                            // 添加topic,里面操作的是 topicQueueTable
                            this.createAndUpdateQueueData(brokerName, entry.getValue());
                        }
                    }
                }
            }
            // 将存活的broker添加到brokerLiveTable结构中,操作的是 brokerLiveTable
            BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                new BrokerLiveInfo(
                    System.currentTimeMillis(),
                    topicConfigWrapper.getDataVersion(),
                    channel,
                    haServerAddr));
            ...
        } finally {
            this.lock.writeLock().unlock();
        }
    } catch (Exception e) {
        log.error("registerBroker Exception", e);
    }

    return result;
}

这样一来,这个方法所做的工作就一目了然了,就是把broker上报的信息包装下,然后放到这几个hashMap中。

了解完成注册操作后,注销操作就不难理解了,它是跟注册相反的操作,所做的事就是从这几个hashMap中移除broker对应的信息,处理方法为RouteInfoManager#unregisterBroker,代码中确实是进行hashMap移除的相关操作,这里就不分析了。

3 根据topic获取路由信息

producerconsumer启动时,都需要根据topicNameServer获取对应的路由信息,处理消息的方法为DefaultRequestProcessor#getRouteInfoByTopic

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader = (GetRouteInfoRequestHeader) request
            .decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);

    // 获取topic信息
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager()
            .pickupTopicRouteData(requestHeader.getTopic());
    // 省略其他
    ...
}

这个方法里调用了RouteInfoManager#pickupTopicRouteData方法,又是RouteInfoManager,这里我们就可以想象到,获取topic路由信息的操作,大致又是操作RouteInfoManager中的几个HashMap了:

 public TopicRouteData pickupTopicRouteData(final String topic) {
        ...
        try {
            try {
                this.lock.readLock().lockInterruptibly();
                // 操作的是 topicQueueTable
                List<QueueData> queueDataList = this.topicQueueTable.get(topic);
                if (queueDataList != null) {
                    ...
                }
            } finally {
                this.lock.readLock().unlock();
            }
        } catch (Exception e) {
            ...
        }

        ...
    }

从代码中来看,操作的是topicQueueTable,根据topictopicQueueTable中获取对应的queue数据后,剩下的无非就是对数据进行包装、过滤等,构造返回值,这里就不细讲了。

4. 查询broker版本信息

broker注册到nameServer前,会先发一个codeQUERY_DATA_VERSION的消息,判断版本号是否有变化再决定是否进行注册,处理该消息的代码为:

public RemotingCommand processRequest(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {

    ...

    switch (request.getCode()) {
        ...
        // 查询dataVersion
        case RequestCode.QUERY_DATA_VERSION:
            return queryBrokerTopicConfig(ctx, request);
        ...
    
    ...
}

进入DefaultRequestProcessor#queryBrokerTopicConfig方法,代码如下:

public RemotingCommand queryBrokerTopicConfig(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {
    ...
    
    // 关键代码:判断版本是否发生变化
    Boolean changed = this.namesrvController.getRouteInfoManager()
        .isBrokerTopicConfigChanged(requestHeader.getBrokerAddr(), dataVersion);
    if (!changed) {
        // 如果没改变,就更新最后一次的上报时间为当前时间
        this.namesrvController.getRouteInfoManager()
            .updateBrokerInfoUpdateTimestamp(requestHeader.getBrokerAddr());
    }

    DataVersion nameSeverDataVersion = this.namesrvController.getRouteInfoManager().
            queryBrokerTopicConfig(requestHeader.getBrokerAddr());
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);

    // 返回 nameServer当前的版本号
    if (nameSeverDataVersion != null) {
        response.setBody(nameSeverDataVersion.encode());
    }
    responseHeader.setChanged(changed);
    return response;
}

这个方法主要包含3个操作:

1. 判断broker版本是否发生变化

这部分的判断,就是判断上报的版本号与NameServer保存的版本号是否一致:

public boolean isBrokerTopicConfigChanged(final String brokerAddr, 
        final DataVersion dataVersion) {
    // 继续查询
    DataVersion prev = queryBrokerTopicConfig(brokerAddr);
    return null == prev || !prev.equals(dataVersion);
}

这个方法就是判断逻辑了,只是一个简单的equals操作,继续看DataVersion的查询:

public DataVersion queryBrokerTopicConfig(final String brokerAddr) {
    BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);
    if (prev != null) {
        return prev.getDataVersion();
    }
    return null;
}

又是对RouteInfoManager那几个hashMap的操作,最终是从brokerLiveTable获取到了NameServer保存的版本号。

2. 版本发生变化时的操作

如果版本没有发生变化,就更新当前时间为最新上报时间,这个流程没法啥好说的,直接上代码:

public void updateBrokerInfoUpdateTimestamp(final String brokerAddr) {
    BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);
    if (prev != null) {
        prev.setLastUpdateTimestamp(System.currentTimeMillis());
    }
}

又是对RouteInfoManager那几个hashMap的操作,这里需要注意的是,当DataVersion没有发生变化,才会更新BrokerLiveInfo#lastUpdateTimestamp成员变量的值为当前时间。

那么当DataVersion发生变化时,就不会更新BrokerLiveInfo#lastUpdateTimestamp的值了吗?并不是,如果DataVersion发生了变化,就表明broker需要再次注册,BrokerLiveInfo#lastUpdateTimestamp会在注册请求里被改变了。

3. 查询当前版本号

查询当前版本号,使用的方法是RouteInfoManager#queryBrokerTopicConfig,在上面的1. 判断broker版本是否发生变化中就用过了,这里就不再赘述了。

5. 定时任务:检测broker是否存活

在前面分析NamesrvController#initialize时,我们提到该方法启动了一个定时任务:

// 开启定时任务,每隔10s扫描一次broker,移除不活跃的broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    }
}, 5, 10, TimeUnit.SECONDS);

它调用的方法是RouteInfoManager#scanNotActiveBroker,代码如下:

public void scanNotActiveBroker() {
    // brokerLiveTable:存放活跃的broker,就是找出其中不活跃的,然后移除,操作的是 brokerLiveTable
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();
        // 上一次的心跳时间
        long last = next.getValue().getLastUpdateTimestamp();
        // 根据心跳时间判断是否存活,超时时间为2min
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            RemotingUtil.closeChannel(next.getValue().getChannel());
            // 移除
            it.remove();
            // 处理channel的关闭,这个方法里会处理其他 hashMap 的移除
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

这个方法先是遍历brokerLiveTable,然后判断每个BrokerLiveInfo的最近一次的上报时间,判断是否超时,如果最近的上报时间距离当前超过了2分钟,说明该broker可能挂了,就将它从brokerLiveTable移除,然后调用RouteInfoManager#onChannelDestroy方法,移除其他hashMapbroker.

6. 总结

本文分析了NameServer对请求消息的处理,nameServer底层使用netty进行通讯,处理brokerproducerconsumer请求消息的ChannelHandlerNettyServerHandler,最终的处理方法为DefaultRequestProcessor#processRequest,这个方法会处理众多的请求,我们重点分析了注册/注销broker消息获取topic路由消息获取broker版本信息的处理流程。

注册/注销broker消息获取topic路由消息获取broker版本信息最终都是在RouteInfoManager类中处理,这个类中有几个非常重要的、类型为HashMap的成员变量如下:

  1. topicQueueTable:存放保存topicQueue的关系,value类型为List,表明一个topic可以有多个queue
  2. brokerAddrTable:记录broker的具体信息,keybroker名称,valuebroker具体信息
  3. clusterAddrTable:集群信息,保存集群名称对应的brokerName
  4. brokerLiveTable:存活的broker信息,keybroker地址,value为具体的broker服务器

这个几成员变量就是NameServer被称为注册中心的原因所在,所谓的注册/注销broker,就是往这几个hashMapputremove相关的broker信息;获取topic路由消息就是从topicQueueTable中获取broker/messageQueue等信息。

nameServer所谓的"注册"、“发现”、“心跳”等,都是对RouteInfoManager这几个hashMap成员变量进行操作的。

好了,本文就到这里了,下篇开始我们将进入broker的分析。


限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,原文链接:mp.weixin.qq.com/s/VPFfsqp_H…

如果您喜欢本文,想了解更多源码分析文章(目前已完成spring/springboot/mybatis/tomcat的源码分析),欢迎关注该公众号,让我们一起在技术的世界里探秘吧!

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改