RocketMQ源码之注册中心NameServer【源码版本:RocketMQ4.6】

hello 大家好,我是还没毕业就有1年工作经验且爱抄中间件代码的路人丙,今天想跟大家分享一下笔者“哪些年 ,抄过的中间件代码”,以及阅读RocketMQ源码剖析之注册中心NameServer的笔记记录。

源码版本:RocketMQ4.6
笔者学习过程的注释:后面会把笔者有注释的地址贴出来

如果你跟我一样也是菜鸟程序员,那一定要看这篇文章!
如果你跟我一样也是菜鸟程序员,那一定要看这篇文章!
如果你跟我一样也是菜鸟程序员,那一定要看这篇文章!

熟悉RocketMQ的小伙伴,都知道NameServer 是MQ的大脑,注册中心;了解注册中心的小伙伴都知道,注册中心有三大基本能力:服务发现、服务注册、元信息路由管理,本篇文章主要记录了笔者通过源码来剖析NameServer中的优与势,以及值得借鉴的编程经验分享

2.1 nameServer启动流程

老规矩,我们先找到程序的入口:相信聪明人已经找到了,代码位置:NamesrvStartup#main()

接下来给大家展示一下nameServer启动大概的流程图:

nameServer启动流程图

这个图大概已经画出了nameServer启动大概会做的3件事:

  • (1)解析命令行以及配置文件,完成配置类的初始化
  • (2)初始化nettyServer并启动端口9876
  • (3)启动2大定时任务:心跳包扫描和打印配置信息

nameServer启动流程的源码比较简单,感兴趣的话,可以跟着图以及源码debug看一下具体的一些细节

笔者觉得比较感兴趣的点如下

  • netty Server构造的编码,比如epoll 和 nio 适配,reactor 反应模型
  • netty公共共享业务处理器抽取:比如编码、连接管理以及具体的请求处理

笔者任务,rpc请求处理写的比较好的原因如下:它把每一个请求都会封装成一个异步任务,并交给对应业务的线程池去执行,这样做法有3个好处:

  • 第一个:采用异步方式,系统的吞吐量会比较好
  • 第二个:采用线程池跟业务绑定同时业务之间隔离,可以方便更灵活的分配和使用资源
  • 第三个:在公司从来没看到过类似的代码,遇到对吞吐量有要求的场景可以抄

这一块其实属于RocketMQ的rpc的内容,代码写的很nice,借鉴意义也比较大,总之比笔者自己写的rpc好太多了(苦笑!!!)

NettyRemotingAbstract

    public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {

        //根据RemotingCommand中的code获取processor和ExecutorService
        final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
        final Pair<NettyRequestProcessor, ExecutorService> pair = null == matched ? this.defaultRequestProcessor : matched;

        //opaque 对应于request ID
        //RemotingCommand会为每一个request产生一个request ID, 从0开始, 每次加1

        final int opaque = cmd.getOpaque();

        if (pair != null) {

            //构造异步任务的执行目标target
            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {

                        //before rpc hook
                        doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);

                        //processor处理请求
                        final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd);

                        //after rpc hook
                        doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);

                        if (!cmd.isOnewayRPC()) {
                            if (response != null) {
                                response.setOpaque(opaque);
                                response.markResponseType();
                                try {
                                    //写入结果
                                    ctx.writeAndFlush(response);
                                } catch (Throwable e) {
                                    log.error("process request over, but response failed", e);
                                    log.error(cmd.toString());
                                    log.error(response.toString());
                                }
                            } else {

                            }
                        }
                    } catch (Throwable e) {
                        log.error("process request exception", e);
                        log.error(cmd.toString());

                        if (!cmd.isOnewayRPC()) {
                            final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR,
                                    RemotingHelper.exceptionSimpleDesc(e));
                            response.setOpaque(opaque);
                            ctx.writeAndFlush(response);
                        }
                    }
                }
            };

            if (pair.getObject1().rejectRequest()) {
                final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                        "[REJECTREQUEST]system busy, start flow control for a while");
                response.setOpaque(opaque);
                ctx.writeAndFlush(response);
                return;
            }

            try {

                //使用异步任务执行目标target,封装RequestTask
                final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);

                //提交任务到线程池
                pair.getObject2().submit(requestTask);
            } catch (RejectedExecutionException e) {
                if ((System.currentTimeMillis() % 10000) == 0) {
                    log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel())
                            + ", too many requests and system thread pool busy, RejectedExecutionException "
                            + pair.getObject2().toString()
                            + " request code: " + cmd.getCode());
                }

                if (!cmd.isOnewayRPC()) {
                    final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                            "[OVERLOAD]system busy, start flow control for a while");
                    response.setOpaque(opaque);
                    ctx.writeAndFlush(response);
                }
            }
        } else {
            String error = " request type " + cmd.getCode() + " not supported";
            final RemotingCommand response =
                    RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error);
            response.setOpaque(opaque);
            ctx.writeAndFlush(response);
            log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + error);
        }
    }

