本文作者:HelloGitHub-老荀
Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,免費開源、有趣、入門級的 ZooKeeper 教程,面向有程式設計基礎的新手。
今兒就帶大家打入 ZooKeeper 的原始碼內部!
一、原始碼除錯
授人以魚不如授人以漁
我始終相信 “紙上得來終覺淺”,最終讀者想要自己真正瞭解到 ZK 內部原理,閱讀原始碼還是必不可少的,如果你們和我一樣也擁有肉眼 Debug 的能力,那其實可以不用大費周章搭建原始碼除錯環境,直接正面硬剛。
但是如果沒有的話,把 ZK 原始碼下載下來,使用稱手的 IDE 直接跑起來,然後在需要學習的地方直接打斷點,豈不是美滋滋
1.1 下載原始碼
上面的連結中隨便選一個下載速度快的,點選下載壓縮包即可,下載完成後解壓縮就會得到如下的目錄結構
.
├── zookeeper-server
├── zookeeper-recipes
├── zookeeper-metrics-providers
├── zookeeper-jute
├── zookeeper-it
├── zookeeper-docs
├── zookeeper-contrib
├── zookeeper-compatibility-tests
├── zookeeper-client
├── zookeeper-assembly
├── zk-merge-pr.py
├── pom.xml
├── owaspSuppressions.xml
├── excludeFindBugsFilter.xml
├── dev
├── conf
├── checkstyleSuppressions.xml
├── checkstyle-strict.xml
├── checkstyle-simple.xml
├── bin
├── README_packaging.md
├── README.md
├── NOTICE.txt
├── LICENSE.txt
├── Jenkinsfile-PreCommit
└── Jenkinsfile
目錄中是有 pom.xml
所以 ZK 需要通過 maven 編譯整個專案,先確保自己的 maven 是安裝好的
$ mvn --version
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T02:33:14+08:00)
Maven home: /your/maven/home/apache-maven-3.5.4
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.16", arch: "x86_64", family: "mac"
如果有這樣的輸出說明 maven 是安裝成功的,具體安裝過程我這裡就略過了,如果你有困難的話,可以留言給我們
1.2 編譯專案
進入和 pom.xml
同級目錄中並輸入
$ mvn install -DskipTests=true
就會看到專案在進行編譯了,等到最後的輸出 BUILD SUCCESS
,就說明專案編譯完成了
[INFO] Reactor Summary:
[INFO]
[INFO] Apache ZooKeeper 3.6.2 ............................. SUCCESS [ 3.621 s]
[INFO] Apache ZooKeeper - Documentation ................... SUCCESS [ 2.086 s]
[INFO] Apache ZooKeeper - Jute ............................ SUCCESS [ 10.633 s]
[INFO] Apache ZooKeeper - Server .......................... SUCCESS [ 19.246 s]
[INFO] Apache ZooKeeper - Metrics Providers ............... SUCCESS [ 0.108 s]
[INFO] Apache ZooKeeper - Prometheus.io Metrics Provider .. SUCCESS [ 1.286 s]
[INFO] Apache ZooKeeper - Client .......................... SUCCESS [ 0.083 s]
[INFO] Apache ZooKeeper - Recipes ......................... SUCCESS [ 0.092 s]
[INFO] Apache ZooKeeper - Recipes - Election .............. SUCCESS [ 0.244 s]
[INFO] Apache ZooKeeper - Recipes - Lock .................. SUCCESS [ 0.259 s]
[INFO] Apache ZooKeeper - Recipes - Queue ................. SUCCESS [ 0.295 s]
[INFO] Apache ZooKeeper - Assembly ........................ SUCCESS [ 5.425 s]
[INFO] Apache ZooKeeper - Compatibility Tests ............. SUCCESS [ 0.072 s]
[INFO] Apache ZooKeeper - Compatibility Tests - Curator 3.6.2 SUCCESS [ 0.432 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 44.263 s
[INFO] Finished at: 2021-01-22T13:49:30+08:00
[INFO] ------------------------------------------------------------------------
1.3 開啟並配置專案
之後就可以通過你的 IDE 開啟這個目錄了,我這裡使用的是 idea
然後開始配置 Run/Debug Configurations
點選 +
新增新的配置
選擇 Application
1.3.1 單機版啟動配置
然後配置按照下圖去填寫或選擇
- 先給這個配置起一個牛逼的名字
- 選擇
Modify options
開啟子選單 - 確保圖中選單中的三個子選項都被選中(前面有 √)
然後我們看具體的配置
在我電腦上解壓縮後的專案路徑為 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2
讀者請根據自己情況修改
- 選擇你本地 jdk (我本地是 1.8 其他版本的不知道行不行,低版本肯定是不行,因為原始碼中用到了 1.8 的一些寫法)
- 選擇
zookeeper
- 配置
VM options
,內容為-Dlog4j.configuration=file:/Users/junjiexun/Desktop/apache-zookeeper-3.6.2/conf/log4j.properties
,如果不配置的話,無法輸出日誌 - 指定啟動類
org.apache.zookeeper.server.ZooKeeperServerMain
- 單機版啟動需要命令列引數,內容為
2181 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data
- 這個應該是不用修改,自動就會填上的,反正內容就是
/Users/junjiexun/Desktop/apache-zookeeper-3.6.2
- 點選中間的
+
新增包路徑,內容為org.apache.zookeeper.server.*
然後點選 Apply
以及 OK
完成儲存。
然後點選這個小蟲子就可以啟動了
2021-01-22 15:12:16,319 [myid:] - INFO [main:NIOServerCnxnFactory@674] - binding to port 0.0.0.0/0.0.0.0:2181
2021-01-22 15:12:16,413 [myid:] - INFO [main:WatchManagerFactory@42] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:12:16,413 [myid:] - INFO [main:WatchManagerFactory@42] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:12:16,413 [myid:] - INFO [main:ZKDatabase@132] - zookeeper.snapshotSizeFactor = 0.33
2021-01-22 15:12:16,413 [myid:] - INFO [main:ZKDatabase@152] - zookeeper.commitLogCount=500
2021-01-22 15:12:16,429 [myid:] - INFO [main:SnapStream@61] - zookeeper.snapshot.compression.method = CHECKED
2021-01-22 15:12:16,432 [myid:] - INFO [main:FileSnap@85] - Reading snapshot /Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data/version-2/snapshot.2
2021-01-22 15:12:16,444 [myid:] - INFO [main:DataTree@1737] - The digest value is empty in snapshot
2021-01-22 15:12:16,480 [myid:] - INFO [main:ZKDatabase@289] - Snapshot loaded in 67 ms, highest zxid is 0x2, digest is 1371985504
2021-01-22 15:12:16,481 [myid:] - INFO [main:FileTxnSnapLog@470] - Snapshotting: 0x2 to /Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data/version-2/snapshot.2
2021-01-22 15:12:16,488 [myid:] - INFO [main:ZooKeeperServer@529] - Snapshot taken in 6 ms
2021-01-22 15:12:16,544 [myid:] - INFO [ProcessThread(sid:0 cport:2181)::PrepRequestProcessor@136] - PrepRequestProcessor (sid:0) started, reconfigEnabled=false
2021-01-22 15:12:16,546 [myid:] - INFO [main:RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
2021-01-22 15:12:16,623 [myid:] - INFO [main:ContainerManager@83] - Using checkIntervalMs=60000 maxPerMinute=10000 maxNeverUsedIntervalMs=0
2021-01-22 15:12:16,628 [myid:] - INFO [main:ZKAuditProvider@42] - ZooKeeper audit is disabled.
看到日誌輸出,如果沒有報錯的話就是成功了!
然後我們可以用客戶端測試下
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 3000, null);
List<String> children = client.getChildren("/", false);
System.out.println(children);
client.close();
輸出為
[zookeeper]
單機版的搞定了!我們下面試試叢集版
1.3.2 叢集版啟動配置
我們有時候需要除錯叢集版 ZK 才有的邏輯,那之前的單機版就不夠用了,並且我這裡推薦將之前的原始碼壓縮包,解壓到兩個不同的目錄下,然後通過 IDE 分別開啟這兩個目錄,去完全模擬兩個不同的節點。叢集版的和單機版配置是差不多的,我們來看看有哪些不一樣的吧?我這裡演示就啟動兩個節點 myid 分別是 1 和 2。
- 首先將預設的
zoo_sample.cfg
複製並重新命名成zoo.cfg
,也可以直接重新命名 - 新建
data
目錄(如果沒有的話),並在其下新建一個文字檔案 myid 文字內容是 1
然後編輯下 zoo.cfg
:
# 修改
dataDir=/Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data
# 新增下面兩行
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2887:3887
具體的配置如下:
- 啟動類不同,叢集的為
org.apache.zookeeper.server.quorum.QuorumPeerMain
- 命令列引數不同,傳入的是
zoo.cfg
路徑,我的路徑是/Users/junjiexun/Desktop/apache-zookeeper-3.6.2/conf/zoo.cfg
然後是配置第二個節點,我這裡假設第二個節點的專案目錄是 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2-bak
第二個節點把 myid 檔案中的內容修改為 2
zoo.cfg
中內容是
# 修改
dataDir=/Users/junjiexun/Desktop/apache-zookeeper-3.6.2-bak/data
# 修改,因為我兩個節點是在一臺機器中的,所以埠是不能重複的
clientPort=2182
# 同樣新增下面兩行
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2887:3887
命令列的引數是 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2-bak/conf/zoo.cfg
其他我沒提到的和節點 1 是一樣的。
我們啟動兩個節點試試
2021-01-22 15:44:08,461 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):WatchManagerFactory@42] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:44:08,461 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):WatchManagerFactory@42] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:44:08,471 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):Learner@677] - Learner received NEWLEADER message
2021-01-22 15:44:08,471 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):QuorumPeer@1811] - Dynamic reconfig is disabled, we don't store the last seen config.
2021-01-22 15:44:08,471 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):FileTxnSnapLog@470] - Snapshotting: 0x28100000001 to /Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data/version-2/snapshot.28100000001
2021-01-22 15:44:08,472 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):ZooKeeperServer@529] - Snapshot taken in 1 ms
2021-01-22 15:44:08,525 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):Learner@661] - Learner received UPTODATE message
2021-01-22 15:44:08,525 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):QuorumPeer@868] - Peer state changed: following - synchronization
2021-01-22 15:44:08,537 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):CommitProcessor@476] - Configuring CommitProcessor with readBatchSize -1 commitBatchSize 1
2021-01-22 15:44:08,537 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):CommitProcessor@438] - Configuring CommitProcessor with 4 worker threads.
2021-01-22 15:44:08,544 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
2021-01-22 15:44:08,567 [myid:1] - INFO [QuorumPeer[myid=1](plain=[0:0:0:0:0:0:0:0]:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: following - broadcast
最後的 Peer state changed
代表選舉完成了,貼出來的這個節點 1 是 Follower,大功告成!
之後當你想要學習原始碼的流程的時候,直接本地啟動服務端即可,是不是美滋滋呢~
1.4 原始碼閱讀指北
- 服務端啟動,叢集
QuorumPeerMain#main
,單機ZooKeeperServerMain#main
- 客戶端
ZooKeeper
- 解析配置相關,
QuorumPeerConfig#parse
- 記憶體模型(小紅本)
DataTree
- 回撥通知(小黃本)
IWatchManager
檢視該介面實現- 預設實現
WatchManager
- 優化方案
WatchManagerOptimized
- 預設實現
- 選舉
FastLeaderElection#lookForLeader
- 服務端例項,設定流水線
setupRequestProcessors
方法- Leader 節點
LeaderZooKeeperServer
- Follower 節點
FollowerZooKeeperServer
- Observer 節點
ObserverZooKeeperServer
- Leader 節點
- 各個流水線員工
RequestProcessor
檢視該介面的實現 - 持久化 log
FileTxnLog
,snapshotFileSnap
- 會話管理
SessionTrackerImpl#run
- 協議
Record
檢視該介面的實現
1.5 原始碼閱讀心得
閱讀大型專案的原始碼一定是一個費時費心費力的工作,我這裡也講一下我閱讀 ZK 原始碼的心得:
- 不要死摳細節!大型專案的原始碼數量通常比較多,如果盯著邏輯中的每一個細節,就會迷失在原始碼的汪洋大海中。
- 通常閱讀原始碼都要帶著一個目的。例如:ZK 是怎麼進行協議轉換的,ZK 是怎麼選舉的等等。有了目的以後,看相關原始碼是要選擇性的忽略一些其他不相關的細節,可以通過方法名或者註釋,來對具體的程式碼塊先有一個感性的認識。
- 碰到讀不懂的地方,可以先去網上看看有沒有人寫過類似的部落格,站在巨人的肩膀上,很可能別人一點你就通了。
- 在 ZK 中一般間接或者直接繼承
ZooKeeperThread
都是執行緒物件,主要邏輯可以檢視run
方法。 - 任何一個類重要的屬性肯定是在成員欄位中,通過檢視成員欄位是可以大致推測出該類背後的資料結構。
- 成員屬性中如果有阻塞佇列的欄位,大概率會是生產者-消費者模式的體現,可以重點關注該阻塞佇列的使用,何時放入以及取出元素。
1.6 小結
我用一些圖文的篇幅介紹瞭如何在本地除錯 ZK 原始碼,以及如何科學的閱讀原始碼。我本地的環境是 Mac,用的 IDE 是 idea,如果你的環境或者工具和我不一樣,碰到了困難的話,也可以給我們留言哦~
二、ZK 中應用到的設計模式
ZK 本身就是分散式的應用,也是優秀的開源專案,我這裡就簡單聊聊我在閱讀原始碼中看到的應用在 ZK 裡的設計模式吧
2.1 生產者消費者
這個是 ZK 中非常有代表性的設計模式應用了,ZK 本身是 C/S 架構的設計,請求就是客戶端傳送給服務端資料,響應則是服務端傳送給客戶端資料,而 ZK 實現一些功能並不是通過線性順序的去呼叫不同的方法去完成的,通常會由生產者執行緒,阻塞佇列和消費者執行緒組成,生產者執行緒將上游收到的一些請求物件放入阻塞佇列,當前的方法就返回了,之後由消費者執行緒通過迴圈不停的從阻塞佇列中獲取,再完成之後的業務邏輯。舉例:
PrepRequestProcessor
,阻塞佇列是submittedRequests
SyncRequestProcessor
,阻塞佇列是queuedRequests
2.2 工廠模式
有一些介面的實現,ZK 本身提供了預設的選擇,但是如果使用者在配置中配置了其他的實現的話,ZK 的工廠就會自動去建立那些其他的實現。舉例:
- 在建立
ClientCnxnSocket
時,會根據zookeeper.clientCnxnSocket
的配置去選擇客戶端的 IO 實現 - 在建立
IWatchManager
時,會根據zookeeper.watchManagerName
的配置去選擇服務端的 watch 管理實現 - 在建立
ServerCnxnFactory
時,會根據zookeeper.serverCnxnFactory
的配置去選擇服務端的 IO 工廠實現
2.3 責任鏈模式
之前有學習過,ZK 服務端業務邏輯處理是通過將一個個 XxxProcessor
串起來實現的,Processor 彼此不關心呼叫順序,僅僅通過 nextProcessor
關聯,不同的服務端角色也可以通過這種方式極大的複用程式碼
- 單機模式下:
PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor
- 叢集模式下 Leader :
LeaderRequestProcessor -> PrepRequestProcessor -> ProposalRequestProcessor -> CommitProcessor -> Leader.ToBeAppliedRequestProcessor -> FinalRequestProcessor
- 叢集模式下 Follower :
FollowerRequestProcessor -> CommitProcessor -> FinalRequestProcessor
- 叢集模式下 Observer :
ObserverRequestProcessor -> CommitProcessor -> FinalRequestProcessor
2.4 策略模式
zookeeper.snapshot.compression.method
可以配置成不同的 snapshot 壓縮演算法,當需要生成 snapshot 檔案的時候,會根據不同的壓縮演算法去執行:
gz
:GZIPInputStream
snappy
:SnappyInputStream
- 預設:
BufferedInputStream
2.5 裝飾器模式
還是剛剛的壓縮演算法,對外提供的是 CheckedInputStream
的統一處理物件,使用 CheckedInputStream
將上面三種壓縮實現包裝起來,這些物件全部都是 InputStream
的子類
switch (根據不同的配置) {
// 策略模式的體現
case GZIP:
is = new GZIPInputStream(fis);
break;
case SNAPPY:
is = new SnappyInputStream(fis);
break;
case CHECKED:
default:
is = new BufferedInputStream(fis);
}
// 都被包裝進了 CheckedInputStream
// 裝飾器模式的體現
return new CheckedInputStream(is, new Adler32());
三、總結
今天我講了如何直接從 ZK 原始碼 DEBUG,介紹了一些 ZK 中用到的設計模式,大家有閱讀原始碼問題的話,歡迎給我留言哦。本文首發於 「HelloGitHub」公眾號
下一期介紹 ZK 的高階用法純實戰,期待一下吧~
老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題討論。