iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Software Development

用 NestJS 闖蕩微服務!系列 第 24

[用NestJS闖蕩微服務!] DAY24 - Saga (一)

  • 分享至 

  • xImage
  •  

微服務下的事務管理

在多數商業應用下,每一個請求都可能需要對資料庫進行操作,在過去 Monolithic 的架構下,如果只針對單一資料庫做操作,在 事務(Transaction) 管理相對簡單,當系統規模越來越龐大時,可能需要處理多個資料庫之間的 Transaction 管理。在每個服務都擁有私有資料庫的微服務架構下,跨越多個服務執行一個 Transaction 成為一項巨大挑戰。

舉個例子,假如有一個購物系統,共有三個服務:

  • 訂單服務:負責處理訂單的業務。
  • 商品服務:負責處理商品的業務。
  • 物流服務:負責處理物流相關業務。

下方是建立訂單的流程圖,當 Client 向訂單服務發送建立訂單的請求時,訂單服務會通知商品服務減少可銷售的庫存,並將該訂單的狀態改為「準備中」,接著,通知物流服務建立物流單,當物流服務建立好物流單之後,訂單服務會更新該訂單的物流資訊:

Create Order Flow

假如在建立訂單的流程中,訂單成功建立了、可銷售庫存順利減少了,但物流單建立失敗了,此時對於訂單服務與商品服務來說,它們完成了自身的 Transaction,不過就以整個流程而言,資料確實產生了 不一致 的情況,這將導致使用者感到困惑,如果不處理這個問題,最終會失去客戶的信賴。

如何確保一致性?

從上方範例可以得知,這種跨服務的 資料一致性 是非常重要的,但也是相當困難的,在過去會使用 分散式事務(Distributed Transaction) 的方式來處理,二階段提交(Two Phase Commit) 就是其中一種處理手段,可以用來確保所有參與者同時完成 提交(Commit) 或是同時針對失敗進行 回復(Rollback)

什麼是 Two Phase Commit 呢?簡單來說,會有一個 協調者(Coordinator) 負責處理 Transaction,它會分兩階段進行:

  1. 在第一階段會針對所有參與方確認是否可以執行 Commit 的請求,這個階段又稱為 投票階段
  2. 當 Coordinator 收到所有參與者的回應後,假如 全數同意 就會向所有參與方發送 Commit 請求;反之,只要有一個參與方不同意,就會向所有參與方發起 撤銷(Abort) 請求。

Two Phase Commit Concept

不過 Distributed Transaction 帶來的問題很明顯,就是它會是以 同步 的方式來確保資料的一致性,雖然可以確保多個參與方之間的資料一致性,但對微服務架構而言,將會因此降低可用性,因為會花費許多資源在處理這些操作。以現在的環境來說,會更傾向使用以 非同步 為基礎的 Saga Pattern

什麼是 Saga?

Saga 是一種基於非同步處理資料一致性的機制。它捨棄了Distributed Transaction 的強一致性,改為依賴每個服務的 Transaction 生命週期來觸發非同步事件,進而執行下一個服務的 Transaction。透過一連串的非同步事件,來完成整個流程中各個服務之間的 Transaction,從而保證 最終一致性

以上方建立訂單的情境來說,以 Saga 的方式來處理就會依照下圖的順序來執行對應的 Transaction:

Create Order Saga Concept

由於 Saga 是透過非同步的方式處理各個服務間的 Transaction,它必須克服與面對下列幾個問題:

  1. 短暫不同步:根據 CAP 理論,系統只能在一致性、可用性與分區容錯性之間作權衡,Saga 的處理方式主要是 提高系統的可用性,所以勢必會 降低一致性
  2. 流程錯誤的處理:當某個服務發生問題時,將會使資料不一致,這時必須要使用 補償事務(Compensation Transaction) 來對資料進行補償。
  3. 缺乏隔離性:Saga 並不滿足 ACID 特性,它只具有 ACD,所以服務應使用 對策(Countermeasure) 來降低因缺乏隔離性導致的異常。

Compensation Transaction

由於 Saga 是一連串獨立的 Transaction 操作,當過程中有服務發生錯誤,前面已經完成的服務是無法 Abort 的,所以在實作 Saga 時,需要識別哪些 Transaction 會涉及資料的改變,並針對這些 Transaction 去設計 Compensation Transaction,當執行該 Compensation Transaction 時,服務必須對對應的 Transaction 進行一定程度的校正,比如:本來建立好的訂單,因為商品服務故障導致可銷售庫存沒有減少,這時訂單建立的事實已經發生,所以應該用拒絕訂單的方式進行 沖銷

Create Order Compensation Transaction

我們來更進一步來探討 Compensation Transaction 的運作,假如正在執行一個 Saga 的流程,執行順序為 T1 到 Tn,當執行到第 n + 1 個 Transaction 時發生錯誤,此時應該要針對前面 n 個 Transaction 進行補償,Saga 的流程必須以 相反的順序 開始執行 Compensation Transaction,也就是 Cn 到 C1,如下圖所示:

Compensation Transaction Concept

Countermeasure

由於 Saga 並不滿足 隔離性(Isolation),這表示在 Saga 執行的過程中,有可能遇到以下問題:

  • 髒讀(Dirty Reads):一個 Saga 或 Transaction 讀取另一個未完成 Saga 所做的更新。
  • 遺失更新(Lost Updates):一個 Saga 覆蓋了另一個 Saga 所做的更新。

