rocketmq基本架构

简介

Name server

  • 负责broker注册、心跳,路由等功能,类似Kafka的ZK
  • name server节点之间不互相通信,broker需要和所有name server进行通信。扩容name server需要重启broker,不然broker不会和name server建立连接
  • producer和consumer也都是和name server建立长连接,获取路由信息,拿到对应的broker信息,与broker建立长连接,然后发送/消费消息

路由发现

Pull的模式。当topic路由信息发生变化时,name server不回主动推送给客户端,而是客户端定时拉取。默认客户端每30秒会拉取一次最新的路由信息

扩展:

1)push模型:实时性好,但是需要维护一个长链接,消耗服务端资源。client数量不多,实时性要求高,server数据变化比较频繁的场景适合此种模式

2)pull模型:实时性差

3)long

polling模型:长轮询模式。客户端定时发送拉取请求,服务端会hold住连接一段时间,在此期间的数据变动通过此连接推送。超过hold时间后才断开连接。兼顾以上两种方式

Broker

  • broker每30s给name server发送一次心跳,name server每120s检查一次所有的broker心跳时间,超过阈值踢出broker
  • broker节点集群是主从集群,master负责处理读写请求,slave负责对master中的数据进行备份。master和slave有相同的broker name,但broker id不同,broker id为0的是master,非0的是slave。每个broker与name server集群中的所有节点建立长连接,定时注册topic信息到所有name server

源码分析

NameServer

NameServer的启动过程分析

NameServer服务器相关的源码在namesrv模块下,目录结构如下:

NamesrvStartup类就是Name Server服务器启动的启动类,NamesrvStartup类中有一个main启动类,main方法调用main0,main0主要流程代码

main0 方法的主要作用就是创建Name Server服务器的控制器,并且启动Name Server服务器的控制器。NamesrvController类的作用就是为Name Server服务的启动提供具体的逻辑实现,主要包括配置信息的加载、远程通信服务器的创建和加载、默认处理器的注册以及心跳检测机器监控Broker的健康状态等。Name Server服务器的控制器的创建方法为createNamesrvController方法,createNamesrvController方法的主要流程代码如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.NamesrvStartup#createNamesrvController
public static NamesrvController createNamesrvController(String[] args){

     //设置rocketMQ的版本信息,REMOTING_VERSION_KEY的值为:rocketmq.remoting.version,CURRENT_VERSION的值为:V4_7_0
    System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));

     //构建命令行,添加帮助命令和Name Server的提示命令,将createNamesrvController方法的args参数进行解析
     //代码省略

     //nameServer 服务器配置类和netty 服务器配置类
     final NamesrvConfig namesrvConfig = new NamesrvConfig();
     final NettyServerConfig nettyServerConfig = new NettyServerConfig();
     //设置netty服务器的监听端口
     nettyServerConfig.setListenPort(9876);

     // 判断上述构建的命令行是否有configFile(缩写为C)配置文件,如果有的话,则读取configFile配置文件的配置信息,
     // 并将转为NamesrvConfig和NettyServerConfig的配置信息
     // 代码省略


    // 如果构建的命令行存在字符'p',就打印所有的配置信息病区退出方法
    // 代码省略

    //首先将构建的命令行转换为Properties,然后将通过反射的方式将Properties的属性转换为namesrvConfig的配置项和配置值。
    MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

    //打印nameServer 服务器配置类和 netty 服务器配置类的配置信息
    MixAll.printObjectProperties(log, namesrvConfig);
    MixAll.printObjectProperties(log, nettyServerConfig);

     //将namesrvConfig和nettyServerConfig作为参数创建nameServer 服务器的控制器
      final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
      //将所有的配置保存在内存中(Properties)
      controller.getConfiguration().registerConfig(properties);
      return controller;
}

createNamesrvController方法主要做了几件事,读取和解析配置信息,包括Name Server服务的配置信息、Netty 服务器的配置信息、打印读取或者解析的配置信息、保存配置信息到本地文件中,以及根据namesrvConfig配置和nettyServerConfig配置作为参数创建nameServer 服务器的控制器。创建好Name server控制器以后,就可以启动它了。启动Name Server的方法的主流程如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.NamesrvStartup#start
public static NamesrvController start(final NamesrvController controller){
     //初始化nameserver 服务器,如果初始化失败则退出
      boolean initResult = controller.initialize();
       if (!initResult) {
           controller.shutdown();
           System.exit(-3);
       }
     //添加关闭的钩子,进行内存清理、对象销毁等惭怍
     Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

     //启动
     controller.start();
}

start方法没什么逻辑,主要作用就是进行初始化工作,然后进行启动Name Server控制器,接下来看看进行了哪些初始化工作以及如何启动Name Server的,初始化initialize方法的主要流程如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.NamesrvStartup#initialize
public boolean initialize() {
     // key-value 配置加载
     this.kvConfigManager.load();

     // //创建netty远程服务器,用来进行网络传输以及通信
     this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

      //远程服务器线程池
        this.remotingExecutor =
            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

       //注册处理器
       this.registerProcessor();

      //每10秒扫描不活跃的broker
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);

        //每10秒打印配置信息(key-value)
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

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

        //省略部分代码

        return true;

 }

initialize方法的主要逻辑如下:

加载配置文件。读取文件名为"user.home/namesrv/kvConfig.json"(其中user.home为用户的目录),然后将读取的文件内容转为KVConfigSerializeWrapper类,最后将所有的key-value保存在如下map中:

//用来保存不同命名空间的key-value private final HashMap<String/* Namespace /,
HashMap<String/
Key /, String/ Value */>> configTable = new

HashMap<String, HashMap<String, String>>();

  • 创建Netty服务器。Name Server 用netty与生产者、消费者以及Boker进行通信。
  • 注册处理器。这里主要注册的是默认的处理器DefaultRequestProcessor,注册的逻辑主要是初始化DefaultRequestProcessor并保存着,待需要使用的时候直接使用。处理器的作用就是处理生产者、消费者以及Broker服务器的不同请求,比如获取生产者和消费者获取所有的路由信息,Broker服务器注册路由信息等。处理器DefaultRequestProcessor处理不同的请求将会在下面进行讲述。
  • 执行定时任务。主要有两个定时任务,一个是每十秒扫描不活跃的Broker。并且将过期的Broker清理掉。另外一个是每十秒打印key-valu的配置信息。

上面就是initialize方法的主要逻辑,特别需要注意每10秒扫描不活跃的broker的定时任务:

