优秀的编程知识分享平台

网站首页 > 技术文章 正文

从Dubbo源码分析RpcException:Forbid consumer

nanyue 2024-07-25 05:54:15 技术文章 13 ℃

跑的好好的服务突然报异常: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贴出来【注释掉公司敏感信息】:

根据Stack Trace,该RpcException是由dubbo源码中类RegistryDirectory的doList方法抛出的,找到RegistryDirectory.java的第579行doList()方法,少废话,直接上代码:

public List<Invoker<T>> 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<Invoker<T>>(0) : invokers;
 }

What? 当forbidden值为true时,抛出该异常?变量forbidden是个什么鬼东西?

/**
* RegistryDirectory
*
* @author william.liangf
* @author chao.liuc
*/public class RegistryDirectory<T> extends AbstractDirectory<T> 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都在哪里用了:

次奥,让类RegistryDirectory的refreshInvoker(List<URL>)方法给改成了true?来来来,上代码,贴证据:

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; // 禁止访问
 this.methodInvokerMap = null; // 置空列表
 destroyAllInvokers(); // 关闭所有Invoker
 } else { this.forbidden = false; // 允许访问
 // 此处删除代码无数行 -- 不关心,就是这么任性!
 }

TMD,来查下是谁暗地里“指使”你改的“forbidden=true”,谁动了我的代码,一个都别跑。

还是RegistryDirectory这个类,有一个叫notify(List<URL>)的方法:这又是个什么鬼?看最后一句代码,确实是它动了,证据确凿!

public synchronized void notify(List<URL> urls) {
 List<URL> invokerUrls = new ArrayList<URL>();
 List<URL> routerUrls = new ArrayList<URL>();
 List<URL> configuratorUrls = 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)
 || 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<Router> routers = toRouters(routerUrls); if(routers != null){ // null - do nothing
 setRouters(routers);
 }
 }
 List<Configurator> 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<URL>)是个什么鬼东西,为何改我的代码?

public class RegistryDirectory<T> extends AbstractDirectory<T> 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 { /**
 * 当收到服务变更通知时触发。
 *
 * 通知需处理契约:<br>
 * 1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。<br>
 * 2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。<br>
 * 3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routers, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。<br>
 * 4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。<br>
 * 5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。<br>
 *
 * @param urls 已注册信息列表,总不为空,含义同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
 */
 void notify(List<URL> urls);
}

这次都是你爹惹的祸,自己不做,指使你去notify。先看下你爹的“供词”

“当收到服务变更通知时触发”,看来背后还有大BOSS ?

根据“证词”,得知部分真相如下:

“Zookeeper收到服务变更通知时,都会触发对notify方法的调用”,TMD叼爆了,大BOSS让干啥,你就干啥,自己不亲自干,还让你儿子去干!先别动大BOSS,先来看看你儿子这傻叉都干了些啥?

先把证据摆出来:

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; // 禁止访问
 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<String>)方法把protocol设置成了empty!证据确凿,找到物证如下:

 private List<URL> toUrlsWithEmpty(URL consumer, String path, List<String> providers) { List<URL> 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<URL> toUrlsWithoutEmpty(URL consumer, List<String> providers) { 	List<URL> urls = new ArrayList<URL>(); 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<String> 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服务的版本?不说了,说多了都是马赛克

最近发表
标签列表