脈絡:我的應用程式需要送一大批資料給另一個行程,多到塞不進單一訊息;或者我發出的請求,其回覆資料量大到單一訊息裝不下。

訊息大小的現實限制#

我們很想相信訊息可以任意大,但單一訊息能裝多少資料有實務上限

  • 有些訊息實作對訊息大小設下絕對上限
  • 有些實作允許訊息相當大,但大訊息仍會傷害效能
  • 即使訊息實作允許大訊息,訊息的生產端或消費端也可能限制一次能處理的資料量——例如許多 COBOL 與主機系統一次只消費或產出 32 KB 的區塊

幾個不理想的作法:

  • 限制應用程式永遠不需要超過訊息層能處理的資料量——這是個武斷的限制,可能讓應用程式做不出想要的功能。
  • 由呼叫端發出多個請求,每個結果區塊一個——但這假設呼叫端知道總共會有幾個結果區塊。
  • 接收端一直監聽資料區塊直到沒有為止——但它怎麼知道沒有了?之後還得設法把區塊重組回原本的大資料,很容易出錯

靈感來自郵購包裹#

郵購公司有時會把一筆訂單分成多個箱子出貨。若有三箱,出貨方會標上「1 of 3」「2 of 3」「3 of 3」,收件人就知道自己收到了哪些、是否全都到齊。訣竅就是把同一手法套用到訊息傳遞上

解法#

三個識別欄位是:

  1. 序列識別碼(Sequence identifier)——把這一叢訊息與其他叢區分開來
  2. 位置識別碼(Position identifier)——唯一識別並依序排列序列中的每則訊息
  3. 大小或結束指示(Size or End indicator)——指出這一叢共有幾則訊息,或標示出最後一則(此時它的位置識別碼就等於這一叢的大小)

序列的設計通常讓每則訊息都標明整個序列的總長度;另一種設計則是讓每則訊息標明自己是不是最後一則

圖 5-2:Message Sequence 解法示意

走一遍流程#

假設一批資料要以三則訊息送出:

  • 三則訊息的序列識別碼是某個唯一 ID
  • 每則訊息的位置識別碼各不相同——1、2、3(假設從 1 開始編號)
  • 若寄件端一開始就知道總數,每則訊息的序列大小都是 3
  • 若寄件端在資料送完之前不知道總數(例如它在串流資料),除了最後一則之外每則訊息的「序列結束」旗標都是 false;準備送最後一則時,才把該訊息的旗標設為 true

兩種作法都能給接收端足夠的資訊,把碎片重組回整體——即使碎片不是按順序抵達的

圖 5-3:帶結束指示的訊息序列

使用上的注意事項#

若接收端預期收到的是 Message Sequence,那送給它的每則訊息都該以序列的形式送出,即使那是只有一則的序列。否則當單則訊息不帶序列識別欄位送來時,接收端可能因為缺欄位而困惑,並判定該訊息無效(見 Invalid Message Channel)。

若接收端收到了序列中的一部分訊息,卻始終等不到全部,它應該把已收到的那些改道送往 Invalid Message Channel。

搭配交易性客戶端#

應用程式可能會想用 Transactional Client 來收送序列:

  • 寄件端可以用單一交易送出序列中的所有訊息——這樣在全部送完之前,任何一則都不會被遞送。
  • 接收端同樣可以用單一交易接收——這樣在收齊之前它不會真正消費任何一則;若序列中有訊息缺漏,接收端可以選擇回滾交易,讓這些訊息稍後再被消費。

在許多訊息系統實作中,若一串訊息在同一個交易中送出,它們會依送出順序被接收——這讓接收端重組資料的工作簡單許多。

與其他模式的關係#

  • 當 Message Sequence 是 Request-Reply 中的回覆訊息時,序列識別碼與 Correlation Identifier 通常是同一個東西。只有在「請求方預期同一個請求會有多個回應,且其中一或多個回應可能分成多份」時,兩者才需要分開;當只預期一個回應時,替回應與其序列各設唯一識別碼是可行的,但多餘

Message Sequence 與 Competing ConsumersMessage Dispatcher 通常不相容。若序列中的不同訊息被不同的消費者/執行者收走,沒有任何一個接收者能在不與其他人交換訊息內容的情況下重組原始資料。因此訊息序列應該透過只有單一消費者的 Message Channel 傳輸。

Message Sequence 的替代方案是 Claim Check:與其在兩個應用程式之間傳輸一份大文件,若雙方都能存取共同的資料庫或檔案系統,就把文件存起來,只用單一訊息傳一張收據

範例:大型文件傳輸、多筆查詢與 SOAP 序列

大型文件傳輸#

寄件端要送一份極大的文件,大到裝不進單一訊息、或一次全送不切實際。那就把它切成幾份,各自以一則訊息送出,每則訊息標明自己在序列中的位置與預期的總訊息數。

舉例來說,MSMQ 訊息的最大尺寸是 4 MB。

多筆結果查詢#

考慮一個「查詢某作者所有著作」的請求。因為清單可能很長,訊息設計可以選擇把每一筆結果當成一則獨立訊息回傳;此時每則訊息都要標明它回應的是哪個查詢、自己在序列中的位置,以及預期的總訊息數。

分散式查詢#

考慮一個由多個接收端分頭執行的查詢。若這些部分之間有順序,就必須在回覆訊息中標示出來,完整回覆才能被正確組裝——每個接收端都得知道自己在整體順序中的位置,並在回覆的訊息序列中標明

JMS 與 .NET#

JMS 與 .NET 都沒有內建支援訊息序列的屬性,因此應用程式必須自行實作序列欄位:

  • JMS 允許應用程式在表頭中定義自己的屬性,這是一個選項
  • .NET 不提供表頭中的應用程式自訂屬性

這些欄位也可以定義在訊息內文中。

但要注意:若序列的接收端需要依序列欄位過濾訊息,那把欄位放在表頭比放在內文容易得多。

Web Services Architecture Usage Scenarios#

〈Web Services Architecture Usage Scenarios〉[WSAUS] 中的 Multiple Asynchronous Responses 情境,在 SOAP 表頭用 message-idresponse-to 欄位把回應與請求關聯起來,並在內文用 sequence-numbertotal-in-sequence 欄位依序識別回應。

含有訊息識別碼的 SOAP 請求:

<?xml version="1.0" ?>
<env:Envelope xmlns:env="http://www.w3.org/2002/06/soap-envelope">
  <env:Header>
    <n:MsgHeader xmlns:n="http://example.org/requestresponse">
      <n:MessageId>uuid:09233523-345b-4351-b623-5dsf35sgs5d6</n:MessageId>
    </n:MsgHeader>
  </env:Header>
  <env:Body>
    ........
  </env:Body>
</env:Envelope>

第一則含有序列與關聯資訊的 SOAP 回應:

<?xml version="1.0" ?>
<env:Envelope xmlns:env="http://www.w3.org/2002/06/soap-envelope">
 <env:Header>
   <n:MsgHeader xmlns:n="http://example.org/requestresponse">
     <!-- MessageId will be unique for each response message -->
     <!-- ResponseTo will be constant for each response message in the sequence-->
     <n:MessageId>uuid:09233523-567b-2891-b623-9dke28yod7m9</n:MessageId>
     <n:ResponseTo>uuid:09233523-345b-4351-b623-5dsf35sgs5d6</n:ResponseTo>
   </n:MsgHeader>
   <s:Sequence xmlns:s="http://example.org/sequence">
     <s:SequenceNumber>1</s:SequenceNumber>
     <s:TotalInSequence>5</s:TotalInSequence>
   </s:Sequence>
 </env:Header>
 <env:Body>
   ........
 </env:Body>
</env:Envelope>

最後一則 SOAP 回應:

<?xml version="1.0" ?>
<env:Envelope xmlns:env="http://www.w3.org/2002/06/soap-envelope">
 <env:Header>
   <n:MsgHeader xmlns:n="http://example.org/requestresponse">
     <n:MessageId>uuid:40195729-sj20-pso3-1092-p20dj28rk104</n:MessageId>
     <n:ResponseTo>uuid:09233523-345b-4351-b623-5dsf35sgs5d6</n:ResponseTo>
   </n:MsgHeader>
   <s:Sequence xmlns:s="http://example.org/sequence">
     <s:SequenceNumber>5</s:SequenceNumber>
     <s:TotalInSequence>5</s:TotalInSequence>
   </s:Sequence>
 </env:Header>
 <env:Body>
   ........
 </env:Body>
</env:Envelope>

這裡表頭中的 message-id 被當作回應的序列識別碼,而每則回應的 sequence-numbertotal-in-sequence 分別是位置識別碼大小指示