脈絡:本章多數路由模式依一組規則把進站訊息路由到一或多個目的地。但有時我們需要的不只是路由到單一元件,而是讓訊息穿過一整串元件

假設我們用 Pipes and Filters 架構處理進站訊息,訊息必須經歷一連串處理步驟與業務規則驗證。因為驗證的性質差異極大、還可能依賴外部系統(例如信用卡驗證),我們把每種步驟實作成獨立的過濾器;不符條件的訊息被路由到例外通道,而過濾器之間的通道決定了驗證的順序。

但如果每則訊息要做的驗證取決於訊息型別呢?(採購單請求不需要信用卡驗證;透過 VPN 送單的客戶或許不需要解密與驗證。)

好解法的四個要求#

  • 訊息流有效率——訊息只該流經必要的步驟,避開不必要的元件
  • 有效使用資源——不該用掉巨量的通道、路由器與其他資源
  • 有彈性——個別訊息的路徑應該容易改變
  • 好維護——需要支援新的訊息型別時,最好有單一個維護點,以免引入錯誤

五個方案的比較#

假設系統提供 A、B、C 三個處理步驟,而當前訊息只需通過 A 與 C。

方案 A:一條長鏈 + 元件內建繞過邏輯#

把所有可能的驗證步驟串成一條長鏈,在每個元件裡加上「若本型別不需要就繞過」的程式碼。這採用的是 Message Filter 中談的反應式過濾。

簡潔雖誘人,但元件混合了業務邏輯(驗證)與路由邏輯(判斷要不要驗證),會更難重用。而且可以想見兩種訊息可能經歷相似步驟卻順序不同——這種寫死的作法無法輕鬆支援。

方案 B:每個步驟前面加一個 Content-Based Router#

為了改善關注點分離、提升可組合性,把每個元件內部的「閘門」邏輯換成 Content-Based Router:訊息抵達路由器時檢查型別,需要就路由過驗證、不需要就直接繞到下一個路由器。

當每個步驟彼此獨立、路由決策能在各步驟本地做出時,這種配置運作得相當好。

缺點是:即使只需要執行少數幾個驗證,訊息仍得穿過一長串路由器——每則訊息跨越通道的次數是「可能元件數的 2 倍」。元件庫一大,這個簡單功能就會造成驚人的訊息流量。

而且路由邏輯被分散到許多過濾器上,很難搞清楚某種型別的訊息實際上會經歷哪些驗證步驟;新增訊息型別時,可能得更新每一個路由器。它同樣受限於「訊息得依共同順序執行步驟」。

方案 C:前置路由器 + 每種型別一條寫死的鏈#

為每種訊息型別各設一條 Pipes and Filters 鏈,用一個 Content-Based Router 依型別把進站訊息導向正確的鏈。

但它要求我們寫死所有可能的驗證規則組合;而且同一個元件可能出現在多條路徑上,就得跑多個實例,造成不必要的重複。訊息型別一多,元件實例與相關通道的數量會變成維護惡夢。

一句話:極有效率,代價是可維護性。

方案 D:在每個步驟「之後」插入路由器#

為了避免寫死所有組合,在每個驗證步驟之後插入 Content-Based Router(最前面還需要一個來啟動)。這些路由器夠聰明,能直接把訊息轉給下一個必要的驗證步驟,而不是盲目地丟給鏈上的下一個步驟。

抽象來看,這與反應式過濾很像(訊息交替穿過路由器與過濾器),但這裡的路由器比單純的是/否判斷更有智慧,能跳過不必要的步驟——本例中訊息只穿過 2 個路由器,而方案 B 要 3 個。

這提供了效率與彈性,但沒有達成「中央控制」的目標——路由邏輯仍分散在一系列獨立路由器中,我們仍得維護數量可能很多的路由器。

方案 E:單一「超級路由器」#

把所有路由器合併成一個超級路由器:每個驗證步驟之後,訊息都被路由回它,由它決定下一步。

因為所有路由決策都集中在單一路由器,我們得想辦法記住已經完成了哪些步驟——要嘛超級路由器是有狀態的,要嘛每個過濾器得在訊息上貼標籤告知「最後經過的是哪個過濾器」。

而且每個驗證步驟仍需兩個通道(去元件、再回超級路由器),流量大約是方案 C 的兩倍

解法#

