dubbo泛化引發的生產故障之dubbo隱藏的坑

不曉得儂發表於2022-01-17

dubbo泛化引發的生產故障之dubbo隱藏的坑

上個月公司zk叢集發生了一次故障,然後要求所有專案組自檢有無使用Dubbo程式設計式/泛化呼叫,強制使用@Reference生成Consumer。具體原因是線上某服務訪問量在短時間大量訪問zk並建立了240萬+的節點,導致zk所有節點陸續崩潰導致,多個應用因無法連線到zk報錯。原因是聽說泛化呼叫時候,provider沒啟動,導致每次請求都在zk建立消費節點。

由於是和自己關聯性不大的專案組,瞭解的並不是很清楚,但是想搞明白這個事情,因此就進行了如下實驗:

試驗1:泛化不使用快取

dubbo泛化寫法

public Result<Map> getProductGenericCache(ProductDTO dto) {
    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(org.pangu.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,因此每次請求這個方法,就會在zk建立個消費節點(無論provider是否啟動),請求量大的時候,就會導致zk所有節點陸續崩潰。使用泛化不快取,這個估計稍微看了官方文件都不會出現這個錯誤。引發這次故障的這個應用功能,又不是初次上線,執行了一段時間了,生產有zk節點數監控,不然初次就發現這個問題了。因此基本可以排除對方是沒有使用快取的問題。

試驗2:泛化使用快取

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

    ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//快取,否則每次請求都會建立一個ReferenceConfig,並在zk註冊節點,最終可能導致zk節點過多影響效能
    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(org.pangu.api.ProductService.class); // 弱型別介面名
    //        reference.setVersion("");
    //        reference.setGroup("");
    reference.setGeneric(true); // 宣告為泛化介面
    GenericService svc = referenceCache.get(reference);//cache.get方法中會快取 Reference物件,並且呼叫ReferenceConfig.get方法啟動ReferenceConfig
    Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//實際閘道器中,方法名、引數型別、引數是作為引數傳入
    return Result.success((Map)target);
}

在provider端無論是否啟動,都只會在zk建立一個消費節點

試驗3:設定服務檢查為true,reference.setCheck(true);

排除了前面兩個試驗,又檢視了下dubbo原始碼,泛化使用ReferenceConfig,那麼無論如何都會執行ReferenceConfig.get(),程式碼如下

public synchronized T get() {
    if (destroyed) {
        throw new IllegalStateException("Already destroyed!");
    }
    if (ref == null) {
        init();
    }
    return ref;
}

ref為null,則執行初始化init,那麼ref是怎麼來的呢?是在init操作內由createProxy生成,createProxy程式碼如下:

//com.alibaba.dubbo.config.ReferenceConfig.createProxy(Map<String, String>)
private T createProxy(Map<String, String> map) {
    //前面程式碼忽略
    //使用Protocol建立Invoker,在zk建立consumer節點

    Boolean c = check;
    if (c == null && consumer != null) {
        c = consumer.isCheck();
    }
    if (c == null) {
        c = true; // default true
    }
    if (c && !invoker.isAvailable()) {
        // 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.使用Protocol建立Invoker

2.檢測服務端check=false,使用proxyFactory建立Invoker代理物件,即ref。

3.檢測服務端check=true,如果provider未啟動,則丟擲IllegalStateException異常,自然ref就還是null了,那麼下次訪問,由於ref為null,則繼續執行init->createProxy,在zk建立consumer節點。

那麼是如何檢測服務是否存活呢,即執行RegistryDirectory.isAvailable(),判斷RegistryDirectory.urlInvokerMap是否為空,為空,肯定說明provider不存在。

PS:RegistryDirectory.urlInvokerMap快取的是Invoker集合

問題大體明白了,因此試驗下,設定check=true

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

    ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//快取,否則每次請求都會建立一個ReferenceConfig,並在zk註冊節點,最終可能導致zk節點過多影響效能
    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); // 宣告為泛化介面
    GenericService svc = referenceCache.get(reference);//cache.get方法中會快取 Reference物件,並且呼叫ReferenceConfig.get方法啟動ReferenceConfig
    Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//實際閘道器中,方法名、引數型別、引數是作為引數傳入
    return Result.success((Map)target);
}

驗證1:先啟動provider服務,然後啟動消費端泛化,請求此泛化方法,在zk只註冊了一個consumer節點;停止provider,再請求此泛化方法,發現zk上此節點數量不變化。為什麼呢?provider停止後,請求不再建立zk節點的原因是RegistryConfig的ref已經在啟動時候生成了代理(由於啟動時候provider服務存在,check=true校驗過通過),因此不再建立。

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

image-20210731001045553

那麼這種情況,為什麼會每次請求都在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不同,如下圖

image-20210731003327358

image-20210731003313365

那麼訂閱url上加上這個timestamp是否有些不合理呢?經過檢視官方,在2.7.5版本中已經將訂閱的URL中的timestamp去掉了,只會對一個URL訂閱一次。

下圖是故障時刻,對zk的dump解析,發現當時的ZK 目錄節點數為170W,實際平時也就10w。

image-20210731003725892

dubbo consumer泛化check=true對應用端的影響

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。把帶時間戳的url加入到ReferenceConfig.urls集合。建立1個RegistryDirectory。

2.第二次請求泛化方法,由於ReferenceConfig已經被快取,這次的ReferenceConfig物件就是首次的ReferenceConfig物件,獲取ReferenceConfig的代理物件ref,由於ReferenceConfig的ref為null,因此執行createProxy,執行的是程式碼@1、@2、@4,在zk建立消費節點,但是由於check=true,因此丟擲IllegalStateException異常,最終ReferenceConfig的ref依然為null。此時ReferenceConfig.urls集合是兩個url,那麼遍歷urls,執行refprotocol.refer(interfaceClass, url),就建立了2個RegistryDirectory。

3.第三此請求泛化方法,基本同2,但是此時ReferenceConfig.urls集合是3個url,那麼遍歷urls,執行refprotocol.refer(interfaceClass, url),就建立了3個RegistryDirectory。

依次類推,第n次請求後,總計建立的RegistryDirectory物件1+2+3+....+n,因此dubbo泛化在設定check=true的情況下,不僅最終會導致zk故障,本地應用也會出現oom。

用這個測試下oom問題,學會分析下dump

jmeter配置

image-20210802214740181

image-20210802214751411

image-20210802214832041

具體在pangu-client-parent工程內

效果圖如下

image-20210802214912309

參考 https://cloud.tencent.com/developer/article/1760931

相關文章