作者:Jonathan Simon
情境#
一家華爾街大型投資銀行著手打造一套債券定價系統,以理順債券交易部門的工作流程。
目前債券交易員必須把大量債券的價格分別送到好幾個交易平台,每個平台各有自己的使用者介面。
系統的目標是:把「替所有債券定價」的瑣碎細節,連同債券市場特有的進階分析功能,一起收進單一個封裝好的使用者介面。
這意味著要與數個元件透過各種通訊協定整合與通訊。
系統元件包含:
- Java 厚客戶端——交易員使用的介面
- 市場資料子系統(遺留)——C++/TIBCO 的行情饋送與分析引擎
- 投稿子系統(遺留)——Contribution Server:負責與交易平台的所有通訊。交易平台是不受該銀行控制的第三方元件。

圖 13-1:系統的高層流程

圖 13-2:遺留的市場資料子系統

圖 13-3:遺留的投稿子系統
第一個決策:子系統之間怎麼溝通#
我們可以讓厚客戶端直接與遺留伺服器通訊,但那會把太多業務邏輯放進客戶端。因此改為建立一對 Java gateway:
- Pricing Gateway——負責市場資料
- Contribution Gateway——負責把價格送到交易平台
四種整合風格的篩選#
- Shared Database——立刻排除。我們想在客戶端與資料庫之間建立一層抽象,不希望客戶端裡有資料庫存取程式碼。
- File Transfer——同樣排除,因為必須把延遲降到最低,才能確保送到交易平台的是最新價格。
剩下 Remote Procedure Invocation 與 Messaging 兩個選項。
Java 平台對兩者都有內建支援:RPC 式整合可用 RMI、CORBA 或 EJB;訊息式整合的共通 API 則是 JMS。兩種風格在 Java 中都容易實作。
為什麼選訊息傳遞#
系統中 Pricing Gateway 與 Contribution Gateway 各只有一個實例,但通常有許多厚客戶端同時連上這些服務(每位登入的交易員一個)。而且銀行希望這是一套通用的定價系統,能被其他應用程式使用——因此除了數量未知的厚客戶端之外,還可能有數量未知的其他應用程式在使用 gateway 送出的定價資料。
厚客戶端要用 RPC 呼叫 gateway 取得定價資料相當容易。但定價資料會持續不斷地被發布,而各客戶端只對特定資料有興趣,要及時把相關資料送到正確的客戶端就會很困難:
- 客戶端輪詢 gateway → 會造成大量開銷
- 由 gateway 主動推送 → gateway 就得追蹤「目前有哪些客戶端活躍、各自想要哪些資料」;而每當有新資料(一秒發生好幾十次),gateway 就得對每個有興趣的客戶端各做一次 RPC。理想上所有客戶端該被同時通知,因此每次 RPC 都得跑在自己的並行執行緒中。
這行得通,但複雜度飆升得極快。
如此一來,gateway 能輕鬆地把新資料送給任何有興趣的一方,完全不必知道有多少監聽者、它們是誰。
客戶端仍需要能喚起 gateway 中的行為。由於 gateway 永遠只有兩個、而且客戶端多半可以在同步呼叫期間阻塞,這些「客戶端到 gateway」的呼叫用 RPC 實作也相當容易;但既然「gateway 到客戶端」已經用訊息了,用訊息實作反方向的通訊大概同樣好。
因此,gateway 與客戶端之間的所有通訊都用訊息完成。因為所有元件都以 Java 撰寫,JMS 是顯而易見的選擇。
這實際上創造出一條 Message Bus 式的架構——未來的系統要與現有系統整合時,訊息基礎設施幾乎不必改動,應用程式的業務功能因而能被銀行開發的其他應用程式輕鬆使用。
JMS 只是規格,我們還得挑一套相容的訊息系統。選了 IBM MQSeries JMS——因為這家銀行是「IBM 店」,已在用 WebSphere 應用伺服器與許多其他 IBM 產品,既有的支援基礎設施與網站授權都現成。

圖 13-4:系統與其元件

