Zookeeper的典型應(yīng)用場(chǎng)景(2)

此文知識(shí)來自于:《從Paxos到Zookeeper分布式一致性原理與實(shí)踐》第六章

  1. 集群管理(子節(jié)點(diǎn))
  2. Master選舉(同時(shí)創(chuàng)建節(jié)點(diǎn))
  3. 分布式鎖(同時(shí)創(chuàng)建節(jié)點(diǎn))
  4. 分布式隊(duì)列(創(chuàng)建順序節(jié)點(diǎn))

5.1 集群管理

隨著分布式系統(tǒng)規(guī)模的日益擴(kuò)大,集群中的機(jī)器規(guī)模也隨之變大,因此,如何更好地進(jìn)行集群管理也顯得越來越重要了。

所謂集群管理,包括集群監(jiān)控集群控制兩大塊、前者側(cè)重對(duì)集群運(yùn)行時(shí)狀態(tài)的收集,后者則是對(duì)集群進(jìn)行操作與控制。在日常開發(fā)和運(yùn)維過程中,我們經(jīng)常會(huì)有
類似于如下的需求。

  • 希望知道當(dāng)前集群中究竟有多少機(jī)器在工作。
  • 對(duì)集群中每臺(tái)機(jī)器的運(yùn)行時(shí)狀態(tài)進(jìn)行數(shù)據(jù)收集。
  • 對(duì)集群中機(jī)器進(jìn)行上下線操作。

在傳統(tǒng)的基于Agent的分布式集群管理體系中,都是通過在集群中的每臺(tái)機(jī)器上部署一個(gè)Agent,由這個(gè)Agent負(fù)責(zé)主動(dòng)向指定的一個(gè)監(jiān)控中心系統(tǒng)(監(jiān)控中心
系統(tǒng)負(fù)責(zé)將所有數(shù)據(jù)進(jìn)行集中處理,形成一系列報(bào)表,并負(fù)責(zé)實(shí)時(shí)報(bào)警,以下簡(jiǎn)稱“監(jiān)控中心”)匯報(bào)自己所在機(jī)器的狀態(tài)。在集群規(guī)模適中的場(chǎng)景下,這確實(shí)
是一種在生產(chǎn)實(shí)踐中廣泛使用的解決方案,能夠快速有效地實(shí)現(xiàn)分布式環(huán)境集群監(jiān)控,但是一旦系統(tǒng)的業(yè)務(wù)場(chǎng)景增多,集群規(guī)模變大,該解決方案的弊端也就顯現(xiàn)出來了:

  • 大規(guī)模升級(jí)困難:以客戶端形式存在的Agent,在大規(guī)模使用后,一旦遇到需要大規(guī)模升級(jí)的情況,就非常麻煩,在升級(jí)成本和升級(jí)進(jìn)度的控制上面臨巨大的挑戰(zhàn)。
  • 統(tǒng)一的Agent無法滿足多樣的需求:對(duì)于機(jī)器的CPU使用率、負(fù)載(Load)、內(nèi)存使用率、網(wǎng)絡(luò)吞吐以及磁盤容量等機(jī)器基本的物理狀態(tài),使用統(tǒng)一的Agent
    來進(jìn)行監(jiān)控或許都可以滿足。但是,如果需要深入應(yīng)用內(nèi)部,對(duì)一些業(yè)務(wù)狀態(tài)進(jìn)行監(jiān)控,例如,在一個(gè)分布式消息中間件中,希望監(jiān)控到每個(gè)消費(fèi)者對(duì)消息的消費(fèi)狀態(tài);
    或者在一個(gè)分布式任務(wù)調(diào)度系統(tǒng)中,需要對(duì)每個(gè)機(jī)器上任務(wù)的執(zhí)行情況進(jìn)行監(jiān)控。很顯然,對(duì)于這些業(yè)務(wù)耦合緊密的監(jiān)控需求,不適合由一個(gè)統(tǒng)一的Agent來提供。
  • 編程語言多樣性:隨著越來越多編程語言的出現(xiàn),各種異構(gòu)系統(tǒng)層出不窮。如果使用傳統(tǒng)的Agent方式,那么需要提供各種語言的Agent客戶端。另一方面,
    “監(jiān)控中心”在對(duì)異構(gòu)系統(tǒng)的數(shù)據(jù)進(jìn)行整合上面臨巨大挑戰(zhàn)。

ZooKeeper具有以下兩大特性:

  • 客戶端如果對(duì)ZooKeeper的一個(gè)數(shù)據(jù)節(jié)點(diǎn)注冊(cè)Watcher監(jiān)聽,那么當(dāng)該數(shù)據(jù)節(jié)點(diǎn)的內(nèi)容或是其子節(jié)點(diǎn)列表發(fā)生變更時(shí),ZooKeeper服務(wù)器就會(huì)向訂閱的
    客戶端發(fā)送變更通知。
  • 對(duì)在ZooKeeper上創(chuàng)建的臨時(shí)節(jié)點(diǎn),一旦客戶端與服務(wù)器之間的會(huì)話失效,那么該臨時(shí)節(jié)點(diǎn)也就被自動(dòng)清除。

利用ZooKeeper的這兩大特性,就可以實(shí)現(xiàn)另一種集群機(jī)器存活性監(jiān)控的系統(tǒng)。例如,監(jiān)控系統(tǒng)在/clusterServers節(jié)點(diǎn)上注冊(cè)一個(gè)Watcher監(jiān)聽,
那么但凡進(jìn)行動(dòng)態(tài)添加機(jī)器的操作,就會(huì)在/clusterServers節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)/clusterServers/[Hostname]。這樣一來監(jiān)控系統(tǒng)就能夠?qū)崟r(shí)
檢測(cè)到機(jī)器的變動(dòng)情況,至于后續(xù)處理就是監(jiān)控系統(tǒng)的業(yè)務(wù)了。下面我們就通過分布式日志收集系統(tǒng)和在線云主機(jī)管理這兩個(gè)典型例子來看看如何使用ZooKeeper
實(shí)現(xiàn)集群管理。

5.1.1 分布式日志收集系統(tǒng)

