20 張圖!常見分佈式理論與解決方案
對於分佈式系統,最簡單的理解就是一堆機器對外提供服務,相比單體服務,它可以承受更高的負載,但是分佈式系統也帶了一系列問題,比如如何解決某個節點故障的問題?如何解決數據一致性的問題?如何解決數據傾斜的問題?今天通過這篇文章,帶大家搞懂和分佈式相關的常見理論和解決方案。
CAP 理論
先從定義開始:
- C(Consistence):一致性,所有的節點訪問的是最新的數據副本,這是什麼意思呢?我們知道在分佈式系統中,爲了高可用,往往一個節點會有若干個數據副本,簡稱 Follower 節點,比較常見的模式是我們的數據更新一般會寫入 Leader 節點,然後會同步給 Follower 節點,當我們讀取數據的時候,不論從哪個節點讀取都可以讀到最新的數據,這就是一致性。
- A(Availability):可用性,非故障節點可以正常的操作,簡單來說就是客戶端一直可以正常訪問並得到系統的正常響應,從用戶角度來看就是不會出現系統操作失敗或者訪問超時等問題,但是系統內部可能會出現網絡延遲等問題。
C 節點因爲自身問題不可用,正常情況會被剔除,B 節點與 A 節點之間可能存在同步延遲,但是 B 節點本身沒有故障,所以 B 節點是可用的。
- P(Partition tolerance):分區容錯性,網絡的問題錯綜複雜,分佈式系統肯定是要考慮這一點的,如果出現某個節點因爲網絡等問題造成數據不一致,或者數據延遲很久才同步過來,雖然會影響部分節點數據的時效性,但是服務節點依然是可用的,分佈式系統要能容忍這種情況的。
B 對應的節點雖然和 Leader 斷了聯繫,但是依然可以對外服務,只不過提供的是老數據。
在分佈式系統中,CAP 是無法同時滿足的,首先由於存在多節點,並且網絡傳輸需要時間,所以可能會存在延遲,那麼節點之間的數據我們無法保證某一時刻完全一致,因此 P(分區容錯性)是要滿足的。在 P 滿足的情況下,爲什麼說 CA 不能同時滿足呢?我們來通過假設看一看,如果 CA 同時滿足會怎麼樣。
-
假設現在要求滿足 C(一致性),那麼就是說所有的節點在某一刻提供的數據都必須一致,我們知道在 P 的情況,是不可能保證的,要保證的話,就只能把其他節點全部幹掉,比如禁止讀寫,那這其實就是和 A 是相悖的(某些節點雖然延遲,但是節點本身可用)。
-
假設現在要求滿足 A(可用性),那麼就是說只要節點本身沒什麼問題,就可以對外提供服務,哪怕有點數據延遲,很明顯這肯定是和 C 相悖的。
在實際的業務中,我們需要根據業務的場景來決定使用 CP,還是 AP。比如對一些和錢掛鉤的業務,數據的一致性按道理應該是最重要的,因此一般會採用 CP,而對於一些不影響主體功能的業務,比如像新聞的閱讀量,不同的用戶看到的閱讀量不一樣並不會造成什麼影響,可以採用 AP。
BASE 理論
由於 CAP 理論中 C 和 A 無法兼得,eBay 的架構師提出了 BASE 理論,BASE 理論主要是在 CA 之間做文章,它不要求強一致性,因此可以滿足一定的可用性。我們還是先從定義開始:
- BA(Basically Available):基本可用,注意這個和不可用不是一回事,在分佈式系統中出現不可預估的故障時,允許損失部分可用性,保證核心功能可用,比如正常一個接口響應 200ms,在出現故障時響應超過 1s,雖然響應時間變長了,但是接口還是可以對外提供服務的,再比如對於一個視頻網站,在突發流量到來時,把視頻的彈幕服務打掛了,但是視頻的播放功能依然正常。
- S(Soft-state):軟狀態,即分佈式系統允許存在一箇中間的狀態,但是這個中間狀態並不會對服務造成嚴重的影響,比如對於主從複製這種,允許從節點短暫的延遲。
- E(Eventually Consistent):最終一致性,由於軟狀態的存在,系統對延遲是可以容忍的,但是在一段時間後,延遲的數據需要最終保持一致。
總的來說,BASE 理論適用性應該更廣泛,很多時候我們並不要求數據的強一致性,只要在短暫的延時之後能達到一致性也是可以的。
一致性 hash
hash 這個詞對我們來說並不陌生,以緩存服務器來說,一般會在線上配置好幾臺服務器,然後根據 hash 來決定請求哪臺緩存服務,比如常見的就是取模方式 hash(key)%num 來獲取目標機器。
假設現在有 3 臺緩存服務器,並且當前有 3 個緩存的 key,分別是 k0,k1,k2,在經過 hash 以後,它們的分佈情況如下:
hash(k0)%3=0 #No.0
hash(k1)%3=1 #No.1
hash(k2)%3=2 #No.2
很幸運,分佈的非常均勻,每臺機器一個。某天,由於線上要做個活動,預計訪問量會加大,需要選擇加一臺服務器來分擔壓力,於是經過 hash 之後,k0,k1,k2 的分佈情況如下:
hash(k0)%4=0 #No.1
hash(k1)%4=1 #No.2
hash(k2)%4=2 #No.3
-
k0 的目標緩存服務器由原本的 No.0 變成了 No.1
-
k1 的目標緩存服務器由原本的 No.1 變成了 No.2
-
k2 的目標緩存服務器由原本的 No.2 變成了 No.3
可以發現因爲添加了一臺緩存節點,導致了 k0,k1,k2 原來的緩存全部失效了,這似乎有點問題,類似緩存雪崩,嚴重的話會對 DB 造成很大的壓力,造成這個問題的主要原因是因爲我們加了一個節點,導致 hash 結果發生了變動,此時的 hash 可以說是不穩定的。
爲了解決 rehash 不穩定的問題,於是出現了一致性 hash 算法。一致性 hash 的原理比較簡單,首先存在一個 hash 圓環,這個圓環可以存放 0-2^32-1 個節點。
-
第一步就是把我們的目標服務器節點通過 hash 映射到這個環上
-
第二步根據我們需要查找的 key,它應該也對應 hash 環上的某個位置
也許你會問,這 k0、k1、k2 也沒和某個緩存節點對上呀~,這就是一致性 hash 不同的地方,它此時查找的方式並不是 hash(key)= 某個節點,而是根據 key 的位置,順時針找到第一個節點,這個節點就是當下這個 key 的目標節點。
我們再來看看在一致性 hash 的情況下,新增一個節點會發生什麼。
當然這樣的節點分佈算是比較理想的了,如果我們的節點是這樣分佈的:
-
cache0->cache0#1
-
cache1->cache1#1
-
cache2->cache2#1
虛擬節點並不是真正的服務節點,它只是一個影子,它的目的就是站坑位,讓節點更加分散,更加均勻。
Gossip 協議
集羣往往是由多個節點共同組成的,當一個節點加入集羣或者一個節點從集羣中下線的時候,都需要讓集羣中其他的節點知道,這樣才能將數據信息分享給新節點而忽略下線節點。
這樣的廣播協議就是今天要說 Gossip 協議,Gossip 協議也叫 Epidemic 協議(流行病協議),當一個消息到來時,通過 Gossip 協議就可以像病毒一樣感染全部集羣節點,當然我們利用的是它這個極強的散播能力。
Gossip 的過程是由一個種子節點發起的,當一個種子節點有信息需要同步到網絡中的其他節點時,它會隨機的選擇周圍幾個節點散播消息,收到消息的節點也會重複該過程,直至最終網絡中所有的節點都收到了消息。這個過程可能需要一定的時間,所以不能保證某個時間點所有的節點都有該條消息,但是理論上最終所有節點都會收到消息,因此它是一個最終一致性協議。
Gossip 協議的特點:
-
Gossip 協議是週期性散播消息,每隔一段時間傳播一次
-
被感染的節點,每次可以繼續散播 N 個節點
-
每次散播消息時,都會選擇尚未發送過的節點進行散播
-
收到消息的節點,不會向發送的節點散播
-
同一個節點可能會收到重複的消息,因爲可能同時多個節點正好向它散播
-
集羣是去中心化的,節點之間都是平等的
-
消息的散播不用等接收節點的 ack,即消息可能會丟失,但是最終應該會被感染
我們來看個例子:
-
種子節點是 A
-
A 節點選擇 B、C 節點進行散播
-
C 散播到 D,B 散播 D 和 E,可以發現 D 收到兩次
-
D 散播到 F,最終整個網絡都同步到了消息
Gossip 有點類似圖的廣度優先遍歷算法,一般用於網絡拓撲結構信息的分享和維護,像 redis、consul 都有使用到。
Raft 算法
分佈式協議的難點之一就是數據的一致性,當由多個節點組成的集羣中只有一個節點收到數據,我們就算成功的話,風險太大,當要求所有節點都收到數據才響應成功,性能又太差,所以一般會在數據的安全和性能之間做個折中,只要保證絕大部分節點同步數據成功,我們就算成功,Raft 算法作爲比較知名的一致性算法,被廣泛應用於許多中間件中,比如像 etcd,接下來我們就看看 Raft 算法是如何工作的。
首先介紹下在 Raft 算法中,幾種情況下每個節點對應的角色:
-
Leader 節點:同大多數分佈式中的 Leader 節點一樣,數據的變更都是通過它的
-
Follower 節點:Leader 節點的追隨者,負責複製數據並且在選舉時候投票的節點
-
Candidate 候選節點:參與選舉的節點,就是 Follower 節點參與選舉時會切換的角色
Raft 算法將一致性問題分解爲兩個的子問題,Leader 選舉和狀態複製。
選舉
首先我們來看看 Leader 的選舉,系統在剛開始的時候,所有節點都爲 Follower 節點,這時大家都有機會參與選舉,也就是把自己變成 Candidate,但是如果每個 Follower 節點都變成 Candidate 那麼就會陷入無限的死循環,於是每個 Follower 都一個定時器,並且定時器的時間是隨機的,當某個 Follower 的定時器時間走完之後,會確認當前是否存在 Leader 節點,如果不存在就會把自己變成 Candidate,這時會投自己 1 票,同時告訴其它節點,讓它們來投票,當拿到超過半數以上的投票時,當前的 Candidate 就會變成 Leader 節點。
-
由於 A 節點的定時器時間最短(10ms),所以 A 會成爲 Candidate
-
A 投自己一票,同時 B、C 也投出自己的同意票,因此 A 會變成 Leader 節點,同時會記錄是第 M 任。這個 M 是做版本校驗的,比如一個編號是 10 的節點,收到了一個編號是 9 的節點的投票請求,那麼就會拒絕這個請求。
在 Leader 節點選舉出來以後,Leader 節點會不斷的發送心跳給其它 Follower 節點證明自己是活着的,其他 Follower 節點在收到心跳後會清空自己的定時器,並回復給 Leader,因爲此時沒必要觸發選舉了。
如果 Leader 節點在某一刻掛了,那麼 Follower 節點就不會收到心跳,因此在定時器到來時就會觸發新一輪的選舉,流程還是一樣,但是如果恰巧兩個 Follower 都變成了 Candidate,並且都得到了同樣的票數,那麼此時就會陷入僵局,爲了打破僵局,這時每個 Candidate 都會隨機推遲一段時間再次請求投票,當然一般情況下,就是先來先得,優先跑完定時器的 Candidate 理論成爲 Leader 的概率更大。
好的選舉流程大致如上,接下來我們來看看數據的複製。
複製
當 Leader 節點收到 Client 的請求變更時,會把變更記錄到 log 中,然後 Leader 會將這個變更隨着下一次的心跳通知給 Follower 節點,收到消息的 Follower 節點把變更同樣寫入日誌中,然後回覆 Leader 節點,當 Leader 收到大多數的回覆後,就把變更寫入自己的存儲空間,同時回覆 client,並告訴 Follower 應用此 log。至此,集羣就變更達成了共識。
最後,Raft 算法是能夠實現分佈式系統強一致性的算法,每個系統節點有三種狀態 Leader、Follower、Candidate,實現 Raft 算法兩個最重要的事是:主的選舉和日誌的複製。
分佈式事務
事務相信大家不陌生,事務的本質是要麼一起向前衝,要麼一起保持不動。對於 MySQL 的 InnoDB 來說,我們只需要執行 begin、commit 就行,有時候我們可能需要回滾 rollback。但是這是在同一數據庫的前提下,如果我們的數據表分庫了或者說我們要操作的資源在不同的網絡節點上該怎麼辦?這就得用到我們今天要說的分佈式事務了,分佈式事務有 2PC、3PC、TCC 等, 但是無論哪種都無法保證完美的 ACID,我們來一起看看是怎麼回事吧。
2PC
從名字可以看出它是分兩個階段的,所以它也叫做二階段提交,即準備和提交,2PC 要求有個事務的協調者,相比常規的事務,我們的請求是發給這個協調者的,然後由協調者幫我們協調各個節點資源的提交。
-
準備階段:協調者會讓各個參與事務的參與者,把除了提交之外所有的事情都幹好,也就是就等着提交了
-
提交階段:協調者收到各個參與者的準備消息後,根據準備情況通知各個參與者提交(commit)或者回滾(rollback)
可以發現整個過程非常依賴協調者,如果協調者掛了,那麼整個分佈式事務就不可用,所以一般建議協調者至少有個備份節點。
如果協調者在收到所有節點的 ok 之後,在準備發送 commit 消息的時候,由於網絡問題,導致其中一個節點始終收不到消息,那麼收不到消息的節點就會一直佔着資源不釋放,出現這種情況的時候,建議協調者有個重試功能,在 commit 失敗之後,不停的重試,直至成功。2PC 協議是一種強一致性協議,它是同步阻塞的,所以在高併發的場景它的性能可能還會有問題。
3PC
2PC 存在一些問題,比如協調者從掛了到恢復後並不知道當前節點的狀態,現在應該做什麼(是該提交還是回滾等等),還有就是當發生網絡問題的時候,無法通信的節點只會傻傻的等待,造成資源一直處於鎖定狀態。鑑於這些問題,出現了 3PC。
首先 3PC 顧名思義,會分爲 3 個階段,分別是準備階段、預提交階段和提交階段。
-
準備階段:主要是詢問參與者自身的狀況,比如你的負載情況如何?能參與接下來的任務吧?
-
預提交階段:除了 commit 之外的所有準備工作,就等着 commit 了
-
提交階段:執行真正的 commit 或者 rollback
其實 2PC 和 3PC 都無法保證絕對的一致性,因爲某個參與者節點可能就是因爲網絡問題收不到消息,但是其他參與者節點已經提交了事務,一般爲了預防這種問題,最好加一個報警,比如監控到事務異常的時候,通過腳本自動補償差異的信息。
TCC
TCC 事務的全程是 Try、Commit、Cancel,TCC 事務使用場景更貼近實際應用,因此它的使用也更廣泛。
-
Try:Try 這個過程,一般表示鎖定資源的過程,比如常見的下單,在 try 階段,我們不是真正的減庫存,而是把下單的庫存給鎖定住。
-
Commit:真正的執行業務邏輯了,帶提交的。
-
Cancel:撤銷,如果 Commit 失敗可以把鎖定的資源釋放回來
TCC 對應用的侵入性強。業務邏輯的每個分支都需要實現 try、confirm、cancel 三個操作,代碼改造成本高。在出現網絡或者其他系統故障時,TCC 要根據實際業務場景實現對應的回滾邏輯。Commit 或者 Cancel 有可能會重試,因此對應的部分最好支持冪等。
最後其實上面 3 種分佈式事務理論上都無法保證絕對的一致性,因爲無法解決網絡等帶來的意外因素,要解決它,要麼只能無限重試,但是這個無限重試最好通過消息隊列 + 守護進程的方式來自動補數據,前提還是得保證消息隊列不丟失數據。總之不僅僅是分佈式事務會帶來這些問題,分佈式本身也會帶來許許多多的問題,沒有絕對的解決方案,只有更好的解決方案。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Jq0qyMoK8F6fuz7e-rSfJg