架構設計中常見的Message與Event機制

古時候的應用程式,大多是一座孤島,作業系統載入一個應用程式(例如計算薪資),當它運行完畢,結束,就退出作業系統,釋放資源,把控制權交出來。當時的應用程式架構很簡單,就…把功能通通寫在一起,完畢。

時間快轉到今年,過去就表過不提,自從有了網路(特別是網際網路)之後,應用程式的架構開始愈來愈複雜。例如,現在很多Web應用程式是前後端分離的架構,後端採用Restful API,前端運行在瀏覽器上,透過javaScript(或某種js框架),與後端Restful API互動,彼此交互以完成一個工作。

這是近代初學者,最常碰到的應用程式架構。

但當應用程式的功能愈來愈多,我們當然不可能(也不需要)在『一個』應用程式當中,一口氣去完成所有功能,我們可以把應用程式切小,然後透過應用程式和應用程式之間的合作,來完成一個比較大的任務,這時候,擔任應用程式與應用程式之間資訊傳遞橋梁的事件(event)和訊息(message)就顯得非常重要了。

事件(Event)和訊息(Message)的意義

在這一篇的討論當中,你看到的『事件』和『訊息』這兩個字眼,大多意味著某種資訊 --> 像是一組JSON資料、一個文檔、或是一個XML資訊、甚至,只是幾個bytes的字串。

總的來說,『事件和訊息』就是應用程式A傳遞給應用程式B的某種資訊,主要的用途是溝通。例如用戶在前端網頁上購物,產生了一筆訂單,這筆『訂單』就是一個訊息,Web的前台系統,會把這個訂單,傳遞給中台或後台的其他程式,來處理出貨、扣庫存、帳務處理…等動作(功能)。

你或許有一個疑問,為何不在前台網站系統用戶產生訂單的那一刻,就扣庫存和安排出貨呢? 技術上可以,但實務上常常不是這樣的。因為不是所有的系統都是一體的,企業的系統可能是分階段、分廠商開發的,甚至可能根本是外購不同廠商不同開發技術的系統拼裝起來的,所以系統和系統之間的溝通,並不一定會發生在同一個資料庫裏面。而且這麼設計,會把系統個體之間的相依性綁得太緊,導致以後牽一髮動全身。如果我們把每一個功能(購物網站、訂單、出貨、庫存、發票…等)都設計成獨立的系統,有各自獨立的資料庫(儲存體或資料表),也都可以獨立運行,那在架構設計上,將會更加彈性和靈活(其實這也導致後來逐漸成形的微服務概念)。而這樣的設計,就會導致系統和系統之間,需要透過『訊息或事件』來進行溝通。

如此說明,你應該知道事件和訊息的意義了。

我們再繼續談,一般來說,事件和訊息都是在應用程式之間傳遞的資料,但是在具體定義上兩者常常有著些許不同,讀者可從底下的表格窺知一二:

比較 事件 訊息
價值(重要性) 一般較低 一般較高
信息內容大小 一般較小 一般較大
急迫性 可暫時忽略 必須處裡
例如 廠區的溫度記錄 訂單
用途 通知 溝通

事件與訊息的處理機制

事件和訊息使用時都有各自的處理機制,在Azure上事件的處裡常常透過Event Architecture(例如Event Hub, Event Grid),訊息的處裡常常使用Services Bus或Queue。

我們先來談談Event Architecture,它基本上就跟我們寫程式的event感覺很像,就是有某些服務(系統)會產生事件,產生出的事件會被丟到一個地方暫時存放,然後會有某些服務(系統)來認領(或訂閱)特定這些事件,形成了底下這樣的架構:
圖片
(from Microsoft architecture center)

整個架構中,會有 event source(或稱event producer)產生各種事件(例如,偵測到檔案上傳、抓取當前溫度、交易金額達到100萬上限…等),這些事件(–>由程式設計師所定義的各種值得被關注的行為)一旦發生,就由程式碼產生一個event object,然後丟出去(到Event Buffer),這時候Event source的任務就結束了。

從這邊你也可以知道,Event似乎並不保證一定會被立即處理,有時候甚至可能不會被處理(而直接拋棄)。這也是我們前面之所以會說–『一般而言event價值低於message』的原因。

接著來看 Event Handler,它負責處理事件。它可以向Event Buffer來訂閱(subscription)某個事件,當這個事件發生時,Event Handler就會取得該事件(前面說過,內容可能是一組JSON,一個字串,一個文檔),然後著手處理。例如,收到了交易金額達到100萬上限,就發送一封通知信件…這樣。

