本文已收錄在Github,關注我,緊跟本系列專欄文章,咱們下篇再續!
- ?? 魔都架構師 | 全網30W技術追隨者
- ?? 大廠分布式系統/數據中臺實戰專家
- ?? 主導交易系統百萬級流量調優 & 車聯網平臺架構
- ?? AIGC應用開發先行者 | 區塊鏈落地實踐者
- ?? 以技術驅動創新,我們的征途是改變世界!
- ?? 實戰干貨:編程嚴選網
0 前言
RocketMQ的Pro只需配置一個接入地址,即可訪問整個集群,而無需client配置每個Broker的地址。
即RocketMQ會自動根據要訪問的topic名稱、queue序號,找到對應Broker地址。
Q:咋實現的?
A:由NameServer協調Broker和client共同實現,可伸縮的分布式集群,都需類似服務。
1 NamingService作用
為client提供路由信息,幫助client找到對應的Broker,NamingService本身也是集群。分布式集群架構的通用設計。
負責維護集群內所有節點的路由信息:client需訪問的某特定服務在啥節點。
集群的節點會主動連接NamingService服務,注冊自身路由信息。給client提供路由尋址服務的方式:
- client直連NamingService,查詢路由信息
- client連接集群內任一節點查詢路由信息,節點再從自身緩存或查詢NamingService
2 NameServer咋提供服務?
NameServer獨立進程,為Broker、Pro、Con提供服務。支持單點、多節點集群部署。
NameServer最主要為client提供尋址服務,協助 client 找到topic對應的Broker地址。也負責監控每個Broker的存活狀態。
每個NameServer節點都保存了集群所有Broker的路由信息,因此可獨立提供服務。
每個Broker都需和【所有NameServer節點】通信。當 Broker 保存的 Topic 信息變化時,要主動通知所有 NameServer 更新路由信息。
為保證數據一致性,Broker會與所有NameServer節點建立長連接,定期上報Broker的路由信息。該上報路由信息的RPC請求,同時還起到 Broker 與 NameServer 間心跳作用,NameServer 依靠該心跳確定Broker健康狀態。
client會選擇連接某一NameServer節點,定期獲取訂閱topic的路由信息,用于Broker尋址。
每個 NameServer 節點都可獨立提供服務,因此對于client(Pro和Con),只需選擇任一 NameServer 節點來查詢路由信息即可。
client在生產或消費某topic的消息前:
- 先從NameServer查詢這topic的路由信息
- 然后根據路由信息獲取到當前topic和隊列對應的Broker物理地址
- 再連接到Broker節點上生產或消費
如果NameServer檢測到與Broker的連接中斷,NameServer會認為這個Broker不能再提供服務。會立即把這Broker從路由信息中移除,避免client連接到一個不可用Broker。
client在與Broker通信失敗后,會重新去NameServer拉取路由信息,然后連接到其他Broker繼續生產或消費消息,實現自動切換失效Broker。
3 組成
UML
- NamesrvStartup:程序入口
- NamesrvController:NameServer的總控制器,負責所有服務的生命周期管理
- BrokerHousekeepingService:監控Broker連接狀態的代理類
- DefaultRequestProcessor:負責處理 client、Broker 發送過來的RPC請求的處理器
- ClusterTestRequestProcessor:用于測試的請求處理器
- RouteInfoManager:NameServer最核心的實現類,負責維護所有集群路由信息,這些路由信息都保存在內存,無持久化
RouteInfoManager
如下5個Map保存了集群所有的Broker和topic的路由信息
public class RouteInfoManager {
/**
* 120s,broker 上一次心跳時間超過該值便會被剔除
*/
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 維護topic和queue信息
* topic消息隊列的路由信息,消息發送時會根據路由表進行負載均衡
* K=topic名稱
* V=Map:
* K=brokerName
* V=隊列數據,如讀/寫隊列的數量、權重
*/
private final HashMap<String/* topic */, Map<String /* brokerName */ , QueueData>> topicQueueTable;
/**
* 維護集群中每個brokerName對應的Broker信息
* broker的基礎信息
* K=brokerName,V=brokerName
* broker所在的集群信息,主備broker的地址。
*/
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
/**
* 維護clusterName與BrokerName的對應關系
* broker集群信息
* K=集群名稱
* V=集群中所有broker的名稱
*/
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**
* 維護每個Broker(brokerAddr)當前的動態信息,包括心跳更新時間,路由數據版本等
* Broker狀態信息,NameServer每次收到心跳包時會替換該信息。這也是NameServer每10s要掃描的信息。
*/
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
/**
* 維護每個Broker (brokerAddr)對應的消息過濾服務的地址(Filter Server),用于服務端消息過濾
* Broker上的FilterServer列表,用于類模式消息過濾。類模式過濾機制在4.4及以后版本被廢棄
*/
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
Broker信息
public class BrokerData implements Comparable<BrokerData> {
// 集群名稱
private String cluster;
private String brokerName;
// 保存Broker物理地址 <brokerId,broker address>
private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
4 NameServer處理Broker注冊的路由信息
NameServer處理Broker和client所有RPC請求的入口方法:
4.1 DefaultRequestProcessor
4.1.1 processRequest
處理Broker注冊請求
public class DefaultRequestProcessor extends AsyncNettyRequestProcessor implements NettyRequestProcessor {
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) {
// 典型的Request請求分發器,根據request.getCode()來分發請求到對應處理器
switch (request.getCode()) {
...
}
return null;
}
4.1.2 REGISTER_BROKER
Broker發給NameServer注冊請求的Code為REGISTER_BROKER,根據Broker版本號不同,分別有兩個不同處理方法
case RequestCode.REGISTER_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);
}
① registerBroker
public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) {
// registerBroker
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
}
② registerBrokerWithFilterServer
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request) {
...
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
兩個方法流程差不多,都是調用
4.2 RouteInfoManager
registerBroker
注冊Broker路由信息。
public RegisterBrokerResult registerBroker(
final String clusterName,
final String brokerAddr,
final String brokerName,
final long brokerId,
final String haServerAddr,
final TopicConfigSerializeWrapper topicConfigWrapper,
final List<String> filterServerList,
final Channel channel) {
RegisterBrokerResult result = new RegisterBrokerResult();
try {
try {
// 加寫鎖,防止并發修改數據
this.lock.writeLock().lockInterruptibly();
// 更新clusterAddrTable
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet<>();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
// 更新brokerAddrTable
boolean registerFirst = false;
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
registerFirst = true; // 標識需要先注冊
brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
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
// 更新brokerAddrTable中的brokerData
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();
}
}
// 如果是新注冊的Master Broker,或者Broker中的路由信息變了,需要更新topicQueueTable
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());
}
}
}
}
// 更新brokerLiveTable
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
if (null == prevBrokerLiveInfo) {
log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}
// 更新filterServerTable
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
// 若是Slave Broker,需要在返回的信息中帶上master的相關信息
if (MixAll.MASTER_ID != brokerId) {
String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
if (masterAddr != null) {
BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
if (brokerLiveInfo != null) {
result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
result.setMasterAddr(masterAddr);
}
}
}
} finally {
// 釋放寫鎖
this.lock.writeLock().unlock();
}
} catch (Exception e) {
log.error("registerBroker Exception", e);
}
return result;
}
5 client定位Broker
對于client,無論Pro、Con,通過topic尋找Broker的流程一致,都是同一實現。
client啟動后,會啟動定時器,定期從NameServer拉取相關topic的路由信息,緩存在本地內存,需要時使用。
每個topic的路由信息由TopicRouteData對象表示:
public class TopicRouteData extends RemotingSerializable {
private String orderTopicConf;
private List<QueueData> queueDatas;
private List<BrokerData> brokerDatas;
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
client選定隊列后,可在對應QueueData找到對應BrokerName,然后找到對應BrokerData對象,最終找到對應Master Broker的地址。
6 根據topic,查詢Broker的路由信息
RouteInfoManager#pickupTopicRouteData
NameServer處理client請求和處理Broker請求流程一致,都是通過路由分發器將請求分發的對應的處理方法中:
public TopicRouteData pickupTopicRouteData(final String topic) {
// 1.初始化返回值 topicRouteData
TopicRouteData topicRouteData = new TopicRouteData();
boolean foundQueueData = false;
boolean foundBrokerData = false;
Set<String> brokerNameSet = new HashSet<>();
List<BrokerData> brokerDataList = new LinkedList<>();
topicRouteData.setBrokerDatas(brokerDataList);
HashMap<String, List<String>> filterServerMap = new HashMap<>();
topicRouteData.setFilterServerTable(filterServerMap);
try {
try {
// 2.加讀鎖
this.lock.readLock().lockInterruptibly();
// 3.先獲取topic對應的queue信息
List<QueueData> queueDataList = this.topicQueueTable.get(topic);
if (queueDataList != null) {
// 4.將queue信息存入返回值
topicRouteData.setQueueDatas(queueDataList);
foundQueueData = true;
// 5.遍歷隊列,找出相關的所有BrokerName
for (QueueData qd : queueDataList) {
brokerNameSet.add(qd.getBrokerName());
}
// 6.遍歷BrokerName,找到對應BrokerData,并寫入返回結果中
for (String brokerName : brokerNameSet) {
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null != brokerData) {
BrokerData brokerDataClone = new BrokerData(brokerData.getCluster(), brokerData.getBrokerName(), (HashMap<Long, String>) brokerData
.getBrokerAddrs().clone());
brokerDataList.add(brokerDataClone);
foundBrokerData = true;
for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) {
List<String> filterServerList = this.filterServerTable.get(brokerAddr);
filterServerMap.put(brokerAddr, filterServerList);
}
}
}
}
} finally {
// 7.釋放讀鎖
this.lock.readLock().unlock();
}
} catch (Exception e) {
log.error("pickupTopicRouteData Exception", e);
}
log.debug("pickupTopicRouteData {} {}", topic, topicRouteData);
if (foundBrokerData && foundQueueData) {
// 8.返回結果
return topicRouteData;
}
return null;
}
本文由博客一文多發平臺 OpenWrite 發布!