ZooKeeper系列文章:ZooKeeper 源碼和實(shí)踐揭秘(三)


導(dǎo)語
ZooKeeper 是個(gè)針對大型分布式系統(tǒng)的高可用、高性能且具有一致性的開源協(xié)調(diào)服務(wù),被廣泛的使用。對于開發(fā)人員,ZooKeeper 是一個(gè)學(xué)習(xí)和實(shí)踐分布式組件的不錯(cuò)的選擇。本文對 ZooKeeper 的源碼進(jìn)行簡析,也會介紹 ZooKeeper 實(shí)踐經(jīng)驗(yàn),希望能幫助到 ZooKeeper 初學(xué)者?。文章部分內(nèi)容參考了一些網(wǎng)絡(luò)文章,已標(biāo)注在末尾參考文獻(xiàn)中。

ZooKeeper簡介
1. 初衷
2.目標(biāo)讀者
本文是介紹 ZooKeeper 基礎(chǔ)知識和源碼分析的入門級材料,適合用于初步進(jìn)入分布式系統(tǒng)的開發(fā)人員,以及使用 ZooKeeper 進(jìn)行生產(chǎn)經(jīng)營的應(yīng)用程序運(yùn)維人員。
Zookeeper系列文章介紹
第 1 篇:主要介紹 ZooKeeper 使命、地位、基礎(chǔ)的概念和基本組成模塊,以及 ZooKeeper 內(nèi)部運(yùn)行原理,此部分主要從書籍《ZooKeeper 分布式過程協(xié)同技術(shù)詳解》摘錄,對于有 ZooKeeper 基礎(chǔ)的可以略過。堅(jiān)持主要目的,不先陷入解析源碼的繁瑣的實(shí)現(xiàn)上,而是從系統(tǒng)和底層看 ZooKeeper 如何運(yùn)行,通過從高層次介紹其所使用的協(xié)議,以及 ZooKeeper 所采用的在提高性能的同時(shí)還具備容錯(cuò)能力的機(jī)制。

服務(wù)端的線程
客戶端
從整體看,客戶端啟動的入口時(shí) ZooKeeperMain,在 ZooKeeperMain 的 run()中,創(chuàng)建出控制臺輸入對象(jline.console.ConsoleReader),然后它進(jìn)入 while 循環(huán),等待用戶的輸入。同時(shí)也調(diào)用 connectToZK 連接服務(wù)器并建立會話(session),在 connect 時(shí)創(chuàng)建 ZooKeeper 對象,在 ZooKeeper 的構(gòu)造函數(shù)中會創(chuàng)建客戶端使用的 NIO socket,并啟動兩個(gè)工作線程 sendThread 和 eventThread,兩個(gè)線程被初始化為守護(hù)線程。
sendThread 的 run()是一個(gè)無限循環(huán),除非運(yùn)到了 close 的條件,否則他就會一直循環(huán)下去,比如向服務(wù)端發(fā)送心跳,或者向服務(wù)端發(fā)送我們在控制臺輸入的數(shù)據(jù)以及接受服務(wù)端發(fā)送過來的響應(yīng)。

客戶端的場景說明(事務(wù)、非事務(wù)請求類型)。
客戶端源碼解析
ZooKeeperMain 初始化

ZooKeeper 的構(gòu)造函數(shù),cnxn.start()會創(chuàng)建 sendThread 和 eventThread 守護(hù)線程。




在 ClientCnxn.java 中,有兩個(gè)重要的數(shù)據(jù)結(jié)構(gòu)。
/**
* These are the packets that have been sent and are waiting for a response.
*/
private final LinkedList pendingQueue = new LinkedList();
/**
* These are the packets that need to be sent.
*/
private final LinkedBlockingDeque
ZooKeeper 類中的對用戶的輸入?yún)?shù)轉(zhuǎn)換為對 ZK 操作,會調(diào)用 cnxn.submitRequest()提交請求,在 ClientCnxn.java 中會把請求封裝為 Packet 并寫入 outgoingQueue,待 sendThread 線程消費(fèi)發(fā)送給服務(wù)端,對于同步接口,調(diào)用 cnxn.submitRequest()會阻塞,其中客戶端等待是自旋鎖。



