1. 程式人生 > >Java 開發基於Zookeeper,Spring,vue.js的高併發多使用者模組化微信商城系統(二) Zookeeper原理

Java 開發基於Zookeeper,Spring,vue.js的高併發多使用者模組化微信商城系統(二) Zookeeper原理

一、分散式協調技術
在介紹Zookeeper之前需要先介紹一種技術–分散式協調技術。在介紹分散式協調技術之前,也有必要介紹一下什麼是分散式系統。關於分散式系統,《分散式系統原理和範型》一書中是這樣定義分散式系統的:“分散式系統是若干獨立計算機的集合,這些計算機對於使用者來說就像是單個相關係統”。
關於這個定義,我們直觀的感受就是:
首先,這種系統相對來說比較牛逼,起碼由好幾臺主機組成。以谷歌、亞馬遜等服務商而言,他們的資料中心都由上萬臺主機支撐起來的。
其次,雖然很牛逼,但對於外人來說,是感覺不到這些主機的存在。也就是說,我們只看到是一個系統在運作。以最近的“亞馬遜 S3 宕機事件”為例,平時,我們壓根不知道亞馬遜所提供的服務背後是由多少臺主機組成,但是等到 S3 宕機才知道,這貨已經是佔了網際網路世界的半壁江山了。
從程序角度看,兩個程式分別執行在兩個臺主機的程序上,它們相互協作最終完成同一個服務(或者功能),那麼理論上這兩個程式所組成的系統,也可以稱作是“分散式系統”。

當然,這個兩個程式可以是不同的程式,也可以是相同的程式。如果是相同的程式,我們又可以稱之為“叢集”。所謂叢集,就是將相同的程式,通過不斷橫向擴充套件,以提高服務能力的方式。換一種更簡單的方法來理解就是:
分散式:一個業務分拆多個子業務,部署在不同的伺服器上
叢集:同一個業務,部署在多個伺服器上

舉個簡單的例子來理解:
小飯店原來只有一個廚師,切菜洗菜備料炒菜全乾。後來客人多了,廚房一個廚師忙不過來,又請了個廚師,兩個廚師都能炒一樣的菜,這兩個廚師的關係是叢集。為了讓廚師專心炒菜,把菜做到極致,又請了個配菜師負責切菜,備菜,備料,廚師和配菜師的關係是分散式,一個配菜師也忙不過來了,又請了個配菜師,兩個配菜師關係是叢集。

相對於分散式系統,早期使用PHP開發的商城系統使用的則是集中式系統了。我們把Web伺服器、資料庫等都會安裝到一臺電腦上。好處是,易於理解、方便維護,想要的東西我都放到了一個地方,東西好找啊。當然弊端也是顯而易見的,如果這臺機子崩了,或者硬碟壞了,那相當與整個系統就奔潰了,而且如果備份也是在這個硬碟上,那相當於招了滅頂之災。

巴菲特有個關於投資的名言,就是“不要把雞蛋放在一個籃子裡”。對於系統而言也是如此。廠商的機子不可能永遠保證永遠不壞,我們也無法保證黑客不會來對我們的系統搞基,最為關鍵的是,我們自己無法保證自己的程式不會出bug。所以問題無法避免,錯誤也不可避免。我們只能雞蛋分散到不同的籃子裡,來減輕一鍋端的風險。這就是為什麼需要分散式系統的原因。

使用分散式系統的另外一個理由是可擴充套件性。畢竟任何主機(哪怕是小型機、超級計算機)都會有效能的極限。而分散式系統可以通過不斷擴張主機的數量以實現橫向水平效能的擴充套件。大家也都瞭解到 Google 的伺服器主機,大多是淘汰的二線機子拼湊的。

毫無疑問,分散式系統對於集中式系統而言,在實現上會更加複雜。分散式系統將會是更難理解、設計、構建 和管理的,同時意味著應用程式的根源問題更難發現。

