時機耦合是最容易被忽視的陷阱
之前我們已經聊過時機耦合可以透過事件驅動架構有效解耦,而且我們討論過三種不同的實踐。最簡單的但卻不可靠的EventEmitter
可以用極小的負擔解決大部分的問題,其次,為了提升可靠度,訊息佇列被加入系統來確保所有事件至少會被執行一次。最後,透過事件朔源確保所有事件完全不會遺失。
儘管如此,在一個資源有限的組織中,訊息佇列是很昂貴的。不只是運維的成本,同時也是人力的成本,因為除了構建之外還必須要監控。訊息佇列算是數一數二昂貴的服務。
因此,在這篇文章中我們試著用最小的實作負擔來達成可靠的事件驅動架構。
上面的綜覽圖是系統的最後長相。從圖上可以知道,我們並沒有使用訊息佇列,儘管如此,這樣的架構還是有相當的可靠度。至少當發生問題時,總是有辦法能夠補救。
我相信,AlertManager
和crontab
基本上是每個組織都必備的元件。因此,唯一的差別只有將原本Component
內部的功能分拆成Emitter
和Handler
而已。
但透過巧妙的組裝,這樣的架構就相當可靠了。
整個系統演化的過程會經過四個步驟,一步一步地提高整個系統的可靠度。
Emitter
和Handler
。具體做法在之前時機耦合的文章中提過。這是最簡單的做法,所有事件射後不理。當然,在沒問題發生的情況下,這做法還不賴。但有兩個隱憂,分別是事件遺失和發送失敗。AlertManager
:接著,我們在問題發生時加入警報機制。將災後復原所必須要用到的資料寫進Elastic Search
並呈現在Kibana
上,那麼值日生就可以在收到Slack
通知後採取對應的動作。換句話說,我們透過「人為介入」解決事件遺失和發送失敗。{
eventName: "purchased",
createAt: "2022/01/01 1:11:11",
expected: ["giveCoupon", "lottery"],
status: 0, // 0: emitted, 1: timeout, 2: processed
done: [],
args: ["user A", 5000]
}
crontab
並實作冪等性:在這時間點,整個架構就成形了,就是之前綜覽圖的長相。上面所有的步驟都有個致命缺陷,那就是任何錯誤都得要人為介入來修復。雖然這樣的確可以確保所有問題都被修復,但平均復原時間(MTTR)會非常長,因此,復原機制可以進一步強化。透過之前事件驅動架構設計模式提到的監控者模式,我們加入crontab
定期確認資料庫內的事件,並試著重試發生的錯誤。有兩個重點必須要注意。首先,所有事件的處理必須是冪等性,這點非常重要。因為所有事件都至少會被執行一次,而不是僅有一次。其次,就算有了冪等性,還是必須要設定重試上限,當超過重試上限,還是得要「人為介入」來查看問題和進行後續處理。
事實上,上述架構有很多值得討論的地方。
Emitter
寫資料庫時,就必須要知道誰是處理者。也就是說,事件生產者和消費者耦合了。但是,我認為這樣的耦合可以接受。只要寫程式透過適當的手段就可以讓這種耦合被視為全域設定,而不僅僅是耦合。crontab
。在處理者重試或crontab
各有優點,在處理者重試可以儘早復原錯誤。但是有時候,錯誤造成的原因是資料庫雍塞,當下重試只是進一步加重資料庫的負擔。crontab
做錯誤還原。當然,這可以透過更複雜機制來確保發送事件是滿足交易性的,但實作負擔很大,因此我覺得不值得這麼做。這篇文章我們討論了許多設計取捨,也看到在一個資源受限的組織如何面對事件驅動架構。
我必須要說,事件驅動架構本身是一個高度複雜的架構,是不是真的適用於一個小型組織有待商榷。但是,這篇文章提供一個簡單的實踐方式能夠在小型系統上運行事件驅動。
此外,我們並沒有加入新的元件,完全利用現有的機制只是經過拼湊就能達成一個還算可靠的架構。
不過,要在眾多設計取捨中找出適合每個組織的實作始終不是個簡單的任務。在每一個直覺的答案背後,都有諸多考量和可能的風險。
當我在設計一個系統,尤其是分散式系統,我總是提醒自己要考慮「墨菲定律」:任何可能會出錯的事終究會出錯。
如何盡可能運用手邊的資源,無論是時間、人力、成本等達成盡可能可靠的系統?這是系統設計最有趣的地方。