powershell 复制代码
//NamesrvController.this.routeInfoManager.scanNotActiveBroker();
//代码位置:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#scanNotActiveBroker
 public void scanNotActiveBroker() {
     //所有存活的Broker
     Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
     //遍历Broker
     while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();
            //最后更新时间加上broker过期时间(120秒)小于当前时间,则关闭与broker的远程连接。并且将缓存在map中的broker信息删除
            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
                RemotingUtil.closeChannel(next.getValue().getChannel());
                it.remove();
                //将过期的Channel连接清理掉。以及删除缓存的Broker
                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
            }
        }
 }

scanNotActiveBroker方法的逻辑主要是遍历缓存在brokerLiveTable的Broker,将Broker最后更新时间加上120秒的结果是否小于当前时间,如果小于当前时间,说明Broker已经过期,可能是已经下线了,所以可以清除Broker信息,并且关闭Name Server 服务器与Broker服务器连接,这样被清除的Broker就不会与Name Server服务器进行远程通信了。brokerLiveTable的结果如下:

powershell 复制代码
//保存broker地址与broker存活信息的对应关系
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;

brokerLiveTable缓存着以brokerAddr为key(Broker 地址),以BrokerLiveInfo为value的结果,BrokerLiveInfo是Broker存活对象,主要有如下几个属性:

powershell 复制代码
class BrokerLiveInfo {
    //最后更新时间
    private long lastUpdateTimestamp;
    //版本信息
    private DataVersion dataVersion;
    //连接
    private Channel channel;
    //高可用服务器地址
    private String haServerAddr;

    //省略代码
}

从BrokerLiveInfo中删除了过期的Broker后,还需要做清理Name Server服务器与Broker服务器的连接,onChannelDestroy方法主要是清理缓存在如下map的信息:

powershell 复制代码
////代码位置:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager

//保存broker地址与broker存活信息的对应关系
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
//保存broker地址与过滤服务器的对应关系,Filter Server 与消息过滤有关系
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
//保存broker 名字与 broker元数据的关系
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
//保存集群名字与集群下所有broker名字对应的关系
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
//保存topic与topic下所有队列元数据的对应关系
 private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;

在扫描过期的broker时,首先找到不活跃的broker,然后onChannelDestroy方法清理与该不活跃broker有关的缓存,清理的主要流程如下:

  • 清理不活跃的broker存活信息。首先遍历brokerLiveTable找到不活跃的broker,然后删除brokerLiveTable中的与该不活跃的broker有关的缓存信息。
  • 清理与消息过滤有关的缓存。找到不活跃的broker存活信息,删除filterServerTable中的与该broker地址有关的消息过滤的服务信息。
  • 清理与不活跃broker的元素居。brokerAddrTable保存着broker名字与broker元素居对应的信息,BrokerData类保存着cluster、brokerName、brokerId与broker name。遍历brokerAddrTable找到与该不活跃broker的名字相等的broker元素进行删除。
  • 清理集群下对应的不活跃broker名字。clusterAddrTable保存集群名字与集群下所有broker名字对应的关系,遍历clusterAddrTable的所有key,从clusterAddrTable中找到与不活跃broker名字相等的元素,然后删除。
  • 清理与该不活跃broker的topic对应队列数据。topicQueueTable保存topic与topic下所有队列元数据的对应关系,QueueData保存着brokerName、readQueueNums(可读队列数量)、writeQueueNums(可写队列数量)等。遍历topicQueueTable的key,找到与不活跃broker名字相同的QueueData进行删除。

初始化nameserver 服务器以后,接下来就可以启动nameserver 服务器:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.NamesrvController#start
public void start() throws Exception {
    //启动远程服务器(netty 服务器)
    this.remotingServer.start();

    //启动文件监听线程
    if (this.fileWatchService != null) {
        this.fileWatchService.start();
    }
}

start方法做了两件事,第一件就是启动netty服务器,netty服务器主要负责与Broker、生产者与消费者之间的通信,处理Broker、生产者与消费者的不同请求。根据nettyConfig配置,设置启动的配置和各种处理器,然后采用netty服务器启动的模板启动服务器,具体的代码就不分析了,有兴趣的可以看看netty启动代码模板是怎么样的。第二件事就是启动文件监听线程,监听tts相关文件是否发生变化。

Name Server 服务器启动流程的源代码分析到此为止了,在这里总结下Name Server 服务器启动流程主要做了什么事:

  • 加载和读取配置。设置Name Server 服务器启动的配置NamesrvConfig和启动Netty服务器启动的配置NettyServerConfig。
  • 初始化相关的组件。netty服务类、远程服务线程池、处理器以及定时任务的初始化。
  • 启动Netty服务器。Netty服务器用来与broker、生产者、消费者进行通信、处理与它们之间的各种请求,并且对请求的响应结果进行处理。

Broker管理和路由信息的管理

Name Server 服务器的作用主要有两个:Broker管理和路由信息管理。

Broker管理

在上面分析的Name Server 服务器的启动过程中,也有一个与Broker管理相关的分析,那就是启动一个定时线程池每十秒去扫描不活跃的Broker。将不活跃的Broker清理掉。除了在Name Server 服务器启动时启动定时任务去扫描不活跃的Broker外,Name Server 服务器启动以后,通过netty服务器接收Broker、生产者、消费者的不同请求,将接收到请求会交给在Name Server服务器启动时注册的处理器DefaultRequestProcessor类的processRequest方法处理。processRequest方法根据请求的不同类型,将请求交给不同的方法进行处理。有关Broker管理的请求主要有注册Broker、注销Broker,processRequest方法处理注册Broker、注销Broker请求的代吗如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest

public RemotingCommand processRequest(ChannelHandlerContext ctx,RemotingCommand request)  {
         switch (request.getCode()) {
            //省略无关代码

            //注册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
            case RequestCode.UNREGISTER_BROKER:
                return this.unregisterBroker(ctx, request);
           //省略无关代码
         }
 }
Broker注册

Broker 服务器启动时,会向Name Server 服务器发送Broker 相关的信息,如集群的名字、Broker地址、Broker名字、topic相关信息等,注册Broker主要的代码比较长,接下来会分成好几部分进行讲解。如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.processor.RouteInfoManager#registerBroker
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();
     this.lock.writeLock().lockInterruptibly();
     //根据集群的名字获取所有的broker名字
     Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
     if (null == brokerNames) {
           brokerNames = new HashSet<String>();
           this.clusterAddrTable.put(clusterName, brokerNames);
      }
      //名字保存在broker名字中
      brokerNames.add(brokerName);

    //省略代码
}

registerBroker方法根据集群的名字获取该集群下所有的Broker名字的Set,如果不存在就创建并添加进clusterAddrTable中,clusterAddrTable保存着集群名字与该集群下所有的Broker名字对应关系,最后将broker名字保存在set中。