分布式日志收集系統(tǒng)的核心工作就是收集分布在不同機(jī)器上的系統(tǒng)日志,在這里我們重點(diǎn)來看分布式日志系統(tǒng)的收集器模塊。

在一個(gè)典型的日志系統(tǒng)的架構(gòu)設(shè)計(jì)中,整個(gè)日志系統(tǒng)會(huì)把所有需要收集的日志機(jī)器(下文以“日志源機(jī)器”代表此類機(jī)器)分為多個(gè)組別,每個(gè)組別對(duì)應(yīng)一個(gè)收集器,
這個(gè)收集器其實(shí)就是一個(gè)后臺(tái)機(jī)器(下文以“收集器機(jī)器”代表此類機(jī)器),用于收集日志。對(duì)于大規(guī)模的分布式日志收集系統(tǒng)場(chǎng)景,通常需要解決如下兩個(gè)問題。

  • 變化的日志源機(jī)器:在生產(chǎn)環(huán)境中,伴隨著機(jī)器的變動(dòng),每個(gè)應(yīng)用的機(jī)器幾乎每天都是在變化的(機(jī)器硬件問題、擴(kuò)容、機(jī)房遷移或是網(wǎng)絡(luò)問題都會(huì)導(dǎo)致一個(gè)應(yīng)用的機(jī)器變化),
    也就是說每個(gè)組別中的日志源機(jī)器通常是在不斷變化的。
  • 變化的收集器機(jī)器:日志收集系統(tǒng)自身也會(huì)有機(jī)器的變更或擴(kuò)容,于是會(huì)出現(xiàn)新的收集器加入或是老的收集器機(jī)器退出的情況。

上面兩個(gè)問題,無論是日志源機(jī)器還是收集器機(jī)器的變更,最終都?xì)w結(jié)為一點(diǎn):如何快速、合理、動(dòng)態(tài)地為每個(gè)收集器分配對(duì)應(yīng)的日志源機(jī)器,這也成為了整個(gè)
日志系統(tǒng)正確穩(wěn)定運(yùn)轉(zhuǎn)的前提,也是日志收集過程中最大的技術(shù)挑戰(zhàn)。在這種情況下,引入ZooKeeper是個(gè)不錯(cuò)的選擇,下面我們來看ZooKeeper在這個(gè)
場(chǎng)景中的使用。

.1 注冊(cè)收集器機(jī)器

使用ZooKeeper來進(jìn)行日志系統(tǒng)收集器的注冊(cè)、典型做法是在ZooKeeper上創(chuàng)建一個(gè)節(jié)點(diǎn)作為收集器的根節(jié)點(diǎn),例如/logs/collector(下文我們以“收集器
節(jié)點(diǎn)”代表該數(shù)據(jù)節(jié)點(diǎn)),每個(gè)收集器機(jī)器在啟動(dòng)的時(shí)候,都會(huì)在收集器節(jié)點(diǎn)下創(chuàng)建自己的節(jié)點(diǎn),例如logs/collector/[Hostname]

.2 任務(wù)分發(fā)

待所有收集器機(jī)器都創(chuàng)建好自己對(duì)應(yīng)的節(jié)點(diǎn)后,系統(tǒng)根據(jù)收集器節(jié)點(diǎn)下子節(jié)點(diǎn)的個(gè)數(shù),將所有日志源機(jī)器分成對(duì)應(yīng)的若干組,然后將分組后的機(jī)器列表分別寫到
這些收集器機(jī)器創(chuàng)建的子節(jié)點(diǎn)(例如/logs/collector/host1)上去。這樣一來,每個(gè)收集器機(jī)器都能夠從自己對(duì)應(yīng)的收集器節(jié)點(diǎn)獲取日志源機(jī)器列表,
進(jìn)而開始進(jìn)行日志收集工作。

.3 狀態(tài)匯報(bào)

完成收集器機(jī)器的注冊(cè)以及任務(wù)分發(fā)后,我們還要考慮到這些機(jī)器隨時(shí)都有掛掉的可能。因此,針對(duì)這個(gè)問題,我們需要有一個(gè)收集器的狀態(tài)匯報(bào)機(jī)制:
每個(gè)收集器機(jī)器在創(chuàng)建完自己的專屬節(jié)點(diǎn)后,還需要在對(duì)應(yīng)的子節(jié)點(diǎn)上創(chuàng)建一個(gè)狀態(tài)子節(jié)點(diǎn),例如/logs/collector/host1/status,每個(gè)收集器都需要定期向
該節(jié)點(diǎn)寫入自己的狀態(tài)信息。我們可以把這種策略看作是一種檢測(cè)機(jī)制,通常收集器機(jī)器都會(huì)在這個(gè)節(jié)點(diǎn)寫入日志收集進(jìn)度信息。日志系統(tǒng)根據(jù)該狀態(tài)子節(jié)點(diǎn)的最后更新時(shí)間
來判斷對(duì)應(yīng)的收集器機(jī)器是否存活。

.4 動(dòng)態(tài)分配

如果收集器機(jī)器掛掉或是擴(kuò)容了,就需要?jiǎng)討B(tài)地進(jìn)行收集任務(wù)的分配。在運(yùn)行過程中,日志系統(tǒng)始終關(guān)注著/logs/collector這個(gè)節(jié)點(diǎn)下所有子節(jié)點(diǎn)的變更,
一旦檢測(cè)到有收集器機(jī)器停止匯報(bào)或是有新的收集器機(jī)器加入,就要開始進(jìn)行任務(wù)的重新分配。無論是針對(duì)收集器機(jī)器停止匯報(bào)還是新機(jī)器加入的情況,
日志系統(tǒng)都需要將之前分配給該收集器的所有任務(wù)轉(zhuǎn)移。為了解決這個(gè)問題,通常有兩種做法。

.4.1 全局動(dòng)態(tài)分配

這是一種簡(jiǎn)單粗暴的做法,在出現(xiàn)收集器機(jī)器掛掉或是新機(jī)器加入的時(shí)候,日志系統(tǒng)需要根據(jù)新的收集器機(jī)器列表,立即對(duì)所有的日志源機(jī)器重新進(jìn)行一次分組,
然后將其分配給剩下的收集器機(jī)器。

