多一個中介者:間接通訊的核心概念
多加一個中間人,反而讓兩端更自由——這是間接通訊反直覺卻極其重要的一課
前幾章的通訊,其實都「太直接」
前面談 RPC、socket 這類直接通訊,有一個共同的假設:送方明確知道收方是誰,兩邊直接連在一起。這很方便,卻也帶來僵化——換一台伺服器、伺服器當機,都會直接波及對方。
這一章要介紹另一條路:間接通訊(indirect communication)——分散式系統中的實體,透過一個中介者(intermediary)溝通,送方與收方之間沒有直接耦合。定義裡「收方」故意寫成可能複數,因為很多間接通訊範式天生就支援一對多。
你要寄文件給「採購部負責人」。直接通訊的話,你得知道那個人是誰、坐哪、現在在不在位子上,親手交給他——哪天他離職、換人、請假,你就卡住了。間接通訊則是把文件交給前台收發室,你根本不必認識負責人本人;就算換了新人,收發室也會把文件轉給「現任負責人」。
電腦科學有句名言,正是在講這件事:「All problems in computer science can be solved by another level of indirection.」(所有問題都能靠多加一層間接來解決。)收發室,就是那「多加的一層」。
直接讀原文,旁邊就是白話
這是本章對「間接通訊」最核心的定義段落。原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。
間接通訊的定義是:分散式系統中的實體,透過一個中介者溝通,送方與收方之間沒有直接耦合。
正因為是直接耦合,要把伺服器換成功能相同的另一台,會變得很麻煩。
同樣地,伺服器一旦當機,client 會直接被波及,還得自己想辦法處理這個失敗。
間接通訊最大的缺點是:多加這一層,必然會帶來一些效能上的額外負擔。
配對的另一句玩笑話:沒有什麼效能問題是「拿掉一層間接」解決不了的。
「加一層間接」解決彈性問題,「拿掉一層間接」解決效能問題——兩句話合起來,正是整個間接通訊要面對的權衡:多一層耦合(coupling)鬆綁帶來的自由,勢必要用一點延遲去換。
招牌互動:直接耦合 vs 間接通訊,同一個場景走兩遍
下面模擬同一件事:A 要把訊息交給 B,但 B 這時還沒上線。按「下一步」,先看「直接耦合」怎麼卡住,再看「間接通訊」怎麼靠中介者解套。
直接耦合下,伺服器一旦失效或還沒上線,會直接波及對方,client 必須自己顯式處理這個失敗。間接通訊把這條連結拿掉之後,A 完全不需要知道 B 是誰、B 存不存在——這正是空間解耦(space uncoupling)和時間解耦(time uncoupling)要處理的問題。
中介者帶來的兩大性質——但它們不一定同時出現
用了中介者之後,系統會出現兩個關鍵的「解耦(uncoupling)」性質。它們是兩件不同維度的事,不要當成同一件事的兩個名字。
送方不需要、也不用知道收方是誰,反之亦然。因為彼此不認識,開發者就有很大自由度應付變化:參與者可以被替換、更新、複製或遷移。
送方與收方可以擁有各自獨立的生命週期——不必同時存在也能溝通。這在易變環境裡特別寶貴:送方與收方常常來來去去。
常見的誤會是「間接=兩種解耦都有」,其實不然。最有名的反例是IP multicast:它是空間解耦,但時間耦合——訊息送往群組(不認識特定收方,空間解耦),但所有收方必須在送出當下都在線(時間耦合)。
門口有面留言板當中介者。你貼一張「冰箱裡的便當記得拿走」,你不必知道便當主人是誰(空間解耦);你早上九點貼、主人下午三點才看到,你們沒有同時存在也完成了溝通(時間解耦)。關鍵在於:便利貼會留在板子上,這就是持久性(persistency)——如果留言板是一塊「只在你站前面才顯示字、一走就熄」的螢幕,時間解耦就不成立了,這正是 IP multicast 的處境。
最容易搞混的一組概念:非同步 ≠ 時間解耦
這兩個詞長得很像,卻是不同維度。非同步通訊(asynchronous communication)解決的是「送方要不要等」的問題;時間解耦問的是「雙方要不要同時存在」——這是多了一個維度的問題。
你得等對方接、講完才能掛——送方被卡住,雙方必須同時在線。
你按下傳送就繼續滑手機,不必等他回。但對方此刻仍得是個「存在的門號」,訊息才送得出去——非同步,卻仍是時間耦合。
寫一封信投進時空膠囊,要給「十年後住進這間房子的人」。收件人現在根本還不存在,但訊息會被保存到他出現——這才是真正的時間解耦。
本章很多技術同時是「非同步且時間解耦」,容易讓人誤以為兩者一體。但若混淆,你可能誤以為「改成非同步就能讓離線使用者補收訊息」——其實那需要的是時間解耦(持久性),不是非同步。反過來,間接通訊也不必然等於非同步:JGroups 的 RpcDispatcher 就是在間接通訊之上做出同步呼叫——透過秘書幫你接通電話,你仍站著等對方回話。
什麼情況最該選間接通訊?
當系統預期會變動時,間接通訊特別好用——行動環境(使用者頻繁連上/斷開全球網路,收方來來去去)、事件散播(收方未知且常變動,例如金融系統的即時行情推播)、大型基礎設施(例如 Google 的關鍵元件就大量使用間接通訊)都是典型場景。
但天下沒有白吃的午餐:多繞一手,文件會晚一點到,效能成本是真實的;而且系統開發者也會更難管理與除錯,因為缺乏直接(空間或時間)耦合可循。
我的系統會常常變動嗎?我能接受多一層延遲、多一點難以追蹤的代價嗎?如果兩個答案都是「是」,間接通訊值得考慮;如果系統穩定不變、追求極致效能,直接通訊往往更划算。
小試身手
把「間接通訊」「空間解耦」「時間解耦」「非同步」這幾個概念釐清了,來兩題檢查一下:
中介者的概念懂了,第一個具體範式,是一次送給一整群人。往下捲。
一次送、全員收:群組通訊
跟一群人同時說同一句話,比想像中更難的地方在於:怎麼確定大家聽到的順序一樣
先搞懂一件事:群組通訊在解決什麼問題
上一個模組講到間接通訊的第一個實例,就是群組通訊(group communication)。它的核心概念很單純:有一個群組(group),程序可以加入(join)或離開(leave)這個群組;送方對群組送一則訊息,系統就把它傳給所有成員,還附帶可靠性與排序的保證。整個過程中,送方完全不知道收方是誰——這正是空間解耦。
書裡先幫你把三種「送給誰」的範圍分清楚:
送給單一一個程序,最基本的一對一通訊。
送給群組裡的所有成員——這正是群組通訊實作的目標。
送給系統裡所有程序,而不只是某個子群組。
群組通訊的關鍵特徵:一個程序只需要發出一次 multicast 操作(在 Java 裡是 aGroup.send(aMessage)),就能把訊息送到一整群程序,而不是對每個成員各自呼叫一次 send。
群組通訊之於 IP multicast,就像 TCP 之於 IP 的點對點服務——群組通訊是多播的抽象,建在 IP multicast 或覆蓋網路之上,額外加上成員管理、失效偵測、可靠性與排序保證。
直接讀原文,旁邊就是白話
這是 6.2 節開場對群組通訊的定義,以及「單一 multicast 操作」為什麼不只是圖方便。原文放左邊,白話導讀放右邊。
群組通訊提供的服務是:把訊息送到一個群組,系統就把它遞送給群組的所有成員。整個過程中,送方根本不知道收方是誰。
群組通訊最核心的特徵是:程序只需要發出一次 multicast 操作,就能把訊息送給一整群程序,不必對每個人各自呼叫一次 send。
「用一次操作取代多次個別送」聽起來只是程式寫起來方便,其實遠不只如此——它讓實作能有效利用頻寬。
如果改用多次個別送,送方半途掛掉,可能有些成員收到訊息、有些沒收到;而且送到任兩個成員的兩則訊息,先後順序完全不保證。
多次個別送,系統沒辦法對「這一群人」做出任何整體保證;一次 multicast 操作,才讓底層有機會保證「要嘛全員收到、要嘛都沒收到」,還能省頻寬——實作可以用散播樹(distribution tree)讓每條連線最多送一次,還能借用網路硬體本身的多播能力。
招牌互動:一次發送,全員收到;還有人半路加入、退出
下面是一個送方對群組發 multicast,三個成員同時收到;接著再示範群組成員的動態變化——有新成員加入,也有成員離開。按「下一步」照順序看。
群組成員會隨時加入、離開、甚至當機。送方只給一個群組識別碼,真正「這次要送給誰」是由位址展開(group address expansion)依「當下」的成員視圖動態決定的——這也是為什麼群組成員管理要跟遞送協調一致,否則有人半路加入或退出時,訊息可能送錯名單。
「可靠」多播,到底可靠在哪三件事上
一對一的可靠通訊靠兩個性質:完整性(integrity)——收到的跟送出的一樣,且不重複遞送;有效性(validity)——送出的訊息終究會被遞送。
但這是「一個收方」的保證。要擴展到「一整群收方」,還要再加上第三個性質:
如果訊息遞送給群組中任一個程序,就會遞送給群組中所有程序。換句話說,可靠多播追求的是「要嘛大家都收到、要嘛大家都沒收到」,就像全班一起及格、或一起補考,不會只有一半人拿到通知。
但光「可靠」還不夠。底層的 IPC 原語本來就不保證排序——如果 multicast 是拿一連串一對一訊息拼出來的,這些訊息可能遭遇任意延遲;就算用 IP multicast,也有類似問題。所以群組通訊還要另外提供有序多播(ordered multicast),程度分成三層。
動手配配看:三種排序保證,誰對應哪種情境?
把下面三種排序保證,拖到它最貼切的情境描述上。配完按「對答案」。
用比喻記住這三層嚴格度:FIFO 排序像是每個人照自己的時間軸講話,但你的第一句和我的第一句誰先到不一定;因果排序像是你回覆了我的話,大家一定先看到我的原話、才看到你的回覆;全序則像全班共用同一份逐字稿,所有人看到的順序一模一樣。
排序與一致同意,本質上都是分散式系統裡「協調與一致同意」的例子。保證愈強,演算法愈複雜、對擴展性的負面衝擊也愈大——尤其是全序。三種保證可以單用,也可以混用,別一味追求最強,按應用真正需要的程度選就好;完整的演算法細節留待第 15 章。
群組服務還有幾個關鍵分類,決定它適合什麼場景
不同群組通訊服務的假設不一樣,其中最重要的兩組區分是:
只有成員能對群組多播,成員自己送出時也會收到。適合「合作的伺服器彼此私下傳訊息,只有它們該收到」。
群組外的程序也能往裡送。適合「把事件遞送給一群有興趣的程序」,例如通知訂閱者。
重疊:一個程序可同時屬於多個群組(現實系統通常如此)。非重疊:每個程序最多只屬於一個群組。
另外還有一組區分是程序群組與物件群組:程序群組的訊息是未結構化的位元組陣列,層次類似 socket;物件群組(如 Electra)呼叫則會自動marshalling與 dispatch,client 透過本地 proxy 把整個群組視為單一物件,介面更高階但實務上較少用。
像 JGroups 這類廣泛使用的工具箱,走的都是程序群組路線——層次低一點、貼近 socket,但夠泛用、夠成熟。物件群組雖然介面更高階,反而較少被真正採用。
誰在維護「現在群組裡有誰」這件事?
真實系統裡,程序會隨時加入、離開、甚至當機。群組成員管理的任務,就是隨時維持一份準確的當前成員視圖,具體有四件事:
建立/銷毀群組、加入/移出程序。
監看成員是否當機或不可達,標記為 Suspected(疑似失效)或 Unsuspected,據此排除疑似失效者。
有程序加入或被排除(失效或主動退出)時,通知其他成員。
送方只給群組識別碼,由成員服務展開成當前成員清單來遞送。
把成員服務想成社團裡那位盡責的幹部:有人入社退社,幹部更新名單;某人連續失聯,先標「疑似失聯」再確認除名;誰來誰走都會公告;你只要說「發給全社」,幹部就照當下名單一個個轉發,你不必自己列名單。IP multicast 其實只做了「一半的幹部工作」——能加入離開、也會位址展開,但不告訴你現在有誰,遞送也不與成員變更協調,要補齊這些得靠更進階的視圖同步(view-synchronous)群組通訊。
維護一致成員視圖對「小規模、靜態」的系統最有效;但在大規模、或成員頻繁進出的環境裡會很吃力,因為每次成員變動都要重新達成一致視圖。突破的方向是用更機率性的成員管理(底層用 gossip 八卦協定),或是專為 ad hoc/行動環境設計的成員協定。
案例研究:JGroups 怎麼把這些保證做出來
JGroups 是一套可靠群組通訊工具箱,讓程序能加入/離開群組、送收訊息,並提供多種可靠性與排序保證。它的核心是三個元件:
給應用開發者最原始的介面,提供加入、離開、送、收四項核心功能。就像對講機的頻道把手——剛建立時是斷線狀態,connect 才綁到一個具名群組,一個 channel 一次只能連一個群組。
建在 channel 之上的更高階抽象,讓常見的使用模式不必每次都從頭手刻。
底層通訊協定,由一疊可組合的協定層構成,決定實際的可靠性與排序語意。
協定堆疊像一疊可任意替換的樂高積木:常見五層有 UDP(傳輸層,用 IP multicast 送全群)、FRAG(訊息分段)、MERGE(處理網路分裂後重新合併)、GMS(群組成員協定,維持一致成員視圖)、CAUSAL(實作因果排序)。因為所有層都實作相同的 up/down 介面,可以任意順序組合——需要 FIFO、全序、加密、流量控制?換上對應的層即可。但有一條鐵律:同一群組的所有成員必須共用相同的協定堆疊,否則大家對訊息的處理方式就不一致了。
小試身手
從「一次 multicast 操作」到「排序保證」,抓住這幾個關鍵點,你就摸到群組通訊的骨架了。來兩題:
群組通訊要事先知道「群組是誰」——送方至少得知道要送到哪個群組。接下來看一種完全不用知道對方是誰的範式。往下捲。
訂閱的是事件,不是人:發布訂閱系統
你訂閱的不是某個人,而是某種「事件」——這個小小的轉念,撐起了半個現代軟體世界
先把定義釘死:一對多,而且互不相識
發布訂閱(publish-subscribe)系統,也叫分散式事件式系統,是本章所有間接通訊技術裡最廣泛使用的一種。
它的運作很簡單:發布者(publisher)把結構化的事件發布到事件服務,訂閱者(subscriber)透過訂閱表達對特定事件的興趣——訂閱可以是事件上任意的樣式。系統要做的事,就是把訂閱跟已發布的事件配對,再正確遞送通知(notification)。
關鍵的一句:一個事件可能同時遞送給很多訂閱者,所以發布訂閱本質上是一對多的通訊範式——這跟我們平常想的「我傳訊息給你」完全不同。
發布訂閱就像訂雜誌:出版社(發布者)出刊,你(訂閱者)只訂你想要的刊物,郵局(事件服務)負責把對的刊物送到對的人手上。你不必認識出版社,出版社也不必認識你——這就是空間解耦。
核心操作也很精簡:publish(e) 發布事件、subscribe(f) 用過濾器 f 表達興趣、unsubscribe(f) 取消訂閱、notify(e) 遞送通知;有些系統還會加上(選用的)advertise(f),讓發布者預先宣告自己將會產生哪種事件。
直接讀原文,旁邊就是白話
這是課本對發布訂閱最直球的定義,原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。
發布訂閱系統就是:發布者把結構化的事件發給事件服務,訂閱者則用訂閱來表達興趣——訂閱可以是套在這些事件結構上的任意樣式,不是只能訂「整個頻道」那麼粗。
這個系統唯一該做好的事,就是把訂閱和已發布的事件對上,並確保通知真的正確送達。
同一個事件常常要送給不只一個訂閱者,所以發布訂閱骨子裡就是「一對多」,跟你一對一傳訊息給朋友完全是兩種邏輯。
通知是發布者「非同步」送出的——送完就不管了,不必等訂閱者收到、也不必跟訂閱者對時間。這樣發布者跟訂閱者才能真正互不牽制,各過各的生活。
「訂閱可以是任意樣式」→ 系統要負責配對 → 一個事件配對到很多訂閱者 → 天生一對多;「發布者不等訂閱者」→ 非同步送出 → 兩邊才能真正解耦。這兩條線,是發布訂閱能撐起這麼多應用的根本原因。
招牌互動:一份事件,送給「對的」訂閱者
下面用課本的交易室系統為例:資訊提供者不斷收到外部行情,把每筆更新當事件發布;不同交易員只關心自己盯的股票。按「下一步」,看事件服務怎麼依訂閱條件,把同一個事件散播給對的人、擋掉不相關的人。
課本特別提醒:為了公平,所有訂閱同一支股票的交易員必須收到相同資訊——這暗示底層遞送機制不能只是「盡力而為」,而需要可靠多播(reliable multicast)撐著,才不會有交易員因為漏收一筆行情而吃虧。
兩個讓它特別好用的特性
發布訂閱系統有兩大特性,是它能被這麼廣泛使用的原因。
原本沒打算互通的元件,只要發布者公開自己會產生什麼事件、訂閱者訂閱樣式並提供處理通知的介面,就能一起工作。好比「小孩回家就自動開暖氣」——感測器和暖氣本來互不相干,靠事件就串起來了,誰也不用認識誰的內部長什麼樣。
通知是發布者非同步送出的,不必等訂閱者、也不用跟訂閱者對時間——兩者就此解耦。發布者可以専心發布,不必操心「訂閱者現在準備好了嗎」。
而訂閱時能表達的「興趣」有多細,取決於訂閱(過濾器)模型——由簡單到精細分成四種:頻道式(channel-based)訂具名頻道,收該頻道全部事件;主題式(topic-based)讓事件帶一個 topic 欄位,可以做成階層;內容式(content-based)能對事件的多個欄位值下查詢條件,表達力遠超前兩者;型別式(type-based)則依事件型別配對,能跟物件導向系統結合。
頻道式最原始,主題式把頻道「顯式化」還能做階層(訂 indirect_communication 收整章,訂 indirect_communication/publish-subscribe 只收更細的子主題——粗細自如);內容式能查多個欄位,型別式能跟物件系統掛鉤。表達力越強,通常代價就是實作越複雜——這條線會在下一屏的架構選擇裡再出現一次。
誰來負責配對與遞送?三種規模的答案
發布訂閱系統的核心任務,是把事件有效率地遞送給所有「過濾器與該事件相符」的訂閱者,同時還要兼顧安全、擴展性、失效處理——這讓實作相當複雜。實作策略,由簡到繁分成三種。
單一節點上的伺服器當事件代理(event broker),發布者把事件送給它,訂閱者把訂閱送給它並收回通知。就像全城只有一間總郵局——簡單好管,但一旦失火(單點失效)就全城停擺,尖峰時段還會大排長龍(效能瓶頸)。
把單一 broker 換成一張互相合作的 broker 網路。像各地都有郵局、彼此合作轉信——某間關門,信還能繞道別間,承載量也大得多,能在節點失效後存活。
不再區分發布者、訂閱者與 broker——所有節點都是 broker,合作完成事件路由。像鄰里互助傳信,沒有專職郵局,每戶人家都兼任轉信站——非常有彈性,適合超大規模,是近期系統很受歡迎的策略。
分散的難度也跟訂閱模型有關:頻道式/主題式的分散實作相對簡單——因為可以把頻道或主題直接對映到群組(group),借助底層多播遞送就好;內容式(推而廣之型別式)的分散實作則明顯更複雜,因為任意的查詢條件沒辦法簡單對映到固定群組——這正是下一屏「事件路由」要解決的問題。
事件路由:怎麼把消息送到對的人,又不浪費頻寬
發布訂閱系統的整體架構是分層的:最底層是各種網路協定(TCP/IP、IP multicast……),核心是事件路由(event routing),靠覆蓋網路(overlay)撐著;最頂層是配對(matching)——確保事件真的符合某條訂閱,這個工作常常被下推進路由機制裡一起做。事件路由要在「簡單」與「省訊息」之間取捨,課本給了三種策略。
把事件送給網路裡所有節點,配對留給訂閱端做(或反過來把訂閱氾流給所有可能的發布者)。好比在大樓裡每一戶都按門鈴喊一遍,誰有興趣自己出來——超簡單,但會製造大量無謂的網路流量。
每個 broker 維護鄰居清單、訂閱清單與路由表;訂閱資訊往發布者方向傳播,沿路存下「這條路通往某訂閱」。好比郵差沿路看路標,只往有人訂的方向轉送——比氾流省流量,但每個節點要多存一份路由表。
把所有可能事件視為一個事件空間,切給各 broker 節點分別負責。好比約在固定地標碰頭——事件與訂閱各自被導向負責的DHT節點,只要兩邊的節點集合有交集就能相遇配對。
純過濾式路由的麻煩是:訂閱要往所有可能發布者的方向傳播,本質上也是一種氾流。如果系統支援廣告(advertisement),讓發布者先宣告自己會發什麼事件,再對稱地把廣告往訂閱者方向傳播,就能大幅減輕這個負擔——有些系統甚至訂閱、廣告兩招並用。動態環境(節點常常上線下線)還有另一條路:informed gossip(帶內容的八卦式傳播),特別適合節點頻繁進出的環境。
小試身手
從「一對多」的定義,到三種架構,再到三種路由策略——來兩題檢查一下:
發布訂閱是一對多、送出去就不回頭。接下來看點對點、而且訊息會被「取走」的間接通訊——訊息佇列。往下捲。
先寄放、再取走:訊息佇列
訊息先寄放在信箱裡,收件人什麼時候上線再拿都行——這是時間解耦最純粹的樣子
跟前兩節長得像,骨子裡完全不同
群組通訊和發布訂閱,說到底都是一對多:一則訊息,同時送給一群訂閱者或成員。訊息佇列(message queue)反過來,走的是點對點(point-to-point)路線——用「佇列」當中介者,一樣達成空間與時間解耦,但一則訊息從頭到尾只服務一個人。
它是點對點的關鍵在這句:送方把訊息放進佇列,再由單一程序取走。誰先來取、什麼時候來取,都跟送方無關。
訊息佇列就像超商的取貨櫃:寄件人(producer)把包裹放進櫃子,放完就走——不必等收件人在場(時間解耦),也不必認識是哪位店員之後來取(空間解耦)。收件人(consumer)之後來把包裹取走,一個包裹只會被一個人取走,這就是點對點。
訊息佇列也稱訊息導向中介軟體(Message-Oriented Middleware),是重要的商業中介軟體類別,代表產品有 IBM WebSphere MQ、Microsoft MSMQ、Oracle AQ。主要用途是企業應用整合(EAI),也因為內建交易支援而廣泛用於交易處理系統。
直接讀原文:這段話定義了整節的骨架
下面這幾句英文,把訊息佇列跟前面兩節的差異、還有它為什麼算「間接通訊」,一次說清楚。原文放左邊,白話放右邊。
群組通訊和發布訂閱都是一對多;訊息佇列不同,它用「佇列」這個間接層走點對點服務,藉此達成空間與時間解耦。
點對點的意思是:送方把訊息放進佇列,之後由「單一」程序取走它。
訊息佇列系統最關鍵的性質是持久性——訊息會被無限期儲存(直到被取走為止),而且會寫入磁碟以確保可靠遞送。
差異在於:訊息傳遞系統的佇列是隱含在收發雙方身上的(例如 MPI 的訊息緩衝區);訊息佇列的佇列則是獨立於收發雙方之外的第三方實體。
第 4 章的訊息傳遞其實也有佇列(例如 MPI 的訊息緩衝區),但那個佇列綁在收發雙方身上,不算獨立第三方。訊息佇列的佇列是獨立於雙方之外的顯式第三方——就像超商的取貨櫃不屬於寄件人也不屬於收件人,才能真正做到「放完就走」。
招牌互動:訊息在佇列裡「過夜」也沒問題
按「下一步」,看一則訊息從 producer 出發,寄放進佇列,然後——即使 consumer 當時根本不在線上——它照樣安全地等在那裡,直到被取走。
持久性讓訊息佇列可以做出很強的承諾:任何送出的訊息終究會被收到(有效性),而且跟送出時一模一樣、不會被重複遞送(完整性)。但它從不承諾遞送的時間點——就像取貨櫃保證包裹一定在裡面等你,但沒說你什麼時候會去拿。
consumer 取信的三種姿勢
回到取貨櫃的比喻:你要怎麼知道包裹到了?課本給了三種 receive 風格,剛好對應三種生活中取貨的方式。
呼叫 receive 之後就傻等,一直等到有合適的訊息才回傳。像站在取貨櫃前不走,直到店員喊到你的包裹。
檢查一下佇列的狀態:有訊息就拿,沒有就回一句「不可用」,然後繼續做別的事。像每隔一陣子去晃一眼取貨櫃,有就拿、沒有就先走。
佇列裡一有合適的訊息,就主動發出事件通知給你。像留了電話號碼給店員,包裹到了他打給你,你才不用自己傻等或一直跑去看。
多個 producer 可以往同一佇列放訊息,多個 consumer 也可以從同一佇列取。預設順序是FIFO(先進先出),但多數實作也支援優先權——高優先權的訊息先取;consumer 甚至能依訊息的元資料挑選特定訊息,而不是照單全收。
訊息的長相,以及「誰在管佇列」
一則訊息含三部分:目的地(佇列識別碼)、元資料(如優先權、遞送模式),還有主體(body)。主體通常是不透明的——系統只負責搬運,不會偷看內容,可以用 marshalled 型別、物件序列化或 XML 序列化包裝。
再往下一層問:這個佇列究竟放在哪裡、由誰管?課本點出一個核心抉擇——集中式還是分散式。
一或多個佇列由某節點上的佇列管理員(queue manager)管理。優點是簡單,但管理員容易變得笨重,可能成為瓶頸或單點失效。
為克服集中式的弱點而生。以 IBM WebSphere MQ 為代表:一台實體伺服器上可以有多個佇列管理員,透過MQI(Message Queue Interface)存取——核心操作就是連線/斷線、送/收訊息,非常單純。
多數商業實作還把送或收包進交易(transaction),達成「全有或全無」;也支援訊息轉換(如 big-endian↔little-endian、SOAP↔IIOP),負責轉換的服務常稱message broker,是處理異質性、達成 EAI 的利器;WebSphere MQ 還能用 SSL 做機密傳輸與認證授權。
JMS:同一套 API,接得起兩種目的地
Java Messaging Service(JMS)是讓分散式 Java 程式間接通訊的標準規格。它最值得注意的一點:把發布訂閱與訊息佇列兩種範式(至少表面上)統一起來——用目的地(destination)當共同抽象,一個目的地不是 topic(主題)就是 queue(佇列)。
換句話說:JMS client 不用學兩套完全不同的 API,只要決定連的是 topic 還是 queue,剩下的程式寫法幾乎一樣——建立 connection、開 session、產生 producer/consumer。市面上實作很多,Joram、ActiveMQ、OpenJMS 都是;WebSphere MQ 也提供 JMS 介面。
這套統一有個限制:TopicConnection 只能開 topic session,QueueConnection 只能開 queue session——不能在同一條連線上混用。就像電話線路分成廣播線和專線兩種,接了哪一種就不能臨時換。所以說,JMS 對兩種範式的整合,是相當表面的統一,不是骨子裡合而為一。
小試身手
訊息佇列的核心就是「點對點+持久性」,加上 JMS 怎麼用同一套 API 撐起兩種範式。來兩題:
前四種範式——群組通訊、發布訂閱、訊息佇列,一路走下來——都是「透過訊息」溝通,只是把送方與收方解耦的方式不斷升級。最後一種範式,野心更大:連訊息長什麼樣子,都想幫你藏起來。往下捲。
共享記憶體的錯覺:DSM、元組空間與總結
把記憶體的「讀寫」錯覺搬到整個網路上,是這一章最後、也最奇特的一種解耦方式
前四種都在「傳訊息」,這一種假裝沒有訊息
群組通訊、發布訂閱、訊息佇列,說到底都是把訊息從一處送到另一處。這一節要介紹完全不同的思路:分散式共享記憶體(DSM)——它讓程式設計師感覺自己只是在讀寫一塊普通記憶體,完全不必知道底層其實在偷偷傳訊息。
DSM 最大的價值,是省去程式設計師處理訊息傳遞的麻煩——本來得自己 marshalling、收送訊息,現在直接讀寫共享變數即可。它特別適合平行運算,但通常不適合典型的 client-server 系統——因為 client 通常把伺服器資源當成抽象資料、以請求方式存取,這是基於模組化與保護的考量。
想像一群分處各地的同事,每人桌上都有一塊白板。任何人在自己白板上寫字,其他人的白板也會自動出現相同內容——你會以為大家共用「同一塊白板」,其實是各自有實體白板,背後有人偷偷把更新抄來抄去。那個「偷偷抄送」的動作,就是 DSM 底層仍然得靠的訊息傳遞。
直接讀原文,旁邊就是白話
這是本章對 DSM 最核心的定義段落,原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。
分散式共享記憶體(DSM)是一種抽象,用來讓不共享實體記憶體的電腦之間也能共享資料。
程序存取 DSM 的方式,就是讀取和更新自己位址空間裡「看起來像普通記憶體」的東西。
DSM 最重要的一點,是讓程式設計師不必操心訊息傳遞——換成訊息傳遞寫法本來會很麻煩的事,現在省掉了。
DSM 通常不太適合 client-server 系統,因為 client 習慣把伺服器手上的資源當成抽象資料,用「請求」方式存取。
既然沒有實體上共享的記憶體,DSM 的執行期支援底層還是得靠訊息,在電腦之間傳送更新。
DSM 只是把「訊息傳遞」這個真相,包裝成「讀寫記憶體」的錯覺,方便寫程式的人。機器之間該傳的訊息,一則都沒少——這正是它與前面三種範式最大的不同:不是不傳訊息,而是把傳訊息這件事藏起來。
藏起來的代價:你不知道一次讀寫要不要「打電話」
訊息傳遞中,所有遠端存取都是明確的,程式設計師永遠知道哪個操作要付通訊代價。DSM 中,任何一次讀或寫可能、也可能不觸發底層通訊,端看資料是否曾被存取、跨機共享樣式如何——這叫成本可見度(cost visibility)低。
你按一下開關(讀寫一個變數),有時只是點亮自己房間的燈(純本地存取),有時卻牽動了遠方一整座發電廠(觸發跨機通訊)——但兩個動作,從你手指按下去的那一刻,長得一模一樣。
訊息傳遞中,各程序有各自私有的位址空間,互不干擾;DSM 因為共享,一個程序不小心寫錯資料,就可能直接害到其他程序——這是拿保護換方便。
訊息傳遞時,marshalling 這一步就順手處理掉了資料表示的差異;DSM 底下,不同電腦的整數表示法要如何共享同一塊記憶體,反而成了一道難題。
實驗顯示,在少量電腦(約 10 台)時,DSM 程式可以跟功能等價的訊息傳遞程式效能相當——但這無法一般化,效能高度取決於資料共享的樣式(例如某個資料項是不是被多個程序同時更新)。是否該用 DSM 沒有定論,最終取決於它能被多有效率地實作。
招牌互動:元組空間——不報地址,只報特徵
DSM 是以位址存取「位元組」;元組空間(tuple space)則提供更高層次的視角:用內容樣式配對存取半結構化資料,這叫聯想式定址(associative addressing),也就是內容定址記憶體。
元組空間就像一個失物招領處,每件物品(元組)是一串有型別的欄位,例如 <"Capital", "Scotland", "Edinburgh">。點下面三個操作,看看它們分別在做什麼——尤其留意 read 和 take 都會阻塞,直到空間裡出現符合的元組為止。
你在失物招領處說「有人撿到藍色雨傘就通知我」,沒有就一直等——這正是 read/take 的阻塞行為,讓程序能藉此同步活動。而元組是不可變的——就像失物只能整件取走或整件放入新的一件,不能站在原地塗改它,要更新一個共享計數器,得先 take(<"counter", integer>) 取出舊值,再 write(<"counter", count+1>) 放回新值。
元組空間天生兩種解耦都占,但規模一大就有麻煩
元組空間的空間解耦來自「元組可以來自任意送方、被任意收方取得」;時間解耦則來自「元組放進空間後會留著(可能無限期),直到被移除為止」——送收方完全不必同時存在。兩者合起來,提供完全在空間與時間上都分散的共享。
但原始的 Linda 用的是單一全域元組空間。系統一大,就出現了「意外混名」的危險:元組一多,read/take 不小心配到錯的元組的機率就上升(尤其是用型別配對,像 take(<String, integer>) 這種寬鬆的樣式時)。後來的系統因此改用多重元組空間,甚至能動態建立,引入「作用域」來降低誤配。
JavaSpaces 把元組變成 entry 物件,一樣用 write、read、take 操作:配對時填入欄位值,就只配到那個值的 entry;欄位留空,則任何同型的 entry 都算符合。這也是元組空間影響力最大的後繼者之一。
整章總結:五種間接通訊,兩大家族
這一章從頭走到這裡,一共看過五種透過「中介者」溝通的範式——群組通訊(中介=群組)、發布訂閱(中介=頻道/主題)、訊息佇列(中介=佇列)、DSM(中介=共享記憶體)、元組空間(中介=元組空間)。它們的共性:都靠中介者間接溝通,因此都帶來「送收解耦」與「應付變化、容錯」的好處。
但可以分成兩大家族:通訊式(communication-based)——群組、發布訂閱、訊息佇列,重點是「傳訊息/傳事件」;狀態式(state-based)——DSM、元組空間,重點是「共享狀態」,像大家圍著一塊白板改來改去。
解耦:空間解耦;時間解耦「可能」(看實作)。可擴展性:有限(需維護成員管理)。主要用途:可靠分散式運算,強調排序與遞送保證。
解耦:空間解耦;時間解耦「可能」(看實作,如 JMS 持久事件)。可擴展性:可能高度擴展。主要用途:資訊散播/EAI,行動與普及運算。
解耦:空間解耦,且一定時間解耦(持久)。可擴展性:可能高度擴展。主要用途:點對點的資訊散播/EAI、商業交易處理。
解耦:空間解耦,且一定時間解耦(可持久)。可擴展性:有限(要維持一致的共享狀態視圖)。主要用途:平行與分散式運算。
解耦:空間解耦,且一定時間解耦(元組留存直到被取出)。可擴展性:有限(破壞性的 take 難以在大規模實作)。主要用途:平行與分散式運算、行動與普及運算;唯一與內容式發布訂閱並列支援聯想式定址。
狀態式範式擴展受限,根源是要在多讀多寫者間維持一致的共享狀態視圖。元組空間比較微妙:元組本身不可變,真正的瓶頸是破壞性的 take 操作在大規模系統裡難以實作。有趣的是:拿掉 take,元組空間看起來就很像發布訂閱系統——因此也就潛在地可以高度擴展。
小試身手
DSM、元組空間的操作、還有整章的解耦與擴展性總結,都到齊了。來兩題檢查一下:
回頭看整章:第一站我們認識了間接通訊的核心——多加一個中介者,讓送方與收方不必直接耦合,換來空間解耦與時間解耦的自由。接著群組通訊教我們一次送給一整群人;發布訂閱把「訂閱興趣」變成篩選訊息的方式;訊息佇列則把訊息穩穩地放進佇列裡,保證點對點、持久、可交易地送達。這一站,我們把「傳訊息」的思路整個翻過來,改用「共享狀態」——DSM 讓你錯覺自己只是在讀寫記憶體,元組空間更進一步,讓你用「內容特徵」而非「地址」去取資料。五種範式殊途同歸:都是為了讓分散式系統裡的元件,能在不知道對方是誰、不必同時存在的情況下,依然合作無間。
你現在手上有一張完整的地圖:五種間接通訊範式,各自在解耦程度、擴展性、適用場景上的取捨。書裡也老實承認,還有一個問題這一章沒有深談——當送收雙方被解耦到這個程度,像即時性、安全性這類端到端的性質該怎麼保證?這仍是留給後面章節、也留給你自己在真實系統裡持續思考的開放題。