powershell 复制代码
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) {

     //省略代码

     boolean registerFirst = false;
     //获取broker 元数据
     BrokerData brokerData = this.brokerAddrTable.get(brokerName);
     if (null == brokerData) {
           registerFirst = true;
           brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
           this.brokerAddrTable.put(brokerName, brokerData);
      }

     //获取所有的broker地址
     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);

   //省略代码

}

上述代码主要做了两件事:

  • 缓存broker元数据信息。首先根据broker名字从brokerAddrTable中获取Broker元数据brokerData,如果brokerData不存在,说明是第一次注册,创建Broker元数据并添加进brokerAddrTable中,brokerAddrTable保存着Broker名字与Broker元数据对应的信息。
  • 从Broker元数据brokerData中获取该元数据中的所有Broker地址信息brokerAddrsMap。brokerAddrsMap保存着brokerId与所有Broker名字对应信息。遍历brokerAddrsMap中的所有broker地址,查找与参数brokerAddr相同但是与参数borkerId不同的进行删除,保证一个broker名字对应着BrokerId,最后将参数brokerId与参数brokerAddr保存到brokerData元数据的brokerAddrsMap中进行缓存。
powershell 复制代码
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) {

     //省略代码

     //如果topic的配置不空并且是broker master
     if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) {
          //如果topic配置改变或者是第一次注册
          if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())|| registerFirst) {
                  //获取所有的topic配置
                  ConcurrentMap<String, TopicConfig> tcTable = topicConfigWrapper.getTopicConfigTable();
                  if (tcTable != null) {
                       //遍历topic配置,创建并更新队列元素
                       for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                             this.createAndUpdateQueueData(brokerName, entry.getValue());
                        }
                    }
             }
       }

    //省略代码

}

如果参数topicConfigWrapper不等于空,并且brokerId等于0时,判断topic是否改变,如果topic改变或者是第一次注册,获取所有的topic配置,并创建和更新队列元数据。QueueData保存着队列元数据,如Broker名字、写队列数量、读队列数量,如果队列缓存中不存在该队列元数据,则添加,否则遍历缓存map找到该队列元数据进行删除,如果是新添加的则添加进队列缓存中。

powershell 复制代码
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) {

   //省略代码

    //创建broker存活对象,并进行保存
    BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,new BrokerLiveInfo(
                        System.currentTimeMillis(),
                        topicConfigWrapper.getDataVersion(),
                        channel,
                        haServerAddr));

    if (null == prevBrokerLiveInfo) {
           log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
    }

    //如果过滤服务地址不为空,则缓存到filterServerTable
    if (filterServerList != null) {
            if (filterServerList.isEmpty()) {
                   this.filterServerTable.remove(brokerAddr);
            } else {
                   this.filterServerTable.put(brokerAddr, filterServerList);
            }
     }

     //如果不是broker master,获取高可用服务器地址以及master地址
     if (MixAll.MASTER_ID != brokerId) {
              String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
              if (masterAddr != null) {
                   BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
                   if (brokerLiveInfo != null) {
                         result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
                         result.setMasterAddr(masterAddr);
                    }
               }
      }

      return result;

 }

最后代码片段,主要做了三件事,首先创建了Broker存活对象BrokerLiveInfo,添加到brokerLiveTable中缓存,在Name Server 启动时,供定时线程任务每十秒进行扫描。以确保非正常的Broker被清理掉。然后是判断参数filterServerList是否为空,如果不为空,则添加到filterServerTable缓存,filterServerTable保存着与消息过滤相关的过滤服务。最后,判断该注册的Broker不是Broker master,则设置高可用服务器地址以及master地址。到此为止,Broker注册的代码就分析完成了,总而言之,Broker注册就是Broker将相关的元数据信息,如Broker名字,Broker地址、topic信息发送给Name Server服务器,Name Server接收到以后将这些元数据缓存起来,以供后续能够快速找到这些元数据,生产者和消费者也可以通过Name Server服务器获取到Broke相关的信息,这样,生产者和消费者就可以和Broker服务器进行通信了,生产者发送消息给Broker服务器,消费者从Broker服务器消费消息。

Broker注销

Broker注销的过程刚好跟Broker注册的过程相反,Broker注册是将Broker相关信息和Topic配置信息缓存起来,以供生产者和消费者使用。而Broker注销则是将Broker注销缓存的Broker信息从缓存中删除,Broker注销unregisterBroker方法主要代码流程如下:

powershell 复制代码
//代码位置:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unregisterBroker
public void unregisterBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId) {

    this.lock.writeLock().lockInterruptibly();
    //将缓存的broker存活对象删除
    BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr);

     //将所有的过滤服务删除
     this.filterServerTable.remove(brokerAddr);

    boolean removeBrokerName = false;
    //删除broker元数据
     if (null != brokerData) {
           String addr = brokerData.getBrokerAddrs().remove(brokerId);
           if (brokerData.getBrokerAddrs().isEmpty()) {
                this.brokerAddrTable.remove(brokerName);
                removeBrokerName = true;
            }
      }

      //如果删除broker元数据成功
      if (removeBrokerName) {
           Set<String> nameSet = this.clusterAddrTable.get(clusterName);
           if (nameSet != null) {
                boolean removed = nameSet.remove(brokerName);
                if (nameSet.isEmpty()) {
                      this.clusterAddrTable.remove(clusterName);
                 }
            }
            //根据brokerName删除topic配置信息
            this.removeTopicByBrokerName(brokerName);
      }
      this.lock.writeLock().unlock();
}

unregisterBroker方法的参数有集群名字、broker地址、broker名字、brokerId,主要逻辑为:

  • 根据broker地址删除broker存活对象。
  • 根据broker地址删除所有消息过滤服务。
  • 删除broker元数据。
  • 如果删除元数据成功,则根据集群名字删除该集群的所有broker名字,以及根据根据- brokerName删除topic配置信息。
路由信息的管理

处理器DefaultRequestProcessor类的processRequest方法除了处理Broker注册和Broker注销的请求外,还处路由信息管理有关的请求,接收到生产者和消费者的路由信息相关的请求,会交给处理器DefaultRequestProcessor类的processRequest方法处理,processRequest方法则会根据不同的请求类型将请求交给RouteInfoManager类的不同方法处理。RouteInfoManager类用map进行缓存路由相关信息,map如下:

powershell 复制代码
//topic与队列数据对应映射关系
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
//broker 名字与broker 元数据对应映射关系
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
//保存cluster的所有broker name
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
//broker 地址 与 BrokerLiveInfo存活对象的对应映射关系
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
//broker 地址 的所有过滤服务
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

RouteInfoManager类利用上面几个map缓存了Broker信息,topic相关信息、集群信息、消息过滤服务信息等,如果这些缓存的信息有变化,就是网这些map新增或删除缓存。这就是Name Server服务的路由信息管理。processRequest方法是如何处理路由信息管理的,具体实现可以阅读具体的代码,无非就是将不同的请求委托给RouteInfoManager的不同方法,RouteInfoManager的不同实现了上面缓存信息的管理。

