談點分布式
什么是分布式呢?
起初,我們的應用流量比較小,所有東西全部部署在一個服務器,比如全部丟給一個tomcat來處理,頂多做一個tomcat的多節點部署多分,再掛一台Nginx做一下負載均衡就OK了。但是隨着業務功能復雜度上升,訪問流程的上升,單體架構就不行了。這個時候就該分布式上場了,將業務模塊做一定拆分,各業務組件分布在網絡上不同的計算機節點上,同時為了保證高可用性和性能,單個組件模塊也會做集群部署。
分布式雖然爽了,但是隨之而來的就是分布式帶來的復雜性,比如在分布式系統中網絡故障問題幾乎是必然存在的;事務也不再是數據庫幫我們保證了,因為可能每個業務有自己的庫,但不同業務之間又有保證事務的需求,這是就需要考慮實現分布式事務了;還有數據一致性問題,集群中副本節點不能及時同步到主節點的數據,會有數據一致性問題需要解決。
分布式理論
實踐需要理論的知道,同樣地,在分布式軟件開發領域內,也是有前輩大神們做了基礎的理論研究。下面將要介紹的就是分布式相關的兩個基礎理論:CAP定理和BASE理論。
CAP定理
在聊CAP定理前,我們先簡單了解下分布式事務。數據庫的事務我們知道。假如銀行轉賬,轉出操作和轉入操作在同一個數據庫中,就很好實現了,只需要在方法上增加一個@Transactional,剩下的事情數據庫會幫我們做好。但是在分布式環境中,我們對比現實中的銀行轉賬,誇行轉賬,不止是誇庫,更是誇不同的銀行系統的。在這種場景,我們需要保證ACID的特性,就是需要分布式事務解決方案了。好了,這里只是做個了結。下面說CAP定理。
CAP定理是說,在一個分布式系統中,不可能同時滿足一致性(Consistency)、可用性(Availiablity)和分區容錯性(Partition tolerance)這三個基本的需求。最多只能滿足其中的兩項。
一致性
在分布式環境中,一致性是指數據在多個副本之間是否能夠保持一致的特性。在一致性的需求下,當一個系統在數據一致的狀態下執行更新操作后,應該保持系統的數據仍然處於一致的狀態。如果對第一個節點上的數據更新成功后,第二個節點上的數據並沒有得到相應的更新,那么如果從第二個節點讀取數據,則獲取到的就是舊數據(或者或是臟數據)。這就是典型的分布式數據不一致的場景。
可用性
可用性是指系統提供的服務必須一直處於可用的狀態,對於用戶的每一個操作請求總是能在有限的時間內返回結果。這里有限的時間就是系統響應時間
分區容錯性
分布式系統在遇到任何網絡分區故障的時候,仍然需要保證能夠對外提供滿足一致性和可用性的服務,除非是整個網絡環境發生了故障。
網絡分區是指在分布式系統中,不同節點分布在不同的子網絡中,由於一些特殊的原因導致這些子網絡之間出現了網絡不連通的情況,但各個子網絡的內部網絡是正常的,從而導致整個系統的網絡環境被切分成了若干個孤立的區域。
上面說到,一個分布式系統不可能同時滿足CAP的特性。但是,需要說明的是,分區容錯性P是一個最基本的需求。因為分布式系統中的組件必然部署在不同的網絡節點上,網絡問題是必然會出現的一個問題。因此就剩下兩種選擇了,即CP和AP。系統架構需要在C和A之間尋求平衡。
BASE理論
BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)的縮寫。BASE是對CAP中一致性和可用性權衡的結果。是基於CAP定理逐步演化而來,其核心思想是即使無法做到強一致性,但每個應用都可以根據自身的業務特點,采用適當的方式來使系統達到最終的一致性。
基本可用
分布式系統在出現不可預知的故障的時候,允許損失部分可用性。比如響應時間上的損失,原來0.2s返回的,現在可能需要2s返回。或者是部分功能上的損失,比如秒殺場景下部分用戶可能會被引導到一個降級的頁面。
軟狀態
是和硬狀態相對的,是指允許系統中的數據存在中間的狀態。但是中間的狀態並不會影響系統的整體可用性。
最終一致性
是指系統中的所有數據副本,經過一定時間的同步后,最終能夠達到一個一致的狀態。而不是要實時保證系統數據的強一致性。
Zookeeper簡介
Apache Zookeeper是由Apache Hadoop的子項目發展而來,2011年正式成為Apache的頂級項目。Zookeeper為分布式應用提供了高效且可靠的分布式協調服務。在解決分布式數據一致性方面,Zookeeper並沒有使用Paxos算法(一種一致性算法),而是采用了ZAB(Zookeeper Atomic Broadcast)的一致性協議。
分布式應用程序可以基於它實現諸如數據發布/訂閱、負載均衡、命名服務、分布式協調、集群管理、Master選舉、分布式鎖和分布式隊列等功能。Zookeeper可以保證如下分布式一致性特性。
順序一致性
同一個客戶端發起的事務請求,最終將會嚴格的按照其發起的順序被應用到Zookeeper中去。
原子性
所有事務請求的處理結果在整個集群中所有機器上的應用情況是一致的,也就是說,也么集群中的所有節點都應用了一個事務,要么都沒有應用。
單一視圖
無論客戶端連接的是哪個Zookeeper服務器,其看到的服務端數據模型都是一致的。
可靠性
一旦服務端成功的應用了一個事務,並完成對客戶端的響應那么該事務所引起的服務端狀態改變將會被一直保留下來,除非有另一個事務又對其進行了變更。
實時性
Zookeeper僅僅保證在一定的時間段內,客戶端最終一定能夠從服務端讀取到最新的數據狀態。
Zookeeper數據模型
Zookeeper中有數據節點的概念,我們稱之為ZNode,ZNode是Zookeeper中數據的最小單元。每個ZNode上都可以保存數據,同時還可以掛載子節點,因此就構成了一個層次化的命名空間,我們叫做樹。類似於Linux文件系統中的目錄樹。
看圖就明白了。

