代码很少,却很优秀!RocketMQ的NameServer做到了!

你好,我是猿java。

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。

本文基于 RocketMQ release-5.2.0

首先,我们回顾下 RocketMQ的内核原理鸟瞰图:

从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker交互,也和 Producer和 Consumer交互,因此,在 RocketMQ中,Nameserver起到了一个纽带性的作用。

接着,我们再看看 NameServer的工程结构,如下图:

整个工程只有 11个类(老版本好像只有不到 10个类),为什么 RocketMQ可以用如此少的 Class类,设计出如此高性能且轻量的注册中心?

我觉得最核心的有 3点是:

  1. AP设计思想
  2. 简单的数据结构
  3. 心跳机制

AP设计思想

像 ZooKeeper,采用了 Zab (Zookeeper Atomic Broadcast) 这种比较重的协议,必须大多数节点(过半数)可用,才能确保了数据的一致性和高可用,大大增加了网络开销和复杂度。

而 NameServer遵守了 CAP理论中 AP,在一个 NameServer集群中,NameServer节点之间是P2P(Peer to Peer)的对等关系,并且 NameServer之间并没有通信,减少很多不必要的网络开销,即便只剩一个 NameServer节点也能继续工作,足以保证高可用。

数据结构

NameServer维护了一套比较简单的数据结构,内部维护了一个路由表,该路由表包含以下几个核心元数据,对应的源码类RouteInfoManager如下:

java 复制代码
public class RouteInfoManager {
    private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; // broker失效时间 120s
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Map<String/* topic */, Map<String, QueueData>> topicQueueTable;
    private final Map<String/* brokerName */, BrokerData> brokerAddrTable;
    private final Map<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    private final Map<BrokerAddrInfo/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    private final Map<BrokerAddrInfo/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
  • topicQueueTable: Topic消息队列路由信息,消息发送时根据路由表进行负载均衡
  • brokerAddrTable: Broker基础信息,包括brokerName、所属集群名称、主备Broker地址
  • clusterAddrTable: Broker集群信息,存储集群中所有Broker名称
  • brokerLiveTable: Broker状态信息,NameServer每次收到心跳包是会替换该信息
  • filterServerTable: Broker上的FilterServer列表,用于过滤标签(Tag)或 SQL表达式,以减轻 Consumer的负担,提高消息消费的效率。

TopicRouteData

TopicRouteData是 NameServer中最重要的数据结构之一,它包括了 Topic对应的所有 Broker信息以及每个 Broker上的队列信息,filter服务器列表,其源码如下:

java 复制代码
public class TopicRouteData {
    private List<QueueData> queueDatas;
    private List<BrokerData> brokerDatas;
    private HashMap<String, List<String>> filterServerTable;
    //It could be null or empty
    private Map<String/*brokerName*/, TopicQueueMappingInfo> topicQueueMappingByBroker;
}

BrokerData

BrokerData包含了 Broker的基本属性,状态,所在集群以及 Broker服务器的 IP地址,其源码如下:

java 复制代码
public class BrokerData {
    private String cluster;//所在的集群
    private String brokerName;//所在的brokerName
    private HashMap<Long, String> brokerAddrs;//该broker对应的机器IP列表
    private String zoneName; // 区域名称
}

QueueData

QueueData包含了 BrokerName,readQueue的数量,writeQueue的数量等信息,对应的源码类是QueueData,其源码如下:

java 复制代码
public class QueueData {
    private String brokerName;//所在的brokerName
    private int readQueueNums;// 读队列数量
    private int writeQueueNums;// 写队列数量
    private int perm; // 读写权限,参考PermName 类
    private int topicSysFlag; // topic同步标记,参考TopicSysFlag 类
}

元数据举例

为了更好地理解元数据,这里对每一种元数据都给出一个数据实例:

text 复制代码
topicQueueTable:{
    "topicA":[
        {
            "brokeName":"broker-a",
            "readQueueNums":4,
            "writeQueueNums":4,
            "perm":6, 
            "topicSyncFlag":0 
        },
        {
            "brokeName":"broker-b",
            "readQueueNums":4,
            "writeQueueNums":4,
            "perm":6, 
            "topicSyncFlag":0
        }
    ],
    "topicB":[]
}
text 复制代码
brokeAddrTable:{
    "broker-a":{
        "cluster":"cluster-1",
        "brokerName":"broker-a",
        "brokerAddrs":{
            0:"192.168.0.1:8000",
            1:"192.168.0.2:8000"
        }
    },
    "broker-b":{
        "cluster":"cluster-1",
        "brokerName":"broker-b",
        "brokerAddrs":{
            0:"192.168.0.3:8000",
            1:"192.168.0.4:8000"
        }
    }
}
text 复制代码
brokerLiveTable:{
    "192.168.0.1:8000":{
        "lastUpdateTimestamp":1533434434344,//long 的时间戳
        "dataVersion":dataVersionObj, //参考DataVersion类
        "channel":channelObj,// 参考io.netty.channel.Channel
        "haServerAddr":"192.168.0.2:8000"
    },
    "192.168.0.2:8000":{
        "lastUpdateTimestamp":1533434434344,//long 的时间戳
        "dataVersion":dataVersionObj, //参考DataVersion类
        "channel":channelObj,// 参考io.netty.channel.Channel
        "haServerAddr":"192.168.0.1:8000"
    },
    "192.168.0.3:8000":{ },
    "192.168.0.4:8000":{ },
}
text 复制代码
clusterAddrTable:{
    "cluster-1":[{"broker-a"},{"broker-b"}],
    "cluster-2":[],
}
text 复制代码
filterServerTable:{
    "192.168.0.1:8000":[{"192.168.0.1:7000"}{"192.168.0.1:9000"}],
    "192.168.0.2:8000":[{"192.168.0.2:7000"}{"192.168.0.2:9000"}],
}

心跳机制

心跳机制是 NameServer维护 Broker的路由信息最重要的一个抓手,主要分为接收心跳、处理心跳、心跳超时 3部分:

接收心跳

Broker每 30s会向所有的 NameServer发送心跳包,告诉它们自己还存活着,从而更新自己在 NameServer的状态,整体交互如下图:

处理心跳

NameServer收到心跳包时会更新 brokerLiveTable缓存中 BrokerLiveInfo的 lastUpdateTimeStamp信息,整体交互如下图:

处理逻辑可以参考源码: org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest#brokerHeartbeat:

java 复制代码
public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final BrokerHeartbeatRequestHeader requestHeader =
        (BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class);