Broker

Broker 主要负责消息的存储,投递和查询以及保证服务的高可用。Broker负责接收生产者发送的消息并存储、同时为消费者消费消息提供支持。为了实现这些功能,Broker包含几个重要的子模块:

通信模块 :负责处理来自客户端(生产者、消费者)的请求。
客户端管理模块 :负责管理客户端(生产者、消费者)和维护消费者的Topic订阅信息。
存储模块 :提供存储消息和查询消息的能力,方便Broker将消息存储到硬盘。
高可用服务(HA Service) :提供数据冗余的能力,保证数据存储到多个服务器上,将Master Broker的数据同步到Slavew Broker上。
索引服务(Index service) :对投递到Broker的消息建立索引,提供快速查询消息的能力。

broker启动过程分析

在Name Server启动以后,Broker就可以开始启动了,启动过程将所有路由信息都注册到Name server服务器上,生产者就可以发送消息到Broker,消费者也可以从Broker消费消息。接下来就来看看Broker的具体启动过程。

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#main
public static void main(String[] args) {
      start(createBrokerController(args));
}

BrokerStartup类是Broker的启动类,在BrokerStartup类的main方法中,首先创建用createBrokerController方法创建Broker控制器(BrokerController类),Broker控制器主要负责Broker启动过程的具体的相关逻辑实现。创建好Broker 控制器以后,就可以启动Broker 控制器了,所以下面将从两个部分分析Broker的启动过程:

  • 创建Broker控制器
  • 初始化配置信息
  • 创建并初始化Broker控制
  • 注册Broker关闭的钩子
  • 启动Broker控制器
创建Broker控制器

Broker在启动的时候,会初始化一些配置,如Broker配置、netty服务端配置、netty客户端配置、消息存储配置,为Broker启动提供配置准备。

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#createBrokerController
 public static BrokerController createBrokerController(String[] args) {
    /**
    省略代码
    注释:
        1、设置RocketMQ的版本
        2、设置netty接收和发送请求的buffer大小
        3、构建命令行:将命令行进行解析封装
    **/

     //broker配置、netty服务端配置、netty客户端配置
     final BrokerConfig brokerConfig = new BrokerConfig();
     final NettyServerConfig nettyServerConfig = new NettyServerConfig();
     final NettyClientConfig nettyClientConfig = new NettyClientConfig();
     nettyClientConfig.setUseTLS(Boolean.parseBoolean(System.getProperty(TLS_ENABLE,
                String.valueOf(TlsSystemConfig.tlsMode == TlsMode.ENFORCING))));
     //设置netty监听接口
     nettyServerConfig.setListenPort(10911);
     //消息存储配置
     final MessageStoreConfig messageStoreConfig = new MessageStoreConfig();
     //如果broker的角色是slave,设置命中消息在内存的最大比例
     if (BrokerRole.SLAVE == messageStoreConfig.getBrokerRole()) {
         int ratio = messageStoreConfig.getAccessMessageInMemoryMaxRatio() - 10;
         messageStoreConfig.setAccessMessageInMemoryMaxRatio(ratio);
     }
     //省略代码
 }

createBrokerController方法创建BrokerConfig、NettyServerConfig、NettyClientConfig、MessageStoreConfig配置类,BrokerConfig类是Broker配置类。

  • BrokerConfig:属性主要包括Broker相关的配置属性,如Broker名字、Broker Id、Broker连接的Name server地址、集群名字等。
  • NettyServerConfig:Broker netty服务端配置类,Broker netty服务端主要用来接收客户端的请求,NettyServerConfig类主要属性包括监听接口、服务工作线程数、接收和发送请求的buffer大小等。
  • NettyClientConfig:netty客户端配置类,用于生产者、消费者这些客户端与Broker进行通信相关配置,配置属性主要包括客户端工作线程数、客户端回调线程数、连接超时时间、连接不活跃时间间隔、连接最大闲置时间等。
  • MessageStoreConfig:消息存储配置类,配置属性包括存储路径、commitlog文件存储目录、CommitLog文件的大小、CommitLog刷盘的时间间隔等。
初始化配置信息

创建完这些配置类以后,接下来会为这些配置类的一些配置属性设置值,先看看如下代码:

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#createBrokerController
 public static BrokerController createBrokerController(String[] args) {
     //省略代码

     //如果命令中包含字母c,则读取配置文件,将配置文件的内容设置到配置类中
      if (commandLine.hasOption('c')) {
          String file = commandLine.getOptionValue('c');
          if (file != null) {
              configFile = file;
              InputStream in = new BufferedInputStream(new FileInputStream(file));
              properties = new Properties();
              properties.load(in);

              //读取配置文件的中namesrv地址
              properties2SystemEnv(properties);
              //将配置文件中的配置项映射到配置类中去
              MixAll.properties2Object(properties, brokerConfig);
              MixAll.properties2Object(properties, nettyServerConfig);
              MixAll.properties2Object(properties, nettyClientConfig);
              MixAll.properties2Object(properties, messageStoreConfig);

              //设置配置broker配置文件
              BrokerPathConfigHelper.setBrokerConfigPath(file);
              in.close();
          }
      }
       //设置broker配置类
       MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), brokerConfig);

     //省略代码
 }

