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 ...
随机推荐
- js中的this和arguments.callee
this和 arguments.callee this 全局作用域下,this指向Window 其他情况下,谁调用this就指向谁 console.log(this) //对调用就指向谁系列 func ...
- Thead基础及两种创建方式
今天本人给大家讲解一下多线程,如有不对的或者讲的不好的可以多多提出,我会进行相应的更改,先提前感谢提出意见的各位了!!! 说说多线程本人自己对它理解的定义:因计算机的CPU支持能够在同一时间执行多于一 ...
- LoadRunner 11破解方法:
LoadRunner 11破解方法: 请严格安装顺序操作! a.用LR8.0中的mlr5lprg.dll.lm70.dll覆盖LR11安装目录下“bin”文件夹中的对应文件: b.运行deleteli ...
- ggplot2(8) 精雕细琢
8.1 主题 主题系统控制着图形中的非数据元素外观,它不会影响几何对象和标度等数据元素.这题不能改变图形的感官性质,但它可以使图形变得更具美感,满足整体一致性的要求.主题的控制包括标题.坐标轴标签.图 ...
- ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)
前言 上一篇讲解了 AQS 的独占锁部分(参看:ReentrantLock 源码分析以及 AQS (一)),这一篇将介绍 AQS 的共享锁,以及基于共享锁实现读写锁分离的 ReentrantReadW ...
- 为什么vue中的data用return返回呢?
不使用return包裹的数据会在项目的全局可见,会造成变量污染:使用return包裹后数据中变量只在当前组件中生效,不会影响其他组件. 当一个组件被定义, data 必须声明为返回一个初始数据对象的函 ...
- 如何在vue中修改动态标签中的样式和修改组件库样式
vue中修改动态标签中的样式和修改组件库样式 因为vue中的标签在渲染的时候,都会给标签加上id 所以你想在<style lang="scss" scoped>直接修改 ...
- 改变 C/C++ 控制台程序的输出颜色和样式
我们经常可以看见Linux自带终端下的许多程序都输出了不同颜色和底纹的字体.最近也想要自己实现一下这种效果,方法是在输出流中插入占位符\033[***. 我从网上收集了一些常用的控制语句,并用以下代码 ...
- C/C++书籍分享(百度网盘版)
作为第一篇博客,该写一些什么好呢,毕竟作为技术博客开创的,不能随便闲谈不是. 那就分享一些书籍作为见面礼吧.链接里面包含有大量的C++学习用书籍,包含了从入门到进阶的大部分高质量书籍,注意仅用作个人学 ...
- 【Weiss】【第03章】练习3.17:懒惰删除
[练习3.17] 不同于我们已经给出的删除方法,另一种是使用懒惰删除的方法. 为了删除一个元素,我们只标记上该元素被删除的信息(使用一个附加的位域). 表中被删除和非被删除的元素个数作为数据结构的一部 ...