什么是分布式系統?
拿一個最簡單的例子,就比如說我們的圖書管理系統。之前的系統包含了所有的功能,比如用戶注冊登錄、管理員功能、圖書借閱管理等。這叫做集中式系統。也就是一個人干了好幾件事。
后來隨着功能的增多,用戶量也越來越大。集中式系統維護太麻煩,拓展性也不好。於是就考慮着把這些功能分開。通俗的理解就是原本需要一個人干的事,現在分給n個人干,各自干各自的,最終取得和一個人干的效果一樣。
稍微正規一點的定義就是:一個業務分拆多個子業務,部署在不同的服務器上。 然后通過一定的通信協議,能夠讓這些子業務之間相互通信。
既然分給了n個人,那就涉及到這些人的溝通交流協作問題。想要去解決這些問題,就需要先聊聊分布式系統中的CAP理論。
CAP原理
CAP原理指的是一個分布式系統最多只能同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance)這三項中的兩項。
這張圖不知道你之前看到過沒,如果你看過書或者是視頻,這張圖應該被列舉了好幾遍了。下面我不准備直接上來就對每一個特性進行概述。我們先從案例出發逐步過渡。
1、一個小例子
首先我們看一張圖。
現在網絡中有兩個節點N1和N2,他們之間網絡可以連通,N1中有一個應用程序A,和一個數據庫V,N2也有一個應用程序B2和一個數據庫V。現在,A和B是分布式系統的兩個部分,V是分布式系統的兩個子數據庫。
現在問題來了。突然有兩個用戶小明和小華分別同時訪問了N1和N2。我們理想中的操作是下面這樣的。
(1)小明訪問N1節點,小華訪問N2節點。同時訪問的。
(2)小明把N1節點的數據V0變成了V1。
(2)N1節點一看自己的數據有變化,立馬執行M操作,告訴了N2節點。
(4)小華讀取到的就是最新的數據。也是正確的數據。
上面這是一種最理想的情景。它滿足了CAP理論的三個特性。現在我們看看如何來理解滿足的這三個特性。
Consistency 一致性
一致性指的是所有節點在同一時間的數據完全一致。
對於一致性,也可以分為從客戶端和服務端兩個不同的視角來理解。
(1)客戶端
從客戶端來看,一致性主要指的是多並發訪問時更新過的數據如何獲取的問題。也就是小明和小華同時訪問,如何獲取更新的最新的數據。
(2)服務端
從服務端來看,則是更新如何分布到整個系統,以保證數據最終一致。也就是N1節點和N2節點如何通信保持數據的一致。
對於一致性,一致的程度不同大體可以分為強、弱、最終一致性三類。
(1)強一致性
對於關系型數據庫,要求更新過的數據能被后續的訪問都能看到,這是強一致性。比如小明更新V0到V1,那么小華讀取的時候也應該是V1。
(2)弱一致性
如果能容忍后續的部分或者全部訪問不到,則是弱一致性。比如小明更新VO到V1,可以容忍那么小華讀取的時候是V0。
可用性
可用性指服務一直可用,而且是正常響應時間。就好比剛剛的N1和N2節點,不管什么時候訪問,都可以正常的獲取數據值。而不會出現問題。好的可用性主要是指系統能夠很好的為用戶服務,不出現用戶操作失敗或者訪問超時等用戶體驗不好的情況。
對於可用性來說就比較好理解了。
分區容錯性
分區容錯性指在遇到某節點或網絡分區故障的時候,仍然能夠對外提供滿足一致性和可用性的服務。就好比是N1節點和N2節點出現故障,但是依然可以很好地對外提供服務。
這個分區容錯性也是很好理解。
在經過上面的分析中,在理想情況下,沒有出現任何錯誤的時候,這三條應該都是滿足的。但是天有不測風雲。系統總是會出現各種各樣的問題。下面來分析一下為什么說CAP理論只能滿足兩條。
驗證CAP理論
既然系統總是會有錯誤,那我們就來看看可能會出現什么錯誤。
N1節點更新了V0到V1,想在也想把這個消息通過M操作告訴N1節點,卻發生了網絡故障。這時候小明和小華都要同時訪問這個數據,怎么辦呢?現在我們依然想要我們的系統具有CAP三個特性,我們分析一下會發生什么。
(1)系統網絡發生了故障,但是系統依然可以訪問,因此具有容錯性。
(2)小明在訪問節點N1的時候更改了V0到V1,想要小華訪問節點N2的V數據庫的時候是V1,因此需要等網絡故障恢復,將N2節點的數據庫進行更新才可以。
(3)在網絡故障恢復的這段時間內,想要系統滿足可用性,是不可能的。因為可用性要求隨時隨地訪問系統都是正確有效的。這就出現了矛盾。
正是這個矛盾所以CAP三個特性肯定不能同時滿足。既然不能滿足,那我們就進行取舍。
有兩種選擇:
(1)犧牲數據一致性,也就是小明看到的衣服數量是10,買了一件應該是9了。但是小華看到的依然是10。
(2)犧牲可用性,也就是小明看到的衣服數量是10,買了一件應該是9了。但是小華想要獲取的最新的數據的話,那就一直等待阻塞,一直到網絡故障恢復。
現在你可以看到了CAP三個特性肯定是不能同時滿足的,但是可以滿足其中兩個。
CAP特性的取舍
我們分析一下既然可以滿足兩個,那么舍棄哪一個比較好呢?
(1)滿足CA舍棄P,也就是滿足一致性和可用性,舍棄容錯性。但是這也就意味着你的系統不是分布式的了,因為涉及分布式的想法就是把功能分開,部署到不同的機器上。
(2)滿足CP舍棄A,也就是滿足一致性和容錯性,舍棄可用性。如果你的系統允許有段時間的訪問失效等問題,這個是可以滿足的。就好比多個人並發買票,后台網絡出現故障,你買的時候系統就崩潰了。
(3)滿足AP舍棄C,也就是滿足可用性和容錯性,舍棄一致性。這也就是意味着你的系統在並發訪問的時候可能會出現數據不一致的情況。
實時證明,大多數都是犧牲了一致性。像12306還有淘寶網,就好比是你買火車票,本來你看到的是還有一張票,其實在這個時刻已經被買走了,你填好了信息准備買的時候發現系統提示你沒票了。這就是犧牲了一致性。
但是不是說犧牲一致性一定是最好的。就好比mysql中的事務機制,張三給李四轉了100塊錢,這時候必須保證張三的賬戶上少了100,李四的賬戶多了100。因此需要數據的一致性,而且什么時候轉錢都可以,也需要可用性。但是可以轉錢失敗是可以允許的。
擴展服務的方案
數據分區: uid % 16
數據鏡像:讓多有的服務器都有相同的數據,提供相當的服務(冗余存儲,一般3份為好)
兩種方案的事務問題
A向B匯錢,兩個用戶不在一個服務器上
鏡像:在不同的服務器上對同一數據的寫操作如何保證一致性。
解決一致性事務問題的技術
Master -Slave
讀寫請求由Master負責
寫請求寫到Master后,由Master同步到Slave上
由Master push or Slave pull
通常是由Slave 周期性來pull,所以是最終一致性
問題: 若在 pull 周期內(不是期間?),master掛掉,那么會導致這個時間片內的數據丟失
若不想讓數據丟掉,Slave 只能成為 ReadOnly方式等Master恢復
若容忍數據丟失,可以讓 Slave代替Master工作
如何保證強一致性?
Master 寫操作,寫完成功后,再寫 Slave,兩者成功后返回成功。若 Slave失敗,兩種方法
標記 Slave 不可用報錯,並繼續服務(等恢復后,再同步Master的數據,多個Slave少了一個而已)
回滾自己並返回失敗
Master-Master
數據同步一般是通過 Master 間的異步完成,所以是最終一致
好處: 一台Master掛掉,另外一台照樣可以提供讀寫服務。當數據沒有被賦值到別的Master上時,數據會丟失。
對同一數據的處理問題:Dynamo的Vector Clock的設計(記錄數據的版本號和修改者),當數據發生沖突時,要開發者自己來處理
兩階段提交 Two Phase Commit (2PC)
第一階段:針對准備工作
協調者問所有節點是否可以執行提交
參與者開始事務,執行准備工作:鎖定資源(獲取鎖操作)
參與者響應協調者,如果事務的准備工作成功,則回應"可以提交",否則,拒絕提交
第二階段:
若都響應可以提交,則協調者項多有參與者發送正式提交的命令(更新值),參與者完成正式提交,釋放資源,回應完成。協調者收到所有節點的完成響應后結束這個全局事務.。若參與者回應拒絕提交,則協調者向所有的參與者發送回滾操作,並釋放資源,當收到全部節點的回滾回應后,取消全局事務
存在的問題:若一個沒提交,就會進行回滾
第一階段:若消息的傳遞未接收到,則需要協調者作超時處理,要么當做失敗,要么重載
第二階段:若參與者的回應超時,要么重試,要么把那個參與者即為問題節點,提出整個集群
在第二階段中,參與者未收到協調者的指示(也許協調者掛掉),則所有參與者會進入“不知所措” 的狀態(但是已經鎖定了資源),所以引入了三段提交
三段提交:把二段提交的第一階段 break 成了兩段
詢問
鎖定資源(獲取鎖)
提交
核心理念:在詢問的時候並不鎖定資源,除非所有人都同意了,才開始鎖定
好處:當發生了失敗或超時時,三段提交可以繼續把狀態變為Commit 狀態,而二段提交則不知所措?
Raxos 算法(少數服從多數)
解決的問題:在一個可能發生異常的分布式系統中如何就某個值達成一致,讓整個集群的節點對某個值的變更達成一致
任何一個節點都可以提出要修改某個數據的提案,是否通過這個提案取決於這個集群中是否有超過半數的節點同意(所以節點數總是單數)—— 版本標記。雖然一致性,但是只能對一個操作進行操作啊??
當一個Server接收到比當前版本號小的提案時,則拒絕。當收到比當前大的版本號的提案時,則鎖定資源,進行修改,返回OK. 也就是說收到超過一半的最大版本的提案才算成功。
核心思想:
在搶占式訪問權的基礎上引入多個acceptor,也就是說當一個版本號更大的提案可以剝奪版本號已經獲取的鎖。
后者認同前者的原則:
在肯定舊epoch 無法生成確定性取值時,新的 epoch 會提交自己的valu
一旦 舊epoch形成確定性取值,新的 epoch肯定可以獲取到此取值,並且會認同此取值,不會被破壞。
步驟
-
- P1 請求Acceptor的 #1,Acceptor 這時並沒有其他線程獲取到鎖,所以把鎖交給 P1,並返回這時 #1 的值為null
- P1 請求Acceptor的 #1,Acceptor 這時並沒有其他線程獲取到鎖,所以把鎖交給 P1,並返回這時 #1 的值為null
-
- 然后 P1 向 第一個 Acceptor 提交 #1 的值,Acceptor 接受並返回 OK
- 然后 P1 向 第一個 Acceptor 提交 #1 的值,Acceptor 接受並返回 OK
-
- 這個時候,P2向Acceptor請求#1上的鎖,因為版本號更大,所以直接搶占了 P1 的鎖。這時 Acceptor 返回了 OK並且返回了 #1 的值
- 這個時候,P2向Acceptor請求#1上的鎖,因為版本號更大,所以直接搶占了 P1 的鎖。這時 Acceptor 返回了 OK並且返回了 #1 的值
-
- 這時 P1 P向 后面兩個 Acceptor 提交 #1 的值,但是由於中間的那個Acceptor 版本號已經更改為 2 了,所以拒絕P1。第三個 Acceptor 接受了,並且返回了 OK
- 這時 P1 P向 后面兩個 Acceptor 提交 #1 的值,但是由於中間的那個Acceptor 版本號已經更改為 2 了,所以拒絕P1。第三個 Acceptor 接受了,並且返回了 OK
-
- 由於后者認同前者的原則,這時 P1 已經形成確定性取值了 V1 了,這時新的 P2 會認同此取值,而不是提交自己的取值。所以,P2會選擇最新的那個取值 也就是V1 進行提交。這時Acceptor 返回 OK
- 由於后者認同前者的原則,這時 P1 已經形成確定性取值了 V1 了,這時新的 P2 會認同此取值,而不是提交自己的取值。所以,P2會選擇最新的那個取值 也就是V1 進行提交。這時Acceptor 返回 OK
6.ZAB 協議
ZAB 協議 ( Zookeeper Atomic Broadcast) 原子廣播協議:保證了發給各副本的消息順序相同
定義:原子廣播協議 ZAB 是一致性協議,Zookeeper 把其作為數據一致性的算法。ZAB 是在 Paxos 算法基礎上進行擴展而來的。Zookeeper 使用單一主進程 Leader用於處理客戶端所有事務請求,采用 ZAB 協議將服務器狀態以事務形式廣播到所有 Follower 上,由於事務間可能存在着依賴關系,ZAB協議保證 Leader 廣播的變更序列被順序的處理,一個狀態被處理那么它所依賴的狀態也已經提前被處理
核心思想:保證任意時刻只有一個節點是Leader,所有更新事務由Leader發起去更新所有副本 Follower,更新時用的是 兩段提交協議,只要多數節點 prepare 成功,就通知他們commit。各個follower 要按當初 leader 讓他們 prepare 的順序來 apply 事務
協議狀態
- Looking:系統剛啟動時 或者 Leader 崩潰后正處於選舉狀態
- Following:Follower 節點所處的狀態,Follower與 Leader處於數據同步狀態
- Leading:Leader 所處狀態,當前集群中有一個 Leader 為主進程
ZooKeeper啟動時所有節點初始狀態為Looking,這時集群會嘗試選舉出一個Leader節點,選舉出的Leader節點切換為Leading狀態;當節點發現集群中已經選舉出Leader則該節點會切換到Following狀態,然后和Leader節點保持同步;當Follower節點與Leader失去聯系時Follower節點則會切換到Looking狀態,開始新一輪選舉;在ZooKeeper的整個生命周期中每個節點都會在Looking、Following、Leading狀態間不斷轉換。
選舉出Leader節點后 ZAB 進入原子廣播階段,這時Leader為和自己同步每個節點 Follower 創建一個操作序列,一個時期一個 Follower 只能和一個Leader保持同步
階段
Election: 在 Looking狀態中選舉出 Leader節點,Leader的LastZXID總是最新的(只有lastZXID的節點才有資格成為Leade,這種情況下選舉出來的Leader總有最新的事務日志)。在選舉的過程中會對每個Follower節點的ZXID進行對比只有highestZXID的Follower才可能當選Leader
每個Follower都向其他節點發送選自身為Leader的Vote投票請求,等待回復;
Follower接受到的Vote如果比自身的大(ZXID更新)時則投票,並更新自身的Vote,否則拒絕投票;
每個Follower中維護着一個投票記錄表,當某個節點收到過半的投票時,結束投票並把該Follower選為Leader,投票結束;
Discovery:Follower 節點向准 Leader推送 FollwerInfo,該信息包含了上一周期的epoch,接受准 Leader 的 NEWLEADER 指令
Sync:將 Follower 與 Leader的數據進行同步,由Leader發起同步指令,最終保持數據的一致性
Broadcast:Leader廣播 Proposal 與 Commit,Follower 接受 Proposal 與 commit。因為一個時刻只有一個Leader節點,若是更新請求,只能由Leader節點執行(若連到的是 Follower 節點,則需轉發到Leader節點執行;讀請求可以從Follower 上讀取,若是要最新的數據,則還是需要在 Leader上讀取)
消息廣播使用了TCP協議進行通訊所有保證了接受和發送事務的順序性。廣播消息時Leader節點為每個事務Proposal分配一個全局遞增的ZXID(事務ID),每個事務Proposal都按照ZXID順序來處理(Paxos 保證不了)
Leader節點為每一個Follower節點分配一個隊列按事務ZXID順序放入到隊列中,且根據隊列的規則FIFO來進行事務的發送。
Recovery :根據Leader的事務日志對Follower 節點數據進行同步更新
同步策略:
Follower將所有事務都同步完成后Leader會把該節點添加到可用Follower列表中;
Follower接收Leader的NEWLEADER指令,如果該指令中epoch比當前Follower的epoch小那么Follower轉到Election階段
SNAP :如果Follower數據太老,Leader將發送快照SNAP指令給Follower同步數據;
DIFF :Leader發送從Follolwer.lastZXID到Leader.lastZXID議案的DIFF指令給Follower同步數據;
TRUNC :當Follower.lastZXID比Leader.lastZXID大時,Leader發送從Leader.lastZXID到Follower.lastZXID的TRUNC指令讓Follower丟棄該段數據;(當老Leader在Commit前掛掉,但是已提交到本地)
7. Raft 算法
Raft 算法也是一種少數服從多數的算法,在任何時候一個服務器可以扮演以下角色之一:
Leader:負責 Client 交互 和 log 復制,同一時刻系統中最多存在一個
Follower:被動響應請求 RPC,從不主動發起請求 RPC
Candidate : 由Follower 向Leader轉換的中間狀態
在選舉Leader的過程中,是有時間限制的,raft 將時間分為一個個 Term,可以認為是“邏輯時間”:
每個 Term中至多存在1個 Leader
某些 Term由於不止一個得到的票數一樣,就會選舉失敗,不存在Leader。則會出現 Split Vote ,再由候選者發出邀票
每個 Server 本地維護 currentTerm
選舉過程:
獲得超過半數的Server的投票,轉換為 Leader,廣播 HeatBeat
接收到 合法 Leader 的 AppendEnties RPC,轉換為Follower
選舉超時,沒有 Server選舉成功,自增 currentTerm ,重新選舉
自增 CurrentTerm,由Follower 轉換為 Candidate,設置 votedFor 為自身,並行發起 RequestVote RPC,不斷重試,直至滿足下列條件之一為止:
當Candidate 在等待投票結果的過程中,可能會接收到來自其他Leader的 AppendEntries RPC ,如果該 Leader 的 Term 不小於本地的 Current Term,則認可該Leader身份的合法性,主動降級為Follower,反之,則維持 candida 身份繼續等待投票結果
Candidate 既沒有選舉成功,也沒有收到其他 Leader 的 RPC (多個節點同時發起選舉,最終每個 Candidate都將超時),為了減少沖突,采取隨機退讓策略,每個 Candidate 重啟選舉定時器
- 日志更新問題:
如果在日志復制過程中,發生了網絡分區或者網絡通信故障,使得Leader不能訪問大多數Follwers了,那么Leader只能正常更新它能訪問的那些Follower服務器,而大多數的服務器Follower因為沒有了Leader,他們重新選舉一個候選者作為Leader,然后這個Leader作為代表於外界打交道,如果外界要求其添加新的日志,這個新的Leader就按上述步驟通知大多數Followers,如果這時網絡故障修復了,那么原先的Leader就變成Follower,在失聯階段這個老Leader的任何更新都不能算commit,都回滾,接受新的Leader的新的更新。 - 流程:
解決辦法:Client 賦予每個 Command唯一標識,Leader在接收 command 之前首先檢查本地log
Client 發送command 命令給 Leader
Leader追加日志項,等待 commit 更新本地狀態機,最終響應 Client
若 Client超時,則不斷重試,直到收到響應為止(重發 command,可能被執行多次,在被執行但是由於網絡通信問題未收到響應)
8. paxos 算法與 raft 算法的差異
raft強調是唯一leader的協議,此leader至高無上
raft:新選舉出來的leader擁有全部提交的日志,而 paxos 需要額外的流程從其他節點獲取已經被提交的日志,它允許日志有空洞
相同點:得到大多數的贊成,這個 entries 就會定下來,最終所有節點都會贊成
NWR模型
N: N個備份
W:要寫入至少 w 份才認為成功
R : 至少讀取 R 個備份
W+ R > N ——> R > N - W(未更新成功的) ,代表每次讀取,都至少讀取到一個最新的版本(更新成功的),從而不會讀到一份舊數據
問題:並非強一致性,會出現一些節點上的數據並不是最新版本,但卻進行了最新的操作
版本沖突問題:矢量鍾 Vector Clock : 誰更新的我,我的版本號是什么(對於同一個操作者的同一操作,版本號遞增)