zookeeper

zookeeper的簡介

Apeche Zookeeper是由Apache Hadoop的子項目,起源於雅虎研究院的一個研究小組開發的一個無單點問題的分佈式協同服務系統,這就是zookeeper,主要提供:統一命名服務、組成員管理、配置管理和分佈式鎖等分佈式的基礎服務(分佈式應用程序可以基於zookeeper實現諸如數據發佈和訂閱、負載均衡、命名服務、分佈式協調和通知、集羣管理、Master選舉,分佈式鎖和分佈式隊列等功能) ,是Goole Chubby的開源實現,將複雜而容易出錯的分佈式一致性服務封裝起來,構成一個高效可靠的原語集,並提供簡單的接口供用戶使用

 

zookeeper的基本使用

zookeeper服務

應用使用zookeeper客戶端庫來使用zookeeper服務,zookeeper客戶端負責和zookeeper集羣交互,事實上就是一個client-server的架構,zookeeper集羣有兩種模式:standalone模式和quorum模式,處於standalone模式的zookeeper集羣中只有一個獨立運行的zookeeper節點,處於quorum模式的zookeeper集羣包含多個zookeeper節點,通常一個zookeeper集羣通常有一組機器組成,一般3-5臺機器就可以組成一個可用的zookeeper集羣,組成zookeeper集羣的每臺機器都會在內存維護當前服務器的狀態,並且每臺機器之間都保持着通信,只要有超過一半的機器能夠正常工作,整個zookeeper集羣就能正常對外提供服務,zookeeper的客戶端會選擇與集羣中的任意一個機器創建一個session(TCP連接),而一旦客戶端和某臺zookeeper服務器之間斷開連接後,客戶端會自動連接到集羣中的其他機器。具體如下:

zookeeper中的基本概念

集羣角色

在分佈式系統中,構建集羣的每個機器都有自己的角色,典型的集羣模式就是Master/Slave(主備模式),Master節點操作寫請求 ,然後通過異步複製的方式同步給Slave,Slave節點提供讀服務,在zookeeper中,沒有采用傳統的主備模式,而是引入了Leader、Follower和Observer三種角色,zookeeper集羣中所有機器通過Leader選舉過程來選定一臺Leader節點。

Leader:Leader作爲整個ZooKeeper集羣的主節點,負責響應所有對ZooKeeper狀態變更的請求(寫請求)。它會將每個狀態更新請求進行排序和編號,以便保證整個集羣內部消息處理的FIFO,寫操作都走leader,先到達leader的寫請求會先被處理,leader決定寫請求的執行順序。

Follower:除了響應本服務器上的讀請求外,follower還要處理leader的提議,並在leader提交該提議時在本地也進行提交。follower在接到寫請求時會把寫請求轉發給Leader來處理,另外需要注意的是,leader和follower構成ZooKeeper集羣的法定人數,也就是說,只有他們才參與新leader的選舉、響應leader的提議。 

Observer:如果ZooKeeper集羣的讀取負載很高,或者客戶端多到跨機房,可以設置一些observer服務器,以提高讀取的吞吐量。是特殊的“Follower”,其可以接收客戶端 reader 請求,但不參與選舉。(擴容系統支撐能力,提高讀取速度)因爲他不接受任何同步的寫入請求,只負責 leader 同步數據;其次是observer不需要將事務持久化到磁盤,一旦observer被重啓,需要從leader重新同步整個名字空間。

Session 會話

Session是指客戶端會話,在zookeeper中,一個客戶端連接是指客戶端和服務器之間的一個TCP長連接,zookeeper對外的服務端口默認是2181,客戶端在啓動的時候,首先會與服務器建立一個TCP連接,從第一次連接建立開始,客戶端會話的生命週期開始,通過這個連接,客戶端能夠通過心跳檢測與服務器保持有效的會話,也能夠向zookeeper服務器發送請求並接受響應,還可以接收來自服務器的watch事件通知,其中Session Timeout這個值用來設置客戶端會話的超時時間,當由於服務器壓力太大,網絡故障或是客戶端主動斷開連接等各種原因導致客戶端連接斷開時,只要在session Timeout規定的時間內重新連接上集羣,這個會話依然有效

數據模型

ZooKeeper 的數據模型是層次模型(Google Chubby 也是這麼做的)。層次模型常見於文件系統。層次模型和 key-value 模型是兩種主流的數據模型。ZooKeeper 的層次模型稱作 znode tree。由斜槓(/)進行分割的路徑,znode tree 的每個節點叫作數據節點- znode,每個znode都可以都會保存自己的數據內容,同時還會保存一些列屬性信息,zookeeper中所有數據都是存儲在內存中,每個節點都有一個版本(version)。版本從 0 開始。

在zookeeper中,znode可以分爲持久節點和臨時節點兩類 ,持久節點指的是一旦這個znode被創建了,除非主動進行znode刪除,否則一直保存在zookeeper上,而臨時節點的生命週期與客戶端的會話綁定,一旦客戶端會話失效,這個客戶端所創建的所有臨時節點將會倍一處,除此之外,每個節點還可以添加一個特殊的屬性:SEQUENTIAL,一旦一個節點標記上這個屬性,那麼這個節點在創建的時候,zookeeper會自動在節點後面追加一個整型數字,這個整型數字是一個由父節點維護的單調自增數字,所以在zookeeper中一個節點可以分爲以下:

1. 久性的 znode (PERSISTENT): ZooKeeper 宕機,或者 client 宕機,這個 znode 一旦創建就不會丟失。只能通過調用delete來進行刪除

2. 臨時性的 znode (EPHEMERAL): 當創建該節點的客戶端崩潰或關閉了與 ZooKeeper的連接時,或者 client 在指定的 timeout 時間內沒有連接 server ,這個節點就會被刪除(臨時節點不允許有子節點)

3. 持久順序性的 znode(PERSISTENT_SEQUENTIAL): znode 除了具備持久性 znode 的特點之外,znode 的名字具備順序性。

4. 臨時順序性的 znode(EPHEMERAL_SEQUENTIAL): znode 除了具備臨時性 znode 的特點之外,znode 的名字具備順序性。

znode一共有以上4種類型:持久的(persistent)、臨時的 (ephemeral)、持久有序的(persistent_sequential)和臨時有序的 (ephemeral_sequential)

狀態信息

通過zookeeper上的數據節點進行數據的寫入和子節點的創建,事實上,每個數據節點除了存儲了數據內容之外,還存儲了數據節點的一些狀態信息,可以通過ls2獲取節點,通過ls2命令顯示的就是節點的狀態信息,這其實就數據節點的Stat對象的格式化輸出,如果就是zookeeper中的Stat類的數據結構

Stat類包含了zookeeper上一個數據節點的所有狀態信息,包括 事務ID、版本信息和子節點個數,如下:

狀態屬性

說明

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

數據版本

 zookeeper在每個znode上都會維護一個叫做Stat的數據結構,Stat中記錄了這個znode的三個數據版本,分別是version(當前znode的版本),cversion(當前znode的子節點的版本)以及aversion(當前znode的ACL版本),在zookeeper中,version屬性是實現的樂觀鎖機制,在zookeeper服務器的PrepRequestProcessor處理器類中,在處理每個數據更新(setDataRequest)請求時,會進行版本檢驗,如下:

version = setDataRequest.getVersion();
            currentVersion = nodeRecord.stat.getVersion();
            if (version != -1 && version != currentVersion) {
                throw new BadVersionException(path);
            }
            version = currentVersion + 1;
               

從上面可以看出,在進行一次setDataRequest請求處理時,首先進行了版本檢查:zookeeper會從setDataRequest請求中獲取當前版本的version,如果version爲-1就說明客戶端不要求使用樂觀鎖,如果不是-1,且版本不對,則拋出BadVersionException

 

Watcher

Watcher是事件監聽器,是zookeeper的一個重要特性,zookeeper允許用戶在指定節點上註冊一些Watcher,並且在一些特定的事件觸發的時候,zookeeper服務端會將事件通知到客戶端上,另外watcher的通知是一次性的,即一旦觸發了一次通知後,Watcher就會失效,因此客戶端需要反覆的註冊Watcher。

