原文鏈接 Dubbo原理剖析 之 @DubboReference.version設置爲*
1 背景
Dubbo在消費端提供了一個功能,即將消費者的版本號指定爲*,那麼不管服務端的接口版本是啥,都可以調用成功。
2 初步猜測:
dubbo接口定位邏輯:接口(全路徑)+服務分組(group字段)+版本號(version字段)。
Zookeeper 是用樹狀來保存數據的,在 Zookeeper 中,可以利用Dubbo接口(全路徑)作爲父節點,再根據group和version信息寫入子節點。
而 Nacos,在 Nacos 的控制檯中,我們看到可以根據服務名或服務分組來模糊查詢服務列表,那麼在消費者訂閱的時候,就根據這兩個模糊查詢就可以了,查出來的健康提供者都是符合的。
下面就深入一下源碼,看看實際的邏輯是不是類似我們的猜想。
3 源碼剖析
3.1 Zookeeper 作爲註冊中心
3.1.1 準備:
弄一個服務提供者、一個服務消費者。服務提供者對外提供一個dubbo接口,版本有1.0.0和2.0.0;服務消費者引入服務提供者提供的dubbo接口,version設置爲*。
啓動服務提供者、接着啓動消費者,觀察後臺日誌打印:
我們可以看到,當我們將@DubboReference的version設置爲*的時候,他就根據註冊url(帶*)去找有哪些服務提供者,然後返回的urls會有多個,其中包含版本號爲1.0.0和2.0.0的url。
2021-05-01 10:24:08.561 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Subscribe: consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, dubbo version: 2.7.7, current host: 127.0.0.1
2021-05-01 10:24:08.572 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Notify urls for subscribe url consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, urls: [dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=1.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820516&version=1.0.0, dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=2.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820187&version=2.0.0, empty://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=configurators&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, empty://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*], dubbo version: 2.7.7, current host: 127.0.0.1
3.1.2 源碼分析
上面我們看到,version=*可以成功訂閱,並且服務提供者有兩個,分別是version=1.0.0和version=2.0.0。
結果是看得出來了,但是我們還是需要看看Zookeeper是怎麼的判斷邏輯。
3.1.2.1 服務消費者訂閱過程
我們都知道,正常發佈Dubbo的消費者,需要配置ReferenceConfig,然後調用export方法;當然了,我們這裏就不過於深入了,直接從日誌的入口來開始:org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
我們服務消費者的訂閱url:
consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=16215&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619842106726&version=*
第一步:獲取dubbo接口全路徑
url.getServiceInterface() -> com.winfun.service.DubboServiceOne
接着判斷是否等於“*”,明顯不是,跳到else分支
第二步:根據url獲取path
獲取根節點:
toCategoriesPath(url) -> /dubbo/com.winfun.service.DubboServiceOne/providers、/dubbo/com.winfun.service.DubboServiceOne/configurators、/dubbo/com.winfun.service.DubboServiceOne/consumers
第三步:遍歷第二步的path、創建父節點
重點在path=/dubbo/com.winfun.service.DubboServiceOne/providers,其他忽略即可
根據path創建節點(非持久化):zkClient.create(root, false);
給path添加子節點監聽器: zkClient.addChildListener(path, zkListener) 並返回子節點列表
- dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=1.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820516&version=1.0.0
- dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=2.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820187&version=2.0.0
configurators 和 consumers 不存在子節點,所以子節點是根據規則生成的url,前綴爲empty
第四步:對上面獲取到的urls進行監聽
調用org.apache.dubbo.registry.support.FailbackRegistry#notify方法。
最後會去到 org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)
重點:在監聽前,會先匹配根據path查詢的所有子節點中,匹配符合當前消費者的子節點(根據group和version判斷),利用org.apache.dubbo.common.utils.UrlUtils#isMatch判斷。
判斷中最重要的邏輯:
String ANY_VALUE = "*";
String consumerGroup = consumerUrl.getParameter(GROUP_KEY);
String consumerVersion = consumerUrl.getParameter(VERSION_KEY);
String consumerClassifier = consumerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);
String providerGroup = providerUrl.getParameter(GROUP_KEY);
String providerVersion = providerUrl.getParameter(VERSION_KEY);
String providerClassifier = providerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);
return (ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup))
&& (ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion))
&& (consumerClassifier == null || ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier));
上面完全可以體現出:當版本號等於*號,dubbo接口根節點下的服務都會作爲當前消費者的服務提供者。
好了,到這裏,我們可以知道Zookeeper是怎麼爲version=*的消費者訂閱服務的,直接根據接口全路徑名到Zookeeper裏獲取所有子節點,並都可以作爲服務提供者。
其實這裏會有一個擴展點:多個服務提供者,調用的時候是怎麼負載的,其實在@DubboReference中的loadbance屬性中看得出,默認的負載策略是隨機。
/**
* Load balance strategy, legal values include: random, roundrobin, leastactive
* <p>
* see Constants#DEFAULT_LOADBALANCE
*/
String loadbalance() default "";
org.apache.dubbo.common.constants.CommonConstants#DEFAULT_LOADBALANCE="random";
3.1.2.2 服務消費者執行過程
proxy執行入口:
我們可以通過debug模式,進入到Dubbo方法執行的入口:org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke
第一步:初步判斷
如果是 Object類 或者 toString、destory、hashCode等方法,直接執行
第二步:創建RpcInvocation
根據執行方法、參數等信息創建RpcInvocation
獲取serviceKey:-> serviceKey = dubbo-api-path/group:version
RpcInvocation設置TargetServiceUniqueName
第三步:調用invoker的invoke方法
來到org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#invoke
判斷是否設置了 mock 或 force
- 如果是調用org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#doMockInvoke方法
- 否則來到org.apache.dubbo.rpc.cluster.support.wrapper.AbstractCluster.InterceptorInvokerNode#invoke
第四步:AbstractClusterInvoker#invoke
調用AbstractClusterInvoker#list獲取invoker列表,可以看到拿到的就是1.0.0版本和2.0.0版本的服務提供者
接着調用 initLoadBalance 方法來初始化負載均衡策略,從訂閱url裏面獲取loadbalance的值,如果沒有設置,返回默認值“random”
/**
* Init LoadBalance.
* <p>
* if invokers is not empty, init from the first invoke's url and invocation
* if invokes is empty, init a default LoadBalance(RandomLoadBalance)
* </p>
*
* @param invokers invokers
* @param invocation invocation
* @return LoadBalance instance. if not need init, return null.
*/
protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) {
if (CollectionUtils.isNotEmpty(invokers)) {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE));
} else {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
}
}
第五步:根據集羣策略執行方法
由於Dubbo默認的集羣策略是 failover,所以會來到來到:org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoker
首先會從註冊url裏面的retries字段獲取重試次數(如果爲空,默認重試次數爲2),此次取的是默認值,所以最後最大調用次數爲3.
循環retries+1次
- 來到:org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#select 選擇Invoker
- 下一步:org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#doSelect
- 下一步:org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance#select
- 由於默認是random的負載均衡策略,所以最後來到:org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance#doSelect
在RandomLoadBalance#doSelect 中,首先會根據服務提供者的權重判斷,如果權重沒賦值,最後會利用ThreadLocalRandom.current().nextInt(invokers.size())隨機選擇一個invoker。
- 執行invoke方法,返回結果
- 如果有錯誤,記錄着,下次循環打印warn日誌
- 如果超過retries+1次調用失敗,往外拋出RpcException異常
到這裏,我們已經非常清楚Zookeeper 是如何支持消費者將 version設置爲*,並且方法調用時是如何選擇服務提供者。
3.2 Nacos 作爲註冊中心
3.2.1 準備:
弄一個服務提供者、一個服務消費者,這次不再是Zookeeper作爲註冊中心,而是Nacos作爲註冊中心。服務提供者對外提供一個dubbo接口,版本有1.0.0和2.0.0;服務消費者引入服務提供者提供的dubbo接口,version設置爲*。
啓動服務提供者、接着啓動消費者,觀察後臺日誌打印:
3.2.2 源碼分析
3.2.2.1 服務消費者訂閱過程
Nacos 源碼分析也是直接從 NacosRegistry#doSubscribe 入口開始
:
org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener)
消費者的註冊url:
consumer://192.168.3.3/com.winfun.service.DubboServiceOne?application=dubbo-consumer-nacos&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=39203&qos.enable=false&reference.filter=default,dubboLogFilter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620177626113&version=*
第一步、根據url獲取serviceName集合:
org.apache.dubbo.registry.nacos.NacosRegistry#getServiceNames0
1 創建 NacosServiceName:
providers:com.winfun.service.DubboServiceOne:*:
2 接着到:org.apache.dubbo.registry.nacos.NacosRegistry#filterServiceNames(org.apache.dubbo.registry.nacos.NacosServiceName)
根據上面的servicename過濾出所有的serviceName
2.1 先利用NamingProxy查詢:
com.alibaba.nacos.client.naming.net.NamingProxy#getServiceList(int, int, java.lang.String, com.alibaba.nacos.api.selector.AbstractSelector)
利用接口全路徑名+group查詢,沒有帶版本號
2.2 最後到:com.alibaba.nacos.common.http.client.NacosRestTemplate#exchangeForm
http 請求:
url:http://127.0.0.1:8848/nacos/v1/ns/service/list
params:{app=unknown, pageSize=2147483647, groupName=DEFAULT_GROUP, namespaceId=public, pageNo=1}
返回結果:
RestResult{code=200, message='null', data={"doms":["providers:com.winfun.service.DubboServiceOne:1.0.0:","providers:com.winfun.service.DubboServiceOne:2.0.0:"],"count":2}}
明顯包含兩個版本的service
第二步、根據條件過濾合適的 service
public boolean isCompatible(NacosServiceName concreteServiceName) {
if (!concreteServiceName.isConcrete()) { // The argument must be the concrete NacosServiceName
return false;
}
// Not match comparison
if (!StringUtils.isEquals(this.category, concreteServiceName.category)
&& !matchRange(this.category, concreteServiceName.category)) {
return false;
}
if (!StringUtils.isEquals(this.serviceInterface, concreteServiceName.serviceInterface)) {
return false;
}
// wildcard condition
// 重點在這裏
if (isWildcard(this.version)) {
return true;
}
if (isWildcard(this.group)) {
return true;
}
// range condition
if (!StringUtils.isEquals(this.version, concreteServiceName.version)
&& !matchRange(this.version, concreteServiceName.version)) {
return false;
}
if (!StringUtils.isEquals(this.group, concreteServiceName.group) &&
!matchRange(this.group, concreteServiceName.group)) {
return false;
}
return true;
}
private boolean isWildcard(String value) {
return WILDCARD.equals(value);
}
public static final String WILDCARD = "*";
過濾後的 service 有兩個,分別是1.0.0和2.0.0
那麼繼續深一步的訂閱流程:org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.Set<java.lang.String>)
第三步、遍歷serviceNames,根據serviceName+group查詢所有實例列表並且進行實例監聽
List<Instance> instances = new LinkedList<>();
for (String serviceName : serviceNames) {
instances.addAll(namingService.getAllInstances(serviceName
, getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP)));
notifySubscriber(url, listener, instances);
subscribeEventListener(serviceName, url, listener);
}
到這裏,整個訂閱流程已經結束,主要是看version=*如何判斷哪些服務實例可提供服務,再深入的就沒有了。
Nacos 作爲註冊中心,查詢服務實例主要是根據 serviceName(接口全路徑名)和group(分組),這是因爲Nacos的數據結構本身主要的就是服務名+分組名。
3.2.2.2 服務消費者調用過程
這個就不再深入講解了,調用過程和 Zookeeper 上基本一致。
總結&彩蛋
到這裏,我們就可以捋清楚不同的註冊中心是如何支持@DubboReference.version設置爲*的;當然了,Dubbo 除了 Zookeeper、Nacos 可以作爲註冊中心,Redis 也是可以的,至於其中原理估計也類似,主要是看 Dubbo 如何利用 Redis 提供的數據結構做文章。
但是我們可以發現,我們上面的例子,服務提供者的所有對外提供的服務都是帶版本號的;但是其實在我們的項目中,大部分項目是不帶版本號的;那麼這裏會有一個疑問:如果服務提供者只提供了不帶版本號的接口,那麼消費者將版本號設置爲*還可以正常使用麼?
這裏我可以先告訴大家,啓動時沒問題的,根據我們上面的分析,不管是Nacos還是Zooleeper,在訂閱的時候都可以找到不帶版本號的服務:
2021-05-07 11:13:20.702 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Subscribe: consumer://172.26.156.9/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13134&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620357200041&version=*, dubbo version: 2.7.8, current host: 172.26.156.9
2021-05-07 11:13:20.722 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Notify urls for subscribe url consumer://172.26.156.9/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13134&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620357200041&version=*, urls: [dubbo://172.26.156.9:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13117&release=2.7.8&service.filter=default,dubboLogFilter&side=provider×tamp=1620357151537, empty://172.26.156.9/com.winfun.service.DubboServiceOne?application=dubbo-service&category=configurators&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13134&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620357200041&version=*, empty://172.26.156.9/com.winfun.service.DubboServiceOne?application=dubbo-service&category=routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13134&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620357200041&version=*], dubbo version: 2.7.8, current host: 172.26.156.9
但是到了服務調用的時候,就會報錯了:
org.apache.dubbo.remoting.RemotingException: Not found exported service: com.winfun.service.DubboServiceOne:*:20880 in [com.winfun.service.DubboServiceOne:20880], may be version or group mismatch , channel: consumer: /172.26.156.9:55595 --> provider: /172.26.156.9:20880, message:RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[], attachments={input=323, path=com.winfun.service.DubboServiceOne, remote.application=dubbo-service, trace_id=162035690727995149419, dubboApplication=dubbo-service, dubbo=2.0.2, interface=com.winfun.service.DubboServiceOne, version=*, uri=/more/version/hello/v2/winfun}]
至於爲什麼到了服務調用的時候就報錯,我們這裏就不再深入研究了,有機會的話交給下一篇文章~