本文首发于Ressmix个人站点:https://www.tpvlog.com
上一章,我们已经搭建好了RocketMQ的源码环境。从本章开始,我们正式进入RocketMQ的源码分析环节。分析一个开源框架的源码,必然从开源框架的入口开始。
在RocketMQ使用的时候,第一个步骤一定是先启动NameServer,那么我们就先来分析NameServer启动这块的源码。
一、启动入口
1.1 启动脚本
我之前在《RocketMQ生产部署》一章中,讲解过NameServer的启动,启动脚本在distribution模块的bin目录下——mqnamesrv
。
这个脚本中有极为关键的一行命令用于启动NameServer进程:
1sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@
可以看到,上面的命令其实是执行了runserver.sh
这个脚本,然后通过这个脚本去启动了NamesrvStartup
这个Java类,下面是runserver.sh
这个脚本的一些内容:
1JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
2JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"
3JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails"
4JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
5JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
6JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"
7JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib"
8#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
9JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
10JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
11
12$JAVA ${JAVA_OPT} $@
其实就是通过java
命令去执行NamesrvStartup.main()
方法,启动一个JVM进程:
1.2 NamesrvStartup类
我们来看下NamesrvStartup类的main方法:
1public class NamesrvStartup { 2 3 // 忽略这些跟主体逻辑相关度不大的分支代码 4 private static InternalLogger log; 5 private static Properties properties = null; 6 private static CommandLine commandLine = null; 7 8 public static void main(String[] args) { 9 main0(args);10 }1112 public static NamesrvController main0(String[] args) {1314 try {15 NamesrvController controller = createNamesrvController(args);16 start(controller);17 String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();18 log.info(tip);19 System.out.printf("%s%n", tip);20 return controller;21 } catch (Throwable e) {22 e.printStackTrace();23 System.exit(-1);24 }2526 return null;27 }2829 //...30}
可以看到,在main()
方法内部最核心的逻辑是创建了一个NamesrvController
对象,然后调用start(controller)
方法来启动这个Controller。
二、创建NamesrvController
NamesrvController是什么呢?其实从命名就可以看出这是一个控制器,熟悉Spring的童鞋应该不会陌生,Controller一般用于接受请求,那么NameServer接受什么请求呢?当然是Broker的注册请求、心跳请求,以及Producer和Consumer的拉取路由信息请求。
NamesrvController这个组件,就是NameServer专门用来接受Broker和客户端的网络请求的一个组件:
2.1 构建NameServer配置对象
我们来看下创建NamesrvController的代码:
1public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException { 2 System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); 3 //PackageConflictDetect.detectFastjson(); 4 5 // 解析命令行中的参数 6 Options options = ServerUtil.buildCommandlineOptions(new Options()); 7 commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser()); 8 if (null == commandLine) { 9 System.exit(-1);10 return null;11 }1213 final NamesrvConfig namesrvConfig = new NamesrvConfig();14 final NettyServerConfig nettyServerConfig = new NettyServerConfig();15 nettyServerConfig.setListenPort(9876);16 if (commandLine.hasOption('c')) {17 String file = commandLine.getOptionValue('c');18 if (file != null) {19 // 读取外部配置文件的内容20 InputStream in = new BufferedInputStream(new FileInputStream(file));21 properties = new Properties();22 properties.load(in);23 MixAll.properties2Object(properties, namesrvConfig);24 MixAll.properties2Object(properties, nettyServerConfig);2526 namesrvConfig.setConfigStorePath(file);2728 System.out.printf("load config properties file OK, %s%n", file);29 in.close();30 }31 }3233 if (commandLine.hasOption('p')) {34 InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);35 MixAll.printObjectProperties(console, namesrvConfig);36 MixAll.printObjectProperties(console, nettyServerConfig);37 System.exit(0);38 }3940 MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);4142 // 如果ROCKETMQ_HOME为空,就报错退出,这就是我们为什么必须设置ROCKETMQ_HOME环境变量的原因43 if (null == namesrvConfig.getRocketmqHome()) {44 System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);45 System.exit(-2);46 }4748 // 日志相关配置49 LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();50 JoranConfigurator configurator = new JoranConfigurator();51 configurator.setContext(lc);52 lc.reset();53 configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");5455 log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);5657 MixAll.printObjectProperties(log, namesrvConfig);58 MixAll.printObjectProperties(log, nettyServerConfig);5960 final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);6162 // remember all configs to prevent discard63 controller.getConfiguration().registerConfig(properties);6465 return controller;66}
别看上面代码一大堆,其实核心就做了一件事情:解析命名行中的相关参数,然后构建出两个配置对象——NamesrvConfig
和NettyServerConfig
。
我们在启动NameServer的时候,是使用mqnamesrv
命令来启动的,启动的时候可能会在命令行里带入一些参数,所以很上面那块代码,就是解析一下我们传递进去的一些命令行参数而已!
这里最关键的是创建了两个配置对象:
1final NamesrvConfig namesrvConfig = new NamesrvConfig();2final NettyServerConfig nettyServerConfig = new NettyServerConfig();3nettyServerConfig.setListenPort(9876);
NamesrvConfig:包含的是NameServer自身运行的一些配置参数,NameServer默认监听请求的端口号是9876,用来接收Broker和客户端的请求;NettyServerConfig:包含的是用于接收网络请求的Netty服务器的配置参数。
我们以NamesrvConfig为例,看下里面的内容,其实就是些NameServer的默认配置:
1public class NamesrvConfig { 2 private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); 3 4 private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); 5 private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json"; 6 private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties"; 7 private String productEnvName = "center"; 8 private boolean clusterTest = false; 9 private boolean orderMessageEnable = false;1011 //...12}
2.2 解析配置文件
我们具体来看下是如何解析配置文件的:
1// 覆盖配置文件中的配置到配置类中 2if (commandLine.hasOption('c')) { 3 String file = commandLine.getOptionValue('c'); 4 if (file != null) { 5 InputStream in = new BufferedInputStream(new FileInputStream(file)); 6 properties = new Properties(); 7 properties.load(in); 8 MixAll.properties2Object(properties, namesrvConfig); 9 MixAll.properties2Object(properties, nettyServerConfig);1011 namesrvConfig.setConfigStorePath(file);1213 System.out.printf("load config properties file OK, %s%n", file);14 in.close();15 }16}1718// 打印配置信息19if (commandLine.hasOption('p')) {20 InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);21 MixAll.printObjectProperties(console, namesrvConfig);22 MixAll.printObjectProperties(console, nettyServerConfig);23 System.exit(0);24}
上面的代码意思是说:在启动NameServer的时候,如果用-c
选项带上了一个配置文件路径,那么运行到上面的代码,就会把配置文件里的配置,放入两个核心配置类里去。比如有一个配置文件是:nameserver.properties,里面有一个配置是serverWorkerThreads=16
,那么上面的代码就会读取出来这个配置,然后覆盖到NettyServerConfig里去!
三、启动NamesrvController
我们回到主流程,构建完了NamesrvController对象后,就执行start()
方法来启动NamesrvController了:
1public static NamesrvController start(final NamesrvController controller) throws Exception { 2 3 if (null == controller) { 4 throw new IllegalArgumentException("NamesrvController is null"); 5 } 6 7 // 初始化NamesrvController 8 boolean initResult = controller.initialize(); 9 if (!initResult) {10 controller.shutdown();11 System.exit(-3);12 }1314 // 注册一个shutdown钩子,JVM关闭时会执行15 Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {16 @Override17 public Void call() throws Exception {18 controller.shutdown();19 return null;20 }21 }));2223 // 启动NamesrvController24 controller.start();2526 return controller;27}
3.1 初始化NamesrvController
start方法的核心就是先执行controller.initialize()
初始化NamesrvController,然后执行controller.start()
启动NamesrvController。我们先来看下initialize方法:
1public boolean initialize() { 2 3 // kvConfigManager用于管理KV配置 4 this.kvConfigManager.load(); 5 6 // 构建NettyRemotingServer对象,其实就是一个Netty网络服务器 7 this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); 8 9 // 创建Netty服务器的工作线程池10 this.remotingExecutor =11 Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));1213 // 把工作线程池将给Netty管理14 this.registerProcessor();1516 // 启动一个定时任务,扫描那些没发送心跳的Broker17 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {18 @Override19 public void run() {20 NamesrvController.this.routeInfoManager.scanNotActiveBroker();21 }22 }, 5, 10, TimeUnit.SECONDS);2324 // 启动一个定时任务,打印KV配置信息25 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {26 @Override27 public void run() {28 NamesrvController.this.kvConfigManager.printAllPeriodically();29 }30 }, 1, 10, TimeUnit.MINUTES);3132 //...33 return true;34}
controller.initialize()
方法,核心就是把NettyRemotingServer
网络服务器组件给构造了出来,其内部用到了Netty的核心类——ServerBootstrap:
1public NettyRemotingServer(final NettyServerConfig nettyServerConfig, 2 final ChannelEventListener channelEventListener) { 3 super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); 4 5 // Netty核心类 6 this.serverBootstrap = new ServerBootstrap(); 7 this.nettyServerConfig = nettyServerConfig; 8 this.channelEventListener = channelEventListener; 910 //...11}
3.2 启动Netty Server
初始化完NamesrvController后,我们再来看下NamesrvController的启动:
1public void start() throws Exception {2 this.remotingServer.start();34 if (this.fileWatchService != null) {5 this.fileWatchService.start();6 }7}
NamesrvContorller的启动,核心就是内部的NettyRemotingServer的启动,这段代码没什么好说的,都是些Netty API的代码:
1public void start() {
2
3 // 对Netty的各种配置,核心就是基于Netty的API去配置和启动一个网络服务器
4 this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
5 nettyServerConfig.getServerWorkerThreads(),
6 new ThreadFactory() {
7 private AtomicInteger threadIndex = new AtomicInteger(0); 8 @Override 9 public Thread newThread(Runnable r) {10 return new Thread(r, "NettyServerCodecThread_" + this.threadIndex.incrementAndGet());11 }12 });1314 prepareSharableHandlers();1516 ServerBootstrap childHandler =17 this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)18 .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)19 .option(ChannelOption.SO_BACKLOG, 1024)20 .option(ChannelOption.SO_REUSEADDR, true)21 .option(ChannelOption.SO_KEEPALIVE, false)22 .childOption(ChannelOption.TCP_NODELAY, true)23 .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())24 .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())25 .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))26 .childHandler(new ChannelInitializer() {27 @Override28 public void initChannel(SocketChannel ch) throws Exception {29 ch.pipeline()30 .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)31 .addLast(defaultEventExecutorGroup,32 encoder,33 new NettyDecoder(),34 new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),35 connectionManageHandler,36 serverHandler37 );38 }39 });4041 if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {42 childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);43 }4445 try {46 // 核心是这里,bind方法就是绑定和监听指定端口,默认是987647 ChannelFuture sync = this.serverBootstrap.bind().sync();48 InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();49 this.port = addr.getPort();50 } catch (InterruptedException e1) {51 throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);52 }5354 if (this.channelEventListener != null) {55 // 启动netty服务56 this.nettyEventExecutor.start();57 }5859 this.timer.scheduleAtFixedRate(new TimerTask() {60 @Override61 public void run() {62 try {63 NettyRemotingServer.this.scanResponseTable();64 } catch (Throwable e) {65 log.error("scanResponseTable exception", e);66 }67 }68 }, 1000 * 3, 1000);69}
三、总结
本章,我分析了NameServer的启动源码,了解到它最核心的就是基于Netty实现了一个网络服务器,然后监听默认的9876端口,这样就可以接收Broker和客户端的网络请求了。