Dubbo 泛化呼叫引發的“血案”

語言: CN / TW / HK

1、背景

上個月公司 ZooKeeper 叢集發生了一次故障,要求所有專案組自檢有無使用 Dubbo 程式設計式或者泛化呼叫,強制使用 @Reference 生成 Consumer。

平臺部給出的故障原因:

泛化呼叫時候,provider 沒啟動,導致每次請求都在 ZooKeeper 建立消費節點,導致在短時間大量訪問 ZooKeeper  並建立了240萬+ 的節點,導致 ZooKeeper 所有節點陸續崩潰導致,多個應用因無法連線到 ZooKeeper 報錯。

原因是聽說泛化呼叫時候,provider 沒啟動,導致每次請求都在 ZooKeeper   建立消費節點。

由於並不是自己負責的專案,為了弄清楚背後的原因,通過進行實驗來探究該故障的深層次原因。

2、求證

2.1 泛化不使用快取

測試程式碼如下:

public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
ApplicationConfig application = new ApplicationConfig();
application.setName("dubbo-demo-client-consumer-generic");
// 連線註冊中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");
// 服務消費者預設值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0);


reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setInterface(com.demo.dubbo.api.ProductService.class); // 弱型別介面名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 宣告為泛化介面
GenericService svc = reference.get();
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});
return Result.success((Map)target);
}

由於沒有快取 reference,因此每次請求這個方法,就會在 ZooKeeper   建立個消費節點(無論 provider 是否啟動)。請求量大的時候,就會導致 ZooKeeper   所有節點陸續崩潰。

如果泛化不使用快取,請求量大時會建立大量 ZooKeeper   節點。

2.2 泛化使用快取

測試程式碼如下:

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();


// 使用快取,否則每次請求都會建立一個 ReferenceConfig
// 並在 ZooKeeper 註冊節點,最終可能導致 ZooKeeper 節點過多影響效能
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
ApplicationConfig application = new ApplicationConfig();
application.setName("pangu-client-consumer-generic");
// 連線註冊中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");


// 服務消費者預設值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0);


reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setInterface(com.demo.dubbo.api.ProductService.class); // 弱型別介面名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 宣告為泛化介面
// cache.get方法中會快取 Reference物件,
// 並且呼叫 ReferenceConfig.get 方法啟動 ReferenceConfig
GenericService svc = referenceCache.get(reference);
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});
return Result.success((Map)target);
}

經過測試,如果使用快取,無論 provider 端無論是否啟動,都只會在 ZooKeeper   建立一個消費節點。

2.3 設定服務檢查為 true

設定 check=true,測試程式碼如下:

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache();


// 使用快取,否則每次請求都會建立一個 ReferenceConfig
// 並在 ZooKeeper 註冊節點,最終可能導致 ZooKeeper 節點過多影響效能
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
ApplicationConfig application = new ApplicationConfig();
application.setName("pangu-client-consumer-generic");
// 連線註冊中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");


// 服務消費者預設值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0);


reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setCheck(true);//試驗3,設定檢測服務存活
reference.setInterface(org.pangu.api.ProductService.class); // 弱型別介面名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 宣告為泛化介面
// cache.get方法中會快取 Reference物件,
// 並且呼叫 ReferenceConfig.get 方法啟動 ReferenceConfig
GenericService svc = referenceCache.get(reference);
// 實際閘道器中,方法名、引數型別、引數是作為引數傳入
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});
return Result.success((Map)target);
}

情況一

啟動 provider 服務,然後啟動消費端泛化,請求此泛化方法時,在 ZooKeeper   只註冊了一個 consumer 節點;停止 provider,再請求此泛化方法,發現 ZooKeeper   上此節點數量不變化。

這是為什麼呢?

provider 停止後,請求不再建立  ZooKeeper   節點的原因是 RegistryConfig 的 ref 已經在啟動時候生成了代理(由於啟動時候 provider 服務存在,check=true 校驗過通過),因此不再建立。

情況二

不啟動 provider 服務,直接啟動消費端泛化,請求此泛化方法,發現每請求一次,在 ZooKeeper   就會建立一個消費節點。至此驗證到故障。

那麼這種情況,為什麼會每次請求都在 ZooKeeper   建立消費節點呢?根本原因是什麼?
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);
}

首次請求泛化方法,由於 ReferenceConfig 的 ref 為 null,因此執行 createProxy,執行程式碼 @1、@2、@3,在 ZooKeeper   建立消費節點,但是由於 check=true,因此丟擲 IllegalStateException 異常,最終 ReferenceConfig 的 ref 依然為 null。

第二次請求泛化方法,由於 ReferenceConfig 已經被快取,這次的 ReferenceConfig 物件就是首次的 ReferenceConfig 物件,獲取 ReferenceConfig 的代理物件 ref,由於 ReferenceConfig 的 ref 為 null,因此執行 createProxy,執行程式碼 @1、@2、@4,在 ZooKeeper   建立消費節點,但是由於 check=true,因此丟擲 IllegalStateException 異常,最終 ReferenceConfig 的 ref 依然為 null。

第三次以及後續的請求,都和第二次請求是一樣效果。

為什麼每次在 ZooKeeper   都建立消費節點,只能說明訂閱 URL 不同導致的。如果 UR 相同,在 ZooKeeper   是不會建立的。那麼訂閱 UR 的組成對一個服務來說有哪些不同呢?

檢視 ReferenceConfig.init(),發現訂閱 UR 上有 timestamp,是當前時間戳,這也說明了為什麼每次都去註冊,因為訂閱 UR 不同,如下圖:

訂閱 UR 上加上這個 timestamp 是否有些不合理呢?

檢視官方文件,在 2.7.5 版本中已經將訂閱的 URL 中的 timestamp 去掉了,只會對一個 URL 訂閱一次。

由於使用了泛化呼叫,但啟動者沒有啟動,而且使用了 check 等於 true,每次呼叫都會嘗試去註冊。但在 Dubbo 2.7.5 之前,註冊的 URL 帶了時間戳,導致每請求一次就在 ZooKeeper   上建立一個節點,導致產生大量節點,最終導致  ZooKeeper   崩掉。

- EOF -

看完本文有收穫?請轉發分享給更多人

關注「ImportNew」,提升Java技能

點贊和在看就是最大的支援 :heart: