脈絡:企業正用訊息傳遞整合應用程式。
訊息在被轉送前該存在哪裡#
非同步訊息相對於 RPC 的主要優勢之一,就是寄件端、接收端與連接兩者的網路不必同時運作:
- 網路不通時,訊息系統必須把訊息存起來,直到網路恢復
- 接收端不可用時,訊息系統必須存起訊息並重試遞送,直到接收端恢復
這正是訊息傳遞所依據的儲存後轉送流程。那麼在轉送之前,訊息該存在哪?
預設情況下,訊息系統把訊息存在記憶體中,直到能成功轉送到下一個儲存點。只要訊息系統穩定運行,這行得通;但訊息系統一旦崩潰(機器斷電、訊息行程意外中止),所有存在記憶體中的訊息就全部遺失。
多數應用程式都得面對類似問題:存在記憶體的資料在崩潰時會遺失,所以應用程式用檔案與資料庫把資料持久化到磁碟,好讓它撐過系統崩潰。訊息系統也需要類似的機制。
解法#
運作方式:
- 訊息系統用內建的資料儲存來持久化訊息;它安裝的每台電腦都有自己的資料儲存,訊息因此能在本地保存。
- 寄件端送出訊息時,送出操作要等到訊息安全地存進寄件端的資料儲存後才算成功完成。
- 之後,訊息在成功轉送並存入下一個資料儲存之前,不會從前一個資料儲存中刪除。
如此一來,寄件端一旦成功送出訊息,它就始終至少存在一台電腦的磁碟上,直到成功遞送給接收端並被確認為止。
代價#
- 效能——持久化提高可靠性,代價是效能。若訊息系統崩潰或關閉時遺失訊息是可以接受的,就別用 Guaranteed Delivery,讓訊息在系統中跑得更快。
- 磁碟空間——在高流量情境下 Guaranteed Delivery 可能吃掉大量磁碟空間。若生產者每秒產出數百至數千則訊息,一次持續數小時的網路中斷就可能耗用驚人的磁碟空間;而且因為網路不通,訊息只能存在生產端本機磁碟上,而那顆磁碟未必是為了裝這麼多資料而設計的。
因此有些訊息系統允許設定重試逾時參數,指定訊息在系統內被緩衝多久。在某些高流量應用中(例如把股票報價串流到終端機),這個逾時可能得設得很短,例如幾分鐘。
所幸在許多這類應用中,訊息被當作 Event Message 使用,經過短暫時間後可以安全丟棄(見 Message Expiration)。
測試與除錯時記得關掉#
在測試與除錯期間關閉 Guaranteed Delivery 很有用——這樣只要停掉再重啟訊息伺服器,就能清空所有通道。
佇列中殘留的訊息會讓除錯連最簡單的訊息程式都變得極其煩人。例如寄件端與接收端由一個 Point-to-Point Channel 相連,若通道上還留著一則舊訊息,接收端會先處理那則舊訊息,才輪到寄件端新產生的訊息——這是非同步、保證送達情境下常見的除錯陷阱。
許多商業訊息實作也允許你個別清空佇列,以便測試時重新開始。
邊欄:保證送達到底有多「保證」?
別忘了:電腦系統的可靠性通常以「幾個 9」來衡量(例如 99.9%)。這告訴我們,幾乎沒有東西是 100% 可靠的,而且從 99.9% 提升到 99.99% 的成本已經是指數上升。
同樣的但書適用於 Guaranteed Delivery——總會有訊息遺失的情境:
- 存放持久化訊息的磁碟故障,訊息就可能遺失。你可以用備援磁碟儲存降低故障機率,這也許能再多爭取一個「9」,但仍不會變成真正的 100%。
- 網路長時間不通時,必須存起來的訊息可能塞爆電腦磁碟,同樣導致訊息遺失。
一句話:Guaranteed Delivery 的設計目的是讓訊息遞送對可預期的中斷(機器故障、網路故障)具備韌性,而不是刀槍不入的 100%。
與其他模式的搭配#
- 在 .NET 的 MSMQ 實作中,通道要持久化就必須宣告為交易性的,這意味著寄件端通常得是 Transactional Client。
- 在 JMS 中,對 Publish-Subscribe Channel 而言,Guaranteed Delivery 只保證訊息會送達當時活躍的訂閱者。要確保訂閱者在非活躍期間也收得到,它必須是 Durable Subscriber。
範例:股票交易與各平台的持久化訊息
股票交易#
- 交易請求與交易確認應該用 Guaranteed Delivery 送出,以免遺失;地址變更公告大概也該如此。
- 價格更新則多半不需要——遺失一些無傷大雅,而且它的頻率高到讓 Guaranteed Delivery 的開銷高得不划算。
在 Durable Subscriber 中提到,某些價格變動的訂閱者可能希望是持久的;若如此,價格變動通道大概也該保證送達。但其他訂閱者可能既不需要持久、也不想承受這份開銷。
怎麼同時滿足這兩種需求? 系統可以實作兩個價格變動通道:一個保證送達、一個不保證。只有真的需要所有更新的訂閱者才訂閱那個持久化通道,且它們的訂閱應該是持久的;發布者也可能因為持久化通道開銷較大,而在那個通道上降低發布頻率。
JMS 持久化訊息#
在 JMS 中,訊息持久化可以逐則設定——同一通道上有些訊息持久、有些不持久。
寄件端透過 MessageProducer 把訊息的 JMSDeliveryMode 設為 PERSISTENT:
Session session = // obtain the session
Destination destination = // obtain the destination
Message message = // create the message
MessageProducer producer = session.createProducer(destination);
producer.send(
message,
javax.jms.DeliveryMode.PERSISTENT,
javax.jms.Message.DEFAULT_PRIORITY,
javax.jms.Message.DEFAULT_TIME_TO_LIVE);若想讓所有訊息都持久化,可設為該 producer 的預設值:
producer.setDeliveryMode(javax.jms.DeliveryMode.PERSISTENT);(事實上,message producer 的預設遞送模式本來就是持久化。)之後直接送出即可:
producer.send(message);同一通道上其他 producer 送出的訊息是否持久化,取決於它們各自的設定。
IBM WebSphere MQ#
在 WebSphere MQ 中,Guaranteed Delivery 可以逐通道或逐訊息設定:
- 通道若不持久,訊息就不可能持久。
- 通道若持久,可以設定成「該通道上所有訊息自動持久化」,或「由個別訊息自行決定」。
通道的持久性在訊息系統中建立時設定。全部訊息都持久化:
DEFINE Q(myQueue) PER(PERS)或讓寄件端逐則指定:
DEFINE Q(myQueue) PER(APP)若通道設定為允許寄件端指定,JMS MessageProducer 就能如前所述設定 delivery-mode 屬性;若通道設定為全部持久化,則 MessageProducer 指定的 delivery-mode 會被忽略。
.NET 持久化訊息#
在 .NET 中,把 MessageQueue 建立為交易性的,就會產生持久化訊息:
MessageQueue.Create("MyQueue", true);送上這個佇列的所有訊息都會自動持久化。