隨著軟體系統從單體應用邁向微服務架構以及資料庫選型去中心化、異構化的趨勢,傳統的ACID事務在分布式系統上能否延續,如何落地,有哪些注意事項?本文將圍繞分布式事務這一技術議題,介紹FreeWheel核心業務系統在相關領域的業務需求、技術決策和線上實踐。
分布式事務的挑戰
技術演進
FreeWheel 核心業務產品歷經十多年的積累和迭代,伴隨著數據體量和功能複雜度的上升,支撐 FreeWheel 核心業務的工程團隊所採用和探索的技術也在不斷演化和革新。
系統拓撲方面:
早期 FreeWheel 核心業務系統是一個單體應用(Monolith):在同一臺伺服器的同一個進程中,完成接收客戶請求、處理請求、數據存儲、返迴響應等步驟。為了提升系統整體的可靠性,方便各個模塊的獨立演化,工程團隊對單體應用進行了拆分部署和服務化,邁向了面向服務的架構(SOA)。隨著服務的不斷細分,單個服務的功能變得更加聚焦,基礎服務和公用設施的組合/編排邏輯則變得更加錯綜複雜,有向微服務發展的趨勢。依託近年來蓬勃發展的雲計算平臺 AWS,FreeWheel 的技術團隊還在積極探索無服務(Serverless)技術。
數據存儲方面:
FreeWheel 核心業務系統最早廣泛使用了以 MySQL 為代表的關係型資料庫(RDBMS)。後來為了滿足多樣化索引和查詢數據的需求,引入了以 ApacheSolr 和 ElasticSearch 為代表的搜素引擎(Search Engine)。隨著數據體量的增長,傳統的關係型資料庫已無法滿足分布式存取海量數據的需求,為此又引入了以 AmazonDynamoDB 和 MongoDB 為代表的 NoSQL 資料庫 。
事務類需求
在諸多變化背後,客戶多年積累下來的使用習慣其實是難以改變的。而看上去日新月異的產品迭代需求,經過抽象不難發現一些恆定的規律和模式:
同步和有序的數據變更:客戶習慣於在集中的入口(UI / API)提交一組數據變更請求,希望在儘可能短的時間內,得到返回結果(成功或失敗);接下來做何種操作,提交什麼數據,取決於之前步驟的執行結果。批量修改,統一結果:一次請求如果對應多條數據變更操作(增加、刪除、修改數據),不管這些操作發生在哪些服務、落到哪個資料庫,最好要麼都成功,要麼都失敗。傳統關係型資料庫中,一批數據操作同時成功、同時失敗的這類需求共性被抽象為事務性,英文縮寫為 ACID:
A (Atomicity, 原子性):一組數據操作如果其中某步操作失敗,之前的操作也要回滾,不允許出現部分成功部分失敗的情況。C(Consistency,一致性):數據操作符合某種業務約束。這個概念來源於財務對帳領域,拓展到資料庫設計上的含義比較模糊,眾說紛紜。甚至有資料說C是為了湊成ACID這個縮寫而添加的。I(Isolation,隔離性):對並發的數據操作有一定的隔離性。Isolation是分等級的, 最差的情況是毫無隔離、互相干擾;最好的情況是並發操作等效於一系列串行操作(Serializable,可串行化)。Isolation等級越高,資料庫需要的資源越多,存取數據的性能(如吞吐量、延遲)越差。D(Durability,持久性):到達資料庫的請求不會「輕易」丟失。通常資料庫設計文檔會對「輕易」做具體的定義,比如在磁碟壞道,機器停電重啟等條件下不會丟數據。隨著系統的服務拓撲從單體應用邁向微服務時代,以及資料庫數量和種類的增長,分布式系統在滿足傳統 ACID 標準的事務性需求上,面臨著新的挑戰。所謂的 CAP 三選二定理是說,任何一個分布式系統不能同時滿足以下三個特性:
C(Consistency,強一致性):分布式系統的任何節點對同一個key的讀寫請求,得到的結果完全一致。也叫線性一致性。A(Availability,可用性):每次請求都能得到及時和正常的響應,但不保證數據是最新的。P(Partition tolerance,承受網絡分隔):分布式系統在節點之間無法連通或者連接超時的前提下還能維持運轉。在 CAP 三個特性中,P 通常是分布式系統無法規避的既定事實,設計者只能在 C 和 A 之間進行取捨。大部分系統經過綜合考慮,都選擇了 A 而放棄 C,目標是高可用,最終一致(不過達成一致需要的時間無上限)。少部分系統堅持 C 而放棄 A,即選擇強一致、低可用(單節點故障將導致服務不可用,可用率取決於故障頻度和恢復時間,無上限)。
技術選型與方案設計
設計目標
我們考慮通過引入一套分布式事務方案,達成以下各項設計目標:
事務性提交:即ACID中的Atomicity。業務根據需要,可以定義一組數據操作,即分布式事務,這組操作無論發生在哪個服務和資料庫,要麼同時成功,要麼同時失敗。事務中只要任何一個操作出現失敗, 之前的操作都需要回滾。系統高可用:當部分服務的部分節點出現故障時,系統整體仍然可用。通過支持服務快速擴容和縮容,實現系統整體的高吞吐量,儘可能縮短數據達成一致性的延遲。框架本身消耗的資源低,引入的額外延遲小。數據最終一致性:並發操作同一條數據的請求到達各個服務和資料庫的次序保持一致,不出現丟失、亂序。 舉一個順序不一致的例子:
如上圖,A、B、C 是三個服務/資料庫, 1 和 2 為並發修改同一個 key 的兩個請求。由於隨機網絡延遲,最終落在三個服務/資料庫的值不一致,A 為 2 的值,B 和 C 為 1 的值。
支持服務獨立演化和部署:除了支持使用RPC和給定協議進行通信之外,不對服務的實現方式做過多要求和假設。支持服務使用異構的數據存儲技術:使用不同的數據存儲技術(關係型資料庫、NoSQL、搜尋引擎等),是FreeWheel核心業務系統的各個服務的現狀和努力方向。架構侵入性低,易於採用:不改動或少改動現有系統的代碼和部署,儘量只通過新增代碼以及服務部署,來實現分布式事務的運行環境和具體業務流程。框架和業務的分工明確,框架代碼維持100%測試覆蓋率, 業務代碼100%可測試,測試成本低。保持系統高可見性和可預測性,儘可能為快速故障定位和恢復提供便利。支持同步和異步流程:提供一種機制,將UI/API和後端入口服務之間的同步交互流程,與可能出現的後端服務之間的異步流程銜接起來。支持事務步驟依賴:事務裡面某個步驟的數據操作是否執行、如何執行,取決於前面的步驟的操作結果。技術選型
XA 協議和多階段提交
XA 協議通過引入一個協調者的角色,以及要求所有參與事務的資料庫支持 Two-phaseCommit(2PC,兩階段提交,即先準備,後提交或回滾)來實現分布式事務。
(圖片來源:https://docs.particular.net/nservicebus/azure/understanding-transactionality-in-azure)
使用 XA 實現分布式事務的優點有:
強一致性:實現了數據在多個資料庫上的強一致提交。業務侵入性小:完全靠資料庫本身的支持實現分布式事務,不需要改動業務邏輯。使用 XA 實現分布式事務的缺點也很明顯:
單點故障:協調者或者任意一個XA資料庫都是能引起故障的單點(Single point of failure)。低性能:支持XA特性的資料庫在設計上有大量的阻塞和資源佔位操作, 數據體量和吞吐量擴展性差。資料庫選型限制:對於服務的資料庫選型引入了支持XA協議這個限制。XA 在設計上沒有考慮到分布式系統的特點,事實上是一個強一致、低可用的設計方案,對網絡分隔的容忍度較差。
Saga
Saga 原意是長篇神話故事。它實現分布式事務的思路是實現一種驅動流程機制,按順序執行每個數據操作步驟,一旦出現失敗,就倒序執行之前各步驟對應的「補償」操作。這要求每個步驟涉及到的服務提供與正向操作接口對應的補償操作接口。
使用 Saga 實現分布式事務的優點有:
微服務架構:通過對一些基礎服務進行組合/編排來完成各種業務需求。資料庫兼容性高:對每個服務使用何種資料庫技術沒有任何要求,服務甚至可以不使用資料庫。使用 Saga 實現分布式事務的缺點有:
要求服務提供補償接口:增加了開發和維護的成本。不符合ACID:沒有涉及Isolation和Durability。Saga 從流程上,還可分為兩種模式:Orchestration(交響樂)和 Choreography(齊舞)。
Saga OrchestrationSaga Orchestration 引入了類似 XA 中的協調者的角色,來驅動整個流程。
(圖片來源:https://medium.com/trendyol-tech/saga-pattern-briefly-5b6cf22dfabc)
如上圖,Order Service 發起分布式事務,Orchestrator 負責驅動分布式事務流程,PaymentService 和 Stock Service 負責提供數據操作的正向接口和補償接口。
Saga ChoreographySaga Choreography 將流程分拆到每個步驟涉及到的服務中,由每個服務自行調用後序或前序服務。
(圖片來源:https://medium.com/trendyol-tech/saga-pattern-briefly-5b6cf22dfabc)
如上圖,Order Service 直接調用 PaymentService 來發起分布式事務,後者再調用 Stock Service,直到完成所有步驟;一旦某步驟出現失敗,服務之間會反向調用。
ACID 事務鏈
ACID 事務鏈可以看作是 SagaChoreography 的增強版,它要求參與分布式事務的所有服務都使用支持傳統 ACID 事務的資料庫,然後通過將每個服務內部的數據操作和同步調用相鄰服務的操作打包到一個 ACID 事務中,通過 ACID 事務的鏈式調用實現分布式事務。
使用 ACID 事務鏈實現分布式事務的優點有:
符合ACID:每個步驟都是傳統ACID事務,整體也符合ACID事務性不需要服務提供補償接口:由支持ACID事務的資料庫進行回滾操作使用 ACID 事務鏈實現分布式事務的缺點有:
資料庫選型限制:對於服務的資料庫選型引入了支持傳統ACID事務這個限制。服務耦合過多:服務之間的依賴是鏈式拓撲,不方便調整步驟順序;隨著使用分布式事務的各種業務流程的增加,很容易產生服務之間的循環依賴,給部署造成困難。選擇 Saga Orchestration
我們首先排除了 XA 方案,它無法滿足系統的可用性和擴展性。其次排除了 ACID 事務鏈,因為它不兼容業務現有的資料庫選型,未來還會引入更多不支持 ACID 事務的資料庫技術。
最終決定採用 Saga 來實現高可用、低延遲、最終一致的分布式事務框架,主要原因是其設計思想非常契合於目前 FreeWheel 核心業務團隊的 SOA/微服務/Serverless 實踐,即通過對一些基礎服務(對於 Serverless 其實是 Lambda,以下不再區分)進行組合/編排來完成各種業務需求。
在 Saga 的兩個變種中,我們選擇了 Orchestration 而不是 Choreography,原因是:
服務解耦:Orchestration天然地將事務本身的驅動邏輯和眾多基礎服務解耦,而Choreography在不引入隊列的前提下,容易出現服務間循環依賴的問題。服務分層:Orchestration天然地將服務分成了組合/編排器和基礎服務兩個調用層級,有利於業務邏輯的擴展和重用。數據解耦:對於某個步驟依賴前序多個步驟結果的業務場景,後者需要前序所有服務透傳其他服務的數據,而Orchestration不需要。採用 Saga Orchestration,勢必需要想辦法克服它的兩個缺點,即要求基礎服務提供補償接口,以及沒有實現 ACID 中的 Isolation 和 Durability。
如何實現數據補償操作呢?數據操作可分為 Insert(新建),Delete(刪除)和 Update(更新)三種,而 Update 又可細分為 Full update(Replace,整體更新)和 Partial update(Patch,部分更新),它們對應的補償操作如下:
Insert:補償操作是Delete,參數為數據的ID,要求在Insert操作之後記錄下數據的ID。Delete:補償操作是Insert,參數為完整的數據,要求在進行Delete操作前記下當前完整的數據。Full update:補償操作是另一個Full update,參數為完整的數據,要求在進行原Full update操作前記下當前完整的數據。Partial update:補償操作是Partial / Full update,參數為改動前的部分數據或者完整數據,要求在進行原Partial update操作前記下當前部分或完整的數據。再來看下如何實現 ACID 中的 I 和 D:
Isolation:其實是並發控制的問題,即如何處理對同一條數據(同一個key)的並發操作。MySQL給出的解決方案是多版本並發控制(MVCC),然而不是所有的資料庫都支持這一特性。控制並發的另一條思路是消除並發,化並為串,一般通過搶佔鎖或者使用隊列來實現。考慮到等待鎖而產生的性能損耗以及鎖順序不一致導致的互鎖問題,優先考慮使用隊列。Durability:指成功提交到系統的事務不能中途丟失,即實現數據持久化。需要考慮的故障包括數據存儲節點的故障和數據處理節點的故障。綜上所述,我們需要增加一個隊列+持久化的技術方案來補足 Saga 的短板,實現 ACID。結合 FreeWheel 核心業務系統現有的基礎設施,我們優先考慮引入 ApacheKafka(以下簡稱 Kafka)。
引入 Kafka
Kafka 是一個功能豐富的隊列+持久化解決方案,針對分布式事務的設計目標,我們看中的是它的這些能力:
消息保序:引入隊列來化並為串,解決並發寫入數據的Isolation問題。消息送達保證:支持「至少一次」(at least once)的消息送達保證,具有冗餘備份和故障恢復能力,有助於解決ACID的Durability問題。性能優秀:各種資料表明,Kafka本身的效率和可靠性都是行業標杆,如果使用得當,它至少不會成為系統的性能瓶頸。另一方面,Kafka 作為一個強大的隊列解決方案,它的眾多特性給分布式事務的設計和實現帶來了新的機遇和挑戰。引入隊列之前,從客戶點擊瀏覽器按鈕,到數據落盤再到返迴響應數據,主流程上的節點都是同步交互的:
如上圖,實線箭頭為 RPC 請求,虛線箭頭為 RPC 響應(下同),數據按照序號標註的順序從客戶發起,先後經過 A、B 和 C 三個服務,所有步驟都是同步的。
引入隊列之後,列兩端的生產者和消費者彼此隔開,整個過程變成了同步→異步→同步:
如上圖,1 和 2 之間是同步的, 2 和 3 之間是異步的,接下來的 3 到 7 又是同步的。
通過化同步為異步,系統整體的吞吐量和資源利用率可以得到進一步的提升。隨之而來的問題是為了維持同步的前端數據流程,需要增加同步流程和異步流程如何銜接的設計。
同步轉異步比較簡單,在此不做討論。異步轉同步的時候,需要建立一種消費者所在節點和生產者所在節點進行點對點通信的機制。我們採取的方案是直接回調:生產者把回調地址打包到消息裡,消費者處理完成後將處理結果發送到回調地址。
分布式事務架構
基於 Saga Orchestration 和 Kafka 的分布式事務架構如下圖所示:
其中服務 A 是編排組織器,它負責驅動 SagaOrchestration 的流程, 服務 B、C、D 是三個使用了獨立且異構的資料庫的基礎服務。
由於使用了 Saga Orchestration 而不是 Choreography,只有服務 A 能感知到分布式事務並且依賴 Kafka 和 Saga,基礎服務 B、C、D 只需要多實現幾個補償接口供 A 調用,沒有產生對 Kafka 和 Saga 的依賴。
分布式事務流程
服務 A 從接到用戶請求,觸發分布式事務,分步驟調用各個基礎服務,到最終返迴響應,流程如下圖:
步驟詳解:
1-2: 服務A的某個節點在接到用戶請求後,首先擔當生產者的角色,將用戶請求和回調地址包裝成消息發送到Kafka,然後處理該用戶請求的處理單元阻塞等待。3-5: 同一個服務A的某個節點的消費者從Kafka接到消息,開始驅動Saga Orchestration的流程,按照業務定義的順序和邏輯依次調用服務B和C的接口。6-7: Saga流程結束後,消費者向Kafka發送消費進度確認操作(ackMessage,也就是更新consumer group offset),然後將結果(成功還是失敗,做了哪些改動)通過RPC回調地址發送給生產者。8: 生產者從回調地址接到數據後,找到對應的用戶請求處理單元,解除阻塞,最後將結果封裝成用戶響應。隊列消息協議設計
一條隊列消息至少包含兩部分信息:元數據(Metadata)和內容(Content)。
元數據:由分布式事務框架讀取和寫入,使用JSON格式,欄位格式固定,業務代碼只能讀取,不能寫入。元數據中最重要的欄位是分布式事務消息的類型(以下簡稱TxType)。生產者通過強類型來指定消息的TxType;消費者進程中的分布式事務框架會根據TxType進行事件分流(event sourcing),調用對應業務邏輯進行消費。內容:由業務代碼讀取和寫入,格式隨意,框架不做解析,只要長度不超過Kafka topic的限制即可(默認1MB)。
Kafka 並行消費模型的改進
Kafka 上的消息數據被分成 topic(主題)和 partition(分區)兩個層級,由 topic、partition 和 offset(偏移量)來唯一標識一條消息。partition 是負責保證消息順序的層次。Kafka 還支持一個消息被不同的「業務」多次消費(稱為多播或扇出),為了區分不同「業務」,引入了消費者組的概念,一個消費者組在一個 partition 上共享一個消費進度(consumergroup offset)。為保證消息送達順序,一個 partition 上的數據,同一時間、同一消費者組最多由一個消費者獲得。
這給 Kafka 的使用者造成了一些實際問題:
高估partition導致資源浪費:為了不丟消息,給定topic上的partition數量只能增加,不能減少。這要求某個topic在上線之前預估其生產能力和消費能力,然後按照生產能力的上限和消費能力的下限,敲定一個partition數量的上限來部署。上線後如果發現topic上的生產能力高於消費能力,必須先擴充partition,再提升消費能力(最直接的途徑是增加消費者數量)。相反如果發現topic上的生產能力低於消費能力(可能是消息的生產速率低於預期或者波動明顯,也可能是單個消費者的消費能力通過優化得到提升),由於partition數量無法回縮,就會造成Kafka的資源浪費。現實情況是,partition數量經常被高估,kafka topic的處理能力經常被浪費。也正是因為如此,業務開發工程師才會設計topic和partition的各種復用機制。partition不足以區分哪些消息需要串行消費,哪些可以並行:Kafka的默認的消息分區策略是通過對消息的Key欄位計算hash值,分配到特定的partition。但是某個消費者組對一個partition上的消息,有可能並不需要全部串行消費。比如某個服務認為消息A、B和C雖然都被劃分到了partition 0,但是只有A和C之間存在次序關係(比如更新的是同一條數據),B可以與A、C並行消費。如果能有一種機制,允許根據業務定義哪些消息需要串行消費,剩下的消息則可以並行消費,就能在不改變partition數量的基礎上提升消費並行度和處理能力,降低代碼對partition數量的依賴程度。針對以上兩個問題,分布式事務的並行消費部分引入了如下改進方案:在不違背 ACID 事務性的前提下,在一個消費者進程內,對 partition(分區)根據一個子分區 ID(以下簡稱 id)和 TxType 進行再次分區,同一個子分區的消息串行消費,不同子分區的消息並行消費。
如上圖所示:
消息id默認復用Kafka消息的Key欄位的值,支持產品工程師自定義消息的id,但是其區分度不能小於消息的Topic + Partition的區分度。消費者進程接到消息後,分布式事務框架會先解析消息的元數據,得到消息的TxType和id。消息會按照TxType+id進行再次分區,由框架自動分配並發送到一個內存隊列(先進先出)和處理單元,交給業務代碼進行實際消費。不同TxType+id的消息會被分配到不同的內存隊列/處理單元,處理單元之間互不阻塞,並行(或並發)執行,並行(並發)度可以調整。由於partition被再次劃分,定義在消費組和partition上的消費進度需要增加一步聚合處理,確保在Kafka發送ack的時候,給定偏移量之前的消息都已處理完畢。可以配置內存隊列/處理單元的最大長度和最大並行度,並且在空閒一段時間後會進行資源回收,避免內存堆積。落地實踐
部署細節
以代碼庫方式發布:不引入獨立的服務,將Saga和Kafka相關的邏輯抽取成公共代碼庫按版本發布,隨著位於組合編排器層的服務一起部署和升級。生產者和消費者以1:1共存於同一個進程:需要發起和管理分布式事務的服務,每個節點都會啟動一個生產者和一個消費者,並且藉助現有的集群部署工具(Amazon EKS),保證該服務的所有節點都可以互相連通,並且可以連接Kafka。這種部署方式允許我們從消費者節點直接回調生產者節點,無需引入額外的消息總線或其他數據共享機制。後續可以根據需要,將生產者和消費者部署在不同的服務上,只要它們的節點之間可以相互連通。支持Kafka和Go channel兩種隊列模式:Kafka隊列模式符合ACID的定義,Go channel隊列模式只能保證ACID中的A,不能保證I和D。開發和單元測試階段可以使用Go channel模式,服務集成測試和線上部署時一般使用Kafka模式。線上Kafka服務整體不可用時,發起分布式事務的服務可降級為Go channel模式。共享Kafka topic和partition:多個服務或流程可以共享Kafka的topic和partition,使用消費者組來進行區分消費進度,使用TxType來做事件分流。系統可用性分析
分布式系統的高可用性,需要依賴參與其中的每個服務足夠健壯。下面對分布式事務中的各種服務進行分類探討,描述當部分服務節點出現故障時系統的可用性。
生產者故障:生產者隨某個組織/編排器服務部署,節點冗餘。假如生產者所在服務的部分節點故障,對於該節點上發出隊列消息、尚未收到回調的所有事務,客戶將看到請求失敗或超時,重試導流到正常節點後可以成功提交。消費者故障:消費者和生產者一樣,隨同組織/編排器服務部署,節點冗餘。假如消費者所在的部分節點故障,對於該節點上接到隊列消息、尚未發送回調和的所有事務,客戶將看到請求超時。Kafka在配置的消費者會話超時(默認是10秒,可以按消費者定製)之後,會標記該消費者下線,然後對topic和partition進行負載調整,按一定算法儘可能平均地分配給當前消費者組剩餘的在線成員,負載調整的耗時一般在秒級。從消費者所在節點故障開始,到Kafka負載調整結束,這段時間裡發生故障的消費者負責的topic和partition上的消息都無法處理。客戶將看到部分請求出現超時錯誤。如果提交的數據和生成的隊列消息的partition有直接映射關係的話,這段時間內同一份數據重試也會失敗。基礎服務故障:給定的分布式事務會依賴多個基礎服務,每個服務獨立部署,節點冗餘。假如某基礎服務部分節點故障,分布式事務的相應請求會在相應的步驟會出現部分失敗,前序步驟依次執行補償接口。客戶看到的超時或者業務定製的失敗信息,並且重試有可能成功。業務可以引入服務熔斷機制,來避免消息堆積。消息隊列故障:Kafka本身具備主從複製、節點冗餘和數據分區來實現的高可用性,在此不做深入討論。線上問題及處理
分布式事務框架隨服務發布之後,經過一段時間的線上運行,基本符合設計預期。期間出現了一些問題,列舉如下。
生產者和消費者的連通性問題使用分布式事務的某服務在部分數據上出現超時,客戶重試無效,而在另一些數據上正常返回。通過分析日誌發現,這些消息的發送和處理都成功了,但是消費者回調生產者失敗。進一步研究日誌發現,消費者所在的節點和生產者所在的節點位於不同的集群,出現了網絡分隔。查看配置,兩個集群的同名服務配置了相同的 Kafkabrokers、topics 和消費者組,兩個集群的消費者連到同一個 Kafka,被隨機分配處理同一個 topic 下多個 partitions。
如上圖所示,位於集群 C 的服務 A(生產者)和集群 D 的服務 A(消費者)使用了相同的 Kafka 配置。他們的節點雖然都能連到 Kafka,但是彼此無法直連,因此第 7 步回調失敗了。之所以有些數據超時且重試無效,有些卻沒有問題,是因為特定數據的值會映射到特定的 partition,如果消息生產者和 partition 的消費者不在同一個集群,就會回調失敗;反之如果在同一個集群則沒有問題。解決方法是通過修改配置,讓不同集群的服務使用不同的 Kafka。
隊列消息類型不符合預期服務 A 出現業務異常報警,內容是分布式事務的消費者接到隊列消息的類型不符合預期。通過分析日誌和查看代碼,發現該消息類型屬於服務 B,而且同樣的消息已經被服務 B 的消費者處理了。查看配置發現服務 A 和 B 的分布式事務使用了同一個 Kafkatopic,通過配置不同的消費者組來區分各自的消費進度。
如上圖所示,服務 A 和 B 共享了 Kafka 的 topic 和 partition,導致異常的消息來自服務 B 的生產者(步驟 1),異常報警出現在 A 的消費者(步驟 2),而且 B 的消費者也收到並處理了這條消息(步驟 3),步驟 2 和 3 之間是並行的。服務 A 的生產者在這次異常事件中沒有發揮作用。解決這個問題有兩種思路:一種是修改配置,取消 Kafkatopic 共享;一種是修改日誌,忽略不認識的分布式事務消息類型。由於短期內在該 topic 上服務 A+B 的生產能力小於消費能力,如果取消共享的話會進一步浪費 Kafka 資源,所以暫時採用了修改日誌的方式。
服務可見性的改進分布式系統的挑戰之一就是在 RPC 調用關係複雜的時候難以追蹤和定位問題。分布式事務由於引入異步隊列,生產者和消費者有可能位於不同的節點,對服務可見性,特別是鏈路的追蹤提出了更高的要求。通過與 FreeWheel 的鏈路追蹤系統進行集成,工程師可以直觀地看到分布式事務數據在各服務的流轉情況,更好地追查和定位功能和性能上的問題,如下圖所示:
此外,還可利用 Kafka 消息多播的能力,使用臨時的消費者組隨時瀏覽、回溯 topic 上的消息數據,只要不使用線上業務的消費者組,就不會妨礙數據的正常消費。
異常細節丟失使用分布式事務的某服務發現,客戶在提交特定數據的時候穩定出現 5xx 錯誤,重試無效。經過分析日誌發現,某個基礎服務對該數據返回了 4xx 的錯誤(業務認為數據不合理),但是經過分布式事務框架的異常捕獲和處理,原始細節丟失,異常在發送給客戶前被改寫成了 5xx 的錯誤。解決辦法是修改框架的異常處理機制,在消費者進程中將每個步驟遇到的原始異常信息進行匯總,打包進回調數據發送給生產者,允許業務代碼做進一步的異常處理。
基礎服務重複創建多條數據使用分布式事務的服務 A 發現,偶爾會出現請求成功,但是在基礎服務 B 管理的資料庫裡創建出了多條同樣的數據的情況。通過 FreeWheel 的鏈路追蹤系統發現,服務 A 調用 B 的創建接口的時候因為超時而進行了重試,但是兩次調用在服務 B 都成功了,而且該接口不具有冪等性(idempotency,即多次調用的效果等於一次調用的效果),導致同樣的數據被多次創建。類似的問題在微服務實踐中經常出現,解決思路有兩種:一種是治標的方法,即 A 和 B 共享超時配置,A 將自己的超時設置 tA 傳給 B,然後 B 按照一個比 tA 更短的超時 tB(考慮到 A 和 B 之間的網絡開銷)來事務性提交數據。另一種是治本的方法,也就是服務 B 的接口實現冪等性(方法可以是資料庫設置唯一索引,創建數據請求要求必傳唯一索引,忽略索引衝突的請求)。無論是否使用分布式事務,客戶端因為網絡問題重試而導致多次請求重複數據的問題,都是每個微服務面臨的現狀,而實現接口冪等性則是可以優先考慮的方案。
下一步工作
將來會在以下幾個方面,對分布式事務方案進行持續優化:
支持局部使用跨異構資料庫的強一致性事務方案自動生成已有服務接口對應的補償接口代碼通過中間件為已有服務接口注入冪等性結語
立足 FreeWheel 核心業務系統的架構變遷和事務性需求,本文介紹了一種支持異構資料庫、實現最終一致性的分布式事務方案以及相關的落地實踐,希望能為面臨類似問題的讀者提供一些思路和啟發。
參考資料
數據密集型應用的設計: https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/Saga介紹:https://microservices.io/patterns/data/saga.htmlKafka介紹:https://kafka.apache.org/intro作者介紹:
楊帆,FreeWheel 高級軟體工程師,技術發燒友、模範消費者。研究方向為彈性架構、雲計算、數據可視化、產品設計等領域。
延伸閱讀:
一個微服務業務系統的中臺構建之路-InfoQ
Freewheel核心業務團隊混沌工程實踐之路-InfoQ
關注我並轉發此篇文章,私信我「領取資料」,即可免費獲得InfoQ價值4999元迷你書,點擊文末「了解更多」,即可移步InfoQ官網,獲取最新資訊~