天天看點

一種提高微服務架構的穩定性與資料一緻性的方法

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

有大量的同步 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 流量會被完整地跑完。

一種提高微服務架構的穩定性與資料一緻性的方法

其他方案

實作跨資料庫和消息隊列的事務一緻性,還有兩種做法:

  • 去哪兒網,利用資料庫作為隊列,然後用資料庫的多表事務來保障一緻性:設計消息中間件時我關心什麼?(解密電商資料一緻性與完整性實作,含PPT)
  • 淘寶 Notify,利用兩階段送出的消息 broker 來實作:淘寶的消息中間件(2013) - taowen - SegmentFault

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

總結

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

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

我們隻需要把這三個獨立的方案結合到一起,就可以把隊列技術應用到純 RPC 同步組合的微服務叢集裡,用于提高可用性和資料的一緻性。同時可以保證這份消息資料是可靠的,進而給其他的業務邏輯把自己放在隊列後面,建立了前提條件。

熬夜不易,點選請老王喝杯烈酒!!!!!!!