Zookeeper之Zookeeper的Client的分析

Love Lenka發表於2017-07-14

1)幾個重要概念 

  • ZooKeeper:客戶端入口

  • Watcher:客戶端註冊的callback

  • ZooKeeper.SendThread: IO執行緒

  • ZooKeeper.EventThread: 事件處理執行緒,處理各類訊息callback

  • ClientCnxnSocketNIO:繼承自ClientCnxnSocket,專門處理IO

 

2)zookeeper初始化

    • 應用提供watch例項

    • 例項化zookeeper

      • 例項化socket,預設使用ClientCnxnSocketNIO,可透過zookeeper.clientCnxnSocket配置定製

      • 例項化ClientCnxn

      • 例項化SendThread

      • 例項化EventThread

    • 啟動zookeeper

      • 啟動SendThread

        • 連線伺服器(見SendThread.startConnect)

          • 產生真正的socket,見ClientCnxnSocketNIO.createSock

          • 向select註冊一個OP_CONNECT事件並連線伺服器,由於是非阻塞連線,此時有可能並不會立即連上,如果連上就會呼叫SendThread.primeConnection初始化連線來註冊讀寫事件,否則會在接下來的輪詢select獲取連線事件中處理

          • 復位socket的incomingBuffer

 

          • 連線成功後會產生一個connect型的請求發給服務,用於獲取本次連線的sessionid

          • 進入迴圈等待來自應用的請求,如果沒有就根據時間來ping 伺服器

 

    • 啟動EventThread

      • 開始進入無限迴圈,從佇列waitingEvents中獲取事件,如果沒有就阻塞等待

 

3)以一個請求為例以 zk.exists("/root", false)為例

  • 客戶端執行緒

    • 構造一個exists型別的請求,請求型別見ZooDefs.OpCode

    • 將請求構造成一個Packet,並將該packet放入outgoingQueue

      • 喚醒select

    • 阻塞等待結果

  • SendThread

    • 透過select 輪詢判斷是否有socket準備好,如果能讀就讀,能寫就寫

    • 此時socket準備好寫了 ,就從outgoingQueue獲取packet, 將packet傳送到服務端 

    • 一旦傳送了一個完整的packet,就將packet從outgoingQueue移除

    • 最後將packet加入到pendingQueue

    • 再次select輪詢看是否有響應資料,如果有首先都去4個位元組的響應頭(包含響應的長度資訊),然後在下一次遍歷中都去響應體

    • 都到響應將packet從pendingQueue移除

    • 如果該請求packet帶有一個callback,那麼會將此packet放入waitingEvents佇列,讓EventThread去處理

    • 最後會呼叫p.notifyAll()解鎖,於是應用執行緒從阻塞中出來

  • 如果使用了帶callback 的exists,EventThread會幹活

 

4)小結

4.1)

SendThread也並非完全對應與請求/響應模式,SendThread也會接受到節點變化的通知,此時客戶端變成了服務端

 

4.2)時間和超時的控制

ClientCnxnSocket作為ClientCnxnSocketNIO的父類,

有3個關鍵的時間欄位

  • now :每次輪詢select之前更新,或者發生錯誤是在catch段中更新為當前時間

  • lastHeard:在讀取了響應,包括上面提到的connect型請求和常規命令型請求的響應以及完成網路連線時更新為當前時間

  • lastSend:每次傳送完ping 命令和請求以及完成網路連線時更新為當前時間

有下面幾個超時設定

  • sessionTimeout:zookeeper初始化時設定的

  • readTimeout:sessionTimeout * 2 / 3

  • connectTimeout:sessionTimeout / hostProvider.size();  //hostProvider.size()為zookeeper伺服器個數

  • getIdleRecv():now - lastHeard

  • getIdleSend():now - lastSend

  • SessionTimeout的計算

    • 如果沒有完成連線to=connectTimeout - getIdleRecv()

    • 如果完成連線to=readTimeout - getIdleRecv()

    • 如果to<=0  就會丟擲SessionTimeoutException

4.3)什麼時候ping

   計算timeToNextPing = readTimeout / 2-getIdleSend()

  如果timeToNextPing <= 0,傳送ping請求(只是將ping請求放入outgoingQueue,並不發生IO)

   

4.4)select阻塞多久

如果上述的0<timeToNextPing<to,那麼阻塞時長為timeToNextPing,否則為to

如果有寫請求,select會被喚醒

 

4.5)sendThread的工作原理

該執行緒作為zookeeper客戶端的核心部分專門負責IO處理 

  • 計算select timeout(上面提到的to)

  • 檢查空閒時間,有可能丟擲SessionTimeoutException或者傳送ping

  • 使用select輪詢,獲取網路事件(連線、讀、寫)也就是這3類

    • 如果是連線,做連線處理

    • 如果讀,過程如下

      • 讀取訊息頭,4個位元組,頭包含了訊息體的位元組數

      • 讀 取訊息體,分為兩個大類訊息,連線型訊息“connect”和非連線型訊息“header”,前者上面提到過就是連線完成之後發的一種訊息,用於確定 sessionid, 另外前者會呼叫sendThread.onConnected,後者會呼叫sendThread.readResponse

      • 非連線型訊息有分為幾類

        • ping 訊息

        • auth認證訊息 

        • 訂閱的訊息,即各種變化的通知,比如子節點變化、節點內容變化,由伺服器推過來的訊息 ,獲取到這類訊息或透過eventThread.queueEvent將訊息推入事件佇列

        • 客戶端命令的response,如果此訊息帶有callback著透過eventThread.queuePacket推入事件佇列,否者喚醒阻塞的應用執行緒,注意到客戶端命令都會有阻塞版本和非同步版本(帶callback)

    • 如果是寫,就從outgoingQueue獲取packet,寫入網路

4.6)請求中的Watcher和StatCallback的差別

兩個都是callback,兩者都由EventThread,但後者控制呼叫執行緒是否會阻塞等待響應

 

4.7)IO模型

如圖

 

 

  • 沒有使用傳統連線池,會和zookeeper叢集中的一臺相連

  • 單IO執行緒(NIO)+事件執行緒,很標準的NIO模式

相關文章