文/朱季謙
遇到一個很詭異的問題,我在啟動多個配置相同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叢集的時間超過了五秒。