設計分散式系統時,經常需要考慮如下的挑戰:
異構性:分散式系統由於基於不同的網路、作業系統、計算機硬體和程式語言來構造,必須要考慮一種通用的網路通訊協議來遮蔽異構系統之間的差異。一般交由中介軟體來處理這些差異。
缺乏全球時鐘:在程式需要協作時,它們通過交換訊息來協調它們的動作。緊密的協調經常依賴於對程式動作發生時間的共識,但是,實際上網路上計算機同步時鐘的準確性受到極大的限制,即沒有一個正確時間的全域性概念。這是通過網路傳送訊息作為唯一的通訊方式這一事實帶來的直接結果。
一致性:資料被分散或者複製到不同的機器上,如何保證各臺主機之間的資料的一致性將成為一個難點。
故障的獨立性:任何計算機都有可能故障,且各種故障不盡相同。他們之間出現故障的時機也是相互獨立的。一般分散式系統要設計成被允許出現部分故障而不影響整個系統的正常使用。
併發:分散式系統的目的,是為了更好的共享資源。那麼系統中的每個資源都必須被設計成在併發環境中是安全的。
透明性:分散式系統中任何元件的故障、或者主機的升級、遷移對於使用者來說都是透明的,不可見的。
開放性:分散式系統由不同的程式設計師來編寫不同的元件,元件最終要整合成為一個系統,那麼元件所釋出的介面必須遵守一定的規範且能夠被互相理解。
安全性:加密用於給共享資源提供適當的保護,在網路上所有傳遞的敏感資訊,都需要進行加密。拒絕服務攻擊仍然是一個有待解決的問題。
可擴充套件性:系統要設計成隨著業務量的增加,相應的系統也必須要能擴充套件來提供對應的服務。

結合以上的問題,我們回過頭來再來思考,什麼是分散式協調技術?分散式協調技術,主要用來解決分散式環境當中多個程序之間的同步控制,讓他們有序的去訪問某種臨界資源,防止造成”髒資料”的後果。
這裡寫圖片描述
在這圖中有三臺機器,每臺機器各跑一個應用程式。然後我們將這三臺機器通過網路將其連線起來,構成一個系統來為使用者提供服務,對使用者來說這個系統的架構是透明的,使用者感覺不到這個系統是一個什麼樣的架構。那麼我們就可以把這種系統稱作一個分散式系統。

那我們接下來再分析一下,在這個分散式系統中如何對程序進行排程,假設在第一臺機器上掛載了一個資源,然後這三個物理分佈的程序都要競爭這個資源,但又不希望他們同時進行訪問,這時候我們就需要一個協調器,來讓他們有序的來訪問這個資源。這個協調器就是我們經常提到的那個鎖,比如說”程序-1”在使用該資源的時候,會先去獲得鎖,”程序1”獲得鎖以後會對該資源保持獨佔,這樣其他程序就無法訪問該資源,”程序1”用完該資源以後就將鎖釋放掉,讓其他程序來獲得鎖,那麼通過這個鎖機制,我們就能保證了分散式系統中多個程序能夠有序的訪問該臨界資源。那麼我們把這個分散式環境下的這個鎖叫作分散式鎖。這個分散式鎖也就是分散式協調技術實現的核心內容。

有人可能會感覺這不是很難。無非是將原來在同一臺機器上對程序排程的原語,通過網路實現在分散式環境中。是的,表面上是可以這麼說。但是問題就在網路這,在分散式系統中,所有在同一臺機器上的假設都不存在:因為網路是不可靠的。

