遠端程序呼叫與遠端物件呼叫有助於隱藏通訊、提升存取透明性,但兩者並非永遠合適。當無法假設接收方在請求發出時正在執行,就需要別的通訊服務;同樣地,RPC「客戶端阻塞到請求處理完畢」的先天同步性質,有時也需要被取代。答案就是訊息傳遞(messaging)。本節先討論假設雙方都在執行的訊息系統,再檢視允許對方不在線的訊息佇列系統。

訊息導向的暫時性通訊#

許多分散式系統與應用直接建立在傳輸層提供的簡單訊息導向模型之上。要理解訊息導向系統在中介軟體方案中的角色,先看傳輸層 socket 的訊息傳遞。

Berkeley Sockets#

傳輸層介面的標準化,讓程式設計師能透過一組簡單的原語使用完整的(訊息)協定套組,也讓應用更容易移植到不同機器。這裡以 1970 年代 Berkeley UNIX 引入的 socket 介面為例(另一個重要介面是 AT&T 開發的 XTI,兩者的網路程式設計模型非常相似,只是原語集不同)。

概念上,socket 是一個通訊端點:應用可以把要送上網路的資料寫進去、把進來的資料讀出來。socket 是對本地作業系統為特定傳輸協定所用的實際通訊端點的抽象。以下聚焦 TCP 的 socket 原語:

  • socket:建立新的通訊端點。作業系統為指定的傳輸協定保留收發訊息所需的資源。
  • bind:把本地位址綁到剛建立的 socket。例如伺服器把機器的 IP 位址加上(可能是眾所周知的)連接埠號綁上去,告訴作業系統它只想在這個位址與連接埠上收訊息。
  • listen:僅用於連線導向通訊。非阻塞呼叫,讓作業系統為呼叫者願意接受的最大連線數保留足夠緩衝區。
  • accept:阻塞等待連線請求。請求到達時,作業系統建立一個與原 socket 性質相同的新 socket 回傳給呼叫者——伺服器可以 fork 一個行程用新 socket 處理實際通訊,自己回頭在原 socket 上等下一個連線請求。
  • connect:客戶端指定要連線的傳輸層位址並送出連線請求,阻塞到連線成功建立為止。客戶端也要先建 socket,但不必明確 bind——連線建立時作業系統會動態分配連接埠。
  • send / receive:連線建立後雙方交換資訊。
  • close:關閉連線。使用 socket 時關閉是對稱的:客戶端與伺服器都要呼叫 close

圖 4-14:TCP/IP 的 socket 原語

伺服器通常依序執行 socketbindlistenaccept,客戶端則是 socketconnect,之後雙方以 sendreceive 往來,最後各自 close

圖 4-15:使用 socket 的連線導向通訊模式

訊息傳遞介面(MPI)#

高效能多電腦(multicomputer)興起後,開發者需要方便又低開銷的訊息導向原語。socket 被認為不足,原因有二:

  • 抽象層次不對——只支援簡單的 send 與 receive。
  • socket 是為了用 TCP/IP 這類通用協定堆疊跨網路通訊而設計,不適合高效能伺服器叢集所用高速互連網路的專屬協定;這些協定需要能處理進階緩衝與同步形式的介面。

結果是各家互連網路與高效能多電腦都附帶自己的專屬通訊函式庫,彼此互不相容,造成可攜性問題。對硬體與平台無關的需求,最終催生了訊息傳遞的標準:訊息傳遞介面(Message-Passing Interface, MPI)

MPI 為平行應用設計,因此以暫時通訊為主,直接利用底層網路;它假設行程當機、網路分割等嚴重故障是致命的,不需要自動復原。MPI 假設通訊發生在已知的行程群組內:每個群組有識別碼,群組內每個行程也有(區域)識別碼,(groupID, processID) 配對即可唯一指出訊息的來源或目的地,取代傳輸層位址。可能同時有多個(可重疊的)行程群組參與運算。