    this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr());

    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
    return response;
}

心跳超时

NameServer每隔 10s(每隔5s + 5s延迟)扫描 brokerLiveTable检查 Broker的状态,如果在 120s内未收到 Broker心跳,则认为 Broker异常,会从路由表将该 Broker摘除并关闭 Socket连接,同时还会更新路由表的其他信息,整体交互如下图:

java 复制代码
private void startScheduleService() {
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
        5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
}

源码参考:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unRegisterBroker(),核心流程:

  1. 遍历brokerAddrTable
  2. 遍历broker地址
  3. 根据 broker地址移除 brokerAddr
  4. 如果当前 Topic只包含待移除的 Broker,则移除该 Topic

其他核心源码解读

NameServer启动

NameServer的启动类为:org.apache.rocketmq.namesrv.NamesrvStartup,整个流程如下图:

图片来自:Mark_Zoe

NameServer启动最核心的 3个事情是:

  1. 加载配置:NameServerConfig、NettyServerConfig主要是映射配置文件,并创建 NamesrvController。
  2. 启动 Netty通信服务:NettyRemotingServer是 NameServer和Broker,Producer,Consumer通信的底层通道 Netty服务器。
  3. 启动定时器和钩子程序:NameServerController实例一方面处理 Netty接收到消息后,一方面内部有多个定时器和钩子程序,它是 NameServer的核心控制器。

总结

NameServer并没有采用复杂的分布式协议来保持数据的一致性,而是采用 CAP理论中的 AP,各个节点之间是Peer to Peer的对等关系,数据的一致性通过心跳机制,定时器,延时感知来完成。

NameServer最核心的 3点设计是:

  1. AP的设计思想
  2. 简单的数据结构
  3. 心跳机制

学习交流

文章总结不易,看到这里的小伙伴,感谢帮忙点赞,在读,或者转发给更多的好友,我们将为你呈现更多的干货, 欢迎关注公众号:猿java

相关推荐
容若只如初见3 小时前
项目实战--Spring Boot + Minio文件切片上传下载
java·spring boot·后端
码农爱java4 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
Apifox.5 小时前
什么是 HTTP POST 请求?初学者指南与示范
后端·http·学习方法·web
无名指的等待7125 小时前
SpringBoot实现图片添加水印(完整)
java·spring boot·后端
甜甜圈的小饼干8 小时前
Spring Boot+Vue项目从零入手
vue.js·spring boot·后端
我曾遇到一束光9 小时前
Spring boot 更改启动LOGO
数据库·spring boot·后端
tiger_angel10 小时前
springboot集成gzip和zip数据压缩传输-满足2k数据自动压缩(适用大数据信息传输)
大数据·spring boot·后端·数据压缩·压缩数据·压缩传输消息
笔触狂放10 小时前
【Django】网上蛋糕项目商城-关键字搜索,商品详情功能
后端·python·django
q5673152310 小时前
Python 3.x 下的 3D 游戏引擎
开发语言·后端·python·3d·django·游戏引擎
奋斗的袍子00711 小时前
SpringBoot:SpringBoot统一响应和统一异常处理
java·spring boot·后端·spring·统一异常处理·统一响应·自定义异常