分散式系統遇到的問題及解決方案
主要參考: 分散式常見的十大坑,你瞭解幾個?
CAP理論
- 分散式系統在設計時只能在一致性(consistency)、可用性(availability)、分割槽容忍性(partition)中滿足兩種。
- 一致性指所有節點訪問同一份最新的資料副本,可用性指系統提供的服務一直處於可用狀態,分割槽容錯性指分散式系統在遇到任何網路分割槽故障的時候,仍需要保證對外提供一致性和可用性服務。在一個分散式系統中,不可能同時滿足三個特性,最多滿足兩個。
- CA放棄分割槽容忍性,關係資料庫按照CA設計
- AP放棄一致性,追求最終一致性,許多非關係型資料庫按照AP進行設計。
- CP放棄可用性,比如跨行轉賬,要求等待雙方銀行系統都完成整個事務才算完成。
BASE理論
- BASE是基本可用(basically available)、軟狀態(soft state)和最終一致性(eventually consistent) 三個短語的縮寫。
- base理論是對CAP中AP的一個擴充套件,通過犧牲強一致性來獲得可用性,當出現故障允許部分不可用但保證核心功能可用,允許資料在一段時間內是不一致的,但最終達到一致性狀態。
- 基本可用:允許損失部分可用功能,保證核心功能可用。
- 軟狀態:允許存在中間狀態,這個狀態不影響系統可用性,如訂單中的“支付中”,“資料同步中”等狀態。待資料最終一致後,改為成功狀態。
- 最終一致性: 指經過一段時間後,所有節點資料都將會達到一致。如“支付中”狀態最終會變為“支付成功”或者“支付失敗”。
熔斷、降級限流
參考:淺析降級、熔斷、限流
降級
- 降級也就是服務降級,當我們的伺服器壓力劇增,為了保證核心功能的可用性,而選擇性的降低了一些功能的可用性,或直接關閉該功能。
- 比如貼吧型別的網站,當伺服器吃不消時,可以選擇關閉發帖功能、使用者服務相關的功能等,保證登入和瀏覽帖子這種核心功能。
熔斷
- 降級一般指我們自身的系統出了故障而降級。而熔斷一般指依賴的外部接口出現故障,斷絕和外部介面的關係。
- 比如A服務中的一個功能依賴B服務,這時B服務出現了問題,返回很慢。此時就需要熔斷。即當發現A要呼叫B,此時就直接返回錯誤。
限流
- 限流指對某一時間視窗內的請求進行限制,保持系統可用性和穩定性,防止因流量暴增而導致系統執行緩慢或宕機。
- 一般限制的指標是請求總量或某段時間內請求總量。
訊息佇列如何做分散式?
冪等性概念
- 無論做多少次操作和第一次操作的結果一樣,則為冪等。用於解決訊息重複消費問題。
解決重複消費問題
- 插入資料庫場景:
- 每次插入資料時,先檢查資料庫中是否有這條資料的主鍵id,如果沒有,則進行更新操作。
- 寫redis場景:
- redis的set操作天然冪等性
- 其他場景:
- 生產者傳送每條資料時,增加一個全域性唯一id,每次消費時,去redis中檢查是否有這個id,如果沒有,則進行正常訊息處理。若有,則說明之前消費過,避免重複消費。
解決訊息丟失問題
如果是訂單下單、支付結果通知、扣費相關訊息丟失,可能造成財務損失。
1.生產者存放訊息的過程中丟失訊息
- 解決方案:
- 確認機制。每次生產者傳送的訊息都會分配一個唯一的id,如果寫到了訊息佇列中,則broker會回傳一個ack訊息,說明訊息接收成功。否則採用回撥機制,讓生產者重發訊息。
2.訊息佇列丟失訊息
- 解決方案:
- broker在訊息刷盤之後再給生產者響應。假設訊息寫入快取中就返回響應,那麼機器突然斷電這訊息就沒了,而生產者以為已經發送成功了。
- 如果broker是叢集部署,有多副本機制,則訊息不僅要寫入當前broker,還需要寫入副本機中。配置成至少寫入兩臺機子後再給生產者響應,這樣基本就能保證儲存的可靠了。
3.消費者丟失訊息
- 解決方案: 消費者處理完訊息,主動ack.
解決訊息亂序問題
- 生產者向訊息佇列按照順序傳送了 2 條訊息,訊息1:增加資料 A,訊息2:刪除資料 A。
- 期望結果:資料 A 被刪除。
- 但是如果有兩個消費者,消費順序是:訊息2、訊息 1。則最後結果是增加了資料 A。
-
解決方案:
-
全域性有序
-
只能有一個生產者往topic傳送訊息,並且一個topic內部只能有一個佇列。消費者也必須單執行緒消費這個佇列。
-
-
部分有序
-
將topic內部拆分,建立多個記憶體queue,訊息1和訊息2進入同一個queue.
-
建立多個消費者,每個消費者對應一個queue.
-
-
解決訊息積壓問題
-
訊息佇列中很多訊息來不及消費,場景如下:
- 消費者都掛了
- 消費者消費的速度太慢了
-
解決方案:
- 修復程式碼層面消費者的問題。
- 停掉現有的消費者。
- 臨時建立好原先5倍的Queue數量
- 臨時建立好原先5倍的消費者。
- 將堆積訊息全部轉入臨時的Queue
解決訊息過期失效
- 解決方案:
- 準備好批量重導的程式
- 手動將訊息閒時批量重導
分散式快取的問題
非同步複製資料導致資料丟失
- 主節點非同步同步資料給從節點過程中,主節點宕機了,導致部分資料未同步到從節點,而該從節點又被選舉為主節點,這個時候就有部分資料丟失了。
腦裂導致資料丟失
- 主節點所在機器脫離了叢集網路,實際上自身還是執行著的。但哨兵選舉出了備用節點作為主節點,這個時候就有兩個主節點都在執行,相當於兩個大腦在指揮這個叢集幹活,但到底聽誰的呢?這個就是腦裂。
- 發生腦裂後,客戶端還沒來得及切換到新的主節點,連的還是第一個主節點,那麼有些資料還是寫入到了第一個主節點中,新的主節點沒有這些資料。等到第一個主節點恢復後,會被作為備用節點連線到叢集環節,而且自身資料會被清空,重新從新的主節點複製資料。而新的主節點沒有之前客戶端寫入的某些資料,導致資料丟失了一部分。
- 解決方案:
- 配置 min-slaves-to-write 1,表示至少有一個備用節點。
- 配置 min-slaves-max-lag 10,表示資料複製和同步的延遲不能超過 10 秒。最多丟失 10 秒的資料。
分庫分表的問題
分庫、分表、垂直拆分、水平拆分
- 分庫: 因一個數據庫支援的最高併發訪問數是有限的,可以將一個數據庫的資料拆分到多個庫中,來增加最高併發訪問數。
- 分表: 因一張表的資料量太大,用索引來查詢資料都搞不定了,所以可以將一張表的資料拆分到多張表,查詢時,只用查拆分後的某一張表,SQL 語句的查詢效能得到提升。
- 分庫分表優勢:分庫分表後,承受的併發增加了多倍;磁碟使用率大大降低;單表資料量減少,SQL 執行效率明顯提升。
- 水平拆分: 把一個表的資料拆分到多個數據庫,每個資料庫中的表結構不變。用多個庫抗更高的併發。比如訂單表每個月有500萬條資料累計,每個月都可以進行水平拆分,將上個月的資料放到另外一個數據庫。
- 垂直拆分: 把一個有很多欄位的表,拆分成多張表到同一個庫或多個庫上面。高頻訪問欄位放到一張表,低頻訪問的欄位放到另外一張表。利用資料庫快取來快取高頻訪問的行資料。比如將一張很多欄位的訂單表拆分成幾張表分別存不同的欄位(可以有冗餘欄位)。
分庫分表之唯一ID
-
生成唯一ID的幾種方式:
-
資料庫自增ID(不適合)
-
UUID (太長,不具有有序性)
-
獲取系統當前時間作為唯一ID(高併發時,1ms內可能具有多個相同的ID)
-
snowflake(雪花演算法)
- 1 bit:不用,統一為 0
- 41 bits:毫秒時間戳,可以表示 69 年的時間。
- 10 bits:5 bits 代表機房 id,5 個 bits 代表機器 id。最多代表 32 個機房,每個機房最多代表 32 臺機器。
- 12 bits:同一毫秒內的 id,最多 4096 個不同 id,自增模式。
- 優點:
- 毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
- 可以根據自身業務特性分配bit位,非常靈活。
- 缺點:
- 強依賴機器時鐘,如果機器上時鐘回撥(可以搜尋 2017 年閏秒 7:59:60),會導致發號重複或者服務會處於不可用狀態。
-
百度的
UIDGenerator
演算法- 基於snowflake的優化演算法
- 借用未來時間和雙 Buffer 來解決時間回撥與生成效能等問題,同時結合 MySQL 進行 ID 分配。
- 優點:解決了時間回撥和生成效能問題。
- 缺點:依賴 MySQL 資料庫。
-
美團的
leaf-snowflake
演算法-
獲取 id 是通過代理服務訪問資料庫獲取一批 id(號段)。
-
雙緩衝:當前一批的 id 使用 10%時,再訪問資料庫獲取新的一批 id 快取起來,等上批的 id 用完後直接用。
-
優點:
-
- Leaf服務可以很方便的線性擴充套件,效能完全能夠支撐大多數業務場景。
- ID號碼是趨勢遞增的8byte的64位數字,滿足上述資料庫儲存的主鍵要求。
- 容災性高:Leaf服務內部有號段快取,即使DB宕機,短時間內Leaf仍能正常對外提供服務。
- 可以自定義max_id的大小,非常方便業務從原有的ID方式上遷移過來。
- 即使DB宕機,Leaf仍能持續發號一段時間。
- 偶爾的網路抖動不會影響下個號段的更新。
-
缺點:
-
- ID號碼不夠隨機,能夠洩露發號數量的資訊,不太安全。
-
-
分散式事務的問題
- 分散式中,存在各個服務之間相互呼叫,鏈路可能很長,如果有任何一方執行出錯,則需要回滾涉及到的其他服務的相關操作。
方案參考:兩天,我把分散式事務搞完了
2PC方案
- 角色:參與者和協調者。 階段:準備階段和提交階段。
- 準備階段:由事務協調者給每個參與者傳送準備命令,每個參與者收到命令之後會執行相關事務操作。但不會提交事務。
- 提交階段:協調者收到每個參與者的響應後進入第二階段,只要有一個參與者準備失敗,那麼協調者就向所有參與者傳送回滾命令,反之傳送提交命令。
- 協調者在第一階段中未收到個別參與者的響應,則等待一定時間就會認為事務失敗,會發送回滾命令,所以在2PC中事務協調者有超時機制。
- 優點:
- 利用資料庫自身功能進行本地事務的提交和回滾,不會入侵業務邏輯。
- 缺點:
- 同步阻塞:在第一階段執行了準備命令後,每個本地資源都處於鎖定狀態,因為除了事務提交啥都做了。
- 單點故障:協調者出現問題,整個事務就執行不下去了。
- 資料不一致: 由於網路可能會出現異常,那麼某些參與者無法收到協調者的請求,某些收到了。比如第二階段的提交請求,此時就產生了資料不一致問題。
TCC方案
-
TCC通過業務程式碼來實現事務的提交和回滾,對業務的侵入較大,是一種業務層面或應用層的兩階段提交。
-
Try階段:對各個服務的資源做檢測以及對資源進行鎖定或預留。
-
Confirm階段:各個服務中執行實際的操作。
-
Cancel階段:如果任何一個服務的業務方法執行出錯,需要將之前操作成功的步驟進行回滾。
-
優點:沒有資源的阻塞,每個方法都是直接提交事務的。
-
缺點:對業務有很大的侵入。
-
注意點:
- 冪等問題:因為網路呼叫無法保證請求一定能夠到達,都會有重調機制,因此對於Try、Confirm、Cancel三個方法都需要冪等實現,避免重複執行產生錯誤。
- 空回滾問題:try方法由於網路問題阻塞超時了,此時事務管理器就會發出Cancel命令。那麼需要支援 Cancel 在未執行 Try 的情況下能正常的 Cancel。
- 懸掛問題:try方法由於網路問題阻塞超時了,觸發了事務管理器的Cancel命令。但執行之後try請求到了。此時凍結操作就被懸掛了,所以空回滾之後還得記錄一下,防止 Try 的再呼叫。
事務訊息方案
- 主要適用於非同步更新的場景,且對資料實時性要求不高的地方。目的是為了解決訊息生產者和消費者之間的資料一致性問題。
- 基本原理:利用RocketMQ來實現訊息事務。保證下單和發訊息這兩個步驟要麼都成功要麼都失敗。
- 第一步:A 系統傳送一個半訊息到
broker
,broker
將訊息狀態標記為prepared
,該訊息對consumer
是不可見的。 - 第二步:broker 響應 A 系統,告訴 A 系統已經接收到訊息了。
- 第三步:A 系統執行本地事務。
- 第四步:若 A 系統執行本地事務成功,將
prepared
訊息改為commit
(提交事務訊息),B 系統就可以訂閱到訊息了。 - 第五步:
broker
也會定時輪詢所有prepared
的訊息,回撥 A 系統,讓 A 系統告訴broker
本地事務處理得怎麼樣了,是繼續等待還是回滾。 - 第六步:A 系統檢查本地事務的執行結果。
- 第七步:若 A 系統執行本地事務失敗,則
broker
收到Rollback
訊號,丟棄訊息。若執行本地事務成功,則broker
收到Commit
訊號。 - B 系統收到訊息後,開始執行本地事務,如果執行失敗,則自動不斷重試直到成功。或 B 系統採取回滾的方式,同時要通過其他方式通知 A 系統也進行回滾。
- B 系統需要保證冪等性。
最大努力通知方案
- 基本原理:
- 系統A執行本地事務後,傳送訊息到
broker
。 broker
將訊息持久化。- 系統B如果執行本地事務失敗,則最大努力服務會定時嘗試重新呼叫系統B,儘自己最大的努力讓系統 B 重試,重試多次後,還是不行就只能放棄了。轉到開發人員去排查以及後續人工補償。
- 系統A執行本地事務後,傳送訊息到
方案選擇
-
跟支付、交易打交道,優先
TCC
。 -
大型系統,但要求不那麼嚴格,考慮 訊息事務方案。
-
單體應用,建議
XA
兩階段提交就可以了。(XA
是2PC
的落地實現) -
最大努力通知方案建議都加上,畢竟不可能一出問題就交給開發排查,先重試幾次看能不能成功。