Dubbo整合ZooKeeper3.6.x時出現zookeeper not connected


一、開端

  • Dubbo 2.7.12 及其以下版本,均默認使用 CuratorZookeeperClient
Dubbo 2.7.13 開始 ZookeeperTransporter 接口 getExtension 方法根據是否可以加載到 CuratorCache 這個類來判別當前依賴的 Curator 是高版本還是低版本;
package org.apache.dubbo.remoting.zookeeper;
 
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.extension.ExtensionScope;
import org.apache.dubbo.common.extension.SPI;
import org.apache.dubbo.rpc.model.ApplicationModel;
 
@SPI(scope = ExtensionScope.APPLICATION)
public interface ZookeeperTransporter {
   
  String CURATOR_5 = "curator5";
   
  String CURATOR = "curator";
   
  ZookeeperClient connect(URL url);
   
  void destroy();
   
  static ZookeeperTransporter getExtension(ApplicationModel applicationModel) {
    ExtensionLoader
   
   
   
           
             extensionLoader = applicationModel.getExtensionLoader(ZookeeperTransporter.class); boolean isHighVersion = isHighVersionCurator(); if (isHighVersion) { return extensionLoader.getExtension(CURATOR_5); } return extensionLoader.getExtension(CURATOR); }   static boolean isHighVersionCurator() { try { Class.forName("org.apache.curator.framework.recipes.cache.CuratorCache"); return true; } catch (ClassNotFoundException e) { return false; } } } 
           
因此,Dubbo 3.0.x 整合 Curator 5.2.0 & ZooKeeper 3.6.3 時,報錯位置在 Curator5ZookeeperClient.<init>(Curator5ZookeeperClient.java:83)
java.lang.IllegalStateException: java.lang.IllegalStateException: zookeeper not connected
	at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.prepareEnvironment(DefaultApplicationDeployer.java:697) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.startConfigCenter(DefaultApplicationDeployer.java:276) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.initialize(DefaultApplicationDeployer.java:198) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.deploy.DefaultModuleDeployer.prepare(DefaultModuleDeployer.java:467) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.spring.context.DubboConfigApplicationListener.initDubboConfigBeans(DubboConfigApplicationListener.java:68) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.spring.context.DubboConfigApplicationListener.onApplicationEvent(DubboConfigApplicationListener.java:55) ~[dubbo-3.0.5.jar:3.0.5]
	at org.apache.dubbo.config.spring.context.DubboConfigApplicationListener.onApplicationEvent(DubboConfigApplicationListener.java:34) ~[dubbo-3.0.5.jar:3.0.5]
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:131) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.context.support.AbstractApplicationContext.registerListeners(AbstractApplicationContext.java:881) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:580) ~[spring-context-5.3.15.jar:5.3.15]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.3.jar:2.6.3]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) [spring-boot-2.6.3.jar:2.6.3]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:414) [spring-boot-2.6.3.jar:2.6.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) [spring-boot-2.6.3.jar:2.6.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.6.3.jar:2.6.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.6.3.jar:2.6.3]
	at org.coderead.ProviderApplication.main(ProviderApplication.java:11) [classes/:na]
Caused by: java.lang.IllegalStateException: zookeeper not connected
	at org.apache.dubbo.remoting.zookeeper.curator5.Curator5ZookeeperClient.
   
   
   
           
             (Curator5ZookeeperClient.java:86) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.remoting.zookeeper.curator5.Curator5ZookeeperTransporter.createZookeeperClient(Curator5ZookeeperTransporter.java:27) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.remoting.zookeeper.AbstractZookeeperTransporter.connect(AbstractZookeeperTransporter.java:69) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfiguration. 
            
              (ZookeeperDynamicConfiguration.java:67) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfigurationFactory.createDynamicConfiguration(ZookeeperDynamicConfigurationFactory.java:47) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.common.config.configcenter.AbstractDynamicConfigurationFactory.lambda$getDynamicConfiguration$0(AbstractDynamicConfigurationFactory.java:39) ~[dubbo-3.0.5.jar:3.0.5] at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) ~[na:1.8.0_131] at org.apache.dubbo.common.config.configcenter.AbstractDynamicConfigurationFactory.getDynamicConfiguration(AbstractDynamicConfigurationFactory.java:39) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.getDynamicConfiguration(DefaultApplicationDeployer.java:734) ~[dubbo-3.0.5.jar:3.0.5] at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.prepareEnvironment(DefaultApplicationDeployer.java:690) ~[dubbo-3.0.5.jar:3.0.5] ... 19 common frames omitted Caused by: java.lang.IllegalStateException: zookeeper not connected at org.apache.dubbo.remoting.zookeeper.curator5.Curator5ZookeeperClient. 
             
               (Curator5ZookeeperClient.java:83) ~[dubbo-3.0.5.jar:3.0.5] ... 28 common frames omitted 
              
             
           

拋出異常的代碼:

// Curator5ZookeeperClient.java

