在 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 响应返回业务响应。初步简化之后,场景中有两类交易:
- 查询,如查询病人的病历,此时的响应由 HTTP 响应返回;
- 订单,如药方,它的业务响应(一般是 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
查看英文原文: Nobody Needs Reliable Messaging
感谢吴宇对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论 1 条评论