上述主要的代码逻辑为如果命令行中存在命令参数为'c'(c是configFile的缩写),那么就读取configFile文件的内容,将configFile配置文件的配置项映射到BrokerConfig、NettyServerConfig、NettyClientConfig、MessageStoreConfig配置类中。接下来createBrokerController方法做一些判断必要配置的合法性,如下代码所示:

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#createBrokerController
public static BrokerController createBrokerController(String[] args) {
     //省略代码

     //如果broker配置文件的rocketmqHome属性值为null,直接结束程序
      if (null == brokerConfig.getRocketmqHome()) {
             System.out.printf("Please set the %s variable in your environment to match the location of 
                                  the RocketMQ installation", MixAll.ROCKETMQ_HOME_ENV);
             System.exit(-2);
       }

        //如果name server服务器的地址不为null
       String namesrvAddr = brokerConfig.getNamesrvAddr();
       if (null != namesrvAddr) {
            try {
                 //namesrvAddr是以";"分割的多个地址
                 String[] addrArray = namesrvAddr.split(";");
                 //每个地址是ip:port的形式,检测下是否形如ip:port的形式
                 for (String addr : addrArray) {
                     RemotingUtil.string2SocketAddress(addr);
                 }
              } catch (Exception e) {
                  System.out.printf(
                        "The Name Server Address[%s] illegal, please set it as follows, 
                           \"127.0.0.1:9876;192.168.0.1:9876\"%n",namesrvAddr);
                  System.exit(-3);
              }
         }

         //设置BrokerId,broker master 的BrokerId设置为0,broker slave 设置为大于0的值
          switch (messageStoreConfig.getBrokerRole()) {
              case ASYNC_MASTER:
              case SYNC_MASTER:
                  brokerConfig.setBrokerId(MixAll.MASTER_ID);
                  break;
               case SLAVE:
                  //如果小于等于0,退出程序
                  if (brokerConfig.getBrokerId() <= 0) {
                      System.out.printf("Slave's brokerId must be > 0");
                      System.exit(-3);
                   }

                   break;
                 default:
                    break;
             }

          //省略代码

 }

首先会判断下RocketmqHome的值是否为空,RocketmqHome是Borker相关配置保存的文件目录,如果为空则直接退出程序,启动Broker失败;然后判断下Name server 地址是否为空,如果不为空则解析以";"分割的name server地址,检测下地址的合法性,如果不合法则直接退出程序;最后判断下Broker的角色,如果是master,BrokerId设置为0,如果是SLAVE,则BrokerId设置为大于0的数,否则直接退出程序,Broker启动失败。

createBrokerController方法进行必要配置参数的判断以后,将进行日志的设置、以及打印配置信息,主要代码如下:

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#createBrokerController
public static BrokerController createBrokerController(String[] args) {
    //省略代码
    //注释:日志设置

    //printConfigItem 打印配置信息
    if (commandLine.hasOption('p')) {
        InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME);
        MixAll.printObjectProperties(console, brokerConfig);
        MixAll.printObjectProperties(console, nettyServerConfig);
        MixAll.printObjectProperties(console, nettyClientConfig);
        MixAll.printObjectProperties(console, messageStoreConfig);
        System.exit(0);
    } else if (commandLine.hasOption('m')) {
    //printImportantConfig 打印重要配置信息
        InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME);
        MixAll.printObjectProperties(console, brokerConfig, true);
        MixAll.printObjectProperties(console, nettyServerConfig, true);
        MixAll.printObjectProperties(console, nettyClientConfig, true);
        MixAll.printObjectProperties(console, messageStoreConfig, true);
        System.exit(0);
    }

    //打印配置信息
    log = InternalLoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME);
    MixAll.printObjectProperties(log, brokerConfig);
    MixAll.printObjectProperties(log, nettyServerConfig);
    MixAll.printObjectProperties(log, nettyClientConfig);
    MixAll.printObjectProperties(log, messageStoreConfig);

    //代码省略

}

createBrokerController方法的以上代码逻辑打印配置信息,先判断命令行参数是否包含字母'p'(printConfigItem的缩写),如果包含字母'p',则打印配置信息,否则判断下命令行是否包含字母'm',则打印被@ImportantField注解的配置属性,也就是重要的配置属性。最后,不管命令行中是否存在字母'p'或者字母'm',都打印配置信息。

以上就是初始化配置信息的全部代码,初始化配置信息主要是创建BrokerConfig、NettyServerConfig、NettyClientConfig、MessageStoreConfig配置类,并为这些配置类设置配置的值,同时根据命令行参数判断打印配置信息。

初始化Broker控制器
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerStartup#createBrokerController
public static BrokerController createBrokerController(String[] args) {

    //省略代码

     //创建BrokerController(broker 控制器)
    final BrokerController controller = new BrokerController(
           brokerConfig,
           nettyServerConfig,
           nettyClientConfig,
           messageStoreConfig);
    // remember all configs to prevent discard
    //将所有的配置信息保存在内存
    controller.getConfiguration().registerConfig(properties);

   //初始化broker控制器
    boolean initResult = controller.initialize();
    //如果初始化失败,则退出
    if (!initResult) {
        controller.shutdown();
         System.exit(-3);
    }

    //省略代码
}

创建并初始化Broker控制的代码比较简单,创建以配置类作为参数的BrokerController对象,并将所有的配置信息保存在内容中,方便在其他地方使用;创建完Broker控制器对象以后,对控制器进行初始化,当初始化失败以后,则直接退出程序。

initialize方法主要是加载一些保存在本地的一些配置数据,总结起来做了如下几方面的事情:

  • 加载topic配置、消费者位移数据、订阅组数据、消费者过滤配置
  • 创建消息相关的组件,并加载消息数据
  • 创建netty服务器
  • 创建一系列线程
  • 注册处理器
  • 启动一系列定时任务
  • 初始化事务组件
  • 初始化acl组件
  • 注册RpcHook
加载topic配置、消费者位移数据、订阅组数据、消费者过滤配置
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
   //加载topic配置 topics.json
    boolean result = this.topicConfigManager.load();
    //加载消费者位移数据 consumerOffset.json
    result = result && this.consumerOffsetManager.load();
    //加载订阅组数据 subscriptionGroup.json
    result = result && this.subscriptionGroupManager.load();
    //加载消费者过滤 consumerFilter.json
    result = result && this.consumerFilterManager.load();

    //省略代码
}

load方法是抽象类ConfigManager的方法,该方法读取文件的内容解码成对应的配置对象,如果文件中的内容为空,就读取备份文件中的内容进行解码。读取的文件都是保存在user.home/store/config/下,user.home是用户目录,不同人的电脑user.home一般不同。topicConfigManager.load()读取topics.json文件,如果该文件的内容为空,那么就读取topics.json.bak文件内容,topics.json保存的是topic数据;同理,consumerOffsetManager.load()方法读取consumerOffset.json和consumerOffset.json.bak文件,保存的是消费者位移数据;subscriptionGroupManager.load()方法读取subscriptionGroup.json和subscriptionGroup.json.bak文件,保存订阅组数据(消费者分组数据)、consumerFilterManager.load()方法读取的是consumerFilter.json和consumerFilter.json.bak的内容,保存的是消费者过滤数据。

创建消息相关的组件,并加载消息数据
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {

        //省略代码

        //如果上述都加载成功
        if (result) {
            try {
                //创建消息存储器
                this.messageStore = new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager,                                this.messageArrivingListener,this.brokerConfig);
                //如果开启了容灾、主从自动切换,添加DLedger角色改变处理器
                if (messageStoreConfig.isEnableDLegerCommitLog()) {
                    DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
                    ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
                }
                //broker 相关统计
                this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
                //load plugin
                //加载消息存储插件
                MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
                this.messageStore = MessageStoreFactory.build(context, this.messageStore);
                this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
            } catch (IOException e) {
                result = false;
                log.error("Failed to initialize", e);
            }
        }

        //加载消息文件
        result = result && this.messageStore.load();

        //省略代码
}

