RocketMQ源码分析1:NameServer启动流程

817 阅读6分钟

本文已参与[新人创作礼]活动,一路开启掘金创作之路。

基于rocketmq-4.9.0 版本分析rocketmq

1.NameServer是什么?

参考官方文档

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

  1. Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
  2. 路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息

2.NameServer启动入口类

public class NamesrvStartup {

    private static InternalLogger log;
    private static Properties properties = null;
    private static CommandLine commandLine = null;

    public static void main(String[] args) {
        main0(args);
    }

    public static NamesrvController main0(String[] args) {

        try {
            NamesrvController controller = createNamesrvController(args);
            //TODO:NameServer启动
            start(controller);
            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
            log.info(tip);
            System.out.printf("%s%n", tip);
            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }

知道了入口,那接下来我们就分析下它是如何启动的,启动的过程中都做了什么?从上面的部分代码中,不难看出,它首先就是创建NamesrvController对象,然后启动(start()方法),那我们就一步一步走进去看看细节.

3.创建 NamesrvController 对象

不要看代码比较多,其实主要就是创建3个对象,请往后看

public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
    System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
    //PackageConflictDetect.detectFastjson();

    Options options = ServerUtil.buildCommandlineOptions(new Options());
    commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
    if (null == commandLine) {
        System.exit(-1);
        return null;
    }

    //TODO:创建NamesrvConfig对象
    final NamesrvConfig namesrvConfig = new NamesrvConfig();
    //TODO: 创建 NettyServerConfig 对象
    final NettyServerConfig nettyServerConfig = new NettyServerConfig();
    //TODO: 设置固定端口号9876,客户端连接的时候就是 nameserverip:9876
    nettyServerConfig.setListenPort(9876);
    //TODO: 读取命令行中 -c 参数值(如果指定该参数,它是一个配置文件)
    if (commandLine.hasOption('c')) {
        String file = commandLine.getOptionValue('c');
        if (file != null) {
            InputStream in = new BufferedInputStream(new FileInputStream(file));
            properties = new Properties();
            properties.load(in);
            MixAll.properties2Object(properties, namesrvConfig);
            MixAll.properties2Object(properties, nettyServerConfig);

            namesrvConfig.setConfigStorePath(file);

            System.out.printf("load config properties file OK, %s%n", file);
            in.close();
        }
    }

    //TODO: 判断是否有 -p 参数值
    if (commandLine.hasOption('p')) {
        InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
        MixAll.printObjectProperties(console, namesrvConfig);
        MixAll.printObjectProperties(console, nettyServerConfig);
        System.exit(0);
    }

    //TODO:将命令行参数映射到 NamesrvConfig类中
    MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

    //TODO: 判断是否指定ROCKETMQ_HOME,本地运行时,必须要指定该值
    if (null == namesrvConfig.getRocketmqHome()) {
        System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
        System.exit(-2);
    }

    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
    JoranConfigurator configurator = new JoranConfigurator();
    configurator.setContext(lc);
    lc.reset();
    configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");

    log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);

    MixAll.printObjectProperties(log, namesrvConfig);
    MixAll.printObjectProperties(log, nettyServerConfig);

    //TODO: 创建  NamesrvController 对象
    final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

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

    return controller;
}

总结一下就是创建3个对象

  1. 创建 NamesrvConfig对象,设置一些配置,比如ROCKETMQ_HOME
public class NamesrvConfig {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
    //TODO: 指定ROCKETMQ_HOME,必须,否则本地运行将失败
    private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
    //TODO: 可以在这个路径下创建kvConfig.json文件,它将会读取值
    private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
    //TODO: 可以在这个路径下创建namesrv.properties文件,它将会读取值
    private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
    private String productEnvName = "center";
    private boolean clusterTest = false;
    private boolean orderMessageEnable = false;
  1. 创建 NettyServerConfig对象,设置固定端口号9876,生产者和消费者连接的时候通过namesrvip:9876进行连接
  2. 创建 NamesrvController对象,并将其两个对象设置进来,除此以外还有一些其他对象
public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) {
    this.namesrvConfig = namesrvConfig;
    this.nettyServerConfig = nettyServerConfig;
    //TODO:读取NamesrvConfig中kvConfigPath路径下的文件
    this.kvConfigManager = new KVConfigManager(this);
    //TODO:这个对象非常重要,nameserver存储的broker信息和路由表信息都保存在这里
    this.routeInfoManager = new RouteInfoManager();
    this.brokerHousekeepingService = new BrokerHousekeepingService(this);
    this.configuration = new Configuration(log, this.namesrvConfig, this.nettyServerConfig);
    this.configuration.setStorePathFromConfig(this.namesrvConfig, "configStorePath");
}