.4.2 局部動(dòng)態(tài)分配

全局動(dòng)態(tài)分配方式雖然策略簡(jiǎn)單,但是存在一個(gè)問題:一個(gè)或部分收集器機(jī)器的變更,就會(huì)導(dǎo)致全局動(dòng)態(tài)任務(wù)的分配,影響面比較大,因此風(fēng)險(xiǎn)也就比較大。
所謂局部動(dòng)態(tài)分配,顧名思義就是在小范圍內(nèi)進(jìn)行任務(wù)的動(dòng)態(tài)分配。在這種策略中,每個(gè)收集器機(jī)器在匯報(bào)自己日志收集狀態(tài)的同時(shí),也會(huì)把自己的負(fù)載匯報(bào)上去。
請(qǐng)注意,這里提到的負(fù)載并不僅僅只是簡(jiǎn)單地指機(jī)器CPU負(fù)載(Load),而是一個(gè)對(duì)當(dāng)前收集器任務(wù)執(zhí)行的綜合評(píng)估。

在這種策略中,如果一個(gè)收集器機(jī)器掛了,那么日志系統(tǒng)就會(huì)把之前分配給這個(gè)機(jī)器的任務(wù)重新分配到那些負(fù)載較低的機(jī)器上去。同樣,如果有新的收集器機(jī)器加入,
會(huì)從那些負(fù)載高的機(jī)器上轉(zhuǎn)移部分任務(wù)給這個(gè)新加入的機(jī)器。

.5 注意事項(xiàng)

.5.1 節(jié)點(diǎn)類型

首先看/logs/collector這個(gè)節(jié)點(diǎn)下面子節(jié)點(diǎn)的節(jié)點(diǎn)類型。這個(gè)節(jié)點(diǎn)下面的所有子節(jié)點(diǎn)都代表了每個(gè)收集器機(jī)器,那么初步認(rèn)為這些子節(jié)點(diǎn)必須選擇臨時(shí)節(jié)點(diǎn),
原因是日志系統(tǒng)可以根據(jù)這些臨時(shí)節(jié)點(diǎn)來判斷收集器機(jī)器的存活性。但是,同時(shí)還需要注意的一點(diǎn)是:在分布式日志收集這個(gè)場(chǎng)景中,收集器節(jié)點(diǎn)上還會(huì)存放所有
已經(jīng)分配給該收集器機(jī)器的日志源機(jī)器列表,如果只是簡(jiǎn)單地依靠ZooKeeper自身的臨時(shí)節(jié)點(diǎn)機(jī)制,那么當(dāng)一個(gè)收集器掛掉或是當(dāng)這個(gè)收集器機(jī)器中斷“心跳匯報(bào)”
的時(shí)候,待該收集器節(jié)點(diǎn)的會(huì)話失效后,ZooKeeper就會(huì)立即刪除該節(jié)點(diǎn),于是,記錄在該節(jié)點(diǎn)上的所有日志源機(jī)器列表也就隨之被清除掉了。

從上面的描述中可以知道,臨時(shí)節(jié)點(diǎn)顯然無法滿足這里的業(yè)務(wù)需求,所以我們選擇了使用持久節(jié)點(diǎn)來標(biāo)識(shí)每一個(gè)收集器機(jī)器,同時(shí)在這個(gè)持久節(jié)點(diǎn)下面分別創(chuàng)建
/logs/collector/[Hostname]/status節(jié)點(diǎn)來表征每一個(gè)收集器機(jī)器的狀態(tài)。這樣一來,既能實(shí)現(xiàn)日志系統(tǒng)對(duì)所有收集器的監(jiān)控,同時(shí)在收集器機(jī)器掛掉
后,依然能夠準(zhǔn)確地將分配于其中的任務(wù)還原。

.5.2 日志系統(tǒng)節(jié)點(diǎn)監(jiān)聽

在實(shí)際生產(chǎn)運(yùn)行過程中,每一個(gè)收集器機(jī)器更改自己狀態(tài)節(jié)點(diǎn)的頻率可能非常高(如每秒1次或更短),而且收集器的數(shù)量可能非常大,如果日志系統(tǒng)監(jiān)聽所有
這些節(jié)點(diǎn)變化,那么通知的消息量可能會(huì)非常大。另一方面,在收集器機(jī)器正常工作的情況下,日志系統(tǒng)沒有必要去實(shí)時(shí)地接收每次節(jié)點(diǎn)狀態(tài)變更,因此大部分
這些變更通知都是無用的。因此我們考慮放棄監(jiān)聽設(shè)置,而是采用日志系統(tǒng)主動(dòng)輪詢收集器節(jié)點(diǎn)的策略,這樣就節(jié)省了不少網(wǎng)卡流量,唯一的缺陷就是有
一定的延時(shí)(考慮到分布式日志收集系統(tǒng)的定位,這個(gè)延時(shí)是可以接受的)。

5.1.2 在線云主機(jī)管理

在線云主機(jī)管理通常出現(xiàn)在那些虛擬主機(jī)提供商的應(yīng)用場(chǎng)景中。在這類集群管理中,有很重要的一塊就是集群機(jī)器的監(jiān)控。這個(gè)場(chǎng)景通常對(duì)于集群中的機(jī)器狀態(tài),
尤其是機(jī)器在線率的統(tǒng)計(jì)有較高的要求,同時(shí)需要能夠快速地對(duì)集群中機(jī)器的變更做出響應(yīng)。

在傳統(tǒng)的實(shí)現(xiàn)方案中,監(jiān)控系統(tǒng)通過某種手段(比如檢測(cè)主機(jī)的指定端口)來對(duì)每臺(tái)機(jī)器進(jìn)行定時(shí)檢測(cè),或者每臺(tái)機(jī)器自己定時(shí)向監(jiān)控系統(tǒng)匯報(bào)“我還活著”。
但是這種方式需要每個(gè)業(yè)務(wù)系統(tǒng)的開發(fā)人員自己來處理網(wǎng)絡(luò)通信、協(xié)議設(shè)計(jì)、調(diào)度和容災(zāi)等諸多瑣碎的問題。下面來看看使用ZooKeeper實(shí)現(xiàn)的另一種集群機(jī)器
存活性監(jiān)控系統(tǒng)。針對(duì)這個(gè)系統(tǒng),我們的需求點(diǎn)通常如下。

  • 如何快速地統(tǒng)計(jì)當(dāng)前生產(chǎn)環(huán)境一共有多少臺(tái)機(jī)器?
  • 如何快速地獲取到機(jī)器上/下線的情況?
  • 如何實(shí)時(shí)監(jiān)控集群中每臺(tái)主機(jī)的運(yùn)行時(shí)狀態(tài)?

