ACID

設計微服務架構須絕對可靠 ACID原則兼顧資料庫效能

微服務交易資料務求一致 編排機制演進迎刃而解(上)

2023-08-21
在殘酷的市場競爭中,為了贏得市場、獲取利潤,企業必須建立一種快速反應市場變化、降低生產成本、提高生產效率的開發方法和系統設計機制,而微服務便是基於這樣的實際案例而演變出的系統架構。

隨著企業隨著業務規模擴大,資訊系統也從公司內部整合延伸到使用外部合作夥伴服務,與雲端SaaS服務API整合,其中複雜的業務流程更因為跨越多個系統和多個內外部組織,增加程式實作上的困難。如圖1所示的購物結帳流程,就涉及到庫存、物流、付款和客戶管理等服務。在殘酷的市場競爭中,為了贏得市場、獲取利潤,企業必須建立一種快速反應市場變化、降低生產成本、提高生產效率的開發方法和系統設計機制,而微服務便是基於這樣的實際案例而演變出的系統架構。

圖1  電商購物結帳流程。

在大型企業的服務導向架構(Service-oriented Architecture,SOA)通常是透過企業服務總線(Enterprise Service Bus,ESB)來負責編排的,或是採用基於異步消息傳送的編排方法,不幸的是,傳統的ESB因導入成本高和使用複雜,逐漸在現代化架構中棄用,且當微服務架構出現後,人們很快發現使用單一ESB來編排多組微服務並不是完美的解決方案,ESB故障將導致整個企業系統停擺,這與強調低耦合高可用的微服務設計恰好背道而馳。

