iT邦幫忙

2022 iThome 鐵人賽

DAY 7
0
Software Development

軟體架構師的自我修養系列 第 7

[Day 7] 分散式交易設計實戰

  • 分享至 

  • xImage
  •  

我們在之前聊過如何準備設計審閱也介紹了分散式交易,那麼讓我們把這兩者結合在一起。讓我來示範一下,如何做一個分散式交易的設計審閱。

為了避免大家忘記,我簡單列一下要點:

  1. C4模型
  2. 使用者故事和使用案例
  3. 設計決策

因此我會按照一個真正的設計審閱流程設計一個分散式交易功能,但太細節的內容會省略。

使用者故事

首先,我們來定義這次的使用者故事。

這次我們要實作的新功能是:送禮物。整個故事如下:

  • 一個使用者可以決定一次要送多少禮物出去。
  • 只要使用者的點數足夠,那麼送禮的數量無上限。
  • 當送禮完成,必須要通知送禮者和所有收禮者對應的結果。

使用案例

整個送禮的使用情境已經被清楚描述了,但還有些細節是沒被定義的。例如:

  • 如果使用者的餘額不足,那所有禮物都無法發送,不允許部分成功。
  • 送禮完成表示的是所有收禮者都收到禮物了。
  • 發出去的通知要包含送禮者和總共送了多少份。
  • 無論要送多少人,完成時間必須要在幾分鐘內。

正如我們上面看到的,使用案例是那些在使用者故事中沒被定義清楚的細節。

C4模型

現在我們可以開始畫整個設計的C4模型了。

Context

根據使用者故事,我們可以畫出對應的上下文並且描述使用者和系統的互動。根據上下文,我們知道有幾個重點必須要討論清楚:

  1. 包含gift的線
  2. Server的行為
  3. 包含notify的線

也許你會問,那notification servicereceivers不用討論嗎?

答案很簡單,那些是第三方服務,我們無法控制他的行為。因此在這場設計審閱中不需要討論。

當然,如果對那些第三方服務有疑慮,那可以再舉行另一場設計審閱來討論通知系統,但那不在這場審閱的範疇。

Container

一但我們有了上下文,我們就可以更深入那三個重點。根據C4模型,我們將其用container的方式展開。

最終我們會得到這結果。

這裡有很多設計決策,但在這時候,我們只需要清楚描述架構即可,設計決策會留到之後一併討論。

  1. 使用者發出送禮請求,包含要送什麼和送給誰。
  2. Server收到請求後,首先先將使用者的點數扣掉。如果餘額不足就直接回nak拒絕請求。如果餘額足夠,就將送禮請求分成數批並存入資料庫,接著將批次任務非同步的送給執行者並帶上送禮的交易序號。
  3. 雖然送禮還沒完成,但準備工作已經結束,所以Server回覆ack表示工作正在進行。
  4. 當處理者收到任務指令,他就專注將被分配的任務做完,並將資料庫內儲存的計數器扣減。若是處理者碰到錯誤,那他要負責重試到好。
  5. 當扣減完計數器後,若處理者發現為0,那就負責把通知送給所有人。

Component和Code

在C4模型還有兩個項目,分別是Component和Code,但這太細節了,會讓這篇文章失焦因此跳過。

設計決策

我們可以從C4模型中發現很多設計決策,就像之前我提過的公式:「為什麼用A而不是B」。現在讓我們來一一檢視。

  1. 為什麼送禮者和收禮者要用半同步的方式溝通?(介於同步和非同步之間)
  2. 為什麼Server分成編排(Orchestration)和處理者?
  3. 為什麼編排和處理者之間是非同步?
  4. 為什麼由最後一個處理者送通知給全部人?
  5. 也許你還可以問出很多沒列出來的問題。

讓我們從頭開始說起。

其實系統本來就已經具備一對一送禮的功能,因此最簡單的方法是跑一個迴圈對所有收禮者一對一送。

當收禮者數量少時,這不會有問題,可是一但收禮者的數量增加,對效能是一個嚴重挑戰。根據計算,送一個禮物需要100-200毫秒,也就是說,當送給十個人,就會達到秒級了。這完全無法接受。

看起來批次作業無可避免,因此我們稍微改造一下。

從圖上可以發現,編排已經出現。但是,所有跟處理者的溝通是以同步的方式進行,且通知要等所有送禮結束由編排發出。

看起來好像能解決效能瓶頸了,對嗎?其實這沒有比較好。

讓我們算個簡單的數學。假設我們要送給一千個人,那我們該怎麼設定批次的大小和數量?

為了在秒級內完成,批次最大只能10個,且100個處理者要同時叫起來處理任務。這是一個嚴苛的挑戰,要能夠瞬間產生100個處理者不是件容易的事。

所以同步看起來行不通,那如果非同步呢?

在第二次嘗試中,我們將編排改成編舞。

因此送禮者可以快速得到回應且送禮過程可以保持流暢。但,真的是這樣?

如果中間的處理者故障會發生什麼事?整條鏈就斷了。有些收禮者因為沒收到通知所以沒有感受,但對送禮者來說,送到一半等於點數已經被扣了部分,卻沒收到通知。更糟的是,回到一開始的使用案例,部分成功不被接受。

編舞相較於編排會有更好的效能和更好的擴充性,但也具有更複雜的流程控制。因此,在這個案例下編排會更加適合。

所以,讓我們來實作一個非同步的編排。

這個架構馬上會遇到兩個問題:

  1. 誰要送通知?
  2. 如果送到一半餘額不足怎麼辦?

送通知的問題還容易解決,就像C4模型中描述的,最後一個處理者送就好。儘管如此,送到一半餘額不足在非同步下基本無解。

綜上所述,最後我們採用半同步的做法。

首先,編排先判斷餘額是否足夠,為了避免競爭,如果足夠就直接將點數扣除。因此,處理者只需要負責送禮,不需要處理點數問題,甚至根本不用確認餘額。

錯誤處理和災難復原

在這樣的架構下,會有一個大麻煩。

怎麼解決處理者失敗?

如果是因為資料庫阻塞造成的,也許重試幾次就好。若是功能錯誤之類無法重試的錯誤,該怎麼復原?

就結果來說,需要一個額外的監控機制,定時確認那些非同步任務的狀態,並且重試那些可以修復的問題,若是無法修復就必須通知人類介入處理。

在之後的文章中會介紹事件驅動架構下常見的設計模式。

結論

總結一下,這個送禮被拆分出幾個核心重點:

  1. 送禮者同步發出請求,並且預先扣除點數。
  2. 所有送禮任務是非同步執行的。
  3. 送禮結果是最終一致性,送出的總額會與扣掉的點數相等。

在這篇文章中我們用實際例子討論了設計分散式交易的挑戰,並探討不同的決策:

  • 同步 vs. 非同步
  • 編排 vs. 編舞
  • 原子性 vs. 最終一致性

這些都是典型分散式交易的取捨,且這些面向大大主宰整個系統設計。


上一篇
[Day 6] 微服務架構的九大挑戰
下一篇
[Day 8] 從單體演化成CQRS實戰
系列文
軟體架構師的自我修養31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言