public Curator5ZookeeperClient(URL url) {
  // ... (省略)
  client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));
  client.start();
  // 這里是一個同步阻塞等待,假如超過了 timeout 的時間,當前ZooKeeper客戶端還是沒有變成“已連接”狀態,當前線程就會被喚醒,繼續向下執行
  boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);
  // 判斷當前客戶端不是“已連接”狀態,主動拋出異常
  if (!connected) {
    throw new IllegalStateException("zookeeper not connected");
  }
  // ... (省略)
}

二、增加超時時長

CuratorZookeeperClient 構造函數和 Curator5ZookeeperClient 的構造函數邏輯類似。

網上有一些解決方案,就是增加超時時長,來避免該 IllegalStateException 異常。比如在 application.properties 中增加配置項:

dubbo.registry.timeout=30000

其他可以設置超時的配置:

三、尋找超時原因

但是,關鍵的關鍵還是得找到超時的原因。

client.start() 是個異步方法,問題就突然陷入了毫無頭緒的境地。

這時候,你需要知道關於 ZooKeeper 源碼的幾個知識點:

  • ClientCnxn 這個類負責管理客戶端的套接字i/o。ZooKeeper 報文的“發送”和“接收”都要經過這個類;
  • ClientCnxn 中包含 SendThreadEventThread,前者負責數據的“發送”,后者負責數據的“接收”;

現在的問題是“連接不上”,因此我們按如下步驟排查:

  1. 使用 ping <ip> 命令排除目標IP或域名 無法訪問到的可能性;
  2. 使用 telnet <ip> <port> 排除端口訪問不通的可能性;
  3. 如果不是前兩者,那就說明TCP通道是通暢的,那就調試一下連接過程的代碼!

我們認准 ClientCnxn.SendThread,找到 startConnect 方法。以下是 ZooKeeper 3.6.3 中的源碼:

另外,給大家看一下 ZooKeeper 3.4.10 中的源碼:

3.1 SaslServerPrincipal.getServerPrincipal

經過測試發現,在 addr.getHostName()ia.getCanonicalHostName() 處分別耗時 10s,共計花費時長 20s

3.2 對比新老ZooKeeper的addr狀態

以下是 ZooKeeper 3.4.10 中的源碼調試時,addr.getHostName() 調用前,addr 的“狀態”:

相對應的,ZooKeeper 3.6.3 中的源碼調試時,addr.getHostName() 調用前,addr 的“狀態”:

3.3 InetSocketAddress.getHostName源碼分析

首先,調用 InetSocketAddress.getHostName

// 當前在類文件 InetSocketAddress.java 中 
public final String getHostName() {
  // 調用InetSocketAddress的內部類InetSocketAddressHolder的getHostName方法
  return holder.getHostName();
}

接着,繼續看 InetSocketAddress.InetSocketAddressHoldergetHostName 方法:

// 當前在類文件 InetSocketAddress.java 中的內部類 InetSocketAddressHolder 中
private String getHostName() {
  // 新老ZooKeeper的addr的此hostname都為null,跳過
  if (hostname != null)
    return hostname;
  // 新老ZooKeeper代碼中,此處的 addr 都是 Inet4Address 實例
  if (addr != null)
    return addr.getHostName();
  return null;
}

當然,不管是 Inet4Address 還是 Inet6Address 都是 InetAddress 的子類,他們都調用的是基類的 getHostName 方法:

// 當前在類文件 InetAddress.java 中
public String getHostName() {
  return getHostName(true);
}

String getHostName(boolean check) {
  // ZooKeeper 3.4.10 代碼在調試時,當前 if 條件判定為 false,跳過
  // ZooKeeper 3.6.3 代碼在調試時,當前 if 條件判定為 true,將調用 InetAddress.getHostFromNameService 方法
  if (holder().getHostName() == null) {
    holder().hostName = InetAddress.getHostFromNameService(this, check);
  }
  return holder().getHostName();
}

如果該地址(InetAddress)是用主機名(hostname)創建的,則會記住並返回該主機名;

否則,將執行DNS反向解析,並根據系統配置的名稱查找服務返回結果。

3.4 創建InetAddress為什么不一樣?

首先,新老ZooKeeper代碼中的 addr 都是由方法 hostProvider.next(1000) 獲取的。

這個方法的作用:就是從 StaticHostProvider 的成員變量 serverAddresses (該成員變量的類型是 InetSocketAddress 列表)中隨機獲取一個地址。

繼續挖掘 serverAddresses 初始化的地方。

ZooKeeper 3.4.10

創建 ZooKeeper 對象時,需要傳入 connectString 參數

經過 ConnectStringParser 處理后得到的 InetSocketAddress 列表,例如:

接着就是 StaticHostProvider 的構造函數的初始化:

