ZooKeeper源碼閱讀心得分享+源碼基本結(jié)構(gòu)+源碼環(huán)境搭建
一、心得分享
1、尋找迷宮入口
2、畫流程圖
3、任務(wù)分解
4、思維導圖
一、心得分享
1、尋找迷宮入口
2、畫流程圖
3、任務(wù)分解
4、思維導圖
二、源碼基本結(jié)構(gòu)
1、客戶端源碼
(1)ClientCnxn客戶端連接抽象
(2)SendThread
(3)EventThread
(4)getData非事務(wù)請求
(5)setData事務(wù)請求
2、服務(wù)端源碼
(1)配置解析
(2)恢復內(nèi)存數(shù)據(jù)庫
(3)監(jiān)聽客戶端連接
(4)Leader選舉
(5)數(shù)據(jù)差異化同步
(6)事務(wù)日志和快照日志
(7)事務(wù)請求流程
(8)會話管理
(9)watcher注冊與觸發(fā)
三、源碼環(huán)境搭建
1、IDEA導入源碼
2、本地運行
(1)偽集群搭建準備
(2)運行
一、心得分享
如何閱讀ZooKeeper源碼?從哪里開始閱讀?最近把ZooKeeper源碼看了個大概,有一些心得想和大家分享和探討:
1、尋找迷宮入口
ZooKeeper源碼的脈絡(luò)就像一個迷宮,要想玩這個迷宮游戲,必須找到迷宮的入口。有兩條入口可供選擇:
從服務(wù)端的啟動流程開始看起,可以了解配置文件zoo.cfg解析過程和配置項在源碼中的應(yīng)用,以及Leader選舉流程等。服務(wù)端源碼比較復雜,在了解服務(wù)端啟動和Leader選舉的過程中,又涉及很多其他知識點,包括內(nèi)存數(shù)據(jù)庫DataTree的原理,日志機制(事務(wù)日志和快照日志),數(shù)據(jù)恢復與同步等。最接近核心,也最難,容易勸退或者舉步維艱。
從客戶端向服務(wù)端建立連接開始看起,可以了解客戶端是如何建立連接、發(fā)送請求和處理響應(yīng)等,相對于服務(wù)端,客戶端源碼要簡單很多。從客戶端開始突破,要順利些。
2、畫流程圖
看源碼一定要畫流程圖。源碼走向是錯綜復雜,每個流程、每個走向都畫好流程圖或者時序圖,有助于原理理解。
客戶端源碼只有兩個線程還好說,服務(wù)端源碼有很多線程,直接繞暈。比如請求處理,就分為事務(wù)請求和非事務(wù)請求,事務(wù)請求又需要經(jīng)過兩階段提交,不畫流程圖,根本梳理不清事務(wù)請求是如何在Leader和Learner之間流轉(zhuǎn)的。
3、任務(wù)分解
任務(wù)拆分,化繁為簡,化整為零,是大家都懂的道理,但是如何拆分并不是一件易事。
Zookeeper源碼有很多大知識點,攻克大知識點很花時間,有時候會因為太難,而一拖再拖,舉步維艱。將大知識點拆分為一個個小知識點,一步步攻克。拆分的過程不是一步到位,不要糾結(jié)于如何拆分,而是先拆起來,進行的過程中不斷拆分,不知不覺一個大的,難的知識點就被攻克了。
4、思維導圖
看完源碼,總結(jié)是非常重要的。將一個知識點擴展成一個思維導圖,每一個分支都是最精華的總結(jié),這樣會更加印象深刻。
二、源碼基本結(jié)構(gòu)
ZooKeeper源碼分為客戶端源碼和服務(wù)端源碼。
1、客戶端源碼
客戶端源碼從一行初始化代碼開始:
String connectString = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"; ZooKeeper zooKeeper = new ZooKeeper(connectString, 20000, null);
初始化一個ZooKeeper實例,初始化過程會解析connectString,并隨機挑選一個服務(wù)器地址建立長連接。
ClientCnxn是對客戶端連接的抽象和封裝,負責連接管理和watcher管理。有兩個核心線程:
負責與服務(wù)端建立連接和通信的SendThread線程。
負責處理watcher遠程回調(diào)和本地事件回調(diào)的EventThread線程。
在客戶端實例ZooKeeper初始化時,會初始化并啟動ClientCnxn,啟動ClientCnxn就是啟動SendThread和EventThread兩個線程。
SendThread線程主要負責與服務(wù)端建立長鏈接,后續(xù)的 getData、setData 等操作都通過SendThread線程與服務(wù)端通信。
SendThread的核心知識點有:
向服務(wù)端建立連接的過程
建立會話的過程
心跳機制保證長鏈接存活
讀寫IO處理
負責底層網(wǎng)絡(luò)建立連接和I/O處理的是ClientCnxnSocket ,實現(xiàn)類有 ClientCnxnSocketNIO 和 ClientCnxnSocketNetty。
SendThread接收到服務(wù)端的 watcher 通知后,會交由EventThread線程去觸發(fā)回調(diào)。注冊watcher的功能只有非事務(wù)請求(getData、exists、getChildren)才有,而事務(wù)請求,如getData可以注冊本地事件,事務(wù)請求響應(yīng)成功后會觸發(fā)本地事件回調(diào),這里的回調(diào)流程也是在EventThread線程中。
非事務(wù)請求不僅僅有g(shù)etData,但流程都差不多。
getData可以注冊watcher,但是如何注冊,并且是如何遠程向服務(wù)端注冊?其實注冊 watcher 只是向服務(wù)端發(fā)送一個是否注冊watcher的布爾值,具體注冊什么事件不會在注冊時聲明,而是在觸發(fā)時判斷。
getData構(gòu)建好請求體和響應(yīng)體,并提交給SendThread線程進行底層網(wǎng)絡(luò)的異步發(fā)送,因為是異步,所以有兩種方式接收響應(yīng):
同步阻塞,getData將請求提交給SendThread之后就會阻塞,直到響應(yīng)到來喚醒。
異步回調(diào),getData可以傳入一個本地回調(diào)事件,等響應(yīng)到了就會交由EventThread線程進行回調(diào)。
事務(wù)請求也并非只有setData,還有create、delete。其實事務(wù)請求和非事務(wù)請求在客戶端差別不大,也就是事務(wù)請求不能注冊watcher,請求發(fā)送也是異步,所以響應(yīng)處理也有兩種,同步阻塞和異步回調(diào)。
無論是事務(wù)請求還是非事務(wù)請求,響應(yīng)都是需要按順序處理。
2、服務(wù)端源碼
服務(wù)端源碼較為復雜,突破口在啟動流程上。在服務(wù)端啟動的過程中,涉及到的知識點:
配置文件解析和配置項在源碼中應(yīng)用。
讀取日志文件恢復內(nèi)存數(shù)據(jù)庫。
監(jiān)聽和接收客戶端連接。
Leader選舉。
Leader和Learner之間差異化數(shù)據(jù)同步。
… …
將配置文件zoo.cfg加載為一個java.util.Properties對象,然后解析映射到QuorumPeerConfig對象中,再將QuorumPeerConfig的變量設(shè)置給QuorumPeer對象,QuorumPeer就是ZAB協(xié)議的具體實現(xiàn)類。
在服務(wù)端啟動時,需要通過讀取日志文件恢復內(nèi)存數(shù)據(jù)庫。首先讀取快照日志文件反序列化出一棵DataTree,然后再讀取事務(wù)日志文件修補增量數(shù)據(jù)。這只是初步恢復,等Leader選舉完成以后,服務(wù)節(jié)點之間還需要進行差異化數(shù)據(jù)同步。
在配置文件zoo.cfg中指定的clientPort就是用來監(jiān)聽客戶端連接的??蛻舳诉B接監(jiān)聽是常規(guī)的Reactor響應(yīng)式線程模型。一個AcceptThread線程監(jiān)聽連接事件,多個SelectorThread輪詢封裝注冊連接,具體網(wǎng)絡(luò)IO事件處理交給一個線程池。
AcceptThread線程接收到來自客戶端連接后,輪詢選擇一個SelectorThread來處理連接;每一個客戶端連接在服務(wù)端都被抽象化成一個ServerCnxn對象,默認實現(xiàn)類為NIOServerCnxn,負責底層網(wǎng)絡(luò)IO處理;具體的IO讀寫事件處理抽象成一個IOWorkRequest任務(wù)對象交給線程池workerPool異步處理。
無論是事務(wù)請求還是非事務(wù)請求從底層網(wǎng)絡(luò)讀取完數(shù)據(jù)并構(gòu)建好請求體后,都會提交給一個節(jié)流閥線程RequestThrottler,RequestThrottler控制請求量,并將請求提交給一個包含多個處理器RequestProcessor的職責鏈處理。
在配置文件中,有幾行這樣格式的配置:
server.A=B:C:D
A是一個數(shù)字,表示每個zk實例的myid文件中的編號,即SID。
B是ip地址,每個zk實例所在機器ip。
C是集群中 Leader和 Learner通信的端口。
D是集群中用于Leader選舉同步票據(jù)的端口。
首先創(chuàng)建一個或者一組線程用于監(jiān)聽投票端口,然后創(chuàng)建一個快速選舉Leader算法FastLeaderElection,并啟動兩個線程WorkerSender和WorkerReceiver分別用于選票發(fā)送和選票接收。
在交換選票前,服務(wù)節(jié)點間互相建立連接,為避免連接重復建立,只有SID較大的服務(wù)器才可以主動向其他服務(wù)器發(fā)起建立連接請求。建立連接后,會為每個連接創(chuàng)建兩個線程SendWorker和RecvWorker分別用于網(wǎng)絡(luò)底層的IO事件處理。
FastLeaderElection#lookForLeader是Leader選舉的核心實現(xiàn),包括將選票廣播給所有其他服務(wù),處理其他服務(wù)同步過來的選票,選票PK,最終選出Leader,完成選票。
數(shù)據(jù)差異化同步發(fā)生在Leader選舉完成之后。Learner服務(wù)器(Follower和Observer)需要向Leader服務(wù)器發(fā)起建立連接請求,Leader啟動LearnerCnxAcceptor線程監(jiān)聽Learner的連接請求,每一個建立的連接會被抽象成一個LearnerHandler對象。
Leader檢測到有過半數(shù)的Follower(Observer不參與過半數(shù)決策)建立連接后,就開始校對Learner的數(shù)據(jù)與自己的數(shù)據(jù)有哪些差異:
如果Learner少了數(shù)據(jù),Leader就會發(fā)送缺少的數(shù)據(jù)給Learner;
如果Learner多出數(shù)據(jù),Leader就會讓Learner回滾到指定位置;
實在差異太大,就全量同步。
在服務(wù)器正常運行的過程,查詢數(shù)據(jù)都是直接從內(nèi)存數(shù)據(jù)庫中獲取,所以響應(yīng)速度很快,但是為了服務(wù)重啟后數(shù)據(jù)還在,才有了將數(shù)據(jù)持久化到磁盤日志文件中。
每條事務(wù)請求都會先落地到事務(wù)日志文件,再提交到內(nèi)存數(shù)據(jù)庫中。經(jīng)過一定事務(wù)請求次數(shù),還會將整個內(nèi)存數(shù)據(jù)庫持久化成一個快照日志文件。一個快照日志文件和其后生成的事務(wù)日志文件共同組成全局數(shù)據(jù)。
FileTxnLog是事務(wù)日志文件持久化實現(xiàn)類,主要封裝對磁盤文件的追加、讀取、截斷、滾動等操作。
FileSnap是快照日志文件持久化實現(xiàn)類,主要封裝兩個操作:將DataTree和會話列表序列化到磁盤文件和讀取磁盤文件反序列化出DataTree和會話列表。
FileTxnSnapLog是對FileTxnLog和FileSnap整合,方便調(diào)用。
事務(wù)請求和非事務(wù)請求都會經(jīng)過一個職責鏈處理,不同的是,事務(wù)請求需要經(jīng)過兩階段提交,而非事務(wù)請求不需要。
兩階段提交只能由Leader發(fā)起提案和進行提交操作,所以Follower和Observer接收到事務(wù)請求必須先轉(zhuǎn)發(fā)給Leader,由Leader發(fā)起兩階段提交。
服務(wù)節(jié)點有三種類型Leader、Follower、Observer,所以有三條請求處理的職責鏈,其中個別處理器相同。
比如三條處理鏈最后都有一個FinalRequestProcessor來處理響應(yīng)或者將請求應(yīng)用到內(nèi)存數(shù)據(jù)庫;Follower和Observer首個處理器都是將事務(wù)請求轉(zhuǎn)發(fā)給 Leader;Observer沒有投票權(quán),不參與兩階段決策,所以沒有響應(yīng)Leader的ACK處理器。
客戶端與服務(wù)端建立連接后,緊接著必須建立會話,之后所有通信都要在會話有效的基礎(chǔ)上進行。會話建立也是事務(wù)請求,sessionID的創(chuàng)建和會話超時時間協(xié)商由當前服務(wù)實例完成,但是會話管理包括會話超時檢查、清理、激活等都必須交由Leader負責。
客戶端發(fā)向服務(wù)端的請求,無論是正常請求還是心跳都會重新激活會話,即重置會話超時時間。而Learner沒有激活會話的權(quán)限,只有在Leader向Learner發(fā)送心跳,Learner響應(yīng)心跳時,將需要激活的會話發(fā)給Leader,由Leader激活會話。
watcher 注冊是非事務(wù)請求特有的??蛻舳瞬⒉粫?watcher 的詳細信息發(fā)送給服務(wù)器,而是只發(fā)送一個是否注冊watcher 的布爾值。
服務(wù)器在處理請求時檢測到請求體里的watch=true,就在內(nèi)存數(shù)據(jù)庫里注冊一個watcher;數(shù)據(jù)發(fā)生變更,就取出該節(jié)點上注冊的所有watcher,進行觸發(fā),觸發(fā)的動作由服務(wù)端傳遞給客戶端;客戶端也保存了節(jié)點和watcher的關(guān)系,客戶端從內(nèi)存中取出該節(jié)點的所有watcher,一個個觸發(fā),觸發(fā)的過程中判斷是發(fā)生了什么事件,如節(jié)點創(chuàng)建、節(jié)點內(nèi)容變更、節(jié)點刪除等。
三、源碼環(huán)境搭建
1、IDEA導入源碼
從 github下拉ZooKeeper源碼最新穩(wěn)定版https://github.com/apache/zookeeper,為了和當時看源碼時的版本一致,這里選擇 release-3.7.0:
git clone -b release-3.7.0 git@github.com:apache/zookeeper.git
源碼導入IDEA即可。org.apache.zookeeper.proto和org.apache.zookeeper.data等包下的類會出現(xiàn)異常:
這是因為這些包的源碼不是現(xiàn)成的,需要通過編譯Jute模塊自動生成。生成的代碼路徑如下:
也可以一勞永逸,直接編譯root項目,這樣就會編譯所有模塊了。
2、本地運行
root項目編譯成功后,就可以像搭建偽集群一樣本地運行源碼了。
如果不知道偽集群搭建需要準備哪些東西,請參考《分布式系統(tǒng)的基石之ZooKeeper——基本原理+場景應(yīng)用+集群搭建(最強萬字入門指南)》。
分別創(chuàng)建三個Application,Program arguments 指定配置文件路徑,Main class有兩種,一種是單體模式ZooKeeperServerMain,一種是集群模式QuorumPeerMain,這里選擇QuorumPeerMain。
分別啟動zoo-1、zoo-2、zoo-3。可能會出現(xiàn)某些類找不到的情況:
這是因為zookeeper-server模塊的pom.xml文件部分依賴的scope是provided的,只有編譯和測試環(huán)境中依賴才起作用,想在運行時也起作用,可以將provided改為compile,或者去掉scope,因為默認scope是compile,編譯,運行,測試環(huán)境依賴都起作用。
修改完zookeeper-server模塊的pom.xml文件后重新編譯就可以了。
如果運行的過程中控制臺沒有打印日志,首先查看zookeeper-server/src/main/resources路徑下是否有l(wèi)og4j.properties文件,如果沒有,就把conf目錄下的log4j.properties復制過來。同時指定zookeeper-server/src/main/resources為Resources目錄才會生效。
如此這般就可以運行了:
在本地運行源碼的好處就是可以debug,debug對于閱讀源碼,理解一些流程非常有幫助。
ZooKeeper源碼注釋:https://github.com/stefanxfy/ZooKeeperLearning
如若文章有錯誤理解,歡迎批評指正,同時非常期待你的評論、和。
如果想了解更多優(yōu)質(zhì)文章,和我更密切的學習交流,請關(guān)注如下同名公眾號【徐同學呀】,期待你的加入。
ZooKeeper
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。