本文中,我們將對ZooKeeper進行介紹。簡單地說,ZooKeeper是一個用來在構成應用的各個子服務之間進行協調的一個服務。
由於其本身並沒有特別復雜的機制,因此我們將會把更多的筆墨集中在如何對ZooKeeper進行使用方面。當然,這也是和其它博文所略有不同的地方,否則我也不會花費時間去寫這這篇博客。
ZooKeeper簡介
我們已經在前面一系列和集群相關的博文中提到過,一個大型服務常常是由一系列子服務共同組成的。這些服務常常包含一系列動態的配置,以告知子服務當前程序運行環境的變化。那么我們需要怎樣完成配置的更新呢?這些配置的更改到底是由誰來發起?如果有多個發起方同時對同一配置進行更改,那么各個不同子服務接收到消息的先后順序將有所不同,進而導致各個子服務內部的動態配置互不相同。這一系列問題我們應該如何解決?
答案就是我們要講解的ZooKeeper。ZooKeeper允許我們將數據組織成為一個類似於文件系統的數據結構。通過數據結點所在的地址,我們可以訪問數據結點中所包含的數據:

為了使用ZooKeeper,我們的各個子服務需要通過一個客戶端與ZooKeeper連接,並通過一系列API調用執行對數據的讀寫操作。而為了解決前面所提到的一系列和配置管理相關的問題,ZooKeeper提供了如下一系列特性:
- Wait free:其表示ZooKeeper的API並沒有使用阻塞原語(blocking primitive)。這是因為阻塞原語常常會導致死鎖,程序運行緩慢等一系列問題。該非阻塞特性也允許客戶端同時發送幾個消息來執行並行的修改。
- 請求的線性有序執行:由於wait free特性會導致各個客戶端之間的相互協作出現問題,如處理多個寫入請求時的寫寫沖突以及在寫入請求被阻塞時對數據的讀取所產生的讀寫沖突等。為了解決這個問題,ZooKeeper的內部實現保證了在同一客戶端的所有的操作都是FIFO的,並且ZooKeeper服務中所有的寫入都是按照線性的方式來執行的。同時ZooKeeper中的數據還存在着version的概念,從而幫助保證寫入的安全。
- 數據緩存在客戶端:為了提高讀操作的運行性能,ZooKeeper會嘗試將數據緩存在客戶端。為了保證數據的一致性,各個客戶端可以使用一個watch機制來添加對特定數據變動的偵聽。
下面就讓我們依次對這些特性進行講解。
首先就是wait free特性。讓我們想象一種情況,那就是對多條配置項的同時改寫。如果每次調用都需要等待所有更改完成,那么整個更新的過程將變得非常緩慢:

從上圖中可以看到,一個非阻塞調用可以直接返回,從而可以消除因為等待響應所導致的阻塞,顯著地提高子服務的運行效率。這種效率的提高在同時更改多個配置時非常顯著。由此ZooKeeper所提供的API主要分為阻塞和非阻塞兩種。
接下來就是各個請求的FIFO執行以及線性讀寫。其實如果您做過多線程,這種保證非常容易理解。假設現在執行了兩個寫入請求,那么寫入的先后順序不同可能導致運行結果不同:

而這種先后順序變化很有可能是由於網絡延遲所導致的。試想一下,如果我們在非阻塞模式中使用了同一客戶端插入了對同一個配置的兩個寫入操作,那么我們就需要保證后一個寫入操作是有效的,而且再次讀取一定能讀取后一個寫入操作所寫入的值。在version的幫助下,我們可以避免計划外寫入,而單一客戶端的請求會按照FIFO順序執行的則可以用來保證寫入請求后的讀取會讀取最后寫入的值。這也是ZooKeeper能夠提供異步模式的基礎。
而為了提高讀寫速度,ZooKeeper會在客戶端保存數據的緩存。這樣就可以使得對數據的重復使用不再需要從ZooKeeper服務中重復讀取。而為了保證客戶端緩存和ZooKeeper服務記錄的數據一致,ZooKeeper使用了了一種被稱為watch的機制:如果一個客戶端發送一個請求,並在該請求中設置了watch標記,那么該客戶端就會在該數據上建立watch機制。一旦該結點所記錄的數據發生了變化,那么它將向客戶端發送一個消息以通知該數據已經失效。該通知只聲明相應的數據發生了更改 ,卻不會指出到底更改為哪個值。如果客戶端需要讀取更新的值,那么它就需要重新執行一次讀取,並在該請求中決定到底是否再次通過watch標記繼續偵聽。也就是說,watch機制是一種一次性的機制。其運行機制大致如下圖所示:

