RocketMQ 源码分析之路由中心(NameServer)
你可能没有看过 RocketMQ 的架构图,没关系,一起来学习一下,RocketMQ 架构图如下:
在 RocketMQ 中,有四个角色:
- Producer:消息的生产者,每个 MQ 中间件都有。
- Consumer:消息的消费者,每个 MQ 中间件都有。
- NameServer:RocketMQ 的路由中心,跟 ZooKeeper 差不多。
- Broker:消息服务器,RocketMQ 的消息全部存储在这里。
Producer 发送消息之前,先从 NameServer 中获取到 Broker 服务器列表,然后根据负载均衡策略选择一台 Broker 发送,消息消费时也是同样的道理。可以说 NameServer 是 RocketMQ 的大脑,想要实现路由分发的功能,那么在 NameServer 必然要维护着 Broker 服务器信息,这中间就会涉及到 Broker 服务器服务状态管理问题,这篇文章就来聊一聊 RocketMQ 是如何做服务状态管理的。
在聊服务状态管理之前,先来讲一讲为何不用 ZooKeeper 来做路由中心?
听闻早期的 RocketMQ 是使用 ZooKeeper 来做路由中心。我们知道 ZooKeeper 功能比较强大,包括自动 Master 选举等,强大的同时部署维护就变得复杂了,但是 ZooKeeper 的很多功能 RocketMQ 并不需要,RocketMQ 只需要一个轻量级的元数据服务器就够了。所以就造了 NameServer 这个轮子。
还有一个原因就是中间件对稳定性要求比较高,使用 ZooKeeper 作为注册和路由中心的话,就依赖了另一个中间件,提高了系统复杂性和维护成本,而 NameServer 只是 RocketMQ 中的一个模块,且只有少量代码,维护起来简单,稳定性也提高了。
好了,说回服务状态管理问题,其实这个并不陌生,在微服务领域有大量的中间件都涉及到了这个问题。对于服务状态管理,一般有两种解决思路。
第一种思路是主动探测,如图:
主动探测是由路由方(比如 NameServer)发起的,每一个被路由方(比如 Broker)需要打开一个端口,然后路由方每隔一段时间(比如 30 秒)探测这些端口是否可用,如果可用就认为服务器正常,否则认为服务不可用,就把服务从列表中删除。
这种方式存在的问题就路由方压力可能过大,如果被路由方部署的实例较多时,那么每次探测的成本会比较高,探测的时间也比较长,可能会导致路由方可能不能正常工作。
第二种思路是心跳模式,如图:
心跳模式不在是路由方发起了,改成被路由方每隔一段时间向路由方发送心跳包,路由方记录被路由方的心跳包,包括服务器IP、上报时间等。每一次上报后,更新对应的信息。路由方启动一个定时器,定期检测当前时间和节点,最近续约时间的差值,如果达到一个阈值(比如说90秒),那么认为这个服务节点不可用。
现在大部分需要服务状态管理的中间件,都采用心跳模式,没有太多的缺陷,也不会对服务器造成多大的压力。在 RocketMQ 中 NameServer 与 Broker 的通信也是采用 心跳模式。
心跳模式中,有上报心跳、保存心跳信息、定时检测这个步骤。我们从上报心跳和定时检测这两个方面,从源码的角度,看看 RocketMQ 是如何实现心跳模式的。
先从上报心跳开始,在 RocketMQ 中,默认情况下,Broker 服务器会每间隔 30秒向集群中的所有 NameServer 发送心跳包。源代码是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);
}
}
// brokerConfig.getRegisterNameServerPeriod() 默认是 30 秒
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
其中上报心跳的时间用户是可以自定义的,但是不会低于 10秒高于 60秒。当然这只是一个定时器,具体发送心跳包的方法是org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll()
,代码如下:
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) {
final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
// 获取所有 NameServer 服务器
List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
// 构建 broker 信息
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);
final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
// 向 NameServer 逐个上报
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 {
countDownLatch.countDown();
}
}
});
}
try {
countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
return registerBrokerResultList;
}
心跳包发送完之后,就是 NameServer 处理心跳包了,NameServer 会将心跳信息保存起来,保存心跳信息的源代码我就不贴了,涉及的东西比较多,有兴趣的可以查看org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest()#RequestCode.REGISTER_BROKER
,一步一步 Debug 就知道保存过程。
来看看最后一个操作定时检测,NameServer 会开启一个探测线程,源代码在org.apache.rocketmq.namesrv.NamesrvController#initialize()
下,代码如下:
// 检测 broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
NameServer 每 10秒会发起一次检测。具体检测源代码是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#scanNotActiveBroker()
,代码如下:
/**
* 检测 broker 状态
*/
public void scanNotActiveBroker() {
// 遍历 broker 存活列表
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// 如果最后一次上报时间已经超过两分钟,则移出
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
RemotingUtil.closeChannel(next.getValue().getChannel());
it.remove();
log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
NameServer 会遍历 Broker 存活列表,如果最后一次发送心跳包的时间超过 120秒,则认为 Broker 服务器不可用,将 Broker 从各种配置列表中移出。
到此为止,RocketMQ 的心跳模式实现就完成了,上面的源代码都是一些粗略的,具体的实现细节还是比较繁琐的,有兴趣的可以深入研究源码,获取更多详细信息。
关于RocketMQ 解决服务状态管理的分享就这些,感谢您的阅读,希望这篇文章对您的学习或者工作有一点帮助。有收获的话,也可以帮忙推荐给其他的小伙伴,让更多的人受益,万分感谢。
最后
目前互联网上很多大佬都有 RocketMQ 相关文章,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。
欢迎关注公众号【互联网平头哥】。这里有职场感悟、Java 技术,虽然不高大上,但通俗易懂。今天最好的是明天最低的要求,愿你我共同进步。
RocketMQ 源码分析之路由中心(NameServer)的更多相关文章
- RocketMQ 源码分析 —— Message 发送与接收
1.概述 Producer 发送消息.主要是同步发送消息源码,涉及到 异步/Oneway发送消息,事务消息会跳过. Broker 接收消息.(存储消息在<RocketMQ 源码分析 —— Mes ...
- RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想
摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...
- 【RocketMQ源码分析】深入消息存储(3)
前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) ConsumeQueue篇 --[RocketMQ源码分析]深入消息存储(2) 前面两篇已经说过了消息如何存储到Co ...
- 【RocketMQ源码分析】深入消息存储(2)
前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) MappedFile篇 --[RocketMQ源码分析]深入消息存储(3) 前文说完了一条消息如何被持久化到本地磁盘 ...
- 【RocketMQ源码分析】深入消息存储(1)
最近在学习RocketMQ相关的东西,在学习之余沉淀几篇笔记. RocketMQ有很多值得关注的设计点,消息发送.消息消费.路由中心NameServer.消息过滤.消息存储.主从同步.事务消息等等. ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- ROCKETMQ源码分析笔记1:tools
rocketmq源码解析笔记 大家好,先安利一下自己,本人男,35岁,已婚.目前就职于小资生活(北京),职位是开发总监. 姓名DaneBrown 好了.我保证本文绝不会太监!转载时请附上以上安利信息. ...
- ROCKETMQ源码分析笔记2:client
CLIENT 之前讲过tools里面有大量调用client的东西.为了从源码层面了解rocket,决定啃下client这块骨头. pom 先看pom,看看CLIENT依赖谁.看完后原来是依赖commo ...
- SOFA 源码分析— 自定义路由寻址
前言 SOFA-RPC 中对服务地址的选择也抽象为了一条处理链,由每一个 Router 进行处理.同 Filter 一样, SOFA-RPC 对 Router 提供了同样的扩展能力. 那么就看看 SO ...
随机推荐
- Vue项目一、node.js和npm的安装和环境搭建
一.为什么安装node.js及npm npm npm是Node.js的包管理工具(package manager),是全球最大的生态系统,同过npm可以找到很多丰富的插件来满足项目的需求. a1.现在 ...
- Javascript中String()和new String()的区别——JS的包装对象
最近在看Symbol不能使用new操作符,然后类比到Number,String,Boolean,因为它们同属于基本类型,但是有有所差异:Number,String,Boolean是可以使用new操作符 ...
- agent判断用户请求设备
- Fortify Audit Workbench 笔记 Cross-Site Scripting-Persistent
Cross-Site Scripting: Persistent Abstract 向 Web 浏览器发送非法数据会导致浏览器执行恶意代码. Explanation Cross-Site Script ...
- 深入理解计算机系统 (CS:APP) Lab2 - Bomb Lab 解析
原文地址:https://billc.io/2019/04/csapp-bomblab/ 写在前面 CS:APP是这学期的一门硬核课程,应该是目前接触到最底层的课程了.学校的教学也是尝试着尽量和CMU ...
- 可运行jar包的几种打包/部署方式(转)
转自:https://www.cnblogs.com/yjmyzz/p/executable-jar.html java项目开发中,最终生成的jar,大概可分为二类,一类是一些通用的工具类(不包含ma ...
- Linux命令进阶篇-文件查看与查找
上一篇的博客对于Linux如何在不同目录下跳转和查看目录下内容做出了总结,主要靠cd和ls,很常见也很实用.但是你看到目录下面那么多不同花花绿绿的文件,心里是不是痒痒,是不是想进去一探究竟,有办法! ...
- requests模块使用一
1.安装与简介 Urllib和requests模块是python中发起http请求最常见的模块,但是requests模块使用更加方便简单. pip install requests 2.GET请求 2 ...
- 泛微E-cology OA 远程代码执行漏洞
分析文章:https://dwz.cn/bYtnsKwa http://127.0.0.1/weaver/bsh.servlet.BshServlet 若存在如上页面,则用下面数据包进行测试. POS ...
- Mac brew命令的使用
mac 终端程序管理工具 能让你更快速的安装你想要的工具.而不用考虑大量的依赖. 安装brew复制下面的命令,终端执行 官网Homebrew /usr/bin/ruby -e "$(cur ...