MPI 核心的暫時通訊原語中,最直觀的幾個如下:

  • MPI_bsend暫時非同步通訊。訊息通常先複製到發送端 MPI 執行期系統的本地緩衝區,複製完成後發送者就繼續執行;MPI 執行期系統在接收方呼叫接收原語時負責送出。
  • MPI_send:阻塞式送出,語意依實作而定——可能阻塞到訊息複製進發送端的 MPI 執行期系統為止,也可能阻塞到接收方發起接收操作為止。
  • MPI_ssend同步通訊,發送者阻塞到請求被接受、開始處理為止。
  • MPI_sendrecv:最強的同步形式——送出請求並阻塞到接收方回覆為止,基本上等同一次普通的 RPC。
  • MPI_isendMPI_send 的變體,避免把訊息從使用者緩衝區複製到 MPI 執行期系統的內部緩衝區——發送者只傳指標給執行期系統就立刻繼續。為避免訊息在通訊完成前被覆寫,MPI 提供檢查完成與(必要時)阻塞的原語。訊息究竟已送達接收方、還是只被本地執行期系統複製,同樣未定義。
  • MPI_issendMPI_ssend 的對應變體,也只傳指標;當執行期系統表示已處理該訊息時,發送者可確定接收方已接受訊息並正在處理。
  • MPI_recv:接收訊息,阻塞到訊息到達為止。
  • MPI_irecv:非同步接收——接收者宣告自己準備好接受訊息,之後可以查詢訊息是否確實到達,或阻塞等待。

圖 4-16:MPI 中最直觀的幾個訊息傳遞原語

MPI 通訊原語的語意並不總是直觀,有時不同原語可以互換而不影響程式正確性。官方說法是:提供這麼多種通訊形式,是為了給 MPI 實作者足夠的效能最佳化空間;譏諷者則說是委員會下不了決心,索性全部塞進去。從 MPI 為高效能平行應用而設計的角度看,這種原語的多樣性就不難理解。MPI 全部函式超過一百個。

訊息導向的持久性通訊#

接下來是一類重要的訊息導向中介軟體服務,通稱訊息佇列系統(message-queuing systems)訊息導向中介軟體(Message-Oriented Middleware, MOM)。它們對持久非同步通訊提供完整支援:本質是提供訊息的中期儲存能力,發送者與接收者都不必在訊息傳輸期間保持活躍。與 socket 和 MPI 的重要差異在於:訊息佇列系統設定的傳輸時間尺度是分鐘級,而非秒或毫秒級。

訊息佇列模型#

基本想法:應用之間透過把訊息放進特定佇列來通訊。訊息經過一連串通訊伺服器轉送,最終送達目的地——即使發送當下目的端是關閉的(實務上多數通訊伺服器彼此直連,訊息通常直接送到目的伺服器)。原則上每個應用有自己的私有佇列供其他應用投遞;佇列只有所屬應用能讀,但多個應用共用一個佇列也是可能的。

發送者得到的保證通常只有一條:訊息終將被放入接收者的佇列。至於何時被讀、甚至會不會被讀,完全取決於接收者的行為,系統不做任何保證。

這種語意造就了時間上的鬆散耦合:接收者不必在訊息送進佇列時執行,發送者也不必在訊息被取走時執行。依收發雙方是否在執行,有四種組合:

  • 發送者與接收者都在執行:訊息全程雙方在線。
  • 只有發送者在執行:接收者處於無法接收的被動狀態,發送者仍可照送。
  • 只有接收者在執行:接收者可以讀取先前送給它的訊息,發送者不必在線。
  • 兩者都不在執行:系統仍持續保存(甚至傳輸)訊息。訊息一旦入列,就會留在那裡直到被取走,與收發雙方是否執行無關。

圖 4-17:使用佇列進行鬆散耦合通訊的四種組合

訊息原則上可含任何資料;中介軟體唯一在意的是訊息有正確定址——實務上以全系統唯一的目的佇列名稱定址。訊息大小可能受限,但底層系統也可能對應用完全透明地切割與重組大訊息。因此提供給應用的基本介面可以極為簡單:

  • put:發送者把訊息交給底層系統,附加到指定佇列。非阻塞呼叫。
  • get:阻塞呼叫——佇列為空時阻塞;否則由獲授權的行程移除指定佇列中等待最久的訊息。變體可依優先權或比對樣式搜尋特定訊息。
  • pollget 的非阻塞變體。佇列為空或找不到特定訊息時,呼叫的行程直接繼續執行。
  • notify:安裝一個回呼(callback)處理函式,訊息進入佇列時自動被呼叫。回呼也可用來在沒有行程執行時自動啟動一個取訊息的行程;常見實作是在接收端放一個持續監看佇列的 daemon。