nameServer的启动流程到这里就结束了,比较简单,其中认为有学习价值的就是其rpc,netty这块的代码可以学习借鉴(抄抄抄)。

2.2 broker元数据注册流程

笔者阅读了broker元数据注册的源码之后,发现注册主要有2个场景:初始化时注册和周期性注册

关于broker注册的元信息有哪些?等后面再分享,这一小节只分析broker注册元信息的流程和相关的一些细节

至于broker的启动流程,就不给大家画了,类似nameServer启动流程

首先还是把源码入口给大家找出来:

BrokerController#start

    public void start() throws Exception {
      / 。。。省略无关代码。。。/

        if (!messageStoreConfig.isEnableDLegerCommitLog()) {
            startProcessorByHa(messageStoreConfig.getBrokerRole());
            handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
            //(1)初始化时注册
            this.registerBrokerAll(true, false, true);
        }
        // (2)周期性心跳包上报自己的元信息:周期的 范围为10 -60s,不配置默认是30秒
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                  //registerBrokerAll 注册的核心方法
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);

        //默认30s
      / 。。。省略无关代码。。。/
    }

这上面的代码主要展示了注册的2个时机:分别是if条件满足的代码(1)以及周期调度(2)

核心方法主要在registerBrokerAll()

接下来简单的画一下broker端注册大概的流程图:
broker注册流程

相对比较简单,这个流程中有一处的代码写的比较好,推荐大家抄起来:(笔者 优化百万级数据Excel文件导出接口 就是抄的这块代码:)

 final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
/...省略代码../
final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
            //(1)遍历 nameServerAddressList 列表
            // 分别向NameServer注册
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() { // 封装成runnable任务
                        try {
                            // (2) 发起元信息注册RPC请求
                            // 这里肯定是发起RPC请求 向NameServer 注册 自己的元信息   rpc核心细节
                            RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                            if (result != null) {
                                // 可能有低并发问题
                                // 但是为什么没有保证其线程安全呢?
                                registerBrokerResultList.add(result);
                            }

                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            // 可以学习的点,放在finally代码块中
                            countDownLatch.countDown();
                        }
                    }
                });
            }

            try {
                // 默认6s超时  这些值都可以压测估算出来
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }

笔者为什么推荐抄这块代码呢?主要有以下3个原因:

  • 笔者在实际业务做接口优化的时候,借鉴过这块代码,落地效果很显著
  • 依稀记得之前美团技术博客也有一篇线程池相关的,也应用了类似这块代码的方案做业务链路rt的优化
  • 你可以把 并发基础知识应用到实践

笔者在公司这么久,从来没见过类似的代码(可能是因为笔者的公司太小了)

这块代码主要应用线程池和countDownLatch工具类实现了异步并发,这样可以很好的提高接口的吞吐量

除此之外,这个流程中还比较关键的就是RemotingCommand类中封装的code =RequestCode.REGISTER_BROKER

RequestCode类(rpc 对应的业务处理器编码)

    public static final int REGISTER_BROKER = 103; // 注册broker

在RocketMQ中rpc请求的RequestCode决定了服务端如何处理这个请求

2.3 nameServer元数据接受流程

上面小结,笔者跟着源码研究了一下broker注册的2大时机,分析了一下大概的流程,其中也分享了笔者认为值得借鉴和抄的代码,接下来,我们就看一下nameServer的netty服务怎么处理code为REGISTER_BROKER的请求

首先我们还是先找到nameServer接受broker注册请求的入口,大家还记的前面分析nameServer启动流程中会起一个nettyServer呢?没错,了解netty的小伙伴,应该都知道业务逻辑都写在业务处理器中:DefaultRequestProcessor

