可靠的消息協議

      在SOA與Web服務的世界中,一個廣爲接受的理念是可靠消息傳輸的必要性。可靠消息傳輸確保消息發送方發出的消息能到達消息接收方,而且僅到達一 次。REST面對的最常見的牴觸之一是,它不提供可靠的消息傳輸機制。Stefan Tilkov寫道:“常有人提出,RESTful HTTP裏沒有與WS-ReliableMessaging(後文簡稱爲WSRM)等價的協議,於是許多人得出結論,REST不能應用於講究可靠消息傳輸的場景中(這等同於幾乎所有跟業務相關的系統)”。當然, Tilkov不這麼認爲,他傾向於在應用層解決這個問題。Joe Gregorio曾在RESTify DayTrader中發表過類似的觀點。正因爲如此,“因爲業務的需要,我們才需要可靠消息傳輸”這樣的假設是錯誤的;而相反的說法“從業務的角度,我們絕對不需要可靠消息傳輸”纔是合理的。如果我們有良好設計的業務語義及業務邏輯,獨立的可靠消息機制就是多此一舉。

   

     

Web服務與可靠性

Web服務提供了隔離消息交換的細節與業務邏輯的機制。其基本構想是通過服務的方式定義業務(譬如,“瀏覽目錄”,“下訂單”,“檢查訂單狀態”,等等)。服務通過業務文檔的交換實現,業務文檔中包含業務語義。如果我們使用的是Web服務,業務文檔就裝載在SOAP信封之中。SOAP信封還包含SOAP頭,它實現了一些消息傳輸的功能:消息安全、一致性、尋址、可靠性等。消息功能間互相獨立:即,你完全可以只實現消息完整性也不實現消息可靠性,反之亦可,二者都要或都不要也沒有問題。


上圖概括了以Web服務作爲實現手段的若干SOA的重要特徵:

  • 業務層獨立與消息傳輸層;
  • Web服務添加了若干獨立的“即插即用”的消息傳輸功能塊;
  • 消息頭中附帶了所需消息傳輸功能的相關信息。

 

Web服務本身並非總是可靠的。一個基本問題是:以訂購一本書爲例,假設我發出一條消息,由於某些網絡故障導致消息無法到達目的地,那麼我就得不到要買的書。解決該問題僅需重發一次消息,如場景1所示。

然而,如果消息送達了,但是響應卻丟失了,重發就不能解決該問題:如果我已經訂購了一本書,我將得到兩本同樣的書,見場景2。

可靠消息傳輸解決方案通常通過確認(acknowledgement)、重發偵測、以及重發移除等手段來解決該問題,如場景3所示。

作爲標準的可靠消息傳輸機制,Web服務提供了WSRM[3]。它能提供若干保障:發出的消息一次且僅一次送達,按序送達消息。由於人們通常需要的是消息一次且僅一次傳輸,所以我將只討論兩個場景:“恰好一次送達”和“消息按序送達”。我們就從消息按序送達開始吧。

 

按序送達

從業務視角來看,WSRM所提供的若干保障與普通的消息傳輸之間存在某種矛盾。在線銀行是能體現按序消息處理的重要性的一個常見場景。假設我從儲蓄賬戶向支票帳戶做一筆轉賬,此時支票帳戶的餘額幾乎是0,然後再從支票帳戶帳戶向第三方轉賬,我希望保持資金轉賬順序的正確性,不然,第二次轉賬可能會因爲帳戶餘額不足而退票。我瞭解它的重要性:因爲我的銀行不提供按序消息處理,如果我忘了將這兩次轉賬分開處理,往往就會被退票……

似乎WSRM能夠用來理想地解決這種情況。然而,如果進一步檢查,這其實並不那麼明確。WSRM如何實現按序消息送達呢?一點兒也不用奇怪,它爲消息附加一個遞增的序號。如果消息沒有按序到達(如,2-1-4),那麼接收端的WSRM程序就會等待丟失消息到達之後再向包含業務邏輯的另一層提交消息(如,先提交1和2,然後等待3的達到,3到達之後再提交3和4)。這裏,第一個不解的地方是:很明顯,順序是業務層非常重要的消息屬性之一。那麼,如果它對業務層如此重要,爲何業務層本身不包含這樣的序號呢?我們有一個消息,有其自身的業務層語義,而且順序是非常重要的:那麼爲何不在業務層的消息中添加一些屬性或元素,由它(們)來標識順序呢?