zookeeper的Watcher機制主要包括了客戶端線程、客戶端WatcherManager和zookeeper服務器,具體的工作流程:客戶端在向zookeeper服務器註冊Watcher的同時,會將Watcher對象存儲在客戶端的WatcherManager中。當zookeeper服務器端觸發Watcher事件後,會向客戶端發送通知,客戶端線程從WatcherManager中取出對應的Watcher對象執行回調邏輯。

Watcher接口

在zookeeper中,接口類Watcher用於處理標準的事件處理器,定義事件通知相關邏輯,包含了KeeperState枚舉類代表通知狀態

和EventType代表事件類型的枚舉,同時定義了事件的回調方法:process(WatcherEvent event)

Watcher事件

同一個事件類型在不同的通知狀態代表的含義不同,具體如下:

此前提到NodeDataChanged事件中節點的數據內容和數據的版本號dataversion,因此即使相同的數據內容來更新還是會觸發這個通知,因爲對於zookeeper來說,無論數據內容是否變更,一旦有客戶端調用數據更新的接口去,且更新成功就會更新dataversion值;NodeChildrenChanged事,件會在數據節點的子節點變更時觸發,即新增或者刪除子節點,子節點內容變化不會會觸發,對於AuthFailed這個事件,觸發條件不是因爲當前客戶端會話沒有權限而是授權失敗,如下:

ZooKeeper zooKeeper = new ZooKeeper("localhost:2181", 5000, null);
        zooKeeper.addAuthInfo("digest","root:123".getBytes());
        List acls = new ArrayList();
        zooKeeper.create("/zookeeper", "".getBytes(), acls, CreateMode.EPHEMERAL);

        /*使用正確的shcema:digest,使用錯誤的Auth*/
        ZooKeeper zooKeeper_error = new ZooKeeper("localhost:2181", 5000, null);
        zooKeeper_error.addAuthInfo("digest","abc:123".getBytes());
        zooKeeper_error.getData("/zookeeper", true,null);

        /*使用錯誤的shcema:digest2,使用錯誤的Auth*/
        ZooKeeper zooKeeper_error2 = new ZooKeeper("localhost:2181", 5000, null);
        zooKeeper_error2.addAuthInfo("digest2","abc:123".getBytes());
        zooKeeper_error2.getData("/zookeeper", true,null);

以上2個程序都使用了錯誤的Auth,root:123,運行第一個程序,會拋出NoAuthExeception,而第二個程序運行後拋出AuthFailedException異常,同時會收到對應的Watcher事件通知:(AuthFailed,Node)

回調方法process()

process方法是Watcher接口中的一個回調方法,當zookeeper想客戶端發送一個watcher事件通知時,客戶端就會相應的process方法進行回調,從而實現對事件的處理

public interface Watcher {
    void process(WatchedEvent var1);
}

public class WatchedEvent {
    private final KeeperState keeperState;
    private final EventType eventType;
    private String path;
}

其中WatcherEvent包含了每一個事件的三個基本屬性:通知狀態(KeeperState)、事件類型(eventType)和節點路徑(path)

zookeeper使用WatchedEvent對象用來封裝服務端事件並傳遞給Watcher,從而方便的回調方法process對服務端事件進行處理

除了WatchedEvent,還有一個WatcherEvent實體,籠統的說兩者表示的是同一個事物,都是對一個服務端事件的封裝,不同的是WatchedEvent是一個邏輯事件,用於服務端和客戶端程序執行過程中所需的邏輯對象,而WatcherEvent因爲實現了序列化接口,可以用於網絡傳輸,如下:

public class WatcherEvent implements Record {
    private int type;
    private int state;
    private String path;
}

服務端在生成WatchedEvent事件之後,會調用getWrapper方法將自己包裝成可序列化的WatcherEvent事件,以便通過網絡傳輸到客戶端,客戶端接收到服務端的事件對象後,首先會將WatcherEvent事件還原成一個WatchedEvent事件並傳遞給process方法處理。無論是WatchedEvent還是WatcherEvent,zookeeper封裝都是簡單的,服務端會發送給客戶端一個“ZNode數據內容變”事件客戶端只能夠接收到如下信息:

KeeperState:syncConnected

EventType:NodeDataChanged

Path:/zookeeper

Watcher的工作機制

zookeeper的Watcher機制,總的來說可以概括以下三個過程:

  • 客戶端註冊Watcher
  • 服務端處理Watcher
  • 客戶端回調Watcher

客戶端註冊Watcher

在創建一個Zookeeper客戶端對象實例的時候,可以向構造方法中傳入要給默認的Wathcer:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

這個Watcher將作爲整個Zookeeper會話期間的默認Watcher,會一直保存在客戶端ZKWatchManager的defaultWatcher中,另外Zookeeper客戶端也可以通過getData、getChildren和exist三個接口來向Zookeeper服務器註冊Watcher,三者註冊Watcher的工作原理是一致的,以getData接口爲例:

public void getData(String path, Watcher watcher, DataCallback cb, Object ctx)

public void getData(String path, boolean watch, DataCallback cb, Object ctx)

第二個接口的boolean參數來標識是否使用上文中提到的默認Watcher來進行註冊,註冊邏輯和第一個接口是一致的                                                                                                            

Watcher特性總結

一次性:無論是客戶端還是服務端,一旦一個Watcher被觸發,Zookeeper都會將其從相應的存儲中刪除,因此Watcher的使用需要反覆註冊,這樣的設計有效減輕服務端的壓力

客戶端串行執行:客戶端Watcher回調的過程是一個串行同步的過程,保證有序,注意不要因爲一個Watcher的處理邏輯影響了整個客戶端的Watcher回調。

輕量:WatchedEvent是zookeeper整個Watcher通知機制的最小通知單元,這個數據結構只有三部分內容:通知狀態,事件類型和節點路徑,而沒有事件的具體內容,客戶端需要主動去獲取事件的變更內容,此外客戶端向服務端註冊Watcher的時候,並不會把真實的Watcher對象傳遞到服務器,僅僅是在客戶端請求中使用boolean類型屬性進行標記,同時服務端僅僅保存了當前連接的ServerCnxn對象。

 

zookeeper的ACL

zookeeper提供了一套完善的ACL(Access Control List)權限控制機制來保障數據的安全,權限控制中,如Unix/Linux文件系統中,使用的是目前最廣泛的權限控制方式-UGO(User,Group,Other)權限控制機制,簡單的說UGO是針對一個文件和目錄,對創建者(User)、創建者所在的組(Group)、和其他用戶(Other)分別配置不同的權限,是一種粗粒度的文件系統權限控制模式,UGO無法做到同組的不同用戶擁有不同的權限

ACL是訪問控制列表,是一種粒度較細的權限管理方式,可以針對任意用戶和組進行細粒度的權限控制,Linux從2.6支持這個特性,zookeeper的ACL和Unix/Linux的ACL有一些區別,主要從三個方面描述ACL機制:

  • 權限模式Schema
  • 授權對象ID
  • 權限Permission

權限模式

用來確定權限驗證過程中使用的檢驗策略,在zookeeper中最多使用的是以下四種權限策略:

IP

IP模式通過IP地址粒度來進行權限控制,如配置"ip:192.160.30.10",則表示權限控制室針對這個IP地址的,同時也支持按照網段的方式進行配置,如"ip:192.160.30.1/24"表示192.160.30.*這個IP段進行權限控制

Digest

是最常用的權限控制模式,類似:"userName:password"形式的權限標識進行權限配置,便於區分不同應用,當我們通過"userName:password"形式標識後,zookeeper會對其先後進行兩次編碼處理,分別是SHA-1算法加密和BASE64編碼,具體實現由:DigestAuthenticationProvider.generateDigest(String idPassword)函數進行封裝,如下

public static void main(String[] args) throws Exception{
    System.out.println(DigestAuthenticationProvider.generateDigest("root:abc123"));
}

允許結果:"root:VVSTY9k7e0y1gfT9P5MZ33kTrlA=",可以看出"root:abc1234 "最終變成一個無法辨認的字符串了

World

World是一種最開放的權限控制模式,數據節點的訪問權限對所有用戶開放,即所有用戶都可以在不進行任何權限校驗的情況下操作zookeeper上的數據,world模式可以看成一種特殊的Digest模式,它只有一個權限標識即"world:anyone"

Super