比如,在同一臺機器上,你對一個服務的呼叫如果成功,那就是成功,如果呼叫失敗,比如丟擲異常那就是呼叫失敗。但是在分散式環境中,由於網路的不可 靠,你對一個服務的呼叫失敗了並不表示一定是失敗的,可能是執行成功了,但是響應返回的時候失敗了。還有,A和B都去呼叫C服務,在時間上 A還先呼叫一些,B後呼叫,那麼最後的結果是不是一定A的請求就先於B到達呢? 這些在同一臺機器上的種種假設,我們都要重新思考,我們還要思考這些問題給我們的設計和編碼帶來了哪些影響。還有,在分散式環境中為了提升可靠性,我們往 往會部署多套服務,但是如何在多套服務中達到一致性,這在同一臺機器上多個程序之間的同步相對來說比較容易辦到,但在分散式環境中確實一個大難題。

所以分散式協調遠比在同一臺機器上對多個程序的排程要難得多,而且如果為每一個分散式應用都開發一個獨立的協調程式。一方面,協調程式的反覆編寫浪 費,且難以形成通用、伸縮性好的協調器。另一方面,協調程式開銷比較大,會影響系統原有的效能。所以,急需一種高可靠、高可用的通用協調機制來用以協調分散式應用。

目前,在分散式協調技術方面做得比較好的就是Google的Chubby還有Apache的ZooKeeper他們都是分散式鎖的實現者。有人會問既然有了Chubby為什麼還要弄一個ZooKeeper,難道Chubby做得不夠好嗎?主要是Chbby是非開源的,Google自家用。後來雅虎模仿Chubby開發出了ZooKeeper,也實現了類似的分散式鎖的功能,並且將ZooKeeper作為一種開源的程式捐獻給了 Apache,那麼這樣就可以使用ZooKeeper所提供鎖服務。而且在分散式領域久經考驗,它的可靠性,可用性都是經過理論和實踐的驗證的。所以我們 在構建一些分散式系統的時候,就可以以這類系統為起點來構建我們的系統,這將節省不少成本,而且bug也 將更少。

二、ZooKeeper概述

ZooKeeper是一種為分散式應用所設計的高可用、高效能且一致的開源協調服務,它提供了一項基本服務:分散式鎖服務。由於ZooKeeper的開源特性,後來我們的開發者在分散式鎖的基礎上,摸索了出了其他的使用方法:配置維護、組服務、分散式訊息佇列、分散式通知/協調等。

注意:ZooKeeper效能上的特點決定了它能夠用在大型的、分散式的系統當中。從可靠性方面來說,它並不會因為一個節點的錯誤而崩潰。除此之外,它嚴格的序列訪問控制意味著複雜的控制原語可以應用在客戶端上。ZooKeeper在一致性、可用性、容錯性的保證,也是ZooKeeper的成功之處,它獲得的一切成功都與它採用的協議——Zab協議是密不可分的,這些內容將會在後面介紹。

前面提到了那麼多的服務,比如分散式鎖、配置維護、組服務等,那它們是如何實現的呢,我相信這才是大家關心的東西。ZooKeeper在實現這些服務時,首先它設計一種新的資料結構——Znode,然後在該資料結構的基礎上定義了一些原語,也就是一些關於該資料結構的一些操作。有了這些資料結構和原語還不夠,因為我們的ZooKeeper是工作在一個分散式的環境下,我們的服務是通過訊息以網路的形式傳送給我們的分散式應用程式,所以還需要一個通知機制——Watcher機制。那麼總結一下,ZooKeeper所提供的服務主要是通過:資料結構+原語+watcher機制三個部分來實現的。

三、ZooKeeper資料模型
ZooKeeper擁有一個層次的名稱空間,這個和標準的檔案系統非常相似
這裡寫圖片描述

與檔案系統目錄樹對比:
這裡寫圖片描述

從圖中我們可以看出ZooKeeper的資料模型,在結構上和標準檔案系統的非常相似,都是採用這種樹形層次結構,ZooKeeper樹中的每個節點被稱為—Znode。和檔案系統的目錄樹一樣,ZooKeeper樹中的每個節點可以擁有子節點。但也有不同之處:

(1) 引用方式

