2.RocketMQ - NameServer源码解析

193 阅读7分钟

一、注册中心的作用

提到MQ,大家本能反应就会想到几个角色,生产者、消费者、服务端。

MQ的设计思路一般都是基于订阅发布机制,生产者将消息发送到某个TOPIC下,消息的服务端负责消息的持久化,消费者需要订阅自己感兴趣的主题,从而拉取对应主题的消息,或者由消息服务器主动把消息推送给消费者

为了避免单点故障问题,通常我们都会部署多台消息服务器,那么消息生产者如何知道消息应该发给哪台机器呢?

注册中心就是为了解决上述问题而设计的。RocketMQ整体架构如下:

image.png

  • 每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。
  • Producer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态。
  • Consumer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息。

几款常见的注册中心比对

其实关于注册中心,已经有很多开源实现,例如Zookeeper、Etcd、Eureka、Nacos、Consul等,其忒行对比如下:

功能ZookeeperEtcdEurekaNacosConsul
服务健康检查长连接心跳可配置传输层和应用层健康检查服务状态、内存、硬盘等
多数据中心---支持-
KV存储支持支持-支持支持
一致性PaxosRaft-RaftRaft
CAPCPCPAP配置中心:CP 注册中心:APCP
使用方式客户端http/grpchttpdns/rpchttp/dns
自身监控-metricsmetrics-metrics

取舍

  • cp or ap
    • 应该选择AP,对于注册中心来说,可用性高于一致性,因此在做技术选型的时候更倾向于Eureka和Nacos
  • 技术体系
    • Etcd和Consul都是基于GO开发的,其他几个都是基于java开发的,因此不同的技术栈,可以选择更容易维护的注册中心
  • 活跃度
    • 在做技术选型的时候,一定要多关注社区的活跃度,毕竟,谁也不想用个没人维护的中间件,以上几款注册中心社区都很活跃

既然已经有了如此多不同的注册中心,那么RocketMQ为什么还要“重复造轮子”呢?别着急,继续往下看。

二、NameServer设计初衷

NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo或者kafka中的zookeeper,Spring Cloud 中的 Eureka,NameServer支持Broker的动态注册与发现。主要包括两个功能:

Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活。

路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。Producer在投递消息之前和Conumser在拉取消息之前,都会通过NameServer获取整个Broker集群的路由信息,从而进行消息的投递和消费。

NameServer通常也是集群的方式部署,每一个NameServer实例上面都保存一份完整的路由信息。

NameServer是无状态的。状态的有无实际上就是数据是否会做存储,有状态的话数据会被持久化,无状态的服务可以理解就是一个内存服务,NameServer本身也是一个内存服务,所有数据都存储在内存中,重启之后都会丢失。

RocketMQ的架构设计决定了只需要一个轻量级的元数据服务器就足够了,只需要保持最终一致,因此,RocketMQ决定使用自制的NameServer来实现简单的路由管理服务,将元数据存储在RocketMQ内部,设计很简单,非常的轻量级。

NameServer 集群间互不通信,因此它们之间的注册信息可能会不一致,这是一种去中心化的架构,所有注册信息保存在内存中(无状态),一台NameServer挂了也没关系,从另一台NameServer上拉取数据即可。

如果选择使用ZK或者其他的注册中心来作为实现,额外的增加了运维成本,可以参考Kafka,最新版本已经移除了对zk的依赖,毕竟,没人比自己更懂自己的需求。还有一个点就是ZK其实是CP模型,而RocketMQ自己实现的NameServer则为AP模型

三、NameServer架构设计

image.png

四、NameServer源码解读

4.1 启动流程