代码入口:

NamesrvController#initialize()-》#registerProcessor()

    private void registerProcessor() {
        if (namesrvConfig.isClusterTest()) {

            this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()),
                this.remotingExecutor);
        } else {
            // 注册默认的通信处理器 核心方法:processRequest(),依据协定的code做相应的业务处理,code位于RequestCode
            // 看到这里就明白了,其实如果我们想看nameServer的功能,我们直接看DefaultRequestProcessor 处理器
            this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);// 注册默认处理器,并绑定remotingExecutor线程池
        }
    }

所以我们可以直接去DefaultRequestProcessor中找:如何处理broker的注册请求
请添加图片描述

DefaultRequestProcessor 实现了 NettyRequestProcessor接口,直接进NettyRequestProcessor

public interface NettyRequestProcessor {
  // 处理请求 定义
    RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)
        throws Exception;
//拒绝请求 定义
    boolean rejectRequest();
}

我们直接看核心代码:processRequest()

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

        if (ctx != null) {
            log.debug("receive request, {} {} {}",
                request.getCode(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                request);
        }

        //看到这里其实大概也能明白为什么 RocketMQ-admin为什么只用配置NameServer集群地址即可
        switch (request.getCode()) {
            // 配置k v 配置
            case RequestCode.PUT_KV_CONFIG:
                return this.putKVConfig(ctx, request);
                // 获取k v 配置
            case RequestCode.GET_KV_CONFIG:
                return this.getKVConfig(ctx, request);
                // 删除 k v 配置
            case RequestCode.DELETE_KV_CONFIG:
                return this.deleteKVConfig(ctx, request);
                // 获取数据版本
            case RequestCode.QUERY_DATA_VERSION:
                return queryBrokerTopicConfig(ctx, request);

                //注册元数据  找到REGISTER_BROKER的code了
            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);
                }
                // 移除元数据
            case RequestCode.UNREGISTER_BROKER:
                return this.unregisterBroker(ctx, request);

                //通过topic获取路由信息
            case RequestCode.GET_ROUTEINTO_BY_TOPIC:
                return this.getRouteInfoByTopic(ctx, request);
                // 获取broker集群信息
            case RequestCode.GET_BROKER_CLUSTER_INFO:
                return this.getBrokerClusterInfo(ctx, request);
            case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
                return this.wipeWritePermOfBroker(ctx, request);
                // 获取所有TOPIC_LIST
            case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
                return getAllTopicListFromNameserver(ctx, request);
                // 删除topic
            case RequestCode.DELETE_TOPIC_IN_NAMESRV:
                return deleteTopicInNamesrv(ctx, request);
            case RequestCode.GET_KVLIST_BY_NAMESPACE:
                return this.getKVListByNamespace(ctx, request);
                // 根据集群获取topic
            case RequestCode.GET_TOPICS_BY_CLUSTER:
                return this.getTopicsByCluster(ctx, request);
            case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
                return this.getSystemTopicListFromNs(ctx, request);
            case RequestCode.GET_UNIT_TOPIC_LIST:
                return this.getUnitTopicList(ctx, request);
            case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
                return this.getHasUnitSubTopicList(ctx, request);
            case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
                return this.getHasUnitSubUnUnitTopicList(ctx, request);
            case RequestCode.UPDATE_NAMESRV_CONFIG:
                return this.updateConfig(ctx, request);
            case RequestCode.GET_NAMESRV_CONFIG:
                return this.getConfig(ctx, request);
            default:
                break;
        }
        return null;
    }

看到Switch没,关键就是RequestCode,我们找到前面讲到broker发送的code REGISTER_BROKER

                //注册元数据  找到REGISTER_BROKER的code了
            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);
                }

看到这个Switch,是不是也理解为什么RocketMQ Admin 只需要配置nameServer的地址就是实现扩展了吧

接下来就不给大家贴代码了,直接上图吧!
nameServer处理broker注册请求源码流程

看到这里应该清楚了,原来nameServer的核心都在这个RouteInfoManager类里

接下来就详细的分析一下broker元数据管理类RouteInfoManager的源码

2.4 核心类分析:元数据管理类