圖 13-5:以 JMS 通訊的 Java 元件
第二個決策:橋接 MQSeries 與 TIBCO#
接下來的問題是:怎麼把 MQSeries 訊息系統,連上獨立的 C++ Contribution 伺服器與基於 TIBCO 的市場資料與分析引擎伺服器?
我們需要讓 MQSeries 的消費者能存取 TIB 訊息。
為什麼 Message Translator 行不通#
也許可以用 Message Translator 把 TIB 訊息轉譯成 MQSeries 訊息?
- MQSeries 的 C++ 客戶端確實能充當 Message Translator,但用它就得犧牲 JMS 伺服器的獨立性
- TIBCO 雖然有 Java API,但客戶的架構師與經理否決了它
因此 Message Translator 的作法只能放棄。
用一對 Channel Adapter 組成 Messaging Bridge#
從 TIB 伺服器到 MQSeries 伺服器的橋接,需要 C++ 與 Java 之間的通訊。我們可以用 CORBA,但訊息怎麼辦?
仔細看 Message Translator 會發現它在「使用通訊協定」這點上與 Channel Adapter 相關——Channel Adapter 的核心正是把非訊息系統接上訊息系統;而一對連接兩套訊息系統的 Channel Adapter,就是 Messaging Bridge。
Messaging Bridge 的目的正是把訊息從一套訊息系統搬到另一套——正是我們要做的事,只是多了「Java 與 C++ 之間跨語言通訊」這層複雜度。
實作方式:
- 建立兩個輕量的 Channel Adapter 伺服器——一個用 C++ 管理與 TIB 的通訊,一個用 Java 管理與 JMS 的通訊
- 這兩個 Channel Adapter 本身就是 Message Endpoint,彼此透過 CORBA 通訊
和選 MQSeries 一樣,這裡選 CORBA 而非 JNI,是因為它是公司標準。
這是模式應用的好例子:我們把兩個 Channel Adapter 與一個非訊息協定組合起來,實作出 Message Translator 模式——實際上是用一個模式去實作另一個模式。
而且我們改變了 Channel Adapter 的脈絡:它不再是「把訊息系統接到非訊息系統」,而是用一個非訊息的跨語言轉譯協定連接兩套訊息系統。

圖 13-6:用 Channel Adapter 實作 Message Translator

圖 13-7:加入 Channel Adapter 後的系統
通道結構的設計#
市場資料饋送:一支債券一個通道#
即時市場資料源自 C++ 伺服器的行情饋送,它在 TIB 上廣播資料,為每一支債券各用一個 Publish-Subscribe Channel。
每支新債券都要一個新通道,聽起來有點極端。但這其實沒那麼嚴重——在 TIBCO 中你不需要真的「建立」通道:通道是由一組階層式的主題名稱(subjects)指涉的,TIBCO 伺服器再依 subject 過濾單一訊息流,把每個唯一 subject 送到一個虛擬通道——結果是非常輕量的訊息通道。
替代方案是在少數幾個通道上發布,讓訂閱者用 Message Filter 或 Selective Consumer 自行過濾、在收到每則訊息時判斷是否處理。
但既然市場資料是發布在債券專屬通道上,訂閱者就能訂閱一系列債券的更新——這實際上讓訂閱者靠「選擇性訂閱通道」來過濾,只收到自己在意的更新,而不必在收到訊息之後才判斷。
值得注意:用大量通道來避免過濾,是訊息通道的非標準用法。 但在 TIBCO 技術的脈絡下,我們真正在決定的是「自己實作過濾器,還是利用 TIBCO 內建的通道過濾」,而不是「要不要用這麼多通道」。
分析引擎的輸出該怎麼發布#
下一個要設計的元件是分析引擎——另一個 C++/TIB 伺服器,它修改市場資料後再廣播回 TIB。
既然我們已經從行情饋送繼承了「一支債券一個通道」,直覺上會想把修改後的資料廣播回同一個債券通道。但這行不通——因為修改債券價格的分析是「交易員專屬」的;若把修改後的資料廣播回債券通道,就會用交易員專屬資料取代通用市場資料,破壞資料完整性。
另一個作法是在同一通道上發布另一種訊息型別代表交易員專屬資料,讓訂閱者自己挑。但這樣客戶端就得實作自己的過濾器來濾掉其他交易員的訊息,而且訂閱者收到的訊息量會大幅增加,帶來不必要的負擔。
於是剩下兩個選項:
- 一位交易員一個通道——每位交易員有一個專屬通道承載修改後的市場資料
- 一位交易員 × 一支債券一個通道——例如債券 ABC 的市場資料發布在「Bond ABC」,交易員 A 的修改後資料發布在「Trader A, Bond ABC」,交易員 B 在「Trader B, Bond ABC」
通道數量的評估#
「一交易員一債券」的作法用掉多得多的通道。最壞情況是「債券總數 × 交易員數」;但我們知道大約只有 20 位交易員、每人定價不超過幾百支債券,上限落在一萬以下——相較於行情饋送本身使用的近十萬個通道,這並不離譜。而且用的是 TIB,通道很便宜,數量不是嚴重問題。
另一方面,通道數量龐大從管理角度可能是問題:每加一支債券,就得為每位交易員維護一個通道。在非常動態的系統中這會很嚴重。
但我們的系統本質上是靜態的,而且已有自動管理訊息通道的基礎設施;再加上遺留元件本來就採類似作法,缺點因而被降到最低。
這不是說我們該毫無必要地製造大量通道——而是說,在有理由時,我們可以採用「使用大量通道」的架構作法。
真正的決定因素:邏輯放在哪裡#
這個案例的理由歸結為邏輯的位置。
若採「一交易員一通道」,分析引擎就需要「把輸入與輸出通道分組」的邏輯——因為它的輸入通道是依債券分的、輸出通道卻要依交易員分,這要求分析引擎把某位交易員橫跨多支債券的分析輸出,全部路由到該交易員專屬的輸出通道。
依照 Message Bus 的結構,分析引擎是一個「可能被其他數個系統使用」的通用伺服器,我們不想用系統專屬功能把它弄髒。
反過來說,「一交易員一債券」的作法行得通,因為『交易員擁有某支債券的分析輸出』是公司公認的實務。它保持了行情饋送的通道分離結構完整,只是多加了一些通道。