public StaticHostProvider(Collection<InetSocketAddress> serverAddresses)
        throws UnknownHostException {
  for (InetSocketAddress address : serverAddresses) {
        InetAddress ia = address.getAddress();
        // 根據前面解析的情況,此時 ia == null,調用 address.getHostName 獲取到 10.47.227.15 作為參數調用 getAllByName
        InetAddress resolvedAddresses[] = InetAddress.getAllByName(
          (ia!=null) ? ia.getHostAddress(): address.getHostName());
        for (InetAddress resolvedAddress : resolvedAddresses) {
            // If hostName is null but the address is not, we can tell that
            // the hostName is an literal IP address. Then we can set the host string as the hostname
            // safely to avoid reverse DNS lookup.
            // As far as i know, the only way to check if the hostName is null is use toString().
            // Both the two implementations of InetAddress are final class, so we can trust the return value of
            // the toString() method.
            if (resolvedAddress.toString().startsWith("/") 
                    && resolvedAddress.getAddress() != null) {
                this.serverAddresses.add(
                        new InetSocketAddress(InetAddress.getByAddress(
                                // 關鍵就這里,使用用戶傳入的 connectString 的中的 host 作為主機名!顯然也不是空的!
                                address.getHostName(), 
                                resolvedAddress.getAddress()), 
                                address.getPort()));
            } else {
                this.serverAddresses.add(new InetSocketAddress(resolvedAddress.getHostAddress(), address.getPort()));
            }  
        }
    }
    
    if (this.serverAddresses.isEmpty()) {
        throw new IllegalArgumentException(
                "A HostProvider may not be empty!");
    }
    Collections.shuffle(this.serverAddresses);
}

getAllByName 的功能是根據 hostName 獲取 IP 地址,源碼如下:

本文中,走到紅框位置,返回了一個 hostname=null,addr不為null 的 Inet4Address 對象。

ZooKeeper 3.6.3

我們再來看看新版本的 ZooKeeper 的構造函數:

public ZooKeeper(
  String connectString,
  int sessionTimeout,
  Watcher watcher,
  boolean canBeReadOnly) throws IOException {
  this(connectString, sessionTimeout, watcher, canBeReadOnly, createDefaultHostProvider(connectString));
}

// default hostprovider
private static HostProvider createDefaultHostProvider(String connectString) {
  // 雖然,寫法上有一些差異,但是 StaticHostProvider 的初始化邏輯和老版本相差無幾。
  // ConnectStringParser的構造函數在解析connectString時增加了對 ipv6 地址解析的支持!
  return new StaticHostProvider(new ConnectStringParser(connectString).getServerAddresses());
}

再來,就是 StaticHostProvider 的構造函數源碼:

*init* 方法中主要是方法參數賦值給成員變量的操作,比較簡單。
private void init(Collection
   
   
   
           
             serverAddresses, long randomnessSeed, Resolver resolver) { this.sourceOfRandomness = new Random(randomnessSeed); this.resolver = resolver; if (serverAddresses.isEmpty()) { throw new IllegalArgumentException("A HostProvider may not be empty!"); } this.serverAddresses = shuffle(serverAddresses); currentIndex = -1; lastIndex = -1; } 
           

Resolver 對象的 getAllByName 方法的調用發生在 hostProvider.next(1000) 調用時。

3.5 總結

回到問題:創建的InetAddress為什么不一樣?
答:

首先,connectString 中的 host:port 格式的字符串被解析后,通過 InetSocketAddress.createUnresolved(host, port) 創建為 InetSocketAddress 對象(這是一個未解析出具體IP地址的地址);

ZooKeeper 3.4.10 ZooKeeper 3.6.3
解析IP地址的發生點 StaticHostProvider 構造函數 調用 StaticHostProvider.next(long) 時
創建解析后的 InetSocketAddress 是否設置了hostName 是,拿 connectString 的 host 作為 hostName

因此 ZooKeeper 3.6.3 是需要 DNS 反向解析的,這就是新版本和老版本之間的區別。

3.6 InetAddress 部分方法說明

方法名 功能 受保護級別
getAllByName 給定主機名,返回其IP地址數組 public
getAddressesFromNameService DNS解析,通過主機名獲取IP地址 private
getHostName 獲取當前IP地址的主機名 public
getHostFromNameService DNS反向解析,通過IP地址獲取主機名 private
getCanonicalHostName 獲取此IP地址的完全限定域名 public

四、建議

  1. 如果沒有IPv6方面的需求,可以考慮繼續使用 ZooKeeper 3.4.10 版本;
  2. 如果一定要用 ZooKeeper 3.6.3 版本,但是用不到 SASL 認證,可以添加JVM參數 -Dzookeeper.sasl.client=false 來禁用 SASL 認證

參考文檔

記一次zookeeper連接慢的問題和解決方法

  • 這篇找到了比較核心的原因

【Zookeeper】zookeeper not connected

  • 這篇只找到了較為表層的原因,給出的解決方案也不好

InetAddress類中的getHostName()方法的坑

  • 這個是通過添加 hosts 的方式,來解決 getHostName 阻塞的問題,感覺不是特別好(如果IP變了還需要新增 hosts 中的條目),但是也是個方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM