生产故障|Dubbo泛化调用引发的“血案”(二)
情况一:启动provider服务,然后启动消费端泛化,请求此泛化方法,在zk只注册了一个consumer节点;停止provider,再请求此泛化方法,发现zk上此节点数量不变化。
为什么呢?provider停止后,请求不再创建zk节点的原因是RegistryConfig的ref已经在启动时候生成了代理(由于启动时候provider服务存在,check=true校验过通过),因此不再创建。
情况二:不启动provider服务,直接启动消费端泛化,请求此泛化方法,发现每请求一次,在zk就会创建一个消费节点。至此验证到故障。
那么这种情况,为什么会每次请求都在zk创建消费节点呢?根本原因是什么?
private T createProxy(Map<String, String> map) {
//忽略其它代码
if (isJvmRefer) {
//忽略其它代码
} else {
if (url != null && url.length() > 0) {
//忽略其它代码
} else { // assemble URL from register center's configuration
List<URL> us = loadRegistries(false);//代码@1
if (us != null && !us.isEmpty()) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));//代码@2
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}
if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));//代码@3
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {//代码@4
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
if (registryURL != null) { // registry url is available
// use AvailableCluster only when register's cluster is available
URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME);
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry url
invoker = cluster.join(new StaticDirectory(invokers));
}
}
}
Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {//check=true,provider服务不存在,抛出异常
// make it possible for consumer to retry later if provider is temporarily unavailable
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}
1.首次请求泛化方法,由于ReferenceConfig的ref为null,因此执行createProxy,执行代码@1、@2、@3,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。
2.第二次请求泛化方法,由于ReferenceConfig已经被缓存,这次的ReferenceConfig对象就是首次的ReferenceConfig对象,获取ReferenceConfig的代理对象ref,由于ReferenceConfig的ref为null,因此执行createProxy,执行代码@1、@2、@4,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。
3.第三次,以及后续的请求,都和第二次请求是一样效果。
为什么每次在zk都创建消费节点,只能说明订阅url不同导致的,如果url相同,在zk是不会创建的。那么订阅url的组成对一个服务来说有哪些不同呢?
查看ReferenceConfig.init(),发现订阅url上有timestamp,是当前时间戳,这也说明了为什么每次都去注册,因为订阅url不同,如下图
订阅url上加上这个timestamp是否有些不合理呢?经过查看官方,在2.7.5版本中已经将订阅的URL中的timestamp去掉了,只会对一个URL订阅一次。
3、故障结论
由于使用了泛化调用,但启动者没有启动,而且使用了check等于true,每次调用都会尝试去注册,但在dubbo2.7.5之前,注册的URL带了时间戳,导致每请求一次就在zk上创建一个节点,导致产生大量节点,最终导致zk崩掉。
文章转自公众号:中间件兴趣圈