流程的開頭插入一個特殊元件,為每則訊息計算所需步驟的清單,把清單當作 Routing Slip 附在訊息上,並把訊息路由到第一個處理步驟;此後每個步驟處理成功後就看一眼 Routing Slip,把訊息交給表中指定的下一個步驟。

這與貼在部門傳閱雜誌上的傳閱單非常像。唯一的差別是 Routing Slip 有明確的元件順序,而多數公司裡你讀完雜誌後可以交給名單上任何一個還沒讀的人(當然,老闆通常排第一)。

圖 7-16:Routing Slip 解法示意

它為什麼好#

Routing Slip 結合了超級路由器(方案 E)的中央控制,與寫死方案(方案 C)的效率:我們事先決定完整的路由方案並附在訊息上,因此不必為了後續決策再回到中央路由器

那麼它和方案 A(元件內建路由邏輯)有什麼不同?

內建於各元件的路由邏輯類似 Return Address——只是這裡的回覆位址是從一份位址清單中挑出來的。就像 Return Address 一樣,元件即使內含一小段路由邏輯,仍保有重用性與可組合性;而路由表的計算現在能在中央一處完成,完全不必動任何處理元件內部的程式碼

限制#

天下沒有白吃的午餐:

  • 訊息尺寸略增——多數情況下微不足道,但要意識到我們現在把流程狀態(哪些步驟已完成)帶在訊息裡

這會有副作用:訊息一旦遺失,我們失去的不只是訊息資料,還有流程資料(它接下來要去哪)。許多情況下,把所有訊息的狀態集中保存以便回報或錯誤復原,是有價值的。

  • 路徑一旦上路就無法改變——這意味著訊息路徑不能依賴途中某個步驟產生的中間結果

但現實中的業務流程常常會依中間結果改變流向——例如依庫存系統回報的可供量決定接下來走哪條路。這也表示必須有一個中央實體能事先決定訊息該經歷的所有步驟,這會讓設計帶上一定的脆弱性,類似使用 Content-Based Router 時的顧慮。

遇到遺留應用程式時#

Routing Slip 假設我們有能力替個別元件加上路由邏輯。面對遺留或套裝應用程式時,我們可能改不了元件本身的功能,此時就得用一個外部路由器透過訊息與元件溝通。

這無可避免會增加通道與元件的數量,但 Routing Slip 在效率、彈性與可維護性之間仍提供了最好的取捨。

圖 7-17:在遺留應用程式上實作 Routing Slip

最適合的場景#

  • 一連串二元的驗證步驟——因為不對訊息添加資訊,「上路後不能改路徑」的限制就不再是問題;我們仍享有「透過重新設定中央 Routing Slip 就能改變驗證順序」的彈性。每個元件可以選擇因錯誤中止流程,或把訊息交給下一步。
  • 每個步驟都是無狀態的轉換——例如來自各商業夥伴的訂單全部抵達同一個通道,但依夥伴不同需要不同的轉換步驟(有些要解密、有些不用;有些要轉換或增益、有些不用)。為每個夥伴各留一張 Routing Slip,就能在中央一處輕鬆重設各夥伴的步驟。
  • 每個步驟只收集資料、不做決策——例如收到 DSL 線路申請時,訊息可能只含申請人的家用電話,我們得去外部來源查出客戶姓名、服務該線路的中心局、與中心局的距離等等;等資料收齊了才決定要提供什麼方案

最後這種情境要評估一下:我們真的需要 Routing Slip 的彈性嗎? 否則一條簡單寫死的 Pipes and Filters 鏈可能就夠了。

用 Routing Slip 實作簡單路由器#

Content-Based Router 的一個缺點是它必須內含每個可能接收者與其路由規則的知識。基於鬆散耦合的精神,「一個中央元件知道許多其他元件」可能是不受歡迎的。

替代方案之一是 Publish-Subscribe Channel 加一組 Message Filter,但那有重複處理的風險。另一個選項是用改造過的 Routing Slip 充當 [GoF] 的 Chain of Responsibility——讓每個元件選擇接受訊息、或把它路由給清單中的下一個元件。

Routing Slip 是一份靜態的參與者清單,因此仍然意味著某個中央元件得知道所有可能的接收者;但它不需要知道每個元件會消費哪些訊息。