/是根目錄,其他子節點都是從根目錄開始。類似於Linux文件系統,不同的是Zookeeper的節點上是可以存儲數據的。
Zookeeper事務
Zookeeper中的事務,和數據庫中具有ACID特性的事務有所區別。在Zookeeper中,事務是指能夠改變Zookeeper服務器狀態的操作,我們叫做事務操作或者更新操作,一般包括數據節點的創建和刪除、數據節點內容更新和客戶端會話創建與失效操作。對於每一個事務請求,Zookeeper都會為其分配一個全局唯一的事務ID,用ZXID來表示,通常是一個64位的數字。每一個ZXID對應一次更新操作,從這些ZXID中可以間接地識別出Zookeeper處理這些更新操作請求的全局順序。
數據節點的類型
在Zookeeper中,節點類型可以分為持久節點(PERSISTENT),臨時節點(EPHEMERAL)和順序節點(SEQUENTIAL)。在具體的節點創建中,通過組合,有下面四種組合節點類型:
持久節點
是最常見的一種節點類型,是指數據節點被創建后,就會一直存在於Zookeeper服務器中,直到有刪除操作來主動刪除這個節點。
持久順序節點
和持久節點的特性是一樣的,額外的特性表現在順序性上。在Zookeeper中,每個父節點都會為它的第一級子節點維護一份順序,用於記錄每個子節點創建的順序。基於這個順序特性,在創建子節點的時候,可以設置這個標記,那么在創建節點過程中,Zookeeper會自動為節點名加上一個數字后綴,作為一個新的完整的節點名。數字后綴的上限是整型的最大值。
臨時節點
和持久節點不同的是,臨時節點的生命周期和客戶端的會話綁定在一起,也就是說,如果客戶端的會話失效,那么這個節點就會被自動清理掉。這里提到的是客戶端會話失效,而非TCP連接斷開。
臨時有序節點
臨時有序節點的特性和臨時節點的特性一樣,只是增加了有序的特性。
狀態信息
在Zookeeper客戶端中,我們通過stat命令,可以查看節點的狀態信息。
1[zk: localhost:2181(CONNECTED) 6] stat /test/node1
2cZxid = 0x8
3ctime = Sun May 03 21:12:24 CST 2020
4mZxid = 0xb
5mtime = Sun May 03 21:13:08 CST 2020
6pZxid = 0x8
7cversion = 0
8dataVersion = 1
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 0
下面簡要介紹下狀態信息中各字段含義:
1cZxid: Created ZXID ,表示數據節點被創建時的事務ID。
2ctime: Created Time,表示節點被創建的時間。
3mZxid: Modified ZXID,表示該節點最后一次被更新時的事務ID。
4mtime: Modified Time,表示該節點最后一次被更新的時間。
5pZxid: 表示該節點的子節點列表最后一次被修改時的事務ID,注意只有子節點列表變更了才會變更pZxid,子節點內容的變更不會影響pZxid。
6cversion: 子節點的版本號。
7dataVersion: 數據的版本號。
8aclVersion: acl權限的版本號。
9ephemeralOwner: 創建該臨時節點的會話的sessionID,如果該節點是持久節點,那么這個屬性值為0。
10dataLength: 數據內容的長度。
11numChildren: 當前節點的子節點個數。
上面狀態中的version字段是一種樂觀鎖機制的保證,保證並發更新數據的安全性。
Zookeeper的Watcher機制
在Zookeeper中,引入了Watcher機制來實現這種分布式的通知功能。Zookeeper允許客戶端向服務器注冊一個Watcher監聽,當服務端的一個特定事件觸發了這個Watcher,那么就會向客戶端發送一個事件通知來實現分布式的通知功能。這個過程可以看下面的圖。

