01

多一個中介者:間接通訊的核心概念

多加一個中間人,反而讓兩端更自由——這是間接通訊反直覺卻極其重要的一課

前幾章的通訊,其實都「太直接」

前面談 RPC、socket 這類直接通訊,有一個共同的假設:送方明確知道收方是誰,兩邊直接連在一起。這很方便,卻也帶來僵化——換一台伺服器、伺服器當機,都會直接波及對方。

這一章要介紹另一條路:間接通訊(indirect communication)——分散式系統中的實體,透過一個中介者(intermediary)溝通,送方與收方之間沒有直接耦合。定義裡「收方」故意寫成可能複數,因為很多間接通訊範式天生就支援一對多。

🏢
先給你一個畫面:公司的前台收發室

你要寄文件給「採購部負責人」。直接通訊的話,你得知道那個人是誰、坐哪、現在在不在位子上,親手交給他——哪天他離職、換人、請假,你就卡住了。間接通訊則是把文件交給前台收發室,你根本不必認識負責人本人;就算換了新人,收發室也會把文件轉給「現任負責人」。

電腦科學有句名言,正是在講這件事:「All problems in computer science can be solved by another level of indirection.」(所有問題都能靠多加一層間接來解決。)收發室,就是那「多加的一層」。

直接讀原文,旁邊就是白話

這是本章對「間接通訊」最核心的定義段落。原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。

原文 · Distributed Systems Ch.6 Indirect communication is defined as communication between entities in a distributed system through an intermediary with no direct coupling between the sender and the receiver(s). Because of the direct coupling, it is more difficult to replace a server with an alternative one offering equivalent functionality. Similarly, if the server fails, this directly affects the client, which must explicitly deal with the failure. The main disadvantage is that there will inevitably be a performance overhead introduced by the added level of indirection. There is no performance problem that cannot be solved by eliminating a level of indirection.
白話翻譯

間接通訊的定義是:分散式系統中的實體,透過一個中介者溝通,送方與收方之間沒有直接耦合。

正因為是直接耦合,要把伺服器換成功能相同的另一台,會變得很麻煩。

同樣地,伺服器一旦當機,client 會直接被波及,還得自己想辦法處理這個失敗。

間接通訊最大的缺點是:多加這一層,必然會帶來一些效能上的額外負擔。

配對的另一句玩笑話:沒有什麼效能問題是「拿掉一層間接」解決不了的。

⚖️
記住這組對照名言

「加一層間接」解決彈性問題,「拿掉一層間接」解決效能問題——兩句話合起來,正是整個間接通訊要面對的權衡:多一層耦合(coupling)鬆綁帶來的自由,勢必要用一點延遲去換。

招牌互動:直接耦合 vs 間接通訊,同一個場景走兩遍

下面模擬同一件事:A 要把訊息交給 B,但 B 這時還沒上線。按「下一步」,先看「直接耦合」怎麼卡住,再看「間接通訊」怎麼靠中介者解套。

🖥️
A(直接耦合)
💤
B(尚未上線)
🖥️
A(間接通訊)
📮
中介者(會保存訊息)
🖥️
B(稍後才上線)
按「下一步」開始
🕳️
B 缺席,A 就得自己扛

直接耦合下,伺服器一旦失效或還沒上線,會直接波及對方,client 必須自己顯式處理這個失敗。間接通訊把這條連結拿掉之後,A 完全不需要知道 B 是誰、B 存不存在——這正是空間解耦(space uncoupling)時間解耦(time uncoupling)要處理的問題。

中介者帶來的兩大性質——但它們不一定同時出現

用了中介者之後,系統會出現兩個關鍵的「解耦(uncoupling)」性質。它們是兩件不同維度的事,不要當成同一件事的兩個名字。

🙈
空間解耦 Space uncoupling

送方不需要、也不用知道收方是誰,反之亦然。因為彼此不認識,開發者就有很大自由度應付變化:參與者可以被替換、更新、複製或遷移。

時間解耦 Time uncoupling

送方與收方可以擁有各自獨立的生命週期——不必同時存在也能溝通。這在易變環境裡特別寶貴:送方與收方常常來來去去。

常見的誤會是「間接=兩種解耦都有」,其實不然。最有名的反例是IP multicast:它是空間解耦,但時間耦合——訊息送往群組(不認識特定收方,空間解耦),但所有收方必須在送出當下都在線(時間耦合)。

📌
比喻:辦公室的留言板

門口有面留言板當中介者。你貼一張「冰箱裡的便當記得拿走」,你不必知道便當主人是誰(空間解耦);你早上九點貼、主人下午三點才看到,你們沒有同時存在也完成了溝通(時間解耦)。關鍵在於:便利貼會留在板子上,這就是持久性(persistency)——如果留言板是一塊「只在你站前面才顯示字、一走就熄」的螢幕,時間解耦就不成立了,這正是 IP multicast 的處境。

最容易搞混的一組概念:非同步 ≠ 時間解耦

這兩個詞長得很像,卻是不同維度。非同步通訊(asynchronous communication)解決的是「送方要不要等」的問題;時間解耦問的是「雙方要不要同時存在」——這是多了一個維度的問題。

📞
同步:打電話

你得等對方接、講完才能掛——送方被卡住,雙方必須同時在線。

💬
非同步:傳簡訊

你按下傳送就繼續滑手機,不必等他回。對方此刻仍得是個「存在的門號」,訊息才送得出去——非同步,卻仍是時間耦合。

📦
時間解耦:投進時空膠囊

寫一封信投進時空膠囊,要給「十年後住進這間房子的人」。收件人現在根本還不存在,但訊息會被保存到他出現——這才是真正的時間解耦。

⚠️
別把「改成非同步」當成「離線補收訊息」的解法

本章很多技術同時是「非同步且時間解耦」,容易讓人誤以為兩者一體。但若混淆,你可能誤以為「改成非同步就能讓離線使用者補收訊息」——其實那需要的是時間解耦(持久性),不是非同步。反過來,間接通訊也不必然等於非同步:JGroups 的 RpcDispatcher 就是在間接通訊之上做出同步呼叫——透過秘書幫你接通電話,你仍站著等對方回話。

什麼情況最該選間接通訊?

當系統預期會變動時,間接通訊特別好用——行動環境(使用者頻繁連上/斷開全球網路,收方來來去去)、事件散播(收方未知且常變動,例如金融系統的即時行情推播)、大型基礎設施(例如 Google 的關鍵元件就大量使用間接通訊)都是典型場景。

但天下沒有白吃的午餐:多繞一手,文件會晚一點到,效能成本是真實的;而且系統開發者也會更難管理與除錯,因為缺乏直接(空間或時間)耦合可循。

🎯
選用前先問自己兩個問題

我的系統會常常變動嗎?我能接受多一層延遲、多一點難以追蹤的代價嗎?如果兩個答案都是「是」,間接通訊值得考慮;如果系統穩定不變、追求極致效能,直接通訊往往更划算。

小試身手

把「間接通訊」「空間解耦」「時間解耦」「非同步」這幾個概念釐清了,來兩題檢查一下:

某團隊抱怨:每次更換後端伺服器,所有 client 都要改設定重連。改用間接通訊最能直接解決的痛點是什麼?
某聊天系統讓使用者離線時收不到任何訊息,上線後也看不到離線期間的訊息。從解耦角度看,它最可能缺少哪一種?
📮
下一站:群組通訊

中介者的概念懂了,第一個具體範式,是一次送給一整群人。往下捲。

02

一次送、全員收:群組通訊

跟一群人同時說同一句話,比想像中更難的地方在於:怎麼確定大家聽到的順序一樣

先搞懂一件事:群組通訊在解決什麼問題

上一個模組講到間接通訊的第一個實例,就是群組通訊(group communication)。它的核心概念很單純:有一個群組(group),程序可以加入(join)離開(leave)這個群組;送方對群組送一則訊息,系統就把它傳給所有成員,還附帶可靠性與排序的保證。整個過程中,送方完全不知道收方是誰——這正是空間解耦。

書裡先幫你把三種「送給誰」的範圍分清楚:

1
Unicast 單播

送給單一一個程序,最基本的一對一通訊。

2
Multicast 多播

送給群組裡的所有成員——這正是群組通訊實作的目標。

3
Broadcast 廣播

送給系統裡所有程序,而不只是某個子群組。

群組通訊的關鍵特徵:一個程序只需要發出一次 multicast 操作(在 Java 裡是 aGroup.send(aMessage)),就能把訊息送到一整群程序,而不是對每個成員各自呼叫一次 send。

🔌
一句話記住它的定位

群組通訊之於 IP multicast,就像 TCP 之於 IP 的點對點服務——群組通訊是多播的抽象,建在 IP multicast 或覆蓋網路之上,額外加上成員管理、失效偵測、可靠性與排序保證。

直接讀原文,旁邊就是白話

這是 6.2 節開場對群組通訊的定義,以及「單一 multicast 操作」為什麼不只是圖方便。原文放左邊,白話導讀放右邊。

原文 · DSCD Ch.6 Group communication offers a service whereby a message is sent to a group and then this message is delivered to all members of the group. In this action, the sender is not aware of the identities of the receivers. The essential feature of group communication is that a process issues only one multicast operation to send a message to each of a group of processes instead of issuing multiple send operations to individual processes. The use of a single multicast operation instead of multiple send operations amounts to much more than a convenience for the programmer: it enables the implementation to be efficient in its utilization of bandwidth. If the sender fails halfway through sending, then some members of the group may receive the message while others do not. In addition, the relative ordering of two messages delivered to any two group members is undefined.
白話翻譯

群組通訊提供的服務是:把訊息送到一個群組,系統就把它遞送給群組的所有成員。整個過程中,送方根本不知道收方是誰。

群組通訊最核心的特徵是:程序只需要發出一次 multicast 操作,就能把訊息送給一整群程序,不必對每個人各自呼叫一次 send。

