Zookeeper之Zookeeper底層客戶端架構實現原理(轉載)

Love Lenka發表於2017-07-14

Zookeeper的Client直接與使用者打交道,是我們使用Zookeeper的interface。瞭解ZK Client的結構和工作原理有利於我們合理的使用ZK,並能在使用中更早的發現問題。本文將在研究原始碼的技術上講述ZK Client的工作原理及內部工作機制。

在看完ZK Client的大致架構以後我希望能有一種簡單的方式描述ZK Client的基本結構,想來想去我覺得還是圖片比較能反映情況,於是我畫了這張大致的結構圖:

 

我想既然我畫了這張圖,就讓我們從這張圖開始講起吧。

模組:

我們可以認為ZK的Client由三個主要模組組成:Zookeeper, WatcherManager, ClientCnxn

Zookeeper是ZK Client端的真正介面,使用者可以操作的最主要的類,當使用者建立一個Zookeeper例項以後,幾乎所有的操作都被這個例項包辦了,使用者不用關心怎麼連線到Server,Watcher什麼時候被觸發等等令人傷神的問題。

WatcherManager,顧名思義,它是用來管理Watcher的,Watcher是ZK的一大特色功能,允許多個Client對一個或多個 ZNode進行監控,當ZNode有變化時能夠通知到監控這個ZNode的各個Client。我們把一個ZK Client簡單看成一個Zookeeper例項,那麼這個例項內部的WatcherManager就管理了ZK Client繫結的所有Watcher。

ClientCnxn是管理所有網路IO的模組,所有和ZK Server互動的資訊和資料都經過這個模組,包括給ZK Server傳送Request,從ZK Server接受Response,以及從ZK Server接受Watcher Event。ClientCnxn完全管理了網路,從外部看來網路操作是透明的。

執行緒:

每當我們建立一個Zookeeper例項的時候,會有兩個執行緒被建立:SendThread和EventThread。所以當我們使用ZK Client端的時候應該儘量只建立一個Zookeeper例項並反覆使用。大量的建立銷燬Zookeeper例項不僅會反覆的建立和銷燬執行緒,而且會在 Server端建立大量的Session。

SendThread是真正處理網路IO的執行緒,所有通過網路傳送和接受的資料包都在這個執行緒中處理。這個執行緒的主體是一個while迴圈:

    while (zooKeeper.state.isAlive()) {
        try {
            if (sockKey == null) {
            // don’t re-establish connection if we are closing
                if (closing) {
                    break;
                }
                startConnect();
                lastSend = now;
                lastHeard = now;
            }
            … ….
            selector.select(to);
            Set<SelectionKey> selected;
            synchronized (this) {
                selected = selector.selectedKeys();
            }
            // Everything below and until we get back to the select is
            // non blocking, so time is effectively a constant. That is
            // Why we just have to do this once, here
            now = System.currentTimeMillis();
            for (SelectionKey k : selected) {
                … …
                if (doIO()) {
                    lastHeard = now;
                }
                … …
            }
        }
        catch() {
            … …
        }
    }

複製程式碼

 

這裡用了java的nio功能,當selector偵測到事件發生的時候就會觸發一次迴圈,主要的操作會在doIO()裡面完成:

    boolean doIO() throws InterruptedException, IOException {
        boolean packetReceived = false;
        SocketChannel sock = (SocketChannel) sockKey.channel();
        if (sock == null) {
            throw new IOException(“Socket is null!”);
        }
        if (sockKey.isReadable()) {
            … …
        }
         
        if (sockKey.isWritable()) {
        … …
        }

        if (outgoingQueue.isEmpty()) {
            disableWrite();
        } else {
            enableWrite();
        }
        return packetReceived;
    }

 

這個過程大概是這樣的:

1. 如果有資料可讀,則讀取資料包,如果資料包是先前發出去的Request的Response,那麼這個資料包一定在Pending Queue裡面。將它從Pending Queue裡面移走,並將此資訊新增到Waiting Event Queue 裡面,如果資料包是一個Watcher Event,將此資訊新增到Waiting Event Queue裡面。

2. 如果OutgoingQueue裡面有資料需要傳送,則傳送資料包並把資料包從Outgoing Queue移至Pending Queue,意思是資料我已經發出去了,但還要等待Server端的回覆,所以這個請求現在是Pending 的狀態。

另外一個執行緒EventThread是用來處理Event的。前面提到SendThread從Server收到資料的時候會把一些資訊新增到 Event Thread裡面,比如Finish Event和Watcher Event。EventThread就是專門用來處理這些Event的,收到Finish Event的時候會把相對應的Package置成Finish狀態,這樣等待結果的Client函式就能得以返回。收到Watcher Event的時候會聯絡WatcherManager找到相對應的Watcher,從WatcherManager裡面移除這個Watcher(因為每個 Watcher只會被通知一次) 並回撥Watcher的process函式。所以所有Watcher的process函式是執行在EventThread裡面的。

保持連線:

到目前為止應該已經大概介紹了ZK Client端的大致結構和處理流程。還剩下一個問題就是當網路出問題時ZK Client是如何處理的。其實這個過程並不複雜,大概是執行以下步驟:

1. 網路發生故障,網路操作丟擲的異常被捕獲。

2. 確認網路操作失敗,清除當前與Server相關的網路資源,包括Socket等等。

3. 在Server列表中逐個嘗試連結Server。

這個過程從外界看來是透明的,外界並不會覺察到ZK Client已經悄悄地更換了一個連線的Server。

好了,對於ZK Client的介紹大概就這麼多了,希望這樣的介紹對於大家學習和使用Zookeeper有一些幫助。對於文章中沒有介紹或者沒有說清楚的地方需要進一步檢視原始碼來解決。

相關文章