.1 機(jī)器上/下線

為了實(shí)現(xiàn)自動(dòng)化的線上運(yùn)維,我們必須對(duì)機(jī)器的上/下線情況有一個(gè)全局的監(jiān)控。通常在新增機(jī)器的時(shí)候,需要首先將指定的Agent部署到這些機(jī)器上去。
Agent部署啟動(dòng)之后,會(huì)首先向ZooKeeper的指定節(jié)點(diǎn)進(jìn)行注冊(cè),具體的做法就是在機(jī)器列表節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)子節(jié)點(diǎn),例如/XAE/machine/[Hostname]
(下文以“主機(jī)節(jié)點(diǎn)”代表這個(gè)節(jié)點(diǎn)),如下圖:


當(dāng)Agent在ZooKeeper上創(chuàng)建完這個(gè)臨時(shí)子節(jié)點(diǎn)后,對(duì)/XAE/machines節(jié)點(diǎn)關(guān)注的監(jiān)控中心就會(huì)接收到“子節(jié)點(diǎn)變更”事件,即上線通知,于是就可以對(duì)這個(gè)
新加入的機(jī)器開啟相應(yīng)的后臺(tái)管理邏輯。另一方面,監(jiān)控中心同樣可以獲取到機(jī)器下線的通知,這樣便實(shí)現(xiàn)了對(duì)機(jī)器上/下線的檢測(cè),同時(shí)能夠很容易地獲取
到在線的機(jī)器列表,對(duì)于大規(guī)模的擴(kuò)容和容量評(píng)估都有很大的幫助。

.2 機(jī)器監(jiān)控

對(duì)于一個(gè)在線云主機(jī)系統(tǒng),不僅要對(duì)機(jī)器的在線狀態(tài)進(jìn)行檢測(cè),還需要對(duì)機(jī)器的運(yùn)行時(shí)狀態(tài)進(jìn)行監(jiān)控。在運(yùn)行的過程中,Agent會(huì)定時(shí)將主機(jī)的運(yùn)行狀態(tài)信息
寫入ZooKeeper上的主機(jī)節(jié)點(diǎn),監(jiān)控中心通過訂閱這些節(jié)點(diǎn)的數(shù)據(jù)變更通知來間接地獲取主機(jī)的運(yùn)行時(shí)信息。

隨著分布式系統(tǒng)規(guī)模變得越來越龐大,對(duì)集群機(jī)器的監(jiān)控和管理顯得越來越重要。上面提到的這種借助ZooKeeper來實(shí)現(xiàn)的方式,不僅能夠?qū)崟r(shí)地檢測(cè)到集群
中機(jī)器的上/下線情況,而且能夠?qū)崟r(shí)地獲取到主機(jī)的運(yùn)行時(shí)信息,從而能夠構(gòu)建出一個(gè)大規(guī)模集群的主機(jī)圖譜。

6.1 Master選舉

Master選舉是一個(gè)在分布式系統(tǒng)中非常常見的應(yīng)用場(chǎng)景。分布式最核心的特性就是能夠?qū)⒕哂歇?dú)立計(jì)算能力的系統(tǒng)單元部署在不同的機(jī)器上,構(gòu)成一個(gè)完整的
分布式系統(tǒng)。而與此同時(shí),實(shí)際場(chǎng)景中往往也需要在這些分布在不同機(jī)器上的獨(dú)立系統(tǒng)單元中選出一個(gè)所謂的“老大”,在計(jì)算機(jī)科學(xué)中,我們稱之為“Master”。

在分布式系統(tǒng)中,Master往往用來協(xié)調(diào)集群中其他系統(tǒng)單元,具有對(duì)分布式系統(tǒng)狀態(tài)變更的決定權(quán)。例如,在一些讀寫分離的應(yīng)用場(chǎng)景中,客戶端的寫請(qǐng)求往往
是由Master來處理的;而在另一些場(chǎng)景中,Master則常常負(fù)責(zé)處理一些復(fù)雜的邏輯,并將處理結(jié)果同步給集群中其它系統(tǒng)單元。Master選舉可以說是ZooKeeper
最典型的應(yīng)用場(chǎng)景了,在本節(jié)中,我們就結(jié)合“一種海量數(shù)據(jù)處理與共享模型”這個(gè)具體例子來看看ZooKeeper在集群Master選舉中的應(yīng)用場(chǎng)景。

在分布式環(huán)境中,經(jīng)常會(huì)碰到這樣的應(yīng)用場(chǎng)景:集群中的所有系統(tǒng)單元需要對(duì)前端業(yè)務(wù)提供數(shù)據(jù),比如一個(gè)商品ID,或者是一個(gè)網(wǎng)站輪播廣告的廣告ID(通常
出現(xiàn)在一些廣告投放系統(tǒng)中)等,而這些商品ID或是廣告ID往往需要從一系列的海量數(shù)據(jù)處理中計(jì)算得到————這通常是一個(gè)非常耗費(fèi)I/O和CPU資源的過程。
鑒于該計(jì)算過程的復(fù)雜性,如果讓集群中的所有機(jī)器都執(zhí)行這個(gè)計(jì)算邏輯的話,那么將耗費(fèi)非常多的資源。一種比較好的方法就是只讓集群中的部分,甚至只
讓其中的一臺(tái)機(jī)器去處理數(shù)據(jù)計(jì)算,一旦計(jì)算出數(shù)據(jù)結(jié)果,就可以共享給整個(gè)集群中的其他所有客戶端機(jī)器,這樣可以大大減少重復(fù)勞動(dòng),提升性能。

