從Dubbo源碼分析RpcException:Forbid consumer

跑的好好的服務突然報異常:com.alibaba.dubbo.RpcException: Forbid consumer ${ip} access service ... use dubbo version 2.5.3, Please check registry access list (whitelist/blacklist),是否有點莫名其妙?TMD,還是先追查問題的ROOT CAUSE,再罵街。。。

來來來,按國際慣例,先把Stack Trace貼出來【註釋掉公司敏感信息】:

從Dubbo源碼分析RpcException:Forbid consumer

根據Stack Trace,該RpcException是由dubbo源碼中類RegistryDirectory的doList方法拋出的,找到RegistryDirectory.java的第579行doList()方法,少廢話,直接上代碼:

public List> doList(Invocation invocation) { if (forbidden) { throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " + NetUtils.getLocalHost() + " access service " + getInterface().getName() + " from registry " + getUrl().getAddress() + " use dubbo version " + Version.getVersion() + ", Please check registry access list (whitelist/blacklist)."); }  //此處省略無關緊要的N行 return invokers == null ? new ArrayList>(0) : invokers; }

What? 當forbidden值為true時,拋出該異常?變量forbidden是個什麼鬼東西?

/*** RegistryDirectory** @author william.liangf* @author chao.liuc*/public class RegistryDirectory extends AbstractDirectory implements NotifyListener { private static final Logger logger = LoggerFactory.getLogger(RegistryDirectory.class);  // 此處刪除無關緊要的N行 private volatile boolean forbidden = false; // 後面刪除無關緊要的N行}

從類RegistryDirectory中發現改值默認初始化為false? 拿尼?誰TMD把老子的forbidden值給改成了true?

Find Usage, 來找一下forbidden都在哪裡用了:

從Dubbo源碼分析RpcException:Forbid consumer

次奧,讓類RegistryDirectory的refreshInvoker(List)方法給改成了true?來來來,上代碼,貼證據:

private void refreshInvoker(List invokerUrls){ if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { this.forbidden = true; // 禁止訪問 this.methodInvokerMap = null; // 置空列表 destroyAllInvokers(); // 關閉所有Invoker } else { this.forbidden = false; // 允許訪問 // 此處刪除代碼無數行 -- 不關心,就是這麼任性! }

TMD,來查下是誰暗地裡“指使”你改的“forbidden=true”,誰動了我的代碼,一個都別跑。

從Dubbo源碼分析RpcException:Forbid consumer

還是RegistryDirectory這個類,有一個叫notify(List

)的方法:這又是個什麼鬼?看最後一句代碼,確實是它動了,證據確鑿!

public synchronized void notify(List urls) { List invokerUrls = new ArrayList(); List routerUrls = new ArrayList(); List configuratorUrls = new ArrayList(); for (URL url : urls) { String protocol = url.getProtocol(); String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY); if (Constants.ROUTERS_CATEGORY.equals(category) || Constants.ROUTE_PROTOCOL.equals(protocol)) { routerUrls.add(url); } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) || Constants.OVERRIDE_PROTOCOL.equals(protocol)) { configuratorUrls.add(url); } else if (Constants.PROVIDERS_CATEGORY.equals(category)) { invokerUrls.add(url); } else { logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost()); } } // configurators if (configuratorUrls != null && configuratorUrls.size() >0 ){ this.configurators = toConfigurators(configuratorUrls); } // routers if (routerUrls != null && routerUrls.size() >0 ){ List routers = toRouters(routerUrls); if(routers != null){ // null - do nothing setRouters(routers); } } List localConfigurators = this.configurators; // local reference // 合併override參數 this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && localConfigurators.size() > 0) { for (Configurator configurator : localConfigurators) { this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } // providers refreshInvoker(invokerUrls); }

這個notify(List)是個什麼鬼東西,為何改我的代碼?

public class RegistryDirectory extends AbstractDirectory implements NotifyListener { // 刪除代碼無數行}

TMD又是RegistryDirectory ?不對,這次不是你,是你爹NotifyListener。

package com.alibaba.dubbo.registry;import java.util.List;import com.alibaba.dubbo.common.URL;/*** NotifyListener. (API, Prototype, ThreadSafe)** @see com.alibaba.dubbo.registry.RegistryService#subscribe(URL, NotifyListener)* @author william.liangf*/public interface NotifyListener { /** * 當收到服務變更通知時觸發。 * * 通知需處理契約:
* 1. 總是以服務接口和數據類型為維度全量通知,即不會通知一個服務的同類型的部分數據,用戶不需要對比上一次通知結果。

* 2. 訂閱時的第一次通知,必須是一個服務的所有類型數據的全量通知。
* 3. 中途變更時,允許不同類型的數據分開通知,比如:providers, consumers, routers, overrides,允許只通知其中一種類型,但該類型的數據必須是全量的,不是增量的。
* 4. 如果一種類型的數據為空,需通知一個empty協議並帶category參數的標識性URL數據。
* 5. 通知者(即註冊中心實現)需保證通知的順序,比如:單線程推送,隊列串行化,帶版本對比。
* * @param urls 已註冊信息列表,總不為空,含義同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。 */ void notify(List urls);}

這次都是你爹惹的禍,自己不做,指使你去notify。先看下你爹的“供詞”

“當收到服務變更通知時觸發”,看來背後還有大BOSS ?

根據“證詞”,得知部分真相如下:

“Zookeeper收到服務變更通知時,都會觸發對notify方法的調用”,TMD叼爆了,大BOSS讓幹啥,你就幹啥,自己不親自幹,還讓你兒子去幹!先別動大BOSS,先來看看你兒子這傻叉都幹了些啥?

先把證據擺出來:

private void refreshInvoker(List invokerUrls){ if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { this.forbidden = true; // 禁止訪問 this.methodInvokerMap = null; // 置空列表 destroyAllInvokers(); // 關閉所有Invoker } else { this.forbidden = false; // 允許訪問 // 後面統統刪除。。。}

看下你改forbidden=true都收了什麼好處?

好處1: invokerUrls != null

好處2: invokerUrls.size() == 1

好處3: invokerUrls.get(0) != null

好處4: Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())

什麼??我眼睛沒瞎?你竟然同時拿了四個好處?我滴乖乖。。

分析下你拿的四個好處,一時竟然不知如何斷案!!!先不關你invokerUrls到底是誰,看下好處4:

Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol()) 協議是空(empty)? 一個zookeeper推送服務註冊信息變更,就把協議改成了empty ?

繼續Find Usage:

有了新的線索:類ZookeeperRegistry的toUrlsWithEmpty(URL, String, List)方法把protocol設置成了empty!證據確鑿,找到物證如下:

 private List toUrlsWithEmpty(URL consumer, String path, List providers) { List urls = toUrlsWithoutEmpty(consumer, providers); if (urls == null || urls.isEmpty()) { int i = path.lastIndexOf('/'); String category = i < 0 ? path : path.substring(i + 1); URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category); urls.add(empty); } return urls; }

什麼?urls==null 或者 urls.isEmpty(),你就把protocol設置成empty?這是為啥?繼續追蹤urls...

 private List toUrlsWithoutEmpty(URL consumer, List providers) { List urls = new ArrayList(); if (providers != null && providers.size() > 0) { for (String provider : providers) { provider = URL.decode(provider); if (provider.contains("://")) { URL url = URL.valueOf(provider); if (UrlUtils.isMatch(consumer, url)) { urls.add(url); } } } } return urls; }

來來來,分析下案情:toUrlsWithoutEmpty的結果是空或者size為0,則強制返回一個protocol為empty的url,看來源頭就在這裡了。傳入的List providers實際上就是最新的服務提供者信息,當某個服務沒有任何provider時,providers就變為一個size為0的List了,導致返回一個協議頭為empty的url,進而導致forbidden為true,屏蔽了consumer調用。

來看下細節:UrlUtils.isMatch(consumer, url)?匹配(true)怎樣?不匹配(false)又怎樣?

 public static boolean isMatch(URL consumerUrl, URL providerUrl) { String consumerInterface = consumerUrl.getServiceInterface(); String providerInterface = providerUrl.getServiceInterface(); if( ! (Constants.ANY_VALUE.equals(consumerInterface) || StringUtils.isEquals(consumerInterface, providerInterface)) ) return false;  if (! isMatchCategory(providerUrl.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY), consumerUrl.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY))) { return false; } if (! providerUrl.getParameter(Constants.ENABLED_KEY, true) && ! Constants.ANY_VALUE.equals(consumerUrl.getParameter(Constants.ENABLED_KEY))) { return false; }  String consumerGroup = consumerUrl.getParameter(Constants.GROUP_KEY); String consumerVersion = consumerUrl.getParameter(Constants.VERSION_KEY); String consumerClassifier = consumerUrl.getParameter(Constants.CLASSIFIER_KEY, Constants.ANY_VALUE);  String providerGroup = providerUrl.getParameter(Constants.GROUP_KEY); String providerVersion = providerUrl.getParameter(Constants.VERSION_KEY); String providerClassifier = providerUrl.getParameter(Constants.CLASSIFIER_KEY, Constants.ANY_VALUE); return (Constants.ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup)) && (Constants.ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion)) && (consumerClassifier == null || Constants.ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier)); }

什麼情況是匹配?return true.

什麼情況不匹配?return false.

return (Constants.ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup))

&& (Constants.ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion))

&& (consumerClassifier == null || Constants.ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier));

什麼?消費者和生產者的group(分組), version(版本),classifier(路由?)必須一致?否則return false, 並將urls就是null, 同時

URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category);

然後就發生了最初的慘劇:this.forbidden=true.

if (forbidden) { throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " + NetUtils.getLocalHost() + " access service " + getInterface().getName() + " from registry " + getUrl().getAddress() + " use dubbo version " + Version.getVersion() + ", Please check registry access list (whitelist/blacklist).");}

分析完案情,來看看現場的情況如何?

【以上敏感信息打了馬賽克】consumerVersion="1.0.0", 然而providerVersion="1.0.1", Amazing....與案情分析完全一致。

So, What the fuck ? 到底是誰升級了服務器dubbo服務的版本?不說了,說多了都是馬賽克

從Dubbo源碼分析RpcException:Forbid consumer


分享到:


相關文章: