Dubbo原始碼分析(七)服務目錄

清幽之地發表於2019-03-25

前言

在上一章節的內容中,我們分析了服務引用的具體流程。在大多數情況下,為避免單點故障,我們的應用會部署在多臺伺服器上。對於我們的Dubbo而言,就會出現多個服務提供者。而且這些服務也並非是一成不變的,那麼就有這樣一個問題: 有新的服務提供者加入或者禁用、修改已有的服務提供者,那麼服務消費者怎麼及時感知它們的變化呢?

一、服務目錄

或許你還有印象 ,在服務引用的時候,我們曾經有用到它。這個就是服務目錄。 RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);

那麼,什麼是服務目錄呢?

簡單來說,服務目錄中儲存了一些和服務提供者有關的資訊,通過服務目錄,服務消費者可獲取到服務提供者的資訊,比如 ip、埠、服務協議等。

服務目錄在獲取註冊中心的服務配置資訊後,會為每條配置資訊生成一個 Invoker 物件,並把這個 Invoker 物件儲存起來,這個 Invoker 才是服務目錄最終持有的物件。多個服務提供者,就構成了服務目錄中的一個Invoker 集合。

假設我們有三個服務提供者,那麼服務目錄,RegistryDirectory物件中儲存的Invoker如下:

Dubbo原始碼分析(七)服務目錄

我們再看下這個類的繼承關係:

Dubbo原始碼分析(七)服務目錄

我們看到,RegistryDirectory實現了Directory介面和NotifyListener介面。那麼,我們重點關注兩個實現。

1、獲取Invocation集合

上面我們看到,RegistryDirectory會儲存服務提供者Invocation的集合。那麼,就得有獲取的方法。獲取的方法很簡單,就是從本地快取中查詢即可。

public class RegistryDirectory<T> extends AbstractDirectory<T> implements NotifyListener {
	public List<Invoker<T>> doList(Invocation invocation) {
		List<Invoker<T>> invokers = null;
		
		//方法名和List<Invoker<T>>的對映
		Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap;
		if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
			//獲取請求的方法名,比如insertInfoUser
			String methodName = RpcUtils.getMethodName(invocation);
			//引數列表
			Object[] args = RpcUtils.getArguments(invocation);
			if (args != null && args.length > 0 && args[0] != null
					&& (args[0] instanceof String || args[0].getClass().isEnum())) {
				invokers = localMethodInvokerMap.get(methodName + "." + args[0]);
			}
			if (invokers == null) {
				 // 通過方法名獲取 Invoker 列表
				invokers = localMethodInvokerMap.get(methodName);
			}
			if (invokers == null) {
				// 通過星號 * 獲取 Invoker 列表
				invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
			}
			if (invokers == null) {
				Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
				if (iterator.hasNext()) {
					invokers = iterator.next();
				}
			}
		}
		return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
	}
}
複製程式碼

2、動態更新

服務目錄並非是一成不變的,如果有的新的服務提供者加入,或者剔除已有的服務提供者,那麼服務目錄需要及時更新資訊。所以它實現了NotifyListener介面,用於重新整理Invoker集合。

  • 觸發點

既然是通知,首先我們就要弄清楚它是在哪裡觸發的。以zookeeper為例,在服務引用的時候,它會監聽服務提供者資料節點的資料變化。

public void childChanged(String parentPath, List<String> currentChilds) {
	ZookeeperRegistry.this.notify(url, listener, 
			toUrlsWithEmpty(url, parentPath, currentChilds));
}
複製程式碼

如上程式碼,toUrlsWithEmpty方法會將當前最新的子節點資料轉換成List物件,然後呼叫ZookeeperRegistry.this.notify,此方法最終將呼叫到服務目錄中的重新整理方法。

  • 重新整理 Invoker 列表

refreshInvoker 方法是保證 RegistryDirectory 隨註冊中心變化而變化的關鍵所在。