這里我們以一個(gè)簡(jiǎn)單的廣告投放系統(tǒng)后臺(tái)場(chǎng)景為例來講解這個(gè)模型。整個(gè)系統(tǒng)大體上可以分成客戶端集群、分布式緩存系統(tǒng)、海量數(shù)據(jù)處理總線和ZooKeeper
四個(gè)部分,如下圖:


Client集群每天定時(shí)會(huì)通過ZooKeeper來實(shí)現(xiàn)Master選舉。選舉產(chǎn)生Master客戶端之后,這個(gè)Master就會(huì)負(fù)責(zé)進(jìn)行一系列的海量數(shù)據(jù)處理,最終計(jì)算得到
一個(gè)數(shù)據(jù)結(jié)果,并將其放置在一個(gè)內(nèi)存/數(shù)據(jù)庫中。同時(shí),Master還需要通知集群中其它所有的客戶端從這個(gè)內(nèi)存/數(shù)據(jù)庫中共享計(jì)算結(jié)果。

接下去,我們將重點(diǎn)來看Master選舉的過程,首先來明確下Master選舉的需求:在集群的所有機(jī)器中選舉出一臺(tái)機(jī)器作為Master。針對(duì)這個(gè)需求,通常情況
下,我們可以選擇常見的關(guān)系型數(shù)據(jù)庫中的主鍵特性來實(shí)現(xiàn):集群中的所有機(jī)器都向數(shù)據(jù)庫中插入一條相同主鍵ID的記錄,數(shù)據(jù)庫會(huì)幫助我們自動(dòng)進(jìn)行主鍵沖突
檢查,也就是說,所有進(jìn)行插入操作的客戶端機(jī)器中,只有一臺(tái)機(jī)器能夠成功————那么,我們就認(rèn)為向數(shù)據(jù)庫中成功插入數(shù)據(jù)的客戶端機(jī)器成為Master。

乍一看,這個(gè)方案確實(shí)可行,依靠關(guān)系型數(shù)據(jù)庫的主鍵特性能夠很好地保證在集群中選舉出唯一的一個(gè)Master。但是我們需要考慮的另一個(gè)問題是,如果當(dāng)前
選舉出的Master掛了,那么該如何處理?誰來告訴我Master掛了呢?顯然,關(guān)系型數(shù)據(jù)庫沒法通知我們這個(gè)事件。

ZooKeeper的強(qiáng)一致性,能夠很好地保證在分布式高并發(fā)情況下節(jié)點(diǎn)的創(chuàng)建一定能夠保證全局唯一性,即ZooKeeper將會(huì)保證客戶端無法重復(fù)創(chuàng)建一個(gè)已經(jīng)存在
的數(shù)據(jù)節(jié)點(diǎn)。也就是說,如果同時(shí)有多個(gè)客戶端請(qǐng)求創(chuàng)建同一個(gè)節(jié)點(diǎn),那么最終一定只有一個(gè)客戶端請(qǐng)求能夠創(chuàng)建成功。利用這個(gè)特性,就能很容易地在分布式
環(huán)境中進(jìn)行Master選舉了。

在這個(gè)系統(tǒng)中,首先會(huì)在ZooKeeper上創(chuàng)建一個(gè)日期節(jié)點(diǎn),如下圖:


客戶端集群每天都會(huì)定時(shí)往ZooKeeper上創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),例如/master_election/2017-09-03/binding。在這個(gè)過程中,只有一個(gè)客戶端能夠成功
創(chuàng)建這個(gè)節(jié)點(diǎn),那么這個(gè)客戶端所在機(jī)器就稱為了Master。同時(shí),其他沒有在ZooKeeper上成功創(chuàng)建節(jié)點(diǎn)的客戶端,都會(huì)在節(jié)點(diǎn)/master_ecection/2017-09-03
上注冊(cè)一個(gè)子節(jié)點(diǎn)變更的Watcher,用于監(jiān)控當(dāng)前的Master機(jī)器是否存活,一旦發(fā)現(xiàn)當(dāng)前的Master掛了,那么其余的客戶端將會(huì)重新進(jìn)行Master選舉。

從上面的講解中,我們可以看到,如果僅僅只是想實(shí)現(xiàn)Master選舉的話,那么其實(shí)只需要有一個(gè)能夠保證唯一性的組件即可,例如關(guān)系型數(shù)據(jù)庫的主鍵模型
就是不錯(cuò)的選擇。但是,如果希望能夠快速地進(jìn)行集群Master動(dòng)態(tài)選舉,那么基于ZooKeeper來實(shí)現(xiàn)是一個(gè)不錯(cuò)的新思路。

7.1 分布式鎖

分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的
時(shí)候,往往需要通過一些互斥手段來防止彼此之間的干擾,以保證一致性,在這種情況下,就需要使用分布式鎖了。

在平時(shí)的實(shí)際項(xiàng)目開發(fā)中,我們往往很少會(huì)去在意分布式鎖,而是依賴于關(guān)系型數(shù)據(jù)庫固有的排他性來實(shí)現(xiàn)不同進(jìn)程之間的互斥。這確實(shí)是一種非常簡(jiǎn)便且被
廣泛使用的分布式鎖實(shí)現(xiàn)方式。然而有一個(gè)不爭(zhēng)的事實(shí)是,目前絕大多數(shù)大型分布式系統(tǒng)的性能瓶頸都集中在數(shù)據(jù)庫操作上。因此,如果上層業(yè)務(wù)再給數(shù)據(jù)庫
添加一些額外的鎖,例如行鎖、表鎖甚至是繁重的事務(wù)處理,那么是不是會(huì)讓數(shù)據(jù)庫更加不堪重負(fù)呢?下面我們來看看使用ZooKeeper如何實(shí)現(xiàn)分布式鎖,
這里主要講解排他鎖和共享鎖兩類分布式鎖。

7.1.1 排他鎖

排他鎖(Exclusive Locks,簡(jiǎn)稱X鎖),又稱為寫鎖或獨(dú)占鎖,是一種基本的鎖類型。如果事務(wù)T1對(duì)數(shù)據(jù)對(duì)象O1加上了排他鎖,那么在整個(gè)加鎖期間,只允許
事務(wù)T1對(duì)O1進(jìn)行讀取和更新操作,其他任何事務(wù)都不能再對(duì)這個(gè)數(shù)據(jù)對(duì)象進(jìn)行任何類型的操作————直到T1釋放了排他鎖。