圖 4-18:訊息佇列系統中佇列的基本介面

訊息佇列系統的一般架構#

第一個限制:訊息只能放進發送者本地的佇列(同一台機器上,或近到能經由 RPC 有效觸及、例如同一 LAN 上的機器),此佇列稱為來源佇列(source queue);同樣地,訊息只能從本地佇列讀取。訊息本身帶著目的佇列的規格,由訊息佇列系統負責提供佇列並把訊息從來源佇列送到目的佇列。

整組佇列分散在多台機器上,因此系統必須維護佇列名稱到網路位置的對映——實務上是一個(可能分散式的)資料庫。這與網際網路電子郵件使用 DNS 完全類似:寄信到邏輯位址 steen@cs.vu.nl 時,郵件系統會查 DNS 取得收件方郵件伺服器的 IP 位址,再進行實際傳輸。

圖 4-19:佇列層級定址與網路層級定址之間的關係

佇列由佇列管理器(queue manager)管理。一般的佇列管理器直接與收發訊息的應用互動,但也有作為路由器(router)中繼(relay)運作的特殊佇列管理器:把進來的訊息轉送給其他佇列管理器。如此一來,訊息佇列系統可以逐漸長成一個完整的應用層覆蓋網路(overlay network),架在既有電腦網路之上。

使用中繼的理由:

  • 佇列到位置對映的管理:許多訊息佇列系統沒有能動態維護對映的通用命名服務,佇列網路拓撲是靜態的,每個佇列管理器都得留一份對映副本,在大型系統中很快成為管理夢魘。解法是設置少數知道網路拓撲的路由器:發送者 A 把給 B 的訊息放進本地佇列後,訊息先送到最近的路由器 R1,R1 依 B 的名稱判斷該轉給哪個路由器。新增或移除佇列時只需更新路由器,其他佇列管理器只要知道最近的路由器在哪。中繼因此有助於建構可擴展的訊息佇列系統——但佇列網路一大,手動組態終究無法管理,唯一出路是像電腦網路那樣採用動態繞送;令人意外的是,一些流行的訊息佇列系統至今仍未整合這種方案。
  • 訊息的二次處理:例如基於安全或容錯考量記錄訊息。
  • 作為閘道進行格式轉換(見下述訊息代理者),或用於多播——把進來的訊息複製進每個送出佇列。

訊息代理者(Message Brokers)#

訊息佇列系統的重要應用領域,是把既有與新的應用整合成單一、一致的分散式資訊系統。整合的前提是應用看得懂彼此的訊息——實務上發送者的輸出格式得跟接收者一致。難點在於:

  • 每加入一個需要獨特訊息格式的應用,每個潛在接收者都得跟著調整。
  • 改約定共同格式在訊息佇列系統的抽象層次上通常行不通:只有當使用這個格式的行程確實有足夠共通點時,共同格式才有意義;若組成分散式資訊系統的應用高度多樣(往往如此),最好的共同格式可能就只剩「一串位元組」。

一般做法是接受多種格式並存,盡量讓轉換簡單。在訊息佇列系統中,轉換由佇列網路中的特殊節點——訊息代理者(message broker)——處理。訊息代理者是訊息佇列系統中的應用層閘道,主要目的是把進來的訊息轉成目的應用看得懂的格式。對訊息佇列系統而言,代理者只是另一個應用,一般不視為佇列系統的一部分。

圖 4-21:訊息佇列系統中訊息代理者的一般組織

  • 最簡單的代理者只做格式重整:例如來訊中資料庫表格以特殊的記錄結尾分隔符隔開、欄位定長,而目的應用期待不同的分隔符與變長欄位,代理者就負責轉換。
  • 進階一點的代理者作為應用層閘道,例如處理兩套不同資料庫應用之間的轉換——此時常無法保證來訊的所有資訊都能轉成目的端合用的形式。
  • 更常見的是把代理者用於進階的企業應用整合(Enterprise Application Integration, EAI):代理者不(只)轉換訊息,而是依交換的訊息媒合應用。這種模型稱為發布/訂閱(publish/subscribe):應用以發布的形式送出訊息(例如發布主題 X 的訊息給代理者),凡表明對主題 X 感興趣(訂閱)的應用就會從代理者收到這些訊息。

