介紹
在集群容錯時,dubbo處理的流程如下圖所示:
下面是來自官網的介紹,各節點關系如下:
- 這里的
Invoker
是Provider
的一個可調用Service
的抽象,Invoker
封裝了Provider
地址及Service
接口信息 -
Directory
代表多個Invoker
,可以把它看成List<Invoker>
,但與List
不同的是,它的值可能是動態變化的,比如注冊中心推送變更 -
Cluster
將Directory
中的多個Invoker
偽裝成一個Invoker
,對上層透明,偽裝過程包含了容錯邏輯,調用失敗后,重試另一個 -
Router
負責從多個Invoker
中按路由規則選出子集,比如讀寫分離,應用隔離等 -
LoadBalance
負責從多個Invoker
中選出具體的一個用于本次調用,選的過程包含了負載均衡算法,調用失敗后,需要重選
本篇將介紹這里的 Directory
。
Directory的定義
看下 Directory
的類圖:
這里有兩種 Directory
:RegistryDirectory
和 StaticDirectory
。看名字就知道是什么意思了,第一種是通過注冊中心獲取,它的值可能是動態變化的;第二種是不會動態變化的,可以查看其構造方法:
public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
super(url == null && invokers != null && !invokers.isEmpty() ? invokers.get(0).getUrl() : url, routers);
if (invokers == null || invokers.isEmpty())
throw new IllegalArgumentException("invokers == null");
this.invokers = invokers;
}
這里的 invokers
只在構造時被設置了一次。本文主要介紹的是 RegistryDirectory
。
看下 Directory
接口的定義:
public interface Directory<T> extends Node {
/**
* get service type.
*
* @return service type.
*/
Class<T> getInterface();
/**
* list invokers.
*
* @return invokers
*/
List<Invoker<T>> list(Invocation invocation) throws RpcException;
}
主要是 list
方法,該方法會列出所有的服務提供者。
代碼分析
下面從官網給出的消費者的實例來進行分析:
public class Consumer {
public static void main(String[] args) {
//Prevent to get IPV6 address,this way only work in debug mode
//But you can pass use -Djava.net.preferIPv4Stack=true,then it work well whether in debug mode or not
System.setProperty("java.net.preferIPv4Stack", "true");
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-consumer.xml"});
context.start();
DemoService demoService = (DemoService) context.getBean("demoService"); // get remote service proxy
while (true) {
try {
Thread.sleep(1000);
String hello = demoService.sayHello("world"); // call remote method
System.out.println(hello); // get result
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
在調用 sayHello
方法時,會進入 MockClusterInvoker
中的 invoke
方法:
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
if (value.length() == 0 || value.equalsIgnoreCase("false")) {
//no mock
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
if (logger.isWarnEnabled()) {
logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
}
//force:direct mock
result = doMockInvoke(invocation, null);
} else {
//fail-mock
try {
result = this.invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isBiz()) {
throw e;
} else {
if (logger.isWarnEnabled()) {
logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
}
result = doMockInvoke(invocation, e);
}
}
}
return result;
}
MockClusterInvoker
主要是進行本地偽裝,這個在以后將 Cluster
的時候再詳細介紹。
這里的第7行進入 AbstractClusterInvoker
中的 invoke
方法:
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
LoadBalance loadbalance = null;
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && !invokers.isEmpty()) {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
...
protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
List<Invoker<T>> invokers = directory.list(invocation);
return invokers;
}
這里可以看到,在 list
方法中調用了 directory
的 list
方法,這里的 directory
對象就是 RegistryDirectory
類型的對象,這里首先會調用 AbstractDirectory
中的 list
方法,然后再調用子類自己實現的 doList
方法:
AbstractDirectory
中的 list
方法:
@Override
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed) {
throw new RpcException("Directory already destroyed .url: " + getUrl());
}
List<Invoker<T>> invokers = doList(invocation);
List<Router> localRouters = this.routers; // local reference
if (localRouters != null && !localRouters.isEmpty()) {
for (Router router : localRouters) {
try {
if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
} catch (Throwable t) {
logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
}
}
}
return invokers;
}
該方法主要有兩個工作:
- 調用具體的
Directory
列出可用的服務提供者; - 根據路由規則過濾。
這里先不考慮路由,看下 RegistryDirectory
中的 doList
方法:
@Override
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
// 1. No service provider 2. Service providers are disabled
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
"No provider available from registry " + getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " + NetUtils.getLocalHost()
+ " use dubbo version " + Version.getVersion() + ", please check status of providers(disabled, not registered or in blacklist).");
}
List<Invoker<T>> invokers = null;
Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference
if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
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]); // The routing can be enumerated according to the first parameter
}
if (invokers == null) {
invokers = localMethodInvokerMap.get(methodName);
}
if (invokers == null) {
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;
}
這里的核心是 this.methodInvokerMap
,Directory
獲取 invoker
就是根據這個值,那么該值是在哪里被更新的呢?
由于是使用的 zookeeper
注冊中心,看下上面的類圖可以知道,是通過 ZookeeperRegistry
來進行更新的,ZookeeperRegistry
繼承自 FailbackRegistry
,在 FailbackRegistry
中的 subscribe
方法會調用 ZookeeperRegistry
實現的 doSubscribe
方法進行更新:
@Override
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
// Sending a subscription request to the server side
doSubscribe(url, listener);
} catch (Exception e) {
...
}
}
從上面的類圖可以看到,RegistryDirectory
實現了 NotifyListener
接口,該接口定義了一個 notify
方法,doSubscribe
方法執行的最終會調用 RegistryDirectory
中的 notify
方法。
看下 notify
方法的實現,這里簡化了一下代碼:
@Override
public synchronized void notify(List<URL> urls) {
List<URL> invokerUrls = new ArrayList<URL>();
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
if (Constants.ROUTERS_CATEGORY.equals(category)
...
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
}
...
}
...
// providers
refreshInvoker(invokerUrls);
}
refreshInvoker
進行 methodInvokerMap
的更新:
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; // Forbid to access
this.methodInvokerMap = null; // Set the method invoker map to null
destroyAllInvokers(); // Close all invokers
} else {
...
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map
...
this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
this.urlInvokerMap = newUrlInvokerMap;
try {
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
} catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}
我們回到 AbstractClusterInvoker
中的 invoke
方法,再來看一下該方法:
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
LoadBalance loadbalance = null;
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && !invokers.isEmpty()) {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
通過上面的分析可知,在 list
方法中,已經經過了 Directory
和 Router
,按照官網給出的集群容錯的圖,可以知道,下一步應該是 LoadBalance
,從代碼中也可以看到通過 SPI
來獲取具體的 LoadBalance
,然后執行具體的調用。
實例分析
下面來具體實踐一下。啟動兩個服務,名字分別為 demo-provider-1
和 demo-provider-2
:
啟動一個消費者,名字為 demo-consumer-2
:
在執行到 invoke
方法時,查看獲取的 list
:
查看消費者的通知信息:
至此, Directory
的分析就結束了,下篇文章會分析一下 Router
的實現。