Super模式就是超級用戶的意思,也是一種特殊的Digest模式,在Super模式下,超級用戶可以對任意Zookeeper上的數據節點進行任何操作,存在以下這種情況,如果一個持久數據節點包含了ACL權限控制,而其創建者客戶端已經退出或者不再使用,這些數據的清理就需要ACL模式的超級管理員來處理,要使用超級管理員權限,首先需要在zookeeper服務器上開啓Super模式,方法是在zookeeper服務器啓動的時候,添加如下系統屬性:

-Dzookeeper.DigestAuthenticationProvider.superDigest=root:VVSTY9k7e0y1gfT9P5MZ33kTrlA=

完成對zookeeper服務器的Super模式開啓後,就可以在應用程序上使用了。

授權對象ID

授權對象指的是權限賦予的用戶或一個指定實體,例如IP地址或是機器等,在不同的權限模式下,授權對象是不同的,關係如下:

權限模式 授權對象
IP 通常是一個IP地址或者IP段
Digest 自定義,通常是“userName:Base64(SHA-1(userName:password))”
World 只有一個ID:“anyone“
Super 與Digest一致

權限:Permission

權限就是指那些通過權限檢查後背允許運行的操作,在zookeeper中所有對數據的操作權限分爲以下五大類:

  • CREATE: 允許授權對象在該數據節點下創建子節點
  • READ:允許授權對象訪問數據節點並讀取其數據內容和列出其子節點
  • WRITE:允許授權對象對數據節點進行更新操作
  • DELETE: 允許授權對象刪除該數據節點的子節點
  • ADMIN: 允許授權對象對數據節點進行ACL相關設置操作

 

設置ACL

通過zkCli腳本登陸zookeeper服務器後,可以通過兩種方式設置ACL,一種是在數據節點創建的同時進行ACL權限設置,命令如下:

create [-s] [-e] path data acl

另外一種是通過使用setACL命令針對已經存在的數據節點進行ACL設置

setAcl path acl

自定義權限控制器

實現自定義權限控制器非常簡單,zookeeper定義了一個標準權限控制器需要實現的接口:

org.apache.zookeeper.server.auth.DigestAuthenticationProvider
public class DigestAuthenticationProvider implements AuthenticationProvider {

    public String getScheme() { 
    } 

    public boolean isAuthenticated() {   
    }

    public boolean isValid(String id) {  
    }

    public boolean matches(String id, String aclExpr) {
       
    }
}

用戶可以基於該接口實現自定義的權限控制器,而上述的幾個權限模式隊友的就是zookeeper自帶的DigestAuthenticationProvider和IPAuthenticationProvider兩個權限控制器

註冊自定義權限控制器

zookeeper支持通過系統屬性和配置文件兩種方式來註冊自定義權限控制器:

系統屬性-Dzookeeper.authProvider.X

即在zookeeper啓動參數中添加如下的系統屬性:-Dzookeeper.authProvider.1=com.zkbook.MyAuthenticationProvider

配置文件方式

在zoo.cfg配置文件中添加如下配置項:

authProvider.1=com.zkbook.MyAuthenticationProvider

對於權限控制器的註冊,zookeeper採用了延遲加載的策略,即只有在第一次處理包含權限控制的客戶端請求時,纔會進行權限控制的初始化,同時,zookeeper還會將所有的權限控制器都註冊到ProviderR egistry中去,在具體實現中,zookeeper首先會將DigestAuthenticationProvider和IPAuthenticationProvider這兩個控制器初始化,然後通過掃描zookeeper.authProvider. 這一屬性獲取到用戶配置的自定義權限控制器,完成初始化

 

zookeeper的ZAB協議

在zookeeper中沒有完全採用Paxos算法,而是使用了一種稱爲Zookeeper Atomic Broadcast(ZAB,zookeeper原子消息廣播協議)的協議作爲其數據一致性的核心算法,ZAB協議是爲分佈式協調服務zookeeper專門設計的一種支持崩潰可恢復的原子廣播協議,基於該協議,zookeeper實現了一種主備模式的系統架構來保持集羣中個副本之間數據的一致性,zookeeper使用一個單一的主進程來接收並處理客戶端的所有事務請求,並採用ZAB的原子廣播協議,將服務器數據的狀態變更以事務Proposal的形式廣播到所有的副本進程上去。

ZAB協議的核心就是定義了對於那些會改變zookeeper服務器數據狀態的事務請求的處理方式,即:

所有事務請求必須有一個全局唯一的服務器來協調處理,這樣的服務器被稱爲Leader服務器,而剩下的其他服務器組成Follower服務器,Leader服務器負責將一個客戶端事務請求轉換成一個事務提議(Proposal),並將該Proposal分發給集羣中所有的Follower服務器,並等待所有的Follower的響應,一旦超過半數的Follower服務器進行了正確的響應,那麼Leader就會再次向所有的Follower發起Commit消息,要求將前一個Proposal進行提交

ZAB協議包括兩種基本的過程(模式):崩潰恢復和消息廣播,當整個服務框架啓動過程中,或是當Leader服務器出現網絡終端、崩潰推出或者重啓等異常情況,ZAB協議就會進入恢復模式並選舉出新的Leader服務器,當選舉產生新的Leader服務器,同時集羣中有過半的機器與該Leader服務器完成了狀態同步之後,ZAB協議就會退出恢復模式,其中狀態同步是指數據同步,用來保證集羣中存在過半機器能夠和Leader服務器的數據狀態保持一致。

當集羣中已經有了過半的Follower服務器完成了和Leader的狀態同步,那麼整個服務框架就可以進入消息廣播模式,當一臺同樣遵守ZAB協議的服務器啓動後加入到集羣中,如果此時集羣中已經存在一個Leader服務器在負責進行消息廣播,那麼新加入的服務器就會自覺地進入數據恢復模式:找到Leader所在的服務器,並與其進行數據同步,然後一起參與到消息廣播流程中去,因爲在zookeeper只能有一個Leader服務器來處理事務請求,所以當Leader服務器接收到客戶端的事務請求後,會生成對應的事務提案併發起一輪廣播協議,而如果是集羣中的其他機器收到客戶端的事務請求,那麼這些非Leader服務器會首先將這個事務請求轉發給Leader服務器

消息廣播

ZAB協議的消息廣播過程使用的是一個原子廣播協議,類似於一個二階段提交的過程,針對客戶端的事務請求,Leader服務器會爲其自動生成對應的事務Proposal,並將其發送給集羣中的其他機器,然後會收集各自的選票,最後進行事務提交,與二階段提交的區別是移除了中斷邏輯,所有的Follower服務器要麼正常反饋Leader提出的事務Proposal,要麼就拋棄Leader服務器,同時,ZAB協議可以在過半的Follower響應反饋後就提交事務Proposal,而不需要等待集羣所有的Follower服務器都反饋響應,整個消息廣播協議是基於具有FIFO特性的TCP協議來進行網絡通信的,因此能夠很容易的保證消息廣播過程中消息的接收和發送的順序性,Leader服務器會爲每個事務請求生成對應的Proposal來進行廣播,並且在廣播事務Proposal之前,Leader服務器會首先爲這個事務Proposal分配一個全局單遞增的唯一ID,稱爲事務ID(ZXID),由於ZAB協議需要保證每個消息的嚴格因果關係,所以必須將每個事務Proposal按照其ZXID的先後順序來進行排序處理,具體的是在消息廣播過程中,Leader服務器會爲每個Follower服務器都各自分配一個單獨的隊列,然後將需要廣播的事務Proposal依次放到這些隊列中去,並且根據FIFO策略進行消息發送,每個Follower接收到事務Proposal之後,首先會將其以事務日誌的形式寫到本地磁盤中去,並且在寫入成功後反饋給Leader服務器一個ACK響應,當Leader服務器接收到過半的Follower的ACK響應後,就會廣播一個Commit消息給所有的Follower服務器通知其進行事務提交,同時Leader自身也會完成對事務的提交,而每個Follower在接收到Commit之後也會完成對事務的提交

崩潰恢復