NameServer启动类是org.apache.rocketmq.namesrv.NamesrvStartup,核心方法是createNamesrvController方法,查看其实现如下:

    public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
        ... ...
        // 1.配置文件,填充NamesrvConfig与NettyServerConfig
        final NamesrvConfig namesrvConfig = new NamesrvConfig();
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        nettyServerConfig.setListenPort(9876);
        ... ...
        // 2.NamesrvController,并调用其initialize方法进行初始化
        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

        // remember all configs to prevent discard
        controller.getConfiguration().registerConfig(properties);

        return controller;
    }

NamesrvController-initialize方法实现如下:

    public boolean initialize() {
        // 加载kv配置
        this.kvConfigManager.load();
        // 创建NettyServer网格处理对象
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

        this.remotingExecutor =
            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

        this.registerProcessor();
        // 定时任务1: NameServer每隔10s扫描一次Broker,移除处于未激活状态的broker
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);
        // 定时任务2: NameServer每隔10s打印一次KV配置
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.kvConfigManager.printAllPeriodically();
            }
        }, 1, 10, TimeUnit.MINUTES);

        if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
            // Register a listener to reload SslContext
            ... ... 
        }

        return true;
    }

这里多提一句,NameServer其实也是使用Netty用来处理网络通信,Netty的使用姿势也是很规范,大家如果有用Netty实现相关业务的需求,可以参考NettyRemotingServer类实现(当然也可以翻一翻博主的Netty源码系列🐶x1)

最后则是注册JVM钩子函数并启动服务器

        // 注册钩子,在jvm进程关闭之前,将线程池关闭,及时释放资源
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));
        
        // 启动Netty服务器
        controller.start();

4.2 NameServer路由相关

  • 实现类

NameServer的路由信息实现类是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager,相关属性如下:

    // topic消息队列的路由信息
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
    // broker基础信息,包含brokerName,所属集群名称,主备broker地址
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
    // broker集群信息,存储集群中所有broker名称
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    // broker状态信息,NameServer每次收到心跳包时会替换改信息
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    // Broker上的FilterServer列表
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

这里注意的是一个topic会对应多个队列,类似kafka的partition,一个broker默认为每一个topic创建4个读队列4个写队列

  • 路由注册

路由注册是通过心跳实现的。Broker启动的时候会向进群中所有的NameServer发送心跳语句,每隔30s发送一次心跳包,NameServer收到心跳的时候会更新brokerLiveTab中的BrokerLiveInfo.lastUpdateTimerstamp,每隔离10s扫描一次brokerLiveTab,如果连续120s没有收到心跳包,则移除broker路由信息。

        // broker发送心跳包,通过registerBrokerAll方法
        // 最终调用的BrokerOuterAPI.registerBrokerAll方法,内部通过netty channel和NameServer通信,对netty感兴趣的同学可以自行研究
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 

NameServer处理心跳包,最终会调用RouteInfoManager.registerBroker方法,核心步骤如下

1.路由注册首先加锁,防止兵法修改
2.维护brokerData信息
3.如果broker是主节点并且由配置发生变更或者初次注册,则更新topic路由信息
4.更新BrokerLiveInfo
5.注册broker的过滤器server地址列表
6.释放锁
  • 路由删除 路由删除则是由RouteInfoManager.scanNotActiveBroker方法去实现的,NameServer每10s执行一次,便利brokerLiveInfo路由表,检测上次收到的心跳包的时间,如果超过120s则认为该broker已不可用,将它溢出并关闭连接。

  • 路由发现

Rocket路由发现是非实时的,当topic路由发现变化后,NameServer不会主动的推送给客户端,而是由客户端定时拉取实现类是org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor-getRouteInfoByTopic方法实现,也比较简单,大家可自行查看。

五、小节

本文主要给大家介绍了下RocketMQ的注册中心NameServer实现,同时对业界内常用的几款注册中心做了比对,分析了RocketMQ为什么自己实现一个注册中心而不是直接复用开源的组件。 NameServer的设计和实现相对简单,作为学习入门RocketMQ的切入点个人觉得是不错的,后面将逐渐带大家深入的学习RocketMQ各个方面的设计。