Zookeeper集群節點類型
構成集群的每一台機器都有自己的角色,最典型的集群模式就是Master/Slave模式。在這種集群模式中,Master節點復雜讀寫操作,Slave負責提供讀服務,並以異步的方式從Master同步數據。
而在Zookeeper集群中,沒有使用傳統的Master/Slave集群模式,而是引入了Leader、Follower和Observer三種角色。ZK集群中的所有機器通過一個Leader選舉過程來選定一台機器作為“Leader”的機器。Leader服務器為客戶端提供讀和寫服務。Follower和Observer都能夠提供讀服務,唯一區別在於Observer機器不參與Leader選舉過程。
Leader
Leader服務器是整個zk集群工作機制中的核心,其主要工作是以下兩個:
- 事務請求的唯一調度和處理者,保證集群事務處理的順序性。
- 集群內部各服務器的調度者。
Follower
Follower服務器時集群狀態的跟隨者,其主要的工作有一下三個。
- 處理客戶端非事務請求,轉發事務請求給Leader服務器。
- 參與事務請求Proposal的投票。
- 參與Leader選舉投票。
Observer
在zk集群中充當了一個觀察者的角色,觀察集群的最新狀態並將這些狀態變更同步過來。Observer服務器的工作原理和Follower服務器基本是一致的,對於非事務的請求都可以進行獨立的處理,對於事務請求會轉發給Leader處理。和Follower唯一的區別在於,Observer不參與任何形式的投票,包括事務請求Proposal投票和Leader選舉投票。
Zookeeper的客戶端操作
安裝好Zookeeper之后,就可以使用zk自帶的客戶端腳本來進行操作了。進入Zookeeper的bin目錄之后,直接執行如下命令:
1sh zkCli.sh
當看到出現下面這句話時,說明已經成功連接到了zkserver。
1WatchedEvent state:SyncConnected type:None path:null
進入客戶端后,可以直接使用help命令看下支持哪些命令。