但是,「理想很豐滿,現實很骨感」,原以為微服務會像管弦演奏一般和諧一致,但現實就像混亂失控的胡士托音樂節(圖2),開發團隊疲於應付服務拆解後的多個專案部署上線流程、分散式系統衍生出的流程協作、服務與服務之間的有效通訊設計等。正因為隨著微服務架構的導入,衍生出這些非業務相關的開發問題,因此最早提出微服務的Thoughtworks Martin Fowler就建議單體優先(https://martinfowler.com/bliki/MonolithFirst.html),直到開發人數眾多,單一版本庫經常出現合併衝突,開發人員相互牽制拖慢生產效率,或單一服務出現效能瓶頸時,再考慮重構成微服務架構。

圖2  理想中的微服務 vs 現實中的微服務。

當然,一開始可以先以單體方式部署,不過專案結構則採用Hexagonal Architecture或Clean Architecture分層設計,把業務領域或實體層(Entities)獨立拆分,實作和介面分成獨立物件,即便未來需要轉變成微服務,也無須改寫程式碼。可是,即便專案結構先採分層獨立階層設計,但寫入資料庫仍舊遇到多個服務要同時寫入資料庫的業務規則一致性要求,傳統單體系統大多呼叫事先規範好的SQL Stored Procedure來控制多個服務的一致性,當正確時則Commit,錯誤時則Rollback的資料庫交易型一致性方式。這方式衍伸出兩大問題:

1. Stored Procedure維護問題:Stored Procedure與資料庫高度耦合,當資料庫因授權成本或效能問題需要更換成其他資料庫方案時,就會造成大量技術債,沒人理解複雜的Stored Procedure,深怕一點異動會影響業務交易正確性。

2. 資料庫效能問題:當業務成長和越來越多系統共用這個單一資料庫,為了完成一致性寫入和資料正確性的業務需求,資料庫存取量和資料列鎖定相互影響,造成業務系統運作緩慢,進而影響使用者體驗和客戶交易,卻無法根本解決這單一資料庫效能問題。

所以,當關鍵的服務一致性問題沒有徹底解決,使用單體或是微服務都會遇到資料庫維護和效能問題,當然微服務就有Database-per-Service Pattern的設計建議,可是若沒有類似ACID的交易控制機制,則必須額外設計回溯沖正的修正服務,這便是Saga設計模式,以下將詳細描述這些控制機制,而微服務編排(Orchestrator)又如何協助解決這一致性問題。

傳統式交易

通常在單體使用交易(Transaction)來確保一個或多個狀態變更是否成功完成,其中可能包括新增、修改或刪除資料,在關聯式資料庫中,通常涉及到單一交易內更新多個資料表。而控制範圍限制在資料庫的資料表、資料列和欄位內,使用此控制機制防止其他人或其他程式在仍未處理完成的交易活動中修改其處理中的相關資料,交易概念建立在控制範圍之上,定義出具備原子性、一致性、隔離性和持久性四種屬性,這些屬性確保交易是全部完成,否則就全部皆不完成,且結果是不會因系統故障而遺失,並且資料庫維持一致的狀態,ACID屬性詳細說明如下:

‧原子性(Atomicity):確保交易中的作業全部完成,不然就是全部都未完成;若某些原因導致交易失敗,則整個作業就會中止並回溯,恢復到之前沒有進行任何更改的狀態。

‧一致性(Consistency):當應用系統觸發資料庫交易,其中應用系統的狀態必須與資料庫資料狀態是一致的,符合業務邏輯設計的正確性,並且保證資料庫維持在邏輯正確的運行狀態。

‧隔離性(Isolation):為了確保資料完整性並防止數據損壞,交易必須對其他交易隱藏在執行過程中的異動情況,這是透過維持交易隔離來實現,並允許多個交易同時進行,彼此互不干擾。

‧持久性(Durability):為了確保交易工作的正確性並允許在系統出現故障時可完整恢復資料,且確保一旦交易完成,可保證在發生系統故障時已完成的交易資料完全不會消失。

圖3顯示了轉帳交易的常見範例,假設同一銀行資料庫中有兩個帳戶Balance_A和Balance_B,透過應用程式或Stored Procedure將1,000元從Balance_A移轉到Balance_B,這過程就稱為交易,在實作上會使用BEGIN或START TRANSACTION等DBMS操作指令啟動交易,轉帳Stored Procedure範例見下方,如果這交易過程沒有檢測到錯誤,就執行COMMIT來完成交易,若在交易過程中檢測到錯誤,則會執行ROLLBACK並結束,撤銷借貸方的金額異動步驟,資料庫DBMS環境會確保隔離性和原子性,透過BEGIN和END來標註交易運行區塊,至今仍舊是資料庫交易的最佳實踐方式,此傳統式交易循序圖如圖4所示。

圖3  銀行轉帳範例示意圖。
圖4  銀行轉帳範例循序圖。

銀行轉帳Stored Procedure程式碼如下:

DELIMITER //  

CREATE PROCEDURE TransferFunds(IN accountA VARCHAR(10), IN accountB VARCHAR(10), IN amount DECIMAL(10, 2)) BEGIN   DECLARE balanceA DECIMAL(10, 2);   DECLARE balanceB DECIMAL(10, 2);    

-- 檢查帳號A是否有足夠的餘額進行轉帳   SELECT balance INTO balanceA FROM accounts WHERE account_number =accountA;     IF balanceA >= amount THEN      

-- 開始事務      START TRANSACTION;        

-- 更新帳號A的餘額      UPDATE accounts SET balance = balance - amount WHERE account_ number = accountA;        

-- 更新帳號B的餘額      UPDATE accounts SET balance = balance + amount WHERE account_ number = accountB;        

-- 完成交易      COMMIT;        SELECT 'Transfer successful' AS message;   ELSE      SELECT 'Insufficient balance'  AS message;   END IF; END //   DELIMITER ;

分散式交易

企業內大型業務系統有時會使用到多個資料庫,或是微服務業務邏輯涉及到多個資料庫,就演變成採用分散式交易來控制多個資料庫以便達到ACID一致性要求,其中兩階段提交協議(Two-Phase Commit)是解決此問題會採用的解決方案之一,或是透過數據抄寫機制針對特定服務的資料庫異動資料,同步到另一個服務的資料庫,並且要保證exactly-once抄寫,又延伸出整合MQ/Kafka的Outbox設計模式(https://microservices.io/patterns/data/transactional-outbox.html)。

圖5則表示使用分散式交易進行跨行轉帳的實際示例。假設Balance_A屬於Bank_A的帳戶,帳戶存款資料儲存在Database_A中,由資料庫管理系統DBMS_A管理,而Balance_B屬於Bank_B的帳戶,其存款資料儲存在Database_B中,並且由DBMS_B管理,使用分散式交易來執行跨行轉帳,當中涉及到Bank_A提供的借方鎖定功能和Bank_B提供的貸方匯入功能,借方服務會扣除存款指定金額,而貸方服務會增加存款指定金額。

圖5  跨行轉帳範例示意圖。

但這並無法用同一套資料庫DBMS來控制,除了各家銀行資料庫不盡相同,金融業也不允許直接存取資料庫,因此都是透過轉帳服務來控制,並遵照財金公司的轉帳流程和電文來實作,這就是最初實現分散式交易的應用情境。

兩階段提交協議(Two-Phase Commit,2PC)是一種在分散式系統中確保所有參與者都同意某項操作的協議,它主要分為兩個階段:投票階段和提交階段:

1. 投票階段:在此階段,Coordinator協調者向所有參與交易的Worker發送一個提議,詢問是否可以允許某項操作。Worker將根據它們的狀態(例如它們是否有足夠的資源來完成操作),並決定它們的投票結果,如圖6所示。

圖6  2PC的投票階段。 (資料來源:O'Reilly Building Microservices, 2nd Edition)

2. 提交階段:接下來,Coordinator協調者根據投票結果來決定是否交付操作。如果所有Worker都同意交付,協調者將向所有Worker發送一個Commit訊息,通知它們完成操作,如圖7所示,如果有任何Worker投票反對交付,協調者將向所有Worker發送一個Abort消息,告訴它們中止操作。

圖7  2PC的提交階段。 (資料來源:O'Reilly Building Microservices, 2nd Edition)

這個協議的重要功能是,一旦協調者收到所有Worker回覆,協調者就進入第二階段(提交階段)的處理,如果有一個Worker無法完成操作,協調者(交易管理器)就通知所有Worker回溯資料,這保證交易的原子性,此2PC分散式交易循序圖請見圖8,此2PC的優點如下:

圖8  2PC分散式交易循序圖。

‧2PC保證交易的原子性,確保所有服務的資料都完成異動,不然就是所有服務沒有任何資料異動。

‧確保其他操作的隔離性,在交易協調器交付變更之前,對資料的異動是無法存取。

‧此運作方法是同步式呼叫,客戶端將收到成功或失敗的通知。

反之,2PC的缺點是:

‧高延遲性,2PC交易與單一服務上的交易速度對比是相當緩慢,高度依賴交易協調器,這在高負載情況會影響系統回應時間。

‧交易鎖可能成為效能瓶頸,並且很有可能出現Deadlock死鎖,兩個交易相互等待並鎖住。

分散交易協調者或交易管理器有多種開源實作框架,例如Seata(https://seata.io/)、Atomicos(https://www.atomikos.com/)或Narayana(https://www.narayana.io/)。然而,2PC也有其缺點,如果在提交階段,協調者發生故障,參與者Worker可能會永遠處於在等待訊息的狀態,這種情況稱為「Blocking(阻塞)」,為了解決這樣問題,就有改良的協議稱為Three-Phase Commit(3PC),添加一個額外的階段來避免阻塞。

待續

介紹過了微服務實際執行時可能遭遇的狀況後,下集文章將提出應對解決的辦法,說明何謂微服務編排技術,並介紹新式的BPM流程引擎。

<本文作者:鄭淳尹,Docker.Taipei社群共同發起人,台北富邦銀行雲端系統部架構師,曾任微軟MVP、國泰金控技術架構師、momo購物網架構師、臺北榮民總醫院資訊工程師、玉山銀行資訊處專員、宏碁eDC維運工程師。開源技術愛好者,曾在多間大學資工系擔任Docker容器技術講師,並翻譯審閱多本容器技術書籍。>


追蹤我們Featrue us

本站使用cookie及相關技術分析來改善使用者體驗。瞭解更多

我知道了!