當Leader服務器出現崩潰或者重啓,亦或是集羣中Leader服務器失去與過半的Follower失去聯繫,那麼在重新開始新的一輪原子廣播事務操作之前,所有的進程首先會使用崩潰恢復協議來達到一個一致的狀態,那麼就會進入崩潰恢復模式,在ZAB協議中爲了保證程序的正確運行,整個恢復過程結束後需要選舉出一個新的Leader服務器,在崩潰恢復過程中爲了保證數據的一致性需要做到以下:

  1. ZAB協議需要確保那些已經在Leader服務器上提交的事務最終被所有服務器都提交(假如一個事務在Leader服務器上被提交了,並且得到了過半的Follow服務器的ACK反饋,但是在它將Commit消息發送給所有Follower機器之前,Leader服務器掛了,ZAB協議能夠確保這個事務最終被所有服務器都提交成功,否則將出現不一致)
  2. 如果在崩潰恢復過程中出現一個需要被丟棄的提案,那麼在崩潰恢復結束後需要跳過該事務Proposal(假如初始的Leader服務器提出一個事務就崩潰退出,從而導致集羣中其他的服務器都沒有收到這個事務的Proposal,於是當Leader服務器恢復過來再次加入集羣,ZAB協議需要保證丟棄這個事務)

Leader選舉算法:爲了確保滿足以上:提交已經被Leader提交的事務Proposal,同時丟棄已經被跳過的事務Proposal,只需要保證Leader選舉算法選舉出來的Leader服務器擁有集羣中所有機器最高編號(即ZXID最大)的事務Proposal,就可以保證這個心選舉出來的Leader一定具有所有已經提交的提案,同時也省去了Leader服務器檢查Proposal的提交和丟棄工作。

數據同步:完成Leader選舉,正式工作(接收客戶端事務請求,然後提出新的提案)之前,Leader服務器首先確認事務日誌中所有的Proposal是否都已經被集羣中過半的機器提交了,即是否完成數據同步。

數據同步詳情:Leader服務器會爲每個Follower服務器都準備一個隊列,並將那些沒有被各Follower服務器同步的事務以Proposal消息的形式逐個發送給Follower服務器,並在每個Proposal消息後緊接着發送一個Commit消息,表示該事務已經被提交,等到Follower服務器將所有其尚未同步的事務Proposal都從Leader服務器上同步過來併成功應用到本地數據庫後,Leader服務器就會將該Follower服務器加入到真正可用的Follower列表中,開始其他流程。

ZAB協議中的事務編號ZXID是一個64位的數字,其中低32位可以看作是一個簡單的單調遞增的計數器,針對客戶端的每一個事務請求,Leader服務器在產生一個新的事務Proposal的時候,都會針對該計數器進行加1,而高32位則代表了Leader週期的epoch號,每當選舉出新的Leader服務器,就會從這個Leader服務器上取出其本地日誌中最大事務Proposal的ZXID,並從該ZXID中解析出對應的epoch值,然後在加1,之後以此編號作爲新的epoch,並將低32位0來開始生成新的ZXID,以此來通過epoch編號區分Leader週期的變化,避免不同的Leader服務器錯誤的使用相同的ZXID提出不同的事務Proposal的異常情況。

基於這樣的策略,當一個包含上一個Leader週期中尚未提交過的事務Proposal的服務器啓動時,其肯定無法成爲Leader,因爲當前集羣中肯定包含一個Quorum集合,該集合中的機器包含了更高的epoch的事務Proposal,因此這臺機器的事務Proposal肯定不是最高的,而且無法成爲Leader,當這臺機器加入集羣中,以Follower角色連接到Leader之後,Laeder服務器會根據自己服務器上最後被提交的Proposal來和Follower服務器的Proposal進行對比,從而Leader會要求Follower進行回退到一個確實已經被集羣過半機器提交的最新的事務Proposal

zookeeper的搭建

安裝Zookeeper及使用zookeeper命令

直接使用docker 安裝zookeeper的單機版,啓動容器後(zookeeper服務),進入容器,運行客戶端服務./zkCli.sh(Zookeeper的一個簡易客戶端),連接服務端成功,若連接不同的主機,可使用如下命令:./zkCli.sh -server ip:port,可以使用幫助命令help來查看客戶端的操作

客戶端嘗試連接到客戶端發送的連接串 localhost/127.0.0.1:2181中的一個服務器。隨後一個確認信息說明客戶端與本地的 ZooKeeper服務器建立了TCP連接。後面的日誌信息確認了會話的建 立,並告訴我們會話ID爲:0x13b6fe376cd0000。最後客戶端庫通過 SyncConncted事件通知了應用。應用需要實現Watcher對象來處理這個

JLine support is enabled

2019-09-15 09:34:04,848 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@879] - Socket connection established to localhost/127.0.0.1:2181, initiating session

[zk: localhost:2181(CONNECTING) 0] 2019-09-15 09:34:04,993 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x101178770b40008, negotiated timeout = 30000

 

 

命令操作演示:

創建節點:使用create命令,可以創建一個Zookeeper節點, 如

create [-s] [-e] path data acl

其中,-s或-e分別指定節點特性,順序或臨時節點,若不指定,則表示持久節點;acl用來進行權限控制。

  1. 創建持久節點,不指定參數,默認是持久的

[zk: localhost:2181(CONNECTED) 3] create /app1 1

Created /app1

  1. 創建持久順序節點

使用 create -s /app2 2 命令創建app2順序節點,可以看到創建的app2節點後面添加了一串數字以示區別。

[zk: localhost:2181(CONNECTED) 6] create -s /app2 2

Created /app20000000001

  1. 創建臨時節點

[zk: localhost:2181(CONNECTED) 8] create -e /app3 3

Created /app3

 

 

  1. 創建臨時順序節點:-s -e 指定即可

ls / 查看所有節點:

[zk: localhost:2181(CONNECTED) 9] ls /

[app1, app20000000001, app3, zookeeper]

使用quit退出客戶端,在使用zkCli連接到zk服務,再次查看所有節點,發現app3這個臨時節點沒有了

[zk: localhost:2181(CONNECTED) 0] ls /

[app1, app20000000001, zookeeper]

 

讀取節點:與讀取相關的命令有ls 命令和get 命令,ls命令可以列出Zookeeper指定節點下的所有子節點,只能查看指定節點下的第一級的所有子節點;get命令可以獲取Zookeeper指定節點的數據內容和屬性信息。其用法分別如下

ls path [watch]:查看path下所有子節點

get path [watch]:獲取path節點保存的數據

ls2 path [watch]:

若獲取根節點下面的所有子節點,使用ls / 命令即可,前面演示過了

[zk: localhost:2181(CONNECTED) 1] ls /

[app1, app20000000001, zookeeper]

[zk: localhost:2181(CONNECTED) 2] get /app1

1

[zk: localhost:2181(CONNECTED) 3] ls2 /app20000000001

'ls2' has been deprecated. Please use 'ls [-s] path' instead.

[]

cZxid = 0x3

ctime = Wed Sep 11 02:39:15 UTC 2019

mZxid = 0x3

mtime = Wed Sep 11 02:39:15 UTC 2019

pZxid = 0x3

cversion = 0

dataVersion = 0

aclVersion = 0

ephemeralOwner = 0x0

dataLength = 1

numChildren = 0

[zk: localhost:2181(CONNECTED) 4]

Ls2命令主要用來獲取根節點數據內容和屬性信息(已經過期推薦使用ls -s來替

 

java客戶端API的使用

zookeeper作爲分佈式服務框架,提供簡單的分佈式原語,並對多種語言提供了API,下面介紹java客戶端的API的使用方式

創建會話

客戶端可以通過一個Zookeeper(org.apache.zookeeper)實例來連接zookeeper服務器,Zookeeper的4種構造方法如下:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly)

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd)

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)

參數說明:

參數名  
connectString 指zookeeper服務器列表,由host:port字符串組成,每一個都代表一個zookeeper機器,如:192.168.1.110:2181,192.168.1.111:2182,192.168.1.112:2183,代表爲客戶端指定三臺服務器的地址,另外也可以設置連接上zookeeper後的根目錄,如:host:port/zookeeper,指定連接服務器後所有對zookeeper操作的都是基於這個/zookeeper目錄
sessionTimeout 會話超時時間,以“毫秒”爲單位的整型值,在一個會話週期內zookeeper客戶端和服務端之間會通過心跳檢測機制來維持會話的有效性,一旦在sessionTimeout時間內沒有進行心跳檢測,會話失效
watcher Zookeeper允許客戶端在構造方法中傳入一個接口Watcher(org.apache.zookeeper.Watcher)的實現類對象作爲默認的Watcher事件通知處理器,當然可以設置爲null,表示不需要默認的Watcher處理器
canBeReadOnly 標識當前會話是否支持“read-only”,默認情況下,zookeeper集羣中一個機器如果和集羣中過半及以上機器失去網絡連接,那麼這個機器將不再處理客戶端請求(包括讀寫),但是在某些場景下,當zookeeper發生故障,我們希望zookeeper服務器能夠提供讀服務
sessionId和sessionPasswd