ClientCnxnSocketNIO.java 主要是調(diào)用 dcIO(), 其中讀就緒,讀取服務(wù)端發(fā)送過來的數(shù)據(jù),寫就緒, 往客戶端發(fā)送用戶在控制臺輸入的命令。

從上面源碼看,客戶端在 cnxn.submitRequest(),會自旋等待服務(wù)端的結(jié)果,直到 Packet 的 finished 被設(shè)置為 true。ClientCnxnSocketNIO.java 調(diào)用 dcIO(),read 邏輯中,會調(diào)用 sendThread.readResponse(), 在 sendThread.readResponse()函數(shù)中的 finally 中調(diào)用 finshPacket()設(shè)置 finished 為 true,進(jìn)而客戶端阻塞解除,返回結(jié)果。




擴(kuò)展閱讀:
https://www.cnblogs.com/ZhuChangwu/p/11587615.html

服務(wù)端和客戶端結(jié)合部分
會話(Session)
服務(wù)端啟動,客戶端啟動; 客戶端發(fā)起 socket 連接; 服務(wù)端 accept socket 連接,socket 連接建立; 客戶端發(fā)送 ConnectRequest 給 server; server 收到后初始化 ServerCnxn,代表一個(gè)和客戶端的連接,即 session,server 發(fā)送 ConnectResponse 給 client; client 處理 ConnectResponse,session 建立完成。
在 clientCnxn.java 中,run 是一個(gè) while 循環(huán),只要 client 沒有被關(guān)閉會一直循環(huán),每次循環(huán)判斷當(dāng)前 client 是否連接到 server,如果沒有則發(fā)起連接,發(fā)起連接調(diào)用了 startConnect。





在 connect 是,傳遞了如下參數(shù),
lastZxid:上一個(gè)事務(wù)的 id; sessionTimeout:client 端配置的 sessionTimeout; sessId:sessionId,如果之前建立過連接取的是上一次連接的 sessionId sessionPasswd:session 的密碼;

服務(wù)端源碼分析
server 在啟動后,會暴露給客戶端連接的地址和端口以提供服務(wù)。我們先看一下NIOServerCnxnFactory,主要是啟動三個(gè)線程。
AcceptThread:用于接收 client 的連接請求,建立連接后交給 SelectorThread 線程處理
SelectorThread:用于處理讀寫請求
ConnectionExpirerThread:檢查 session 連接是否過期


client 發(fā)起 socket 連接的時(shí)候,server 監(jiān)聽了該端口,接收到 client 的連接請求,然后把建立練級的 SocketChannel 放入隊(duì)列里面,交給 SelectorThread 處理。


session 生成算法