圖 13-8:一位交易員一個通道

圖 13-9:一位交易員一支債券一個通道
在抵達客戶端之前收攏通道#
在抵達客戶端之前,我們需要一個 Content-Based Router 把這一大堆通道併成可管理的數量——我們可不希望跑在交易員桌面上的客戶端去監聽成千上萬個通道。
那 Content-Based Router 該放哪?
- 業務邏輯會被拆散在 C++ 與 Java 之間
- 會失去 TIB 端通道分離所帶來的好處(讓我們得以在資料流後段避免過濾)
若把「以債券分離通道」的結構一路保持到客戶端,Pricing Gateway 就得在 JMS 端複製所有債券專屬的 TIB 通道;即使在 Gateway 與客戶端之間再加一個中介元件,Gateway 仍得複製全部通道。
作法是:Pricing Gateway 透過 C++/TIB Channel Adapter 為系統中每位交易員的每支債券註冊成消費者,然後只把與某位交易員相關的訊息轉給對應的客戶端。
如此一來,JMS 端只用少量通道,同時把 TIB 端通道分離的好處發揮到最大。
這段通道配置的討論,是「整合模式很重要」的好例子:光說「我用了某個模式」是不夠的,你得想清楚如何最好地實作它、如何把它融進系統來解決手上的問題。
這個例子也展現了業務作用力的實際影響——若我們能把業務邏輯實作在任何元件中,大可採用「一交易員一通道」的作法,得到一個通道少得多、整體更簡單的方案。

圖 13-10:抵達客戶端的完整市場資料流
挑選訊息通道型別#
JMS 規格描述兩種通道型別:**Point-to-Point Channel(JMS Queue)**與 Publish-Subscribe Channel(JMS Topic)。
系統的高層訊息流是:市場資料從 Pricing Gateway 流向客戶端,客戶端再送到 Contribution Gateway;客戶端送訊息給 Pricing Gateway 以改變套用在各債券上的分析;Contribution Gateway 也送訊息給客戶端,回報各交易平台的價格更新狀態。
廣播給所有客戶端?#
許多系統會單純把訊息廣播給所有客戶端,讓每個客戶端自己決定要不要處理。這對我們行不通——送給每個客戶端的市場資料訊息量非常大,若把更新廣播給沒興趣的交易員,就是在白白浪費客戶端的處理器週期。
純點對點?#
點對點聽起來不錯,因為客戶端與伺服器之間是一對一的。

圖 13-12:以點對點訊息傳遞價格更新
用 Recipient List 補救?#
我們可以用 Recipient List 解決:系統為每位交易員建立一份「該交易員所有客戶端實例」的清單,送出與某位交易員相關的訊息時就送給清單上的每個應用程式,保證該交易員的所有客戶端實例都收得到。

圖 13-13:以 Recipient List 傳遞價格更新
最終方案:兩個方向用不同通道型別#
改用 Publish-Subscribe Channel,系統就能在交易員專屬通道(而非客戶端專屬通道)上廣播訊息,該交易員的所有客戶端都會收到並處理。
但發布訂閱的缺點是無法保證伺服器元件端的唯一處理:同一個伺服器元件的多個實例可能各自處理同一則訊息,甚至送出無效的價格。
回顧系統的訊息流會發現:每種通道型別都只有一個方向令人滿意——
- 伺服器 → 客戶端:發布訂閱令人滿意,點對點不滿意
- 客戶端 → 伺服器:點對點令人滿意,發布訂閱不滿意
既然沒有必要在兩個方向使用同一種通道,我們就讓每種通道只走一個方向:
- 客戶端 → 伺服器:用點對點
- 伺服器 → 客戶端:用發布訂閱
這個組合讓系統同時享有「與伺服器元件直接通訊」與「發布訂閱的多播特性」,卻避開了兩者的缺點。

圖 13-11:系統的訊息流

圖 13-14:以發布訂閱訊息傳遞價格更新
