RocketMQ源码浅析
NameServer
启动
- 启动过程
- mqnamesrv.sh
- 调用脚本 runserver.sh
- 执行命令:一大堆JVM参数和
org.apache.rocketmq.namesrv.NamesrvStartup
类- 启动JVM进程
NamesrvStartup.main
- 启动JVM进程
- 执行命令:一大堆JVM参数和
- 调用脚本 runserver.sh
- mqnamesrv.sh
public static void main(String[] args) {
main0(args);
}
NamesrvStartup#main0
public static NamesrvController main0(String[] args) {
NamesrvController controller = createNamesrvController(args);
start(controller);
return controller;
}
NamesrvController
猜测一下,NameServer启动以后,需要接收Broker的请求,因为Broker需要将自己注册到NameServer中去;并且Producer客户端需要从NameServer中拉取元数据:Topic的MessageQueue到底是在哪一台Broker机器上。
NamesrvController
这个组件,很可能就是NameServer中专门用来接受Broker和客户端的网络请求的一个组件!
因为平时我们写Java Web系统的时候,大家都喜欢用Spring MVC框架,在Spring MVC框架中,用于接受HTTP请求的,就是Controller组件!
NamesrvController创建–createNamesrvController()
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
NamesrvConfig
NameServer自身运行的一些配置参数
核心配置参数:
// ROCKETMQ_HOME环境变量
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";
// Namserver自己的配置属性的路径
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
// 生产环境的名称
private String productEnvName = "center";
// 是否开启了clusterTest测试集群,默认为false
private boolean clusterTest = false;
// 是否支持有序消息,默认为false
private boolean orderMessageEnable = false;
NettyServerConfig
用于接收网络请求的Netty服务器的配置参数。
- NameServer对外接收Broker和客户端的网络请求的时候,底层应该是基于Netty实现的网络服务器!
nettyServerConfig.setListenPort(9876)
- NameServer默认监听的请求端口号:9876
// 端口号,但是端口号被设置为9876了
private int listenPort = 8888;
// NettyServer工作线程数量
private int serverWorkerThreads = 8;
// Netty public线程池的线程数量,默认为0
private int serverCallbackExecutorThreads = 0;
// Netty IO线程池的线程数量,负责解析网络请求
// 解析完网络请求后,就会把请求交给work线程来处理
private int serverSelectorThreads = 3;
// Broker端在基于netty构建网络服务器时用到的参数
private int serverOnewaySemaphoreValue = 256;
private int serverAsyncSemaphoreValue = 64;
// 如果一个网络连接空闲超过120s,就会被关闭
private int serverChannelMaxIdleTimeSeconds = 120;
// socket send buffer缓冲区以及receive buffer缓冲区的大小
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
// ByteBuffer是否开启缓存,默认开启
private boolean serverPooledByteBufAllocatorEnable = true;
// 是否启动epoll IO模型,默认不启动
private boolean useEpollNativeSelector = false;
NameServer这两个核心配置类如何解析的?
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();
}
}
比如说你在启动NameServer的时候,用-c选项带上了一个配置文件的地址,然后此时他启动的时候,运行到上面的代码,就会把你配置文件里的配置,放入两个核心配置类里去。 比如你有一个配置文件是:nameserver.properties,里面有一个配置是serverWorkerThreads=16,那么上面的代码就会读取出来这个配置,然后覆盖到NettyServerConfig里去!
// 如果你的mqnamesrv如果带了-p,则打印出来
if (commandLine.hasOption('p')) {
InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
MixAll.printObjectProperties(console, namesrvConfig);
MixAll.printObjectProperties(console, nettyServerConfig);
System.exit(0);
}
// 将你在mqnamesrv命令行中带上的配置选项,都读取出来,覆盖到NameSrvConfig中去
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
// 检查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);
完成Controller创建
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
// remember all configs to prevent discard
controller.getConfiguration().registerConfig(properties);
NamesrvStartup#start
上面只是创建了NamesrvController,设置了一些属性而已,而没有去做一些核心的操作,比如说启动Netty网络服务器。
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);
}
...
}
NameServer是如何初始化基于Netty的网络通信架构的
public boolean initialize() {
this.kvConfigManager.load();
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
...
}
NettyRemotingServer
-
构造函数
-
this.serverBootstrap = new ServerBootstrap();
ServerBootstrap
,就是Netty里的一个核心的类,他就是代表了一个Netty网络服务器,通过这个东西,最终可以让Netty监听一个端口号上的网络请求。
-
NamesrvController#start
public void start() throws Exception {
this.remotingServer.start();
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
NettyRemotingServer#start
–启动Netty服务器
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());
}
});
prepareSharableHandlers();
ServerBootstrap childHandler =
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
.channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.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())
.localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
.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(),
new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
connectionManageHandler,
serverHandler
);
}
});
if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
try {
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);
}
if (this.channelEventListener != null) {
this.nettyEventExecutor.start();
}
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
NettyRemotingServer.this.scanResponseTable();
} catch (Throwable e) {
log.error("scanResponseTable exception", e);
}
}
}, 1000 * 3, 1000);
}
上面主要是对于Netty中核心组件的创建和初始化。(我需要补一补Netty的原理)
NameServer启动过程总结
NameServer的启动过程,从他的启动脚本开始,然后一路讲解了他的配置的初始化,以及核心的NamesrvController组件的初始化和启动,最后通过源码一步一步发现,居然底层是基于Netty构建了一个网络服务器,然后监听了9876端口号。 当我们的NameServer启动之后,其实他就是有一个Netty服务器监听了9876端口号,此时Broker、客户端这些就可以跟NameServer建立长连接和进行网络通信了!
Broker
Broker启动
Broker启动脚本 mqbroker.sh
sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@
runbroker.sh:
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_broker_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch"
JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking"
JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib:${JAVA_HOME}/lib/ext"
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
主要是关于JVM的参数
另外,Broker启动的核心在于BrokerStartup#main
。
public static void main(String[] args) {
start(createBrokerController(args));
}
- createBrokerController()
- start(BrokerController)
初始化核心配置
-
org.apache.rocketmq.broker.BrokerStartup#createBrokerController
final BrokerConfig brokerConfig = new BrokerConfig(); final NettyServerConfig nettyServerConfig = new NettyServerConfig(); final NettyClientConfig nettyClientConfig = new NettyClientConfig(); // netty客户端是否使用TLS(加密机制) nettyClientConfig.setUseTLS(Boolean.parseBoolean(System.getProperty(TLS_ENABLE, String.valueOf(TlsSystemConfig.tlsMode == TlsMode.ENFORCING)))); // netty服务器监听10911端口 nettyServerConfig.setListenPort(10911); final MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); // 假设当前Broker为SLAVE的话,设置。。。。。。 if (BrokerRole.SLAVE == messageStoreConfig.getBrokerRole()) { int ratio = messageStoreConfig.getAccessMessageInMemoryMaxRatio() - 10; messageStoreConfig.setAccessMessageInMemoryMaxRatio(ratio); }
- 核心配置类:
- BrokerConfig: Broker配置
- NettyServerConfig: Netty服务端配置
- NettyClientConfig: Netty客户端配置
- MessageStoreConfig: Broker用来存储消息的配置信息
- 核心配置类:
Q: Broker为什么既是netty客户端,又是netty服务端呢?
- 当Producer或者Consumer客户端连接到Broker上,此时Broker的角色为Netty服务器,负责监听客户端的请求
- 当Broker和NameServer建立连接,此时Broker为Netty客户端,与NameServer的Netty服务器建立连接
if (commandLine.hasOption('c')) {
String file = commandLine.getOptionValue('c');
if (file != null) {
configFile = file;
InputStream in = new BufferedInputStream(new FileInputStream(file));
properties = new Properties();
properties.load(in);
properties2SystemEnv(properties);
MixAll.properties2Object(properties, brokerConfig);
MixAll.properties2Object(properties, nettyServerConfig);
MixAll.properties2Object(properties, nettyClientConfig);
MixAll.properties2Object(properties, messageStoreConfig);
BrokerPathConfigHelper.setBrokerConfigPath(file);
in.close();
}
}
加载-c configFile 中配置的文件,解析配置信息,设置到配置类上。
开源框架的通用思路
- 构建配置类
- 读取配置文件中的配置
- 解析命令行的配置
- 执行各种配置项的校验和设置
BrokerController的创建
// org.apache.rocketmq.broker.BrokerStartup#createBrokerController
final BrokerController controller = new BrokerController(
brokerConfig,
nettyServerConfig,
nettyClientConfig,
messageStoreConfig);
// remember all configs to prevent discard
controller.getConfiguration().registerConfig(properties);
- BrokerController语义分析:Broker管理控制组件
Broker的理解
我们用mqbroker脚本启动的JVM进程,实际上可以认为就是一个Broker,这里Broker实际上应该是代表了一个JVM进程的概念,而不是任何一个代码组件!
BrokerStartup与BrokerController
BrokerStartup作为一个main class,其实是属于一个代码组件,他的作用是准备好核心配置组件,然后就是创建、初始化以及启动BrokerController这个核心组件,也就是启动一个Broker管理控制组件,让BrokerController去控制和管理Broker这个JVM进程运行过程中的一切行为,包括接收网络请求、包括管理磁盘上的消息数据,以及一大堆的后台线程的运行。
Broker这个概念本身代表的不是一个代码组件,他就是你用mqbroker脚本启动的VM进程。然后JVM进程的main class是
BrokerStartup
,他是一个启动组件,负责初始化核心配置组件,然后基于核心配置组件去启动BrokerControler这个管控组件。 然后在Broker这个JVM进程运行期间,都是由BrokerController这个管控组件去管理Broker的请求处理、后台线程以及磁盘数据。
BrokerController之初始化
org.apache.rocketmq.broker.BrokerStartup#createBrokerController
boolean initResult = controller.initialize(); if (!initResult) { controller.shutdown(); System.exit(-3); }
public boolean initialize() throws CloneNotSupportedException {
// 加载topic配置
boolean result = this.topicConfigManager.load();
// 加载Consumer消费offset管理器
result = result && this.consumerOffsetManager.load();
// 加载Consumer订阅组管理器
result = result && this.subscriptionGroupManager.load();
// Consumer Filter
result = result && this.consumerFilterManager.load();\
...
// Netty相关
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);
//后面还初始化了一堆线程池
}
-
初始化的作用
-
准备好了一个Netty服务器,可以接受请求了
-
准备好处理各种请求的线程池
-
准备好各种执行后台定时调度任务的线程池
-
BrokerController启动:BrokerController#start
创建和初始化完成后,就可以启动BrokerController
了。
public void start() throws Exception {
if (this.messageStore != null) {
this.messageStore.start();
}
// 启动Netty服务器
if (this.remotingServer != null) {
this.remotingServer.start();
}
if (this.fastRemotingServer != null) {
this.fastRemotingServer.start();
}
...
// 核心组件,让Broker通过Netty客户端去发送请求其他人
// 比如说,Broker发送请求到NameServer去注册以及维持心跳
if (this.brokerOuterAPI != null) {
this.brokerOuterAPI.start();
}
// Broker向NameServer注册
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);
}
抓住干,不要陷入源码的细节中
总结:
- 启动Netty服务器,可以接收网络请求
- BrokerOuterAPI组件是基于Netty客户端发送请求给别人的
- Broker启动一个线程向NameServer注册自己
“高屋建瓴”看Broker启动原理
Broker里有这么一些核心组件,都进行了初始化以及完成了启动,但是你应该最主要关注的事情是这么几个:
- Broker启动了,必然要去注册自己到NameServer去,所以BrokerOuterAPl这个组件必须要画到自己的图里去,这是一个核心组件
- Broker启动之后,必然要有一个网络服务器去接收别人的请求,此时NettyServer这个组件是必须要知道
- 当你的NettyServer接收到网络请求之后,需要有线程池来处理,你需要知道这里应该有一个处理各种请求的线程池
- 你处理请求的线程池在处理每个请求的时候,是不是需要各种核心功能组件的协调?比如写入消息到commitlog,然后写入索引到indexfile和consumer queue文件里去,此时你是不是需要对应的一些MessageStore之类的组件来配合你?
- 除此之外,你是不是需要一些后台定时调度运行的线程来工作?比如定时发送心跳到NameServer去,类似这种事情。
RocketMQ源码阅读之“要义”
- 场景驱动:一定要从各种场景驱动,去理解RocketMQ的源码,包括Broker的注册和心跳,客户端Producer的启动和初始化,Producer从NameServer拉取路由信息,Producer根据负载均衡算法选择一个Broker机器,Producer跟Broker建立网络连接,Producer发送消息到Broker,Broker把消息存储到磁盘。
上面每一个都是RocketMQ这个中间件运行的时候一个场景,一定要从这些场景出发,一点点去理解在每一个场景下,RocketMQ的各个源码中的组件是如何配合运行的。
切记:干万不要在看源码的时候,傻乎乎的一个类一个类的看,那样绝对是会放弃阅读一个源码的!
不要陷入细节;场景驱动学习源码
Broker注册到NameServer
-
注册时机:Broker启动时
-
注册目的:只有完成了注册,NameServer才能知道集群里有哪些Broker,然后Producer和Consumer才能找NameServer去拉取路由数据,他们才会知道有哪些Broker,才能去跟Broker通信。
-
注册触发:
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
BrokerController#registerBrokerAll
public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission())
|| !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) {
ConcurrentHashMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<String, TopicConfig>();
for (TopicConfig topicConfig : topicConfigWrapper.getTopicConfigTable().values()) {
TopicConfig tmp =
new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(),
this.brokerConfig.getBrokerPermission());
topicConfigTable.put(topicConfig.getTopicName(), tmp);
}
topicConfigWrapper.setTopicConfigTable(topicConfigTable);
}
// 核心注册逻辑
if (forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(),
this.getBrokerAddr(),
this.brokerConfig.getBrokerName(),
this.brokerConfig.getBrokerId(),
this.brokerConfig.getRegisterBrokerTimeoutMills())) {
// 执行注册
doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
}
}
BrokerController#doRegisterBrokerAll
List<RegisterBrokerResult> registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll(
this.brokerConfig.getBrokerClusterName(),
this.getBrokerAddr(),
this.brokerConfig.getBrokerName(),
this.brokerConfig.getBrokerId(),
this.getHAServerAddr(),
topicConfigWrapper,
this.filterServerManager.buildNewFilterServerList(),
oneway,
this.brokerConfig.getRegisterBrokerTimeoutMills(),
this.brokerConfig.isCompressedRegister()
);
- 核心注册代码: this.brokerOuterAPI.registerBrokerAll
- 核心注册组件:org.apache.rocketmq.broker.out.BrokerOuterAPI
- 注册结果:
List<RegisterBrokerResult>
- 为何是List,因为Broker需要向(NameServer集群中)所有的NameServer机器注册
BrokerOuterAPI#registerBrokerAll
// 保存注册结果
final List<RegisterBrokerResult> registerBrokerResultList = new CopyOnWriteArrayList<>();
// NameServer的地址
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);
// 注册完所有的NameServer,代码才会往下走
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);
}
} catch (Exception e) {
log.warn("registerBroker Exception, {}", namesrvAddr, e);
} finally {
countDownLatch.countDown();
}
}
});
}
try {
countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
return registerBrokerResultList;
- RegisterBrokerRequestHeader:请求头
- RegisterBrokerBody:请求体
- RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
- BrokerOuter API注册的底层逻辑
BrokerOuter API之注册原理
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
request.setBody(body);
...
RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
- RemotingCommand:封装了requestHeader和requestBody
- remotingClient:NettyClient
NettyRemotingClient#invokeSync
...
final Channel channel = this.getAndCreateChannel(addr);
...
// 发送网络请求
RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
-
Channel:代表Broker和NameServer之间的连接
这里的Channel是netty中的
io.netty.channel.Channel
,并非Java NIO原生的Channel。
获取/创建连接:getAndCreateChannel
如何与NameServer之间建立网络连接?
private Channel getAndCreateChannel(final String addr) throws RemotingConnectException, InterruptedException {
if (null == addr) {
return getAndCreateNameserverChannel();
}
ChannelWrapper cw = this.channelTables.get(addr);
if (cw != null && cw.isOK()) {
return cw.getChannel();
}
return this.createChannel(addr);
}
- 先从缓存中尝试获取连接
- 如果没有连接,则创建一个
createChannel
if (createNewConnection) {
// 创建连接
ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr));
cw = new ChannelWrapper(channelFuture);
this.channelTables.put(addr, cw);
}
- 使用了Netty中的Bootstrap类的connect方法创建连接
通过Channel网络连接发送请求:NettyRemotingAbstract#invokeSyncImpl
final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
this.responseTable.put(opaque, responseFuture);
final SocketAddress addr = channel.remoteAddress();
// 基于Netty通过Channel发送请求
channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
responseFuture.setSendRequestOK(true);
return;
} else {
responseFuture.setSendRequestOK(false);
}
responseTable.remove(opaque);
responseFuture.setCause(f.cause());
responseFuture.putResponse(null);
log.warn("send a request command to channel <" + addr + "> failed.");
}
});
// 等待响应结果
RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
这里还是借助Netty去发送请求
NameServer是如何处理Broker的注册请求
-
org.apache.rocketmq.namesrv.NamesrvController#initialize
-
this.registerProcessor();
注册请求处理器
-
处理前:注册请求处理器
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);
将
DefaultRequestProcessor
注册给NettyServer,Netty Server接收到网络请求后,就会使用DefaultRequestProcessor
来处理。
网络请求处理组件:DefaultRequestProcessor#processRequest
switch (request.getCode()) {
case RequestCode.PUT_KV_CONFIG:
return this.putKVConfig(ctx, request);
case RequestCode.GET_KV_CONFIG:
return this.getKVConfig(ctx, request);
case RequestCode.DELETE_KV_CONFIG:
return this.deleteKVConfig(ctx, request);
case RequestCode.QUERY_DATA_VERSION:
return queryBrokerTopicConfig(ctx, request);
case RequestCode.REGISTER_BROKER: // 处理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);
}
DefaultRequestProcessor#registerBroker
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
requestHeader.getBrokerAddr(),
requestHeader.getBrokerName(),
requestHeader.getBrokerId(),
requestHeader.getHaServerAddr(),
topicConfigWrapper,
null,
ctx.channel()
);
- this.namesrvController.getRouteInfoManager()
- org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager
- 路由信息管理组件
- org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager
RouteInfoManager:路由信息管理组件
this.lock.writeLock().lockInterruptibly();
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet<String>();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
boolean registerFirst = false;
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
this.brokerAddrTable.put(brokerName, brokerData);
}
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
//The same IP:PORT must only have one record in brokerAddrTable
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
it.remove();
}
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
if (null != topicConfigWrapper
&& MixAll.MASTER_ID == brokerId) {
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
|| registerFirst) {
ConcurrentMap<String, TopicConfig> tcTable =
topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
主要通过加写锁,实现单线程处理。
用一些Map类的数据结构,去存放你的Broker的路由数据就可以了,包括了Broker的clusterName、brokerld、brokerName这些核心数据。
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable; private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable; private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable; private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable; private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
而且在更新的时候,一定会基于Java并发包下的ReadWriteLock进行读写锁加锁,因为在这里更新那么多的内存Map数据结构,必须要加一个写锁,此时只能有一个线程来更新他们才行!
Broker如何发送定时心跳/NameServer如何进行故障感知?
Q:Broker是如何定时发送心跳到NameServer,让NameServer感知到Broker一直都存活着;如果Broker一段时间没有发送心跳到NameServer,那么NameServer是如何感知到Broker已经挂掉了。
Broker发送心跳
-
BrokerController#start
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);
- 启动一个定时任务,默认是每隔30s就会执行一次Broker注册的过程
- registerNameServerPeriod默认的值就是30s
NameServer处理心跳请求
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
每隔30秒的心跳时间,最新的BrokerLiveInfo都会覆盖上一次的BrokerLiveInfo
System.currentTimeMillis():代表最近一次的心跳时间
NameServer如何感知到Broker故障
Q:假设Broker已经挂了,或者故障了,隔了很久都没有发送那个每隔30s一次的注册请求作为心跳,那么此时NameServer是如何感知到这个Broker已经挂掉的呢?
-
org.apache.rocketmq.namesrv.NamesrvController#initialize
this.scheduledExecutorService.scheduleAtFixedRate( () -> NamesrvController.this.routeInfoManager.scanNotActiveBroker(), 5, 10, TimeUnit.SECONDS);
启动一个定时调度线程,每隔10s扫描一次目前不活跃的Broker。
RouteInfoManager#scanNotActiveBroker
public void scanNotActiveBroker() {
// 获取Broker最近一次心跳刷新的BrokerLiveInfo
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// 如果当前时间距离上一次心跳时间,超过了broker心跳超时时间(默认120s),会认为broker故障
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
RemotingUtil.closeChannel(next.getValue().getChannel());
it.remove();
// 把Broker从路由数据表移除,涉及到各种Map以及读写锁的处理
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
Broker原理总结
Producer
创建
DefaultMQProducer producer=new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("localhost:9876");
// 启动Producer
producer.start();
- 通过构造函数,传入分组名称,创建Producer对象
- 设置NameServer的地址
启动
入口:org.apache.rocketmq.client.producer.DefaultMQProducer#start
@Override
public void start() throws MQClientException {
this.setProducerGroup(withNamespace(this.producerGroup));
this.defaultMQProducerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
log.warn("trace dispatcher start failed ", e);
}
}
}
-
defaultMQProducerImpl:实际的生产者组件
-
Producer需要知道哪些信息:
假设我们后续要通过Producer发送消息,必然会指定我们要往哪个Topic里发送消息。所以我们也知道,Producer必然是知道Topic的一些路由数据的,比如Topic有哪些MessageQueue,每个MessageQueue在哪些Broker上。
生产消息
获取Topic路由信息
Q:当我们发送消息的时候,是如何从NameServer拉取Topic元数据的
-
发送消息核心API
-
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
-
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
检查发送消息的Topic的路由数据是否在客户端本地;如果不再本地,则需要从NameServer中拉取,并且缓存在客户端本地
-
-
我们只要知道拉取路由信息的思路即可,底层代码是通过Netty去连接和请求NameServer来实现的。
// 从NameServer中拉取路由信息,并且写入客户端缓存
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
Producer是如何选择MessageQueue去发送的
当你拿到了一个Topic的路由数据之后,其实接下来就应该选择要发送消息到这个Topic的哪一个MessageQueue上去了!
Topic理解:
- Topic是一个逻辑上的概念,一个Topic的数据往往是分布式存储在多台Broker机器上的,因此Topic本质是由多个MessageQueue组成的。
- 每个MessageQueue都可以在不同的Broker机器上,当然也可能一个Topic的多个MessageQueue在一个Broker机器上。
Q:你要发送的消息,到底应该发送到这个Topic的哪个MessageQueue上去呢?
只要你知道了要发送消息到哪个MessageQueue上去,然后就知道这个MessageQueue在哪台Broker机器上,接着就跟那台Broker机器建立连接,发送消息给他就可以了。
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
-
选择Topic的一个MessageQueue
int index = tpInfo.getSendWhichQueue().getAndIncrement(); for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) { int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size(); if (pos < 0) pos = 0; MessageQueue mq = tpInfo.getMessageQueueList().get(pos); if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) return mq; }
这是最基本的一个选择MessageQueue的算法
- 先获取到了一个自增长的index
- 接着用这个index对Topic的MessageQueue列表进行了取模操作,获取到了一个MessageQueue列表的位置,然后返回了这个位置的MessageQueue。
这是一种简单的负载均衡的算法,比如一个Topic有8个MessageQueue,那么可能第一次发送消息到MessageQueue01,第二次就发送消息到MessageQueue02,以此类推,就是轮询把消息发送到各个MessageQueue!
发送消息到Broker
// 获取Broker Name
brokersSent[times] = mq.getBrokerName();
// 发送消息
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
sendKernelImpl
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
msg,
requestHeader,
timeout - costTimeSync,
communicationMode,
context,
this);
基于Netty去发送信息
Broker收到消息后的处理
Broker通过Netty服务器获取消息,然后会把消息写入到CommitLog中,同时以异步的方式把消息写入到ConsumeQueue中。
注意:一个Broker机器只有一个CommitLog文件,所有Topic的消息都会写入到这一个文件中。
因为一个Topic有多个MessageQueue,任何一条消息都是写入一个MessageQueue的,那个MessageQueue其实就是对应了一个ConsumeQueue文件。所以一条写入MessageQueue的消息,必然会异步进入对应的ConsumeQueue文件。
同时还会异步把消息写入一个IndexFile里,在里面主要就是把每条消息的key和消息在CommitLog中的offset偏移量做一个索引,这样后续如果要根据消息key从CommitLog文件里查询消息,就可以根据IndexFile的索引来了。
CommitLog
-
磁盘目录结构:
-
CommitLog文件的存储目录是在**${ROCKETMQ_HOME}/store/commitlog**下的,里面会有很多的CommitLog文件,每个文件默认是1GB大小,一个文件写满了就创建一个新的文件,文件名的话,就是文件中的第一个偏移量,如下面所示。文件名如果不足20位的话,就用0来补齐就可以了。 00000000000000000000
000000000003052631924
-
在把消息写入CommitLog文件的时候,会申请一个putMessageLock锁。也就是说,在Broker上写入消息到CommitLog文件的时候,都是串行的,不会让你并发的写入,并发写入文件必然会有数据错乱的问题。
这里是写入到MappedFile 内存中,然后再根据我们的刷盘策略,将内存中的消息刷入CommitLog中。
另外就是不管是同步刷盘还是异步刷盘,假设你配置了主从同步,一旦你写入完消息到CommitLog之后,接下来都会进行主从同步复制的。
-
刷盘策略
CommitLog#putMessage
// 决定怎么刷盘
handleDiskFlush(result, putMessageResult, msg);
// 决定如何把消息同步给slave Broker
handleHA(result, putMessageResult, msg);
handleDiskFlush
-
同步刷盘
if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { final GroupCommitService service = (GroupCommitService) this.flushCommitLogService; if (messageExt.isWaitStoreMsgOK()) { GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes()); service.putRequest(request); CompletableFuture<PutMessageStatus> flushOkFuture = request.future(); PutMessageStatus flushStatus = null; try { flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(), TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { //flushOK=false; } if (flushStatus != PutMessageStatus.PUT_OK) { log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags() + " client address: " + messageExt.getBornHostString()); putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT); } } else { service.wakeup(); } }
-
其实上面就是构建了一个GroupCommitRequest,然后提交给了GroupCommitService去进行处理,然后调用request.waitForFlush()方法等待同步刷盘成功 万一刷盘失败了,就打印日志。
-
具体刷盘是由GroupCommitService执行的,他的doCommit()方法最终会执行同步刷盘的逻辑,里面有如下代码。
CommitLog.this.mappedFileQueue.flush(0);
// 这个MappedByteBuffer就是JDKNIO包下的API,他的force方法就是强迫把你写入内存的数据刷入到磁盘文件里去,到此就是同步刷盘成功了。 this.mappedByteBuffer.force();
-
-
异步刷盘
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { flushCommitLogService.wakeup(); } else { commitLogService.wakeup(); }
-
唤醒flushCommitLogService组件
-
abstract class FlushCommitLogService extends ServiceThread { protected static final int RETRY_TIMES_OVER = 10; } class CommitRealTimeService extends FlushCommitLogService {
-
实现类:CommitRealTimeService
其实简单来说,就是每隔一定时间执行一次刷盘,最大间隔是10s,所以一旦执行异步刷盘,那么最多就是10秒就会执行一次刷盘。
-
-
-
ConsumeQueue/indexFile
实际上,Broker启动的时候会开启一个线程,ReputMessageService,他会把CommitLog更新事件转发出去,然后让任务处理器去更新ConsumeQueue和IndexFile。
Broker清理磁盘数据
broker不停的接收数据,然后磁盘上的数据越来越多,但是万一磁盘都放满了,怎么办呢?
- broker会启动后台线程,这个后台线程会自动去检查Commitlog、ConsumeQueue文件,因为这些文件都是多个的,比如CommitLog会有多个,ConsumeQueue也会有多个。
- 然后如果是那种比较旧的超过72小时的文件,就会被删除掉,也就是说,默认来说,broker只会给你把数据保留3天而已,当然你也可以自己通过fileReservedTime来配置这个时间,要保留几天的时间。
这个定时检查过期数据文件的线程代码,在DefaultMessageStore这个类里,他的start)方法中会调用一个addScheduleTask)方法,里面会每隔10s定时调度执行一个后台检查任务。
- BrokerController#start
- org.apache.rocketmq.store.DefaultMessageStore#start
- org.apache.rocketmq.store.DefaultMessageStore#addScheduleTask
- org.apache.rocketmq.store.DefaultMessageStore#start
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DefaultMessageStore.this.cleanFilesPeriodically();
}
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
private void cleanFilesPeriodically() {
this.cleanCommitLogService.run();
this.cleanConsumeQueueService.run();
}
- 清理CommitLog
- 清理ConsumeQueue
在清理文件的时候,他会具体判断一下,如果当前时间是预先设置的凌晨4点,就会触发删除文件的逻辑,这个时间是默认的;或者是如果磁盘空间不足了,就是超过了85%的使用率了,立马会触发删除文件逻辑。
上面两个条件,第一个是说如果磁盘没有满,那么每天就默认一次会删除磁盘文件,默认就是凌晨4点执行,那个时候必然是业务低峰期,因为凌晨4点大部分人都睡觉了,无论什么业务都不会有太高业务量的。
第二个是说,如果磁盘使用率超过85%了,那么此时可以允许继续写入数据,但是此时会立马触发删除文件的逻辑;如果磁盘使用率超过90%了,那么此时不允许在磁盘里写入新数据,立马删除文件。这是因为,一旦磁盘满了,那么你写入磁盘会失败,此时你MQ就彻底故障了。
所以一旦磁盘满了,也会立马删除文件的。
在删除文件的时候,无非就是对文件进行遍历,如果一个文件超过72小时都没修改过了,此时就可以删除了,哪怕有的消息你可能还没消费过,但是此时也不会再让你消费了,就直接删除掉。
Consumer
- 创建:org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl(构造器)
- 启动:org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start
// 基于Netty创建与Broker的长连接
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
// Consumer重平衡
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
// PullAPI
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
// OffsetStore
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load();
-
Netty:与Broker实现网络通信
-
Consumer重平衡:
- 假设你的ConsumerGroup里加入了一个新的Consumer,那么就会重新分配每个Consumer消费的MessageQueue;
- 如果ConsumerGroup里某个Consumer宕机了,也会重新分配MessageQueue,这就是所谓的重平衡。
-
PullAPI: 拉取消息的组件
-
OffsetStore:存储和管理Consumer消费进度offset的组件
启动原理分析:Consumer创建好,就要去启动;启动时首先根据Consumer重平衡组件去分配得到一些MessageQueue去消费;我们消费消息时需要通过PullAPI借助Netty网络通信模块去拉取消息;在拉取消息过程中,需要借助OffsetStore模块去管理和维护消费进度。如果ConsumerGroup中多了或少了Consumer,则需要依托重平衡组件重新分配MessageQueue。
Consumer负载均衡
当你一个业务系统部署多台机器的时候,每个系统里都启动了一个Consumer,多个Consumer会组成一个ConsumerGroup,也就是消费组,此时就会有一个消费组内的多个Consumer同时消费一个Topic,而且这个Topic是有多个MessageQueue分布在多个Broker上的。
假设一个业务系统部署在两台机器上,对应一个消费组里就有两个Consumer,那么现在一个Topic有三个MessageQueue,该怎么分配呢?
-
实际上,每个Consumer在启动之后,都会向所有的Broker进行注册,并且持续保持自己的心跳,让每个Broker都能感知到一个消费组内有哪些Consumer。
-
然后呢,每个Consumer在启动之后,其实重平衡组件都会随机挑选一个Broker,从里面获取到这个消费组里有哪些Consumer存在。
-
此时重平衡组件一旦知道了消费组内有哪些Consumer之后,接着就好办了,无非就是把Topic下的MessageQueue均匀的分配给这些Consumer了,这个时候其实有几种算法可以进行分配,但是比较常用的一种算法就是简单的平均分配。
总之,一切都是平均分配的,尽量保证每个Consumer的负载是差不多的。
-
一旦MessageQueue负载确定了之后,下一步其实Consumer就知道自己要消费哪几个MessageQueue的消息了,就可以连接到那个Broker上去,从里面不停的拉取消息过来进行消费了。
消费组
-
不同的系统应该设置不同的消费组,如果不同的消费组订阅了同一个Topic,对于Topic中的每一条消息,每个消费组都会获得消息
-
集群模式消费(常用):一个消费组获取到一条消息,只会交给组内的一台机器去处理,不是每台机器都可以获取到这条消息的。
-
广播模式消费(很少使用):对于消费组获取到的一条消息,组内每台机器都可以获取到这条消息。
-
但是相对而言广播模式其实用的很少,常见基本上都是使用集群模式来进行消费的。
-
消费模式
-
Push模式:
一般我们使用RocketMQ的时候,消费模式通常都是基于他的Push模式来做的,因为Pul模式的代码写起来更加的复杂和繁琐,而且Push模式底层本身就是基于消息拉取的方式来做的,只不过时效性更好而已。 当消费者发送请求到Broker去拉取消息的时候,如果有新的消息可以消费那么就会立马返回一批消息到消费机器去处理,处理完之后会接着立刻发送请求到Broker机器去拉取下一批消息。
所以消费机器在Push模式下会处理完一批消息,立马发起请求拉取下一批消息,消息处理的时效性非常好,看起来就跟Broker一直不停的推送消息到消费机器一样。
-
Pull模式:请求挂起和长轮询
当你的请求发送到Broker,结果他发现没有新的消息给你处理的时候,就会让请求线程挂起,默认是挂起15秒,然后这个期间他会有后台线程每隔一会儿就去检查一下是否有的新的消息给你,另外如果在这个挂起过程中,如果有新的消息到达了会主动唤醒挂起的线程,然后把消息返回给你。
-
-
消费消息的本质
- 所以其实消费消息的时候,本质就是根据你要消费的MessageQueue以及开始消费的位置,去找到对应的ConsumeQueue读取里面对应位置的消息在CommitLog中的物理offset偏移量,然后到CommitLog中根据offset读取消息数据,返回给消费者机器。
-
消费者机器如何处理消息、进行ACK以及提交消费进度
当我们处理完这批消息之后,消费者机器就会提交我们目前的一个消费进度到Broker上去,然后Broker就会存储我们的消费进度,比如我们现在对ConsumeQueue0的消费进度假设就是在offset=1的位置,那么他会记录下来一个ConsumeOffset的东西去标记我们的消费进度。
巨人的肩膀
-
[儒猿技术窝《从0开始带你成为消息中间件实战高手》](儒猿技术窝 (xiaoe-tech.com))
注:儒猿技术窝,原名狸猫技术窝