而另一個使watch機制失效的便是會話結束。這抑或是子服務自動退出服務等正常業務邏輯執行(例如在使用Amazon的Spot Instance時會常常遇到這種情況),或者是子服務失效等非正常情況。
除此之外,ZooKeeper還有一個比較重要的概念就是數據的版本。相信使用過樂觀鎖等機制的讀者都應該知道,版本實際上就是為了防止過期數據的。而且我們也在剛剛的講解中提到過版本的作用,因此在這里不做詳述。
由於ZooKeeper常常管理着整個集群中所有的動態配置信息,因此它自身已經包含了高可用性設計。通常情況下,ZooKeeper的高可用性常常是通過多個服務實例來完成的。不同的客戶端可能連接到不同的服務實例上:

這些服務實例中,一個服務器將扮演leader角色,而其它服務實例將扮演follower角色。在一個讀請求到達時,任何服務實例將可以直接將其所記錄的數據返回。而在需要寫入數據的時候,接收到寫入請求的服務實例會將該請求轉向ZooKeeper的leader,然后再由follower來對數據進行更新:

而為了能從失效狀態恢復,ZooKeeper會定期保存數據的快照,並記錄對數據的操作日志。如果一個ZooKeeper實例失效,那么我們就可以通過選取最近的Snapshot並重新執行相應的操作日志來完成該實例的恢復。
這里有一個問題,那就是leader是如何決定的,以及在leader失效時如何得到新的leader。這一切都是通過其內置的選舉協議來完成的。一種說法是,leader的選舉是按照一種特殊的Quorum選舉方式,Simple Majority Quorums來完成的。簡單地說,其表示絕大多數結點同意的選舉方式。而另一種說法則是按照Paxos協議族來完成的。不管怎樣,您只要知曉ZooKeeper中有一個leader即可。
最后要說的一點就是數據結點的類型。在ZooKeeper中,數據結點主要分為兩種類型:Regular以及Ephemeral。Regular結點需要通過客戶端顯式地創建及刪除,而Ephemeral結點則會在會話結束之時即被銷毀。
OK。現在呢,基本上所有的基礎機制都已經介紹完畢了。那么在本節最后,就讓我們來看看ZooKeeper所提供的一系列API。在這里,我們並不區分API是同步的還是異步的:
- create(path, data, flags):在特定路徑下創建一個結點。我們可以通過flags控制結點是否在會話結束時被移除等一系列行為
- getData(path, watch):獲得特定路徑下結點的數據,並通過watch參數來決定是否啟用watch機制。
- setData(path, data, version):設置特定路徑下結點的數據。其通過傳入的version來決定是否發出請求的客戶端擁有最新的數據。如果不是,那么該請求被認為是非正確的並不被執行。
- getChildren(path, watch):獲得特定路徑下的所有子結點。
- getACL(path, version):獲取結點所擁有的ACL。
- setACL(path, acls, version):設置結點的ACL,以控制對它的訪問。
- sync():等待對客戶端所連接結點的更新完畢。
- delete(path, version):刪除特定路徑下的結點。通過傳入的version來決定是否發出請求的客戶端擁有最新的數據。如果不是,那么該請求被認為是非正確的並不被執行。
ZooKeeper使用示例
在了解了ZooKeeper的使用之后,我們現在就以一個簡單的示例來看看到底如何使用ZooKeeper。
在《On cloud, be cloud native》一文中,我們穿插着介紹了Amazon所提供的一系列服務。其中兩個重要的服務就是AMI和Auto Scaling Group。兩者組合可以制定出高效的可擴展(Scalability,我更傾向“伸縮”一詞),高可用集群。在啟動時,Auto Scaling Group會使用指定的AMI創建虛擬機實例並執行動態腳本對創建好的虛擬機進行配置。這其中就包括從ZooKeeper中讀取作為動態腳本輸入的動態配置信息:

從上圖中可以看到,在程序啟動時,其將直接從ZooKeeper中直接讀取已經放置在特定位置的配置文件。該配置文件中記錄了虛擬機實例所需要運行的代碼在S3上的地址。接下來,虛擬機實例將會從Amazon S3中下載並部署這些代碼,並動態地對虛擬機進行配置。而在該讀取配置的過程中,新創建的虛擬機實例將會把watch選項設置為true。
而在程序運行一段時間后,我們可能就需要對該Auto Scaling Group所承載的服務進行升級。此時我們首先需要將更新后的Source Bundle放置在Amazon S3上,並更新ZooKeeper中所記錄的配置。由於之前在創建虛擬機時我們已經將watch選項設置為true,因此該配置更改會發送一條通知到Auto Scaling Group中的各個虛擬機實例之上。接下來,這些虛擬機通過向Auto Scaling Group發送Detach消息從該Auto Scaling Group移除。在該Detach消息中,我們應選擇保持Auto Scaling Group容量不變這一選項。這樣做的結果就是,Auto Scaling Group將會使用新的配置重新創建一台新的虛擬機。這樣我們只需要對配置進行更改,而其它的一切工作都由Amazon的各種自動化功能自行完成。
這個流程中有一個問題,那就是我們要能夠保證整個應用能持續地提供服務(假設更新后的服務邏輯具有后向兼容性)。當配置更改消息發送到各個虛擬機時,它們可能都會開始嘗試從Auto Scaling Group移除,從而導致Auto Scaling Group的容量快速下降,進而導致整個應用無法提供服務。而解決方案便是通過ZooKeeper實現互斥鎖的功能,從而使得Auto Scaling Group中的各個實例的移除是逐個進行的。
基於ZooKeeper的互斥鎖的實現非常簡單:創建一個“lock file”即可。這種方法實際上在很多軟件中被使用。例如最常見的就是Git。其原理很簡單,那就是嘗試創建一個文件。如果文件創建成功,那么其就是得到了鎖。而如果由於文件已經存在而創建失敗,那么就是沒有得到鎖。而在基於ZooKeeper的互斥鎖實現中,我們嘗試創建的則是一個結點:為了獲得鎖,ZooKeeper的客戶端會嘗試在特定路徑下創建一個Ephemeral結點。如果其創建成功,那么它就可以得到該鎖。而其它各個沒有成功獲得鎖的實例將通過發送一個讀請求並標示watch標志來偵聽該結點的變化。一旦擁有鎖的實例發現Auto Scaling Group已經創建好了新的虛擬機,那么它就可以向ZooKeeper發送請求刪除該文件。此時其它等待的實例將得到通知並再次嘗試獲得鎖。
那么擁有鎖的實例是如何得知新的虛擬機已經創建完畢了呢?其使用的就是ZooKeeper的另外一種常見使用方法:Group Membership。在ZooKeeper中,我們可以創建一個結點,並在它之下添加/刪除子結點。而在父結點進行監聽的客戶端將由於子結點的增刪得到通知消息。在得到通知之后,其就可以通過getChildren()函數得到所有的子結點。也就是說,在得到鎖並將自身移出Auto Scaling Group的虛擬機實例可以首先通過getChildren()函數添加對Auto Scaling Group所對應結點的監聽。在新的虛擬機實例創建完畢並添加到Auto Scaling Group中后,其將向Auto Scaling Group所對應的結點添加一個子結點,從而通知原本移出Auto Scaling Group的虛擬機實例新的虛擬機實例已經創建完畢。那么原虛擬機實例就可以釋放互斥鎖,從而使得下一個虛擬機開始執行替換的步驟。
通過該互斥鎖實現,我們能夠讓Auto Scaling Group中的各虛擬機實例逐個移除並替換。當然啊,網絡上還有很多種對ZooKeeper的精妙使用。這里就不再一一贅述了。
轉載請注明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/5185796.html
商業轉載請事先與我聯系:silverfox715@sina.com
公眾號一定幫忙別標成原創,因為協調起來太麻煩了。。。