用 Routing Slip 可以避免重複處理的風險,也容易判斷「某則訊息是否沒有被任何元件處理」。

主要取捨是處理較慢、網路流量增加:Content-Based Router 不論系統數量多少都只發布一則訊息,而 Routing Slip 作法平均會發布「系統數量的 1/2」則訊息。

若能把最有可能處理該訊息的系統排在前面,可以減少這個數字,但訊息數量很可能仍高於預測式的 Content-Based Router

什麼時候該改用 Process Manager#

有些情況需要比「簡單的循序清單」更多的控制,或需要依中間結果改變訊息流向Process Manager 能滿足這些需求,因為它支援分支條件、fork 與 join。

本質上,Routing Slip 是「動態設定的業務流程」的一個特例

動態的 Routing Slip 結合了「中央維護點」與「寫死方案的效率」。但隨著複雜度上升,分析與除錯會愈來愈困難——因為路由的狀態資訊被分散在各則訊息中。

而且當流程定義的語意開始納入決策、fork、join 這類構造時,設定檔可能變得難以理解與維護。我們當然可以在路由表中放入條件語句、並強化各元件中的路由模組去解釋這些條件命令,但要小心別讓額外功能壓垮這個解法的簡潔

一旦需要這種程度的複雜度,也許就該放棄 Routing Slip 的執行期效率,改用強大得多的 Process Manager。

範例:把 Routing Slip 當成組合服務、以及 WS-Routing

Routing Slip 作為組合服務#

建構服務導向架構時,單一個邏輯功能常由多個獨立步驟組成,主因有二:

  • 套裝應用傾向依其內部 API 暴露細粒度介面。把它們整合進方案時,我們想在更高的抽象層次上工作——例如「新增帳戶」在帳務系統中可能需要多個步驟:建立新客戶、選擇服務方案、設定地址屬性、驗證信用資料等等。
  • 單一邏輯功能可能橫跨多個系統。我們想對其他系統隱藏這件事,好保有「在系統之間重新分配責任而不影響整合方案其餘部分」的彈性。

用 Routing Slip 就能輕鬆地為單一請求訊息執行多個內部步驟,而且對外看起來就像單一步驟

  1. 進站請求訊息(指明意圖操作與必要資料)送到查詢元件
  2. 查詢元件從服務目錄取出該操作所需的處理步驟清單,把通道清單(每個通道對應一個細粒度操作)加進訊息表頭,並把回傳通道也加進清單,好讓完成的訊息回到查詢元件
  3. 查詢元件把訊息發布到第一個活動的通道
  4. 每個路由器從佇列讀取請求、交給服務提供者;執行完畢後標記該活動為已完成,並依路由表把訊息路由到下一個通道
  5. 查詢元件從回傳通道消費訊息並轉發給請求方——對外,整個過程看起來就像一次簡單的請求-回覆交換

圖 7-18:以 Routing Slip 實作組合服務

圖 7-19:組合服務的訊息流程

WS-Routing#

Web service 請求常需要經過多個中介者,Microsoft 為此定義了 WS-Routing 規格:一個基於 SOAP、把訊息從寄件端經一連串中介者送到接收端的協定。

以下範例顯示一則從節點 A 經中介者 B、C 送到 D 的訊息的 SOAP 表頭:

<SOAP-ENV:Envelope
      xmlns:SOAP-ENV="http://www.w3.org/2001/06/soap-envelope">
   <SOAP-ENV:Header>
      <wsrp:path xmlns:wsrp="http://schemas.xmlsoap.org/rp/">
         <wsrp:action>http://www.im.org/chat</wsrp:action>
         <wsrp:to>soap://D.com/some/endpoint</wsrp:to>
         <wsrp:fwd>
            <wsrp:via>soap://B.com</wsrp:via>
            <wsrp:via>soap://C.com</wsrp:via>
         </wsrp:fwd>
         <wsrp:from>soap://A.com/some/endpoint</wsrp:from>
         <wsrp:id>uuid:84b9f5d0-33fb-4a81-b02b-5b760641c1d6</wsrp:id>
      </wsrp:path>
   </SOAP-ENV:Header>
   <SOAP-ENV:Body>
      ...
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

和多數服務規格一樣,WS-Routing 很可能隨時間演進或與其他規格合併。這裡收錄它,是作為「Web services 社群在路由這件事上的走向」的一個快照。