可能的原因有兩個。首先,在消息負載(payload)中也包含一個序號,它附帶了與順序相關的業務語義。如果有,我們爲什麼還需要WSRM呢?這是多此一舉。也許,在某些很少見的情況下,同一件事做兩次也許就是需要如此的(譬如,有一個高效快速的WSRM盒子,能夠非常非常快地處理消息順序,然後業務層在接受到順序消息後僅根據消息負載中的序號進行校驗),但是通常來說,同一件事做兩次對我來說還是相當怪異。它帶來了冗餘:如果WSRM協議頭中的序號指示器與消息負載中的不一致呢,我該怎麼處理?我如何確保產生錯誤時還能在兩個層次間保持按序處理(譬如,當一個丟失的消息永遠不會達到時:我是該後續消息一概不提交,還是任由所有消息都包含一個錯誤狀態,或提出告警等人去處理該錯誤)?只有當這兩層——WSRM層與業務層——都遵守相同的邏輯時才奏效。

第二個可能的回答是:消息負載中無序號。畢竟,也許有人會說,我們已經有了WSRM,我們爲何還要在消息負載中加序號呢? 老實說,這是本末倒置的做法。如果順序對業務層很重要,那麼業務層就需要標識順序,確保其正確的處理順序及持久化。如果讓對於業務層非常重要的順序,依賴於消息從一端推進WSRM總線,從另一端離開的順序而決定,就等於就讓業務邏輯的永久特性依賴於消息傳輸的臨時特性:即消息從總線離開的順序。WSRM的序號在離開該總線後就丟失了,這使得任何有意義的日誌或審計都無法進行。當然,還可以在消息上附加一新序號,由它來指定消息離開WSRM總線的順序,但是,缺少了完整的WSRM流的日誌,對嚴格的審計目的該新序號的價值頗輕。其實,記錄整個WSRM流的日誌也一定能做到,但是這一切看起來卻相當不是那麼回事

再者,按序消息處理不單是在WSRM層與業務層見交互的特徵。如果我的銀行是由兩個銀行合併而來的,那麼我的儲蓄賬戶與支票帳戶的數據庫很可能不在一起,在另一臺機器上,或另一地點:這時,單純地從WSRM層向業務層提交正確的順序還不足夠。業務層還要確保儲蓄賬戶與支票帳戶也按正確的順序執行它們的任務。該示例顯示了按序消息處理在業務邏輯中嵌得很深。實施該場景時,不在消息負載中增加順序指示就是愚蠢。

總之:如果按序消息處理屬於我們正要處理的業務之屬性,我們就需要在消息中爲業務層設置順序指示器,並附上合適的業務語義及業務邏輯。若我們遵循這條簡單而不錯的設計原則,那麼我們就不需要WSRM。也許在某些情況下,WSRM可能會更加高效。但是,從業務的角度看,在功能上實現正確的業務層根本不需要WSRM。

一次,且僅一次

上述論證也適用於一次且僅一次傳輸。假設我要發給你一條消息,那麼在業務層上,一次且僅一次傳輸就顯得非常重要。譬如,我下了一買書訂單,那麼我既不想兩次收到相同的書,也不想根本收不到該書。現在,如果在業務層上,正好得到一本書對我很重要,那麼確保我的消息發送到對方且只發送一次到底能給我帶來什麼?我希望知道你的圖書訂單系統已經收到我的訂單了。如果WSRM總線接受了該消息,而後續的圖書系統因爲我輸入了錯誤的客戶號或不存在的目錄項而拒絕我的訂單,那麼即便知道消息已被接收,也不能帶來任何保障。而且,即使消息在語法和語義上都沒有問題,在圖書缺貨時,依然不起任何作用:我要的是我的書,而不只是確保我的訂單已被準確無誤地接收。如果消息的一次且僅一次傳輸對業務層很重要,那麼我就需要在業務層對此進行確認,如下圖所示,傳輸層的確認對於業務層毫無意義:我們需要的是業務上的確認。

 

許有人會說,WSRM模型應該也能對進入的消息做語法檢驗。並且,WSRM模型當然可以做許多語法檢驗,如格式驗證。但是就客戶號以及目錄項而言,若不通過數據庫查詢,它就不可能驗證某個客戶號或某個目錄項是否有效。此外,在語法級獲得某個目錄項的庫存狀態根本不可能。沒有實際提交到業務層,就不可能保證消息是否會被受理。如果業務層可能拒絕我的消息,那麼即便得知WSRM是否正確地接收了消息,這不再我想要的了。我需要的是業務上的迴應,確保我的消息在業務層被受理,而且恰好受理一次。如果每個消息的一次且僅一次傳輸對業務很重要,那麼業務層應該返回一個消息,表明我的消息已被接收,並且被受理。仍然,如果遵循此簡單的設計原則,那麼從業務的角度看,在功能上就根本不需要獨立的可靠消息傳輸。