「用一次操作取代多次個別送」聽起來只是程式寫起來方便,其實遠不只如此——它讓實作能有效利用頻寬。

如果改用多次個別送,送方半途掛掉,可能有些成員收到訊息、有些沒收到;而且送到任兩個成員的兩則訊息,先後順序完全不保證。

🔑
「一次操作」不是懶惰,是承諾

多次個別送,系統沒辦法對「這一群人」做出任何整體保證;一次 multicast 操作,才讓底層有機會保證「要嘛全員收到、要嘛都沒收到」,還能省頻寬——實作可以用散播樹(distribution tree)讓每條連線最多送一次,還能借用網路硬體本身的多播能力。

招牌互動:一次發送,全員收到;還有人半路加入、退出

下面是一個送方對群組發 multicast,三個成員同時收到;接著再示範群組成員的動態變化——有新成員加入,也有成員離開。按「下一步」照順序看。

📣
送方
🧑
成員一
🧑
成員二(後來離開)
🧑
成員三
🆕
新成員(中途加入)
按「下一步」開始
📋
成員名單不是一次寫死就好

群組成員會隨時加入、離開、甚至當機。送方只給一個群組識別碼,真正「這次要送給誰」是由位址展開(group address expansion)依「當下」的成員視圖動態決定的——這也是為什麼群組成員管理要跟遞送協調一致,否則有人半路加入或退出時,訊息可能送錯名單。

「可靠」多播,到底可靠在哪三件事上

一對一的可靠通訊靠兩個性質:完整性(integrity)——收到的跟送出的一樣,且不重複遞送;有效性(validity)——送出的訊息終究會被遞送。

但這是「一個收方」的保證。要擴展到「一整群收方」,還要再加上第三個性質:

🤝
一致同意(agreement)

如果訊息遞送給群組中任一個程序,就會遞送給群組中所有程序。換句話說,可靠多播追求的是「要嘛大家都收到、要嘛大家都沒收到」,就像全班一起及格、或一起補考,不會只有一半人拿到通知。

但光「可靠」還不夠。底層的 IPC 原語本來就不保證排序——如果 multicast 是拿一連串一對一訊息拼出來的,這些訊息可能遭遇任意延遲;就算用 IP multicast,也有類似問題。所以群組通訊還要另外提供有序多播(ordered multicast),程度分成三層。

動手配配看:三種排序保證,誰對應哪種情境?

把下面三種排序保證,拖到它最貼切的情境描述上。配完按「對答案」。

FIFO 排序(來源排序)
因果排序
全序
同一個送方先送出的訊息,會先被所有成員收到;但不同送方之間誰先誰後不保證
拖到這裡
聊天室裡「回覆」絕對不能比「原訊息」更早出現在任何人畫面上
拖到這裡
多份資料副本必須依同一連串更新,做出完全一致的最終狀態
拖到這裡

用比喻記住這三層嚴格度:FIFO 排序像是每個人照自己的時間軸講話,但你的第一句和我的第一句誰先到不一定;因果排序像是你回覆了我的話,大家一定先看到我的原話、才看到你的回覆;全序則像全班共用同一份逐字稿,所有人看到的順序一模一樣。

⚖️
保證愈強,代價愈高

排序與一致同意,本質上都是分散式系統裡「協調與一致同意」的例子。保證愈強,演算法愈複雜、對擴展性的負面衝擊也愈大——尤其是全序。三種保證可以單用,也可以混用,別一味追求最強,按應用真正需要的程度選就好;完整的演算法細節留待第 15 章。

群組服務還有幾個關鍵分類,決定它適合什麼場景

不同群組通訊服務的假設不一樣,其中最重要的兩組區分是:

🔒
封閉群組 Closed

只有成員能對群組多播,成員自己送出時也會收到。適合「合作的伺服器彼此私下傳訊息,只有它們該收到」。

🌐
開放群組 Open

群組外的程序也能往裡送。適合「把事件遞送給一群有興趣的程序」,例如通知訂閱者。

🔗
重疊 vs 非重疊

重疊:一個程序可同時屬於多個群組(現實系統通常如此)。非重疊:每個程序最多只屬於一個群組。

另外還有一組區分是程序群組物件群組:程序群組的訊息是未結構化的位元組陣列,層次類似 socket;物件群組(如 Electra)呼叫則會自動marshalling與 dispatch,client 透過本地 proxy 把整個群組視為單一物件,介面更高階但實務上較少用。

🧩
實務上,程序群組才是主流

像 JGroups 這類廣泛使用的工具箱,走的都是程序群組路線——層次低一點、貼近 socket,但夠泛用、夠成熟。物件群組雖然介面更高階,反而較少被真正採用。

誰在維護「現在群組裡有誰」這件事?

真實系統裡,程序會隨時加入、離開、甚至當機。群組成員管理的任務,就是隨時維持一份準確的當前成員視圖,具體有四件事:

1
提供成員變更介面

建立/銷毀群組、加入/移出程序。

2
失效偵測

監看成員是否當機或不可達,標記為 Suspected(疑似失效)或 Unsuspected,據此排除疑似失效者。