上面一节主要分析了,nameServer是如何接收broker的注册请求并发现其核心就是RouteInfoManager类在维护,所以我们接下来就详细的看一下RouteInfoManager的源码

2.4.1RouteInfoManager的核心属性极其含义
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
//    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 20;
// (1) broker 心跳包最大的间隔时间
    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; // nameserver 与 broker的空闲时长
// (2)  读写锁   负责维护下面几个map的读写安全
    private final ReadWriteLock lock = new ReentrantReadWriteLock(); // 读写锁
 // (3) key : broker名(broker配置文件中可配置),value:broker元信息    主从元信息
    //核心Map: 集群影射: brokerName到Master/Slave机器列表
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable; // brokerName  和 broker元信息的映射

	// (4) key:topic (生产者生产时设置的topic) value: topic的分区消息  topic 与 队列关系,记录同一个主题的消息分布在哪些broker上
    //核心Map之1: topic到QueueData ,topic的分区消息或者消费队列信息
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable; 

// (5) key : 集群名     value:broker名(一般一个broker名都至少是一主一从)
    //核心Map之3: 集群名称,到集群节点映射  : 1个cluster多个broker
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;// 每个集群包含哪些broker
// (6)broker 注册的心跳包元信息  (前面已经源码分析过broker注册的流程了)
    //活跃Broker映射表,NameServer每次收到心跳包是会替换该信息
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable; // 当前活跃的broker,不是实时存在至少120s延迟:存在的问题,假死继续接受消息,造成消息发送失败
// (7)笔者暂时不清楚
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

RouteInfoManager核心属性的注释,都写在上面了,关于RouteInfoManager的方法基本上主要负责维护上面提到的(1)~(6)个map的维护,主要使用读写锁ReentrantReadWriteLock保证线程安全。

为什么使用读写锁呢?笔者认为:

注册中心,典型的读多写少的场景,使用ReentrantReadWriteLock能比较好的发挥读并发性能,所以如果你之前在生产环境没用过ReentrantReadWriteLock,可以参考这部分的代码,在类似的业务场景尝试使用读写锁提高程序的整体吞吐量,笔者也是参考了这块代码,才比较有信心在类似业务场景中使用

2.5 broker主动剔除场景(或者说 broker主动下线)

前面,已经提到broker主要通过初始时以及周期性定时2个时机向nameServer集群发送自己的心跳信息,称为注册;那么大家肯定和笔者猜到了,他肯定还有一个不注册的功能,熟悉注册中心的小伙伴,可能说:这不就是注册中心的基本更能嘛,确实如此哈。

接下来就带大家看一下入口:不知道大家还记不记得nameServer的启动流程有一个jvm的钩子函数,专业点:叫优雅的关闭,没错broker启动的时候也有这样的代码:

BrokerStartup#createBrokerController()

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                private volatile boolean hasShutdown = false;
                private AtomicInteger shutdownTimes = new AtomicInteger(0);

                @Override
                public void run() {
                    synchronized (this) {
                        log.info("Shutdown hook was invoked, {}", this.shutdownTimes.incrementAndGet());
                        if (!this.hasShutdown) {
                            this.hasShutdown = true;
                            long beginTime = System.currentTimeMillis();
                          // (1)关闭
                            controller.shutdown();
                            long consumingTimeTotal = System.currentTimeMillis() - beginTime;
                            log.info("Shutdown hook over, consuming total time(ms): {}", consumingTimeTotal);
                        }
                    }
                }
            }, "ShutdownHook"));

接下来就不画图了,简单给大家标一下调用方法,因为这个流程跟注册流程差不太多

 controller.shutdown();   //BrokerController
-》
this.unregisterBrokerAll();   //BrokerController
-》
unregisterBrokerAll()  //BrokerOuterAPI
-》
unregisterBroker()   //BrokerOuterAPI
-》
invokeSync()  //RemotingClient   netty client   封装的code RequestCode.UNREGISTER_BROKER

接下来就算不看源码,大家其实也能猜到最后肯定会调到RouteInfoManager的某个方法去维护上一节提到几个map集合,秉着学习的态度,我们还是简单看一下

承接上面梳理的入口,简单给大家标记一下nameServer是如何处理 broker下线的:

DefaultRequestProcessor

