Dubbo 配置 Loadbalance 不生效?擼一把源碼
文末本文轉載自微信公眾號「捉蟲大師」,作者捉蟲大師 。轉載本文請聯系捉蟲大師公眾號。
背景
很久之前我給業務方寫了一個 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 初始化時
- 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 參數加載負載均衡算法,第一次調用進行加載,后續調用則使用緩存