3
通知成員變更

有程序加入或被排除(失效或主動退出)時,通知其他成員。

4
群組位址展開

送方只給群組識別碼,由成員服務展開成當前成員清單來遞送。

📇
比喻:社團幹部的即時通訊錄

把成員服務想成社團裡那位盡責的幹部:有人入社退社,幹部更新名單;某人連續失聯,先標「疑似失聯」再確認除名;誰來誰走都會公告;你只要說「發給全社」,幹部就照當下名單一個個轉發,你不必自己列名單。IP multicast 其實只做了「一半的幹部工作」——能加入離開、也會位址展開,但不告訴你現在有誰,遞送也不與成員變更協調,要補齊這些得靠更進階的視圖同步(view-synchronous)群組通訊

📈
規模一大、變動一頻繁,成本就爆炸

維護一致成員視圖對「小規模、靜態」的系統最有效;但在大規模、或成員頻繁進出的環境裡會很吃力,因為每次成員變動都要重新達成一致視圖。突破的方向是用更機率性的成員管理(底層用 gossip 八卦協定),或是專為 ad hoc/行動環境設計的成員協定。

案例研究:JGroups 怎麼把這些保證做出來

JGroups 是一套可靠群組通訊工具箱,讓程序能加入/離開群組、送收訊息,並提供多種可靠性與排序保證。它的核心是三個元件:

📻
Channel 通道

給應用開發者最原始的介面,提供加入、離開、送、收四項核心功能。就像對講機的頻道把手——剛建立時是斷線狀態,connect 才綁到一個具名群組,一個 channel 一次只能連一個群組。

🧱
Building blocks 建構區塊

建在 channel 之上的更高階抽象,讓常見的使用模式不必每次都從頭手刻。

🔧
Protocol stack 協定堆疊

底層通訊協定,由一疊可組合的協定層構成,決定實際的可靠性與排序語意。

🧱
可組合的協定堆疊,是 JGroups 的真本事

協定堆疊像一疊可任意替換的樂高積木:常見五層有 UDP(傳輸層,用 IP multicast 送全群)、FRAG(訊息分段)、MERGE(處理網路分裂後重新合併)、GMS(群組成員協定,維持一致成員視圖)、CAUSAL(實作因果排序)。因為所有層都實作相同的 up/down 介面,可以任意順序組合——需要 FIFO、全序、加密、流量控制?換上對應的層即可。但有一條鐵律:同一群組的所有成員必須共用相同的協定堆疊,否則大家對訊息的處理方式就不一致了。

小試身手

從「一次 multicast 操作」到「排序保證」,抓住這幾個關鍵點,你就摸到群組通訊的骨架了。來兩題:

為什麼說「用單一多播操作」不只是方便,還更可靠?
聊天室裡「回覆」絕對不能比「原訊息」更早出現在任何人畫面上。最適合的排序保證是?
📡
下一站:發布訂閱系統

群組通訊要事先知道「群組是誰」——送方至少得知道要送到哪個群組。接下來看一種完全不用知道對方是誰的範式。往下捲。

03

訂閱的是事件,不是人:發布訂閱系統

你訂閱的不是某個人,而是某種「事件」——這個小小的轉念,撐起了半個現代軟體世界

先把定義釘死:一對多,而且互不相識

發布訂閱(publish-subscribe)系統,也叫分散式事件式系統,是本章所有間接通訊技術裡最廣泛使用的一種。

它的運作很簡單:發布者(publisher)把結構化的事件發布到事件服務,訂閱者(subscriber)透過訂閱表達對特定事件的興趣——訂閱可以是事件上任意的樣式。系統要做的事,就是把訂閱跟已發布的事件配對,再正確遞送通知(notification)

關鍵的一句:一個事件可能同時遞送給很多訂閱者,所以發布訂閱本質上是一對多的通訊範式——這跟我們平常想的「我傳訊息給你」完全不同。

📰
先給你一個畫面:訂雜誌

發布訂閱就像訂雜誌:出版社(發布者)出刊,你(訂閱者)只訂你想要的刊物,郵局(事件服務)負責把對的刊物送到對的人手上。你不必認識出版社,出版社也不必認識你——這就是空間解耦

核心操作也很精簡:publish(e) 發布事件、subscribe(f) 用過濾器 f 表達興趣、unsubscribe(f) 取消訂閱、notify(e) 遞送通知;有些系統還會加上(選用的)advertise(f),讓發布者預先宣告自己將會產生哪種事件。

直接讀原文,旁邊就是白話

這是課本對發布訂閱最直球的定義,原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。

原文 · DSCD Ch.6 A publish-subscribe system is a system where publishers publish structured events to an event service and subscribers express interest in particular events through subscriptions which can be arbitrary patterns over the structured events. The task of the publish-subscribe system is to match subscriptions against published events and ensure the correct delivery of event notifications. A given event will be delivered to potentially many subscribers, and hence publish-subscribe is fundamentally a one-to-many communications paradigm. Notifications are sent asynchronously by event-generating publishers to all the subscribers that have expressed an interest in them to prevent publishers needing to synchronize with subscribers – publishers and subscribers need to be decoupled.
白話翻譯

