Dubbo注冊(cè)中心
前言
本篇主要介紹一下Dubbo的注冊(cè)中心的總體工作流程愈涩,以及不同類型注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)原理,同時(shí)還會(huì)介紹一下注冊(cè)中心支持的通用特性慢显,如緩存機(jī)制持际、重試機(jī)制,最會(huì)會(huì)對(duì)整個(gè)注冊(cè)中心中用到的設(shè)計(jì)模式做深度的解析殴蹄,主要目的是為了深入理解Dubbo各種注冊(cè)中心的實(shí)現(xiàn)原理究抓,方便后續(xù)快速理解并拓展。
概述
注冊(cè)中心是Dubbo體系中核心組件之一袭灯,通過(guò)注冊(cè)中心實(shí)現(xiàn)了再分布式環(huán)境中各服務(wù)之間的注冊(cè)發(fā)現(xiàn)刺下,其主要作用如下:
- 動(dòng)態(tài)加入:一個(gè)服務(wù)提供者通過(guò)注冊(cè)中心動(dòng)態(tài)地把自己暴露給其他消費(fèi)者,無(wú)需消費(fèi)者挨個(gè)去更新配置文件稽荧。
- 動(dòng)態(tài)發(fā)現(xiàn):一個(gè)消費(fèi)者可以動(dòng)態(tài)的感知新的配置橘茉,路由規(guī)則和新的服務(wù)提供者,無(wú)需重啟服務(wù)就可以生效。
- 動(dòng)態(tài)調(diào)整:注冊(cè)中心支持參數(shù)的動(dòng)態(tài)調(diào)整畅卓,新參數(shù)自動(dòng)更新到所有相關(guān)服務(wù)節(jié)點(diǎn)擅腰。
- 統(tǒng)一配置:避免了本地配置導(dǎo)致各個(gè)服務(wù)的配置不一致的問(wèn)題。
模塊介紹
當(dāng)前dubbo版本: 3.0.4
模塊名稱 | 模塊介紹 |
---|---|
dubbo-registry-api | 包含注冊(cè)中心所有API和抽象實(shí)現(xiàn)類 |
dubbo-registry-kubernetes | 在kubernetes中注冊(cè)中心的實(shí)現(xiàn) |
dubbo-registry-dns | 使用dns作注冊(cè)中心的實(shí)現(xiàn) |
dubbo-registry-multicast | multicast模式的服務(wù)的注冊(cè)和發(fā)現(xiàn) |
dubbo-registry-multiple | 多注冊(cè)中心模式下服務(wù)的注冊(cè)和發(fā)現(xiàn) |
dubbo-registry-nacos | 使用Nacos作為注冊(cè)中心的實(shí)現(xiàn) |
dubbo-registry-xds | Service-mesh模式下服務(wù)的注冊(cè)和發(fā)現(xiàn)(Service mesh) |
dubbo-registry-zookeeper | 使用ZooKeeper作為注冊(cè)中心的實(shí)現(xiàn) |
從dubbo-registry的模塊中我們可以看到髓介,Dubbo主要包含6種注冊(cè)中心的實(shí)現(xiàn)惕鼓,分別是:kubernetes、dns唐础、multicast箱歧、nacos、xds一膨、zookeeper呀邢。
下面我們來(lái)看下各種維度的對(duì)比
工作流程
注冊(cè)紅心的總體工作流程比較簡(jiǎn)單,總體流程如下圖所示:
- 服務(wù)提供者服務(wù)啟動(dòng)時(shí)豹绪,會(huì)向注冊(cè)中心寫入自己的元數(shù)據(jù)信息价淌,同時(shí)會(huì)訂閱配置元數(shù)據(jù)信息。
- 消費(fèi)者啟動(dòng)時(shí)瞒津,也會(huì)想注冊(cè)中心寫入自己的元數(shù)據(jù)信息蝉衣,并訂閱服務(wù)提供者、路由和配置元數(shù)據(jù)信息巷蚪。
- 服務(wù)治理中心(dubbo-admin)啟動(dòng)時(shí)病毡,會(huì)同時(shí)訂閱所有的消費(fèi)者、服務(wù)提供者屁柏、路由和配置元數(shù)據(jù)信息啦膜。
- 當(dāng)有服務(wù)提供者下線或者有新的服務(wù)提供者加入時(shí),注冊(cè)中心服務(wù)提供者目錄會(huì)發(fā)生變化淌喻,變化信息會(huì)動(dòng)態(tài)給消費(fèi)者僧家、服務(wù)治理中心。
- 當(dāng)消費(fèi)者發(fā)起服務(wù)調(diào)用時(shí)裸删,會(huì)異步將調(diào)用八拱、統(tǒng)計(jì)信息等上報(bào)給監(jiān)控中心(dubbo-monitor-simple)。
數(shù)據(jù)結(jié)構(gòu)
注冊(cè)中心的總體流程大致類似涯塔,但是不同注冊(cè)中心有不同的實(shí)現(xiàn)方式肌稻,其數(shù)據(jù)結(jié)構(gòu)也不相同。ZooKeeper伤塌、Nacos等注冊(cè)中心都實(shí)現(xiàn)了這個(gè)流程,又要有些注冊(cè)中心并不常用轧铁,因此本篇只分析ZooKeeper每聪、Nacos兩種實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)。
ZooKeeper原理概述
ZooKeeper是樹形結(jié)構(gòu)的注冊(cè)中心,每個(gè)節(jié)點(diǎn)的類型分為持久化節(jié)點(diǎn)药薯、持久化順序節(jié)點(diǎn)绑洛、臨時(shí)節(jié)點(diǎn)、臨時(shí)順序節(jié)點(diǎn)童本。
- 持久化節(jié)點(diǎn):服務(wù)注冊(cè)后保證節(jié)點(diǎn)不會(huì)丟失真屯,注冊(cè)中心重啟也會(huì)存在。
- 持久化順序節(jié)點(diǎn):在持久節(jié)點(diǎn)特性的基礎(chǔ)上增加了節(jié)點(diǎn)先后順序的能力穷娱。
- 臨時(shí)節(jié)點(diǎn):服務(wù)注冊(cè)后連接丟失或session超時(shí)绑蔫,注冊(cè)的節(jié)點(diǎn)會(huì)自動(dòng)被移除。
- 臨時(shí)順序節(jié)點(diǎn):在臨時(shí)節(jié)點(diǎn)的特性上增加了節(jié)點(diǎn)先后順序的能力泵额。
Dubbo使用zk作為注冊(cè)中心時(shí)配深,只會(huì)創(chuàng)建持久化節(jié)點(diǎn)和臨時(shí)節(jié)點(diǎn)兩種,對(duì)創(chuàng)建的順序并沒(méi)有要求嫁盲。
/dubbo/org.apache.dubbo.demo.DemoService/providers是服務(wù)提供者在ZooKeeper注冊(cè)中心的路徑示例篓叶,是一種樹形結(jié)構(gòu),該結(jié)構(gòu)分為四層:root(根節(jié)點(diǎn)羞秤,對(duì)應(yīng)示例中的dubbo)缸托、service(接口名稱,對(duì)應(yīng)示例中的org.apache.dubbo.demo.DemoService)瘾蛋、四種服務(wù)目錄(對(duì)應(yīng)示例中的providers俐镐,其他目錄為consumers、routers瘦黑、configurations)京革。樹形結(jié)構(gòu)如下:
++ /dubbo
++-- xxxservice
+-- providers
+-- consumers
+-- routers
+-- configurators
樹形結(jié)構(gòu)的關(guān)系:
- 樹的節(jié)點(diǎn)是注冊(cè)中心分組。下面有多個(gè)服務(wù)接口幸斥,分組值分別來(lái)自用戶配置
<dubbo:registry>
中的group屬性匹摇,默認(rèn)是/dubbo。 - 服務(wù)接口下包含四類子目錄甲葬,分別是providers廊勃,consumers,routers经窖,configurations這個(gè)路徑是持久化節(jié)點(diǎn)坡垫。
- 服務(wù)提供者目錄(/dubbo/service/providers)下面包含的接口有多個(gè)服務(wù)者URL元數(shù)據(jù)信息。
- 服務(wù)消費(fèi)者目錄(/dubbo/service/consumers)下面包含的接口有多個(gè)消費(fèi)者URL元數(shù)據(jù)信息画侣。
- 路由配置目錄(/dubbo/service/routers)下面包含多個(gè)用于消費(fèi)者路由策略URL元數(shù)據(jù)信息冰悠。
- 動(dòng)態(tài)配置目錄(/dubbo/service/configurators)下面包含多個(gè)用于服務(wù)者動(dòng)態(tài)配置URL元數(shù)據(jù)信息。
大致的存儲(chǔ)結(jié)構(gòu)為:
- 在Dubbo框架啟動(dòng)時(shí)配乱,會(huì)根據(jù)用戶配置的服務(wù)溉卓,在注冊(cè)中心創(chuàng)建四個(gè)目錄皮迟,在providers和consumers目錄中分別存儲(chǔ)服務(wù)提供方、消費(fèi)方元數(shù)據(jù)信息桑寨,主要包括IP伏尼、端口、權(quán)重和應(yīng)用名等數(shù)據(jù)尉尾。
- 在Dubbo框架進(jìn)行服務(wù)調(diào)用時(shí)爆阶,用戶可以通過(guò)服務(wù)治理平臺(tái)(dubbo-admin)下發(fā)路由配置。如果要運(yùn)行時(shí)改變服務(wù)參數(shù)沙咏,則用戶可以通過(guò)服務(wù)治理平臺(tái)(dubbo-admin)下發(fā)動(dòng)態(tài)配置辨图。服務(wù)端會(huì)通過(guò)訂閱機(jī)制收到屬性變更,并重新更新已經(jīng)暴露的服務(wù)芭碍。
Nacos原理概述
不同的是在Nacos 中徒役,服務(wù)注冊(cè)時(shí)在服務(wù)端本地會(huì)通過(guò)輪詢注冊(cè)中心集群節(jié)點(diǎn)地址進(jìn)行服務(wù)得注冊(cè),在注冊(cè)中心上窖壕,即Nacos Server上采用了Map保存實(shí)例信息忧勿,當(dāng)然配置了持久化的服務(wù)會(huì)被保存到數(shù)據(jù)庫(kù)中,在服務(wù)的調(diào)用方瞻讽,為了保證本地服務(wù)實(shí)例列表的動(dòng)態(tài)感知鸳吸,Nacos與其他注冊(cè)中心不同的是,采用了 Pull/Push同時(shí)運(yùn)作的方式速勇。通過(guò)這些我們對(duì)Nacos注冊(cè)中心的原理有了一定的了解晌砾。
訂閱與發(fā)布
訂閱/發(fā)布是整個(gè)注冊(cè)中心的核心功能之一。與傳統(tǒng)系統(tǒng)應(yīng)用的差別:配置變化時(shí)烦磁,無(wú)需手動(dòng)觸發(fā)配置重新加載养匈,自動(dòng)化運(yùn)維。
當(dāng)我們使用注冊(cè)中心都伪,一個(gè)服務(wù)節(jié)點(diǎn)下線或者新增一個(gè)服務(wù)提供者節(jié)點(diǎn)呕乎,訂閱對(duì)應(yīng)接口的消費(fèi)者和治理中心就能及時(shí)厚道注冊(cè)中心的通知,并更新本地配置信息陨晶。整個(gè)過(guò)程都是自動(dòng)完成猬仁,無(wú)需人工參與。
Dubbo在上層抽象類這樣一個(gè)共走流程先誉,但可以有不同實(shí)現(xiàn)湿刽。本篇主要講ZooKeeper和Nacos的實(shí)現(xiàn)方式。
ZooKeeper的實(shí)現(xiàn)
發(fā)布的實(shí)現(xiàn)
服務(wù)提供者和消費(fèi)者都需要把自己注冊(cè)到注冊(cè)中心褐耳,服務(wù)提供者的注冊(cè)是為了讓消費(fèi)者感知服務(wù)的存在诈闺,從而發(fā)起遠(yuǎn)程調(diào)用,也讓服務(wù)治理中心感知有新的服務(wù)提供者上線铃芦。消費(fèi)者的發(fā)布是為了讓服務(wù)治理中心也可以發(fā)現(xiàn)自己雅镊。ZooKeeper發(fā)布代碼非常簡(jiǎn)單把曼,只是調(diào)用ZooKeeper的客戶端再注冊(cè)中心創(chuàng)建了一個(gè)目錄,代碼如下:
創(chuàng)建目錄
zkClient.create(toUrlPath(url));
url.getParameter(DYNAMIC_KEY, true)
刪除路徑
zkClient.delete(toUrlPath(url));
訂閱的實(shí)現(xiàn)
訂閱通常有pull和push兩種方式漓穿,一種是客戶端定時(shí)輪詢注冊(cè)中心拉取配置,另一種是注冊(cè)中心主動(dòng)推送數(shù)據(jù)給客戶端注盈。這兩種方式各有利弊晃危,目前dubbo采用的是第一次啟動(dòng)拉取方式,后續(xù)接收時(shí)間重新拉取數(shù)據(jù)老客。
在服務(wù)暴露時(shí)僚饭,服務(wù)端會(huì)訂閱configurators用于監(jiān)聽(tīng)動(dòng)態(tài)配置,在消費(fèi)者啟動(dòng)時(shí)胧砰,消費(fèi)端會(huì)訂閱providers鳍鸵、routers和configurators這三個(gè)目錄,分別對(duì)一個(gè)服務(wù)提供者尉间、路由和動(dòng)態(tài)配置變化通知偿乖。
Dubbo中有哪些ZooKeeper客戶端實(shí)現(xiàn)?
- Apache Curator
- zkClient
ZooKeeper注冊(cè)中心采用的是 事件通知 + 客戶端拉取的方式哲嘲,客戶端在第一次連接上注冊(cè)中心時(shí)贪薪,會(huì)獲取對(duì)應(yīng)目錄下全量的數(shù)據(jù),并在訂閱的節(jié)點(diǎn)上注冊(cè)一個(gè)watcher眠副,客戶端與注冊(cè)中心之間保持TCP長(zhǎng)連接画切,后續(xù)每個(gè)節(jié)點(diǎn)有任何數(shù)據(jù)變化的時(shí)候,注冊(cè)中心會(huì)根據(jù)watcher的回調(diào)通知客戶端(事件通知)囱怕,客戶端接收到通知霍弹,會(huì)把對(duì)應(yīng)節(jié)點(diǎn)的全量數(shù)據(jù)都拉取過(guò)來(lái)(客戶端拉取)娃弓,代碼中再NotifyListener#notify(List<URL> urls)
接口上就有約束的注釋說(shuō)明典格。
注意:全量拉取有局限性,當(dāng)微服務(wù)節(jié)點(diǎn)較多會(huì)對(duì)網(wǎng)絡(luò)造成很大壓力忘闻。
ZooKeeper每個(gè)節(jié)點(diǎn)都有一個(gè)版本號(hào)钝计,當(dāng)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變化(即事務(wù)操作)時(shí),該節(jié)點(diǎn)的版本號(hào)就會(huì)發(fā)生變化齐佳,并觸發(fā)watcher事件私恬,推送數(shù)據(jù)給訂閱方。版本號(hào)強(qiáng)調(diào)的是變更次數(shù)炼吴,即使該節(jié)點(diǎn)的值沒(méi)有變化本鸣,只要有更新操作,依然會(huì)使版本號(hào)變化硅蹦。
ZooKeeper全量訂閱服務(wù)代碼分析荣德,核心代碼來(lái)自ZookeeperRegistry#doSubscribe闷煤,代碼如下:
if (ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
boolean check = url.getParameter(CHECK_KEY, false);
//listeners為空說(shuō)明緩存中沒(méi)有就初始化一個(gè)空map
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
//這里把listeners放入緩存中
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> {
//內(nèi)部方法 不會(huì)立即執(zhí)行,只會(huì)在觸發(fā)通知時(shí)執(zhí)行
for (String child : currentChilds) {
//如果子節(jié)點(diǎn)有變化則會(huì)接到新通知涮瞻,遍歷所有的子節(jié)點(diǎn)
child = URL.decode(child);
//如果存在子節(jié)點(diǎn)還沒(méi)有被訂閱鲤拿,說(shuō)明是新節(jié)點(diǎn),則訂閱
if (!anyServices.contains(child)) {
anyServices.add(child);
subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
Constants.CHECK_KEY, String.valueOf(check)), k);
}
}
});
//創(chuàng)建持久化節(jié)點(diǎn)署咽,接下來(lái)訂閱持久化節(jié)點(diǎn)的直接子節(jié)點(diǎn)
zkClient.create(root, false);
List<String> services = zkClient.addChildListener(root, zkListener);
if (CollectionUtils.isNotEmpty(services)) {
//遍歷所有子節(jié)點(diǎn)進(jìn)行訂閱
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
//增加當(dāng)前節(jié)點(diǎn)的訂閱近顷,并且會(huì)返回該節(jié)點(diǎn)下所有子節(jié)點(diǎn)列表
subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
Constants.CHECK_KEY, String.valueOf(check)), listener);
}
}
}
從上面可以看出,此處主要支持Dubbo服務(wù)治理平臺(tái)(dubbo-admin),平臺(tái)在啟動(dòng)時(shí)會(huì)訂閱全量接口宁否,它會(huì)感知每個(gè)服務(wù)的狀態(tài)窒升。
接下來(lái)看一下普通消費(fèi)者的訂閱邏輯,首先根據(jù)URL的類別得到一組需要訂閱的路徑慕匠。如果類別是*饱须,則會(huì)訂閱四種類型的路徑(providers、routers台谊、consumers蓉媳、configurators),否則只訂閱providers路徑锅铅,代碼如下:
List<URL> urls = new ArrayList<>();
//根據(jù)URL的類別督怜,獲取一組要訂閱的路徑
for (String path : toCategoriesPath(url)) {
//如果listeners緩存為空則創(chuàng)建緩存
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
//如果zkListener緩存為空則創(chuàng)建緩存
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> new RegistryChildListenerImpl(url, path, k, latch));
if (zkListener instanceof RegistryChildListenerImpl) {
((RegistryChildListenerImpl) zkListener).setLatch(latch);
}
zkClient.create(path, false);
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
//訂閱,返回該節(jié)點(diǎn)下的子路徑并緩存狠角。
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
//回調(diào)NotifyListener号杠,更新本地緩存信息
notify(url, listener, urls);
注意:此處會(huì)根據(jù)URL中的category屬性獲取具體的類別(providers、routers丰歌、consumers姨蟋、configurators),然后拉取直接子節(jié)點(diǎn)的數(shù)據(jù)進(jìn)行通知立帖,如果是providers類別的數(shù)據(jù)眼溶,則訂閱方會(huì)更新本地Directory管理的Invoker服務(wù)列表,如果是roters分類晓勇,則訂閱會(huì)更新本地路由規(guī)則列表堂飞,如果是configuators類別,則訂閱方會(huì)更新或覆蓋本地動(dòng)態(tài)參數(shù)列表绑咱。
Nacos的實(shí)現(xiàn)
發(fā)布的實(shí)現(xiàn)
Nacos發(fā)布代碼也不復(fù)雜绰筛,只是調(diào)用Nacos的客戶端注冊(cè)了一個(gè)實(shí)例,代碼如下:
注冊(cè)實(shí)例
public void doRegister(URL url) {
try {
String serviceName = getServiceName(url);
Instance instance = createInstance(url);
/**
* namingService.registerInstance with {@link org.apache.dubbo.registry.support.AbstractRegistry#registryUrl}
* default {@link DEFAULT_GROUP}
*
* in https://github.com/apache/dubbo/issues/5978
*/
//調(diào)用nacos客戶端創(chuàng)建實(shí)例
namingService.registerInstance(serviceName,
getUrl().getGroup(Constants.DEFAULT_GROUP), instance);
} catch (Throwable cause) {
throw new RpcException("Failed to register " + url + " to nacos " + getUrl() + ", cause: " + cause.getMessage(), cause);
}
}
dubbo中描融,所以的服務(wù)都被封裝成了URL铝噩,對(duì)應(yīng)nacos中的服務(wù)實(shí)例Instance,所以服務(wù)注冊(cè)時(shí)窿克,只需要簡(jiǎn)單的將URL轉(zhuǎn)換成Instance就可以注冊(cè)到nacos中骏庸,針對(duì)具體細(xì)節(jié)可參考nacos官方文檔
銷毀實(shí)例
public void doUnregister(final URL url) {
try {
String serviceName = getServiceName(url);
Instance instance = createInstance(url);
//調(diào)用nacos客戶端刪除實(shí)例
namingService.deregisterInstance(serviceName,
getUrl().getGroup(Constants.DEFAULT_GROUP),
instance.getIp()
, instance.getPort());
} catch (Throwable cause) {
throw new RpcException("Failed to unregister " + url + " to nacos " + getUrl() + ", cause: " + cause.getMessage(), cause);
}
}
訂閱的實(shí)現(xiàn)
org.apache.dubbo.registry.nacos.NacosRegistry:518
private void subscribeEventListener(String serviceName, final URL url, final NotifyListener listener)
throws NacosException {
EventListener eventListener = new RegistryChildListenerImpl(serviceName, url, listener);
namingService.subscribe(serviceName,
getUrl().getGroup(Constants.DEFAULT_GROUP),
eventListener);
}
nacos的服務(wù)監(jiān)聽(tīng)是EventListener毛甲,所以dubbo的服務(wù)訂閱只需要將NotifyListener的處理包裝進(jìn)onEvent中處理即可, 通過(guò)namingService.subscribe添加nacos的訂閱具被。最終EventListener對(duì)象會(huì)被添加到事件調(diào)度器的監(jiān)聽(tīng)器列表中玻募,見(jiàn)如下代碼
com.alibaba.nacos.client.naming.event.InstancesChangeNotifier:54
private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();
private final Object lock = new Object();
/**
* register listener.
*
* @param groupName group name
* @param serviceName serviceName
* @param clusters clusters, concat by ','. such as 'xxx,yyy'
* @param listener custom listener
*/
public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
if (eventListeners == null) {
synchronized (lock) {
eventListeners = listenerMap.get(key);
if (eventListeners == null) {
eventListeners = new ConcurrentHashSet<EventListener>();
listenerMap.put(key, eventListeners);
}
}
}
eventListeners.add(listener);
}
當(dāng)有instanceEvent變化時(shí)觸發(fā)他的onEvent方法,代碼如下:
@Override
public void onEvent(InstancesChangeEvent event) {
String key = ServiceInfo
.getKey(NamingUtils.getGroupedName(event.getServiceName(), event.getGroupName()), event.getClusters());
ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
if (CollectionUtils.isEmpty(eventListeners)) {
return;
}
for (final EventListener listener : eventListeners) {
final com.alibaba.nacos.api.naming.listener.Event namingEvent = transferToNamingEvent(event);
if (listener instanceof AbstractEventListener && ((AbstractEventListener) listener).getExecutor() != null) {
((AbstractEventListener) listener).getExecutor().execute(() -> listener.onEvent(namingEvent));
} else {
listener.onEvent(namingEvent);
}
}
}
緩存機(jī)制
緩存的意義就在于拿空間換時(shí)間一姿,如果每次遠(yuǎn)程調(diào)用都要從注冊(cè)中心獲取一下可以調(diào)用的服務(wù)列表补箍,注冊(cè)中心要承受巨大的流量壓力。此外啸蜜,每次額外的網(wǎng)絡(luò)開銷也會(huì)讓整個(gè)系統(tǒng)的性能下降,因此Dubbo的注冊(cè)中心實(shí)現(xiàn)了通用的緩存機(jī)制辈挂,在抽象類AbstractRegistry中實(shí)現(xiàn)衬横。AbstractRegistry類結(jié)構(gòu)關(guān)系圖如下:
消費(fèi)者活服務(wù)中心獲取注冊(cè)信息后會(huì)做本地緩存,內(nèi)存中會(huì)有一份终蒂,保存到Properties對(duì)象中蜂林,磁盤上也會(huì)持久化一份文件,通過(guò)file對(duì)象引用拇泣。在AbstractRegistry抽象類中有如下定義:
private final Properties properties = new Properties();
// Local disk cache file
private File file;
private final ConcurrentMap<URL, Map<String, List<URL>>> notified =
new ConcurrentHashMap<>();
內(nèi)存中的緩存notified是ConcurrentHashMap里面又嵌套了一個(gè)Map噪叙,外層Map的key是消費(fèi)者的URL,內(nèi)層Map的key是分類霉翔,包含providers睁蕾,consumers,routers债朵,configurations四種子眶。value則是對(duì)應(yīng)的服務(wù)列表,對(duì)于沒(méi)有服務(wù)提供者提供服務(wù)的URL序芦,會(huì)已特殊的empty://
前綴開頭臭杰。
緩存的加載
在服務(wù)初始化的時(shí)候,AbstractRegistry構(gòu)造函數(shù)會(huì)從本地磁盤文件中把持久化的注冊(cè)數(shù)據(jù)督導(dǎo)properties對(duì)象中谚中,并接在到內(nèi)存緩存中渴杆,代碼如下:
private void loadProperties() {
if (file != null && file.exists()) {
InputStream in = null;
try {
//讀取磁盤中的文件
in = new FileInputStream(file);
properties.load(in);
if (logger.isInfoEnabled()) {
logger.info("Loaded registry cache file " + file);
}
} catch (Throwable e) {
logger.warn("Failed to load registry cache file " + file, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
}
Properties保存了所有服務(wù)提供者的URL,使用url#getServiceKey()作為key宪塔,提供者列表磁奖、路由規(guī)則列表、配置規(guī)則列表等作為value某筐。由于value是列表点寥,當(dāng)存在多個(gè)的時(shí)候使用空格隔開。還有一個(gè)特殊的key.registies来吩,保存所有的注冊(cè)中心的地址敢辩。如果應(yīng)用在啟動(dòng)過(guò)程中蔽莱,注冊(cè)中心無(wú)法連接活宕機(jī),則Dubbo框架會(huì)自動(dòng)通過(guò)本地緩存加載Invoker戚长。
緩存的保存與更新
緩存的保存有同步和異步兩種方式盗冷。異步會(huì)使用線程池異步保存,如果線程在執(zhí)行過(guò)程中出現(xiàn)異常同廉,則會(huì)再次調(diào)用線程池不斷重試仪糖,代碼如下所示。
同步與異步更新緩存
if (syncSaveFile) {
//同步保存
doSaveProperties(version);
} else {
//異步保存迫肖,放入線程池锅劝,會(huì)傳入一個(gè)AtomicLong的版本號(hào),保證數(shù)據(jù)是最新的蟆湖。
registryCacheExecutor.execute(new SaveProperties(version));
}
AbstractRegistry#notify
方法中封裝了更新內(nèi)存緩存和更新文件緩存的邏輯故爵。當(dāng)客戶端第一次訂閱獲取全量數(shù)據(jù),或者后續(xù)由于訂閱得到新數(shù)據(jù)時(shí)隅津,都會(huì)調(diào)用該方法進(jìn)行保存诬垂。
重試機(jī)制
由上面AbstractRegistry
相關(guān)類關(guān)系圖我們可以得知org.apache.dubbo.registry.support.FailbackRegistry
繼承了AbstractRegistry
,并在此基礎(chǔ)上增加了失敗重試機(jī)制作為抽象能力伦仍。ZookeeperRegistry
和RedisRegistry
繼承該抽象方法后结窘,直接使用即可。
FailbackRegistry
抽象類中定義了一個(gè)ScheduledExecutorService
,每經(jīng)過(guò)固定間隔(默認(rèn)
為5秒)調(diào)用FailbackRegistry#retry()
方法充蓝。另外隧枫,該抽象類中還有五個(gè)比較重要的集合,如下表所示谓苟。
集合名稱 | 集合介紹 |
---|---|
Set failedRegistered | 發(fā)起注冊(cè)失敗的URL集合 |
Set failedUnregistered | 取消注冊(cè)失敗的URL集合 |
ConcurrentMap> failedSubscribed | 發(fā)起訂閱失敗的監(jiān)聽(tīng)器集合 |
ConcurrentMap> failedUnsubscribed | 取消訂閱失敗的監(jiān)聽(tīng)器集合 |
ConcurrentMap>> failedNotified | 通知失敗的URL集合 |
在定時(shí)器中調(diào)用retry方法的時(shí)候悠垛,會(huì)把這五個(gè)集合分別遍歷和重試,重試成功則從集合中移除娜谊。FailbackRegistry
實(shí)現(xiàn)了subscribe确买、 unsubscribe
等通用方法,里面調(diào)用了未實(shí)現(xiàn)的模板方法纱皆,會(huì)由子類實(shí)現(xiàn)个曙。通用方法會(huì)調(diào)用這些模板方法闺金,如果捕獲到異常,則會(huì)把URL添加到對(duì)應(yīng)的重試集合中,以供定時(shí)器去重試啄糙。
設(shè)計(jì)模式
Dubbo注冊(cè)中心擁有良好的擴(kuò)展性铝宵,用戶可以在其基礎(chǔ)上代嗤,快速開發(fā)出符合自己業(yè)務(wù)需求的注冊(cè)中心菱农。這種擴(kuò)展性和Dubbo中使用的設(shè)計(jì)模式密不可分,下面介紹注冊(cè)中心模塊使用的設(shè)計(jì)模式。學(xué)完之后歧譬,能降低讀者對(duì)注冊(cè)中心源碼閱讀的門檻岸浑。
模板模式
整個(gè)注冊(cè)中心的邏輯部分使用了模板模式,其類的關(guān)系圖如圖所示瑰步。
AbstractRegistry
實(shí)現(xiàn)了Registry 接口中的注冊(cè)矢洲、訂閱、查詢缩焦、通知等方法读虏,還實(shí)現(xiàn)了磁盤文件持久化注冊(cè)信息這一通用方法。 但是注冊(cè)袁滥、訂閱盖桥、查詢、通知等方法只是簡(jiǎn)單地把URL
加入對(duì)應(yīng)的集合题翻,沒(méi)有具體的注冊(cè)或訂閱邏輯揩徊。
FailbackRegistry
又繼承了AbstractRegistry
, 重寫了父類的注冊(cè)、訂閱藐握、查詢和通知等方法,并且添加了重試機(jī)制垃喊。此外猾普,還添加了四個(gè)未實(shí)現(xiàn)的抽象模板方法,代碼如下本谜。
未實(shí)現(xiàn)的抽象模板方法
// ==== Template method ====
public abstract void doRegister(URL url);
public abstract void doUnregister(URL url);
public abstract void doSubscribe(URL url, NotifyListener listener);
public abstract void doUnsubscribe(URL url, NotifyListener listener);
以訂閱為例初家,FailbackRegistry
重寫了subscribe
方法,但只實(shí)現(xiàn)了訂閱的大體邏輯及異常處理等通用性的東西乌助。具體如何訂閱溜在,交給繼承的子類實(shí)現(xiàn)。這就是模板模式的具體實(shí)現(xiàn)他托,代碼如下所示掖肋。
模板模式調(diào)用
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
//此處調(diào)用了模板方法,由子類自行實(shí)現(xiàn)
// Sending a subscription request to the server side
doSubscribe(url, listener);
} catch (Exception e) {
...
}
}
工廠模式
所有的注冊(cè)中心實(shí)現(xiàn)赏参,都是通過(guò)對(duì)應(yīng)的工廠創(chuàng)建的志笼。工廠類之間的關(guān)系如圖所示。
AbstractRegistryFactory
實(shí)現(xiàn)了RegistryFactory
接口的getRegistry(URL ur1)
方法把篓,是一個(gè)通用實(shí)現(xiàn)纫溃,主要完成了加鎖,以及調(diào)用抽象模板方法createRegistry(URL ur1)
創(chuàng)建具體實(shí)現(xiàn)等操作韧掩,并緩存在內(nèi)存中紊浩。抽象模板方法會(huì)由具體子類繼承并實(shí)現(xiàn),代碼如下所示。
getRegistry抽象實(shí)現(xiàn)
// Lock the registry access process to ensure a single instance of the registry
registryManager.getRegistryLock().lock();
try {
// double check
// fix https://github.com/apache/dubbo/issues/7265.
defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed();
if (null != defaultNopRegistry) {
return defaultNopRegistry;
}
registry = registryManager.getRegistry(key);
if (registry != null) {
//緩存中有 則直接返回
return registry;
}
//create registry by spi/ioc
//如果注冊(cè)中心還沒(méi)創(chuàng)建過(guò)坊谁,
//則調(diào)用抽象方法createRegistry(ur1)重新創(chuàng)建一個(gè)createRegistry方法由具體的子類實(shí)現(xiàn)
registry = createRegistry(url);
} catch (Exception e) {
if (check) {
throw new RuntimeException("Can not create registry " + url, e);
} else {
LOGGER.warn("Failed to obtain or create registry ", e);
}
} finally {
// Release the lock
registryManager.getRegistryLock().unlock();
}
if (registry != null) {
//創(chuàng)建成功 緩存起來(lái)
registryManager.putRegistry(key, registry);
}
雖然每種注冊(cè)中心都有自己具體的工廠類费彼,但是在什么地方判斷,應(yīng)該調(diào)用哪個(gè)工廠類實(shí)現(xiàn)呢?代碼中并沒(méi)有看到顯式的判斷呜袁。答案就在RegistryFactory
接口中敌买,該接口里有一個(gè)Registry getRegistry(URL url)
方法,該方法上有@Adaptive({"protocol"})注解阶界,代碼如下所示虹钮。
RegistryFactory源碼
@SPI(scope = APPLICATION)
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
了解AOP的胖友就會(huì)很容易理解,這個(gè)注解會(huì)自動(dòng)生成代碼實(shí)現(xiàn)一些邏輯膘融,它的value參數(shù)會(huì)從URL中獲取protocol
鍵的值芙粱,并根據(jù)獲取的值來(lái)調(diào)用不同的工廠類。例如氧映,當(dāng)url.protocol = zookeeper
獲得 ZookeeperRegistryFactory
實(shí)現(xiàn)類春畔。具體Adaptive
注解的實(shí)現(xiàn)原理會(huì)在第4章Dubbo加載機(jī)制中講解。
小結(jié)
本篇介紹了Dubbo中已經(jīng)支持的注冊(cè)中心岛都。重點(diǎn)介紹了ZooKeeper和Nacos兩種注冊(cè)中心律姨。講解了兩種注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu),以及訂閱發(fā)布機(jī)制的具體實(shí)現(xiàn)臼疫。然后介紹了注冊(cè)中心中一些通用的關(guān)鍵特性择份,如數(shù)據(jù)緩存、重試等機(jī)制烫堤。最后荣赶,在對(duì)各種機(jī)制已經(jīng)了解的前提下,理解了整個(gè)注冊(cè)中心源碼的設(shè)計(jì)模式鸽斟。下一篇拔创,我們會(huì)詳細(xì)探討Dubbo SPI擴(kuò)展點(diǎn)加載的原理。
關(guān)注我富蓄,獲取免費(fèi)學(xué)習(xí)資料剩燥,無(wú)套路。
公眾號(hào):爪哇干貨分享