監(jiān)視(Watch)
本小節(jié)主要看看 ZooKeeper 怎么設(shè)置監(jiān)視和監(jiān)控點(diǎn)的通知。ZooKeeper 可以定義不同類型的通知,如監(jiān)控 znode 的數(shù)據(jù)變化,監(jiān)控 znode 子節(jié)點(diǎn)的變化,監(jiān)控 znode 的創(chuàng)建或者刪除。ZooKeeper 的服務(wù)端實(shí)現(xiàn)了監(jiān)視點(diǎn)管理器(watch manager)。
一個(gè) WatchManager 類的實(shí)例負(fù)責(zé)管理當(dāng)前已經(jīng)注冊的監(jiān)視點(diǎn)列表,并負(fù)責(zé)觸發(fā)他們,監(jiān)視點(diǎn)只會存在內(nèi)存且為本地服務(wù)端的概念,所有類型的服務(wù)器都是使用同樣的方式處理監(jiān)控點(diǎn)。
DataTree 類中持有一個(gè)監(jiān)視點(diǎn)管理器來負(fù)責(zé)子節(jié)點(diǎn)監(jiān)控和數(shù)據(jù)的監(jiān)控。
在服務(wù)端觸發(fā)一個(gè)監(jiān)視點(diǎn),最終會傳播到客戶端,負(fù)責(zé)處理傳播的為服務(wù)端的 cnxn 對象(ServerCnxn 類),此對象表示客戶端和服務(wù)端的連接并實(shí)現(xiàn)了 Watcher 接口。Watch.process 方法序列化了監(jiān)視點(diǎn)事件為一定的格式,以便于網(wǎng)絡(luò)傳送。ZooKeeper 客戶端接收序列化的監(jiān)視點(diǎn)事件,并將其反序列化為監(jiān)控點(diǎn)事件的對象,并傳遞給應(yīng)用程序。

客戶端 Watcher 的 process()接口

客戶端 watcher 實(shí)現(xiàn)
創(chuàng)建 WatchRegistration wcb= new DataWatchRegistration(watcher, clientPath),path 和 watch 封裝進(jìn)了一個(gè)對象; 創(chuàng)建一個(gè) request,設(shè)置 type 為 GetData 對應(yīng)的數(shù)值; request.setWatch(watcher != null),setWatch 參數(shù)為一個(gè) bool 值。 調(diào)用 ClientCnxn.submitRequest(...) , 將請求包裝為 Packet,queuePacket()方法的參數(shù)中存在創(chuàng)建的 path+watcher 的封裝類 WatchRegistration,請求會被 sendThread 消費(fèi)發(fā)送到服務(wù)端。

客戶端 GetData()


服務(wù)端 GetData()


服務(wù)端 GetData()

服務(wù)端 addWatch ()
為了測試服務(wù)端監(jiān)視通知客戶端,我們在客戶端本地輸入的命令,
set /path newValue

客戶端 SetData()
從 SetData 的源碼看,本次的 submitRequest 參數(shù)中,WatchRegistration==null,可以推斷,服務(wù)端在 FinalRequestProcessor 中再處理時(shí)取出的 watcher==null,也就不會將 path+watcher 保存進(jìn) maptable 中,其他的處理過程和上面 GetData 類似。
服務(wù)端在滿足觸發(fā)監(jiān)控點(diǎn)時(shí),并通過 cnxn 的 process()方法處理(NIOServerCnxn 類)通知到客戶端。在服務(wù)端處理的 SetData()函數(shù)看,Set 數(shù)值后,會觸發(fā) watch 回調(diào),即 triggerWatch()。

服務(wù)端 SetData()

服務(wù)端 triggerWatch ()

服務(wù)端 NIOServerCnxn 的 process()
從上面看服務(wù)端在往客戶端發(fā)送事務(wù)型消息, 并且 new ReplyHeader(-1, -1L,0)第一個(gè)位置上的參數(shù)是-1。
在客戶端的 SendThread 讀就緒源碼部分(readResponse),在 readResponse 函數(shù)中會判斷 xid==-1 時(shí)然后調(diào)用 eventThread.queueEvent(we ),把響應(yīng)交給 EventThread 處理。

其 eventThread 是一個(gè)守護(hù)線程,run()函數(shù)在 while(true)去消費(fèi) waitingEvents,最終調(diào)用會 watcher.process(pair.event),其中 process 是 watcher 的 process 的接口的實(shí)現(xiàn),從而完成 wacher 回調(diào)處理。

客戶端 eventThread 的 run()

客戶端 processEvent()

客戶端接口 watch process()