如果加载topic配置、消费者位移数据、订阅组数据、消费者过滤配置成功以后,就创建消息相关的组件,并加载消息数据,这个过程创建了消息存储器、DLedger角色改变处理器、Broker统计相关组件以及消息存储插件,然后加载消息文件中的数据。接下来具体看看加载消息文件中的messageStore.load()方法:

powershell 复制代码
//代码位置:org.apache.rocketmq.store.DefaultMessageStore#load
public boolean load() {
        boolean result = true;

        try {
            //判断abort是否存在
            boolean lastExitOK = !this.isTempFileExist();
            log.info("last shutdown {}", lastExitOK ? "normally" : "abnormally");

            //加载定时消费服务器
            if (null != scheduleMessageService) {
            //读取delayOffset.json文件
                result = result && this.scheduleMessageService.load();
            }

            // load Commit Log
            //加载 Commit log 文件
            result = result && this.commitLog.load();

            // load Consume Queue
            //加载消费者队列 文件consumequeue
            result = result && this.loadConsumeQueue();

            if (result) {
                //加载检查点文件checkpoint
                this.storeCheckpoint =
                    new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));

                //加载索引文件
                this.indexService.load(lastExitOK);

                //数据恢复
                this.recover(lastExitOK);

                log.info("load over, and the max phy offset = {}", this.getMaxPhyOffset());
            }
        } catch (Exception e) {
            log.error("load exception", e);
            result = false;
        }

        if (!result) {
            this.allocateMappedFileService.shutdown();
        }

        return result;
}

load方法主要逻辑就是加载各种数据文件,主要有以下几方面进行加载数据:

  • isTempFileExist方法判断abort是否存在,如果不存在,说明Broker是正常关闭的,否则就是异常关闭。
  • scheduleMessageService.load()方法读取user.home/store/config/下的delayOffset.json文件的内容,该文件内容保存延迟消息的位移数据。
  • commitLog.load()加载 CommitLog 文件, CommitLog 文件保存的是消息内容
  • loadConsumeQueue()方法加载consumequeue目录下的内容,ConsumeQueue(消息消费队列)是消费消息的索引,消费者通过ConsumeQueue可以快速找到查找待消费的消息,consumequeue目录下的文件组织方式是:topic/queueId/fileName,所以就可以快速找待消费的消息在哪一个Commit log 文件中。
  • indexService.load(lastExitOK)加载索引文件,加载的是user.home/store/index/目录下文件,文件名fileName是以创建时的时间戳命名的,所以可以通过时间区间来快速查询消息,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故底层实现为hash索引。
  • recover(lastExitOK)方法将CommitLog 文件的内容加载到内存中以及topic队列。
创建netty服务器
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {

    //省略代码
    if (result) {

        //创建netty远程服务器
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
         NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
         fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
         this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);

        //省略代码
    }

     //省略代码
}

创建netty服务器的时候创建了两个,一个是普通的,一个是快速的,remotingServer用来与生产者、消费者进行通信。当isSendMessageWithVIPChannel=true的时候会选择port-2的fastRemotingServer进行的消息的处理,为了防止某些很重要的业务阻塞,就再开启了一个remotingServer进行处理,但是现在默认是不开启的,fastRemotingServer主要是为了兼容老版本的RocketMQ.。

创建一系列线程池
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {

            //代码省略

            //发送消息线程池
            this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor(
                this.brokerConfig.getSendMessageThreadPoolNums(),
                this.brokerConfig.getSendMessageThreadPoolNums(),
                1000 * 60,
                TimeUnit.MILLISECONDS,
                this.sendThreadPoolQueue,
                new ThreadFactoryImpl("SendMessageThread_"));

            //拉取消息线程池
            //this.pullMessageExecutor 

            //回复消息线程池
            //this.replyMessageExecutor 

            //查询消息线程池
            //this.queryMessageExecutor 

            //broker 管理线程池
            //this.adminBrokerExecutor

            //客户端管理线程池
            //this.clientManageExecutor 

            //心跳线程池
            //this.heartbeatExecutor 
            //事务线程池
           // this.endTransactionExecutor 

            //消费者管理线程池
            this.consumerManageExecutor =
                Executors.newFixedThreadPool(this.brokerConfig.getConsumerManageThreadPoolNums(), new ThreadFactoryImpl(
                    "ConsumerManageThread_"));

            //代码省略
}

创建的线程池对象有发送消息线程池、拉取消息线程池、回复消息线程池、查询消息线程池、broker 管理线程池、客户端管理线程池、心跳线程池、事务线程池、消费者管理线程池。

注册请求处理器
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
    //省略代码

    //注册处理器
     this.registerProcessor();

    //省略代码
}

registerProcessor()方法如下:

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#registerProcessor
public void registerProcessor() {

        //发送消息处理器
        SendMessageProcessor sendProcessor = new SendMessageProcessor(this);
        sendProcessor.registerSendMessageHook(sendMessageHookList);
        sendProcessor.registerConsumeMessageHook(consumeMessageHookList);

        //远程服务注册发送消息处理器
        this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
        this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor);
        this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor);
        this.remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor);
        this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
        this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor);
        this.fastRemotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor);
        this.fastRemotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor);

        //注册拉消息处理器
        //注册回复消息处理器
        //注册查询消息处理器
        //注册客户端管理处理器
        //注册消费者管理处理器
        //注册事务处理器

         //注册broker处理器
        AdminBrokerProcessor adminProcessor = new AdminBrokerProcessor(this);
        this.remotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor);
        this.fastRemotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor);
}

registerProcessor方法注册了发送消息处理器、远程服务注册发送消息处理器、拉消息处理器、回复消息处理器、查询消息处理器、客户端管理处理器、消费者管理处理器、事务处理器、broker处理器。registerProcessor注册方法也很简单,就是以RequestCode作为key,以Pair<处理器,线程池>作为Value保存在名字为processorTable的HashMap中。每个请求都是在线程池中处理的,这样可以提高处理请求的性能。对于每个传入的请求,根据RequestCode就可以在processorTable查找处理器来处理请求。每个处理器都有有一个processRequest方法进行处理请求。

启动一系列定时任务

Broker初始化方法initialize中,会启动一系列的后台定时线程任务,这些后台任务包括都是由scheduledExecutorService线程池执行的,scheduledExecutorService是单线程线程池( Executors.newSingleThreadScheduledExecutor()),只用单线程线程池执行后台定时任务有一个好处就是减少线程过多,反而导致线程为了抢占CPU加剧了竞争。这一些后台定时线程任务如下:

  • 每24小时打印昨天产生了多少消息,消费了多少消息
  • 每五秒保存消费者位移到文件中
  • 每10秒保存消费者过滤到文件中
  • 每3分钟定时检测消费的进度
  • 每秒打印队列的大小以及队列头部元素存在的时间
  • 每分钟打印已存储在CommitLog中但尚未分派到消费队列的字节数
  • 每两分钟定时获取获取name server 地址
  • 每分钟定时打印slave 数据同步落后多少
powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
    //省略代码
    final long period = 1000 * 60 * 60 * 24;
    //每24小时打印昨天产生了多少消息,消费了多少消息
     this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
              try {
                   BrokerController.this.getBrokerStats().record();
               } catch (Throwable e) {
                    log.error("schedule record error.", e);
               }
             }
      }, initialDelay, period, TimeUnit.MILLISECONDS);
    //省略代码
}

每24小时打印昨天产生了多少消息,消费了多少消息的定时任务比较简单,就是将昨天消息的生产和消费的数量统计出来,然后把这两个指标打印出来。

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
    //省略代码
    //每五秒保存消费者位移
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
           try {
                  BrokerController.this.consumerOffsetManager.persist();
            } catch (Throwable e) {
                  log.error("schedule persist consumerOffset error.", e);
            }
        }
    }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

    //每10秒保存消费者过滤
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
         @Override
        public void run() {
          try {
              BrokerController.this.consumerFilterManager.persist();
          } catch (Throwable e) {
               log.error("schedule persist consumer filter error.", e);
          }
        }
    }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);
    //省略代码
}

每五秒保存消费者位移和每10秒保存消费者过滤定时任务都是保存在文件中,每五秒保存消费者位移定时任务将消费者位移保存在consumerOffset.json文件中,每10秒保存消费者过滤定时任务将消费者过滤保存在consumerFilter.json文件中。

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
    //省略代码
    //每3分钟定时检测消费的进度
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
           @Override
           public void run() {
                try {
                    BrokerController.this.protectBroker();
                } catch (Throwable e) {
                    log.error("protectBroker error.", e);
                }
            }
     }, 3, 3, TimeUnit.MINUTES);
    //省略代码
}

每3分钟定时检测消费进度的定时任务的作用是检测消费者的消费进度,当消费者消费消息的进度落后大于配置的最大落后阈值时,就停止消费者消费,具体的实现看protectBroker的源码:

//源代码位置:org.apache.rocketmq.broker.BrokerController#protectBroker

public void protectBroker() {

//是否开启慢消费检测开关,默认未开启

if (this.brokerConfig.isDisableConsumeIfConsumerReadSlowly()) {

//遍历统计项

final Iterator<Map.Entry<String, MomentStatsItem>> it = this.brokerStatsManager.getMomentStatsItemSetFallSize().getStatsItemTable().entrySet().iterator();

while (it.hasNext()) {

final Map.Entry<String, MomentStatsItem> next = it.next();

final long fallBehindBytes = next.getValue().getValue().get();

//消费者消费消息的进度落后消费者落后阈值

if (fallBehindBytes > this.brokerConfig.getConsumerFallbehindThreshold()) {

final String[] split = next.getValue().getStatsKey().split("@");

final String group = split[2];

LOG_PROTECTION.info("[PROTECT_BROKER] the consumer[{}] consume slowly, {} bytes, disable it", group, fallBehindBytes);

//设置消费者消费的标志,关闭消费

this.subscriptionGroupManager.disableConsume(group);

}

}

}

}

protectBroker方法首先判别是否开启慢消费检测开关,如果开启了,就进行遍历统计项,判断消费者消费消息的进度落后消费者落后阈值的时候,就停止该消费者停止消费来保护broker,如果消费者消费比较慢,那么在Broker的消费会越来越多,积压在Broker上,所以停止慢消费者消费消息,让其他消费者消费,减少消息的积压。

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#initialize
public boolean initialize() throws CloneNotSupportedException {
    //代码省略
    //每秒打印队列的大小以及队列头部元素存在的时间
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
         @Override
         public void run() {
             try {
                  BrokerController.this.printWaterMark();
              } catch (Throwable e) {
                   log.error("printWaterMark error.", e);
             }
        }
     }, 10, 1, TimeUnit.SECONDS);

    //代码省略
}

每秒打印队列的大小以及队列头部元素存在的时间定时任务,会打印发送消息线程池队列、拉取消息线程池队列、查询消息线程池队列、结束事务线程池队列的大小,以及打印队列头部元素存在的时间,这个时间等于当前时间减去头部元素创建的时间,就是该元素创建到现在已经花费了多长时间。具体的代码如下:

powershell 复制代码
//源代码位置:org.apache.rocketmq.broker.BrokerController#headSlowTimeMills
public long headSlowTimeMills(BlockingQueue<Runnable> q) {
        long slowTimeMills = 0;
        //队列的头
        final Runnable peek = q.peek();
        if (peek != null) {
            RequestTask rt = BrokerFastFailure.castRunnable(peek);
            //当前时间减去创建时间
            slowTimeMills = rt == null ? 0 : this.messageStore.now() - rt.getCreateTimestamp();
        }

        if (slowTimeMills < 0) {
            slowTimeMills = 0;
        }

        return slowTimeMills;
}
初始化事务消息
powershell 复制代码
//源码位置:org.apache.rocketmq.broker.BrokerController#initialTransaction
private void initialTransaction() {
        //加载transactionalMessageService,利用spi
        this.transactionalMessageService = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_SERVICE_ID, TransactionalMessageService.class);
        if (null == this.transactionalMessageService) {
            this.transactionalMessageService = new TransactionalMessageServiceImpl(new TransactionalMessageBridge(this, this.getMessageStore()));
            log.warn("Load default transaction message hook service: {}", TransactionalMessageServiceImpl.class.getSimpleName());
        }
        //创建transactionalMessage检查监听器
        this.transactionalMessageCheckListener = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_LISTENER_ID, AbstractTransactionalMessageCheckListener.class);
        if (null == this.transactionalMessageCheckListener) {
            this.transactionalMessageCheckListener = new DefaultTransactionalMessageCheckListener();
            log.warn("Load default discard message hook service: {}", DefaultTransactionalMessageCheckListener.class.getSimpleName());
        }
        this.transactionalMessageCheckListener.setBrokerController(this);
        //创建事务消息检查服务
        this.transactionalMessageCheckService = new TransactionalMessageCheckService(this);
 }

