轉(zhuǎn)自:https://blog.csdn.net/manzhizhen/article/details/53025666
我們知道俺猿,對于服務(wù)治理框架來說,服務(wù)通信(RPC)和服務(wù)管理兩部分必不可少,而服務(wù)管理又分為服務(wù)注冊欠橘、服務(wù)發(fā)現(xiàn)和服務(wù)人工介入涛目,我們來看看Dubbo框架的結(jié)構(gòu)圖
圖中可以看出,服務(wù)提供者Provider往服務(wù)注冊中心Registry注冊服務(wù)靠抑,而的消費(fèi)者Consumer從服務(wù)注冊中心訂閱它需要的服務(wù)量九,而不是全部服務(wù),當(dāng)有新的Provider出現(xiàn)颂碧,或者現(xiàn)有Provider宕機(jī)荠列,注冊中心Registry都應(yīng)該能盡早發(fā)現(xiàn),并將新的Provider列表推送給對應(yīng)的Consumer载城,有了這樣的機(jī)制肌似,Dubbo才能做到Failover,而Failover的時(shí)效性诉瓦,由注冊中心Registry的實(shí)現(xiàn)決定川队。
Dubbo線上支持三種注冊中心:自帶的SimpleRegistry、Redis和Zookeeper睬澡,當(dāng)然固额,最常用的還是Zookeeper作為注冊中心,因?yàn)樘喾植际降闹虚g件需要依賴Zookeeper作為協(xié)作者猴贰。那么怎么才能讓Dubbo知道我們使用哪個(gè)實(shí)現(xiàn)作為注冊中心呢对雪?我們只需要在dubbo的xml配置文件中配置dubbo:registry節(jié)點(diǎn)即可:
<dubbo:registry id="dubboRegistry"protocol="zookeeper"address="${dubbo.registry.address}"/>
沒錯(cuò),protocol就指明了注冊中心的實(shí)現(xiàn)米绕。
要想做到服務(wù)的可靠瑟捣,避免分布式系統(tǒng)的單點(diǎn)問題近迁,除了Provider可以集群部署外拴疤,注冊中心的弱依賴也是必須的碍讨,注冊中心的宕機(jī)拓诸,不會(huì)影響現(xiàn)有服務(wù)的運(yùn)行,只是不能注冊新的服務(wù)和進(jìn)行服務(wù)發(fā)現(xiàn)桑李,F(xiàn)ailover還是可以做的踱蛀,比如Consumer可以通過服務(wù)調(diào)用來簡單判斷當(dāng)前的Provier是否可用。如果某個(gè)Consumer宕機(jī)了贵白,當(dāng)它重啟后率拒,發(fā)現(xiàn)注冊中心也掛了,那咋辦禁荒?為了防止這種問題出現(xiàn)猬膨,Dubbo的Consumer會(huì)將自己需要的Provider列表在本地保存一份,當(dāng)然呛伴,里面也包括自己暴露的服務(wù)信息(即自己也作為Provider)勃痴,我們可以看看AbstractRegistry中的實(shí)現(xiàn):
public AbstractRegistry(URL url) {
setUrl(url);
// 啟動(dòng)文件保存定時(shí)器
syncSaveFile= url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY,false);
String filename =url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-"+ url.getHost() +".cache");
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = newFile(filename);
if(! file.exists() &&file.getParentFile() !=null&&! file.getParentFile().exists()){
if(! file.getParentFile().mkdirs()){
throw new IllegalArgumentException("Invalid registry store file "+ file +", cause: Failed to create directory" + file.getParentFile()+"!");
}
}
}
this.file= file;
loadProperties();
notify(url.getBackupUrls());
}
注意看黃底代碼部分,如果沒有在屬性文件中配置file(Constants.FILE_KEY),就將在用戶的當(dāng)前用戶目錄/.dubbo/目錄下新建一個(gè)dubbo-registry開頭的保存所有URL信息的Cache文件热康,通常來說一個(gè)應(yīng)用可以在多個(gè)注冊中心暴露自己的服務(wù)沛申,也可以從多個(gè)注冊中心訂閱不同的服務(wù),所以這里的Cache文件名加入了注冊中心的主機(jī)名姐军。還有一個(gè)lock文件铁材,用來防止不同的JVM進(jìn)程同時(shí)修改Cache文件,注意庶弃,這里只是防止衫贬,所以意味著同一目錄的Cache文件可以由多個(gè)JVM進(jìn)程共享德澈,當(dāng)多個(gè)JVM進(jìn)程恰巧同時(shí)修改Cache文件時(shí)歇攻,將會(huì)有一個(gè)進(jìn)程獲取lock文件的鎖失敗,見保存Cache的過程的AbstractRegistry#doSaveProperties方法的片段:
FileChannel channel = raf.getChannel();
try {
FileLocklock = channel.tryLock();
if (lock == null) {
thrownew IOException("Can not lock theregistry cache file "+file.getAbsolutePath() + ", ignore and retrylater, maybe multi java process use the file, please config:dubbo.registry.file=xxx.properties");
}
這將導(dǎo)致某個(gè)URL更新到Cache文件失敗梆造,但Dubbo提供了重試機(jī)制缴守,以保證Cache文件中信息能和內(nèi)存中的信息最終一致。但不要認(rèn)為Cache文件中的Provider和Consumer列表是和當(dāng)前運(yùn)行的服務(wù)一致镇辉,因?yàn)楫?dāng)一個(gè)服務(wù)部署多個(gè)應(yīng)用時(shí)屡穗,Cache文件被多個(gè)JVM同時(shí)寫的概率還是很大的,所以這時(shí)總有JVM進(jìn)程度lock文件獲取鎖失敽龈亍(即FileChannel#tryLock()失敶迳啊),這時(shí)它只能乖乖稍后重試了屹逛。寫Cache的方式也很簡單粗暴础废,即先讀取整個(gè)Cache文件汛骂,然后再往其寫入當(dāng)前處理的URL,然后再全量寫入评腺,可見帘瞭,如果某個(gè)服務(wù)(URL)已經(jīng)不再使用,它有可能一直存在于Cache文件中蒿讥。
保存Cache還分為同步保存和異步保存蝶念,我們知道內(nèi)存中服務(wù)列表的更新相對于服務(wù)調(diào)用來說肯定是異步的,但為啥保存Cache文件還要分同步和異步呢芋绸?因?yàn)樵贒ubbo中媒殉,服務(wù)(或者叫URL)是一個(gè)個(gè)來更新的,也就是說摔敛,當(dāng)服務(wù)比較多時(shí)适袜,使用異步保存Cache文件能使應(yīng)用啟動(dòng)和服務(wù)更新速度更快,而整個(gè)更新過程是由AbstractRegistry#notify來觸發(fā)的舷夺。
我們再來看看如果選擇使用Zookeeper用來做Dubbo的注冊中心苦酱,那么Provider和Consumer的數(shù)據(jù)在上是怎么存儲(chǔ)的。Dubbo在ZK的所有數(shù)據(jù)都在/dubbo節(jié)點(diǎn)下给猾,如下圖:
/dubbo
/com.manzhizhen.user.Service1
/consumers
/routers
/providers
/configurators
/com.manzhizhen.user.Service2
/consumers
/routers
/providers
/configurators
/com.manzhizhen.user.Service3
/consumers
/routers
/providers
/configurators
我們可以看到疫萤,每個(gè)服務(wù)(URL)在dubbo節(jié)點(diǎn)下都會(huì)有一個(gè)對應(yīng)的ZK持久化節(jié)點(diǎn),而每個(gè)服務(wù)節(jié)點(diǎn)下面都會(huì)有四個(gè)持久化子節(jié)點(diǎn)敢伸,代表消費(fèi)者(consumer)扯饶、路由(routers)、提供者(providers)和配置(configurators)池颈,consumer和providers節(jié)點(diǎn)好理解尾序,放的就是該URL下消費(fèi)者和提供者的URL全部信息,而routers和configurators主要用于控制路由規(guī)則躯砰,這在正常情況下是用的比較少的每币,所以這兩個(gè)節(jié)點(diǎn)數(shù)據(jù)通常為空。
現(xiàn)在我們說說和服務(wù)注冊相關(guān)的兩個(gè)異常信息琢歇, 先給出Dubbo的集群容錯(cuò)圖:
一個(gè)常見的異常信息是"Forbid consumer XXXXXaccess service XXXXX from registry XXXXX use dubbo version 2.5.3, Please checkregistry access list (whitelist/blacklist)."兰怠,當(dāng)我們需要調(diào)用服務(wù)時(shí),會(huì)先從本地的注冊目錄也就是RegistryDirectory來拿取調(diào)用(Invoker)列表李茫,見上圖Directory節(jié)點(diǎn)揭保,RegistryDirectory#doList代碼片段如下:
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
thrownew RpcException(RpcException.FORBIDDEN_EXCEPTION,"Forbid consumer "+ NetUtils.getLocalHost() + " access service" +getInterface().getName() + " from registry "+ getUrl().getAddress() +" use dubbo version " + Version.getVersion() + ", Please check registry access list(whitelist/blacklist).");
}
List<Invoker<T>> invokers = null;
Map<String, List<Invoker<T>>>localMethodInvokerMap =this.methodInvokerMap;// local reference
可見,當(dāng)forbidden為false時(shí)魄宏,會(huì)拋出該異常信息秸侣,當(dāng)注冊中心給它推送最新的Provider列表時(shí),上面的forbidden的值已經(jīng)變成了false,見RegistryDirectory#refreshInvoker代碼片段:
private void refreshInvoker(List<URL>invokerUrls){
if(invokerUrls !=null&&invokerUrls.size() ==1&& invokerUrls.get(0) !=null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden=true;//禁止訪問
this.methodInvokerMap=null; // 置空列表
destroyAllInvokers(); // 關(guān)閉所有Invoker
} else{
this.forbidden=false;//允許訪問
Map<String, Invoker<T>>oldUrlInvokerMap =this.urlInvokerMap;// local reference
if (invokerUrls.size() ==0&&this.cachedInvokerUrls!=null){
invokerUrls.addAll(this.cachedInvokerUrls);
} else{
this.cachedInvokerUrls=new HashSet<URL>();
this.cachedInvokerUrls.addAll(invokerUrls);//緩存invokerUrls列表味榛,便于交叉對比
}
從上面代碼可以看出方篮,當(dāng)該URL協(xié)議為empty時(shí),說明該URL已經(jīng)被禁止(forbidden)了励负,那什么時(shí)候URL的協(xié)議會(huì)被設(shè)置成empty呢藕溅?我們看看ZookeeperRegistry#toUrlsWithEmpty方法:
private List<URL> toUrlsWithEmpty(URLconsumer, String path, List<String> providers) {
List<URL> urls = toUrlsWithoutEmpty(consumer, providers);
if (urls == null || urls.isEmpty()) {
int i = path.lastIndexOf('/');
String category = i < 0? path : path.substring(i +1);
URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category);
urls.add(empty);
}
return urls;
}
可見,當(dāng)providers列表為空時(shí)继榆,也就是某個(gè)URL下沒有活著的Provider時(shí)巾表,Consumer會(huì)將本地的invokerUrl的協(xié)議設(shè)置成empty,而toUrlsWithEmpty是在ZookeeperRegistry訂閱方法doSubscribe中被調(diào)用的略吨,這里不再給出代碼集币。
另一個(gè)是"Failed to invoke the method XXXXXin the service XXXXX. No provider available for the service XXXXX from registryXXXXX on the consumer XXXXX using the dubbo version 2.5.3. Please check if theproviders have been started and registered.",因?yàn)槊看握{(diào)用時(shí)都會(huì)去檢查調(diào)用列表翠忠,如果列表有多個(gè)可用服務(wù)(即多個(gè)Provider)鞠苟,將會(huì)使用配置的負(fù)載均衡方式來選擇一個(gè)服務(wù)來調(diào)用,但如果服務(wù)列表為空秽之,則會(huì)拋異常当娱,也就是在上圖的Invoker節(jié)點(diǎn)拋出異常,這種情況一般是說明當(dāng)前沒有可用的Provider考榨,見AbstractClusterInvoker#checkInvokers代碼:
protected void checkInvokers(List<Invoker<T>> invokers, Invocation invocation) {
if (invokers == null|| invokers.size() ==0) {
thrownew RpcException("Failed to invokethe method "
+ invocation.getMethodName() +" in the service "+ getInterface().getName()
+ ". No provideravailable for the service "+directory.getUrl().getServiceKey()
+ " from registry" + directory.getUrl().getAddress()
+ " on the consumer" + NetUtils.getLocalHost()
+ " using the dubboversion " + Version.getVersion()
+ ". Please check ifthe providers have been started and registered.");
}
}
對于這兩個(gè)異常的直接結(jié)論是跨细,如果某個(gè)URL去注冊中心注冊過,但后來該URL下沒有Provider了河质,那么此時(shí)Consumer調(diào)用Provider將報(bào)第一種異常冀惭;如果Consumer調(diào)用了一個(gè)從未去注冊中心注冊過的URL,則會(huì)報(bào)第二種異常掀鹅。
需要明確一點(diǎn)的是散休,注冊中心的兩個(gè)重要目的是服務(wù)發(fā)現(xiàn)和服務(wù)人工介入,線上的Provider和Consumer都不能強(qiáng)依賴注冊中心乐尊,哪怕注冊中心是雙機(jī)部署戚丸,但要做到對注冊中心的弱依賴,Consumer端需要有簡單的負(fù)載均衡和Failover機(jī)制科吭。
文末附上dubbo調(diào)用邏輯