這整個就是『Event Architecture』這個機制的處理方式,它被廣泛的用在許多情境與場合。

訊息(Message)的處理

當我們談到『訊息』這個字眼,一般來說,指的是比較重要(有價值)的資訊,也需要被盡快處理,像是訂單。

雖然在技術上,你可以透過API,把訂單(不管是JSON或是XML甚或是一個pdf檔案這樣的形式),從A程式傳遞到B程式,當然也可以儲存在某一個shared folder中,讓別的應用程式來存取…不過最常用於訊息傳遞的機制,還是Queue的形式。

所有開發人員都應該知道Queue,它是一種先進先出(FIFO)的存儲結構,我們可以用陣列來實作一個最最最陽春的Queue。

而Azure當中,有兩個標準的Queue機制,一個是Storage Queue(儲存體中的佇列),另一個就是Services Bus。

Storage Queue 可於 Storage 儲存體中建置:
圖片

而Services Bus則是一個獨立的服務,建立方式如下:

你會發現透過上面影片中的操作流程,建立出來的是,服務匯流排(service bus)命名空間(namespace),接著我們可以在其中建立queue,完成後,就可以透過程式碼來操作它,做各樣的應用。

services bus比起storage queue而言,有更高的可用性(Avalibility),並且支援geo-replication等高階功能。

Queue-based load leveling

Queue這個架構,本身有一個常見的應用,就是load leveling:
圖片
(from Microsoft architecture center)

Queue作為load leveling的機制,可以讓我們用比較低的成本,來處理大量地湧入的工作(前提是允許非同步)。

例如,瞬間的搶票常常造成系統崩潰,如果採用傳統一點的思維,你可能會想要把Web Server做Scale Out,把整個系統改善成HA架構,一旦前端流量多的時候,就擴展出五台十台伺服器,來同步處理大量湧入的訂單。

不過,事情可能沒你想的簡單,先不提這樣可能會讓系統建置成本高出很多,且Scale Out不一定是最正確的解決方案。因為,常常瓶頸其實並非在你的前端Web系統。很可能,其實是你後端資料庫有問題,也可能,根本不是你的問題,而是外部相依的服務,例如刷卡功能無法撐住大流量。這時候,你前端採用再怎麼強大的auto scale功能,系統網站還是掛掉。

但如果,換一個思維,改成上面的Queue-based load leveling呢?
當大量訂單湧入的時候,先把收到的訂單,丟進去 message queue 裡面去排隊,然後由後端某一個 service,24小時慢慢處理,可能白天有非常多訂單進來,但深夜新的訂單量就大幅降低,這時後端的service還是維持穩定的處理速度慢慢工作,就可以逐步消化白天大量的訂單,根本不需要 auto scale這種高檔高價格的機制。

當然,在這樣的 Queue-based load leveling 架構下,用戶送出訂單,可能得等比較長的時間才會得知結果,無法立刻得知是否已經搶到票。這樣在實體購票流程上也需要配合,例如,可能得先讓用戶輸入想要的演唱會座位區域順序(有點像是選填志願),然後由後端在處理到該訂單時,先幫用戶確認是否有該座位區域,如果確定有,再發出郵件通知,請用戶完成刷卡或匯款結帳。而非直覺的線上購票後即時刷卡結帳,整個流程上就變得比較不同步。

但你仔細想想,真實世界中的銷售行為,本來就常常是不同步的,而並非是即時進行的。因為匯款和超商繳費都要時間,用戶也都可能會棄單,所以流程上也需要有第二輪、第三輪購票這樣的規劃。

這跟很多開發人員心裡想的購票同步結帳的『即時處理流程』大不相同,但其實這很可能才是真實世界的狀態。

你如果在疫情期間,有上網預約疫苗,你會發現,是不是其實也是這樣的流程? 我們先預約,填寫意願,然後等系統判斷資格並且統計數量,一批處理完之後,再通知我們去選注射的地點,然後才進行注射,然後再進行下一批次…

這是一種以空間換取時間的做法,將即時的線上處理,調整成批次非同步處理,可以大幅降低系統的負擔,在不需要auto scale這種高端技術機制下,就可以實現負載平衡。這麼做成本極低,你需要的只是一個簡單便宜的Queue機制即可。

留言

這個網誌中的熱門文章

在POC或迷你專案中使用 LiteDB

使用Qdrant向量資料庫實作語意相似度比對

專業的價值...

使用 Airtable 在小型需求上取代傳統資料庫

讓 LINE Bot 對談機器人顯示 "Loading..." 動畫