從上面講解的排他鎖的基本概念中,我們可以看到,排他鎖的核心是如何保證當(dāng)前有且僅有一個(gè)事務(wù)獲得鎖,并且鎖被釋放后,所有正在等待獲取鎖的事務(wù)都
能夠被通知到。下面我們就看看如何借助ZooKeeper實(shí)現(xiàn)排他鎖。

.1 定義鎖

有兩種常見的方式可以用來定義鎖,分別是synchronized機(jī)制和JDK5提供的ReentrantLock。然而,在ZooKeeper中,沒有類似于這樣的API可以直接使用,
而是通過ZooKeeper上的數(shù)據(jù)節(jié)點(diǎn)來表示一個(gè)鎖,例如/exclusive_lock/lock節(jié)點(diǎn)就可以被定義為一個(gè)鎖,如下圖:

.2 獲取鎖

在需要獲取排他鎖時(shí),所有的客戶端都會(huì)試圖通過調(diào)用create()接口,在/exclusive_lock節(jié)點(diǎn)下創(chuàng)建臨時(shí)子節(jié)點(diǎn)/exclusive_lock/lock。而ZooKeeper
會(huì)保證在所有的客戶端中,最終只有一個(gè)客戶端能夠創(chuàng)建成功,那么就可以認(rèn)為該客戶端獲取了鎖。同時(shí),所有沒有獲取到鎖的客戶端就需要到/exclusive_lock
節(jié)點(diǎn)上注冊(cè)一個(gè)子節(jié)點(diǎn)變更的Watcher監(jiān)聽,以便實(shí)時(shí)監(jiān)聽到lock節(jié)點(diǎn)的變更情況。

.3 釋放鎖

由于是臨時(shí)節(jié)點(diǎn),有下面兩種情況,可能釋放鎖:

  • 當(dāng)前獲取鎖的客戶端機(jī)器發(fā)生宕機(jī)
  • 正常執(zhí)行完業(yè)務(wù)邏輯后,客戶端主動(dòng)將臨時(shí)節(jié)點(diǎn)刪除。

無論在上面情況下移除了lock節(jié)點(diǎn),ZooKeeper都會(huì)通知所有在/exclusive_lock節(jié)點(diǎn)上注冊(cè)了子節(jié)點(diǎn)變更Watcher監(jiān)聽的客戶端。這些客戶端在接收到通知后,
再次重新發(fā)起分布式鎖獲取,即重復(fù)“獲取鎖”過程。如下圖:

7.1.2 共享鎖

共享鎖(Shared Locks,簡(jiǎn)稱S鎖),又稱讀鎖,同樣是一種基本的鎖類型。如果事務(wù)T1對(duì)數(shù)據(jù)對(duì)象O1加上了共享鎖,那么當(dāng)前事務(wù)只能對(duì)O1進(jìn)行讀取操作,
其他事務(wù)也只能對(duì)這個(gè)數(shù)據(jù)對(duì)象加共享鎖————直到該數(shù)據(jù)對(duì)象上的所有共享鎖都被釋放。

共享鎖和排他鎖最根本的區(qū)別在于,加上排他鎖后,數(shù)據(jù)對(duì)象只對(duì)一個(gè)事務(wù)可見,而加上共享鎖后,數(shù)據(jù)對(duì)所有事務(wù)都可見。

.1 定義鎖

和排他鎖一樣,同樣是通過ZooKeeper上的數(shù)據(jù)節(jié)點(diǎn)來表示一個(gè)鎖,是一個(gè)類似于/shared_lock/[Hostname]-請(qǐng)求類型-序號(hào)的臨時(shí)順序節(jié)點(diǎn),例如
/shared_lock/192.168.0.1-R-0000000001,那么,這個(gè)節(jié)點(diǎn)就代表了一個(gè)共享鎖,如下圖:

.2 獲取鎖

在需要獲取共享鎖時(shí),所有客戶端都會(huì)到/shared_lock這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),如果當(dāng)前是讀請(qǐng)求,那么就創(chuàng)建例如/shared_lock/192.168.0.1-R-000000001/
的節(jié)點(diǎn);如果是寫請(qǐng)求,那么就創(chuàng)建例如/shared_lock/192.168.0.1-W-000000001的節(jié)點(diǎn)。

.3 判斷讀寫順序

根據(jù)共享鎖的定義,不同的事務(wù)都可以同時(shí)對(duì)同一數(shù)據(jù)對(duì)象進(jìn)行讀取操作,而更新操作必須在當(dāng)前沒有任何事務(wù)進(jìn)行讀寫操作的情況下進(jìn)行?;谶@個(gè)原則,
我們來看看如何通過ZooKeeper的節(jié)點(diǎn)來確定分布式讀寫順序,大致可以分為如下4個(gè)步驟。

  1. 創(chuàng)建完節(jié)點(diǎn)后,獲取/shared_lock節(jié)點(diǎn)下的所有子節(jié)點(diǎn),并對(duì)該節(jié)點(diǎn)注冊(cè)子節(jié)點(diǎn)變更的Watcher監(jiān)聽。
  2. 確定自己的節(jié)點(diǎn)序號(hào)在所有子節(jié)點(diǎn)中的順序。
  3. 如果當(dāng)前節(jié)點(diǎn)業(yè)務(wù)為讀請(qǐng)求:如果沒有比自己序號(hào)小的子節(jié)點(diǎn),或是所有比自己序號(hào)小的子節(jié)點(diǎn)都是讀請(qǐng)求,那么表明自己已經(jīng)成功獲取到了共享鎖,同時(shí)
    開始執(zhí)行讀取邏輯。如果比自己序號(hào)小的子節(jié)點(diǎn)有寫請(qǐng)求,那么就需要進(jìn)入等待。
    如果當(dāng)前節(jié)點(diǎn)業(yè)務(wù)為寫請(qǐng)求:如果自己不是序號(hào)最小的子節(jié)點(diǎn), 那么就需要進(jìn)入等待。
  4. 接收到Watcher通知后,重復(fù)步驟1。