private void refreshInvoker(List<URL> invokerUrls) {
	
	//如果invokerUrls中只要一個元素,且協議頭為empty,就銷燬所有的服務
	if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
			&& "empty".equals(invokerUrls.get(0).getProtocol())) {
		this.forbidden = true; 
		this.methodInvokerMap = null; 
		//銷燬所有服務引用
		destroyAllInvokers(); 
	} else {
		this.forbidden = false; 
		//原有的urlInvokerMap
		Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; 
		if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
			// 新增快取 url 到 invokerUrls 中
			invokerUrls.addAll(this.cachedInvokerUrls);
		} else {
			this.cachedInvokerUrls = new HashSet<URL>();
			// 快取 invokerUrls
			this.cachedInvokerUrls.addAll(invokerUrls);
		}
		if (invokerUrls.isEmpty()) {
			return;
		}
		//將invokerUrls轉換為Invoker
		//這裡會根據url中的協議名稱呼叫對應的Protocol實現
		Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);
		//將 newUrlInvokerMap 轉成方法名到 Invoker 列表的對映
		Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); 
		if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
			logger.error(new IllegalStateException("......");
			return;
		}
		this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : 
													newMethodInvokerMap;
		this.urlInvokerMap = newUrlInvokerMap;
		try {
			//銷燬無用的服務引用Invoker
			destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap);
		} catch (Exception e) {
			logger.warn("destroyUnusedInvokers error. ", e);
		}
	}
}
複製程式碼

如上程式碼,我們分為這樣幾個步驟來理解。

  • 從註冊中心中,獲取當前最新的invokerUrls
  • 呼叫toInvokers(invokerUrls)方法。此方法根據URL中的協議名稱,呼叫對應的Protocol實現,來引用服務。比如DubboProtocol.refer ,它返回已經構建好的Invoker物件集合。
  • 呼叫toMethodInvokers(newUrlInvokerMap)方法,根據最新的InvokerMap,重置方法名和Invoker物件的對映關係。
  • 更新methodInvokerMap快取。

我們剛剛看到,上面獲取Invocation集合的時候,就是從methodInvokerMap物件中獲取資料。那麼在這裡改變這個物件的屬性,就相當於更新了服務目錄的Invoker資訊。

  • 銷燬

在更新完服務目錄Invoker之後,還要銷燬無用的服務引用Invoker。

private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, 
					Map<String, Invoker<T>> newUrlInvokerMap) {
					
	//如果新的Invoker為空,則銷燬全部
	if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
		destroyAllInvokers();
		return;
	}
	
	List<String> deleted = null;
	if (oldUrlInvokerMap != null) {
		Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values();
		//遍歷舊的Invoker
		for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) {
			//判斷新的newInvokers是否包含舊的Invoker物件
			if (!newInvokers.contains(entry.getValue())) {
				if (deleted == null) {
					deleted = new ArrayList<String>();
				}
				// 若不包含,則將舊的 Invoker 對應的 url 存入 deleted 列表中
				deleted.add(entry.getKey());
			}
		}
	}
	if (deleted != null) {
		for (String url : deleted) {
			if (url != null) {
				// 從 oldUrlInvokerMap 中移除 url 對應的 Invoker
				Invoker<T> invoker = oldUrlInvokerMap.remove(url);
				if (invoker != null) {
					try {
						//呼叫銷燬方法
						invoker.destroy();
						if (logger.isDebugEnabled()) {
							logger.debug("destory invoker[" + invoker.getUrl() + "] success. ");
						}
					} 
				}
			}
		}
	}
}
複製程式碼

以上程式碼看起來比較長,其實也很簡單。就是通過對比兩個InvokerMap,如果新的InvokerMap中不包含舊的InvokerMap中的節點,那麼這個Invoker就是要被銷燬的。

我們在上一章節分析服務引用的時候,我們提到了可以對服務引用進行監聽。那麼這裡,就是觸發服務銷燬監聽方法的地方。

public class ListenerInvokerWrapper<T> implements Invoker<T> {

	public void destroy() {
		try {
			invoker.destroy();
		} finally {	
			//自定義監聽器
			if (listeners != null && !listeners.isEmpty()) {
				for (InvokerListener listener : listeners) {
					if (listener != null) {
						try {
							//呼叫監聽器方法
							listener.destroyed(invoker);
						} catch (Throwable t) {
							logger.error(t.getMessage(), t);
						}
					}
				}
			}
		}
	}
}
複製程式碼

服務目錄是叢集容錯和負載均衡機制的基礎部分,為什麼這樣說呢?

當有多個服務提供者的時候:

我們怎麼選取其中的一個服務去呼叫,這是負載均衡機制 當呼叫服務失敗後,我們怎麼處理當前的請求?丟擲異常亦或是重試?這是叢集容錯機制

有了服務目錄,我們才能獲取所有的服務提供者列表,並感知註冊中心的資料變化,及時更新目錄中的Invoker物件資訊。

相關文章