咱們知道,對於服務治理框架來講,服務通訊(RPC)和服務管理兩部分必不可少,而服務管理又分爲服務註冊、服務發現和服務人工介入,咱們來看看Dubbo框架的結構圖(來源網絡):java
圖中能夠看出,服務提供者Provider往服務註冊中心Registry註冊服務,而的消費者Consumer從服務註冊中心訂閱它須要的服務,而不是所有服務,當有新的Provider出現,或者現有Provider宕機,註冊中心Registry都應該能儘早發現,並將新的Provider列表推送給對應的Consumer,有了這樣的機制,Dubbo才能作到Failover,而Failover的時效性,由註冊中心Registry的實現決定。緩存
Dubbo線上支持三種註冊中心:自帶的SimpleRegistry、Redis和Zookeeper,固然,最經常使用的仍是Zookeeper做爲註冊中心,由於太多分佈式的中間件須要依賴Zookeeper做爲協做者。那麼怎麼才能讓Dubbo知道咱們使用哪一個實現做爲註冊中心呢?咱們只須要在dubbo的xml配置文件中配置dubbo:registry節點便可:網絡
<dubbo:registry id="dubboRegistry"protocol="zookeeper"address="${dubbo.registry.address}"/>負載均衡
沒錯,protocol就指明瞭註冊中心的實現。框架
要想作到服務的可靠,避免分佈式系統的單點問題,除了Provider能夠集羣部署外,註冊中心的弱依賴也是必須的,註冊中心的宕機,不會影響現有服務的運行,只是不能註冊新的服務和進行服務發現,Failover仍是能夠作的,好比Consumer能夠經過服務調用來簡單判斷當前的Provier是否可用。若是某個Consumer宕機了,當它重啓後,發現註冊中心也掛了,那咋辦?爲了防止這種問題出現,Dubbo的Consumer會將本身須要的Provider列表在本地保存一份,固然,裏面也包括本身暴露的服務信息(即本身也做爲Provider),咱們能夠看看AbstractRegistry中的實現:異步
public AbstractRegistry(URL url) {分佈式
setUrl(url);ide
// 啓動文件保存定時器this
syncSaveFile= url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY,false);url
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),就將在用戶的當前用戶目錄/.dubbo/目錄下新建一個dubbo-registry開頭的保存全部URL信息的Cache文件,一般來講一個應用能夠在多個註冊中心暴露本身的服務,也能夠從多個註冊中心訂閱不一樣的服務,因此這裏的Cache文件名加入了註冊中心的主機名。還有一個lock文件,用來防止不一樣的JVM進程同時修改Cache文件,注意,這裏只是防止,因此意味着同一目錄的Cache文件能夠由多個JVM進程共享,當多個JVM進程恰巧同時修改Cache文件時,將會有一個進程獲取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");
}
這將致使某個URL更新到Cache文件失敗,但Dubbo提供了重試機制,以保證Cache文件中信息能和內存中的信息最終一致。但不要認爲Cache文件中的Provider和Consumer列表是和當前運行的服務一致,由於當一個服務部署多個應用時,Cache文件被多個JVM同時寫的機率仍是很大的,因此這時總有JVM進程度lock文件獲取鎖失敗(即FileChannel#tryLock()失敗),這時它只能乖乖稍後重試了。寫Cache的方式也很簡單粗暴,即先讀取整個Cache文件,而後再往其寫入當前處理的URL,而後再全量寫入,可見,若是某個服務(URL)已經再也不使用,它有可能一直存在於Cache文件中。
保存Cache還分爲同步保存和異步保存,咱們知道內存中服務列表的更新相對於服務調用來講確定是異步的,但爲啥保存Cache文件還要分同步和異步呢?由於在Dubbo中,服務(或者叫URL)是一個個來更新的,也就是說,當服務比較多時,使用異步保存Cache文件能使應用啓動和服務更新速度更快,而整個更新過程是由AbstractRegistry#notify來觸發的。
咱們再來看看若是選擇使用Zookeeper用來作Dubbo的註冊中心,那麼Provider和Consumer的數據在上是怎麼存儲的。Dubbo在ZK的全部數據都在/dubbo節點下,以下圖:
/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
咱們能夠看到,每一個服務(URL)在dubbo節點下都會有一個對應的ZK持久化節點,而每一個服務節點下面都會有四個持久化子節點,表明消費者(consumer)、路由(routers)、提供者(providers)和配置(configurators),consumer和providers節點好理解,放的就是該URL下消費者和提供者的URL所有信息,而routers和configurators主要用於控制路由規則,這在正常狀況下是用的比較少的,因此這兩個節點數據一般爲空。
如今咱們說說和服務註冊相關的兩個異常信息, 先給出Dubbo的集羣容錯圖:
一個常見的異常信息是"Forbid consumer XXXXXaccess service XXXXX from registry XXXXX use dubbo version 2.5.3, Please checkregistry access list (whitelist/blacklist).",當咱們須要調用服務時,會先從本地的註冊目錄也就是RegistryDirectory來拿取調用(Invoker)列表,見上圖Directory節點,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
可見,當forbidden爲false時,會拋出該異常信息,當註冊中心給它推送最新的Provider列表時,上面的forbidden的值已經變成了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(); // 關閉全部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列表,便於交叉對比
}
從上面代碼能夠看出,當該URL協議爲empty時,說明該URL已經被禁止(forbidden)了,那何時URL的協議會被設置成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;
}
可見,當providers列表爲空時,也就是某個URL下沒有活着的Provider時,Consumer會將本地的invokerUrl的協議設置成empty,而toUrlsWithEmpty是在ZookeeperRegistry訂閱方法doSubscribe中被調用的,這裏再也不給出代碼。
另外一個是"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.",由於每次調用時都會去檢查調用列表,若是列表有多個可用服務(即多個Provider),將會使用配置的負載均衡方式來選擇一個服務來調用,但若是服務列表爲空,則會拋異常,也就是在上圖的Invoker節點拋出異常,這種狀況通常是說明當前沒有可用的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.");
}
}
對於這兩個異常的直接結論是,若是某個URL去註冊中心註冊過,但後來該URL下沒有Provider了,那麼此時Consumer調用Provider將報第一種異常;若是Consumer調用了一個從未去註冊中心註冊過的URL,則會報第二種異常。
須要明確一點的是,註冊中心的兩個重要目的是服務發現和服務人工介入,線上的Provider和Consumer都不能強依賴註冊中心,哪怕註冊中心是雙機部署,但要作到對註冊中心的弱依賴,Consumer端須要有簡單的負載均衡和Failover機制。
本文轉自:http://blog.csdn.net/manzhizhen/article/details/53025666