// broker主动下线入口
    public RemotingCommand unregisterBroker(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        final UnRegisterBrokerRequestHeader requestHeader =
            (UnRegisterBrokerRequestHeader) request.decodeCommandCustomHeader(UnRegisterBrokerRequestHeader.class);
		// (1)委托给RouteInfoManager
        this.namesrvController.getRouteInfoManager().unregisterBroker(
            requestHeader.getClusterName(),
            requestHeader.getBrokerAddr(),
            requestHeader.getBrokerName(),
            requestHeader.getBrokerId());

        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }

DefaultRequestProcessor这个类在前面已经讲过了,在初始化nameServer的netty Server时注册的默认处理器,主要负责基本的rpc code处理

那我们简单看一下,RouteInfoManager是如何处理broker主动下线的:

 public void unregisterBroker(
            final String clusterName,
            final String brokerAddr,
            final String brokerName,
            final long brokerId) {
        try {
            try {
                this.lock.writeLock().lockInterruptibly();
                //(1) 移除心跳包
                BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr);
                log.info("unregisterBroker, remove from brokerLiveTable {}, {}",
                        brokerLiveInfo != null ? "OK" : "Failed",
                        brokerAddr
                );

                this.filterServerTable.remove(brokerAddr);

                boolean removeBrokerName = false;
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                // (2)从BrokerData移除自己的addr 
                if (null != brokerData) {
                    String addr = brokerData.getBrokerAddrs().remove(brokerId);
                    log.info("unregisterBroker, remove addr from brokerAddrTable {}, {}",
                            addr != null ? "OK" : "Failed",
                            brokerAddr
                    );

                    if (brokerData.getBrokerAddrs().isEmpty()) {
                        // 如果主从集群都不在了,移除这个BrokerData集群信息
                        this.brokerAddrTable.remove(brokerName);
                        log.info("unregisterBroker, remove name from brokerAddrTable OK, {}",
                                brokerName
                        );

                        removeBrokerName = true;
                    }
                }

                if (removeBrokerName) {
                    // (3) 从集群映射信息中移除broker
                    Set<String> nameSet = this.clusterAddrTable.get(clusterName);
                    if (nameSet != null) {
                        boolean removed = nameSet.remove(brokerName);
                        log.info("unregisterBroker, remove name from clusterAddrTable {}, {}",
                                removed ? "OK" : "Failed",
                                brokerName);

                        if (nameSet.isEmpty()) {
                            this.clusterAddrTable.remove(clusterName);
                            log.info("unregisterBroker, remove cluster from clusterAddrTable {}",
                                    clusterName
                            );
                        }
                    }
                    // (4)移除 broker相关的topic信息
                    this.removeTopicByBrokerName(brokerName);
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("unregisterBroker Exception", e);
        }
    }

以上代码,大概做的事情:先拿锁,然后把自己的broker信息从nameServer上移除

2.6 被动剔除路由场景(broker被动下线)

在分析nameServer启动流程的时候,我们就知道,nameServer注册了一个定时器,定时扫描自己维护的brokerLiveTable 心跳表,那么我们就跟着源码,看看都做了些什么事情?

代码入口:

// 定时调度任务        
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                // 委托给routeInfoManager
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);



    
public void scanNotActiveBroker() {
        //获取broker的live信息表
        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
        //1 遍历存储所有存活broker的表
        while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();

            //如果收到心跳包的时间距当时时间是否超过120s
            //3 broker心跳间隔是否超过120s
            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
                //3.1、关闭长连接
                RemotingUtil.closeChannel(next.getValue().getChannel());
                //3.2、移除当前broker在心跳表维护信息
                it.remove();
                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
                //3.3、维护路由表
                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
            }
        }
    }

推测清除broker相关信息的方法肯定在onChannelDestroy(),这个方法代码有点长,就不贴出来了,大概的逻辑类似于:前面说的broker主动下线的unregisterBroker()方法,先拿锁,然后清除心跳超时的broker相关信息;

提示:大家如果想看这块的细节的话,可以debug模拟心跳包过期场景来看

2.7 nameServer服务发现(难点)

笔者自己理解服务发现:其实服务发现主要是指服务调用方,以什么样的方式能够找到服务提供者

nameServer的服务发现功能主要提供给消费者和生产者使用:简单来说,就是消费者和生产者通过nameServer就知道有哪些broker可以提供服务,本质上就是生产者或者消费者怎样才能知道服务提供者broker相关的元信息