讓我們更詳細地看一看“一次且僅一次”的需求。比方說,在業務層上每個消息的一次且僅一次送達非常重要。比方說按順序處理消息,它意味着每個消息應包含唯一的業務交易。類似於消息的按序傳輸,WSRM通過在消息上附加唯一號、確認收據、以及還可能建立重發或刪除重複等機制來保證一次且僅一次傳輸。同樣,如果每個消息是一個獨立的業務交易,那麼很明顯在業務層上一定有一個唯一號:訂單號、預約號、或其他唯一信令。 而且,如果我們在業務層需要這類唯一信令,業務層就會表明其唯一新。業務層的唯一性不應依賴於消息傳輸層臨時的唯一性,而它必須是業務消息的持久特徵,而且,業務語義也應保證這一點。

 

拯救者:冪等性

當業務邏輯要求按序傳輸或一次且僅一次傳輸時,很明顯這需要業務上的響應,換言之,在業務層,業務回覆纔是我的消息被正確接收和受理的唯一保證。單純地使用業務響應代替所有WSRM的幻術就能比WSRM做的更好。如果我們確實在業務層上實現了唯一的業務交易號和業務確認,那麼會發生什麼呢?從根本上說,我們使得每個消息在傳輸層是冪等的。如果我們有了唯一業務號、重複偵測與業務確認,那麼在傳輸層上進行消息重發將永遠是安全的。這使得可靠消息傳輸簡單明瞭:如果我在消息傳輸層到一個HTTP 200 Ok響應(或其他表示“成功的”響應),那麼一切都好,因爲我的消息被接收了;而如果業務響應沒能通過HTTP的響應返回,那麼我將等待,直到它到達爲止。當然,在實現Web服務時,我們應確保在發送‘200 OK’響應消息之前,請求消息應該已經在保存在某永久性介質中了,否則當計算機癱瘓時消息仍可能丟失。但是,即使有了WSRM,我們仍然需要類似的保障,WSRM本身並不提供該保障。而且,如果由於網絡的中斷,我沒有收到‘200 OK’,因爲消息是冪等的,所以可以安全地重發消息,直到收到響應。

案例分享——荷蘭衛生保健中心

在荷蘭,我們爲國家衛生保健中心建立了基礎設施。所有衛生組織都將通過一中心衛生保健信息代理進行信息交換。擁有身份憑證的任何醫護專家都能通過此交換中心獲得他(她)的病人信息。國家標準組織,Nictiz[4]基於HL7v3(它是個醫療詞彙及消息傳輸的框架)開發了相關的國家標準和許多Web服務。

最初並沒有可靠消息傳輸的標準:在2003年與2004年,地盤爭奪戰在WS-Reliability與WSRM之間展開,現在還仍然繼續着,當時我們決定在硝煙停息之前,使用臨時的自開發的解決方案;到了2008年及2009年,我們重新回到可靠性問題上,由於國家交換標準的蒸蒸日上,臨時解決方案將走向終點。我們基於WSRM設計了一個解決方案,最後決定摒棄它。下面我們來看一些細節。

按序處理與我們的場景幾乎不相干:一次且一次傳輸有些關係,或者我們是這麼理解的。我們使用基於HTTP的SOAP的同步交互,通過HTTP請求發送消息,由HTTP響應返回業務響應。初步簡化之後,場景中有兩類交易:

  1. 查詢,如查詢病人的病歷,此時的響應由HTTP響應返回;
  2. 訂單,如藥方,它的業務響應(一般是HL7v3確認報文)由HTTP響應返回。

在第一個場景中,即查詢,根本不需要可靠消息機制。即便由於某種通訊中斷造成了查詢請求或結果的丟失,再查一次就好了。查詢是安全的,服務器的狀態怎麼都不會改變(不同於流量計數和其他不相關的副作用)。

就訂單而言,情況有所不同。如果GP(譯者注:General Practitioners,全科醫生,指得所有科都看的醫生)向藥劑師發送一張處方,此時知道處方是否成功送達以及是否只發送了一次非常重要。如果一切順利,當然沒問題:GP發送一張處方,藥劑師的服務器返回‘200 OK’的HTTP響應,然後GP的應用程序報告處方已經送達。如果進展不順利,則會有問題。如果GP的應用程序沒有收到‘200 OK’的響應,該怎麼辦?如果處方根本就沒有到達藥劑師的服務器,則應該重發該處方。如果響應消息沒有到達,則可能不應該重發,因爲重發會被看作另一張處方,而不是原始處方。

