NameServer架构设计
NameServer可以说是RocketMQ的“大脑”。她负责路由管理、服务注册及服务发现。客户端想要往服务端发送消息或者从服务端订阅消息时,必须要知道服务端的路由信息。在服务端集群部署下,更加尤为重要。为了解决这个问题,必须引入类似zookeeper这类分布式协调中心。那么业界已有ZK这个神器,为何RocketMQ要架设NameServer,不用ZK呢?
原因很简单,ZK具有强一致性,在ZK、消息服务端都在大规模集群环境下,网络通信将变得异常庞大复杂,尤其是ZK。所以RocketMQ架设了NameServer。NameServer在设计就定位简单,确实简单得不得了,总共才8个类。但是,简单关简单,她却很强大!
接下来,我们先来看看RocketMQ的逻辑部署图,这张图将贯穿整个RocketMQ专栏。
Broker消息服务器在启动时向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer与每台Broker服务器保持长链接,并间隔30s检测Broker是否存活,如果检测到Broker宕机,则从路由注册列表中将其移除。但是路由变化不会马上通知消息生产者。
NameServer本身的高可用可通过集群部署来实现,但是彼此之间互不通信,也就是说,存在某一个时刻,NameServer服务器之间的数据并不会完全相同。但这对消息发送不会造成任何影响。
不知道读者是否也有这样的疑问?路由变化不是实时通知,NameServer服务器之间数据不同步,那么生产者拿到宕机的路由信息是极有可能的,消息发送的高可用何以体现?
原因我们不着急分析,留待下文【消息发送】逐一揭开。在这里我们要转换思考点,尝试去理解为什么要这样设计?因为要降低NameServer实现的复杂度,同时这也是NameServer的一个设计亮点。
总结:
- NameServer通过心跳检测到Broker不可用时,并不会马上通知客户端
- NameServer集群中各节点互不通信
NameServer大道至简到哪怕所有NameServer所有节点宕机,依旧能在一定条件下保证RocketMQ稳定运行。这里的一定条件可以是NameServer所有节点宕机期间内,Broker集群所有节点没有全宕机。不得不钦佩设计者们!
NameServer启动流程
从这一节开始,开始进入源码分析。启动NameServer,需要关注NameServer相关启动参数。
我们看一下启动类org.apache.rocketmq.namesrv.NamesrvStartup,会创建一个NameServer核心控制器–NamesrvController 。
public static void main(String[] args) {
main0(args);
}
public static NamesrvController main0(String[] args) {
try {
NamesrvController controller = createNamesrvController(args);
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;
}
创建核心控制器时,需要两个配置对象NamesrvConfig 、NettyServerConfig 。可以发现底层网络通信是基于Netty实现的。两个配置对象是在NameServer启动时把指定的配置文件或启动命令中的选项值解析出来,并填充到各自对象内。
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
nettyServerConfig.setListenPort(9876);
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();
}
}
// 启动命令的参数选项
if (commandLine.hasOption('p')) {
InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
MixAll.printObjectProperties(console, namesrvConfig);
MixAll.printObjectProperties(console, nettyServerConfig);
System.exit(0);
}
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
}
参数来源具体是怎样的呢?
- -c configFile 通过-c命令指定配置文件的路径。
- 使用 “–属性名 属性值” 例如 --listenPort 9876
我们来具体看看这两个对象的参数属性,了解一下即可。
NamesrvConfig:
/**
* rocketmq 主目录 可以通过-Drocketmq.home.dir=path或通过设置环境变量ROCKETMQ_HOME来配置ROCKETMQ的主目录
*/
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
/**
* NameServer 存储KV配置属性的持久化路径
*/
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
/**
* NameServer 默认配置文件路径,不生效。NameServer启动时如果需要通过配置文件配置NameServer启动属性的话,请使用-c选项
*/
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;
NettyServerConfig:
/**
* NameServer监听端口,该值默认会被初始化为9876
*/
private int listenPort = 8888;
/**
* Netty业务线程池线程个数
*/
private int serverWorkerThreads = 8;
/**
* Netty public 任务线程池线程个数,netty网络设计,
* 根据业务类型会创建不同的线程池,比如处理消息发送、消息消费、心跳检测等
* 如果该业务类型(RequestCode)未注册线程池,则由public线程池执行
*/
private int serverCallbackExecu