ZooKeeper 實(shí)踐經(jīng)驗(yàn)
業(yè)務(wù)的控制面架構(gòu)
ZooKeeper 集群的特點(diǎn)
ZooKeeper 集群規(guī)模,以地域級集群舉例(2020 前)
| 地域 | 集群規(guī)模(設(shè)備數(shù)目) | 備注 |
|---|---|---|
| 上海 | 168 | |
| 廣州 | 95 | |
| 北京 | 41 | |
| 其他 | 4 ~ 12 |
ZooKeeper 集群的 Znode 數(shù)目

ZooKeeper 集群的 Watch 數(shù)目
選擇單臺設(shè)備看

實(shí)踐場景分析和優(yōu)化措施
災(zāi)備集群搭建
在現(xiàn)網(wǎng)運(yùn)營中,出現(xiàn)過半個(gè)小時(shí)以上,服務(wù)不可用的情況,災(zāi)備集群的搭建顯得十分重要。
ZooKeeper 數(shù)據(jù)存儲的一個(gè)優(yōu)點(diǎn)是,數(shù)據(jù)的存儲方式是一樣的,通過事務(wù)日志和快照的合并可以得到正確的數(shù)據(jù)視圖,可以拷貝日志文件和快照文件到另外的新集群。
目前我們切換新舊集群還是人工參與,不過可以大幅度降低服務(wù)不可用的整體時(shí)間。在搭建災(zāi)備集群時(shí),也會遇到環(huán)境,配置,機(jī)型等問題,需要在實(shí)踐中摸索,并能熟練的切換。
Observer 單核高負(fù)載時(shí) Observer 數(shù)據(jù)落地慢
觸發(fā)點(diǎn)
ZK 數(shù)據(jù)有突發(fā)寫入時(shí),子樹數(shù)據(jù)量大。
故障現(xiàn)象
客戶端感知數(shù)據(jù)變化慢,下發(fā)配置不及時(shí),導(dǎo)致用戶業(yè)務(wù)受影響。
故障過程
ZooKeeper 數(shù)據(jù)有突發(fā)寫入時(shí);
客戶端從 Observer 拉取大子樹(children 很多的節(jié)點(diǎn)的 children 列表);
觸發(fā) Observer 發(fā)生單核高負(fù)載,高負(fù)載 CPU 主要處理 getChildren 時(shí)的數(shù)據(jù)序列化去了;
4.客戶端看見從 Observer getChildren 回來的數(shù)據(jù)是很舊的數(shù)據(jù),而此時(shí) ZooKeeper 數(shù)據(jù)早就寫入主集群了;
5.客戶端一次不能看見的數(shù)據(jù)變化特別慢,導(dǎo)致客戶端花了很長時(shí)間才感知并在本地處理完這些突發(fā)寫入。
故障原因分析
寫子樹時(shí),觸發(fā)客戶端的 Children 事件,由于 ZooKeepeer 實(shí)現(xiàn)的機(jī)制不能單獨(dú)通知哪個(gè) Children 節(jié)點(diǎn)變化,客戶端必須自己去 getChildren 獲得全量的 Children 節(jié)點(diǎn)(例如 Children 層機(jī)有 10w 節(jié)點(diǎn),在新增一個(gè)節(jié)點(diǎn),客戶端需要下拉 10w+的數(shù)據(jù)到本地),如果 Children 數(shù)量很大,會極大消耗 Observer 的性能,在 Observer 高負(fù)載后處理不及時(shí),導(dǎo)致下發(fā)配置延時(shí)。
優(yōu)化措施
服務(wù)器 Full GC 導(dǎo)致會話異常
觸發(fā)點(diǎn)
ZooKeeper 的服務(wù)端機(jī)器發(fā)生了 gc,gc 時(shí)間過長,gc 結(jié)束后發(fā)生會話超時(shí)處理。
故障現(xiàn)象
長時(shí)間的 gc 后,會話超時(shí),客戶端再請求服務(wù)器時(shí),遇到異常,客戶端會重啟。服務(wù)端斷開大量的客戶端時(shí),會帶來連接沖擊。
機(jī)房網(wǎng)絡(luò)中斷,大量連接沖擊 Observer
觸發(fā)點(diǎn)
客戶端,Observer,主集群跨區(qū)部署,某區(qū)機(jī)房網(wǎng)絡(luò)短暫中斷。
連接沖擊現(xiàn)象
集群有連接沖擊發(fā)生時(shí),closeSession 事務(wù)導(dǎo)致所有 Observer 無法快速處理新建的連接和其他請求,從而客戶端主動斷連,又出現(xiàn)更多的 closeSession。幾乎無法自行恢復(fù)。

