一種提高微服務架構的穩定性與數據一致性的方法



微服務架構解決了很多問題,但是同時引入了很多問題。本文要探討的是如何解決下面這幾個問題。

有大量的同步 RPC 依賴,如何保證自身的可靠性?

依賴的微服務調用失敗了,我應該失敗,還是成功。依賴很多外部服務之后,自身如何保障穩定性。如果所有依賴的服務成功,我才算成功,自身的穩定性就堪憂了。


RPC 調用失敗,降級處理之后如何保證數據可修復?

如果調用失敗時,選擇跳過。那么因此產生的數據不一致性問題如何修復?平時毛毛雨,可以忽略。但是大故障之后,人工還是要來擦屁股的,這個成本就特別高。使用消息隊列的最大的意義是在讓消息可以在故障的時候堆積起來,等故障恢復了再慢慢來處理,減少人工介入的成本。

消息隊列是一個RPC主流程的旁路流程,怎么保證可靠性?

依賴消息隊列做系統解耦的時候,怎么確保消息自身是可靠入隊列的?消息是否需要先可靠寫入隊列,然后再提交數據庫事務?如果消息必須先寫入隊列,比如 kafka。但是 kafka 掛了怎么辦?那我在線業務豈不被離線的隊列給連累了?

消息隊列怎么保持與數據庫的事務一致?

如果消息是先寫入隊列,然后數據庫提交事務。那么就會有因為並發修改的情況下,數據庫提交失敗,但是消息已經寫入到隊列的情況。如果隊列后面掛了獎勵等業務流程,這個時候就會導致錯發,或者要求獎勵那邊去再查一遍數據庫的狀態。但是如果先提交數據庫事務,后寫入隊列,又無法嚴格保證隊列里的消息是沒有丟失的。

這些問題是所有混用了 RPC 和異步隊列的業務都會遇到的普遍問題。這里我給一個提案來解決以上的所有問題。

同步轉異步,解決穩定性問題

在平時的時候,都是 RPC 同步調用。如果調用失敗了,則自動把同步調用降級為異步的。消息此時進入隊列,然后異步被重試。所以處理下游依賴就變成了三種可能性

  • 完全強依賴,下游不能掛
  • 因為我的返回值依賴了某個下游的處理結果,我必須同步調用它。但是不是強依賴,可降級。降級時不返回這部分的數據。同步調用降級時轉為異步的。
  • 完全異步化。下游服務只是消費我寫入的隊列,我不與之直接RPC通信

把消息隊列放入到主流程

如果要把重要的業務邏輯掛在消息隊列后面。必須要保證消息隊列里的數據的完整性,不能有丟失的情況。所以不能是把消息隊列的寫入作為一個旁路的邏輯。如果消息隊列寫入失敗或者超時,都應該直接返回錯誤,而不是允許繼續執行。

Kafka 的穩定性和延遲時常不能滿足在線服務的需要。比如如果要可靠寫入三副本,Kafka 需要等待多個 broker 的應答,這個延遲可能會有比較大的波動。在無法及時寫入的情況,我們需要使用本地文件充當一個緩沖。實際上是通過引入本地文件隊列結合遠程分布式隊列構成一個可用性更高,延遲更低的組合隊列方案。這個本地的隊列如果能封裝到一個 Kafka 的 Agent 作為本地寫入的代理,那是最理想的實現方式。

保障數據庫與隊列的事務一致性

需求是當數據庫的事務成功時,消息一定要保證寫入了隊列里。如果數據庫的事務失敗,消息不應該出現在隊列里。所以肯定不能先寫隊列,再寫數據庫,否則要讓 Kafka 支持消息的回滾,這會是一個很麻煩的事情。那么就要防范這么兩種情況

  • 數據庫寫入成功。然后寫隊列,但是隊列寫入失敗。返回錯誤,讓上游重試。但是上游可能會放棄,導致消息丟失。
  • 數據庫寫入成功。然后全機房斷電了。