發布訂閱系統就是:發布者把結構化的事件發給事件服務,訂閱者則用訂閱來表達興趣——訂閱可以是套在這些事件結構上的任意樣式,不是只能訂「整個頻道」那麼粗。

這個系統唯一該做好的事,就是把訂閱和已發布的事件對上,並確保通知真的正確送達。

同一個事件常常要送給不只一個訂閱者,所以發布訂閱骨子裡就是「一對多」,跟你一對一傳訊息給朋友完全是兩種邏輯。

通知是發布者「非同步」送出的——送完就不管了,不必等訂閱者收到、也不必跟訂閱者對時間。這樣發布者跟訂閱者才能真正互不牽制,各過各的生活。

🔑
記住這句因果鏈

「訂閱可以是任意樣式」→ 系統要負責配對 → 一個事件配對到很多訂閱者 → 天生一對多;「發布者不等訂閱者」→ 非同步送出 → 兩邊才能真正解耦。這兩條線,是發布訂閱能撐起這麼多應用的根本原因。

招牌互動:一份事件,送給「對的」訂閱者

下面用課本的交易室系統為例:資訊提供者不斷收到外部行情,把每筆更新當事件發布;不同交易員只關心自己盯的股票。按「下一步」,看事件服務怎麼依訂閱條件,把同一個事件散播給對的人、擋掉不相關的人。

📡
資訊提供者
🗂️
事件服務(broker)
💹
交易員A(訂科技股)
🛢️
交易員B(訂原物料股)
💹
交易員C(也訂科技股)
按「下一步」開始
⚖️
公平性藏在細節裡

課本特別提醒:為了公平,所有訂閱同一支股票的交易員必須收到相同資訊——這暗示底層遞送機制不能只是「盡力而為」,而需要可靠多播(reliable multicast)撐著,才不會有交易員因為漏收一筆行情而吃虧。

兩個讓它特別好用的特性

發布訂閱系統有兩大特性,是它能被這麼廣泛使用的原因。

1
異質性(heterogeneity)

原本沒打算互通的元件,只要發布者公開自己會產生什麼事件、訂閱者訂閱樣式並提供處理通知的介面,就能一起工作。好比「小孩回家就自動開暖氣」——感測器和暖氣本來互不相干,靠事件就串起來了,誰也不用認識誰的內部長什麼樣。

2
非同步性(asynchronicity)

通知是發布者非同步送出的,不必等訂閱者、也不用跟訂閱者對時間——兩者就此解耦。發布者可以専心發布,不必操心「訂閱者現在準備好了嗎」。

而訂閱時能表達的「興趣」有多細,取決於訂閱(過濾器)模型——由簡單到精細分成四種:頻道式(channel-based)訂具名頻道,收該頻道全部事件;主題式(topic-based)讓事件帶一個 topic 欄位,可以做成階層;內容式(content-based)能對事件的多個欄位值下查詢條件,表達力遠超前兩者;型別式(type-based)則依事件型別配對,能跟物件導向系統結合。

📶
表達能力,是一路遞增的

頻道式最原始,主題式把頻道「顯式化」還能做階層(訂 indirect_communication 收整章,訂 indirect_communication/publish-subscribe 只收更細的子主題——粗細自如);內容式能查多個欄位,型別式能跟物件系統掛鉤。表達力越強,通常代價就是實作越複雜——這條線會在下一屏的架構選擇裡再出現一次。

誰來負責配對與遞送?三種規模的答案

發布訂閱系統的核心任務,是把事件有效率地遞送給所有「過濾器與該事件相符」的訂閱者,同時還要兼顧安全、擴展性、失效處理——這讓實作相當複雜。實作策略,由簡到繁分成三種。

🏢
集中式 Centralized

單一節點上的伺服器當事件代理(event broker),發布者把事件送給它,訂閱者把訂閱送給它並收回通知。就像全城只有一間總郵局——簡單好管,但一旦失火(單點失效)就全城停擺,尖峰時段還會大排長龍(效能瓶頸)。

🕸️
分散式 Distributed

把單一 broker 換成一張互相合作的 broker 網路。像各地都有郵局、彼此合作轉信——某間關門,信還能繞道別間,承載量也大得多,能在節點失效後存活。

🤝
完全 P2P Peer-to-peer

不再區分發布者、訂閱者與 broker——所有節點都是 broker,合作完成事件路由。像鄰里互助傳信,沒有專職郵局,每戶人家都兼任轉信站——非常有彈性,適合超大規模,是近期系統很受歡迎的策略。

分散的難度也跟訂閱模型有關:頻道式/主題式的分散實作相對簡單——因為可以把頻道或主題直接對映到群組(group),借助底層多播遞送就好;內容式(推而廣之型別式)的分散實作則明顯更複雜,因為任意的查詢條件沒辦法簡單對映到固定群組——這正是下一屏「事件路由」要解決的問題。