為了解決上述問題,需要使用一些 Countermeasure 的技巧來防止問題產生,但在講解 Countermeasure 技巧之前,需要先了解 Saga 流程中各個服務的 Transaction 類型,這對於 Countermeasure 會很有幫助:

  1. 可補償的事務(Compensatable Transactions):可以使用 Compensation Transaction 校正的 Transaction。
  2. 關鍵事務(Pivot Transaction):Saga 流程中最關鍵的 Transaction,當它成功時,Saga 會一路執行到完成。
  3. 可重複事務(Retriable Transaction):在 Pivot Transaction 後的 Transaction,必須保證成功。

以上方建立訂單的流程來說,可以識別出各個 Transaction 的類型:

  1. 建立訂單:為 Compensatable Transaction。當可銷售庫存減少失敗時,應該透過 Compensation Transaction 拒絕訂單。
  2. 減少可銷售庫存:為 Compensatable Transaction。當更新訂單狀態時失敗,應該透過Compensation Transaction 將可銷售庫存補回。
  3. 更新訂單狀態:為 Compensatable Transaction。當物流單建立失敗時,應該透過 Compensation Transaction 將訂單狀態恢復。
  4. 建立物流單:為 Pivot Transaction。該 Transaction 成功時,後續的步驟將不再需要 Compensation Transaction。
  5. 更新訂單資訊:為 Retriable Transaction。由於物流單已經建立,此 Transaction 必須保證成功,縱使失敗了也應該要重試直到成功。

Saga Structure

了解三種類型的 Transaction 後,就可以來了解幾個常見的 Countermeasure。

語意鎖(Semantic Lock)

Saga 流程中的 Compensatable Transaction 會在建立或更新資料時以某種標誌來鎖定資源,目的是防止其他 Transaction 讀取該資料或提醒該 Transaction 要更加謹慎處理。通常這個標誌會由 Retriable TransactionCompensation Transaction 修改、清除:

  • 由 Retriable Transaction 修改或清除時,表示 Saga 已通過 Pivot Transaction,會保證 Saga 完成。
  • 由 Compensation Transaction 修改或清除時,表示 Saga 發生了補償。

以上方建立訂單的範例來說,可以在訂單中加上 _lock 欄位:

  • 建立訂單時,_lock 的值為 APPROVAL_PENDING
  • 當物流單建立完畢後,要更新訂單資訊時,會將 _lock 的值改為 APPROVED
  • 如果在建立物流單時失敗了,那麼在執行 Compensation Transaction 時,會將 _lock 的值改為 REJECTED

不過語意鎖本身只是靠特定字串來標誌狀態,還是需要針對這些標誌的特徵值來做出防備。以上方範例來說,在 _lockAPPROVAL_PENDING 時,如果遇到「取消訂單」的 Saga 時,應該要拒絕或阻塞該操作。

交換式更新(Commutative Updates)

將更新的操作設計成不計較執行順序的,也就是每個操作都是可交換的,這樣的方式可以避免 Lost Updates。

以上方建立訂單的範例來說,會針對可銷售庫存做增減,一般狀況下會以「庫存減 1」的方式減少可銷售庫存;反之,當更新訂單狀態失敗時,會以「庫存加 1」的方式補回可銷售庫存。

悲觀檢視(Pessimistic View)

以最悲觀的角度來考慮 Saga 執行的順序,來降低 Dirty Reads 帶來的風險,不過須特別注意,它的預防並非 100%。

重新讀值(Reread Value)

讀取資料時,可能會拿到過時的值,所以 Reread Value 的做法相當簡單暴力,在執行更新之前,再次讀取資料確保資料有沒有異動過,如果沒有異動就執行更新;反之,要決定是否中止操作。舉例來說,在付款流程中,如果我們讀取到的訂單狀態為「待付款」,但後續操作該訂單狀態變更為「已取消」,此時需要重新讀取訂單狀態,確認其當前狀態是否允許進行扣款操作。

小結

在 Monolithic 架構下,管理 Transaction 是相對容易的,但在微服務架構下,要實現跨服務的 Transaction 是相當複雜的。在過去,會使用 Two Phase Commit 的技巧來保證資料的強一致性,但這種同步機制效能不佳,在可用性方面表現較差,在需要高效運作的微服務架構下並不一定是好的選擇。為了解決資料一致性與效能問題,而產生了基於非同步的 Saga Pattern,它透過一連串非同步事件將各個獨立的 Transaction 串起來,雖然犧牲了強一致性,但依然可以保證資料最終一致,且提升了系統可用性。由於 Saga 是多個 Transaction 所組合起來的流程,勢必要針對中途失敗的情況做處理,所以需要為 Compensatable Transactions 設計對應的 Compensation Transaction 來校正資料,另外,Saga 還需克服缺少 Isolation 所帶來的問題,包含:Dirty Reads、Lost Updates,所以需要使用 Countermeasure 來避免異常發生。

由於 Saga 是一個較複雜的議題,本篇只針對其概念作說明,下一篇將會介紹要如何實作 Saga,敬請期待!


上一篇
[用NestJS闖蕩微服務!] DAY23 - Circuit Breaker
下一篇
[用NestJS闖蕩微服務!] DAY25 - Saga (二)
系列文
用 NestJS 闖蕩微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言