--- tags: 鐵人賽 --- # Day 7 我們在之前聊過如何準備設計審閱也介紹了分散式交易,那麼讓我們把這兩者結合在一起。讓我來示範一下,如何做一個分散式交易的設計審閱。 為了避免大家忘記,我簡單列一下要點: 1. C4模型 2. 使用者故事和使用案例 3. 設計決策 因此我會按照一個真正的設計審閱流程設計一個分散式交易功能,但太細節的內容會省略。 ## 使用者故事 首先,我們來定義這次的使用者故事。 這次我們要實作的新功能是:送禮物。整個故事如下: - 一個使用者可以決定一次要送多少禮物出去。 - 只要使用者的點數足夠,那麼送禮的數量無上限。 - 當送禮完成,必須要通知送禮者和所有收禮者對應的結果。 ## 使用案例 整個送禮的使用情境已經被清楚描述了,但還有些細節是沒被定義的。例如: - 如果使用者的餘額不足,那所有禮物都無法發送,不允許部分成功。 - 送禮完成表示的是所有收禮者都收到禮物了。 - 發出去的通知要包含送禮者和總共送了多少份。 - 無論要送多少人,完成時間必須要在幾分鐘內。 正如我們上面看到的,使用案例是那些在使用者故事中沒被定義清楚的細節。 ## C4模型 現在我們可以開始畫整個設計的C4模型了。 ### Context ```mermaid graph TD sender --gift-->Server--notify-->notif-->recv1 & recv2 & recv3 sender((Sender)) notif[Notification Service] recv1((Recv 1)) recv2((Recv 2)) recv3((Recv 3)) ``` 根據使用者故事,我們可以畫出對應的上下文並且描述使用者和系統的互動。根據上下文,我們知道有幾個重點必須要討論清楚: 1. 包含`gift`的線 2. `Server`的行為 3. 包含`notify`的線 也許你會問,那`notification service`和`receivers`不用討論嗎? 答案很簡單,那些是第三方服務,我們無法控制他的行為。因此在這場設計審閱中不需要討論。 當然,如果對那些第三方服務有疑慮,那可以再舉行另一場設計審閱來討論通知系統,但那不在這場審閱的範疇。 ### Container 一但我們有了上下文,我們就可以更深入那三個重點。根據C4模型,我們將其用container的方式展開。 ```mermaid graph LR sender--1. gift-->orch--3. ack or nak-->sender orch -.tx id, meta.-> w1 & w2 & w3 orch --2. proc & tx id, counter--> DB w1 & w2 & w3 --4. proc & decr--> DB w3 --5. all batches done--> notif DB[(DB)] sender((Sender)) orch[Orchestration] notif[Notification Service] w1[Worker] w2[Worker] w3[Worker] ``` 最終我們會得到這結果。 這裡有很多設計決策,但在這時候,我們只需要清楚描述架構即可,設計決策會留到之後一併討論。 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毫秒,也就是說,當送給十個人,就會達到秒級了。這完全無法接受。 看起來批次作業無可避免,因此我們稍微改造一下。 ```mermaid graph LR sender--1-->orch--2-->w1 & w2 & w3 orch--3-->notif orch--4-->sender sender((Sender)) orch[Orchestration] notif[Notification Service] w1[Worker] w2[Worker] w3[Worker] ``` 從圖上可以發現,編排已經出現。但是,所有跟處理者的溝通是以同步的方式進行,且通知要等所有送禮結束由編排發出。 看起來好像能解決效能瓶頸了,對嗎?其實這沒有比較好。 讓我們算個簡單的數學。假設我們要送給一千個人,那我們該怎麼設定批次的大小和數量? 為了在秒級內完成,批次最大只能10個,且100個處理者要同時叫起來處理任務。這是一個嚴苛的挑戰,要能夠瞬間產生100個處理者不是件容易的事。 所以同步看起來行不通,那如果非同步呢? ```mermaid graph LR sender--1-->chore-.3.->w1-.4.->w2-.5.->w3-.6.->notif chore--2-->sender sender((Sender)) chore[Choreography] notif[Notification Service] w1[Worker] w2[Worker] w3[Worker] ``` 在第二次嘗試中,我們將編排改成編舞。 因此送禮者可以快速得到回應且送禮過程可以保持流暢。但,真的是這樣? 如果中間的處理者故障會發生什麼事?整條鏈就斷了。有些收禮者因為沒收到通知所以沒有感受,但對送禮者來說,送到一半等於點數已經被扣了部分,卻沒收到通知。更糟的是,回到一開始的使用案例,部分成功不被接受。 編舞相較於編排會有更好的效能和更好的擴充性,但也具有更複雜的流程控制。因此,在這個案例下編排會更加適合。 所以,讓我們來實作一個非同步的編排。 ```mermaid graph LR sender--1-->orch-.3.->w1 & w2 & w3 orch--2-->sender sender((Sender)) orch[Orchestration] w1[Worker] w2[Worker] w3[Worker] ``` 這個架構馬上會遇到兩個問題: 1. 誰要送通知? 2. 如果送到一半餘額不足怎麼辦? 送通知的問題還容易解決,就像C4模型中描述的,最後一個處理者送就好。儘管如此,送到一半餘額不足在非同步下基本無解。 綜上所述,最後我們採用半同步的做法。 首先,編排先判斷餘額是否足夠,為了避免競爭,如果足夠就直接將點數扣除。因此,處理者只需要負責送禮,不需要處理點數問題,甚至根本不用確認餘額。 ## 錯誤處理和災難復原 在這樣的架構下,會有一個大麻煩。 > 怎麼解決處理者失敗? 如果是因為資料庫阻塞造成的,也可重試幾次就好。若是功能錯誤之類無法重試的錯誤,該怎麼復原? 就結果來說,需要一個額外的監控機制,定時確認那些非同步任務的狀態,並且重試那些可以修復的問題,若是無法修復就必須通知人類介入處理。 在之後的文章中會介紹事件驅動架構下常見的設計模式。 ## 結論 總結一下,這個送禮被拆分出幾個核心重點: 1. 送禮者同步發出請求,並且預先扣除點數。 2. 所有送禮任務是非同步執行的。 3. 送禮結果是最終一致性,送出的總額會與扣掉的點數相等。 在這篇文章中我們用實際例子討論了設計分散式架構的挑戰,並探討不同的決策: - 同步 vs. 非同步 - 編排 vs. 編舞 - 原子性 vs. 最終一致性 這些都是典型分散式交易的取捨,且這些面向大大主宰整個系統設計。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up