代表會話ID和會話密鑰i,這2個參數唯一確定一個會話,同時客戶端使用這2個參數可以實現客戶端的會話複用,已達到恢復會話的效果,當第一次連接上Zookeeper服務器時,通過調用Zookeeper對象實例的兩個接口ukeyi獲取當前會話ID和密鑰

long getSessionId();

byte [] getSessionPasswd();

下次創建Zookeeper對象時傳入即可

注意:Zookeeper客戶端和服務端會話的建立時一個異步的過程,構造方法會在處理完客戶端初始化工作後立即返回,大多數情況下此時並沒有真正建立好一個可用的會話,此時處於會話生命週期的"CONNECTING"的狀態,當會話真正創建完,Zookeeper服務端會向客戶端發送一個事件通知,此時纔算真正建立了會話,demo如下:在接受到服務端發來的SyncConnected事件之後,解除主程序的等待阻塞。

static CountDownLatch latch = new CountDownLatch(1);
    public static void main(String[] args) throws Exception{
        ZooKeeper zooKeeper = new ZooKeeper("localhost:2181", 5000, new Watcher() {
            public void process(WatchedEvent event) {
                if(event.getState() == Event.KeeperState.SyncConnected){
                    latch.countDown();
                }
            }
        });
        System.out.println(zooKeeper.getState());
        try{
            latch.await();
        }catch (Exception e){

        }
    }

創建節點

客戶端使用Zookeeper的API來創建一個數據節點,有如下兩個接口:

public String create(String path, byte[] data, List<ACL> acl, CreateMode createMode) 

public void create(String path, byte[] data, List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)

 

參數說明:

參數名 說明
path 需要創建的數據節點的節點路徑
data[] 節點創建後的初始內容,節點內容只支持字節數組,zookeeper不負責節點內容的序列化
acl 節點的acl策略
createMode

節點類型,是一個枚舉類型,通常有4種節點類型

  • 持久(PERSISTENT)
  • 持久有序(PERSOSTENT_SEQUENTIAL)
  • 臨時(EPHEMERAL)
  • 臨時有序(EPHEMERAL_SEQUENTIAL)
cb

註冊一個異步回調函數,需要實現StringCallback接口,重寫void processResult(int rc,String path,Object ctx,String name)

當服務端節點創建完畢後,zookeeper客戶端會自動調用這個方法,處理相關業務邏輯

ctx 用於傳遞一個對象,可以在上面的回調方法執行的時候使用,通常放一個上下文(Context)信息

可以看出上面2個創建節點的方法,一個是同步一個是異步的,無論同步還是異步,zookeeper都不支持遞歸創建,即無法在父節點不存在的情況下創建子節點,另外如果一個節點已經存在,創建同名節點,拋出NodeExistsException異常

異步創建接口時候需要實現AsyncCallback.StringCallback()接口,AsyncCallback包含了StatCallback、DataCallback、ACLCallback、ChildrenCallback、Children2Callback、StringCallback和VoidCallback七種不同的回調接口,可以在不同的異步接口中實現不同的接口,和同步接口的區別是節點的創建過程(包含網絡通信和服務端的節點創建過程)是異步的,並且在同步接口調用過程中,需要關注接口拋出異常的可能,而異步接口中,接口本事不會拋出異常,所有的異常都會在回調函數中的Result Code(響應碼)來體現,主要是void ProcessResult(int rc, String path, Object ctx, String name)方法的執行,參數說明如下:

參數名 說明
rc

Result Code 服務端響應碼,客戶端可以從這個響應碼中識別API調用的結果,常見響應碼如下

0(ok):接口調用成功

-4(ConnectionLoss):客戶端和服務端連接已斷開

-110(NodeExists):指定節點已存在

-112(SessionExpired):會話已過期

path 接口調用時傳入API的數據節點的節點路徑參考值
ctx 接口調用時傳入API的ctx參考值
name 實際在服務端創建的節點名,如創建順序節點時,在回調方法中服務端會返回這個數據節點的完整節點路徑

刪除節點

客戶端可以通過zookeeper的API來刪除一個節點,有如下兩個接口:同樣一個是同步刪除一個是異步刪除

public void delete(String path, int version)

public void delete(String path, int version, VoidCallback cb, Object ctx)

參數說明:

參數名 說明
path 指定數據節點的節點路徑
version 指定節點的數據版本,即表明本次刪除操作是針對該數據版本進行的
cb 註冊一個異步回調函數
ctx 用於傳遞上下文信息的對象

注意:在zookeeper中只允許刪除葉子節點,如果一個節點存在至少一個子節點,該節點無法刪除,必須先刪除其所有的子節點後才能刪除

讀取節點

讀取數據包括子節點列表的獲取和節點數據的獲取,子節點列表的獲取,使用getChildren()方法獲取,而節點數據的內容使用getData()方法獲取。

getChildren:有以下8個重載方法,其中包括了同步和異步:

public List<String> getChildren(String path, Watcher watcher)

public List<String> getChildren(String path, boolean watch)

public void getChildren(String path, Watcher watcher, ChildrenCallback cb, Object ctx) 

public void getChildren(String path, boolean watch, ChildrenCallback cb, Object ctx) 

public List<String> getChildren(String path, Watcher watcher, Stat stat)

public List<String> getChildren(String path, boolean watch, Stat stat)

public void getChildren(String path, Watcher watcher, Children2Callback cb, Object ctx) 

public void getChildren(String path, boolean watch, Children2Callback cb, Object ctx)

方法參數的定義:

參數名 說明
path 指定數據節點的節點路徑,即API調用的目的是獲取該節點的子節點列表
watcher 註冊的Watcher,一旦在本次的子節點獲取之後,子節點列表發生變更,那麼就會向客戶端發送通知,可以爲null
watch 表明是否註冊一個Watcher,如果爲true,則表示使用默認的Watcher(創建zookeeper時指定的Watcher),如果爲false,表明不需要註冊Watcher
cb 註冊一個異步回調函數
ctx 用於傳遞上下文信息的對象
stat 指定數據節點的節點狀態信息,用法是在接口中傳入一箇舊的stat變量,該stat變量會在方法執行過程中被來自服務端響應的新的stat對象替換

具體的,在獲取子節點列表時,如果Zookeeper客戶端在獲取到指定節點的子節點列表後還需要訂閱這個子節點列表的變化通知,那麼就可以通過註冊一個Watcher來實現,當有子節點被添加或者刪除,服務端就會向客戶端發送一個NodeChildrenChanged(EventType.NodeChildrenChanged)類型的事件通知,但這個通知並不包含新的子節點列表,必須手動在獲取一次子節點列表,另外節點狀態信息對象stat中記錄了一個節點的基本屬性信息,包含節點創建時候事務ID(cZxid)、最後一次修改的事務ID(mZxid)和節點的內容長度(dataLength)等,當我們需要獲取節點最新的節點狀態信息,我們可以通過傳入一箇舊的stat變量到API接口,在執行過程中,它會被服務端響應的新的stat對象替換。

getData有以下4個重載方法,具體如下

public byte[] getData(String path, Watcher watcher, Stat stat)

public byte[] getData(String path, boolean watch, Stat stat)

public void getData(String path, Watcher watcher, DataCallback cb, Object ctx)

public void getData(String path, boolean watch, DataCallback cb, Object ctx)

具體的參數說明和getChildren的基本一致,只是getChildren是獲取節點的列表,返回的是List<String>,而getData是獲取該節點的數據內容,返回的是數據的Byte[],因爲在數據只支持這種類型的存儲,所有獲取數據時也返回該類型。另外需要注意的是節點數據的變化是指數據的版本變化,而不是數據值的變化,就算設置相同的值也會觸發通知(註冊通知的前提)

更新數據

