iT邦幫忙

2022 iThome 鐵人賽

DAY 13
2
Software Development

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

[Day 13] 用最小負擔實作事件驅動架構

  • 分享至 

  • xImage
  •  

時機耦合是最容易被忽視的陷阱

之前我們已經聊過時機耦合可以透過事件驅動架構有效解耦,而且我們討論過三種不同的實踐。最簡單的但卻不可靠的EventEmitter可以用極小的負擔解決大部分的問題,其次,為了提升可靠度,訊息佇列被加入系統來確保所有事件至少會被執行一次。最後,透過事件朔源確保所有事件完全不會遺失。

儘管如此,在一個資源有限的組織中,訊息佇列是很昂貴的。不只是運維的成本,同時也是人力的成本,因為除了構建之外還必須要監控。訊息佇列算是數一數二昂貴的服務。

因此,在這篇文章中我們試著用最小的實作負擔來達成可靠的事件驅動架構。

系統概覽

上面的綜覽圖是系統的最後長相。從圖上可以知道,我們並沒有使用訊息佇列,儘管如此,這樣的架構還是有相當的可靠度。至少當發生問題時,總是有辦法能夠補救。

我相信,AlertManagercrontab基本上是每個組織都必備的元件。因此,唯一的差別只有將原本Component內部的功能分拆成EmitterHandler而已。

但透過巧妙的組裝,這樣的架構就相當可靠了。

系統演化

整個系統演化的過程會經過四個步驟,一步一步地提高整個系統的可靠度。

  • 盡力而為(Best effort):一開始,我們簡單的分拆功能變成EmitterHandler。具體做法在之前時機耦合的文章中提過。這是最簡單的做法,所有事件射後不理。當然,在沒問題發生的情況下,這做法還不賴。但有兩個隱憂,分別是事件遺失和發送失敗。
  • 整合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做錯誤還原。當然,這可以透過更複雜機制來確保發送事件是滿足交易性的,但實作負擔很大,因此我覺得不值得這麼做。

結論

這篇文章我們討論了許多設計取捨,也看到在一個資源受限的組織如何面對事件驅動架構。

我必須要說,事件驅動架構本身是一個高度複雜的架構,是不是真的適用於一個小型組織有待商榷。但是,這篇文章提供一個簡單的實踐方式能夠在小型系統上運行事件驅動。

此外,我們並沒有加入新的元件,完全利用現有的機制只是經過拼湊就能達成一個還算可靠的架構。

不過,要在眾多設計取捨中找出適合每個組織的實作始終不是個簡單的任務。在每一個直覺的答案背後,都有諸多考量和可能的風險。

當我在設計一個系統,尤其是分散式系統,我總是提醒自己要考慮「墨菲定律」:任何可能會出錯的事終究會出錯。

如何盡可能運用手邊的資源,無論是時間、人力、成本等達成盡可能可靠的系統?這是系統設計最有趣的地方。


上一篇
[Day 12] 在程式碼中的時機耦合
下一篇
[Day 14] 設計一個電商網站
系列文
軟體架構師的自我修養31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言