所謂分布式系統(tǒng),指的是一組通過發(fā)送消息實(shí)現(xiàn)協(xié)作、從而共同達(dá)成同一目標(biāo)的資源集合。正如知名計(jì)算機(jī)科學(xué)家Leslie Lamport所指出之定義:“所謂分布式系統(tǒng),其中任意一臺(tái)計(jì)算設(shè)備——即使使用者并未直接使用甚至對(duì)其存在毫不知情——出現(xiàn)故障,亦會(huì)影響到其它設(shè)備的正常運(yùn)作。”
而這條定義也恰恰概括了我們?cè)诜植际较到y(tǒng)當(dāng)中經(jīng)常遇到的一類問題。事實(shí)上,在云計(jì)算時(shí)代之下,資源的匯聚已經(jīng)成為滿足負(fù)載對(duì)于計(jì)算及存儲(chǔ)實(shí)際需求的一種必然手段。這類系統(tǒng)的特點(diǎn)在于包含大量需要管理的資源,而其中故障的出現(xiàn)頻率與整體規(guī)模則成正比關(guān)系。
在分布式系統(tǒng)當(dāng)中,故障屬于一類常規(guī)事態(tài),而非意外狀況——這意味著我們必須時(shí)刻做好心理準(zhǔn)備。有鑒于此,相關(guān)社區(qū)專門創(chuàng)建出專門的工具,旨在幫助開發(fā)人員應(yīng)對(duì)這方面問題,而Apache ZooKeeper正是其中之一。
ZooKeeper是一款極具實(shí)用性、現(xiàn)場(chǎng)測(cè)試能力并擁有廣泛用戶群體的中間件,專門用于構(gòu)建分布式應(yīng)用程序。在OpenStack當(dāng)中,ZooKeeper也成為Nova ServiceGroup API后端中的組成部分。最近,ZooKeeper還與Ceilometer相集成,從而為Central Agent帶來更為理想的高可用性水平——當(dāng)然,這方面話題我們以后將另行討論。
我們?yōu)槭裁葱枰猌ooKeeper?
一般來講,當(dāng)大家設(shè)計(jì)一款分布式應(yīng)用程序時(shí),常常會(huì)發(fā)現(xiàn)需要將各類流程加以協(xié)同才能執(zhí)行預(yù)期任務(wù)。在大多數(shù)情況下,這種協(xié)作關(guān)系依賴于最基本的分布式協(xié)作機(jī)制。
Heat是一款OpenStack編排程序。大家可以利用它創(chuàng)建出一系列云資源,而這類資源會(huì)由一個(gè)模板文件負(fù)責(zé)指定,這就是堆棧的概念。Heat允許用戶對(duì)堆棧進(jìn)行更新,但更新過程必須以原子方式進(jìn)行,否則可能會(huì)導(dǎo)致資源復(fù)制或者相關(guān)性破壞等沖突。這類問題在并發(fā)更新場(chǎng)景下非常常見,而為了解決此類問題,Heat會(huì)在對(duì)堆棧進(jìn)行更新之前首先設(shè)置一套所謂分布式互斥鎖。
在這類原型基礎(chǔ)之上進(jìn)行開發(fā)是項(xiàng)極為困難的工作,而且經(jīng)常帶來令人頭痛的麻煩。事實(shí)上,在分布式系統(tǒng)當(dāng)中反復(fù)出現(xiàn)的這些問題早已成為技術(shù)圈中的共識(shí)。為了簡(jiǎn)化開發(fā)人員的日常工作,雅虎實(shí)驗(yàn)室創(chuàng)造出了Apache ZooKeeper項(xiàng)目,旨在為這些協(xié)作因素提供一套集中式API。歸功于ZooKeeper的幫助,現(xiàn)在我們已經(jīng)能夠輕松實(shí)現(xiàn)多種不同協(xié)議,包括分布式鎖、屏障以及隊(duì)列等等。
ZooKeeper應(yīng)用程序的架構(gòu)與優(yōu)勢(shì)
一款ZooKeeper應(yīng)用程序由一臺(tái)或者多臺(tái)ZK服務(wù)器支撐而成,我們可以將其稱為一個(gè)“集合”,在應(yīng)用程序端則為一組ZK客戶端。
其設(shè)計(jì)思路在于,該分布式應(yīng)用程序的每一個(gè)節(jié)點(diǎn)都通過使用一個(gè)ZK客戶端在應(yīng)用層級(jí)使用相關(guān)API,這意味著應(yīng)用的運(yùn)行將依賴于ZooKeeper服務(wù)器實(shí)現(xiàn)。
這種架構(gòu)方案擁有以下幾項(xiàng)突出優(yōu)勢(shì):
我們可以立足于應(yīng)用層提取大部分分布式同步負(fù)載,從而實(shí)現(xiàn)一套所謂KISS(即Keep It Simple, Stupid,保持一切簡(jiǎn)單且具傻瓜式特性)架構(gòu)。
常見的各類分布式協(xié)作元能夠?qū)崿F(xiàn)開箱即用,因此開發(fā)人員無需再自行對(duì)其加以處理。
開發(fā)人員不需要處理服務(wù)故障等狀況,因?yàn)檎左w系擁有出色的彈性。ZooKeeper以應(yīng)用程序神經(jīng)中心的姿態(tài)存在,因?yàn)樗?fù)責(zé)控制整個(gè)協(xié)作機(jī)制,因此眾多組件都需要依附于它以實(shí)現(xiàn)作用。出于這些理由,ZooKeeper在設(shè)計(jì)中引入了出色的分布式算法,從而提供開發(fā)人員所需要的高可靠性與可用性保障。一個(gè)ZooKeeper集合基于群體形式存在,且通常由三到五臺(tái)服務(wù)器構(gòu)成。
ZooKeeper集合能夠在多種場(chǎng)景之下發(fā)揮作用,下面讓我們從實(shí)踐角度出發(fā)一同來了解。
實(shí)踐場(chǎng)景中的ZooKeeper
ZooKeeper的API非常簡(jiǎn)單而且直觀,其數(shù)據(jù)模型基于以內(nèi)存樹形式存儲(chǔ)的分層命名空間。該樹中的各項(xiàng)元素被稱為znode,以文件形式容納數(shù)據(jù)并能夠如目錄一般擁有子znode。
首先,大家需要確保自己的運(yùn)行環(huán)境滿足系統(tǒng)配置要求,接下來我們就要著手部署一臺(tái)ZK服務(wù)器了:
$ wget http://apache.crihan.fr/dist/zookeeper/zookeeper-3.4.6/zookeeper-3.4.6.tar.gz
$ tar xzf zookeeper-3.4.6.tar.gz
$ cd zookeeper-3.4.6
$ cp conf/zoo_sample.cfg conf/zoo.cfg
$ ./bin/zkServer.sh start
現(xiàn)在ZooKeeper服務(wù)器已經(jīng)能夠以獨(dú)立模式運(yùn)行了,且會(huì)在默認(rèn)情況下監(jiān)聽127.0.0.1:2181。如果大家需要部署一整套服務(wù)器集合,則可以點(diǎn)擊此處閱讀其相關(guān)管理指南。
ZooKeeper命令行界面
我們可以利用ZooKeeper命令行界面(./bin/zkCli.sh)完成一些基礎(chǔ)性操作。其使用方式與shell控制臺(tái)非常相似,操作感受也與文件系統(tǒng)相當(dāng)接近。
下面列出root znode“/”中的全部子znode:
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
創(chuàng)建一個(gè)路徑為“/myZnode”的znode,其相關(guān)數(shù)據(jù)則為“myData”:
[zk: localhost:2181(CONNECTED) 1] create /myZnode myData
Created /myZnode
[zk: localhost:2181(CONNECTED) 2] ls /
[myZnode, zookeeper]
刪除一個(gè)znode:
[zk: localhost:2181(CONNECTED) 3] delete /myZnode
大家可以輸入“help”命令來查看更多操作命令。在本次示例當(dāng)中,我們將使用應(yīng)用程序編程接口(簡(jiǎn)稱API)來編寫一款分布式應(yīng)用程序。
Python ZooKeeper API
我們的這套ZooKeeper服務(wù)器是由Java編程語言構(gòu)建而成,且綁定了多種由不同語言編寫而成的客戶端集合。在今天的文章中,我們將通過Kazzo這一Python捆綁客戶端來了解該API。
我們可以在虛擬環(huán)境下輕松完成Kazoo的安裝工作:
$ pip install kazoo
首先,我們需要接入一個(gè)ZooKeeper集合:
from kazoo import client as kz_client
my_client = kz_client.KazooClient(hosts='127.0.0.1:2181')
def my_listener(state):
if state == kz_client.KazooState.CONNECTED:
print("Client connected !")
my_client.add_listener(my_listener)
my_client.start(timeout=5)
在以上代碼當(dāng)中,我們利用KazooClient類創(chuàng)建了一個(gè)ZK客戶端。其中的“hosts”參數(shù)負(fù)責(zé)定義該ZK服務(wù)器地址,并以逗號(hào)加以分隔,因此如果某臺(tái)服務(wù)器出現(xiàn)故障,那么該客戶端將自動(dòng)嘗試接入其它服務(wù)器。
Kazoo能夠在連接狀態(tài)出現(xiàn)變化時(shí)向我們發(fā)出通知,根據(jù)當(dāng)前具體狀況,這項(xiàng)功能可以非常實(shí)用地觸發(fā)我們預(yù)設(shè)的各類操作。舉例來說,當(dāng)連接無法順利建立時(shí),該客戶端應(yīng)當(dāng)停止發(fā)送命令,而這正是add_listener()方法的作用所在。
而start()方法則能夠在確認(rèn)會(huì)話創(chuàng)建完畢之后,在客戶端與一臺(tái)ZK服務(wù)器之間建立起連接。每臺(tái)服務(wù)器都會(huì)追蹤每個(gè)客戶端中的一項(xiàng)會(huì)話,這種特性在實(shí)際分布式協(xié)作元方面起到非常重要的基礎(chǔ)性作用。
對(duì)znode進(jìn)行增刪改查
與znode進(jìn)行交互同樣非常簡(jiǎn)單:
# create a znode with data
my_client.create(“/my_parent_znode”)
my_client.create(“/my_parent_znode/child_1”, “child_data_1”)
my_client.create(“/my_parent_znode/child_2”, “child_data_2”)
# get the children of a znode
my_client.get_children(“/my_parent_znode”)
# get the data of a znode
my_client.get(“/my_parent_znode/child_1”)
# update a znode
my_client.set(“/my_parent_znode/child_1”, b"child_new_data_1")
# delete a znode
my_client.delete(“/my_parent_znode/child_1”)
其中set()方法會(huì)接受一條version參數(shù),而后者則允許我們執(zhí)行類似于CAS的操作,如此一來即保證了任何使用者都無法在不讀取最新版本的前提下進(jìn)行數(shù)據(jù)更新。
有時(shí)候,大家可能希望確保某個(gè)znode名稱獨(dú)一無二。我們可以通過使用連續(xù)znode(即sequential znode)的方式實(shí)現(xiàn)這項(xiàng)目標(biāo),相當(dāng)于告知服務(wù)器在每段路徑的結(jié)尾添加一個(gè)單遞增計(jì)數(shù)器。
在這一點(diǎn)上,ZooKeeper的運(yùn)作方式類似于一套普通的數(shù)據(jù)庫,不過更有趣的特性還在后面。
觀察者
觀察者機(jī)制可以算是ZooKeeper的核心功能之一,我們可以利用它對(duì)znode事件進(jìn)行通知。換句話來說,每個(gè)客戶端都能夠訂閱某個(gè)指定znode的事件,并在其狀態(tài)發(fā)生變化時(shí)得到通知。要獲取這類通知,該客戶端必須注冊(cè)一項(xiàng)回調(diào)方法——該方法在特定事件發(fā)生時(shí)即被調(diào)用(通過后臺(tái)線程)。感興趣的朋友可以點(diǎn)擊此處查看ZooKeeper所支持的各不同事件類型(英文原文)。
下面來看一段示例代碼,我們可以在某znode的子集發(fā)生變更時(shí)觸發(fā)通知機(jī)制:
def my_func(event):
# check to see what the children are now
# Set a watcher on "/my_parent_znode", call my_func() when its children change
children = zk.get_children("/my_parent_znode", watch=my_func)
值得指出的是,一旦執(zhí)行了回調(diào),客戶端就必須對(duì)其進(jìn)行重置以保證下次事件發(fā)生時(shí)能夠再次正常獲取通知。
臨時(shí)性znode
正如之前所提到,當(dāng)客戶端與服務(wù)器相對(duì)接時(shí),會(huì)建立一個(gè)會(huì)話。該會(huì)話會(huì)始終保持開啟,負(fù)責(zé)向服務(wù)器發(fā)送心跳消息。而在經(jīng)過一段時(shí)間的閑置之后,如果服務(wù)器端沒有監(jiān)聽到來自客戶端的更多活動(dòng),則該會(huì)話即被關(guān)閉。由于該會(huì)話的存在,服務(wù)器才能夠判斷目標(biāo)客戶端是否仍處于活動(dòng)狀態(tài)。
臨時(shí)性znode與正常znode沒有什么本質(zhì)區(qū)別,最大的不同在于前者會(huì)在該會(huì)話過期時(shí)被服務(wù)器所自動(dòng)釋放。
如果將觀察者與臨時(shí)性znode相結(jié)合,我們就能夠?qū)崿F(xiàn)ZooKeeper的一項(xiàng)殺手級(jí)特性。事實(shí)上,這類特性可以說為我們的分布式協(xié)作元實(shí)現(xiàn)工作帶來了數(shù)量龐大的可能性。下面我們就一起來看看分布鎖機(jī)制。
分布鎖
分布鎖應(yīng)該算是分布式應(yīng)用程序當(dāng)中出鏡頻率最高的機(jī)制了,這是因?yàn)槲覀儠?huì)經(jīng)常需要以互斥的方式訪問某些資源。
在ZooKeeper當(dāng)中,這項(xiàng)任務(wù)可以說非常輕松:
my_lock = my_client.Lock("/lockpath", "my-identifier")
with lock: # blocks waiting for lock acquisition
# do something with the lock
其涉及的API與本地鎖完全一樣,但引擎之下到底發(fā)生了什么?要找到問題的答案,我們首先來聊聊分布式算法的設(shè)計(jì)方式。
任何一種分布式算法都必須滿足兩項(xiàng)特性:安全性與活性。
其中安全性確保了該算法絕對(duì)不會(huì)偏離自己的目標(biāo),而對(duì)于分布樂來說,這意味著只有一個(gè)節(jié)點(diǎn)能夠獲得該鎖。從直觀角度講,同一時(shí)段內(nèi)不可能有兩個(gè)節(jié)點(diǎn)同時(shí)擁有分布鎖。
而活性則確保了該算法的持續(xù)遞進(jìn),在分布鎖這一場(chǎng)景當(dāng)中,這意味著如果某個(gè)節(jié)點(diǎn)嘗試獲取該鎖、那么最終一定能夠獲取到。
以本地方式實(shí)現(xiàn)鎖機(jī)制屬于眾所周知的難題,而且有大量專門作為解決方案的算法出現(xiàn)——例如Dekker算法,而且每一種現(xiàn)代編程語言都會(huì)將其囊括在標(biāo)準(zhǔn)庫當(dāng)中。不過需要強(qiáng)調(diào)的是,在分布式環(huán)境下這個(gè)問題會(huì)變得更加復(fù)雜。這兩大特性之所以難于實(shí)現(xiàn),是因?yàn)楦鱾€(gè)節(jié)點(diǎn)隨時(shí)可能出現(xiàn)故障,而這勢(shì)必造成大量可能出現(xiàn)的故障場(chǎng)景。
ZooKeeper ensures these properties for us:則能夠幫助我們確保這兩大特性:
活性的保障:將多個(gè)臨時(shí)性znode加以結(jié)合以檢測(cè)故障節(jié)點(diǎn),而觀察者機(jī)制則負(fù)責(zé)向其它節(jié)點(diǎn)發(fā)出通知。因此,如果某個(gè)節(jié)點(diǎn)獲得了分布鎖并出現(xiàn)故障,那么其它節(jié)點(diǎn)將立即識(shí)別到這一狀況。
安全性的保障:利用連續(xù)znode以確保各節(jié)點(diǎn)皆擁有彼此獨(dú)立的命名,這樣只有一個(gè)節(jié)點(diǎn)會(huì)獲得分布鎖。
我強(qiáng)烈建議大家點(diǎn)擊此處查看分布鎖說明文檔,其中提到了Kazoo的多種實(shí)現(xiàn)方式。
總結(jié)陳詞
構(gòu)建一款分布式應(yīng)用程序往往會(huì)成為一場(chǎng)令人頭痛的噩夢(mèng),因?yàn)槲覀儽仨氁A(yù)料到一切隨時(shí)可能出現(xiàn)的異常狀況(即隨機(jī)出現(xiàn)的故障),同時(shí)處理多種元素彼此組合產(chǎn)生的指數(shù)級(jí)狀況增長(zhǎng)(系統(tǒng)規(guī)模越大,狀況的具體數(shù)量也就越多)。ZooKeeper是一款非常便捷的工具,而且適合大家用于打理自己的基礎(chǔ)設(shè)施堆棧。有了它的幫助,我們可以將更多精力集中在應(yīng)用程序邏輯身上。
在OpenStack當(dāng)中,我們希望能夠充分發(fā)揮ZooKeeper的設(shè)計(jì)目標(biāo),即利用單獨(dú)一款通用型工具解決所有分布式系統(tǒng)帶來的復(fù)雜難題。因此,我們創(chuàng)建了一套名為Tooz的庫,用于實(shí)現(xiàn)一部分常見的分布式協(xié)作元。Tooz的正常運(yùn)行依賴于多種不同后端驅(qū)動(dòng)要素——ZooKeeper當(dāng)然也是其中之一——而且能夠作用于所有OpenStack項(xiàng)目當(dāng)中。
在下一篇文章中,我們將了解如何利用OpenStack Ceilometer讓Central Agent擁有出色的高可用性——其中涉及另一種重要的分布式元,即組成員(group membership)。屆時(shí)我們也將開發(fā)出自己第一款基于ZooKeeper的真正應(yīng)用程序,咱們到時(shí)候見!