背景
很久之前我給業務方寫了一個 dubbo loadbalance 的擴充套件(為了敘述方便,這個 loadbalance 擴充套件就叫它 XLB
吧),這兩天業務方反饋說 XLB
不生效了
我心想,不可能啊,都用了大半年了~
排查
於是我登上不生效的 consumer 機器進行排查,還好我留了一手,當 XLB
載入時,會列印一行日誌
看了下這個服務,並沒有列印日誌,說明 XLB
並沒有載入成功
於是,我就去問對應的開發,有按照我的文件配置 loadbalance 嗎?答覆:完全按照文件配置
這下我就有點不相信了,但轉念一想,配置 loadbalance 如此簡單,不應該出錯啊,我的文件和他的應用都在 xml 檔案中配置了 consumer 的 loadbalance
<dubbo:consumer loadbalance="xlb"/>
抱著試一試的態度,拉取了他們專案的程式碼,發現配置確實如上,但我發現他們的 application.properties 配置檔案也配了一個 consumer 的屬性
dubbo.consumer.check=false
以多年和 dubbo 打交道的經驗來說,這裡有問題,又確認了程式碼,確實 xml 和 application.properties 都載入了
那這裡可能就有問題了,dubbo 從 xml 載入生成了一個 consumer 配置,dubbo-springboot-starter 又從 application.properties 載入配置生成了一個 consumer 配置,這不就衝突了?
別看只配置了 dubbo.consumer.check,它實際上會生成一個完整的 consumer 配置,只不過 loadbalance 為預設值
業務方為什麼會這樣配置?大概率是因為我的文件裡只給出了 xml 形式的配置,沒有給 spring-boot 配置,他們原先使用的是 spring-boot 的配置方式,然後看到我的文件是 xml,結果就不會配置了,也寫了個 xml,和原先的配置衝突
驗證
為了驗證是這個問題導致,我把他的 application.properties 的 dubbo.consumer.check 配置挪到了 xml 檔案中,果然重啟後就載入到了 XLB
隨後我又在本地的測試應用上做了這樣一個驗證:
<!-- case 1 -->
<dubbo:consumer />
<dubbo:consumer loadbalance="xlb"/>
<!-- case 2 -->
<dubbo:consumer loadbalance="xlb"/>
<dubbo:consumer />
兩組配置相同,但順序不同,測試結果為 case 1 可以載入到 XLB
,case 2 不行
於是猜測,dubbo consumer 配置以後載入的為準
擼原始碼
顯然猜測不符合我的風格,下面開擼原始碼,不感興趣可以劃過,最下面有總結
首先搞清楚,何時會載入 loadbalance,在 AbstractClusterInvoker
的 invoke
方法中,載入了 loadbalance
@Override
public Result invoke(final Invocation invocation) throws RpcException {
...
List<Invoker<T>> invokers = list(invocation);
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
載入程式碼如下
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);
}
}
帶快取的載入擴充套件
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
可以看出
- loadbalance 是發起 dubbo 呼叫時,且當
invokers
非空時(即 providers 非空)會被初始化,後續都從快取中取 - loadbalance 是根據第一個 invoker 的 loadbalance 引數決定使用哪個 loadbalance 的
於是問題轉移到 invoker 的 loadbalance 從哪來?provider 不會配置 loadbalance,所以這個引數一定是從 consumer 的配置上得到的
順藤摸瓜,在 RegistryDirectory
的 toInvokers
方法中呼叫了 mergeUrl
,它是在註冊中心通知時被呼叫,也就是從註冊中心上拿到 provider url 時,還得 merge 一下才能用,merge 了些什麼內容?
private URL mergeUrl(URL providerUrl) {
// 1. merge consumer 引數
providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap);
// 2. merge configurator 引數
providerUrl = overrideWithConfigurator(providerUrl);
...
return providerUrl;
}
1中 merge 了queryMap 裡的引數,這個queryMap 其實就是 consumer 的引數,它來自配置的 reference
再看 reference 配置,當 ReferenceConfig
初始化時
// 1
public synchronized void init() {
...
checkAndUpdateSubConfigs();
...
AbstractConfig.appendParameters(map, consumer);
...
}
// 2
public void checkAndUpdateSubConfigs() {
...
checkDefault();
...
}
// 3
public void checkDefault() throws IllegalStateException {
if (consumer == null) {
consumer = ApplicationModel.getConfigManager()
.getDefaultConsumer()
.orElse(new ConsumerConfig());
}
}
// 4
public Optional<ConsumerConfig> getDefaultConsumer() {
List<ConsumerConfig> consumerConfigs = getDefaultConfigs(getConfigsMap(getTagName(ConsumerConfig.class)));
if (CollectionUtils.isNotEmpty(consumerConfigs)) {
return Optional.of(consumerConfigs.get(0));
}
return Optional.empty();
}
上面呼叫鏈從 1 到 4
,4
中獲取了第1個 consumer,這就是我們要找的根源
總結
- 每配置一個 consumer ,無論是從 xml 檔案,或是 spring-boot 配置,或是 api 直接建立,都會生成一個 consumerConfig 物件
- 當消費介面,即配置 reference 時,會將 consumer 的引數 merge 過來,如果存在多個 consumer,會挑第一個,當然我們並不知道誰先載入
- 當 reference 存在 consumer 的配置時,註冊中心通知的 provider urls 會和 reference 的引數進行合併,合併後生成可呼叫的 invoker
- 對於 loadbalance 來說,呼叫時,如果 invokers 非空,則會嘗試通過第一個 invoker 的 loadbalance 引數載入負載均衡演算法,第一次呼叫進行載入,後續呼叫則使用快取
搜尋關注微信公眾號"捉蟲大師",後端技術分享,架構設計、效能優化、原始碼閱讀、問題排查、踩坑實踐。