4.启动过程(NamesrvController#start()

4.1 初始化(NamesrvController#initialize()

public boolean initialize() {
    //TODO: 读取NamesrvConfig中的指定路径的文件,然后保存在配置表中(Map)
    this.kvConfigManager.load();

    //TODO:创建远程netty服务,这个很重要,后面还会看到
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

    //TODO:创建线程池
    this.remotingExecutor =
        Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

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

    //TODO:启动定时任务,扫描不活跃的broker
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

        @Override
        public void run() {
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);
    
    //TODO: ....省略部分代码......

    return true;
}

4.1.1 加载配置

this.kvConfigManager.load();

在前面创建 NamesrvConfig对象时,其内部有固定的kvConfigPath属性,如果存在这个文件,那么KVConfigManager对象将在这里读取,然后保存在其内部的配置表中configTable(Map)

4.1.2 创建远程服务NettyRemotingServer对象

this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

我们看它的构造函数都做了什么

public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
    final ChannelEventListener channelEventListener) {
    super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());
    //TODO:创建ServerBootstrap对象,它是netty的服务端启动类
    this.serverBootstrap = new ServerBootstrap();
    this.nettyServerConfig = nettyServerConfig;
    this.channelEventListener = channelEventListener;

    int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
    if (publicThreadNums <= 0) {
        publicThreadNums = 4;
    }

    //TODO:创建一个线程池
    this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() {
        private AtomicInteger threadIndex = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "NettyServerPublicExecutor_" + this.threadIndex.incrementAndGet());
        }
    });

   //TODO:这里有一个usePoll的判断,为了减少篇幅,我移除了;无论是否为true,主要都是创建下面两个对象
   {
    //TODO:负责监听TCP网络连接请求
    this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() {
        private AtomicInteger threadIndex = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, String.format("NettyNIOBoss_%d", this.threadIndex.incrementAndGet()));
        }
    });

    //TODO: 在eventLoopGroupBoss接收到连接请求时,负责将建立好连接的socket注册到selector中去
    this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
        private AtomicInteger threadIndex = new AtomicInteger(0);
        private int threadTotal = nettyServerConfig.getServerSelectorThreads();

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, String.format("NettyServerNIOSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet()));
        }
    });
   }

   //TODO:....省略代码......
}

总结一下创建对象时都做了什么

  1. 创建netty服务端启动类ServerBootstrap
  2. 创建一个线程池publicExecutor,他的作用就是一个默认线程池,不过这个线程池在nameserver这里其实并没有怎么使用(在broker启动的时候也会创建NettyRemotingServer对象,broker在注册处理器时,如果没有指定线程池,那么就会使用这个默认的线程池)
  3. eventLoopGroupBosseventLoopGroupSelector线程组:它俩分别是netty用来处理连接事件与读写事件的线程了,eventLoopGroupBoss对应的是netty的boss线程组,eventLoopGroupSelector对应的是worker线程组

4.1.3 创建线程池

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

NettyRemotingServer对象有2个比较重要的方法

void registerProcessor(final int requestCode, final NettyRequestProcessor processor,
    final ExecutorService executor);

void registerDefaultProcessor(final NettyRequestProcessor processor, final ExecutorService executor);

不难发现,第三个参数是线程池,nameserver会调用registerDefaultProcessor方法注册处理器,而使用的线程池就是上面创建的remotingExecutor(请看步骤4.1.4)

4.1.4 注册处理器