接下来,就简单研究一下RocketMQ中的服务发现是怎么实现的?

首先我们先从角色的维度来看一下:

  • 生产者

首先我先跟着代码找到了生产者的send api,我直接把入口标出来:

DefaultMQProducerImpl#sendDefaultImpl

//step2 查找路由, 找元数据
       
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());

    //获取topic对应的路由信息
    //查找路由
    private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {

        //优先搞定本地缓存:从缓存中获得主题的路由信息
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);

        //路由信息为空,则从NameServer获取路由
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            //则从NameServer获取路由表
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);

        }

        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
            return topicPublishInfo;
        } else {
            //如果未找到当前主题的路由信息,则用默认主题继续查找
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

最后我们会发现对应的rpc code 为RequestCode.GET_ROUTEINTO_BY_TOPIC

关于rpc处理的细节就不多说了

我们直接看核心方法:updateTopicRouteInfoFromNameServer()

    /** 更新路由表的具体方法 */
    public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
        DefaultMQProducer defaultMQProducer) {
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                //锁了 3秒
                try {
                    TopicRouteData topicRouteData;
                    //使用默认主题从NameServer获取路由信息
                    if (isDefault && defaultMQProducer != null) {
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                            1000 * 3);
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                                data.setReadQueueNums(queueNums);
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    } else {
                        //(1)使用指定主题从NameServer获取路由信息    和nameServer通信查询主题对应的路由元信息   这里就是发起rpc请求到 nameServer
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
                    if (topicRouteData != null) {
                        //(2)这里判断路由是否被更改
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        } else {
                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
                        }

                        if (changed) {
                            // 2.1 如果被更改了,怎么办
                            // 1) 复制一份路由信息
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
                            // 2) 更新broker地址
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }

                            // Update Pub info
                            {
                                //3)将topicRouteData路由 转换为 生产路由信息publishInfo
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                                publishInfo.setHaveTopicRouterInfo(true);
                                //遍历 生产者
                                //4)把塞给了每一个生产者的topicPublishInfoTable属性  (可以debug看一下,具体是一些什么信息)
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }

                            // Update sub info
                            //将topicRouteData路由 转换为 消费路由信息
                            {
                                //5) 把topicRouteData 路由信息转换成了MessageQueue
                                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) {
                                        // 6)塞给了每一个消费者的subscriptionInner属性(subscriptionInner并不直接是消费者的属性,但可以这么理解)
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
                            // 7) 更新了 topicRouteTable
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    } else {
                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);
                    }
                } catch (MQClientException e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);
                    }
                } catch (RemotingException e) {
                    log.error("updateTopicRouteInfoFromNameServer Exception", e);
                    throw new IllegalStateException(e);
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS);
            }
        } catch (InterruptedException e) {
            log.warn("updateTopicRouteInfoFromNameServer Exception", e);
        }

        return false;
    }

简单看下来这个核心方法其实主要做了5件事情:

  • (1)通过rpc code 为GET_ROUTEINTO_BY_TOPIC 获取指定topic的路由信息
  • (2)通过路由信息更新了 broker缓存地址brokerAddrTable
  • (3)通过路由信息获取TopicPublishInfo实列,并塞给了每一个生产者
  • (4)通过路由信息获取MessageQueue实列集合。并塞给了每一个消费者
  • (5)克隆了rpc获取的topic的路由信息,并更新了路由信息缓存table,topicRouteTable
    虽然我们把这个方法的核心梳理清楚了,但是其实我们目前并不知道为什么要这么做?
    其实要明白为什么这么做,我们就得弄清楚其中几个关键问题?

rpc获取的路由信息到底有些什么信息?

塞给生产者的元信息是什么?

塞给消费者的元信息是什么?

可能其他小伙伴跟笔者有一样的疑问

为什么他们要分开设计?

在分析上面的问题之前,我们先说一下另外一个定时场景的入口:

MQClientInstance#startScheduledTask()

        //关键代码
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            //每隔30S尝试更新主题路由信息
            @Override
            public void run() {
                try {
                  // 前面我们已经分析过的核心方法
                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();
                } catch (Exception e) {
                    log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
                }
            }
        }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

说明MQClientInstance实列调用start()方法的时候也会初始化定时任务去定时拉取broker元信息(默认每隔30s,可以设置)

