博客从RocketMQ我们学到了什么之NameServer以邮局的功能作为类比,通俗易懂地介绍了RocketMQ中的NameServer在整个框架中的作用。而本篇文章,是以源码阅读笔记的形式,记录学习RocketMQ的过程。
首先,NameServer的启动类为org.apache.rocketmq.namesrv.NamesrvStartup,方法的流程很简单:
1. 读取配置。从启动命令中读取配置文件,如果配置文件中有修改NamesrvConfig或NettyServerConfig设置的默认值,会将配置文件中的值覆盖NamesrvConfig或NettyServerConfig中的选项。NamesrvConfig、NettyServerConfig的配置项如下:
public class NamesrvConfig {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
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;
}
public class NettyServerConfig implements Cloneable {
private int listenPort = 8888; // 服务端监听端口
// 执行ChannelHandler的方法(例如channelRead()等方法)的线程组
private int serverWorkerThreads = 8;
// 执行请求回调方法的线程组线程数
private int serverCallbackExecutorThreads = 0;
// netty worker线程的数量(另外,Boss线程组的数量为1)
private int serverSelectorThreads = 3;
// 单向请求、异步请求的最大并发量,超过默认数值,会阻塞
private int serverOnewaySemaphoreValue = 256;
private int serverAsyncSemaphoreValue = 64;
// 监听客户端心跳的最大空闲时间,单位s
private int serverChannelMaxIdleTimeSeconds = 120;
// netty发送/接收数据的缓冲区大小, 默认值65535,可通过设置系统属性修改
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
// netty是否使用PooledByteBufAllocator也就是内存池,默认使用
private boolean serverPooledByteBufAllocatorEnable = true;
// netty使用EpollEventLoopGroup还是NioEventLoopGroup
private boolean useEpollNativeSelector = false;
}
2. 创建NamesrvController。使用NamesrvConfig、NettyServerConfig创建实例。
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
3. 启动NamesrvController。代码如下:
public static NamesrvController start(final NamesrvController controller) throws Exception {
if (null == controller) {
throw new IllegalArgumentException("NamesrvController is null");
}
boolean initResult = controller.initialize();
if (!initResult) {
controller.shutdown();
System.exit(-3);
}
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
controller.start();
return controller;
}
首先,初始化NamesrvController的各个组件,如果失败,直接停止;然后,添加关闭钩子(所谓关闭钩子,是指进程在停止时执行的一个任务),在进程关闭时停止NameServer服务。最后,真正启动NamesrvController。
下面主要关心initialize()以及start()方法:
public boolean initialize() {
// KVConfigManager装载写入文件中的key和value
this.kvConfigManager.load();
// 启动netty服务器
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// NameServer接收到RemotingCommand请求后,在此线程组中执行请求,并返回结果
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// 注册NameServer的请求处理器DefaultRequestProcessor
this.registerProcessor();
// 设置定时任务,扫描不活跃的Broker,5秒后开始执行,以后每10秒扫描一次
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 1分钟后每隔10分钟,打印一次KVConfigManager的属性
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
// 初始化TLS配置监听线程FileWatchService,当TLS证书或密码文件有修改时,通过HASH码校验重载配置
if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
// Register a listener to reload SslContext
try {
fileWatchService = new FileWatchService(
new String[] {
TlsSystemConfig.tlsServerCertPath,
TlsSystemConfig.tlsServerKeyPath,
TlsSystemConfig.tlsServerTrustCertPath
},
new FileWatchService.Listener() {
boolean certChanged, keyChanged = false;
@Override
public void onChanged(String path) {
if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
log.info("The trust certificate changed, reload the ssl context");
reloadServerSslContext();
}
if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
certChanged = true;
}
if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
keyChanged = true;
}
if (certChanged && keyChanged) {
log.info("The certificate and private key changed, reload the ssl context");
certChanged = keyChanged = false;
reloadServerSslContext();
}
}
private void reloadServerSslContext() {
((NettyRemotingServer) remotingServer).loadSslContext();
}
});
} catch (Exception e) {
log.warn("FileWatchService created error, can't load the certificate dynamically");
}
}
return true;
}
public void start() throws Exception {
// 启动Netty服务器,具体内容参考介绍NettyRemotingServer的文章
this.remotingServer.start();
// 启动监听TLS配置文件的线程
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
到此为止,NameServer的启动流程就结束了。相对而言,NameServer组件较少,功能比较简单,就像官网上说的NameServer是一种无状态的服务,它仅仅记录所有的Broker的注册地址,topic的分布情况等。暂时对KVConfigManager的功能未知,看代码也是作为一种集中式的存储key-value,像是一个简单的配置中心。
目前为止,RouteInfoManager组件没有介绍到,这就是NameServer用来存储Broker的注册地址和topic的位置的组件。
之前在NettyRemotingServer的文章中介绍到,所有的请求封装为RemotingCommand后,交给对应的NettyRequestProcessor处理。在NameServer中,设置了一个DefaultRequestProcessor,在其processRequest()方法中有请求的处理方法,其中注册Broker请求如下:
switch (request.getCode()) {
// 其他请求略过
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);
}
case RequestCode.UNREGISTER_BROKER:
return this.unregisterBroker(ctx, request);
case RequestCode.GET_ROUTEINTO_BY_TOPIC:
return this.getRouteInfoByTopic(ctx, request);
case RequestCode.GET_BROKER_CLUSTER_INFO:
return this.getBrokerClusterInfo(ctx, request);
case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
return this.wipeWritePermOfBroker(ctx, request);
// 其他请求略过...
default:
break;
}
看看Broker注册的代码:
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
throws RemotingCommandException {
// 创建response对象
final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
// 从RemotingCommand中解析出Broker注册参数RegisterBrokerResponseHeader
final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
final RegisterBrokerRequestHeader requestHeader =
(RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
if (!checksum(ctx, request, requestHeader)) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("crc32 not match");
return response;
}
RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
if (request.getBody() != null) {
try {
registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
} catch (Exception e) {
throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
}
} else {
registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
}
// 注册Broker
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
requestHeader.getBrokerAddr(),
requestHeader.getBrokerName(),
requestHeader.getBrokerId(),
requestHeader.getHaServerAddr(),
registerBrokerBody.getTopicConfigSerializeWrapper(),
registerBrokerBody.getFilterServerList(),
ctx.channel());
responseHeader.setHaServerAddr(result.getHaServerAddr());
responseHeader.setMasterAddr(result.getMasterAddr());
byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
response.setBody(jsonValue);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
继续关注this.namesrvController.getRouteInfoManager().registerBroker()方法:
public RegisterBrokerResult registerBroker(
final String clusterName,
final String brokerAddr,
final String brokerName,
final long brokerId,
final String haServerAddr,
final TopicConfigSerializeWrapper topicConfigWrapper,
final List filterServerList,
final Channel channel) {
RegisterBrokerResult result = new RegisterBrokerResult();
try {
try {
this.lock.writeLock().lockInterruptibly();
// 注册集群信息
Set brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
boolean registerFirst = false;
// 注册Broker
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap());
this.brokerAddrTable.put(brokerName, brokerData);
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
if (null != topicConfigWrapper
&& MixAll.MASTER_ID == brokerId) {
// 检查Version,如果version不一致,说明有更新。第一次注册也要更新queueData
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
|| registerFirst) {
// 获取Broker当前的topic配置
ConcurrentMap tcTable =
topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry entry : tcTable.entrySet()) {
// 在NameServer上,更新Broker的topic信息
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
// 更新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);
}
// 更新服务器过滤列表,其功能暂时未知,需结合broker了解其功能
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
// 根据master的属性设置haServer
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);
}
}
}
} finally {
this.lock.writeLock().unlock();
}
} catch (Exception e) {
log.error("registerBroker Exception", e);
}
return result;
}
Netty的线程模型也就是多线程Reactor模型,使用拥有少量线程的、独立的Acceptor线程池专门处理NIO的accept事件,生成channel。使用线程组处理消息的拆包、解码、业务处理和返回消息等。
RocketMQ中使用Netty作为网络通讯工具,它使用的线程模型与Netty一般使用的线程模型略有不同,模型如图所示:
上图画出了客户端发起连接请求,到发送Request请求,再到返回response结果以及执行回调的过程。其步骤如下:
综上,可以看出RocketMQ线程模型一个很重要的想法:针对不同的用途设置专用的线程组,这样可以根据业务需求精确的调整用于每个部分的线程组的数量。
首先还是来看RocketMQ的架构:
1.每个Broker与NameServer都有一个连接,而NameServer与NameServer之间没有连接,从代码上来说也没有数据的同步,说明NameServer和Zookeeper不一样,不保证数据的一致性,是AP的。
2.从Broker的注册代码来看,一个BrokerName可以包含一个Matser Broker和多个Slave Broker,其中Master的id为0,slave的id为1,2,3...
3.Broker心跳超时时,会关闭Channel。从NameServer的定时任务来看,RocketMQ每隔10秒钟扫描一次Broker检测最后一次发消息的时间,如果超过两分钟,则会关闭channel。说明RocketMQ的默认失效时间为2分钟。
4.当Broker失效后,NameServer只能在2分钟后将其从Broker存活列表中删除,在这段时间中,发送消息时通过失败选择下一个Broker重试的方法,规避NameServer保证数据一致性的复杂设计。
5.RocketMQ针对不同的用途设置专用的线程组,可以根据业务需求精确的调整用于每个部分的线程组的数量。
1.KVConfigManager是一个包含命名空间的KV配置存储管理组件,他在RocketMQ中起到什么样的作用?
2.暂时不理解注册Broker时,传入的参数filterServerList的含义。
3.在注册Broker的代码的后面,有这样一行代码:
result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
其中brokerLiveInfo是master,说明如果broker不是Master,那么这里返回的result中设置的负载均衡的server地址是Master传过来的地址,但是往brokerLiveTable中添加的又是请求传过来的haServer,就是说两者有可能不一样,不知道这里有什么区别。另外,当同属于brokerName为broker-a的一个slave broker先于master启动,那么这里result的haServer又是空的,不知道这种情况是否有影响,或者RocketMQ是否有master必定先于slave启动的要求?