1. 程式人生 > 實用技巧 >分散式系統遇到的問題及解決方案

分散式系統遇到的問題及解決方案

主要參考: 分散式常見的十大坑,你瞭解幾個?

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號碼不夠隨機,能夠洩露發號數量的資訊,不太安全。

分散式事務的問題

  • 分散式中,存在各個服務之間相互呼叫,鏈路可能很長,如果有任何一方執行出錯,則需要回滾涉及到的其他服務的相關操作。

方案參考:兩天,我把分散式事務搞完了

訊息佇列之事務訊息,RocketMQ 和 Kafka是如何做的?

2PC方案

  • 角色:參與者和協調者。 階段:準備階段和提交階段。
  • 準備階段:由事務協調者給每個參與者傳送準備命令,每個參與者收到命令之後會執行相關事務操作。但不會提交事務。
  • 提交階段:協調者收到每個參與者的響應後進入第二階段,只要有一個參與者準備失敗,那麼協調者就向所有參與者傳送回滾命令,反之傳送提交命令。
  • 協調者在第一階段中未收到個別參與者的響應,則等待一定時間就會認為事務失敗,會發送回滾命令,所以在2PC中事務協調者有超時機制。
  • 優點:
    • 利用資料庫自身功能進行本地事務的提交和回滾,不會入侵業務邏輯。
  • 缺點:
    • 同步阻塞:在第一階段執行了準備命令後,每個本地資源都處於鎖定狀態,因為除了事務提交啥都做了。
    • 單點故障:協調者出現問題,整個事務就執行不下去了。
    • 資料不一致: 由於網路可能會出現異常,那麼某些參與者無法收到協調者的請求,某些收到了。比如第二階段的提交請求,此時就產生了資料不一致問題。

TCC方案

  • TCC通過業務程式碼來實現事務的提交和回滾,對業務的侵入較大,是一種業務層面或應用層的兩階段提交。

  • Try階段:對各個服務的資源做檢測以及對資源進行鎖定或預留。

  • Confirm階段:各個服務中執行實際的操作。

  • Cancel階段:如果任何一個服務的業務方法執行出錯,需要將之前操作成功的步驟進行回滾。

  • 優點:沒有資源的阻塞,每個方法都是直接提交事務的。

  • 缺點:對業務有很大的侵入。

  • 注意點:

    • 冪等問題:因為網路呼叫無法保證請求一定能夠到達,都會有重調機制,因此對於Try、Confirm、Cancel三個方法都需要冪等實現,避免重複執行產生錯誤。
    • 空回滾問題:try方法由於網路問題阻塞超時了,此時事務管理器就會發出Cancel命令。那麼需要支援 Cancel 在未執行 Try 的情況下能正常的 Cancel。
    • 懸掛問題:try方法由於網路問題阻塞超時了,觸發了事務管理器的Cancel命令。但執行之後try請求到了。此時凍結操作就被懸掛了,所以空回滾之後還得記錄一下,防止 Try 的再呼叫。

事務訊息方案

  • 主要適用於非同步更新的場景,且對資料實時性要求不高的地方。目的是為了解決訊息生產者和消費者之間的資料一致性問題。

  • 基本原理:利用RocketMQ來實現訊息事務。保證下單和發訊息這兩個步驟要麼都成功要麼都失敗。
  • 第一步:A 系統傳送一個半訊息到 brokerbroker將訊息狀態標記為 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 重試,重試多次後,還是不行就只能放棄了。轉到開發人員去排查以及後續人工補償。

方案選擇

  • 跟支付、交易打交道,優先 TCC

  • 大型系統,但要求不那麼嚴格,考慮 訊息事務方案。

  • 單體應用,建議 XA兩階段提交就可以了。(XA2PC的落地實現)

  • 最大努力通知方案建議都加上,畢竟不可能一出問題就交給開發排查,先重試幾次看能不能成功。