啟動Dubbo專案註冊Zookeeper時提示zookeeper not connected異常原理解析

朱季謙發表於2021-10-27

文/朱季謙

遇到一個很詭異的問題,我在啟動多個配置相同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主要包含以下資訊——
image
第一步、從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異常問題,就是連線超時設定太短了!
image
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叢集的時間超過了五秒。

相關文章