手摸手教你閱讀和除錯大型開源專案 ZooKeeper

削微寒 發表於 2021-04-08

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

本文作者:HelloGitHub-老荀

Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,免費開源、有趣、入門級的 ZooKeeper 教程,面向有程式設計基礎的新手。

專案地址:https://github.com/HelloGitHub-Team/HelloZooKeeper

今兒就帶大家打入 ZooKeeper 的原始碼內部!

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

一、原始碼除錯

授人以魚不如授人以漁

我始終相信 “紙上得來終覺淺”,最終讀者想要自己真正瞭解到 ZK 內部原理,閱讀原始碼還是必不可少的,如果你們和我一樣也擁有肉眼 Debug 的能力,那其實可以不用大費周章搭建原始碼除錯環境,直接正面硬剛。

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

但是如果沒有的話,把 ZK 原始碼下載下來,使用稱手的 IDE 直接跑起來,然後在需要學習的地方直接打斷點,豈不是美滋滋

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

1.1 下載原始碼

ZooKeeper 3.6.2 原始碼下載頁面

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

上面的連結中隨便選一個下載速度快的,點選下載壓縮包即可,下載完成後解壓縮就會得到如下的目錄結構

.
├── 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

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

然後開始配置 Run/Debug Configurations

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

點選 + 新增新的配置

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

選擇 Application

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

1.3.1 單機版啟動配置

然後配置按照下圖去填寫或選擇

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

  1. 先給這個配置起一個牛逼的名字
  2. 選擇 Modify options 開啟子選單
  3. 確保圖中選單中的三個子選項都被選中(前面有 √)

然後我們看具體的配置

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

在我電腦上解壓縮後的專案路徑為 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2 讀者請根據自己情況修改

  1. 選擇你本地 jdk (我本地是 1.8 其他版本的不知道行不行,低版本肯定是不行,因為原始碼中用到了 1.8 的一些寫法)
  2. 選擇 zookeeper
  3. 配置 VM options,內容為 -Dlog4j.configuration=file:/Users/junjiexun/Desktop/apache-zookeeper-3.6.2/conf/log4j.properties,如果不配置的話,無法輸出日誌
  4. 指定啟動類 org.apache.zookeeper.server.ZooKeeperServerMain
  5. 單機版啟動需要命令列引數,內容為 2181 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2/data
  6. 這個應該是不用修改,自動就會填上的,反正內容就是 /Users/junjiexun/Desktop/apache-zookeeper-3.6.2
  7. 點選中間的 + 新增包路徑,內容為 org.apache.zookeeper.server.*

然後點選 Apply 以及 OK 完成儲存。

然後點選這個小蟲子就可以啟動了

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

2021-01-22 15:12:16,319 [myid:] - INFO  [main:[email protected]] - binding to port 0.0.0.0/0.0.0.0:2181
2021-01-22 15:12:16,413 [myid:] - INFO  [main:[email protected]] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:12:16,413 [myid:] - INFO  [main:[email protected]] - Using org.apache.zookeeper.server.watch.WatchManager as watch manager
2021-01-22 15:12:16,413 [myid:] - INFO  [main:[email protected]] - zookeeper.snapshotSizeFactor = 0.33
2021-01-22 15:12:16,413 [myid:] - INFO  [main:[email protected]] - zookeeper.commitLogCount=500
2021-01-22 15:12:16,429 [myid:] - INFO  [main:[email protected]] - zookeeper.snapshot.compression.method = CHECKED
2021-01-22 15:12:16,432 [myid:] - INFO  [main:Fi[email protected]] - 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:[email protected]] - The digest value is empty in snapshot
2021-01-22 15:12:16,480 [myid:] - INFO  [main:[email protected]] - Snapshot loaded in 67 ms, highest zxid is 0x2, digest is 1371985504
2021-01-22 15:12:16,481 [myid:] - INFO  [main:[email protected]] - 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:[email protected]] - Snapshot taken in 6 ms
2021-01-22 15:12:16,544 [myid:] - INFO  [ProcessThread(sid:0 cport:2181)::[email protected]] - PrepRequestProcessor (sid:0) started, reconfigEnabled=false
2021-01-22 15:12:16,546 [myid:] - INFO  [main:[email protected]] - zookeeper.request_throttler.shutdownTimeout = 10000
2021-01-22 15:12:16,623 [myid:] - INFO  [main:[email protected]] - Using checkIntervalMs=60000 maxPerMinute=10000 maxNeverUsedIntervalMs=0
2021-01-22 15:12:16,628 [myid:] - INFO  [main:[email protected]] - 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。

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

  1. 首先將預設的 zoo_sample.cfg 複製並重新命名成 zoo.cfg,也可以直接重新命名
  2. 新建 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

具體的配置如下:

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

  1. 啟動類不同,叢集的為 org.apache.zookeeper.server.quorum.QuorumPeerMain
  2. 命令列引數不同,傳入的是 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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):[email protected]] - 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
  • 各個流水線員工 RequestProcessor 檢視該介面的實現
  • 持久化 log FileTxnLog,snapshot FileSnap
  • 會話管理 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 檔案的時候,會根據不同的壓縮演算法去執行:

  • gzGZIPInputStream
  • snappySnappyInputStream
  • 預設: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 的高階用法純實戰,期待一下吧~

手摸手教你閱讀和除錯大型開源專案 ZooKeeper

老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題討論。

地址:https://www.yuque.com/kaixin1002/yla8hz