訊息代理者的核心是一個規則與程式的儲存庫,能把型別 T1 的訊息轉成型別 T2。問題在於規則要有人定、程式要有人寫。多數訊息代理者產品附有精緻的開發工具,但儲存庫終究得靠專家填滿——這是商業產品被誤導性地宣稱「智慧」的絕佳例子:真正的智慧其實在那些專家的腦袋裡。

與電子郵件系統的對照#

乍看之下,訊息佇列系統似乎早就以電子郵件的形式存在了。電子郵件系統通常由一群郵件伺服器實作,替直連主機上的使用者儲存並轉送訊息;繞送通常省略,因為郵件系統可直接利用底層傳輸服務——例如網際網路郵件協定 SMTP 就是與目的郵件伺服器建立直接的 TCP 連線來傳訊。

差異在於定位:

  • 電子郵件系統主要直接服務終端使用者,因此有自動郵件過濾、進階訊息資料庫(方便撈舊信)等特殊需求;一些群組軟體(groupware)應用也直接建立在電子郵件系統上。
  • 一般的訊息佇列系統不只服務終端使用者,而是要在行程之間建立持久通訊——不論行程是在跑使用者應用、處理資料庫存取還是執行運算。這帶來不同的需求組合:保證遞送、訊息優先權、日誌設施、高效多播、負載平衡、容錯等,都是電子郵件系統一般不必提供的。

因此通用訊息佇列系統的應用範圍很廣,包括電子郵件、工作流程、群組軟體與批次處理;但最重要的應用領域,是把(可能廣泛分散的)資料庫與應用整合成聯邦式資訊系統。例如跨多個資料庫的查詢可能得拆成子查詢分送各資料庫——訊息佇列系統正好把每個子查詢包成訊息路由到正確的資料庫,本章討論的其他通訊設施都遠不如它合適。

實例:IBM WebSphere MQ#

以一個具體系統理解訊息佇列系統的實務運作:IBM WebSphere 產品內的訊息佇列系統,前身是 MQSeries,現稱 WebSphere MQ

概觀#

MQ 佇列網路的基本架構相當直接。所有佇列由佇列管理器(queue manager)管理:它負責把訊息從自己的送出佇列(send queue)移出、轉送給其他佇列管理器,也負責從底層網路接下進來的訊息、存進適當的輸入佇列(input queue)。訊息的預設大小上限是 4 MB(可調高到 100 MB);一個佇列通常限制在 2 GB 資料(依作業系統可輕易調高)。

佇列管理器之間以訊息通道(message channel)成對相連。訊息通道是傳輸層連線的抽象:一條單向且可靠的連線,把佇列中的訊息從發送端佇列管理器送到接收端佇列管理器;基於網際網路的訊息通道就實作為一條 TCP 連線。通道兩端各由一個**訊息通道代理(Message Channel Agent, MCA)**管理:發送端 MCA 基本上就是不斷檢查送出佇列、把訊息包進傳輸層封包、沿連線送給對面的接收端 MCA;接收端 MCA 則監聽進來的封包、拆包後把訊息存入適當的佇列。

佇列管理器可以與應用連結在同一個行程內——此時佇列藏在標準介面後面,實際上可被應用直接操作。另一種組織方式是佇列管理器與應用在不同機器上執行——應用看到的介面相同,但介面實作成一個代理(proxy),以傳統的 RPC 同步通訊與佇列管理器溝通。MQ 藉此維持「只能存取應用本地佇列」的模型。

圖 4-22:IBM 訊息佇列系統的一般組織

通道#

每條訊息通道恰好有一個關聯的送出佇列,通道從中取出要傳送的訊息。通道要能傳輸,兩端的 MCA 都必須啟動且在運行。除了手動啟動,還有幾種方式:

  • 由應用直接啟動自己這端的 MCA——但從透明性的角度看不太可取。
  • 較好的做法:在通道的送出佇列上設定觸發器(trigger),第一則訊息入列時觸發關聯的處理程式,啟動發送端 MCA。
  • 跨網路啟動:若通道一端已啟動,它可以送控制訊息請求啟動另一端的 MCA。控制訊息送給對方機器上監聽眾所周知位址的 daemon。

通道在一段指定時間內沒有新訊息進入送出佇列後會自動停止。