然而,處方本身已包含了唯一處方號了。

<Prescription>
    <id extension="0003000201"
        root="2.16.840.1.113883.2.4.6.1.6005465.12.1"/>

該XML段展示了HL7v3格式中的處方標識符。‘root’部分是OID,它是分配給每個衛生保健中心的應用程序的;任意兩個應用程序都不會拿到相同的root屬性。‘extension’部分是爲處方設置的本地唯一號;它與打印出來的處方單子上的序號是一致的。兩部分合並起來構成了全局的唯一標識。

由於處方號是唯一的,我們要求接收方使用處方號對重複處方進行檢驗。如果某處方被接收了兩次,就會返回一個錯誤消息(在有些必要的情況下,當原始回覆中包含了它所需的信息時,我們也要求接收方返回原始迴應的副本)。此處做了哪些事情?由於在業務層消除了重複,所有的消息都變成了冪等的:在不確信通訊是否成功時,重發消息總是可以的。

傳輸層可靠消息傳輸的大多用例都已不復存在,若沒有收到任何確認,簡單地重發一次即可。在接收第一次發送的處方之後,該處方的重發所得到的錯誤消息也能像最初的確認消息一樣,表明第一次請求消息的成功送達。我們的規範在錯誤及解釋方面做了加強,所有獨立的可靠消息機制已不再重要,它的存在甚至是一個負擔:因爲HL7v3已包含處方號,而且它必須是唯一的,每個應用都得處理該處方號。當第二次收到同一處方時,如果確認信息中包含了GP需要的信息,那麼可以要求藥劑師重構最初的確認消息並且返回。這種情況發生的機率較小,簡單的確認消息不需要這麼做。 因此,業務規則上的一點加強就可以消除對獨立傳輸可靠性的需要。它所能做的就是增加一層,在該層中設定唯一號並再次處理消息重發。注意,可靠傳輸層協議不單指WSRM,它可能是有力的競爭者,但也有其它的,如ebMXL消息傳輸或WS-Reliability,相同的論證也適合於它們。

單靠WSRM也無法很好地支持同步消息傳輸。對於客戶端躲在防火牆後面,或者使用不穩定的移動連接等情況,服務端無法直接定位客戶端,如果HTTP連接突然中斷,服務端根本無法向客戶端發送‘未接收到消息’響應。這種情況下,需要另一個WS-*規範,即WS-MakeConnection,它能幫助客戶端建立新的HTTP連接並輪詢可能在等待接收的響應消息。因爲荷蘭衛生保健中心的應用中所有的交互都是同步的,所以該附加功能是必要的。因此,與其只爲所有可能客戶升級WSRM,還不如一併升級更新的WS-MakeConnection(目前大多數用戶並沒有該協議的支撐庫)。WS-MakeConnection在錯誤發生時基本上將所有同步交互轉換成異步交互。雖然這不一定是壞事,但可能會導致荷蘭衛生保健中心的規範變得越發複雜。WS-*聖歌常常這麼唱:軟件爲開發者屏蔽了規範的複雜性——安裝相應的WS-*庫,一切複雜的工作都交由它去處理。我從不相信該“複雜性隱藏哲學”。凡是稱職的開發者都希望瞭解在這層屏風之後的真實內幕。如果你不瞭解‘交互是否同步‘或‘是否被安全拆分’就根本無法對實際場景進行調試。

總結

在荷蘭衛生保健中心,考慮到可靠消息機制的複雜性,並且從業務角度看可靠消息傳輸的場景幾乎不存在,所以我們決定不使用傳輸層的可靠消息傳輸機制。相反,我們選擇進一步加強業務邏輯,而在業務上唯一處方是必須的,事實上我們的解決方案更加簡單。

總結:如果可靠性對於業務層很重要,那麼就應在業務層處理它。可靠消息傳輸層僅能處理通用邏輯,但這並不是我們想要的——我們要的是與針對具體業務的按序傳輸和一次僅一次傳輸。WSRM(及其同類競爭者)有時可能會帶來很好的價值,尤其在點對點傳輸的場景中。但是,從業務的角度看,設計精良的業務解決方案並不需要可靠消息傳輸機制。

 

該文章轉載於:http://www.infoq.com/cn/articles/no-reliable-messaging

 

該文中描述了消息協議的使用和爲什麼要使用消息協議,理解消息的重要性提供了比較好的圖例和文字描述。在restful http的過程中,如果有請求需要可靠的回饋,就需要參照文中的經驗,形成新的體系對消息進行驗證。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章