接下来,我们就debug一下,看看消费者、生产者各自维护的元信息都有哪些。

2.7.1 debug 生产者源码 (服务发现相关)
  • rpc获取元信息

请添加图片描述
请添加图片描述
请添加图片描述

  • 生产者元信息

请添加图片描述

  • 消费者元信息

请添加图片描述

请添加图片描述

上面debug的前提如下:
请添加图片描述

所以经过我们的debug之后,我们大概知道生产者、消费者以及rpc发起请求获取的元信息有哪些内容

rpc获取的元信息:

TopicRouteData

    private String orderTopicConf; // 暂不清楚

    //所有的分区信息   元素:topic下某个broker队列相关元数据。
    private List<QueueData> queueDatas;
    //所有的 broker 集群   元素:代表该topic 某一个集群中的broker元数据;比如id、ip地址等
    private List<BrokerData> brokerDatas;
    // 暂不清楚
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable; 

QueueData

    // broker 组的名称
    private String brokerName;
    // 读队列数量
    private int readQueueNums;
    // 写队列数量
    private int writeQueueNums;
    //队列的读写权限  位运算来判断的
      //perm=4 #可读
      //perm=2 #可写
    private int perm;
    //队列的同步标志 同步复制还是异步复制
    private int topicSynFlag;

生产者获取的元信息:

TopicPublishInfo

    //是否是顺序主题,用于发送顺序消息
    private boolean orderTopic = false;
    private boolean haveTopicRouterInfo = false;
    //一个topic下面的所有的队列 且只要主节点broker的
	// 通过rpc元信息当中的QueueData的writeQueueNums属性构造的
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();

    //round-robin负载均衡,每选择一次消息队列,该值+1      每选择一次消息队列,该值会自增1,如果超过
    //Integer.MAX_VALUE,则重置为0,用于选择消息队列。 
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();

    //关联Topic路由元信息   前面提到的 rpc获取的元信息
    private TopicRouteData topicRouteData;

详细的初始化逻辑可见:MQClientInstance#topicRouteData2TopicPublishInfo

消费者者获取的元信息:

MessageQueue集合:只要可读权限的

    //主题
    private String topic;
    //broker 名称
    private String brokerName;

    //队列的id,从 0开始的
    private int queueId;

看到这里终于没那么糊涂了,总算对消息队列之间的元信息有一些了解了

2.7.2 简单小结一下:

(1)从角色的角度:

服务发现的能力主要为生产者和消费者提供 broker元信息的能力

生产者和消费者对元信息的关注度不同,生产者更关注于broker的写队列设置,而消费者更关注具有可读权限的读队列设置,共同点就是他们都需要topic、queueId、brokerName等基础元信息

(2)从场景来看:

主要有主动和定时2个触发场景

主动场景:生产者调用发送send api的时候

被动场景:生产者和消费者都有通过定时任务来触发,默认30s

(3)从范围来看

主要指定topic下的broker元信息为主

2.8 nameserver选择策略

上面我们已经分析服务发现的核心流程,大概了解到RockeMQ服务发现的机制大致可以分为角色、场景、范围3个维度,接下来我们就跟着源码分析一下,它是如何在nameServer集群中选择了其中一个

代码核心入口:

NettyRemotingClient#invokeSync()

    public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
        throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
        long beginStartTime = System.currentTimeMillis();
      // (1)核心方法获取 nameServer长连接
        final Channel channel = this.getAndCreateChannel(addr);//长连接
        if (channel != null && channel.isActive()) {
            try {
                doBeforeRpcHooks(addr, request); // rpc 前置钩子函数
                long costTime = System.currentTimeMillis() - beginStartTime;
                if (timeoutMillis < costTime) {
                    throw new RemotingTimeoutException("invokeSync call timeout");
                }
                //netty发送消息核心
                RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);// rpc 后置钩子函数
                return response;
            } catch (RemotingSendRequestException e) {
                log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
                this.closeChannel(addr, channel);
                throw e;
            } catch (RemotingTimeoutException e) {
                if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
                    this.closeChannel(addr, channel);
                    log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
                }
                log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
                throw e;
            }
        } else {
            this.closeChannel(addr, channel);
            throw new RemotingConnectException(addr);
        }
    }