下面說一下在zk客戶端中怎么操作節點和數據。
增加節點
使用create命令新建一個節點。命令格式如下:
1create [-s] [-e] [-c] [-t ttl] path [data] [acl]
-s是順序特性,-e是臨時節點。
比如執行下面命令
1[zk: localhost:2181(CONNECTED) 10] create /Java hello
2Created /Java
會在根節點下創建一個/Java節點,並且節點的數據內容是hello。默認創建的是持久節點。
可以繼續在/Java節點下創建子節點,比如:
1[zk: localhost:2181(CONNECTED) 11] create /Java/spring 123
2Created /Java/spring
讀取
使用ls命令可以看到指定節點下的所有子節點。只能看一級,不能列出子節點樹。命令格式為:
1ls [-s] [-w] [-R] path
比如執行 ls / 命令,可以看下根節點下的子節點情況。
1[zk: localhost:2181(CONNECTED) 12] ls /
2[Java, aaa, happy, test, zookeeper]
可以使用get 命令獲取節點的數據內容,命令格式為:
1get [-s] [-w] path
比如我們獲取一下/Java節點中的數據內容:
1[zk: localhost:2181(CONNECTED) 14] get /Java
2hello
獲取節點狀態信息
使用stat命令可以獲取節點的狀態信息,命令格式如下:
1stat [-w] path
例如,我們獲取一下/Java節點的狀態信息
1[zk: localhost:2181(CONNECTED) 15] stat /Java
2cZxid = 0x14
3ctime = Mon May 11 21:40:38 CST 2020
4mZxid = 0x14
5mtime = Mon May 11 21:40:38 CST 2020
6pZxid = 0x15
7cversion = 1
8dataVersion = 0
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 1
上面這些信息我們在之前聊到Zookeeper數據節點是有講過是什么意思,可以回顧下。
更新
使用set命令可以更新指定節點的數據內容。命令格式如下:
1 set [-s] [-v version] path data
比如我們將/Java節點的內容更新為 world。
1[zk: localhost:2181(CONNECTED) 16] set /Java world
2[zk: localhost:2181(CONNECTED) 17] get /Java
3world
數據更新完成之后,我們可以使用stat命令,再看一下節點的狀態信息,發現dataVersion已經由0變為1了。
1[zk: localhost:2181(CONNECTED) 18] stat /Java
2cZxid = 0x14
3ctime = Mon May 11 21:40:38 CST 2020
4mZxid = 0x16
5mtime = Mon May 11 21:52:19 CST 2020
6pZxid = 0x15
7cversion = 1
8dataVersion = 1
9aclVersion = 0
10ephemeralOwner = 0x0
11dataLength = 5
12numChildren = 1
因為剛才更新數據內容的操作導致數據版本升級。
刪除
使用delete命令刪除Zookeeper節點,用法如下:
1 delete [-v version] path
比如我們將/Java節點刪除掉。不過需要注意的是,要刪除的節點必須沒有子節點才可以。下面直接刪除/Java節點是刪除不掉的。因為它下面有子節點。
1[zk: localhost:2181(CONNECTED) 20] delete /Java
2Node not empty: /Java
3[zk: localhost:2181(CONNECTED) 21] ls /
4[Java, aaa, happy, test, zookeeper]
可以刪除/Java/spring節點
1[zk: localhost:2181(CONNECTED) 22] ls /Java
2[spring]
3[zk: localhost:2181(CONNECTED) 23] delete /Java/spring
4[zk: localhost:2181(CONNECTED) 24] ls /Java
5[]
ZAB協議
ZAB協議是為分布式協調服務Zookeeper專門設計的一種支持崩潰恢復的原子廣播協議。它並不是Paxos算法的一種實現。
在Zookeeper中,主要依賴ZAB協議來實現分布式數據一致性,基於該協議,Zookeeper實現了一種主備模式的系統架構來保證集群模中各副本之間數據的一致性。ZAB協議要滿足下面一些核心需求。
- Zookeeper使用一個單一的主進程來接收和處理客戶端的所有事務請求,並采用ZAB的原子廣播協議,將服務器數據狀態的變更以事務Proposal的形式廣播到所有副本進程上去。
- 要保證事務執行的順序性。ZAB協議必須保證一個全局的變更系列被順序地應用。
- 最后就是考慮到主進程(也就是Leader服務器)隨時都有可能崩潰或者退出。ZAB協議要做到Leader在出現上述異常的情況下,依然能夠正常的工作。
ZAB協議的核心機制
其核心機制是定義了對於那些會改變Zookeeper服務器數據狀態的事務請求的處理方式,即:
所有的事務請求必須由一個全局唯一的服務器來協調處理,這樣的服務器稱為Leader服務器,而余下的其他服務器是Follower(這里暫不說Observer,因為不參與投票)。Leader服務器復雜將一個客戶端的事務請求轉換成一個事務提議(Proposal),並將該Proposal分發給集群中所有的Follower服務器。之后Leader服務器需要等待所有Follower服務器的反饋,一旦超過半數的Follower服務器進行了正確地反饋后,那么Leader就會再次向所有的Follower服務器分發Commit消息,要求其將前一個Proposal進行提交。
針對ZAB協議這里只做簡要介紹,至於崩潰恢復和消息廣播的具體內容不詳細展開。
Zookeeper的Leader選舉
前面說到在ZK集群中,有一個Leader負責處理事務請求。Leader是通過一種選舉算法選出來的一個Zookeeper服務器節點。下面簡要介紹下Leader選舉的過程。
Leader選舉有兩個時機:
1、服務器啟動時Leader選舉
2、運行期Leader節點掛了,需要選舉新的Leader。
服務器啟動時的Leader選舉
這里以3台機器組成的集群為例子。Server1(myid為1)、Server2(myid為2)、Server3(myid為3)。myid是在zk集群中用來標識每一台機器的,不能重復。假設Server1最先啟動,然后Server2,再Server3。
1、首先每台Server會發出一個投票
由於是初始的情況,每台機器都會選自己作為Leader。每次投票的最基本信息就是服務器的myid和ZXID,我們用(myid,zxid)這種形式標識。那Server1發出的投票就是(1,0),Server2發出的投票就是(2,0),Server3(3,0)然后將各自的投票發送給集群中剩下的其他所有機器。
2、接收來自各個服務器的投票
每個服務器都會接收來自其他服務器的投票。集群中每個服務器在接收到投票后,首先會判斷該投票的有效性,包括檢查投票是否是本輪投票,是否來自LOOKING狀態的服務器。
3、處理投票
主要是拿自己的投票和其他服務器發送過來的投票做一個PK,PK的規則如下:
- 有限檢查ZXID。ZXID比較大的服務器優先作為Leader
- 如果ZXID相同的話,那么就比較myid。myid比較大的服務器作為Leader。
這里對於Server1來說,自己的投票是(1,0),收到的投票是(2,0),經過PK之后,Server1會更新自己的投票(2,0),然后將票重新發出去。而對於Server2來說,不需要更新自己的投票信息。
4、統計投票信息
每次投票后,服務器都會統計所有的投票,判斷是否已經有過半的機器收到了相同的投票信息。這里對於Server1,Server2來說,都統計出集群中已經有兩台機器接收了(2,0)這個投票信息。此時,就認為已經選舉出了Leader
5、改變服務器狀態
一旦確定了Leader,每個服務器都會更新自己的狀態:如果是Follower,那么就變為FOLLOWING,如果是Leader,那么就變為LEADING。
運行期Leader節點掛了,需要選舉新的Leader。
zk集群正常運行的過程中,一旦選出了Leader,那它一直就是Leader,除非這個Leader掛了,才會進入新一輪的Leader選舉,也就是下面要說的這種情況。這個過程其實和啟動期間Leader選擇過程基本是一致的。
1、變更服務器狀態
當Leader掛了之后,余下的非Observer服務器都會將自己的服務器狀態變為LOOKING,然后開始進入Leader選舉流程。
2、每個Server會發出一個投票
這里ZXID可能就會是不一樣的,因為是運行期,每台機器上的數據同步情況可能會有差異。
3、接收來自各個服務器的投票
4、處理投票
5、統計投票
6、改變服務器的狀態
Zookeeper的典型應用場景
1、使用Zookeeper可以做配置中心
可以將配置信息集中存儲在zk的節點中,客戶端可以注冊一些監聽,一旦節點數據發生變更,服務端就會向相應的客戶端發送Watcher事件通知,客戶端接收到這個通知之后,可以主動到服務端獲取最新的數據。
2、作為注冊中心
Dubbo中就是默認用zk作為注冊中心的。將服務的url信息注冊到zk的節點上。利用臨時節點和watcher機制實現服務的動態感知。
3、作為分布式鎖
分布式鎖是分布式場景下保證資源同步的一種方式。在zk中,所有客戶端都在一個節點(比如/lock)下調用create()方法創建臨時子節點,只會有一個創建成功,這個創建成功的就認為是獲取了鎖。其他客戶端就需要在/lock節點上注冊一個子節點變更的watcher監聽。