每個 MCA 有一組屬性決定通道的整體行為。兩端 MCA 的屬性值必須相容,可能得先協商才能建立通道——例如兩端顯然必須支援相同的傳輸協定。屬性分兩類:不可協商的屬性,如訊息是否要依放入送出佇列的順序遞送(一端要求 FIFO,另一端就必須配合);可協商的屬性,如最大訊息長度(直接取兩端指定值的最小值)。

圖 4-23:與訊息通道代理相關的一些屬性

訊息傳輸#

訊息要從一個佇列管理器送到另一個(可能遠端的)佇列管理器,必須帶著目的位址,放在傳輸表頭(transmission header)中。MQ 的位址由兩部分組成:目的佇列管理器的名稱,加上該管理器下目的佇列的名稱

除了目的位址,還要指定路由——做法是給出要附加到哪個本地送出佇列的名稱,不必在訊息中提供完整路徑。因為每條通道恰好對應一個送出佇列,指定送出佇列就等於指定了下一個佇列管理器。路由多半明確存在佇列管理器內的**繞送表(routing table)**中,每筆是 (destQM, sendQ) 配對:destQM 是目的佇列管理器名稱,sendQ 是給該管理器的訊息應附加的本地送出佇列。(MQ 稱繞送表項目為 alias。)訊息可能要跨越多個佇列管理器才到得了目的地:每個中途的佇列管理器只需從訊息表頭取出目的佇列管理器名稱、查繞送表、把訊息放進對應的本地送出佇列。

每個佇列管理器有全系統唯一的名稱,實際上就是它的識別碼。問題在於:更換佇列管理器或改名,會影響所有送訊息給它的應用。緩解之道是別名(alias):在佇列管理器 M1 內定義的別名,是佇列管理器 M2 的另一個名字,只對介接 M1 的應用可見。如此即使佇列的管理器換了,應用仍可沿用相同的(邏輯)名稱;改名時只需更新各佇列管理器內的別名,應用不受影響。

延伸案例:alias 與繞送表的合作

連結到佇列管理器 QMA 的應用,可以用本地別名 LA1 指涉某個遠端佇列管理器。QMA 先查別名表,發現實際目的地是佇列管理器 QMC;再查繞送表得知給 QMC 的訊息應附加到送出佇列 SQ1——這條佇列用來把訊息傳給佇列管理器 QMB;QMB 收到後再用自己的繞送表把訊息轉給 QMC。

圖 4-24:使用繞送表與別名的 MQ 佇列網路的一般組織

這套繞送加別名的做法,換來一個本質上相當簡單的程式介面——訊息佇列介面(Message Queue Interface, MQI),最重要的原語如下:

  • MQopen:開啟佇列。放訊息時指定特定佇列管理器中的目的佇列(管理器可用本地別名指名),目的佇列在遠端與否對應用完全透明;要從本地佇列取訊息也得先 MQopen——只有本地佇列能開來讀取
  • MQclose:用完佇列後關閉。
  • MQput / MQget:寫入/讀取訊息。訊息原則上依優先權移出,同優先權者先進先出(等最久的先出);也可以請求特定訊息。MQ 另提供訊息到達時通知應用的設施,避免應用不斷輪詢佇列。

圖 4-25:訊息佇列介面中可用的原語

覆蓋網路的管理#

管理 MQ 系統的重要工作,是把各佇列管理器連成一致的覆蓋網路並長期維護。小型網路只需普通的行政工作量,但當訊息佇列被用來整合、拆解大型既有系統時,事情就複雜了。

MQ 的一大問題是覆蓋網路必須手動管理:不只要建立佇列管理器之間的通道,還要填繞送表,很容易演變成一場惡夢。MQ 的管理支援「先進」之處只在於管理者幾乎能設定所有屬性、微調任何組態——但通道與繞送表終究得手工維護。

覆蓋網路管理的核心是**通道控制功能(channel control function)**元件,邏輯上位於訊息通道代理之間:它讓操作者精確監看通道兩端的狀況,也用來建立通道與繞送表,以及管理承載 MCA 的佇列管理器。這種做法很像用單一管理伺服器管理叢集伺服器——後者本質上只是對每台機器提供遠端 shell,加上少數幾個群體操作。分散式系統管理的好消息是:如果你在找一個能對嚴肅問題探索新解法的領域,這裡機會很多。