事件路由:怎麼把消息送到對的人,又不浪費頻寬

發布訂閱系統的整體架構是分層的:最底層是各種網路協定(TCP/IP、IP multicast……),核心是事件路由(event routing),靠覆蓋網路(overlay)撐著;最頂層是配對(matching)——確保事件真的符合某條訂閱,這個工作常常被下推進路由機制裡一起做。事件路由要在「簡單」與「省訊息」之間取捨,課本給了三種策略。

📢
氾流 Flooding

把事件送給網路裡所有節點,配對留給訂閱端做(或反過來把訂閱氾流給所有可能的發布者)。好比在大樓裡每一戶都按門鈴喊一遍,誰有興趣自己出來——超簡單,但會製造大量無謂的網路流量。

🪧
過濾式 Filtering-based

每個 broker 維護鄰居清單、訂閱清單與路由表;訂閱資訊往發布者方向傳播,沿路存下「這條路通往某訂閱」。好比郵差沿路看路標,只往有人訂的方向轉送——比氾流省流量,但每個節點要多存一份路由表。

🎯
會合式 Rendezvous

把所有可能事件視為一個事件空間,切給各 broker 節點分別負責。好比約在固定地標碰頭——事件與訂閱各自被導向負責的DHT節點,只要兩邊的節點集合有交集就能相遇配對。

📣
用「廣告」幫過濾式省流量

純過濾式路由的麻煩是:訂閱要往所有可能發布者的方向傳播,本質上也是一種氾流。如果系統支援廣告(advertisement),讓發布者先宣告自己會發什麼事件,再對稱地把廣告往訂閱者方向傳播,就能大幅減輕這個負擔——有些系統甚至訂閱、廣告兩招並用。動態環境(節點常常上線下線)還有另一條路:informed gossip(帶內容的八卦式傳播),特別適合節點頻繁進出的環境。

小試身手

從「一對多」的定義,到三種架構,再到三種路由策略——來兩題檢查一下:

集中式發布訂閱(單一 event broker)最主要的兩個弱點是什麼?
過濾式路由(filtering-based routing)是如何減少網路流量的?
📬
下一站:訊息佇列

發布訂閱是一對多、送出去就不回頭。接下來看點對點、而且訊息會被「取走」的間接通訊——訊息佇列。往下捲。

04

先寄放、再取走:訊息佇列

訊息先寄放在信箱裡,收件人什麼時候上線再拿都行——這是時間解耦最純粹的樣子

跟前兩節長得像,骨子裡完全不同

群組通訊和發布訂閱,說到底都是一對多:一則訊息,同時送給一群訂閱者或成員。訊息佇列(message queue)反過來,走的是點對點(point-to-point)路線——用「佇列」當中介者,一樣達成空間與時間解耦,但一則訊息從頭到尾只服務一個人。

它是點對點的關鍵在這句:送方把訊息放進佇列,再由單一程序取走。誰先來取、什麼時候來取,都跟送方無關。

📦
先給你一個畫面:超商的取貨櫃

訊息佇列就像超商的取貨櫃:寄件人(producer)把包裹放進櫃子,放完就走——不必等收件人在場(時間解耦),也不必認識是哪位店員之後來取(空間解耦)。收件人(consumer)之後來把包裹取走,一個包裹只會被一個人取走,這就是點對點。

訊息佇列也稱訊息導向中介軟體(Message-Oriented Middleware),是重要的商業中介軟體類別,代表產品有 IBM WebSphere MQ、Microsoft MSMQ、Oracle AQ。主要用途是企業應用整合(EAI),也因為內建交易支援而廣泛用於交易處理系統。

直接讀原文:這段話定義了整節的骨架

下面這幾句英文,把訊息佇列跟前面兩節的差異、還有它為什麼算「間接通訊」,一次說清楚。原文放左邊,白話放右邊。

原文 · Distributed Systems Ch.6 Whereas groups and publish-subscribe provide a one-to-many style of communication, message queues provide a point-to-point service using the concept of a message queue as an indirection, thus achieving the desired properties of space and time uncoupling. They are point-to-point in that the sender places the message into a queue, and it is then removed by a single process. One crucial property of message queue systems is that messages are persistent – that is, message queues will store the messages indefinitely (until they are consumed) and will also commit the messages to disk to enable reliable delivery. The difference is that whereas message-passing systems have implicit queues associated with senders and receivers (for example, the message buffers in MPI), message queuing systems have explicit queues that are third-party entities, separate from the sender and the receiver.
白話翻譯

群組通訊和發布訂閱都是一對多;訊息佇列不同,它用「佇列」這個間接層走點對點服務,藉此達成空間與時間解耦。

點對點的意思是:送方把訊息放進佇列,之後由「單一」程序取走它。

訊息佇列系統最關鍵的性質是持久性——訊息會被無限期儲存(直到被取走為止),而且會寫入磁碟以確保可靠遞送。

差異在於:訊息傳遞系統的佇列是隱含在收發雙方身上的(例如 MPI 的訊息緩衝區);訊息佇列的佇列則是獨立於收發雙方之外的第三方實體。

