1. 分散式術語
1.1. 異常
伺服器當機
記憶體錯誤、伺服器停電等都會導致伺服器當機,此時節點無法正常工作,稱為不可用。
伺服器當機會導致節點失去所有記憶體資訊,因此需要將記憶體資訊儲存到持久化介質上。
網路異常
有一種特殊的網路異常稱為——網路分割槽 ,即叢集的所有節點被劃分為多個區域,每個區域內部可以通訊,但是區域之間無法通訊。
磁碟故障
磁碟故障是一種發生概率很高的異常。
使用冗餘機制,將資料儲存到多臺伺服器。
1.2. 超時
在分散式系統中,一個請求除了成功和失敗兩種狀態,還存在著超時狀態。
可以將伺服器的操作設計為具有 冪等性 ,即執行多次的結果與執行一次的結果相同。如果使用這種方式,當出現超時的時候,可以不斷地重新請求直到成功。
1.3. 衡量指標
效能
常見的效能指標有:吞吐量、響應時間。
其中,吞吐量指系統在某一段時間可以處理的請求總數,通常為每秒的讀運算元或者寫運算元;響應時間指從某個請求發出到接收到返回結果消耗的時間。
這兩個指標往往是矛盾的,追求高吞吐的系統,往往很難做到低響應時間,解釋如下:
在無併發的系統中,吞吐量為響應時間的倒數,例如響應時間為 10 ms,那麼吞吐量為 100 req/s,因此高吞吐也就意味著低響應時間。
但是在併發的系統中,由於一個請求在呼叫 I/O 資源的時候,需要進行等待。伺服器端一般使用的是非同步等待方式,即等待的請求被阻塞之後不需要一直佔用 CPU 資源。這種方式能大大提高 CPU 資源的利用率,例如上面的例子中,單個請求在無併發的系統中響應時間為 10 ms,如果在併發的系統中,那麼吞吐量將大於 100 req/s。因此為了追求高吞吐量,通常會提高併發程度。但是併發程度的增加,會導致請求的平均響應時間也增加,因為請求不能馬上被處理,需要和其它請求一起進行併發處理,響應時間自然就會增高。
可用性
可用性指系統在面對各種異常時可以提供正常服務的能力。可以用系統可用時間佔總時間的比值來衡量,4 個 9 的可用性表示系統 99.99% 的時間是可用的。
一致性
可以從兩個角度理解一致性:從客戶端的角度,讀寫操作是否滿足某種特性;從伺服器的角度,多個資料副本之間是否一致。
可擴充套件性
指系統通過擴充套件叢集伺服器規模來提高效能的能力。理想的分散式系統需要實現“線性可擴充套件”,即隨著叢集規模的增加,系統的整體效能也會線性增加。
2. 資料分佈
分散式儲存系統的資料分佈在多個節點中,常用的資料分佈方式有雜湊分佈和順序分佈。
資料庫的水平切分(Sharding)也是一種分散式儲存方法,下面的資料分佈方法同樣適用於 Sharding。
2.1. 雜湊分佈
雜湊分佈就是將資料計算雜湊值之後,按照雜湊值分配到不同的節點上。例如有 N 個節點,資料的主鍵為 key,則將該資料分配的節點序號為:hash(key)%N。
傳統的雜湊分佈演算法存在一個問題:當節點數量變化時,也就是 N 值變化,那麼幾乎所有的資料都需要重新分佈,將導致大量的資料遷移。
一致性雜湊
Distributed Hash Table(DHT):對於雜湊空間 [0, 2n-1],將該雜湊空間看成一個雜湊環,將每個節點都配置到雜湊環上。每個資料物件通過雜湊取模得到雜湊值之後,存放到雜湊環中順時針方向第一個大於等於該雜湊值的節點上。
一致性雜湊的優點是在增加或者刪除節點時只會影響到雜湊環中相鄰的節點,例如下圖中新增節點 X,只需要將資料物件 C 重新存放到節點 X 上即可,對於節點 A、B、D 都沒有影響。
2.2. 順序分佈
雜湊分散式破壞了資料的有序性,順序分佈則不會。
順序分佈的資料劃分為多個連續的部分,按資料的 ID 或者時間分佈到不同節點上。例如下圖中,User 表的 ID 範圍為 1 ~ 7000,使用順序分佈可以將其劃分成多個子表,對應的主鍵範圍為 1 ~ 1000,1001 ~ 2000,...,6001 ~ 7000。
順序分佈的優點是可以充分利用每個節點的空間,而雜湊分佈很難控制一個節點儲存多少資料。
但是順序分佈需要使用一個對映表來儲存資料到節點的對映,這個對映表通常使用單獨的節點來儲存。當資料量非常大時,對映表也隨著變大,那麼一個節點就可能無法存放下整個對映表。並且單個節點維護著整個對映表的開銷很大,查詢速度也會變慢。為了解決以上問題,引入了一箇中間層,也就是 Meta 表,從而分擔對映表的維護工作。
2.3. 負載均衡
衡量負載的因素很多,如 CPU、記憶體、磁碟等資源使用情況、讀寫請求數等。
分散式系統儲存應當能夠自動負載均衡,當某個節點的負載較高,將它的部分資料遷移到其它節點。
每個叢集都有一個總控節點,其它節點為工作節點,由總控節點根據全域性負載資訊進行整體排程,工作節點定時傳送心跳包(Heartbeat)將節點負載相關的資訊傳送給總控節點。
一個新上線的工作節點,由於其負載較低,如果不加控制,總控節點會將大量資料同時遷移到該節點上,造成該節點一段時間內無法工作。因此負載均衡操作需要平滑進行,新加入的節點需要較長的一段時間來達到比較均衡的狀態。
3. 分散式理論
3.1. CAP
分散式系統不可能同時滿足一致性(C:Consistency)、可用性(A:Availability)和分割槽容忍性(P:Partition Tolerance),最多隻能同時滿足其中兩項。
一致性
一致性指的是多個資料副本是否能保持一致的特性。
在一致性的條件下,系統在執行資料更新操作之後能夠從一致性狀態轉移到另一個一致性狀態。
對系統的一個資料更新成功之後,如果所有使用者都能夠讀取到最新的值,該系統就被認為具有強一致性。
可用性
可用性指分散式系統在面對各種異常時可以提供正常服務的能力,可以用系統可用時間佔總時間的比值來衡量,4 個 9 的可用性表示系統 99.99% 的時間是可用的。
在可用性條件下,系統提供的服務一直處於可用的狀態,對於使用者的每一個操作請求總是能夠在有限的時間內返回結果。
分割槽容忍性
網路分割槽指分散式系統中的節點被劃分為多個區域,每個區域內部可以通訊,但是區域之間無法通訊。
在分割槽容忍性條件下,分散式系統在遇到任何網路分割槽故障的時候,仍然需要能對外提供一致性和可用性的服務,除非是整個網路環境都發生了故障。
權衡
在分散式系統中,分割槽容忍性必不可少,因為需要總是假設網路是不可靠的。因此,CAP 理論實際在是要在可用性和一致性之間做權衡。
可用性和一致性往往是衝突的,很難都使它們同時滿足。在多個節點之間進行資料同步時,
- 為了保證一致性(CP),就需要讓所有節點下線成為不可用的狀態,等待同步完成;
- 為了保證可用性(AP),在同步過程中允許讀取所有節點的資料,但是資料可能不一致。
3.2. BASE
BASE 是基本可用(Basically Available)、軟狀態(Soft State)和最終一致性(Eventually Consistent)三個短語的縮寫。
BASE 理論是對 CAP 中一致性和可用性權衡的結果,它的理論的核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性。
基本可用
指分散式系統在出現故障的時候,保證核心可用,允許損失部分可用性。
例如,電商在做促銷時,為了保證購物系統的穩定性,部分消費者可能會被引導到一個降級的頁面。
軟狀態
指允許系統中的資料存在中間狀態,並認為該中間狀態不會影響系統整體可用性,即允許系統不同節點的資料副本之間進行同步的過程存在延時。
最終一致性
最終一致性強調的是系統中所有的資料副本,在經過一段時間的同步後,最終能達到一致的狀態。
ACID 要求強一致性,通常運用在傳統的資料庫系統上。而 BASE 要求最終一致性,通過犧牲強一致性來達到可用性,通常運用在大型分散式系統中。
在實際的分散式場景中,不同業務單元和元件對一致性的要求是不同的,因此 ACID 和 BASE 往往會結合在一起使用。
4. 分散式事務問題
4.1. 兩階段提交(2PC)
兩階段提交(Two-phase Commit,2PC)
主要用於實現分散式事務,分散式事務指的是事務操作跨越多個節點,並且要求滿足事務的 ACID 特性。
通過引入協調者(Coordinator)來排程參與者的行為,並最終決定這些參與者是否要真正執行事務。
執行過程
準備階段
協調者詢問參與者事務是否執行成功,參與者發回事務執行結果。
提交階段
如果事務在每個參與者上都執行成功,事務協調者傳送通知讓參與者提交事務;否則,協調者傳送通知讓參與者回滾事務。
需要注意的是,在準備階段,參與者執行了事務,但是還未提交。只有在提交階段接收到協調者發來的通知後,才進行提交或者回滾。
問題
同步阻塞
所有事務參與者在等待其它參與者響應的時候都處於同步阻塞狀態,無法進行其它操作。
單點問題
協調者在 2PC 中起到非常大的作用,發生故障將會造成很大影響,特別是在階段二發生故障,所有參與者會一直等待狀態,無法完成其它操作。
資料不一致
在階段二,如果協調者只傳送了部分 Commit 訊息,此時網路發生異常,那麼只有部分參與者接收到 Commit 訊息,也就是說只有部分參與者提交了事務,使得系統資料不一致。
太過保守
任意一個節點失敗就會導致整個事務失敗,沒有完善的容錯機制。
2PC 優缺點
優點:儘量保證了資料的強一致,適合對資料強一致要求很高的關鍵領域。(其實也不能 100%保證強一致) 缺點:實現複雜,犧牲了可用性,對效能影響較大,不適合高併發高效能場景。
4.2. 補償事務(TCC)
補償事務(TCC)其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。它分為三個階段:
- Try 階段主要是對業務系統做檢測及資源預留。
- Confirm 階段主要是對業務系統做確認提交,Try 階段執行成功並開始執行 Confirm 階段時,預設 Confirm 階段是不會出錯的。即:只要 Try 成功,Confirm 一定成功。
- Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放。
舉個例子,假設 Bob 要向 Smith 轉賬,思路大概是:
- 首先在 Try 階段,要先呼叫遠端介面把 Smith 和 Bob 的錢給凍結起來。
- 在 Confirm 階段,執行遠端呼叫的轉賬的操作,轉賬成功進行解凍。
- 如果第 2 步執行成功,那麼轉賬成功,如果第二步執行失敗,則呼叫遠端凍結介面對應的解凍方法 (Cancel)。
TCC 優缺點
- 優點:跟 2PC 比起來,實現以及流程相對簡單了一些,但資料的一致性比 2PC 也要差一些。
- 缺點:缺點還是比較明顯的,在 2,3 步中都有可能失敗。TCC 屬於應用層的一種補償方式,所以需要程式設計師在實現的時候多寫很多補償的程式碼,在一些場景中,一些業務流程可能用 TCC 不太好定義及處理。
4.3. 本地訊息表(非同步確保)
本地訊息表與業務資料表處於同一個資料庫中,這樣就能利用本地事務來保證在對這兩個表的操作滿足事務特性。
- 在分散式事務操作的一方完成寫業務資料的操作之後向本地訊息表傳送一個訊息,本地事務能保證這個訊息一定會被寫入本地訊息表中。
- 之後將本地訊息表中的訊息轉發到 Kafka 等訊息佇列(MQ)中,如果轉發成功則將訊息從本地訊息表中刪除,否則繼續重新轉發。
- 在分散式事務操作的另一方從訊息佇列中讀取一個訊息,並執行訊息中的操作。
這種方案遵循 BASE 理論,採用的是最終一致性。
本地訊息表利用了本地事務來實現分散式事務,並且使用了訊息佇列來保證最終一致性。
本地訊息表優缺點
- 優點:一種非常經典的實現,避免了分散式事務,實現了最終一致性。
- 缺點:訊息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。
4.4. MQ 事務訊息
有一些第三方的 MQ 是支援事務訊息的,比如 RocketMQ,他們支援事務訊息的方式也是類似於採用的二階段提交。但是市面上一些主流的 MQ 都是不支援事務訊息的,比如 RabbitMQ 和 Kafka 都不支援。
以阿里的 RocketMQ 中介軟體為例,其思路大致為:
- Prepared 訊息,會拿到訊息的地址。
- 執行本地事務。
- 通過第一階段拿到的地址去訪問訊息,並修改狀態。
也就是說在業務方法內要想訊息佇列提交兩次請求,一次傳送訊息和一次確認訊息。如果確認訊息傳送失敗了 RocketMQ 會定期掃描訊息叢集中的事務訊息,這時候發現了 Prepared 訊息,它會向訊息傳送者確認,所以生產方需要實現一個 check 介面,RocketMQ 會根據傳送端設定的策略來決定是回滾還是繼續傳送確認訊息。這樣就保證了訊息傳送與本地事務同時成功或同時失敗。
MQ 事務訊息優缺點
- 優點:實現了最終一致性,不需要依賴本地資料庫事務。
- 缺點:實現難度大,主流 MQ 不支援。
5. 共識性問題
5.1. Paxos
用於達成共識性問題,即對多個節點產生的值,該演算法能保證只選出唯一一個值。
主要有三類節點:
- 提議者(Proposer):提議一個值;
- 接受者(Acceptor):對每個提議進行投票;
- 告知者(Learner):被告知投票的結果,不參與投票過程。
演算法需要滿足 safety 和 liveness 兩方面的約束要求(實際上這兩個基礎屬性是大部分分散式演算法都該考慮的):
- safety:保證決議結果是對的,無歧義的,不會出現錯誤情況。
- 決議(value)只有在被 proposers 提出的 proposal 才能被最終批准;
- 在一次執行例項中,只批准(chosen)一個最終決議,意味著多數接受(accept)的結果能成為決議;
- liveness:保證決議過程能在有限時間內完成。
- 決議總會產生,並且 learners 能獲得被批准(chosen)的決議。
基本過程包括 proposer 提出提案,先爭取大多數 acceptor 的支援,超過一半支援時,則傳送結案結果給所有人進行確認。一個潛在的問題是 proposer 在此過程中出現故障,可以通過超時機制來解決。極為湊巧的情況下,每次新的一輪提案的 proposer 都恰好故障,系統則永遠無法達成一致(概率很小)。
Paxos 能保證在超過 $1/2$ 的正常節點存在時,系統能達成共識。
單個提案者+多接收者
如果系統中限定只有某個特定節點是提案者,那麼一致性肯定能達成(只有一個方案,要麼達成,要麼失敗)。提案者只要收到了來自多數接收者的投票,即可認為通過,因為系統中不存在其他的提案。
但一旦提案者故障,則系統無法工作。
多個提案者+單個接收者
限定某個節點作為接收者。這種情況下,共識也很容易達成,接收者收到多個提案,選第一個提案作為決議,拒絕掉後續的提案即可。
缺陷也是容易發生單點故障,包括接收者故障或首個提案者節點故障。
以上兩種情形其實類似主從模式,雖然不那麼可靠,但因為原理簡單而被廣泛採用。
當提案者和接收者都推廣到多個的情形,會出現一些挑戰。
多個提案者+多個接收者
既然限定單提案者或單接收者都會出現故障,那麼就得允許出現多個提案者和多個接收者。問題一下子變得複雜了。
一種情況是同一時間片段(如一個提案週期)內只有一個提案者,這時可以退化到單提案者的情形。需要設計一種機制來保障提案者的正確產生,例如按照時間、序列、或者大家猜拳(出一個數字來比較)之類。考慮到分散式系統要處理的工作量很大,這個過程要儘量高效,滿足這一條件的機制非常難設計。
另一種情況是允許同一時間片段內可以出現多個提案者。那同一個節點可能收到多份提案,怎麼對他們進行區分呢?這個時候採用只接受第一個提案而拒絕後續提案的方法也不適用。很自然的,提案需要帶上不同的序號。節點需要根據提案序號來判斷接受哪個。比如接受其中序號較大(往往意味著是接受新提出的,因為舊提案者故障概率更大)的提案。
如何為提案分配序號呢?一種可能方案是每個節點的提案數字區間彼此隔離開,互相不衝突。為了滿足遞增的需求可以配合用時間戳作為字首欄位。
此外,提案者即便收到了多數接收者的投票,也不敢說就一定通過。因為在此過程中系統可能還有其它的提案。
5.2. Raft
Raft 演算法是 Paxos 演算法的一種簡化實現。
包括三種角色:leader、candidate 和 follower,其基本過程為:
- Leader 選舉 - 每個 candidate 隨機經過一定時間都會提出選舉方案,最近階段中得票最多者被選為 leader;
- 同步 log - leader 會找到系統中 log 最新的記錄,並強制所有的 follower 來重新整理到這個記錄;
單個 Candidate 的競選
有三種節點:Follower、Candidate 和 Leader。Leader 會週期性的傳送心跳包給 Follower。每個 Follower 都設定了一個隨機的競選超時時間,一般為 150ms~300ms,如果在這個時間內沒有收到 Leader 的心跳包,就會變成 Candidate,進入競選階段。
- 下圖表示一個分散式系統的最初階段,此時只有 Follower,沒有 Leader。Follower A 等待一個隨機的競選超時時間之後,沒收到 Leader 發來的心跳包,因此進入競選階段。
- 此時 A 傳送投票請求給其它所有節點。
- 其它節點會對請求進行回覆,如果超過一半的節點回復了,那麼該 Candidate 就會變成 Leader。
- 之後 Leader 會週期性地傳送心跳包給 Follower,Follower 接收到心跳包,會重新開始計時。
多個 Candidate 競選
- 如果有多個 Follower 成為 Candidate,並且所獲得票數相同,那麼就需要重新開始投票,例如下圖中 Candidate B 和 Candidate D 都獲得兩票,因此需要重新開始投票。
- 當重新開始投票時,由於每個節點設定的隨機競選超時時間不同,因此能下一次再次出現多個 Candidate 並獲得同樣票數的概率很低。
同步日誌
- 來自客戶端的修改都會被傳入 Leader。注意該修改還未被提交,只是寫入日誌中。
- Leader 會把修改複製到所有 Follower。
- Leader 會等待大多數的 Follower 也進行了修改,然後才將修改提交。
- 此時 Leader 會通知的所有 Follower 讓它們也提交修改,此時所有節點的值達成一致。
6. 分散式快取問題
6.1. 快取雪崩
快取雪崩是指:在高併發場景下,由於原有快取失效,新快取未到期間(例如:我們設定快取時採用了相同的過期時間,在同一時刻出現大面積的快取過期),所有原本應該訪問快取的請求都去查詢資料庫了,而對資料庫 CPU 和記憶體造成巨大壓力,嚴重的會造成資料庫當機。從而形成一系列連鎖反應,造成整個系統崩潰。
解決方案:
- 用加鎖或者佇列的方式保證來保證不會有大量的執行緒對資料庫一次性進行讀寫,從而避免失效時大量的併發請求落到底層儲存系統上。
- 還有一個簡單的方案,就是將快取失效時間分散開,不要所有快取時間長度都設定成 5 分鐘或者 10 分鐘;比如我們可以在原有的失效時間基礎上增加一個隨機值,比如 1-5 分鐘隨機,這樣每一個快取的過期時間的重複率就會降低,就很難引發集體失效的事件。
快取失效時產生的雪崩效應,將所有請求全部放在資料庫上,這樣很容易就達到資料庫的瓶頸,導致服務無法正常提供。儘量避免這種場景的發生。
6.2. 快取穿透
快取穿透是指:使用者查詢的資料,在資料庫沒有,自然在快取中也不會有。這樣就導致使用者查詢的時候,在快取中找不到,每次都要去資料庫再查詢一遍,然後返回空(相當於進行了兩次無用的查詢)。這樣請求就繞過快取直接查資料庫,這也是經常提的快取命中率問題。
當在流量較大時,出現這樣的情況,一直請求 DB,很容易導致服務掛掉。
解決方案:
- 在封裝的快取 SET 和 GET 部分增加個步驟,如果查詢一個 KEY 不存在,就以這個 KEY 為字首設定一個標識 KEY;以後再查詢該 KEY 的時候,先查詢標識 KEY,如果標識 KEY 存在,就返回一個協定好的非 false 或者 NULL 值,然後 APP 做相應的處理,這樣快取層就不會被穿透。當然這個驗證 KEY 的失效時間不能太長。
- 如果一個查詢返回的資料為空(不管是資料不存在,還是系統故障),我們仍然把這個空結果進行快取,但它的過期時間會很短,一般只有幾分鐘。
- 採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的 bitmap 中,一個一定不存在的資料會被這個 bitmap 攔截掉,從而避免了對底層儲存系統的查詢壓力。
6.3. 快取預熱
快取預熱這個應該是一個比較常見的概念,相信很多小夥伴都應該可以很容易的理解,快取預熱就是系統上線後,將相關的快取資料直接載入到快取系統。這樣就可以避免在使用者請求的時候,先查詢資料庫,然後再將資料快取的問題!使用者直接查詢事先被預熱的快取資料!
解決方案:
- 直接寫個快取重新整理頁面,上線時手工操作下;
- 資料量不大,可以在專案啟動的時候自動進行載入;
- 定時重新整理快取;
6.4. 快取更新
除了快取伺服器自帶的快取失效策略之外(Redis 預設的有 6 中策略可供選擇),我們還可以根據具體的業務需求進行自定義的快取淘汰,常見的策略有兩種:
- 定時去清理過期的快取;
- 當有使用者請求過來時,再判斷這個請求所用到的快取是否過期,過期的話就去底層系統得到新資料並更新快取。
兩者各有優劣,第一種的缺點是維護大量快取的 key 是比較麻煩的,第二種的缺點就是每次使用者請求過來都要判斷快取失效,邏輯相對比較複雜!具體用哪種方案,大家可以根據自己的應用場景來權衡。
6.5. 快取降級
當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的效能時,仍然需要保證服務還是可用的,即使是有損服務。系統可以根據一些關鍵資料進行自動降級,也可以配置開關實現人工降級。
降級的最終目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的(如加入購物車、結算)。
免費Java資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程,一共30G。
傳送門: mp.weixin.qq.com/s/JzddfH-7y…