客戶端通過zookeeperde API來更新一個節點的數據內容,使用setData方法,同步和異步2種:

public void getData(String path, boolean watch, DataCallback cb, Object ctx) 

public Stat setData(String path, byte[] data, int version)

參數說明:

參數名 說明
path 指定數據節點的節點路徑,API調用的目的是更新該節點的數據內容
data[] 一個字節數組,即需要使用該數據內容來覆蓋節點現在的數據內容
version 指定節點的數據版本,表明本次更新操作是針對該數據的這個版本進行的,一個樂觀鎖
cb 註冊的一個異步回調函數
ctx 傳遞上下文信息的對象

上面的version參數,可以有效的避免分佈式更新的併發問題,在使用setData傳入version的時候,當傳入的值是“-1”,是可以更新成功的,在zookeeper中版本都是從0開始的計數的,嚴格的說“-1"不是一個版本號而是一個標識符,就是告訴zookeeper服務器,客戶端需要基於數據最新版本進行操作,如果對zookeeper數據節點的更新操作沒有原子性要求,就可以使用“-1”。

檢測節點是否存在

有以下4個重載方法判斷節點是否存在,帶有StatCallback的是異步接口

public Stat exists(String path, Watcher watcher)

public Stat exists(String path, boolean watch)

public void exists(String path, Watcher watcher, StatCallback cb, Object ctx)

public void exists(String path, boolean watch, StatCallback cb, Object ctx) 

參數說明:

參數名 說明
path 指定數據節點的節點路徑,即API調用的目的是檢測該節點是否存在
watcher

註冊的watcher,用於監聽以下三類事件:

  • 節點被創建
  • 節點被刪除
  • 節點被更新
watch 指定是否複用zookeeper中默認的watch

exists方法主要檢測節點是否存在,返回值是一個stat對象,另外如果在調用的該接口的時候註冊Watcher的話,還可以對節點是否存在進行監聽----一旦節點被創建或者被刪除或被更新都會通知客戶端

權限控制

在zookeeper的實際應用中,我們的做法往往是搭建公用的一個zookeeper集羣爲了保證不同應用之間的數據安全性,需要對zookeeper上的數據訪問進行權限控制(Access Control),zookeeper提供了ACL的權限控制機制,簡單的說就是通過設置zookeeper服務器上數據節點的ACL來控制客戶端對其的訪問權限,如果一個客戶端符合該ACL機制,那麼就可以對其進行訪問,否則無法操作。

zookeeper提供了多個權限控制模式(Scheme),分別是world、auth、digest、ip和super

  • world:默認方式,相當於全世界都能訪問
  • auth:代表已經認證通過的用戶(cli中可以通過addauth digest user:pwd 來添加當前上下文中的授權用戶)
  • digest:即用戶名:密碼這種方式認證,這也是業務系統中最常用的
  • ip:使用Ip地址認證

ACL支持權限:

  • CREATE: 能創建子節點
  • READ:能獲取節點數據和列出其子節點
  • WRITE: 能設置節點數據
  • DELETE: 能刪除子節點
  • ADMIN: 能設置權限

在使用zookeeper的權限控制功能,需要在創建zookeeper會話後給該會話添加相關的權限信息(AuthInfo),zookeeper客戶端提供了以下API接口來進行權限信息的設置

addAuthInfo(Sting scheme, byte[] auth)

其中:scheme:權限控制模式,分爲world、auth、digest、ip和super;auth:是具體的權限信息,如下:

ZooKeeper zooKeeper = new ZooKeeper("localhost:2181", 5000, null);
zooKeeper.addAuthInfo("digest","root:123".getBytes());

需要注意的是:刪除節點的權限控制比較特殊,當客戶端對一個數據節點添加權限信息後,對於刪除操作而言,其作用的範圍是其子節點,也就是說當我們對一個數據節點添加權限信息後依然可以刪除這個節點,但是對於這個節點的子節點,就必須使用響應的權限信息才能夠刪除。

常見的開源客戶端:

zkClient

 

zookeeper的應用場景

數據發佈和訂閱(配置中心)

發佈者將數據發佈到zookeeper的一個或一系列節點上,供訂閱者進行數據訂閱,從而達到動態的獲取數據的目的,實現配置信息的集中管理和動態更新

一般發佈/訂閱模式有兩種設計模式,分別是:

推模式(push):在推模式,服務端主動將數據更新發送給所有訂閱的客戶端

和拉模式(pull):拉模式則是由客戶端主動發起請求去獲取最新的數據,通常客戶端採用的是定時進行輪詢拉取

而zookeeper採用的是二者相結合的方式:客戶端想服務端註冊自己需要關注的節點,一旦節點數據發生變更,那麼服務端就會向相應的客戶端發送watcher事件通知,客戶端收到通知後,主動到服務端拉取最新的數據,一般我們可以在應用啓動的時候主動到zookeeper的服務端進行一次配置的獲取,同時註冊一個watcher監聽,當配置發生變化的時候,服務端會通知所有訂閱的客戶端,從而達到獲取最新配置的目的。

分佈式全局唯一ID

所謂的ID就是一個能夠唯一標識某個對象的標識符,在我們熟悉的關係型數據庫中,各個表的主鍵就是唯一ID,在分庫分表的情況下auto_increment屬性僅能針對單一表中的記錄自動生成ID,而無法做到分佈式環境下全局唯一,通常UUID是非常不錯的全局唯一ID的生成方式,UUID是通用唯一標識碼的簡稱,能夠非常簡單的保證分佈式環境中的唯一性,一個標準的UUID是一個包含32位字符和4個短線_的字符串,但是UUID也有不好的地方,主要如下:

  • 長度過長,存儲UUID需要花費更多的空間
  • 含義不明,看不出這個ID表達的含義

當然zookeeper可以實現這種全局唯一ID,主要利用就是zookeeper的API接口創建一個順序節點,生成全局唯一ID,步驟如下:

  1. 所有客戶端都會根據自己的任務類型,通過create()接口創建一個順序節點,如:“user-”節點
  2. 節點創建完畢後,create()接口會返回一個完整的字節名,如“user-0000000003”
  3. 客戶端你啊到這個返回值,拼接上type類型就可以作爲一個全局ID,如:“sh-user-0000000003”

在zookeeper中,每一個數據節點都能夠維護一份子節點的順序順列,當客戶端對其創建一個順序子節點的時候,zookeeper會自動以後綴的形式在其子節點上添加一個序號。

Master選舉

Master選舉是一個在分佈式系統常見的應用場景,分佈式最核心的特性就是能夠將獨立計算能力的系統單元部署在不同的機器上,構成一個分佈式系統,在分佈式系統中,Master往往是用來協調集羣中其他系統單元,具有對分佈式系統狀態變更的決定權,如在讀寫分離的應用場景中,客戶端的些請求往往由Master處理,通常來說Master都會處理一些複雜的邏輯,並將結果同步給集羣中的其他系統單元。

在集羣中所有機器中選舉一臺機器作爲Master,通常情況下我們可以選擇常見的關係型數據庫中的主鍵特性來實現:即集羣中所有的機器都向數據庫插入一條相同主鍵ID的記錄,數據庫幫助我們進行主鍵衝突檢查,只有一個機器能夠成功插入,那麼這個成功插入的機器我們就可以看作是Master,但是如果Master掛了我們就無法知道並繼續選舉新的Master了,基於zookeeper的強一致性,保證在分佈式高併發情況下節點的創建一定能夠保證全局唯一性,即zookeeper將會保證客戶端無法重複創建一個已經存在的數據節點,也就是說如果多個客戶端請求創建同一個節點,最終只有一個客戶端能夠請求成功,利用這個特性可以輕鬆完成Master選舉

具體步驟:

  1. 集羣中的多個客戶端每天定時創建一個臨時節點/master_election/2020-02-02/master,只有一個客戶端能夠成功創建成功並且成爲Master
  2. 同時沒有創建成功的客戶端都會在/master_election/2020-02-02上註冊一個子節點變更的Watcher,用於監控當前Master機器是否存活,一旦Master掛了,其餘客戶端重新進行Master選舉(即創建master節點)

基於zookeeper即可實現集羣Master的動態選舉。

分佈式鎖