.4 釋放鎖

釋放鎖的邏輯和排他鎖是一致的。

.5 羊群效應(yīng)

上面講解的這個(gè)共享鎖實(shí)現(xiàn),大體上能夠滿足一般的分布式集群競(jìng)爭(zhēng)鎖的需求,并且性能都還可以————這里說的一般場(chǎng)景是指集群規(guī)模不是特別大,一般是在
10臺(tái)機(jī)器以內(nèi)。但是如果機(jī)器規(guī)模擴(kuò)大之后,會(huì)有什么問題呢?我們著重來看上面“判斷讀寫順序”過程的步驟3,如下圖,看看實(shí)際運(yùn)行中的情況。

  1. 192.168.0.1這臺(tái)機(jī)器首先進(jìn)行讀操作,完成讀操作后將節(jié)點(diǎn)/192.168.0.1-R-000000001刪除。
  2. 余下的4臺(tái)機(jī)器均收到了這個(gè)節(jié)點(diǎn)被移除的通知,然后重新從/shared_lock/節(jié)點(diǎn)上獲取一份新的子節(jié)點(diǎn)列表。
  3. 每個(gè)機(jī)器判斷自己的讀寫順序。其中192.168.0.2這臺(tái)機(jī)器檢測(cè)到自己已經(jīng)是序號(hào)最小的機(jī)器了,于是開始進(jìn)行寫操作,而余下的其他機(jī)器發(fā)現(xiàn)沒有輪到
    自己進(jìn)行讀取或更新操作,于是繼續(xù)等待。
  4. 繼續(xù)......

上面這個(gè)過程就是共享鎖在實(shí)際運(yùn)行中最主要的步驟了,我們著重看下上面步驟3中提到的:“而余下的其他機(jī)器發(fā)現(xiàn)沒有輪到自己進(jìn)行讀取或更新操作,于是繼續(xù)等待。”
很明顯,我們看到,192.168.0.1這個(gè)客戶端在移除自己的共享鎖后,ZooKeeper發(fā)送了子節(jié)點(diǎn)變更Watcher通知給所有機(jī)器,然而這個(gè)通知除了給192.168.0.2
這臺(tái)機(jī)器產(chǎn)生實(shí)際影響外,對(duì)于余下的其他所有機(jī)器都沒有任何作用。

相信讀者也已經(jīng)意思到了,在這整個(gè)分布式鎖的競(jìng)爭(zhēng)過程中,大量的“Watcher通知”和“子節(jié)點(diǎn)列表獲取”兩個(gè)操作重復(fù)運(yùn)行,并且絕大多數(shù)的運(yùn)行結(jié)果都是
判斷出自己并非是序號(hào)最小的節(jié)點(diǎn),從而繼續(xù)等待下一次通知————這個(gè)看起來顯然不怎么科學(xué)??蛻舳藷o端地接收到過多和自己并不相關(guān)的事件通知,如果在集群
規(guī)模比較大的情況下,不僅會(huì)對(duì)ZooKeeper服務(wù)器造成巨大的性能影響和網(wǎng)絡(luò)沖擊,更為嚴(yán)重的是,如果同一時(shí)間有多個(gè)節(jié)點(diǎn)對(duì)應(yīng)的客戶端完成事務(wù)或是事務(wù)
中斷引起節(jié)點(diǎn)消息,ZooKeeper服務(wù)器就會(huì)在短時(shí)間內(nèi)向其余客戶端發(fā)送大量的事件通知————這就是所謂的羊群效應(yīng)。

上面這個(gè)ZooKeeper分布式共享鎖實(shí)現(xiàn)中出現(xiàn)羊群效應(yīng)的根源在于,沒有找準(zhǔn)客戶端真正的關(guān)注點(diǎn)。我們?cè)賮砘仡櫼幌律厦娴姆植际芥i競(jìng)爭(zhēng)過程,它和核心
邏輯在于:判斷自己是否是所有子節(jié)點(diǎn)中序號(hào)最小的。于是,很容易可以聯(lián)想到,每個(gè)節(jié)點(diǎn)對(duì)應(yīng)的客戶端只需要關(guān)注比自己序號(hào)小的那個(gè)相關(guān)節(jié)點(diǎn)的變更情況
就可以了————而不需要關(guān)注全局的子列表變更情況。

.6 改進(jìn)后的分布式鎖實(shí)現(xiàn)

現(xiàn)在我們來看看如何改進(jìn)上面的分布式鎖實(shí)現(xiàn)。首先,我們需要肯定的一點(diǎn)是,上面提到的共享鎖實(shí)現(xiàn),從整體思路上來說完全正確。這里主要的改動(dòng)在于:
每個(gè)鎖競(jìng)爭(zhēng)者,只需要關(guān)注/shared_lock/節(jié)點(diǎn)下序號(hào)比自己小的那個(gè)節(jié)點(diǎn)是否存在即可,具體實(shí)現(xiàn)如下:

  1. 客戶端調(diào)用create()方法創(chuàng)建一個(gè)類似于/shared_lock/[Hostname]-請(qǐng)求類型-序號(hào)的臨時(shí)順序節(jié)點(diǎn)。
  2. 客戶端調(diào)用getChildren()接口來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)列表,注意,這里不注冊(cè)任何Watcher。
  3. 如果無法獲取共享鎖,那么就調(diào)用exist()來對(duì)比自己小的那個(gè)節(jié)點(diǎn)注冊(cè)Watcher。注意,這里“比自己小的節(jié)點(diǎn)”只是一個(gè)籠統(tǒng)的說法,具體對(duì)于讀請(qǐng)求和寫請(qǐng)求不一樣。
    讀請(qǐng)求:向比自己序號(hào)小的最后一個(gè)寫請(qǐng)求節(jié)點(diǎn)注冊(cè)Watcher監(jiān)聽。
    寫請(qǐng)求:向比自己序號(hào)小的最后一個(gè)節(jié)點(diǎn)注冊(cè)Watcher監(jiān)聽。
  4. 等待Watcher通知,繼續(xù)進(jìn)入步驟2。

