消息中间件目前是RocketMQ和Kafka两家独大,RocketMQ作为java开源代码,很适合中间件开发人员作为一手的学习材料,设计中的很多思想都是值得学习的。废话不多,进入正题。
一、整体架构
书中是从Nameserver切入整个RocketMQ的,但我这里从整体入手,希望你能先牢记这张图
包含几个重要部分:
Producer集群,NameServer集群,Broker集群以及Consumer集群。如果你熟悉微服务架构,那么整个集群和Eureka是非常像的:由Nameserver集群提供注册和发现,Producer集群负责生产消息,Consumer集群负责消息消费,Broker集群负责转发并持久化消息;NameServer集群保证AP,各个节点通过NameServer集群进行通信并确认状态。
是不是感觉很简单,没错架构是很简单。如果你了解分布式架构,那么你一定会提出一个问题:RocketMQ是否需要保证一致性?如果你了解过Kafka你也会问,为什么RocketMQ里没有ZK去保证Broker集群的高可用?这是因为RockerMQ在设计之初,就希望通过规避复杂的ZK集群而设计了自己的NameServer,这也导致RocketMQ在最初无法保证高可用,Broker节点虽然可以主从,但是Master写节点宕机之后该Broker是无法提供服务的。以至于后来,RocketMQ又根据Raft协议实现了自己的高可用架构Dledger,这都是后话了。
注意,如果你想部署一个RocketMQ,你要分别部署NameServer,Broker,Producer,Consumer。
看不懂没关系,先往下走。
1.1 NamerServer
书中是直接上代码的,虽然我不太推崇这么做,因为这样会让你预先陷入细节之中,我还是希望在看的你先从整体入手,尝试理解整个架构,代码只作为理解的工具,到你自己真正进入源码阅读阶段的时候,再进行详细的整理。
NameServer启动类包括NamerSrvStartup和NameSrvController,startup负责启动,controller负责配置信息录入和状态存放,我们聚焦在controller上:
先关注controller的成员变量:
public class NamesrvController{
private final NamesrvConfig namesrvConfig;
private final NettyServerConfig nettyServerConfig;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"NSScheduledThread"));
private final KVConfigManager kvConfigManager;
//Broker路由信息管理
private final RouteInfoManager routeInfoManager;
private RemotingServer remotingServer;
private BrokerHousekeepingService brokerHousekeepingService;
private ExecutorService remotingExecutor;
private Configuration configuration;
private FileWatchService fileWatchService;
}
1.1.1NamesrvConfig和NettyServerConfig
定义了两个NS所需要的初始化配置:
// nameServer 初始化配置
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
public class NamesrvConfig{
//rocketMq主目录
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
//kv配置的持久化路径
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";
}
public class NettyServerConfig{
private int listenPort = 8888;
private int serverWorkerThreads = 8;
private int serverCallbackExecutorThreads = 0;
private int serverSelectorThreads = 3;
private int serverOnewaySemaphoreValue = 256;
private int serverAsyncSemaphoreValue = 64;
private int serverChannelMaxIdleTimeSeconds = 120;
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
private boolean serverPooledByteBufAllocatorEnable = true;
private boolean useEpollNativeSelector = false;
}
很明显,NettyServerConfig是为Netty服务的,也就是说NS也使用了NIO作为通信框架。具体配置我不再多说,英文也是一门很直接的语言,变量的作用和英文一模一样,不用怀疑,你熟悉netty的话应该完全不是问题。
public boolean initialize() {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
}
在Controller的initialize()方法中定义了两个定时任务,通过scheduledExecutorService构建,每隔10S分别扫描活跃的Broker,维持心跳,同时每隔10S打印一次KV配置。
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<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
controller.start();
return controller;
}
startup里还有个钩子函数,会在start()调用之前,去关闭需要释放的资源,类似监听者模式。
1.1.2 RouteInfoManager
Broker管理的核心类,我是建议对这部分参数要非常非常的熟悉甚至是背诵,好处你会懂的,重要的成员变量:
public class RouteInfoManager {
//负载均衡表Topic级别
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
//broker基础信息
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
//集群中所有的broker名称
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
// broker状态信息 ,每次收到心跳包时更新
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
// 消息过滤,用于类模式消息过滤
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
不走许愿派风格,我们感性的从一个实例说明一下这些参数存了什么:
topicQueueTable:存储topic和broker的映射用于消息路由
brokerAddrTable:broker的基础信息,存放broker所在的集群,主备broker地址,0代表master,其余代表slave
clusterAddrTable:broker集群信息
brokerLiveTable:活跃的broker信息,NS和broker维持长tcp连接,每当NS收到broker心跳时,都会更新lastupdatetimestamp,确保broker的存活状态。
filterServerTable:用于消息过滤,这个之后会提到,类似kafka里的拦截器
1.1.3 Broker和NS
broker需要向NS注册自己,并需要在注册之后,维持和NS的心跳,确保自己的存活状态被感知。
在BrokerController的start方法中,新建任务周期性的向NS更新自己的状态
public void start() throws Exception {
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);
}
看一下注册方法底层,这段注册代码可以拿来当所有注册的模板了,你要自己开发中间件,就这么写,经典;registerBroker方法跟到底层,你会发现又是netty编程,所以这块需要你任务调度和netty编程的基础知识。
public List<RegisterBrokerResult> registerBrokerAll(
final String clusterName,
final String brokerAddr,
final String brokerName,
final long brokerId,
final String haServerAddr,
final TopicConfigSerializeWrapper topicConfigWrapper,
final List<String> filterServerList,
final boolean oneway,
final int timeoutMills,
final boolean compressed) {
//broker向所有nameserver注册
final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
//组装请求报文
final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
requestHeader.setBrokerAddr(brokerAddr);
requestHeader.setBrokerId(brokerId);
requestHeader.setBrokerName(brokerName);
requestHeader.setClusterName(clusterName);
requestHeader.setHaServerAddr(haServerAddr);
requestHeader.setCompressed(compressed);
RegisterBrokerBody requestBody = new RegisterBrokerBody();
requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
requestBody.setFilterServerList(filterServerList);
final byte[] body = requestBody.encode(compressed);
final int bodyCrc32 = UtilAll.crc32(body);
requestHeader.setBodyCrc32(bodyCrc32);
// 要一个countDownLatch保证所有任务执行完
final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
for (final String namesrvAddr : nameServerAddressList) {
brokerOuterExecutor.execute(new Runnable() {
@Override
public void run() {
try {
RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
if (result != null) {
//注册有结果就记录结果,子线程结束
registerBrokerResultList.add(result);
}
log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
} catch (Exception e) {
log.warn("registerBroker Exception, {}", namesrvAddr, e);
} finally {
// 一定要在finally里countDown
countDownLatch.countDown();
}
}
});
}
try {
//等待向所有NS注册线程完成或者失败
countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
return registerBrokerResultList;
}
路由发现和删除我就不展开了,源码很有意思,这里其实和使用关系不大,不过源码确实很精彩,希望大家自己看看。
最后放一张NS路由,注册,发现的状态转换图
NS先到这里,下一节开Producer!