Zonde通過路徑引用,如同Unix中的檔案路徑。路徑必須是絕對的,因此他們必須由斜槓字元來開頭。除此以外,他們必須是唯一的,也就是說每一個路徑只有一個表示,因此這些路徑不能改變。在ZooKeeper中,路徑由Unicode字串組成,並且有一些限制。字串”/zookeeper”用以儲存管理資訊,比如關鍵配額資訊。

(2) Znode結構

ZooKeeper名稱空間中的Znode,兼具檔案和目錄兩種特點。既像檔案一樣維護著資料、元資訊、ACL、時間戳等資料結構,又像目錄一樣可以作為路徑標識的一部分。圖中的每個節點稱為一個Znode。 每個Znode由3部分組成:

① stat:此為狀態資訊, 描述該Znode的版本, 許可權等資訊

② data:與該Znode關聯的資料

③ children:該Znode下的子節點

ZooKeeper雖然可以關聯一些資料,但並沒有被設計為常規的資料庫或者大資料儲存,相反的是,它用來管理排程資料,比如分散式應用中的配置檔案資訊、狀態資訊、彙集位置等等。這些資料的共同特性就是它們都是很小的資料,通常以KB為大小單位。ZooKeeper的伺服器和客戶端都被設計為嚴格檢查並限制每個Znode的資料大小至多1M,但常規使用中應該遠小於此值。

(3) 資料訪問

ZooKeeper中的每個節點儲存的資料要被原子性的操作。也就是說讀操作將獲取與節點相關的所有資料,寫操作也將替換掉節點的所有資料。另外,每一個節點都擁有自己的ACL(訪問控制列表),這個列表規定了使用者的許可權,即限定了特定使用者對目標節點可以執行的操作。

(4) 節點型別

ZooKeeper中的節點有兩種,分別為臨時節點和永久節點。節點的型別在建立時即被確定,並且不能改變。

① 臨時節點:該節點的生命週期依賴於建立它們的會話。一旦會話(Session)結束,臨時節點將被自動刪除,當然可以也可以手動刪除。雖然每個臨時的Znode都會繫結到一個客戶端會話,但他們對所有的客戶端還是可見的。另外,ZooKeeper的臨時節點不允許擁有子節點。

② 永久節點:該節點的生命週期不依賴於會話,並且只有在客戶端顯示執行刪除操作的時候,他們才能被刪除。

(5) 順序節點

當建立Znode的時候,使用者可以請求在ZooKeeper的路徑結尾新增一個遞增的計數。這個計數對於此節點的父節點來說是唯一的,它的格式為”%10d”(10位數字,沒有數值的數位用0補充,例如”0000000001”)。當計數值大於232-1時,計數器將溢位。

(6) 觀察

客戶端可以在節點上設定watch,我們稱之為監視器。當節點狀態發生改變時(Znode的增、刪、改)將會觸發watch所對應的操作。當watch被觸發時,ZooKeeper將會向客戶端傳送且僅傳送一條通知,因為watch只能被觸發一次,這樣可以減少網路流量。

ZooKeeper中的時間
ZooKeeper有多種記錄時間的形式,其中包含以下幾個主要屬性:

(1) Zxid

致使ZooKeeper節點狀態改變的每一個操作都將使節點接收到一個Zxid格式的時間戳,並且這個時間戳全域性有序。也就是說,也就是說,每個對 節點的改變都將產生一個唯一的Zxid。如果Zxid1的值小於Zxid2的值,那麼Zxid1所對應的事件發生在Zxid2所對應的事件之前。實際 上,ZooKeeper的每個節點維護者三個Zxid值,為別為:cZxid、mZxid、pZxid。

① cZxid: 是節點的建立時間所對應的Zxid格式時間戳。
② mZxid:是節點的修改時間所對應的Zxid格式時間戳。

實現中Zxid是一個64為的數字,它高32位是epoch用來標識leader關係是否改變,每次一個leader被選出來,它都會有一個 新的epoch。低32位是個遞增計數。 (2) 版本號