//TODO:核心逻辑就这一行
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);

不难发现,它注册了一个默认的处理器对象DefaultRequestProcessor,同时指定的线程池就是4.1.3中创建的线程池。
注册逻辑:

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

4.1.5 启动定时任务

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

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

启动定时任务,定时扫描不活跃的broker,然后将其从注册表中移除。

到这里 “初始化”过程就结束了,接下来就是启动了。

4.2 启动

public void start() throws Exception {
    this.remotingServer.start();
    //TODO:....省略部分代码......
}

调用remotingServerNettyRemotingServer)的start()方法

public void start() {
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
        nettyServerConfig.getServerWorkerThreads(),
        new ThreadFactory() {

            private AtomicInteger threadIndex = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "NettyServerCodecThread_" + this.threadIndex.incrementAndGet());
            }
        });

    //TODO:创建一些对象
    prepareSharableHandlers();

    ServerBootstrap childHandler =
        //TODO:首先设置 bossGroup和workerGroup
        this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
            //TODO:判断使用 epoll 还是 nio
            .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
            //TODO:一些其他选项
            .option(ChannelOption.SO_BACKLOG, 1024)
            .option(ChannelOption.SO_REUSEADDR, true)
            .option(ChannelOption.SO_KEEPALIVE, false)
            .childOption(ChannelOption.TCP_NODELAY, true)
            .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())
            .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())
            //TODO:绑定ip + 端口
            .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
            //TODO:绑定handler
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline()
                        .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                        .addLast(defaultEventExecutorGroup,
                            encoder,
                            new NettyDecoder(),
                            //TODO:处理心跳的handler
                            new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                            //TODO:处理连接的handler
                            connectionManageHandler,
                            //TODO:处理读写请求的handler,在上面prepareSharableHandlers()方法中创建的对象
                            serverHandler
                        );
                }
            });

    if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
        childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }

    try {
        //TODO: 启动netty服务端
        ChannelFuture sync = this.serverBootstrap.bind().sync();
        InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
        this.port = addr.getPort();
    } catch (InterruptedException e1) {
        throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);
    }

    //TODO:......省略部分代码........
}

不难发现,这个方法就是真正的在启动netty服务端了,简单总结下关注的点

  1. 设置bossGroup和workerGroup线程组。这是netty中两个执行任务的循环线程组,bossGroup负责处理accept事件,workerGroup负责处理read/write事件.
  2. 决策使用epoll还是nio channel
  3. 绑定ip和端口
  4. 添加handler。如果Channel是出现了连接/读/写等事件,这些事件会经过Pipeline上的ChannelHandler上进行流转
  • HandshakeHandler: 负责处理握手操作
  • NettyEncoder:负责处理报文编码和解码
  • IdleStateHandler:负责处理心跳
  • NettyConnectManageHandler: 负责处理连接
  • NettyServerHandler:负责处理读写请求。这个ChannelHandler就是用来处理broker注册消息以及producer/consumer获取topic消息的。这个要特别关注下,后面我们会看到。
  1. 启动
//TODO:netty启动
ChannelFuture sync = this.serverBootstrap.bind().sync();

至此,NamesrvController对象的start()方法就执行完毕了。NameServer算了启动起来了,然后就是等待外部连接。

5.总结

本文分析了NameServer的启动过程,整个过程大概可以总结为:

  1. 创建NamesrvController对象,并设置必要的配置类。
  2. 初始化NamesrvController对象,主要是构建NettyRemotingServer对象(其内部维护了netty服务端启动对象ServerBootstrap
  3. 启动NamesrvController对象,就是启动NettyRemotingServer对象,进一步是启动netty服务端对象ServerBootstrap,这样就可以进行连接,读写操作了。

NameServer是存储broker信息以及topic路由表信息,那么它存储在哪里了呢?

在最开始创建NamesrvController对象时,其内部会创RouteInfoManager对象,而这个对象就是存储信息的容器,下一章节我们就看到它的存储细节。

限于作者个人水平,文中难免有错误之处,欢迎指正! 勿喷,感谢