單臺 Observer 臨時(shí)節(jié)點(diǎn)的數(shù)量變化

集群中 Fellower 數(shù)量變化
故障過程
階段 1:網(wǎng)絡(luò)異常,Observer 和主集群的通信中斷,Leader 把 Observer 踢出集群(從上圖的Fellower 的數(shù)量變化可以看出),大量客戶端開始斷連(從上圖的臨時(shí)節(jié)點(diǎn)的數(shù)量變化可以看出);
階段 2: 網(wǎng)絡(luò)恢復(fù)后 Observer 感知到了被踢出,進(jìn)入自恢復(fù)邏輯;
階段 3: Observer 同步完新事務(wù),并進(jìn)入 Serving 狀態(tài);
階段 4: 大量客戶端開始重連 Observer,Observer 沒有限制住連接沖擊導(dǎo)致卡死。
故障原因
在階段 4,觀察分析 Observer 的 pps 不是很高,不過處理事務(wù)非常慢,線程棧發(fā)現(xiàn)有兩個(gè)線程互相卡慢,使得 closeSession 事務(wù)無法在 Observer 上有效執(zhí)行,也使 NIO 連接接入層線程無法處理連接的數(shù)據(jù)接收和數(shù)據(jù)回復(fù)和建立新連接。

優(yōu)化措施
限制或者抑制連接沖擊。在故障時(shí),根據(jù) tcp 狀態(tài)為 established 的連接數(shù)量動態(tài)限制連接,不過 established 的連接數(shù)量其未過閥值,但是觀察到 fd 仍是滿的,大部分連接處于 tcp 的 close-wait 狀態(tài),其中 fd 消耗過多,如果 Observer 落地日志的話,也會造成寫 binlog 或 snapshot 失敗導(dǎo)致進(jìn)程異常退出。
initlimit 和 syncLimit 參數(shù)配置對集群和會話的影響
initLimit 參數(shù)
initLimit 是追隨者最初連接到群首時(shí)的超時(shí)值,單位為 tick 值的倍數(shù)。當(dāng)某個(gè)追隨者最初與群首建立連接時(shí),它們之間會傳輸相當(dāng)多的數(shù)據(jù),尤其是追隨者落后整體很多時(shí)。配置 initLimit 參數(shù)值取決于群首與追隨者之間的網(wǎng)絡(luò)傳輸速度情況,以及傳輸?shù)臄?shù)據(jù)量大小。如果 ZooKeeper 中保存的數(shù)據(jù)量特別大時(shí)或者網(wǎng)絡(luò)非常緩慢時(shí),就需要增大 initLimit。
故障場景:在相同數(shù)據(jù)量的情況下,對于一個(gè)正常運(yùn)行中的 3 節(jié)點(diǎn)主集群,如果一臺 follower 重啟或一臺 observer 想要加入集群:initLimit 過小,會使這臺機(jī)器無法加入主集群。
原因分析
ZooKeeper 的 3.4.4 版本的 observer/follower 啟動時(shí)會讀取一次 snapshot,在選舉邏輯知道 leader 信息后,與 leader quorum 端口(2001、2888)交互前,還會再讀取一次 snapshot。
另外,initLimit 影響 leader 對 observer/follower 的 newLeaderAck(ZooKeeper3.4.4 或 3.4.6 版本),成員加入集群前,成員機(jī)器上會進(jìn)行一次 snapshot 刷出,耗時(shí)如果過長,會使 leader 對 observer 或 follower 的的 newLeaderAck 讀取超時(shí)(tickTime*initLimit)。如果此時(shí)正處理 leader 剛選舉完要給一個(gè) follower 同步數(shù)據(jù)的時(shí)候,還會導(dǎo)致 leader 不能及時(shí)收到足夠數(shù)量的 newLeaderAck 而導(dǎo)致集群組建失敗。
在 ZooKeePeer 的 3.5 版本后,初始化加載 snapshot 只會加載一次,不過需要同步的數(shù)據(jù)量比較大時(shí),initLimit 還是要調(diào)大一些。
syncLimit 參數(shù)
syncLimit 是追隨者與群首進(jìn)行 sync 操作時(shí)的超時(shí)值,單位為 tick 值的倍數(shù)。
追隨者總是會稍微落后于群首,但是因?yàn)榉?wù)器負(fù)載或者網(wǎng)絡(luò)問題,就會導(dǎo)致追隨者落后群首太多,甚至需要放棄該追隨者,如果群首與追隨者無法進(jìn)行 sync 操作,而且超過了 syncLimit 的 tick 時(shí)間,就會放棄該追隨者。
測試追隨者與群首的網(wǎng)絡(luò)情況,進(jìn)行規(guī)劃配置,并實(shí)時(shí)監(jiān)控集群數(shù)據(jù)量的變化。
提高服務(wù)端的性能,網(wǎng)卡性能。