initialTransaction方法主要创建与事务消息相关的类,创建transactionalMessageService(事务消息服务)、transactionalMessageCheckListener(事务消息检查监听器)、transactionalMessageCheckService(事务消息检查服务)。transactionalMessageService用于处理事务消息,transactionalMessageCheckListener主要用来回查消息监听,transactionalMessageCheckService用于检查超时的 Half 消息是否需要回查。RocketMQ发送事务消息是将消费先写入到事务相关的topic的中,这个消息就称为半消息,当本地事务成功执行,那么半消息会还原为原来的消息,然后再进行保存。initialTransaction在创建transactionalMessageService和transactionalMessageCheckListener都使用了ServiceProvider.loadClass方法,这个方法就是采用SPI原理,SPI原理就是利用反射加载META-INF/service目录下的某个接口的所有实现,只要实现接口,然后META-INF/service目录下添加文件名为全类名的文件,这样SPI就可以加载具体的实现类,具有可拓展性。

初始化acl组件
powershell 复制代码
//源码位置:org.apache.rocketmq.broker.BrokerController#initialAcl
private void initialAcl() {
        if (!this.brokerConfig.isAclEnable()) {
            log.info("The broker dose not enable acl");
            return;
        }

        //利用SPI加载权限相关的校验器
        List<AccessValidator> accessValidators = ServiceProvider.load(ServiceProvider.ACL_VALIDATOR_ID, AccessValidator.class);
        if (accessValidators == null || accessValidators.isEmpty()) {
            log.info("The broker dose not load the AccessValidator");
            return;
        }

        //将所有的权限校验器进行缓存以及注册
        for (AccessValidator accessValidator: accessValidators) {
            final AccessValidator validator = accessValidator;
            accessValidatorMap.put(validator.getClass(),validator);
            this.registerServerRPCHook(new RPCHook() {

                @Override
                public void doBeforeRequest(String remoteAddr, RemotingCommand request) {
                    //Do not catch the exception
                    validator.validate(validator.parse(request, remoteAddr));
                }

                @Override
                public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) {
                }
            });
        }
    }

initialAcl方法主要是加载权限相关校验器,RocketMQ的相关的管理的权限验证和安全就交给这里的加载的校验器了。initialAcl方法也利用SPI原理加载接口的具体实现类,将所有加载的校验器缓存在map中,然后再注册RPC钩子,在请求之前调用校验器的validate的方法。

注册RpcHook
powershell 复制代码
//源码位置:org.apache.rocketmq.broker.BrokerController#initialRpcHooks
private void initialRpcHooks() {

      //利用SPI加载钩子
      List<RPCHook> rpcHooks = ServiceProvider.load(ServiceProvider.RPC_HOOK_ID, RPCHook.class);
      if (rpcHooks == null || rpcHooks.isEmpty()) {
           return;
       }
        //注册钩子
       for (RPCHook rpcHook: rpcHooks) {
           this.registerServerRPCHook(rpcHook);
       }
}

initialRpcHooks方法加RPC钩子,利用SPI原理加载具体的钩子实现,然后将所有的钩子进行注册,钩子的注册是将钩子保存在List中。

以上分析就是创建Broker控制器的全过程,这个过程首先进行一些必要的初始化配置,如Broker配置、网络通信Neety配置以及存储相关配置等。然后在创建并初始化Broker控制器,创建并初始化Broker控制器的过程中,又进行了多个步骤,如加载topic配置、消费者位移数据、启动一系列后台定时任务、创建事务消息相关组件等。

Broker控制器的启动

powershell 复制代码
//源码位置:org.apache.rocketmq.broker.BrokerController#start
public static BrokerController start(BrokerController controller) {
        try {

            //Broker控制器启动
            controller.start();

            //打印Broker成功的消息
            String tip = "The broker[" + controller.getBrokerConfig().getBrokerName() + ", "
                + controller.getBrokerAddr() + "] boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();

            if (null != controller.getBrokerConfig().getNamesrvAddr()) {
                tip += " and name server is " + controller.getBrokerConfig().getNamesrvAddr();
            }

            log.info(tip);
            System.out.printf("%s%n", tip);
            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
}

controller.start()方法主要是启动各种组件:

  • 启动消息消息存储器
  • netty服务的启动
  • 文件监听器启动
  • broker 对外api启动
  • 长轮询拉取消息服务启动
  • 客户端长连接服务启动
  • 过滤服务管理启动
  • broker 相关统计启动
  • broker 快速失败启动
powershell 复制代码
//源码位置:org.apache.rocketmq.broker.BrokerController#start
public void start() throws Exception {
        if (this.messageStore != null) {
            //启动消息消息存储
            this.messageStore.start();
        }

        if (this.remotingServer != null) {
            //netty服务的启动
            this.remotingServer.start();
        }

        if (this.fastRemotingServer != null) {
            this.fastRemotingServer.start();
        }

        //文件改变监听启动
        if (this.fileWatchService != null) {
            this.fileWatchService.start();
        }

        //broker 对外api启动
        if (this.brokerOuterAPI != null) {
            this.brokerOuterAPI.start();
        }

        //保持长轮询请求的服务启动
        if (this.pullRequestHoldService != null) {
            this.pullRequestHoldService.start();
        }

        //客户端长连接服务启动
        if (this.clientHousekeepingService != null) {
            this.clientHousekeepingService.start();
        }

        //过滤服务管理启动
        if (this.filterServerManager != null) {
            this.filterServerManager.start();
        }

        //如果没有采用主从切换(多副本)
        if (!messageStoreConfig.isEnableDLegerCommitLog()) {
            startProcessorByHa(messageStoreConfig.getBrokerRole());
            handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
            this.registerBrokerAll(true, false, true);
        }

        //定时注册broker
        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);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);

        //broker 相关统计启动
        if (this.brokerStatsManager != null) {
            this.brokerStatsManager.start();
        }

        //broker 快速失败启动
        if (this.brokerFastFailure != null) {
            this.brokerFastFailure.start();
        }
 }

启动过程还有很多细节没有分析,放到下个文章吧吧吧吧吧

文章大量参考:https://www.zhihu.com/column/c_1437729921845690368

相关推荐
web150854159356 小时前
从零到一:Spring Boot 与 RocketMQ 的完美集成指南
spring boot·rocketmq·java-rocketmq
续亮~21 小时前
RocketMQ 学习笔记01
笔记·学习·rocketmq
简单的东西为什么越来越复杂1 天前
RocketMQ 安装使用
后端·面试·rocketmq
喵王叭4 天前
RocketMQ 知识速览
rocketmq
大梦谁先觉i5 天前
RocketMQ、Kafka、RabbitMQ,如何选型?
kafka·rabbitmq·rocketmq
笑我归无处6 天前
云服务器安装RocketMQ教程
运维·服务器·rocketmq
怪只怪满眼尽是人间烟火7 天前
Spring Boot集成RocketMQ
spring boot·rocketmq·java-rocketmq
问仙长何方蓬莱8 天前
中间件 | RocketMq - [broker 配置]
中间件·rocketmq
veminhe10 天前
安装rocketmq dashboard
rocketmq