本文將從系統模型、序列化與協議、客戶端工作原理、會話、服務端工作原理以及數據存儲等方面來揭示ZooKeeper的技術內幕。
一、系統模型
1.1 數據模型
ZooKeeper的視圖結構使用了其特有的“數據節點”概念,我們稱之為ZNode。ZNode是ZooKeeper中數據的最小單元,每個ZNode上都可以保存數據,同時還可以掛載子節點,因此構成了一個層次化的命名空間,我們稱之為樹。
1.2 節點特性
我們已知,ZooKeeper的命名空間是由一系列數據節點組成的,我們將對數據節點做詳細講解。
節點類型
在ZooKeeper中,每個數據節點都是有生命周期的,其生命周期的長短取決于數據節點的節點類型。在ZooKeeper中,節點類型可以分為持久節點(PERSISTENT)、臨時節點(EPHEMERAL)和順序節點(SEQUENTIAL)三大類,ju'ti具體在節點創建過程中,通過組合使用,可以生成以下四種組合型節點類型:
- 持久節點(PERSISTENT)
數據節點被創建后,就會一直存在于ZooKeeper服務器上,直到有刪除操作來主動清除這個節點。
- 持久順序節點(PERSISTENT_SEQUENTIAL)
他的基本特性和持久節點是一致的,額外的特性表現在順序性上。在ZooKeeper中,每個父節點都會為他的第一級子節點維護一份順序,用于記錄下每個子節點創建的先后順序。基于這個順序特性,在創建子節點的時候,可以設置這個標記,那么在創建節點過程中,ZooKeeper會自動為給定節點加上一個數字后綴,作為一個新的、完整的節點名。另外需要注意的是,這個數字后綴的上限是整型的最大值。
- 臨時節點(EPHEMERAL)
臨時節點的生命周期和客戶端的會話綁定在一起,也就是說,如果客戶端會話失效,那么這個節點就會被自動清理掉。這里提到的客戶端會話失效,而非TCP連接斷開。
- 臨時順序節點(EPHEMERAL_SEQUENTIAL)
在臨時節點基礎上,添加了順序的特性。
狀態信息
每個數據節點除了存儲了數據內容外,還存儲了數據節點本身的一些狀態信息。
狀態屬性 | 說明 |
---|---|
czxid | 即Created ZXID,表示該節點被創建時的事務ID |
mzxid | 即Modified ZXID,表示該節點最后一次被更新時的事務ID |
ctime | 即Created Time |
mtime | 即Modified Time |
version | 數據節點的版本號 |
cversion | 子節點的版本號 |
aversion | 節點的ACL版本號 |
ephemeralOwner | 創建該臨時節點的會話的sessionID。如果該節點是持久節點,那么這個屬性值為0 |
dataLength | 數據內容長度 |
numChildren | 當前節點的子節點個數 |
pzxid | 表示該節點的子節點列表最后一次被修改時的事務ID。注意,只有子節點列表變更了才會變更pzxid,子節點內容變更不會影響pzxid。 |
1.3 版本-保證分布式數據原子性操作
ZooKeeper中為數據節點引入了版本的概念,每個數據節點都具有三種類型的版本信息,對數據節點的任何更新操作都會引起版本號的變化。
版本類型 | 說明 |
---|---|
version | 當前數據節點數據內容的版本號 |
cversion | 當前數據節點子節點的版本號 |
aversion | 當前數據節點ACL變更版本號 |
在ZooKeeper中,version屬性正是用來實現樂觀鎖機制中的“寫入校驗”的。
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if(version != -1 && version != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;
1.4 Watcher-數據變更的通知
在ZooKeeper中,引入了Watcher機制來實現這種分布式的通知功能。ZooKeeper允許客戶端向服務端注冊一個Watcher監聽,當服務器的一些指定事件出發了這個Watcher,那么就會向指定客戶端發送一個事件通知來實現分布式的通知功能。
從圖中我們可以看到,ZooKeeper的Watcher機制主要包括ke'hu'duan'xian'c客戶端線程、客戶端WatcherManager和ZooKeeper服務器三部分。在具體工作流程上,客戶端在向ZooKeeper服務器注冊Watcher的同時,會將Watcher對象存儲在客戶端的WatcherManager中。當ZooKeeper服務器端觸發Watcher事件后,會向客戶端發送通知,客戶端線程從WatcherManager中取出對應的Watcher對象來執行回調邏輯。
1.5 ACL--保障數據的安全
提到權限控制,我們首先看看目前應用最廣泛的權限控制方式--UGO(User、Group和Others)權限控制機制。簡單地講,UGO就是針對一個文件或目錄,對創建者(User)、創建者所在的組(Group)和其他用戶(Other)分別配置不同的權限。從這里可以看出,UGO其實是一種粗粒度的文件系統權限控制模式,利用UGO只能對三類用戶jin'xing進行權限控制,即文件的創建者、創建者所在的組以及其他所有用戶,很顯然,UGO無法解決下面這個場景:
用戶U1創建了文件F1,希望U1所在的用戶組G1擁有對F1讀寫和執行的權限,另一個用戶組G2擁有讀權限,而另一個用戶U3則沒有任何權限。
下面我們來看另外一種典型的權限控制方式:ACL。ACL,即訪問控制列表,是一種相對來說比較新穎且更細粒度的權限管理方式。可以針對任何用戶和組進行細粒度的權限控制。
ACL介紹
ZooKeeper的ACL權限控制和Unix/Linux操作系統中的ACL有一些區別,讀者可以從三個方面來理解ACL機制,分別是:權限模式(Scheme)、授權對象(ID)和權限(Permission),通常使用“scheme:id:permission”來標識一個有效的ACL信息。
權限模式:Scheme
權限模式用來確定權限驗證過程中使用的校驗策略。在ZooKeeper中,開發人員使用最多的就是以下四種權限模式。
- IP
IP模式通過IP地址粒度來進行權限控制。也支持按照網段的方式進行配置。
- Digest
以類似于“username:password”形式的權限標識來進行權限配置,便于區分不同應用來進行權限控制。
- World
數據節點的訪問權限對所有用戶開發,即所有用戶可以在不進行任何權限校驗的情況下操作ZooKeeper上的數據。另外,World模式也可以看作是一種特殊的Digest模式,他只有一個權限標識,即“world:anyone”。
- Super
超級用戶的意思。
授權對象:ID
權限賦予的用戶或一個指定實體。在不同的權限模式下,授權對象是不同的。
權限:Permission
在ZooKeeper中,所有對數據的操作權限分為以下五大類:
- CREATE(C)
- DELETE(D)
- READ(R)
- WRITE(W)
- ADMIN(A)
權限擴展體系
實現自定義權限控制器
二、序列化協議
ZooKeeper的客戶端和服務端之間會進行一系列的網絡通信以實現數據的傳輸。對于一個網絡通信,首先要解決的就是對數據的序列化和反序列化處理,在ZooKeeper中,使用了Jute這一序列化組件來進行數據的序列化和反序列化操作。同時,為了實現一個高效的網絡通信程序,良好的通信協議設計也是至關重要的。
通信協議
基于TCP/IP協議,ZooKeeper實現了自己的通信協議來完成客戶端與服務端、服務端與服務端之間的網絡通信。ZooKeeper通信協議整體上的設計非常簡單,對于請求,主要包含請求頭和請求體,對于響應,則主要包含響應頭和相應體。
三、客戶端
客戶端是開發人員使用ZooKeeper最主要的途徑,因此我們有必要對ZooKeeper客戶端的內部原理進行詳細講解。ZooKeeper的客戶端主要由以下幾個核心組件組成。
- ZooKeeper實例:客戶端的入口。
- ClientWatchManager:客戶端Watcher管理器。
- HostProvider:客戶端地址列表管理器。
- ClientCnxn:客戶端核心線程,其內部又包含兩個線程,即SendThread和EventThread。前者是一個I/O線程,主要負責ZooKeeper客戶端和服務端之間的網絡I/O通信;后者是一個事件線程,主要負責對服務端事件進行處理。
客戶端的整個初始化和啟動過程大體可以分為以下三個步驟。
- 設置默認Watcher。
- 設置ZooKeeper服務器地址列表。
- 創建ClientCnxn。
3.1 一次會話的創建過程
初始化階段
- 初始化ZooKeeper對象。
- 設置會話默認Watcher。
- 構造ZooKeeper服務器地址列表管理器:HostProvider。
- 創建并初始化客戶端網絡連接器:ClientCnxn。
- 初始化SendThread和EventThread。
會話創建階段
- 啟動SendThread和EventThread
- 獲取一個服務器地址。
- 創建TCP連接。
- 構造ConnectRequest請求。
- 發送請求。
響應處理階段
- 接受服務端響應
- 處理Response
- 連接成功
- 生成時間:SyncConnected-None
- 查詢Watcher
- 處理事件
四、會話
會話(Session)是ZooKeeper中最重要的概念之一,客戶端和服務端之間的任何交互操作都與會話息息相關,這其中就包括臨時節點的生命周期、客戶端請求的順序執行以及Watcher通知機制等。
4.1 會話狀態
在ZooKeeper客戶端和服務端成功完成連接創建后,就建立了一個會話。ZooKeeper會話在整個運行期間的聲明周期中,會在不同的會話狀態之間進行切換,這些狀態一般可以分為CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE等。
如果客戶端需要與服務端創建一個會話,那么客戶端必須提供一個使用字符串表示的服務器地址列表:“host1:port,host2:port,host3:port”。一旦客戶端開始創建ZooKeeper對象,那么客戶端狀態就會變成CONNECTING,同時客戶端開始從上述服務器地址列表中逐個選取IP地址來嘗試進行網絡連接,直到成功連接上服務器,然后將客戶端狀態變更為CONNECTED。
通常,伴隨著網絡閃斷或是其他原因,客戶端和服務器之間的連接會出現斷開情況。一旦碰到這種情況,ZooKeeper客戶端會自動進行重連操作,同時客戶端的狀態再次變為CONNECTING,直到重新連接上ZooKeeper服務器后,客戶端狀態又會再次轉變成CONNECTED。因此,在通常情況下,在ZooKeeper運行期間,客戶端的狀態總是介于CONNECTING和CONNECTED兩者之一。
另外,如果出現諸如會話超時、權限檢查失敗或是客戶端主動退出程序等情況,那么客戶端的狀態就會直接變為CLOSE。
4.2 會話創建
Session
Session是ZooKeeper中的會話實體,代表了一個客戶端會話。其包含以下4個基礎屬性、
- sessionId:會話id,用來唯一標識一個會話,每次客戶端創建新會話的時候,ZooKeeper都會為其分配一個全局唯一的sessionId。
- TimeOut:會話超時時間。客戶端在構造ZooKeeper實例的時候,會配置一個sessionTimeout參數用于指定會話的超時時間。ZooKeeper客戶端向服務器發送這個超時時間后,服務器會根據自己的超時時間限制最終確定會話的超時時間。
- TickTime:下次會話超時時間點。
- isClosing:該屬性用于標記一個會話是否已經被關閉。
sessionID
在SeesionTracker初始化的時候,會調用initializeNextSession方法來生成一個初始化的sessionID,之后在ZooKeeper的正常運行過程中,會在該sessionID的基礎上為每個會話進行分配,其初始化算法如下:
public static long initializeNextSeesion(long id) {
long nextSid = 0;
nextSid = (System.currentTimeMillis() << 24) >> 8;
nextSid = nextSid | (id << 56);
return nextSid;
}
上面這個方法就是ZooKeeper初始化sessionID的算法,我們一起深入的探究下。從上面的代碼片段中,可以看出sessionID的生成大體可以分為以下5個步驟。
- 獲取當前的毫秒表示。
- 左移24位。
- 右移8位。
- 添加機器標識:SID。
- 將步驟3和步驟4得到的兩個64位表示的數值進行“|”操作。
簡單地講,可以將上述算法概括為:高8位確定了所在機器,后56位使用當前時間的毫秒進行隨機。
SessionTracker
SessionTracker是ZooKeeper服務端的會話管理器,負責會話的創建、管理和清理等工作。可以說,整個會話的生命周期都離不開SessionTracker的管理。每一個會話在SessionTracker內部都保留了三份,具體如下。
- sessionsById:這是一個HashMap<Long, SeesionImpl>類型的數據結構,用于根據sessionID來管理Session實體。
- sessionsWithTimeout:這是一個ConcurrentHashMap<Long, Integer>類型的數據結構,用于根據sessionID來管理會話的超時時間。該數據結構和ZooKeeper內存數據庫相連通,會被定期持久化到快照文件中去。
- sessionSets:這是一個HashMap<Long, SessionSet>類型的數據結構,用于根據下次會話超時時間來歸檔會話,便于進行會話管理和超時檢查。
創建連接
服務端對于客戶端的“會話創建”請求的處理,大體可以分為四大步驟,分別是ConnectRequest請求、會話創建、處理器鏈路處理和會話響應。
4.3 會話管理
分桶策略
ZooKeeper的會話管理主要是由SessionTracker負責的,其采用了一種特殊的會化管理方式,我們稱之為“分桶策略”。所謂分桶策略,是指將類似的會話放在同一區塊中進行管理,以便于ZooKeeper對會話進行不同區塊的格里處理以及同一區塊的統一處理。
ZooKeeper將所有的會話都分配在了不同的區塊之中,分配的原則是每個會話的“下次超時時間點”(ExpirationTime)。ExpirationTime是指該會話最近一次可能超時的時間點,對于一個新創建的會話而言,其會話創建完畢后,ZooKeeper就會為其計算ExpirationTime,計算方式如下:
ExpirationTime = CurrentTime + SessionTimeout
在ZooKeeper的實際實現中,Zookeeper的Leader服務器在運行期間會定時的進行會話超時檢查,其時間間隔是ExpirationInterval,單位是毫秒,默認值是tickTime的值,即默認情況下,每隔2000毫秒進行一次會話超時檢查。為了方便對多個會話同時進行超時檢查,完整的ExpirationTime的計算方式如下:
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_/ExpirationInterval + 1) * ExpirationInterval
會話激活
為了保持客戶端會話的有效性,在ZooKeeper的運行過程中,客戶端會在會話超時時間國企范圍內向服務端發送PING請求來保持會話的有效性,我們俗稱“心跳檢測”。同時,服務端需要不斷地接收來自客戶端的這個心跳檢測,并且需要重新激活對應的客戶端會話,我們將這個重新激活的過程稱為TouchSession。會話激活的過程,不僅能夠使服務端檢測到對應客戶端的存活性,也能讓客戶端自己保持連接狀態。
會話超時檢查
在ZooKeeper中,會話超時檢查同樣是由SessionTracker負責的。SessionTracker中有一個單獨的線程專門進行會話超時檢查,這里我們稱其為“超時檢查線程”,其工作機制的核心思路非常簡單:逐個依次對會話桶中剩下的會話進行清理。
4.4 會話清理
當SessionTracker的會話超時檢查線程整理出一些已經過期的會話后,那么就要開始進行會話清理了。會話清理的步驟大致可以分為以下七步。
- 標記會話狀態為“已關閉”
為了保證在清理期間不再處理來自該客戶端的新請求,SessionTracker會首先將該會話的isClosing屬性標記為true。
- 發起“會話關閉”請求
為了使該會話的關閉操作在整個服務端集群中都生效,ZooKeeper使用了提交“會話關閉”請求的方式,并立即交付給PrepRequestProcessor處理器進行處理。
- 收集需要清理的臨時節點
在ZooKeeper的內存數據庫中,為每個會話都單獨保存了一份由該會話維護的所有臨時節點集合,因此在會話清理階段,只需要根據當前即將關閉的會話的sessionID從內存數據庫中獲取到這份臨時節點列表即可。
實際上,有如下細節需要處理:在ZooKeeper處理會話關閉請求之前,正好有以下請求到達了服務端并正在處理中:
- 節點刪除請求,刪除的目標節點正好是上述臨時節點中的一個。
- 臨時節點創建請求,創建的目標節點正好是上述臨時節點中的一個。
嘉定我們當前獲取的臨時節點列表是ephemerals,那么針對第一類請求,我們需要將所有這些請求對應的數據節點路徑從ephemerals中移除,以避免重復刪除。針對第二類,我們需要將所有這些請求對應的數據節點路徑添加到ephemerals中去,以刪除這些即將會被創建但是尚未保存到內存數據庫中去的臨時節點。
- 添加“節點刪除”事務變更
完成該會話相關的臨時節點收集后,ZooKeeper會逐個將這些臨時節點轉換成“節點刪除”請求,并放入事務變更隊列outstandingChanges中去。
- 刪除臨時節點
FinalRequestProcessor處理器會觸發內存數據庫,刪除該會話對應的所有臨時節點。
- 移除會話
完成節點刪除后,需要將會話從SessionTracker中移除。主要就是從上面提到的三個數據結構(sessionById、sessionsWithTimeout和sessionSets)中將該會話移除掉。
- 關閉NIOServerCnxn
最后,從NIOServerCnxnFactory找到該會話對應的NIOServerCnxn,將其關閉。
4.5 重連
當客戶端和服務端之間的網絡連接斷開時,ZooKeeper客戶端會自動進行反復的重連,知道最終成功連接上ZooKeeper集群中的一臺機器。在這種情況下,再次連接上服務端的客戶端有可能會處于以下兩種狀態之一。
- CONNECTED:重連成功
- EXPIRED:如果是在會話超時時間以外重新連接上,那么服務端其實已經對該會話進行了會話清理操作,因此再次連接上的會話將被視為非法會話。
當客戶端和服務端之間的連接斷開后,用戶在客戶端可能會看到兩類異常:CONNECTION_LOSS(連接斷開)和SESSION_EXPIRED(會話過期)。
五、服務器啟動
我們首先看看ZooKeeper服務端的整體架構,如圖
5.1 單機版服務器啟動
ZooKeeper服務器的啟動,大體可以分為以下五個主要步驟:配置文件解析、初始化數據管理器、初始化網絡I/O管理器、數據恢復和對外服務。下圖是單機版ZooKeeper服務器的啟動流程圖。
預啟動
預啟動的步驟如下。
- 統一由QuorumPeerMain作為啟動類
- 解析配置文件zoo.cfg
- 創建并啟動歷史文件清理器DatadirCleanupManager
- 判斷當前是集群模式還是單機模式的啟動
- 再次進行配置文件zoo.cfg的解析
- 創建服務器實例ZooKeeperServer
初始化
初始化的步驟如下。
- 創建服務器統計器ServerStats
- 創建ZooKeeper數據管理器FileTxnSnapLog
- 設置服務器tickTime和會話超時時間限制
- 創建ServerCnxnFactory
- 初始化ServerCnxnFactory
- 啟動ServerCnxnFactory主線程
- 恢復本地數據
- 創建并啟動會話管理器
- 初始化ZooKeeper的請求處理鏈
- 注冊JMX服務
- 注冊ZooKeeper服務器實例
5.2 集群版服務器啟動
集群版和單機版ZooKeeper服務器啟動過程在很多地方是一致的,所以這里只會對有差異的地方展開進行講解。下圖是集群版ZooKeeper服務器的啟動流程圖。
預啟動
預啟動的步驟如下。
- 統一由QuorumPeerMain作為啟動類
- 解析配置文件zoo.cfg
- 創建并啟動歷史文件清理器DatadirCleanupManager
- 判斷當前是集群模式還是單機模式啟動
初始化
初始化的步驟如下
- 創建ServerCnxnFactory
- 初始化ServerCnxnFactory
- 創建ZooKeeper數據管理器FileTxnSnapLog
- 創建QuorumPeer
- 創建內存數據庫ZKDatabase
- 初始化QuorumPeer
- 恢復本地數據
- 啟動ServerCnxnFactory主線程
Leader選舉
Leader選舉的步驟如下
- 初始化Leader選舉
- 注冊JMX服務
- 監測當前服務器狀態
- Leader選舉
Leader和Follower啟動期交互過程
六、Leader選舉
6.1 Leader選舉概述
服務器啟動時期的Leader選舉
要進行Leader選舉的時候,隱式條件便是ZooKeeper的集群規模至少是2臺機器,只有一臺服務器啟動的時候,是無法進行Leader選舉的。
- 每個Server會發出一個投票
初始情況,對于Server1和Server2來說,都會投給自己,每次投票包含的最基本的元素包括:所推舉的服務器myid和ZXID。
- 接收來自各個服務器的投票
集群中每個服務器在收到投票后,首先會判斷投票的有效性,包含檢查是否是本輪投票,是否來自LOOKING狀態的服務器。
- 處理投票
在接收到來自其他服務器的投票后,針對每個投票,服務器都需要將別人的投票和自己的投票進行PK,PK的規則如下
- 優先檢查ZXID。ZXID比較大d服務器優先作為Leader
- 如果ZXID相同的話,那么就比較myid。myid比較大的服務器作為Leader服務器。
- 統計投票
每次投票后,服務器都會統計所有投票,判斷是否已經有過半機器接收到相同的投票信息。
- 改變服務器狀態
一旦確定了Leader,每個服務器就會更新自己的狀態:如果是Follower,那么就變更為FOLLOWING,如果是Leader,那么就變更為LEADING。
服務器運行期間的Leader選舉
在ZooKeeper集群正常運行過程中,一旦選出一個Leader,那么所有服務器的集群角色一般不會再發生變化,不管是是非Leader集群掛了還是新機器加入集群,都不會影響Leader。一旦Leader掛了,那么整個集群將暫時無法對外服務,而是進入新一輪的Leader選舉。
6.2 Leader選舉的算法分析
在ZooKeeper中,提供了三種Leader選舉的算法,分別是LeaderElection、UDP版本的FastLeaderElection和TCP版本的FastLeaderElection,可以通過在配置文件zoo.cfg中使用electionAlg屬性來指定,分別用數字0-3表示。0表示LeaderElection,1表示UDP版本的FastLeaderElection,并且是非授權模式,2表示UDP版本的FastLeaderElection,使用授權模式,3代表TCP版本的FastLeaderElection。從3.4.0版本開始,Zookeeper廢棄了0-2這三種算法,只保留了TCP版本的FastLeaderElection選舉算法。
術語解釋
- SID:服務器ID
- ZXID:事務ID
- Vote:投票
- Quorum:過半機器數
算法分析
進入Leader選舉
當ZooKeeper集群中的一臺服務器出現以下兩種情況時,就會開始進入Leader選舉
- 服務器初始化啟動
- 服務器運行期間無法和Leader保持連接
而當一臺機器進入Leadeader選舉流程時,當前集群也可能會處于以下兩種狀態
- 集群中本來就存在一個Leader
- 集群中確實不存在Leader
第一種情況,這種情況通常是某一臺服務器啟動比較晚,在他啟動之前,集群已經可以正常工作。針對這種情況,當該機器試圖去選舉Leader時,會被告知當前服務器的Leader信息,對于該機器來說,僅僅需要和Leader機器建立起連接,并進行狀態同步即可。
下面我們看看集群中不存在Leader的情況下,如何進行Leader選舉。
開始第一次投票
通常有兩種情況會導致集群中不存在Leader,一種是整個服務器剛剛初始化啟動時,另一種情況就是運行期間當前Leader所在的服務器掛了。此時,集群中所有機器都處于LOOKING的狀態。當一臺服務器處于LOOKING狀態時,那么他就會向集群中所有其他機器發送消息,我們稱這個消息為“投票”。
在這個投票消息中包含了兩個最基本的信息:所推舉的服務器SID和ZXID,用(SID,ZXID)表示。一般都是投自己。
變更投票
集群中每臺機器發出自己的投票后,也會接收到來自集群中其他機器的投票。每臺機器都會根據一定的規則,來處理收到的其他機器的投票,并以此來決定是否需要變更自己的投票。這個規則也成了整個Leader選舉算法的核心所在。我們首先定義一些術語。
- vote_sid:接收到的投票中所推舉Leader服務器的SID
- vote_zxid:接收到的投票中所推舉Leader服務器的ZXID
- self_sid:當前服務器自己的SID
- self_zxid:當前服務器自己的ZXID
對比過程如下:
- 規則1:如果vote_zxid>self_zxid,就認可當前收到投票,并再次將該投票發送出去。
- 規則2:如果vote_zxid<self_zxid,就堅持自己的投票,不作任何變更。
- 規則3:如果vote_zxid=self_zxid,就對比兩者的SID。如果vote_sid>self_sid,就認可當前收到的投票,并在此將該投票發出去。
- 規則4:如果vote_zxid=self_zxid,并且vote_sid<self_sid,那么同樣堅持自己的投票,不作變更。
確定Leader
經過這第二次投票后,集群中每臺機器都會再次受到其他機器的投票,然后開始統計投票。如果一臺機器收到了超過半數的相同的投票,那么這個投票對應的SID機器ji'wei即為Leader。
小結
通常哪臺服務器上的越新,那么越有可能成為Leader,原因很簡單,數據越新,ZXID越大,也就越能夠保證數據的恢復。
6.3 Leader選舉的實現細節
服務器狀態
- LOOKING
- FOLLOWING
- LEADING
- OBSERVING
投票數據結構
屬性 | 說明 |
---|---|
id | 被推舉的Leader的SID值 |
zxid | 被推舉的Leader的事務ID |
electionEpoch | 邏輯時鐘,用來判斷多個投票是否在同一輪選舉周期中。該值在服務端是一個自增序列。每次進入新一輪投票后,都會對該值進行加一 |
peerEpoch | 被推舉的Leader的epoch |
state | 當前服務器狀態 |
QuorumCnxManager:網絡I/O
每臺服務器啟動的時候,都會啟動一個QuorumCnxManager,負責各臺服務器之間的底層Leader選舉過程中的網絡通信。
消息隊列
在QuorumCnxManager這個類內部維護了一系列的隊列,用于保存接收到的、待發送的消息,以及消息的發送器。
- recvQueue:消息接收隊列
- queueSendMap:消息發送隊列,用于保存那些待發送的消息
- senderWorkerMap:發送器集合
- lastMessageSent:最近發送過的消息
建立連接
QuorumCnxManager在啟動的時候,會創建一個ServerSocket來監聽Leader選舉的通信接口(Leader選舉的通信端口默認是3888)。開啟端口監聽后,ZooKeeper就能夠不斷地接收到來自其他服務器的“創建連接”請求,在收到其他服務器的TCP連接請求時,會交由receiveConnection函數來處理。為了避免兩臺機器之間重復的創建TCP連接,ZooKeeper設計了建立TCP連接的規則:只允許SID大的服務器主動與其他服務器建立連接,否則斷開鏈接。
一旦建立起連接,就會根據遠程服務器的SID來創建相應的消息發送器SendWorker和消息接收器RecvWorker,并啟動他們。
消息接收與發送
消息的接收過程是由消息接收器RecvWorker來負責的。ZooKeeper會為每個遠程服務器分配一個單獨的RecvWorker,每個RecvWorker只需要不斷地從這個TCP連接中讀取消息,并將其保存到recvQueue隊列中。
消息發送過程也比較簡單,由于ZooKeeper同樣已經為每個遠程服務器單獨分別分配了消息發送器SendWorker,那么每個SendWorker只需要不斷地從對應的消息發送隊列中取出一個消息來發送即可,同時將這個消息放入lastMessageSent中來作為最近發送過的消息。
FastLeaderElection:選舉算法的核心部分
先約定幾個概念:
- 外部投票:其他服務器發來的投票
- 內部投票:自身當前的投票
- 選舉輪次:ZooKeeper服務器Leader選舉的輪次,即logicalclock
- PK:指對內部投票和外部投票進行一個對比來確定是否需要變更內部投票
選票管理
- sendqueue:選票發送隊列,用于保存待發送的選票
- recvqueue:選票接收隊列,用于保存接收到的外部投票
- WorkerReceiver:選票接收器。該接收器會不斷地從QuorumCnxManager中獲取其他服務器發來的選舉消息,并將其轉換成一個選票,然后保存到recvQueue隊列中去。在選票接收過程中,如果發現該外部投票的選舉輪次小于當前服務器,就直接忽略這個外部投票,同時立即發出自己的內部投票。當然,如果當前服務器并不是LOOKING狀態,即yi'j已經選出了Leader,那么也將忽略這個外部投票,同時將Leader信息已投票信息發送出去。另外,如果接收到的消息來自Observer服務器,那么就直接忽略掉,并將自己當前的投票發送出去。
- WorkerSender:選票發送器,會不斷從sendqueue隊列中獲取待發送的選票,并將其傳遞到底層QuorumCnxManager中去。
算法核心
七、各服務器角色介紹
7.1 Leader
Leader服務器是整個ZooKeeper集群工作機制的核心,其主要工作有以下兩個。
- 事務請求的唯一調度和處理者,保證集群事務處理的順序性。
- 集群內部各服務器的調度者。
7.2 Follower
Follower服務器是ZooKeeper集群狀態的跟隨者,主要工作
- 處理客戶端非事務請求,轉發事務請求給Leader服務器。
- 參與事務請求Proposal的投票
- 參與Leader選舉投票
7.3 Observer
工作原理與Follower基本一致,唯一區別在于Observer不參與任何形式的投票,包括事務請求Proposal的投票和Leader選舉投票。簡單的講,Observer服務器只提供非事務服務,通常用于在不影響集群事務處理能力的前提下提升集群的非事務處理能力。
7.4 集群間消息通信
ZooKeeper的消息類型大體上可以分為四類,分別是:數據同步型、服務器初始化型、請求處理型和會話管理型。
數據同步型
是指在Learner和Leader服務器進行數據同步的時候,網絡通信所用到的消息,通常有DIFF、TRUNC、SNAP和UPTPDATE四種。
服務器初始化型
是指在整個集群或是某些新機器初始化時,Leader和Learner之間相互通信所使用的消息類型,常見的有OBSEERVERINFO、FOLLOWERINFO、LEADERINFO、ACKEPOCH和NEWLEADER五種。
請求處理型
是指在進行請求處理的過程中,Leader和Learner服務器之間相互通信所使用的消息,常見的有REQUEST、PROPOSAL、ACK、COMMIT、INFORM和SYNC六種。
會話管理型
是指ZooKeeper在進行會話管理的過程中,和Learner服務器之間互相通信所使用的消息,常見的有PING和REVALIDATE兩種。
八、請求處理
8.1 會話創建請求
ZooKeeper服務端對于會話創建的處理,大體可以分為請求接收、會話創建、預處理、事務處理、事務應用和會話響應6大環節。
8.2 SetData請求
服務端對于SetData請求的處理,大體可以分為4大步驟,分別是請求的預處理、事務處理、事務應用和請求響應。
8.3 事務請求轉發
在事務請求的處理過程中,需要我們注意的一個細節是,為了保證事務請求被順序執行,從而確保ZooKeeper集群的數據一致性,所有的事務請求必須由Leader服務器來處理。但是,并不是所有的ZooKeeper都和Leader服務器保持連接,那么如何保證所有的事務請求都由Leader來處理呢?
ZooKeeper實現了非常特別的事務請求轉發機制:所有非Leader服務器如果接收到了來自客戶端的事務請求,那么必須將其轉發給Leader服務器來處理。
8.4 GetData請求
服務端對于GetData請求的處理,大體可以分為3大步驟,分別是請求的預處理、非事務處理和請求響應。
九、小結
ZooKeeper以樹作為其內存數據模型,樹上的每一個節點是最小的數據單元,即ZNode。ZNode具有不同的節點特性,同時每個節點都具有一個遞增的版本號,以此可以實現分布式數據的原子性更新。
ZooKeeper的序列化層使用從Hadoop中遺留下來的Jute組件,該組件并不是性能最好的序列化框架,但是在ZooKeeper中已經夠用。
ZooKeeper的客戶端和服務端之間會建立起TCP長連接來進行網絡通信,基于該TCP連接衍生出來的會話概念,是客戶端和服務端之間所有請求和響應交互的基石。在會話的生命周期中,會出現連接斷開、重連或是會話失效等一系列問題,這些都是ZooKeeper的會話管理器需要處理的問題--Leader服務器會負責管理每個會話的生命周期,包括會話的創建、心跳檢測和銷毀等。
在服務器啟動階段,會進行磁盤數據的恢復,完成數據恢復后就會進行Leader選舉。一旦選舉產生Leader服務器后,就立即開始進行集群間的數據同步--在整個過程中,ZooKeeper都處于不可用狀態,知道數據同步完畢(集群中絕大部分機器數據和Leader一致),ZooKeeper才可以對外提供正常服務。在運行期間,如果Leader服務器所在的機器掛掉或是和集群中絕大部分服務器斷開連接,那么就會觸發新一輪的Leader選舉。同樣,在新的Leader服務器選舉產生之前,ZooKeeper無法對外提供服務。
一個正常運行的ZooKeeper集群,其機器通常由Leader、Follower和Observer組成。ZooKeeper對于客戶端請求的處理,嚴格按照ZAB協議規范來進行。每個服務器在啟動初始化階段都會組裝一個請求處理鏈,Leader服務器能夠處理所有類型的客戶端請求,而對于Follower或是Observer服務器來說,可以正常處理非事務請求,而事務請求則需要轉發給Leader服務器來處理,同時,對于每個事務請求,Leader都會為其分配一個全局唯一且遞增的ZXID,以此來保證事務處理的順序性。在事務請求的處理過程中,Leader和Follower服務器都會進行事務日志的記錄。
ZooKeeper通過JDK的File接口簡單實現了自己的數據存儲系統,其底層數據存儲包括事務日志和快照數據兩部分,這些都是ZooKeeper實現數據一致性非常關鍵的部分。