🔑
「隱含」跟「顯式」的佇列,差在哪?

第 4 章的訊息傳遞其實也有佇列(例如 MPI 的訊息緩衝區),但那個佇列綁在收發雙方身上,不算獨立第三方。訊息佇列的佇列是獨立於雙方之外的顯式第三方——就像超商的取貨櫃不屬於寄件人也不屬於收件人,才能真正做到「放完就走」。

招牌互動:訊息在佇列裡「過夜」也沒問題

按「下一步」,看一則訊息從 producer 出發,寄放進佇列,然後——即使 consumer 當時根本不在線上——它照樣安全地等在那裡,直到被取走。

📤
Producer
📮
訊息佇列(持久保存)
📥
Consumer(之後才上線)
按「下一步」開始
🕓
保證「會到」,不保證「幾點到」

持久性讓訊息佇列可以做出很強的承諾:任何送出的訊息終究會被收到(有效性),而且跟送出時一模一樣、不會被重複遞送(完整性)。但它從不承諾遞送的時間點——就像取貨櫃保證包裹一定在裡面等你,但沒說你什麼時候會去拿。

consumer 取信的三種姿勢

回到取貨櫃的比喻:你要怎麼知道包裹到了?課本給了三種 receive 風格,剛好對應三種生活中取貨的方式。

阻塞式 Receive

呼叫 receive 之後就傻等,一直等到有合適的訊息才回傳。像站在取貨櫃前不走,直到店員喊到你的包裹。

🔁
非阻塞式 Receive(輪詢)

檢查一下佇列的狀態:有訊息就拿,沒有就回一句「不可用」,然後繼續做別的事。像每隔一陣子去晃一眼取貨櫃,有就拿、沒有就先走。

🔔
Notify 操作

佇列裡一有合適的訊息,就主動發出事件通知給你。像留了電話號碼給店員,包裹到了他打給你,你才不用自己傻等或一直跑去看。

🗄️
多人共用同一個櫃子,順序怎麼排?

多個 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 撐起兩種範式。來兩題:

訊息佇列與發布訂閱在「通訊模式」上最根本的差異是?
JMS 的 message selector 能對訊息的哪些部分做過濾?
🧩
下一站:共享記憶體取向

前四種範式——群組通訊、發布訂閱、訊息佇列,一路走下來——都是「透過訊息」溝通,只是把送方與收方解耦的方式不斷升級。最後一種範式,野心更大:連訊息長什麼樣子,都想幫你藏起來。往下捲。

05

共享記憶體的錯覺:DSM、元組空間與總結

把記憶體的「讀寫」錯覺搬到整個網路上,是這一章最後、也最奇特的一種解耦方式

前四種都在「傳訊息」,這一種假裝沒有訊息

群組通訊、發布訂閱、訊息佇列,說到底都是把訊息從一處送到另一處。這一節要介紹完全不同的思路:分散式共享記憶體(DSM)——它讓程式設計師感覺自己只是在讀寫一塊普通記憶體,完全不必知道底層其實在偷偷傳訊息。

DSM 最大的價值,是省去程式設計師處理訊息傳遞的麻煩——本來得自己 marshalling、收送訊息,現在直接讀寫共享變數即可。它特別適合平行運算,但通常不適合典型的 client-server 系統——因為 client 通常把伺服器資源當成抽象資料、以請求方式存取,這是基於模組化與保護的考量。

🖊️
先給你一個畫面:每人桌上的「同步白板」

想像一群分處各地的同事,每人桌上都有一塊白板。任何人在自己白板上寫字,其他人的白板也會自動出現相同內容——你會以為大家共用「同一塊白板」,其實是各自有實體白板,背後有人偷偷把更新抄來抄去。那個「偷偷抄送」的動作,就是 DSM 底層仍然得靠的訊息傳遞

直接讀原文,旁邊就是白話

這是本章對 DSM 最核心的定義段落,原文放左邊,白話導讀放右邊——先讀懂原文的氣口,再看整段在說什麼。

原文 · Distributed Systems Ch.6 Distributed shared memory (DSM) is an abstraction used for sharing data between computers that do not share physical memory. Processes access DSM by reads and updates to what appears to be ordinary memory within their address space. The main point of DSM is that it spares the programmer the concerns of message passing when writing applications that might otherwise have to use it. DSM is in general less appropriate in client-server systems, where clients normally view server-held resources as abstract data and access them by request. In the absence of physically shared memory, the DSM runtime support has to send updates in messages between computers.
白話翻譯