我们直接进入getAndCreateChannel方法

    // 有连接就复用,无就创建(真正创建时会加锁)
    private Channel getAndCreateChannel(final String addr) throws RemotingConnectException, InterruptedException {
        if (null == addr) {
          //核心
            return getAndCreateNameserverChannel();
        }
        // 从长连接池中获取一个长连接(而且还是包装过的长链接)
        ChannelWrapper cw = this.channelTables.get(addr);
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }
        // 根据addr建立一个新的连接
        return this.createChannel(addr);
    }

继续跟一下

private Channel getAndCreateNameserverChannel() throws RemotingConnectException, InterruptedException {
        //优先选择这个NameServer namesrvAddrChoosed是一个原子引用
    String addr = this.namesrvAddrChoosed.get();
    if (addr != null) {
        ChannelWrapper cw = this.channelTables.get(addr);
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }
    }
	// 通过下面的代码,我们知道主要通过随机+轮训的方式获取到address,然后通过netty的Bootstrap的connect()方法建立新的长链接
  //核心代码
    final List<String> addrList = this.namesrvAddrList.get();
    if (this.lockNamesrvChannel.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
        try {
            addr = this.namesrvAddrChoosed.get();
            if (addr != null) {
                ChannelWrapper cw = this.channelTables.get(addr);
                if (cw != null && cw.isOK()) {
                    return cw.getChannel();
                }
            }

            if (addrList != null && !addrList.isEmpty()) {
                for (int i = 0; i < addrList.size(); i++) {

                    //除非与这个 namesrvAddrChoosed 节点通信出现异常的情况下,才会选择其他节点			// namesrvIndex初始化的时候采用随机
                    int index = this.namesrvIndex.incrementAndGet();
                    index = Math.abs(index);
                    index = index % addrList.size();
                    String newAddr = addrList.get(index);

                    this.namesrvAddrChoosed.set(newAddr);
                    log.info("new name server is chosen. OLD: {} , NEW: {}. namesrvIndex = {}", addr, newAddr, namesrvIndex);
                    Channel channelNew = this.createChannel(newAddr);
                    if (channelNew != null) {
                        return channelNew;
                    }
                }
                throw new RemotingConnectException(addrList.toString());
            }
        } finally {
            this.lockNamesrvChannel.unlock();
        }
    } else {
        log.warn("getAndCreateNameserverChannel: try to lock name server, but timeout, {}ms", LOCK_TIMEOUT_MILLIS);
    }

    return null;
}

通过上面的核心代码,我们知道RockertMQ在做nameServer选择的时候,主要通过随机+轮训的方式获取到address,然后通过netty的Bootstrap的connect()方法建立新的长链接

虽然采用随机+轮训,前提也是之前的长链接不可用或者之前的nameserver挂掉了,不然会继续复用上一次的长链接

2.9 谈谈对阅读nameServer的6大收获
  • 基本了解nameServer和broker、生产者、消费者之间的交互流程,知道rpc相关的业务都有唯一的rpc code,且实现逻辑在默认的处理器DefaultProcesser类里可以看到
  • 基本了解nameServer的启动流程以及,以及broker元信息维护
  • 基本了解nameServer的提供的服务发现功能:2个场景,针对特定topic定时 +主动
  • 基本了解nameServer的提供的服务注册功能:2个场景,针对特定topic定时 +主动
  • 基本了解nameServer关于并发工具的实践应用:读写锁应用场景、countDownLatch + 异步优化并发rpc请求
  • 基本了解nameServer 对于netty的实践应用:比如通过map来实现不同rpc业务处理器和对应线程池的绑定和业务隔离

通过源码学习,笔者终于对nameServer有一个比较全面的了解了,而且发现nameServer之间是互不通信的,并且broker元信息是通过心跳包维护的,所以在某些时刻nameServer集群中的某些节点的broker元数据可能是不同的,但是最终都会一致,所以nameServer并不是数据强一致性的,而是弱一致性,但是nameServer是支持高可用的,同时也在nameServer这块的源码了解到基础并发工具的运用,比如读写锁应用场景、countDownLatch + 异步优化并发rpc请求、jvm钩子函数优化关闭,其中也了解到一些netty相关的应用实践:比如通过map来实现不同rpc业务处理器和对应线程池的绑定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值