一、開端
- 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 中包含 SendThread 和 EventThread,前者負責數據的“發送”,后者負責數據的“接收”;
現在的問題是“連接不上”,因此我們按如下步驟排查:
- 使用 ping <ip> 命令排除目標IP或域名 無法訪問到的可能性;
- 使用 telnet <ip> <port> 排除端口訪問不通的可能性;
- 如果不是前兩者,那就說明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.InetSocketAddressHolder 的 getHostName 方法:
// 當前在類文件 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 |
四、建議
- 如果沒有IPv6方面的需求,可以考慮繼續使用 ZooKeeper 3.4.10 版本;
- 如果一定要用 ZooKeeper 3.6.3 版本,但是用不到 SASL 認證,可以添加JVM參數 -Dzookeeper.sasl.client=false 來禁用 SASL 認證
參考文檔
- 這篇找到了比較核心的原因
【Zookeeper】zookeeper not connected
- 這篇只找到了較為表層的原因,給出的解決方案也不好
InetAddress類中的getHostName()方法的坑
- 這個是通過添加 hosts 的方式,來解決 getHostName 阻塞的問題,感覺不是特別好(如果IP變了還需要新增 hosts 中的條目),但是也是個方法。