文/朱季謙
遇到一個很詭異的問題,我在啟動多個配置相同zookeeper的Dubbo項目時,其他項目都是正常啟動,唯獨有一個項目在啟動過程中,Dubbo注冊zookeeper協議時,竟然出現了這樣的異常提示——
Caused by: java.lang.IllegalStateException: zookeeper not connected
at org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient.<init>(CuratorZookeeperClient.java:80)
... 79 common frames omitted
我愣了一下,原以為是zookeeper集群掛了,然后檢查了一下,都正常啊,奇怪的是,其他系統也是正常連接,為啥會有一台出現了這樣的異常呢?
看了一下異常提示,當我深入研究了一下出錯的地方時,才恍然明白出現這個異常究竟是為什么了。
可謂是,在源碼面前,一切都是裸泳。
先來看異常提示出現的類方法CuratorZookeeperClient,這個方法的作用是建立zookeeper客戶端的連接,類似http通信一般,在建立通信前,需要先建立三次握手連接,同理,在zookeeper客戶端創建各類節點前,同樣需要先建立客戶端連接到服務器上——
public CuratorZookeeperClient(URL url) {
super(url);
try {
int timeout = url.getParameter(TIMEOUT_KEY, DEFAULT_CONNECTION_TIMEOUT_MS);
int sessionExpireMs = url.getParameter(ZK_SESSION_EXPIRE_KEY, DEFAULT_SESSION_TIMEOUT_MS);
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
.connectString(url.getBackupAddress())
.retryPolicy(new RetryNTimes(1, 1000))
.connectionTimeoutMs(timeout)
.sessionTimeoutMs(sessionExpireMs);
String authority = url.getAuthority();
if (authority != null && authority.length() > 0) {
builder = builder.authorization("digest", authority.getBytes());
}
client = builder.build();
client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));
client.start();
boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);
if (!connected) {
throw new IllegalStateException("zookeeper not connected");
}
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
根據CuratorZookeeperClient方法可知,出現zookeeper not connected異常提示是發生在這一段代碼當中——
if (!connected) {
throw new IllegalStateException("zookeeper not connected");
}
connected表示連接狀態,當它的值為false時,便會執行這段代碼,那么,究竟是什么情況會導致它的值為false呢?
接下來,讓我們打一個斷點,一步一步解析這段代碼。
首先,用作測試的dubbo和zookeeper配置如下——
dubbo:
application:
name: testervice
registry:
address: zookeeper://120.77.217.245
# timeout: 20000
protocol:
name: dubbo
port: 20880
解析來,開始debug,打斷點,CuratorZookeeperClient方法參數url主要包含以下信息——
第一步、從url中獲取超時時間timeout參數——
int timeout = url.getParameter(TIMEOUT_KEY, DEFAULT_CONNECTION_TIMEOUT_MS);
這里的大概邏輯是,如果yaml配置registry注冊zookeeper部分參數當中含有 timeout話,那么就返回配置當中定義的超時時間,如果yaml沒有進行配置,那么,就用默認的超時時間,默認即常量DEFAULT_CONNECTION_TIMEOUT_MS,值是5 * 1000,也就是5秒,這個參數其實就是本篇文章的核心。
若自定義形式配置該參數,形式如下timeout: 20000——
dubbo:
application:
name: testervice
registry:
address: zookeeper://120.77.217.245
timeout: 20000
第二步、獲取客戶端過期時間——
int sessionExpireMs = url.getParameter(ZK_SESSION_EXPIRE_KEY, DEFAULT_SESSION_TIMEOUT_MS);
同理,無自定義配置話,則使用默認值DEFAULT_SESSION_TIMEOUT_MS = 60 * 1000,即6分鍾;
第三步、創建一個設置過期時間為6分鍾,連接超時為5秒,重試策略為每秒重試一次,連接服務端為url.getBackupAddress()(注:我這里得到的是120.77.217.245:9090,即配置的zookeeper連接url)的CuratorFramework客戶端實例——
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
.connectString(url.getBackupAddress())
.retryPolicy(new RetryNTimes(1, 1000))
.connectionTimeoutMs(timeout)
.sessionTimeoutMs(sessionExpireMs);
client = builder.build();
第四步、添加連接狀態的監控,可以監控操作節點與連接情況——
client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));
第五步、開啟客戶端——
client.start();
最后一步,監控客戶端連接情況,若能連接成功,則證明創建客戶端成功,反之,失敗。可見,若出現zookeeper not connected,問題就在於客戶端連接過程是失敗的,至於為何失敗,原理就在client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS)代碼里。
boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);
if (!connected) {
throw new IllegalStateException("zookeeper not connected");
}
進入到 client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS)源碼里,這里的maxWaitTime即前邊的timeout,默認值是5秒,大概分析一下下邊代碼——
public synchronized boolean blockUntilConnected(int maxWaitTime, TimeUnit units) throws InterruptedException
{
//獲取當前時間
long startTime = System.currentTimeMillis();
//這里是true
boolean hasMaxWait = (units != null);
//maxWaitTimeMs等於5000毫秒,即5秒
long maxWaitTimeMs = hasMaxWait ? TimeUnit.MILLISECONDS.convert(maxWaitTime, units) : 0;
while ( !isConnected() )
{
//hasMaxWait為true
if ( hasMaxWait )
{
//倒數5秒
long waitTime = maxWaitTimeMs - (System.currentTimeMillis() - startTime);
//執行到這里,已經過去5秒話,就執行以下方法,返回isConnected()值
if ( waitTime <= 0 )
{
return isConnected();
}
//還沒到5秒話,假如執行到這里還有3秒,那么就會執行Object.wait(long timeout)方法,即該線程阻塞3秒后再自動喚醒,接着繼續執行
wait(waitTime);
}
else
{
wait();
}
}
return isConnected();
}
該方法的核心會等待maxWaitTime時間,時間一到,就會返回isConnected()值,這里其實很好理解,就是客戶端發起連接后,這里用一個while循環來等待指定的超時時間,默認是5秒,若5秒過了,就返回isConnected()值,而這里的isConnected()就是驗證是否連接成功了,
那么,這里就剩最后一個答案了,isConnected()是什么?
public synchronized boolean isConnected(){
return (currentConnectionState != null) && currentConnectionState.isConnected();
}
這里應該是判斷客戶端連接狀態,即在client.start()方法里,會有一個狀態,若創建連接成功,那么currentConnectionState.isConnected()就能得到true值,這里更像是一個觀察模式,觀察指定的連接超時時間內,是否連接成功。
根據debug,發現未連接成功時,值是null,得到的即為false,當我們把默認為5秒的連接超時設置為timeout: 20000,等待連接過程,發現連接成功了,返回currentConnectionState的值為RECONNECTED。
可見,之前出現zookeeper not connected異常問題,就是連接超時設置太短了!
currentConnectionState.isConnected()得到的是一個枚舉值,RECONNECTED返回的是true——
CONNECTED {
public boolean isConnected() {
return true;
}
},
SUSPENDED {
public boolean isConnected() {
return false;
}
},
RECONNECTED {
public boolean isConnected() {
return true;
}
},
LOST {
public boolean isConnected() {
return false;
}
},
READ_ONLY {
public boolean isConnected() {
return true;
}
};
當返回true話,那么!connected就為false,就不會執行以下異常提示了——
if (!connected) {
throw new IllegalStateException("zookeeper not connected");
}
根據上邊分析,可見啟動Dubbo項目注冊Zookeeper時提示zookeeper not connected異常,是因為沒有在配置里設置連接超時,而是使用了默認的5秒,導致5秒內沒有成功連接,就出現連接異常而無法成功連接,當調長時間后,就正常連接成功了,同時也說明了,這次本地連接zookeeper集群的時間超過了五秒。