流程圖如下:


.7 注意

看到這里,相信很多讀者都會(huì)覺得改進(jìn)后的分布式鎖實(shí)現(xiàn)相對(duì)來說比較麻煩。確實(shí)如此,如同在多線程并發(fā)編程實(shí)踐中,我們會(huì)去盡量縮小鎖的范圍————對(duì)于
分布式鎖實(shí)現(xiàn)的改進(jìn)其實(shí)也是同樣的思路。那么對(duì)于開發(fā)人員來說,是否必須按照改進(jìn)后的思路來設(shè)計(jì)實(shí)現(xiàn)自己的分布式鎖呢?答案是否定的。在具體的實(shí)際開發(fā)
過程中,我們提倡根據(jù)具體的業(yè)務(wù)場(chǎng)景和集群規(guī)模來選擇適合自己的分布式鎖實(shí)現(xiàn):在集群規(guī)模不大、網(wǎng)絡(luò)資源豐富的情況下,第一種分布式鎖實(shí)現(xiàn)方式是
簡(jiǎn)單實(shí)用的選擇;而如果集群規(guī)模達(dá)到一定程度,并且希望能夠精細(xì)化地控制分布式鎖機(jī)制,那么不妨試試改進(jìn)版的分布式鎖實(shí)現(xiàn)。

8.1 分布式隊(duì)列

業(yè)界有不少分布式隊(duì)列產(chǎn)品,不過絕大多數(shù)都是類似于ActiveMQ、Kafka等的消息中間件。在本節(jié)中,我們主要介紹基于ZooKeeper實(shí)現(xiàn)的分布式隊(duì)列。
分布式隊(duì)列,簡(jiǎn)單地講分為兩大類,一種是常規(guī)的先入先出隊(duì)列,另一種則是要等到隊(duì)列元素集聚之后才統(tǒng)一安排執(zhí)行的Barrier模型。

8.1.1 FIFO:先進(jìn)先出

使用ZooKeeper實(shí)現(xiàn)FIFO隊(duì)列,和共享鎖的實(shí)現(xiàn)非常類似。FIFO隊(duì)列就類似于一個(gè)全寫的共享鎖模型,大體的設(shè)計(jì)思想其實(shí)非常簡(jiǎn)單:所有客戶端都會(huì)到
/queue_fifo這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),例如/queue_fifo/192.168.0.1-0000000001,如下圖:


創(chuàng)建完節(jié)點(diǎn)之后,根據(jù)如下4個(gè)步驟來確定執(zhí)行順序。

  1. 通過調(diào)用getChildren()接口來獲取/queue_fifo節(jié)點(diǎn)下的所有子節(jié)點(diǎn),即獲取隊(duì)列中所有的元素。
  2. 確定自己的節(jié)點(diǎn)序號(hào)在所有子節(jié)點(diǎn)中的順序。
  3. 如果自己不是序號(hào)最小的子節(jié)點(diǎn),那么就需要進(jìn)入等待,同時(shí)向比自己序號(hào)小的最后一個(gè)節(jié)點(diǎn)注冊(cè)Watcher監(jiān)聽。
  4. 接收到Watcher通知到,重復(fù)步驟1。

整個(gè)FIFO隊(duì)列的工作流程,如下圖:


8.1.2 Barrier:分布式屏障

Barrier原意是指障礙物、屏障,而在分布式系統(tǒng)中,特指系統(tǒng)之間的一個(gè)協(xié)調(diào)條件,規(guī)定了一個(gè)隊(duì)列的元素必須都集聚后才能統(tǒng)一進(jìn)行安排,否則一直等待。
這往往出現(xiàn)在那些大規(guī)模分布式并行計(jì)算的應(yīng)用場(chǎng)景了:最終的合并計(jì)算需要基于很多并行計(jì)算的子結(jié)果來進(jìn)行。這些隊(duì)列其實(shí)是FIFO隊(duì)列的基礎(chǔ)上進(jìn)行了
增強(qiáng),大致的設(shè)計(jì)思想如下:開始時(shí),/queue_barrier節(jié)點(diǎn)是一個(gè)已經(jīng)存在的默認(rèn)節(jié)點(diǎn),并且將其節(jié)點(diǎn)的數(shù)據(jù)內(nèi)容賦值為一個(gè)數(shù)字n來代表Barrier值,
例如n=10表示只有當(dāng)/queue_barrier節(jié)點(diǎn)下的子節(jié)點(diǎn)個(gè)數(shù)達(dá)到10后,才會(huì)打開Barrier。之后,所有的客戶端都會(huì)到/queue_barrier節(jié)點(diǎn)下創(chuàng)建一個(gè)
臨時(shí)節(jié)點(diǎn),例如/queue_barrier/192.168.0.1,如下圖:


創(chuàng)建完節(jié)點(diǎn)之后,根據(jù)如下5個(gè)步驟來確定執(zhí)行順序。

  1. 通過調(diào)用getDate()接口獲取/queue_barrier節(jié)點(diǎn)的數(shù)據(jù)內(nèi)容:10。
  2. 通過調(diào)用getChildren()接口獲取/queue_barrier節(jié)點(diǎn)下的所有子節(jié)點(diǎn),即獲取隊(duì)列中所有元素,同時(shí)注冊(cè)對(duì)子節(jié)點(diǎn)列表變更的Watcher監(jiān)聽。
  3. 統(tǒng)計(jì)子節(jié)點(diǎn)的個(gè)數(shù)。
  4. 如果子節(jié)點(diǎn)個(gè)數(shù)還不足10個(gè),那么就需要進(jìn)入等待。
  5. 接收到Watcher通知后,重復(fù)步驟2。

    博主理解為,如果在很少的時(shí)間內(nèi),同時(shí)超過了10個(gè)以上的業(yè)務(wù)機(jī)創(chuàng)建了臨時(shí)節(jié)點(diǎn),那么業(yè)務(wù)處理的速度并不是恒定的,因?yàn)橛锌赡苓@個(gè)業(yè)務(wù)被11個(gè)機(jī)器處理,
    下一個(gè)被12個(gè)業(yè)務(wù)機(jī)處理?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容