目前,騰訊云微服務(wù)引擎(Tencent Cloud Service Engine,簡稱TSE)已上線,并發(fā)布子產(chǎn)品服務(wù)注冊、配置中心(ZooKeeper/Nacos/Eureka/Apollo)、治理中心(PolarisMesh)。支持一鍵創(chuàng)建、免運(yùn)維、高可用、開源增強(qiáng)的組件托管服務(wù),歡迎點(diǎn)擊文末的「閱讀原文」了解詳情并使用!
TSE官網(wǎng)地址:
https://cloud.tencent.com/product/tse
參考文獻(xiàn)
ZooKeeper-選舉實(shí)現(xiàn)分析: https://juejin.im/post/5cc2af405188252da4250047 Apache ZooKeeper 官網(wǎng): https://zookeeper.apache.org/ ZooKeeper github: https://github.com/apache/zookeeper 《zookeeper-分布式過程協(xié)同技術(shù)詳解》【美】里德,【美】Flavio Junqueira 著 ZooKeeper 源碼分析: https://blog.reactor.top/tags/Zookeeper/ ZooKeeper-選舉實(shí)現(xiàn)分析: https://juejin.im/post/5cc2af405188252da4250047 ZooKeeper 源碼分析: https://www.cnblogs.com/sunshine-2015/tag/zookeeper/
往期
推薦
《騰訊云消息隊(duì)列 TDMQ Pulsar 版商業(yè)化首發(fā)|持續(xù)提供高性能、強(qiáng)一致的消息服務(wù)》
《應(yīng)用多環(huán)境部署的最佳實(shí)踐》
《單元化架構(gòu)在金融行業(yè)的最佳實(shí)踐》
《服務(wù)器又崩了?深度解析高可用架構(gòu)的挑戰(zhàn)和實(shí)踐》
《Kratos技術(shù)系列|從Kratos設(shè)計(jì)看Go微服務(wù)工程實(shí)踐》
《Pulsar技術(shù)系列 - 深度解讀Pulsar Schema》
《Apache Pulsar事務(wù)機(jī)制原理解析|Apache Pulsar 技術(shù)系列》

掃描下方二維碼關(guān)注本公眾號,
了解更多微服務(wù)、消息隊(duì)列的相關(guān)信息!
解鎖超多鵝廠周邊!


點(diǎn)個(gè)在看你最好看
