可靠的消息传输协议,有必要吗?

在SOA与Web服务的世界中,一个广为接受的理念是可靠消息传输的必要性。可靠消息传输确保消息发送方发出的消息能到达消息接收方,而且仅到达一
次。REST面对的最常见的抵触之一是,它不提供可靠的消息传输机制。Stefan Tilkov写道:“常有人提出,RESTful
HTTP里没有与WS-ReliableMessaging(后文简称为WSRM)等价的协议,于是许多人得出结论,REST不能应用于讲究可靠消息传输的场景中(这等同于几乎所有跟业务相关的系统)”[1]。当然, Tilkov不这么认为,他倾向于在应用层解决这个问题。Joe Gregorio曾在RESTify DayTrader中发表过类似的观点[2]。正因为如此,“因为业务的需要,我们才需要可靠消息传输”这样的假设是错误的;而相反的说法“从业务的角度,我们绝对不需要可靠消息传输”才是合理的。如果我们有良好设计的业务语义及业务逻辑,独立的可靠消息机制就是多此一举。

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(及其同类竞争者)有时可能会带来很好的价值,尤其在点对点传输的场景中。但是,从业务的角度看,设计精良的业务解
决方案并不需要可靠消息传输机制。

关于作者

Marc是一名独立咨询顾问,从事IT业愈20年。他专攻跨企业互操作与语义等,并经常作为讲师及作者的身份出现。目前他生活和工作的城市是荷兰的阿姆斯特丹。


[1] 参见Stefan Tilkov编写的“REST释疑” (http://www.infoq.com/articles/tilkov-rest-doubts)第7点:探讨REST的不可靠性。

[2] 参见Joe Gregorio的RESTify DayTrader, http://bitworking.org/news/201/RESTify-DayTrader

[3] 参见Paul Fremantle编写的“Web服务可靠消息传输入门”,很好的阅读材料。http://www.infoq.com/articles/fremantle-wsrm- introduction

[4] http://www.nictiz.nl/

查看英文原文:Nobody Needs Reliable Messaging

This entry was posted in SOA. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s