分散式共享記憶體(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">。點下面三個操作,看看它們分別在做什麼——尤其留意 readtake 都會阻塞,直到空間裡出現符合的元組為止。

write(寫入)
read(讀取,不移除)
take(取出,會移除)
點擊上方操作查看說明
☂️
比喻:「有人撿到藍色雨傘就通知我」

你在失物招領處說「有人撿到藍色雨傘就通知我」,沒有就一直等——這正是 read/take 的阻塞行為,讓程序能藉此同步活動。而元組是不可變的——就像失物只能整件取走或整件放入新的一件,不能站在原地塗改它,要更新一個共享計數器,得先 take(<"counter", integer>) 取出舊值,再 write(<"counter", count+1>) 放回新值。

元組空間天生兩種解耦都占,但規模一大就有麻煩

元組空間的空間解耦來自「元組可以來自任意送方、被任意收方取得」;時間解耦則來自「元組放進空間後會留著(可能無限期),直到被移除為止」——送收方完全不必同時存在。兩者合起來,提供完全在空間與時間上都分散的共享。

但原始的 Linda 用的是單一全域元組空間。系統一大,就出現了「意外混名」的危險:元組一多,readtake 不小心配到錯的元組的機率就上升(尤其是用型別配對,像 take(<String, integer>) 這種寬鬆的樣式時)。後來的系統因此改用多重元組空間,甚至能動態建立,引入「作用域」來降低誤配。

🏷️
案例:JavaSpaces 把元組帶進物件導向世界

JavaSpaces 把元組變成 entry 物件,一樣用 write、read、take 操作:配對時填入欄位值,就只配到那個值的 entry;欄位留空,則任何同型的 entry 都算符合。這也是元組空間影響力最大的後繼者之一。

整章總結:五種間接通訊,兩大家族

這一章從頭走到這裡,一共看過五種透過「中介者」溝通的範式——群組通訊(中介=群組)、發布訂閱(中介=頻道/主題)、訊息佇列(中介=佇列)、DSM(中介=共享記憶體)、元組空間(中介=元組空間)。它們的共性:都靠中介者間接溝通,因此都帶來「送收解耦」與「應付變化、容錯」的好處。

但可以分成兩大家族:通訊式(communication-based)——群組、發布訂閱、訊息佇列,重點是「傳訊息/傳事件」;狀態式(state-based)——DSM、元組空間,重點是「共享狀態」,像大家圍著一塊白板改來改去。

👥
群組通訊

解耦:空間解耦;時間解耦「可能」(看實作)。可擴展性:有限(需維護成員管理)。主要用途:可靠分散式運算,強調排序與遞送保證。

📰
發布訂閱

解耦:空間解耦;時間解耦「可能」(看實作,如 JMS 持久事件)。可擴展性:可能高度擴展。主要用途:資訊散播/EAI,行動與普及運算。

📬
訊息佇列

解耦:空間解耦,且一定時間解耦(持久)。可擴展性:可能高度擴展。主要用途:點對點的資訊散播/EAI、商業交易處理。

🖊️
DSM

解耦:空間解耦,且一定時間解耦(可持久)。可擴展性:有限(要維持一致的共享狀態視圖)。主要用途:平行與分散式運算。

🗂️
元組空間

解耦:空間解耦,且一定時間解耦(元組留存直到被取出)。可擴展性:有限(破壞性的 take 難以在大規模實作)。主要用途:平行與分散式運算、行動與普及運算;唯一與內容式發布訂閱並列支援聯想式定址。

🔍
一個有趣的觀察:拿掉 take,元組空間就很像發布訂閱

狀態式範式擴展受限,根源是要在多讀多寫者間維持一致的共享狀態視圖。元組空間比較微妙:元組本身不可變,真正的瓶頸是破壞性的 take 操作在大規模系統裡難以實作。有趣的是:拿掉 take,元組空間看起來就很像發布訂閱系統——因此也就潛在地可以高度擴展。

小試身手

DSM、元組空間的操作、還有整章的解耦與擴展性總結,都到齊了。來兩題檢查一下:

DSM 為程式設計師帶來的主要好處是什麼?
書中有個有趣觀察:元組空間若拿掉某個操作,就會變得很像發布訂閱(因此潛在可高度擴展)。是哪個操作?
🧭
本章總結:一條「多一層間接」的主線,串起五種範式

回頭看整章:第一站我們認識了間接通訊的核心——多加一個中介者,讓送方與收方不必直接耦合,換來空間解耦與時間解耦的自由。接著群組通訊教我們一次送給一整群人;發布訂閱把「訂閱興趣」變成篩選訊息的方式;訊息佇列則把訊息穩穩地放進佇列裡,保證點對點、持久、可交易地送達。這一站,我們把「傳訊息」的思路整個翻過來,改用「共享狀態」——DSM 讓你錯覺自己只是在讀寫記憶體,元組空間更進一步,讓你用「內容特徵」而非「地址」去取資料。五種範式殊途同歸:都是為了讓分散式系統裡的元件,能在不知道對方是誰、不必同時存在的情況下,依然合作無間。

🌅
這一站,也是整章的終點

你現在手上有一張完整的地圖:五種間接通訊範式,各自在解耦程度、擴展性、適用場景上的取捨。書裡也老實承認,還有一個問題這一章沒有深談——當送收雙方被解耦到這個程度,像即時性、安全性這類端到端的性質該怎麼保證?這仍是留給後面章節、也留給你自己在真實系統裡持續思考的開放題。