脈絡:Pipes and Filters 鏈中的多個處理步驟由 Message Channel 連接。
固定管道為什麼不夠#
Pipes and Filters 風格用固定管道把過濾器直接接在一起。對許多 Pipes and Filters 的應用(例如 [POSA])而言這很合理,因為它們處理的是一大批資料項,每一項都經歷同樣的循序步驟——編譯器永遠先做語彙分析、再做語法分析、最後做語意分析。
訊息式整合方案面對的則是個別訊息,它們未必屬於單一個更大的資料集。因此個別訊息更可能需要不同的處理步驟序列。
幾種行不通的替代方案#
Message Channel 讓寄件端與收件端解耦,這代表多個應用程式可以往同一個通道發布訊息——於是通道裡可能混著來自不同來源、需要依型別或其他準則區別對待的訊息。
方案一:為每種訊息型別各開一個通道(也就是後面會詳談的 Datatype Channel),再把各通道接到該型別所需的處理步驟。
這要求訊息發起者知道不同處理步驟的挑選準則才能發到正確通道,也可能導致通道數量爆炸。而且決定訊息走哪些步驟的因素未必只跟來源有關——想像一個「目的地隨著至今通過該通道的訊息數量而改變」的情境:沒有任何單一發起者知道這個數字,因此也無法把訊息送到正確的通道。
方案二:靠通道本身的訂閱關係做路由。 應用程式把訊息發到通道後就不再知道它的去向,訊息路徑會隨「誰訂閱了這個通道」而改變。
但這種「路由」完全不考慮個別訊息的屬性:元件一旦訂閱某通道,預設就會消費該通道上的所有訊息。這類似 Unix 的管線符號——你能把行程組合成一條 Pipes and Filters 鏈,但在這條鏈存續期間,所有文字行都經歷相同的步驟。
方案三:讓接收元件自己判斷該不該處理這則訊息。
這也有問題:訊息一旦被消費,元件才發現自己不想要,它沒辦法把訊息放回通道讓別的元件檢視。
有些訊息系統允許接收端在不移除訊息的前提下檢視訊息屬性,藉此決定是否消費。但這不是通用解法,而且會把消費元件綁死在特定訊息型別上——因為挑選邏輯現在被寫進元件裡了。這會降低該元件的重用潛力,也扼殺 Pipes and Filters 模型最關鍵的強項:可組合性。
上述多數替代方案都假設我們能修改參與其中的元件。但在多數整合方案裡,這些「元件」是龐大的應用程式,而且往往完全不能改——因為它們是套裝或遺留應用程式。要為了訊息系統或其他應用程式的需要去調整訊息的生產端或消費端,並不划算,甚至根本不可能。
解法#
Pipes and Filters 的優點正是元件的可組合性:我們能在鏈中插入額外步驟而不必修改既有元件。這開啟了一個選項——在兩個過濾器之間插入另一個過濾器,由它決定下一步該執行什麼。
Message Router 與最基本的 Pipes and Filters 概念不同之處,在於它連接多個輸出通道。拜 Pipes and Filters 架構之賜,圍繞在 Message Router 周圍的元件完全不知道它的存在。
Message Router 的一個關鍵性質是:它不修改訊息內容,只關心訊息的目的地。
好處#
使用 Message Router 的關鍵好處是:決定訊息目的地的準則被維護在單一位置。
- 定義新訊息型別、加入新處理元件、或路由規則改變時,我們只需修改 Message Router 的邏輯,其他元件一概不受影響。
- 而且因為所有訊息都經過同一個 Message Router,進站訊息保證會依正確順序逐一處理。
代價與陷阱#
- 可能適得其反地增加耦合——Message Router 必須知道所有可能的訊息目的地才能正確導向。某些情況下這份目的地清單變動頻繁,會讓 Message Router 變成維護瓶頸。此時不如讓各接收者自己決定對哪些訊息有興趣——作法是用 Publish-Subscribe Channel 搭配一組 Message Filter。本書把這兩種替代方案稱為預測式路由(predictive routing)與反應式過濾(reactive filtering)(詳見 Message Filter)。
- 效能損耗——Message Router 需要插入額外處理步驟。許多訊息式系統必須先把訊息從一個通道解碼,才能放到另一個通道;即使訊息本身其實沒變,這仍造成計算開銷,可能讓路由器變成效能瓶頸。用多個平行路由器或增加硬體可以減輕這個效應:訊息吞吐量或許不受影響,但延遲幾乎肯定會增加。
鬆散耦合的系統本來就難以看清「大局」——也就是訊息在系統中的整體流動。這是訊息方案的通病,而路由器會讓問題惡化:如果每樣東西都與其他東西鬆散耦合,就不可能搞懂訊息實際上往哪走,測試、除錯與維護都跟著變複雜。
有幾種工具能緩解這個問題:
- 用 Message History 在執行期檢視訊息,看它走過哪些元件。
- 彙整系統中每個元件所訂閱與發布的通道清單,據此畫出跨元件的所有可能訊息流圖。許多 EAI 套件把通道訂閱資訊維護在中央儲存庫,讓這類靜態分析更容易。
Message Router 的變體#
Message Router 可以用任意多的準則來決定進站訊息的輸出通道:
- 固定路由器(fixed router)——最平凡的情況:只定義單一輸入與單一輸出通道,從輸入消費一則、發布到輸出。為什麼要用這種笨路由器?它可以刻意用來解耦子系統,或在多個整合方案之間轉接訊息。多數情況下,固定路由器會與 Message Translator 或 Channel Adapter 搭配,用來轉換訊息內容或改走另一種通道型別。
- Content-Based Router(內容式路由器)——僅依訊息本身的屬性(型別、特定欄位值)決定目的地。這類路由器太常見了,因此有自己的專門模式。
- Context-Based Router(情境式路由器)——依環境條件決定目的地,常用於負載平衡、測試或容錯移轉。例如某個處理元件掛了,情境式路由器可以把訊息改送到另一個元件;也有路由器把訊息平均分散到多個通道以達成平行處理。
Message Channel 本身就已提供基本的負載平衡能力(多個競爭消費者各自從同一通道盡快消費),不需要路由器。但 Message Router 可以內建額外的智慧來路由訊息,而不只是通道那種簡單的輪詢。
- 無狀態 vs. 有狀態——多數路由器是無狀態的,一次只看一則訊息就做決定;其他路由器則會把先前訊息的內容納入考量,例如保存一份已收訊息清單來剔除重複訊息的路由器就是有狀態的。
- 連上 Control Bus 的路由器——多數 Message Router 的路由邏輯是寫死的,但有些變體連上 Control Bus,讓中介軟體能在不改程式碼、不中斷訊息流的前提下改變判斷準則。例如 Control Bus 可以把某個全域變數的值傳播給系統中所有路由器,讓訊息系統從「測試」切換到「正式」模式,這在測試時非常有用。
- Dynamic Router(動態路由器)——依各潛在接收者送來的控制訊息動態設定自己。
範例:商業 EAI 工具與一個簡單的 C#/MSMQ 路由器
商業 EAI 工具#
Message Router 的概念,正是幾乎所有商業 EAI 工具都實作的 Message Broker 的核心:這些工具接收進站訊息、驗證、轉換,再路由到正確目的地。
這種架構讓參與其中的應用程式完全不必知道其他應用程式的存在,因為 message broker 在應用程式之間居中斡旋。這在 EAI 中是關鍵功能——待連接的應用程式多半是套裝或遺留系統,整合必須非侵入式地發生(不修改應用程式碼),因此路由邏輯必須全部由中介軟體承擔。
Message Broker 是 [GoF] 中 Mediator 模式在整合領域的對應物。
用 C# 與 MSMQ 寫一個簡單路由器#
以下範例把進站訊息路由到兩個可能的輸出通道之一:
class SimpleRouter
{
protected MessageQueue inQueue;
protected MessageQueue outQueue1;
protected MessageQueue outQueue2;
public SimpleRouter(MessageQueue inQueue, MessageQueue outQueue1, MessageQueue outQueue2)
{
this.inQueue = inQueue;
this.outQueue1 = outQueue1;
this.outQueue2 = outQueue2;
inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage);
inQueue.BeginReceive();
}
private void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult)
{
MessageQueue mq = (MessageQueue)source;
Message message = mq.EndReceive(asyncResult.AsyncResult);
if (IsConditionFulfilled())
outQueue1.Send(message);
else
outQueue2.Send(message);
mq.BeginReceive();
}
protected bool toggle = false;
protected bool IsConditionFulfilled ()
{
toggle = !toggle;
return toggle;
}
}程式相當直接:它用 C# delegate 實作了一個事件驅動的訊息消費者。建構子把 OnMessage 註冊為 inQueue 上訊息的處理器,於是 .NET 框架會對每則抵達 inQueue 的訊息呼叫它;OnMessage 再呼叫 IsConditionFulfilled 決定往哪路由。在這個極簡範例中,IsConditionFulfilled 只是在兩個通道之間切換,把訊息平均分給 outQueue1 與 outQueue2。
為了把程式碼壓到最小,這個簡單路由器不具交易性——若它在消費了輸入訊息之後、發布到輸出通道之前崩潰,訊息就遺失了。後續章節會說明如何讓端點具備交易性(見 Transactional Client)。