對節點的每一個操作都將致使這個節點的版本號增加。每個節點維護著三個版本號,他們分別為:

① version:節點資料版本號
② cversion:子節點版本號
③ aversion:節點所擁有的ACL版本號

ZooKeeper節點屬性
通過前面的介紹,我們可以瞭解到,一個節點自身擁有表示其狀態的許多重要屬性,如下圖所示
這裡寫圖片描述

四、ZooKeeper服務中操作
在ZooKeeper中有9個基本操作,如下圖所示:
這裡寫圖片描述

更新ZooKeeper操作是有限制的。delete或setData必須明確要更新的Znode的版本號,我們可以呼叫exists找到。如果版本號不匹配,更新將會失敗。

更新ZooKeeper操作是非阻塞式的。因此客戶端如果失去了一個更新(由於另一個程序在同時更新這個Znode),他可以在不阻塞其他程序執行的情況下,選擇重新嘗試或進行其他操作。

儘管ZooKeeper可以被看做是一個檔案系統,但是處於便利,摒棄了一些檔案系統地操作原語。因為檔案非常的小並且使整體讀寫的,所以不需要開啟、關閉或是尋地的操作。

五、Watch觸發器
(1) watch概述

ZooKeeper可以為所有的讀操作設定watch,這些讀操作包括:exists()、getChildren()及getData()。watch事件是一次性的觸發器,當watch的物件狀態發生改變時,將會觸發此物件上watch所對應的事件。watch事件將被非同步地傳送給客戶端,並且ZooKeeper為watch機制提供了有序的一致性保證。理論上,客戶端接收watch事件的時間要快於其看到watch物件狀態變化的時間。

(2) watch型別

ZooKeeper所管理的watch可以分為兩類:

① 資料watch(data watches):getData和exists負責設定資料watch
② 孩子watch(child watches):getChildren負責設定孩子watch

我們可以通過操作返回的資料來設定不同的watch:

① getData和exists:返回關於節點的資料資訊
② getChildren:返回孩子列表

因此

① 一個成功的setData操作將觸發Znode的資料watch

② 一個成功的create操作將觸發Znode的資料watch以及孩子watch

③ 一個成功的delete操作將觸發Znode的資料watch以及孩子watch

(3) watch註冊與處觸發
這裡寫圖片描述

① exists操作上的watch,在被監視的Znode建立、刪除或資料更新時被觸發。
② getData操作上的watch,在被監視的Znode刪除或資料更新時被觸發。在被建立時不能被觸發,因為只有Znode一定存在,getData操作才會成功。
③ getChildren操作上的watch,在被監視的Znode的子節點建立或刪除,或是這個Znode自身被刪除時被觸發。可以通過檢視watch事件型別來區分是Znode,還是他的子節點被刪除:NodeDelete表示Znode被刪除,NodeDeletedChanged表示子節點被刪除。

Watch由客戶端所連線的ZooKeeper伺服器在本地維護,因此watch可以非常容易地設定、管理和分派。當客戶端連線到一個新的伺服器 時,任何的會話事件都將可能觸發watch。另外,當從伺服器斷開連線的時候,watch將不會被接收。但是,當一個客戶端重新建立連線的時候,任何先前 註冊過的watch都會被重新註冊。

(4) 需要注意的幾點

Zookeeper的watch實際上要處理兩類事件:

① 連線狀態事件(type=None, path=null)

這類事件不需要註冊,也不需要我們連續觸發,我們只要處理就行了。

② 節點事件

節點的建立,刪除,資料的修改。它是one time trigger,我們需要不停的註冊觸發,還可能發生事件丟失的情況。

上面2類事件都在Watch中處理,也就是過載的process(Event event)

節點事件的觸發,通過函式exists,getData或getChildren來處理這類函式,有雙重作用:

① 註冊觸發事件

② 函式本身的功能

函式的本身的功能又可以用非同步的回撥函式來實現,過載processResult()過程中處理函式本身的的功能。