環境:dubbo-2.6.5(公司內部版本),註冊中心:zookeeper
問題描述
生產服務有兩個提供者,突然發現目前僅剩下了一個節點,登錄丟失的節點,查看服務是正常的,而且沒有任何error日誌。
問題分析
推斷1:dubbo admin頁面展示問題,服務實際是正常提供的
查看服務的應用日誌,發現服務沒有任何實際的調用,也就是說,消費者已經真的看不到該提供者,故不成立。
查看日誌
查看日誌定位到服務丟失的時間點。發現當天出現zk出現了大量的超時,原因是當天的zk主節點宕機了。
推斷2:dubbo zk上提供者節點數據丟失
查看zk下的提供者節點確實僅剩下了一個提供者節點數據。問題已定位那麼原因是什麼呢?
問題原因
問題是否出現在了dubbo對zk重連恢復數據這塊,開始查源碼。註冊中心源碼ZookeeperRegistry。
- 連接註冊zk:通過zkclient添加zk狀態監聽。並且繼承了FailbackRegistry各種失敗重試。
public class ZookeeperRegistry extends FailbackRegistry {
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
...
// 1. 連接zk
zkClient = zookeeperTransporter.connect(url);
// 2. 添加zk狀態監聽
zkClient.addStateListener(new StateListener() {
@Override
public void stateChanged(int state) {
// 3. 重新連接後恢復動作,將當前的註冊服務於訂閱任務添加至重試列表中等待重試
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
}
- zk客戶端:默認使用CuratorZookeeperClient實現
public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {
public CuratorZookeeperClient(URL url) {
...
client = builder.build();
// dubbo對接zk連接狀態監聽器
client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
@Override
public void stateChanged(CuratorFramework client, ConnectionState state) {
if (state == ConnectionState.LOST) {
CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED);
} else if (state == ConnectionState.CONNECTED) {
CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED);
} else if (state == ConnectionState.RECONNECTED) {
CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED);
}
}
});
client.start();
...
}
}
- 重試任務:註冊重新失敗重連任務FailbackRegistry中的DubboRegistryFailedRetryTimer,默認5秒檢查一次是否需要失敗恢復
public FailbackRegistry(URL url) {
super(url);
this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// Check and connect to the registry
try {
// failedRegistered失敗註冊重試,failedUnregistered失敗註銷重試,failedSubscribed失敗訂閱重試,failedUnsubscribed失敗取消訂閱重試,failedNotified失敗通知重試
retry();
} catch (Throwable t) { // Defensive fault tolerance
logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
}
}
}, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
}
通過三部分的代碼我們可以推斷,如果zk狀態監聽與恢復部分出現問題可能會導致數據丟失問題。於是查看相關的api並且嘗試查看dubbo社區的問題與bug,果然發現了類似問題的修改與原因分析:https://github.com/apache/dubbo/pull/5135
問題的原因已經在代碼的註釋中說明,大體含義:如果ZNode數據已經存在,在會話超時期間,此時我們將重建一個數據節點,這個重複的異常原因可能是由於zk server中老的超時會話依然持有節點導致該節點的delete刪除事件延遲,並且zk server還沒有來得及去執行刪除,可能由這種場景引起。在這個情景下,我們可以本地刪除節點後再創建恢復節點數據。
個人理解是:如果會話斷開連接又重新連接成功。斷開連接發出的刪除節點事件,因爲延遲原因走在了重新連接恢復節點事件的後面。導致重新連接後沒能成功恢復節點。也就是我麼見到的,provider有一個節點服務正常,但是zk註冊中心中的提供者節點數據丟失,導致出現該節點對其他訂閱者不可見的現象
@Override
protected void createEphemeral(String path, String data) {
byte[] dataBytes = data.getBytes(CHARSET);
try {
client.create().withMode(CreateMode.EPHEMERAL).forPath(path, dataBytes);
} catch (NodeExistsException e) {
logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration" +
", this duplication might be caused by a delete delay from the zk server, which means the old expired session" +
" may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, " +
"we can just try to delete and create again.", e);
deletePath(path);
createEphemeral(path, data);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}