分佈式鎖匙控制分佈式系統之間同步訪問共享資源的一種方式,如果不同的系統或是同一個系統不同主鍵之間共享一個或一組資源,那麼訪問這些資源的時候往往需要通過一些互斥手段保證彼此之間的干擾,以保證一致性,這時候就需要使用到分佈式鎖。

一般情況下,我們可以利用關係型數據庫固有的排他性來實現不同進程之間的互斥(分佈式鎖),但是分佈式系統的性能瓶頸都集中在數據庫上,如果出了業務之外再添加一些額外的行鎖或者表鎖,使得數據庫更加不堪重負,可以基於zookeeper實現分佈式鎖,主要分爲排他鎖和共享鎖

排他鎖

排他鎖(Exclusive 簡稱X鎖),又稱爲寫鎖或者獨佔鎖,表明如果事務T1對數據對象O1加了排他鎖,在加鎖期間,只允許事務T1對O1的讀取和更新,其他事務都無法對這個對象進行任何操作。

定義鎖

在java中常見的排他鎖有兩種常見的方式:synchronized關鍵字和ReentrantLock對象,在zookeeper中通過一個數據節點表示一個鎖,創建一個節點成功就代表加鎖成功

獲取鎖

  • zookeeper獲取排他鎖就是調用create()方法創建一個臨時節點,比如在/exclusive_lock下創建臨時子節點/exclusive_lock/lock,zookeeper會保證只有一個客戶端能夠創建成功,該客戶端獲取鎖。
  • 同時,在沒有獲取所得客戶端就需要在/exclusive_lock節點上註冊一個子節點變更的watcher監聽,達到實時監聽lock節點變更的情況

釋放鎖

因爲在獲取鎖的時候創建的是要給臨時節點,所以有以下兩種情況會釋放鎖:

  • 當前獲取鎖的客戶端機器宕機,zookeeper移除這個臨時節點,釋放鎖
  • 正常執行邏輯後,客戶端調用delete主動刪除這個臨時節點,也釋放鎖

無論什麼情況下移除lock節點,zookeeper都會通知/exclusive_lock節點上註冊Watcher監聽的所有客戶端,這些客戶端在收到通知後重新發起獲取鎖的請求,重複執行!具體的流程圖如下:

共享鎖

共享鎖(Shared Locks,簡稱S鎖),又稱讀鎖,表明如果事務T1對數據對象O1加上共享鎖,那麼當前事務只能對O1進行讀取操作,其他事務也只能對這個數據對象加共享鎖,直到該數據對象上所有的共享鎖都被釋放

共享鎖和排他鎖的區別在於,加了排他鎖之後數據對象只對一個事務可見,而加了共享鎖的數據對所有事務可見。此外需要注意的是,對於排他鎖的節點是臨時節點,創建節點成功就是上鎖成功,而共享鎖的節點是臨時順序節點,創建節點成功後需要根據節點的操作類型和順序來決定是否上鎖成功。

定義鎖:

通過zookeeper的一個數據節點表示一個鎖,是一個類似於“/shared_lock/hostName-請求類型-序號”的臨時順序節點,如:創建了:/shared_lock/192.168.0.1-R-0000000001,/shared_lock/192.168.0.2-W-0000000002,這些個節點代表了共享鎖。

獲取鎖:

獲取共享鎖就是所有客戶端都會在/shared_lock節點下創建一個臨時順序節點,如果當前是讀請求就創建如:/shared_lock/192.168.0.1-R-0000000001,如果是寫請求就創建如:/shared_lock/192.168.0.2-W-0000000001的節點

判斷讀寫順序

根據共享鎖的定義,不同的事務都可以對同一個數據對象進行都操作,而更新操作必須是在當前沒有任何事務進行讀寫的情況下進行,基於此確定的讀寫順序步驟如下:

  1. 創建完節點後,獲取所有/shared_lock節點下所有子節點,並對該節點註冊子節點變更的Watcher監聽
  2. 確定自己在節點序號在所有子節點的順序
  3. 對於都請求:如果沒有比自己序號小的子節點,或所有比自己序號小的子節點都是都請求,表明成功獲取鎖,執行讀邏輯;如果有比自己序號小的子節點中有寫請求,那麼就進入等待;對於寫請求:如果自己不是序號最小的子節點,那麼就需要進入等待
  4. 收到Watcher通知後,重複步驟1

釋放鎖:同排他鎖一致

執行的流程圖如下:

羊羣效應:當某臺機器完成讀操作,並刪除節點,其他機器在收到節點刪除的通知後,重新在/shared_lock節點上獲取一份新的子節點列表,判斷自己的讀寫順序,其中最小的序號的機器將獲取鎖,執行操作,其他的機器發現沒有輪到自己讀取或者更新,將繼續等待。事實上所有的客戶端在收到節點刪除的通知,只有一個最小序號的客戶端纔會拿到鎖,而其他的客戶端基本不需要做任何操作,所以在整個分佈式鎖的競爭過程中,大量的"watcher通知"和子節點列表的獲取都是重複無效的操作,且絕大數的客戶都按判斷的結果都是自己並非最小序號節點,而繼續等待下一次通知。在集羣規模比較大的情況下,這種情況不僅對zookeeper服務器造成巨大的性能影響和網絡衝擊,嚴重的是如果同一時間多個節點對應的客戶端完成事務或者中斷事務引起的節點消失,zookeeper就會在短時間內向其餘客戶端發送大量的事件通知,這就是所謂的

改進:只要將每個鎖競爭者只關注/shared_lock節點下序號比自己小的那個節點是否存在即可,具體步驟:

  1. 客戶端調用create()方法創建要給類似於"/shared_lock/host-請求類型-序號"的臨時順序節點
  2. 客戶端調用getChildren()接口獲取所有已經創建的子節點列表,注意,這裏不註冊任何watcher
  3. 判斷讀寫順序(同前),如果無法獲取共享鎖,那麼調用exist()來對比自己小的那個節點註冊watcher,這裏分爲讀或者寫請求讀請求:向比自己序號小的最後一個寫請求節點註冊Watcher監聽寫請求:向比自己序號小的最後一個節點註冊Watcher監聽
  4. 等待watcher通知,重複步驟2

分佈式隊列

 分佈式隊列,簡單的講究分爲兩大類,一種是常規的先進先出隊列,另外一種則是要等到隊列元素集聚之後才同意安排執行的Barrier模型

FIFO先進先出

使用zookeeper實現FIFO隊列和實現共享鎖的實現非常類似,FIFO隊列就類似於一個全寫的共享鎖模型,思路如下:所有客戶端都會到/queue_fifo這個節點下面創建一個臨時順序節點,如:/queue_fifo/192.168.0.1-000000001,/queue_fifo/192.168.0.2-000000002,/queue_fifo/192.168.0.3-000000003,創建完節點之後,按照4個步驟執行順序:

  1. 通過調用getChildren()接口來獲取/queue_fifo/節點下所有子節點,即獲取隊列中所有的元素
  2. 確定自己的節點序號在所有子節點的順序
  3. 如果自己不是序號最小的子節點,那麼就需要進入等待,同時比向自己序號小的最後一個節點註冊watcher監聽
  4. 接收到watcher通知後,重複步驟1

Barrier:分佈式屏障

Barrier的意思是障礙物,屏障,在分佈式系統中,特製系統之間的一個協調田間,規定一個隊列的元素必須都集聚後才能統一安排,否則一直等待,應用在分佈式並行計算上:最終的合併計算需要基於很多並行計算的自結果來進行,基於zookeeper的實現思想如下:在/queue_barrier節點的數據內容賦值爲一個數字n來代表Barrier值,如n=10表示當/queue_barrier節點下的子節點個數達到10後,纔會打開Barrier,所有客戶端都會到/queue_barrier節點下創建一個臨時節點,節點創建完後按照如下:

  1. 通過調用getData()接口獲取/queue_barrier節點的數據內容:10
  2. 通過調用getChildren()接口獲取/queue_barrier節點下的所有子節點,即獲取隊列中的所有元素,同時註冊對子節點列表變更的Warcher監聽
  3. 統計子節點的個數
  4. 如果子節點個數還不足10個,進入等待
  5. 接收到Watcher通知後,重複步驟2

 

Master-Worker架構