這兩種情況下都會出現消息沒有寫入隊列的情況。如何僅僅依靠 Kafka 和 Mysql 這兩個組件,實現數據庫與隊列的事務一致性呢?構想如下

  1. 所有請求,先寫入到 write-ahead-queue 這個 topic。如果這個消息就寫入失敗,直接返回錯誤給調用方,讓其重試。
  2. 處理數據庫事務
  3. 如果數據庫事務失敗。則移動 write-ahead-queue 的 offset,代表這個請求已經被處理完畢。
  4. 如果數據庫事務成功。則接下來寫 business-event-queue 這個 topic
  5. 如果寫入隊列成功。則移動 write-ahead-queue 的 offset,代表這個請求已經被處理完畢。
  6. 如果寫入隊列失敗,返回成功給調用方。然后異步去重試寫入 business-event-queue 這個 topic
  7. 在數據庫事務成功到消息寫入到business-event-queue這個topic中間,write-ahead-queue 的 offset 都是沒有被移動的。也就是如果這個過程被中斷,可以從 write-ahead-queue 恢復回來。
  8. 經過重試,最終 business-event-queue 寫入成功。這個時候移動 write-ahead-queue 的 offset,標記這個請求被處理完畢

也就是說,通過引入 write-ahead-queue,以及控制這個 topic 的 offset 位置,來標記完整的分布式事務是否已經被處理完成。在過去,這個處理是否完成是以數據庫的事務為標准的,沒有辦法保障數據庫事務之后發生的事情的必然發生。


雖然看上去很復雜。但是這個連兩階段提交都不是,因為沒有回滾的需求,只要數據庫寫入成功,消息隊列寫入無論如何都要成功。整個方案的關鍵是通過 write-ahead-queue 的寫入和offset的移動這兩個動作,標記了一個分布式事務的范圍。只要這個過程沒有完全做完,就會通過不斷重試 write-ahead-queue 的方式保證其最終會被完整執行。

在沒有 write-ahead-queue 的時候,我們的 RPC 執行過程是這樣的


這個串行過程,因為沒有保護,所以可能被中斷,不能被確保完整執行。引入 write-ahead-queue 的目的就是讓這個過程變得可靠


Write-Ahead-Queue 的 Offset 管理

前面的事務方案的假設是整個處理過程,對於一個 Kafka 的Partition 是獨占的。這也就意味着有多少個 RPC 的並發處理線程(或者協程)就需要有多少個對應的 Partition 來跟蹤對應線程的處理狀態。這樣就會變得很不經濟,需要開大量的 Kafka Partition。但是如果讓多個 RPC 線程共享一個 Kafka Partition,那么由誰來移動 Offset 來標記事務的執行成功呢?這里就需要引入一個 Offset 管理者,來去協調多個 RPC 線程的 Offset 的移動。

  1. RPC 線程1,寫入了 WAL1 (Write-Ahead-Log),其 Offset 為 1
  2. RPC 線程2,寫入了 WAL2,其 Offset 為 2
  3. RPC 線程3,寫入了 WAL3,其 Offset 為 3
  4. RPC 線程3執行完畢,欲把WAL3標記為執行成功,移動Offset到3。但是因為前面1和2,還沒有執行成功,這個時候Offset不能被移動。
  5. RPC 線程1執行完畢,欲把WAL1標記為執行成功,移動Offset到1。因為前面沒有尚未執行完成的WAL,所以這個時候Offset被移動到1成功。
  6. RPC 線程2執行完畢,欲把WAL2標記為執行成功,移動Offset到2。因為后面的3已經被執行完了,所以Offset被直接更新為3。

這個處理邏輯和 TCP 的窗口移動邏輯是非常類似的。用這種方式,大概就是一個RPC的進程,對應一個kafka的partition去跟蹤它的處理流程。相當於給 RPC 框架,加了一個 WAL 的保護,用於保證 RPC 流量會被完整地跑完。


其他方案

實現跨數據庫和消息隊列的事務一致性,還有兩種做法:

兩種實現都需要用 mysql 來作為消息中間件,引入了比較高的運維成本。


總結

前面給了三個獨立的技術方案

  • 使用同步轉異步的方案,提高同步 RPC 的可用性,同時提高數據一致性。
  • 引入本地隊列作為兜底,提高消息隊列的總體可用性,以及降低延遲。
  • 通過引入兩級隊列,讓 Write-Ahead-Queue 來保證 Business-Event-Queue 一定會在數據庫事務成功之后被寫入。

我們只需要把這三個獨立的方案結合到一起,就可以把隊列技術應用到純 RPC 同步組合的微服務集群里,用於提高可用性和數據的一致性。同時可以保證這份消息數據是可靠的,從而給其他的業務邏輯把自己放在隊列后面,建立了前提條件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM