ZooKeeper是一個相對簡單的分布式協調服務,通過閱讀源碼我們能夠更進一步的清楚分布式的原理。
ZooKeeper 3.4.9
在bin/zkCli.sh中,我們看到client端的真實入口其實是一個org.apache.zookeeper.ZooKeeperMain的Java類
通過源碼走讀,看到在ZooKeeperMain中主要由兩部分構成
構造一個ZooKeeper對象,同ZooKeeperServer進行建立通信連接
通過反射調用jline.ConsoleReader類,對終端輸入進行讀取,然後通過解析單行命令,調用ZooKeeper接口。
如上所述,client端其實是對 zookeeper.jar 的簡單封裝,在構造出一個ZooKeeper對象後,通過解析用戶輸入,調用 ZooKeeper 接口和 Server 進行交互。
剛才我們看到 client 端同 ZooKeeper Server 之間的交互其實是通過 ZooKeeper 對象進行的,接下來我們詳細深入到 ZooKeeper 類中,看看其和服務端的交互邏輯。
在 ZooKeeper的構造方法中,可以看到 ZooKeeper 中使用 Server 的伺服器地址構建了一個 ClientCnxn 類,在這個類中,系統新建了兩個線程
其中,SendThread 負責將ZooKeeper的請求信息封裝成一個Packet,發送給 Server ,並維持同Server的心跳,EventThread負責解析通過通過SendThread得到的Response,之後發送給Watcher::processEvent進行詳細的事件處理。
Client 時序圖
如上圖所示,Client中在終端輸入指令後,會被封裝成一個Request請求,通過submitRequest,進一步被封裝成Packet包,提交給SendThread處理。
SendThread通過doTransport將Packet發送給Server,並通過readResponse獲取結果,解析成一個Event,再將Event加入EventThread的隊列中等待執行。
EventThread通過processEvent消費隊列中的Event事件。
SendThread 的主要作用除了將Packet包發送給Server之外,還負責維持Client和Server之間的心跳,確保 session 存活。
現在讓我們從源碼出發,看看SendThread究竟是如何運行的。
SendThread是一個線程類,因此我們進入其run()方法,看看他的啟動流程。
從上面的代碼中,可以看出SendThread的主要任務如下:
創建同 Server 之間的 socket 連結
判斷連結是否超時
定時發送心跳任務
將ZooKeeper指令發送給Server
ZooKeeper通過獲取ZOOKEEPER_CLIENT_CNXN_SOCKET變量構造了一個ClientCnxnSocket對象,默認情況下是ClientCnxnSocketNIO類。
在ClientCnxnSocketNIO::connect中我們可以看到這裡同Server之間創建了一個socket連結。
在SendThread::run中,可以看到針對連結是否建立分別有readTimeout和connetTimeout 兩種超時時間,一旦發現連結超時,則拋出異常,終止 SendThread。
在沒有超時的情況下,如果判斷距離上次心跳時間超過了1/2個超時時間,會再次發送心跳數據,避免訪問超時。
在時序圖中,我們看到從終端輸入指令後,我們會將其解析成一個Packet 包,等待SendThread進行發送。
以ZooKeeper::create為例
在這裡create指令,被封裝成了一個 CreateRequest,通過submitRequest被轉成了一個Packet包
在submitRequest中,我們進一步看到Request被封裝成一個Packet包,並加入SendThread::outgoingQueue隊列中,等待執行。
Note: 在這裡我們還看到,ZooKeeper方法中所謂的同步方法其實就是在Packet被提交到SendThread之後,陷入一個while循環,等待處理完成後再跳出的過程
在SendThread::run的while循環中,ZooKeeper通過doTransport將存放在outgoingQueue中的Packet包發送給 Server。
在doIO發送socket信息之前,先從socket中獲取返回數據,通過readResonse進行處理。
在readReponse中,通過解析數據,我們可以得到WatchedEvent對象,並將其壓入EventThread的消息隊列,等待分發
在EventThread中通過processEvent對隊列中的事件進行消費,並分發給不同的Watcher
通常在ZooKeeper中,我們會為指定節點添加一個Watcher,用於監聽節點變化情況,以ZooKeeper:exist為例
代碼的大致邏輯和create類似,但是對wathcer做了一層ExistWatchRegistration的包裝,當packet對象完成請求之後,調用register方法,根據不同包裝的WatchRegistration將watch註冊到不同watch列表中,等待回調。
在 ZooKeeper 中一共有三種類型的WatchRegistration,分別對應DataWatchRegistration,ChildWatchRegistration,ExistWatchRegistration。 並在ZKWatchManager類中根據每種類型的WatchRegistration,分別有一張map表負責存放。
當EventThread::processEvent 時,根據event的所屬路徑,從三張map中獲取對應的watch列表進行消息通知及處理。
client 端的源碼分析就到此為止了。
ZooKeeper Client 的源碼很簡單,擁有三個獨立線程分別對命令進行處理,分發和響應操作,在保證各個線程相互獨立的基礎上,儘可能避免了多線程操作中出現鎖的情況。