Master-Worker模式是常用的並行模式之一。它的核心思想是,系統由兩類進程協作工作:Master進程和Worker進程。Master進程爲主要進程,主要負責接收和分配任務,它維護了一個Worker進程隊列、子任務隊列和子結果集,Worker進程負責處理子任務。不停地從任務隊列中提取要處理的子任務,並將子任務的處理結果寫入結果集, 當各個Worker進程將子任務處理完成後,將結果返回給Master進程,由Master進程做歸納和彙總,從而得到系統的最終結果,其處理過程如圖所示:

Master-Worker模式的好處,它能夠將一個大任務分解成若干個小任務,並且執行,從而提高系統的吞吐量。而對於系統請求者Client來說,任務一旦提交,Master進程會分配任務並立即返回,並不會等待系統全部處理完成後再返回,其處理過程是異步的。因此Client不會出現等待現象。

需要注意的是:

  • 在任何時候,系統最多只能有一個master,不可以出現兩個master的情況,多個master共存會導致腦裂
  • 系統中除了處於active狀態的master還有一個backup master,在active master失敗的時候backup master可以很快的進入active狀態
  • master實時監控worker狀態,能夠及時收到worker成員變化通知,master在收到worker成員變化通知後,通常重新進行任務分配

使用zookeeper實現master-worker架構

  1. 使用一個臨時節點/master表示master,master在行使master的智能前,首先創建這個master的節點,如果創建成功,則進入active狀態,否則的話進入backup狀態,使用watch機制監控/master,在系統中存在active master和backup master的情況下,如果active master掛了,那麼創建的/master節點就會被zookeeper自動刪除,這時候backup master收到通知,通過再次創建/master節點成爲新的active master
  2. worker通過在/workers下面創建臨時節點加入集羣
  3. 處於active狀態的master會通過watch機制監控/workers節點下面的znode列表來實時獲取worker成員的變化

Zookeeper的技術

序列化和協議

zookeeper的客戶端和服務端之間會進行一系列的網絡通信來實現數據的傳輸,對於一個網絡通信,首先要解決的就是對數據的序列化和反序列化,在zookeeper中,使用Jute這一序列化組件來進行數據的序列化和反序列化操作

Jute是zookeeper中序列化組件,最初也是Hadoop中默認的序列化組件,使用Jute進行序列化和反序列化操作,如下:

        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public class MockRequest implements Record {

            private Long id;

            private String type; 
        }


        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BinaryOutputArchive archive = BinaryOutputArchive.getArchive(baos);
        new MockRequest(1L,"INT").serialize(archive,"request");
        //tcp網絡傳輸對象
        ByteBuffer buffer = ByteBuffer.wrap(baos.toByteArray());
        //反序列化
        ByteBufferInputStream bbis = new ByteBufferInputStream(buffer);
        BinaryInputArchive inArchive = BinaryInputArchive.getArchive(bbis);
        MockRequest mockRequest = new MockRequest();
        mockRequest.deserialize(inArchive,"request");
        bbis.close();
        baos.close();

上述代碼實現對MockRequest對象的序列化和反序列化,總的來說分爲4步:

  • 實體類需要實現Record接口的serialize和deserialize方法
  • 構建一個序列化其BinaryOutputArchive
  • 執行序列化,調用實體類的serialize方法,將其對象序列化到指定的tag中去,如上的將MockRequest對象序列化到header中去
  • 執行反序列化,調用實體類的deserialize,從指定的tag中反序列化出數據內容

zookeeper客戶端

zookeeper客戶端是開發人員使用zookeeper的最主要的途徑,zookeeper的客戶端主要由以下以下幾個核心組件組成

Zookeeper實例:客戶端入口

ClientWatcherManager:客戶端Watcher管理器

HostProvider:客戶端地址列表管理器

ClientCnxn:客戶端地址列表管理器:客戶端核心線程,內部包含兩個線程,即SendThread和EventThread,前者是一個I/O線程,主要負責Zookeeper客戶端與服務端的I/O通信,後者是一個事件線程,主要負責對服務端事件進行處理

zookeeper客戶端的初始化和啓動環節,實際上就是zookeeper對象的實例化過程,客戶端的整個初始化和啓動過程大體分爲以下3個步驟:

  1. 設置默認的Watcher
  2. 設置Zookeeper的服務器地址列表
  3. 創建ClientCnxn

如果在zookeeper的構造方法中傳入一個Watcher對象的話,那麼zookeeper就會將這個Watcher對象保存在ZKWatcherManager的defaultWatcher中,作爲整個客戶端會話期間默認的Watcher

一次會話的創建過程:主要分爲3個階段:

初始化階段-會話創建階段-響應處理階段

初始化階段:

  1. 初始化Zookeeper對象,通過調用Zookeeper的構造方法來實例化一個zookeeper對象,在初始化過程中會創建一個客戶端的Watcher管理器:ClientWatchManager
  2. 設置會話默認的Watcher,如果傳入一個Watcher,就會將他作爲默認的Watcher保存在ClientWatcherManager中
  3. 構造Zookeeper服務器地址列表管理器:HostProvider,對於在構造方法中傳入服務器地址,客戶端會將其存放在服務器地址列表管理器HostProvider中
  4. 創建並初始化客戶端網絡連接器:ClientCnxn,用來管理客戶端和服務端的網絡交互,在創建ClientCnxn的同時,還會初始化兩個核心隊列outgoingQueue和pendingQueue,分別作爲客戶端的請求發送隊列和服務端響應的等待隊列
  5. 客戶端還會初始化SendThread和EventThread,分別用來管理客戶端和服務端之間的所有網絡I/O和進行客戶端的事件處理,同時客戶端將ClientCnxnSocket分配給SendThread作爲底層網絡I/O處理器,並初始化EventThread的待處理事件隊列waitingEvents,用於存放所有等待被客戶端處理的事件。

會話創建階段

  1. 啓動SendThread和EventThread,SendThread首先會判斷當前客戶端的狀態,進行一些了的清理工作,爲客戶端發送“會話創建”請求做準備
  2. 獲取一個服務器地址,在開始創建TCP連接之前,SendThread首先需要獲取一個Zookeeper服務器的目標地址,通常是從HostProvider中隨機獲取一個地址,然後委託給ClientCnxnSocket去創建與zookeeper服務器之間的TCP連接
  3. 獲取一個服務器地址後,ClientCnxnSocket負責和服務器創建一個TCP長連接
  4. 構造ConeentRequest請求,上面的步驟只是純粹的從網絡TCP 層面完成客戶端和服務端之間的Socket連接,但還沒完成Zookeeper客戶端會話的創建,SendThread會負責根據當前客戶端的實際設置構造出一個ConnectRequest請求,代表客戶端與服務器之間創建的一個會話,同時Zookeeper客戶端還會進一步將帶請求包裝成網絡I/O層的Packet對象,放入請求發送隊列OutgoingQueue中去
  5. 發送請求,在準備完畢後,就開始向服務端發送請求了,ClientCnxnSocket負責從outgoingQueue中取出一個待發送的Packet對象,將其序列化成ByteBuffer,向服務端進行發送

響應處理階段

接收服務端的響應,ClientCnxnSocket接收到服務端的響應後,會首先判斷當前客戶端的狀態是否是“已初始化”,如果尚未完成初始化,則認定該響應一個是會話創建請求的響應,直接交給readConnectResult方法來處理該響應

處理Response,ClientCnxnSocket會首先接收服務端的響應進行反序列化,得到ConnectResponse對象,從中獲取到zookeeper服務端分配的會話sessionId

連接成功後,一方面通知SendThread線程,進一步對客戶端進行會話參數設置,包括readTimeout和connectTimeout等,並更新客戶端的狀態,另一方面,通知地址管理器HostProvider當前成功連接的服務器地址

生成事件:SyncConnected-None,爲了能夠讓上層應用感知會話創建成功,SendThread會生成一個事件SyncConnected-None,代表客戶端和服務器會話創建成功,並將該事件傳遞給EventThread線程

查詢Watcher,在EventThread線程收到事件後,會從ClientWatcherManager管理器中查詢對應的Watcher,針對SyncConnected-Node事件,找到存儲的默認Watcher,然後將其放到EventThread的waitingEvents隊列中去

處理時間,EventThread不斷的從waitingEvents隊列中取出待處理的Watcher對象,然後直接調用該對象的process接口方法,觸發Watcher

 

                                                         密

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章