目前,面向服务的架构(SOA)已成为连接复杂服务系统的主要解决方案。虽然SOA的理论很容易理解,但要部署一个设计良好、真正实用的SOA系统却非常困难。本文试图通过解析SOA的模式,提供与架构相关的技术指导,进而对以上问题提供详尽的的解答。
在本文中,一共提到了五种模式。表1列出了这五种模式以及各自相关的问题。
模式名称 | 相关问题 |
服务托管 | 如何使服务能够适应不同的配置,避免设置监听器、组件连接等重复性常规工作。 |
主动式服务 | 如何提高服务的独立性以及如何处理暂时性的问题? |
事务处理服务 | 如何可靠地处理消息? |
工作流化 | 如何提高服务对不断变化的业务流程的适应性? |
边缘组件 | 如何将服务的业务功能从无关的交叉问题(比如安全、登录等)中分离出来。 |
表1:模式列表
其中服务托管(ServiceHost)与主动式服务(Active Service)是两种最常见的模式——即使服务的使用范围很小,通常也会使用这两种模式。这两种模式的主要内容都与解决服务相关问题有关,即与具体的服务部署有关(见图1)。
模式一:服务托管
服务托管是我们要讨论的第一个模式。它是最基本的模式,或者至少是最基本的模式之一。服务托管模式主要负责运行着服务实例的环境,以及与此相关的路由任务。
问题
随便选一个服务,任何服务都可以,别告诉我具体是哪个:)。你可以找到一些处理传入的消息或请求的监听代码;你可以找到一些连接组件的代码,还有一些初始化并激活这个服务的代码;或许你还能找到一些能适当地配置服务的代码。有没有觉得我很厉害?实际上,你可以在服务里找到上面的所有代码,至少是大部分。
有许多工作都是重复性的、常见的。我们可以好好利用这一点。
如何使服务能够适应不同的配置,避免设置监听器、组件连接等重复性常规工作?
第一个办法(实际上也不是什么办法),就是为每一个服务重写所有的连接代码。很显然,这不是个好方法,因为重写的次数越多,就越可能产生一些缺陷。并且,对于维护来说,许多重复的代码产生的问题更为严重。在维护的时候,你不仅要确保每一个服务中的缺陷都已经得到修正,还要保证没有任何疏漏、所有的服务都已经同步更新。
另一个相对较合理的办法,就是创建一个共同任务库,所有的服务都通过API与库相连接。这样确实会有所帮助,但是为了充分利用库的功能,你仍然需要编写连接代码。
还有一个办法是利用继承创建一个超类,用超类实现共同的功能,然后让各个服务继承这个类。然而利用继承也有问题,因为服务的功能通常无法通过一个单独的类获得很好的实现。此外,不同的服务所处理的业务也完全不同——否则它们就是一样的服务了。因此,也无法让把这些服务归于同一类结构。
继承几乎已经可以解决我们面临的问题——因为我们只需要写一次代码,只有不同的服务才需要定制。如果想不用继承得到相同的结果,我们就得使用框架。
解决方案
创建一个通用的服务托管组件,把它当作服务的容器或者框架。容器是可以配置的,并执行服务连接、安装等工作(见图2)。
服务托管就像是一个肩负着许多职责的迷你框架。它的第一个职责就是正确地示例服务所包含的组件或类。服务托管还负责读取配置信息,比如,它可以读取服务消费者用来连接服务的端口。其它职责包括创建服务环境,比如在终端创建监听器。最后,服务托管可以负责连接组件——终端监听器上的协议绑定或者创建一个与数据库的连接。所有这些职责的共同特征是它们都与服务的实例化和初始化有关,并且正如我们在问题部分所描述的一样,你很可能会在多个服务中遇到同样的情况。前面已经提到,服务托管是一个框架,与第二种方法中描述的库的概念有所不同。库是一个实用类或方法集,你可以调用它们来获取特定的功能。而框架则包含了一些功能或流程,并通过调用代码来扩展流程或将其转变为具体的流程。这就是“控制反转(Inversion of Control)”原理。这种原理已经在面向对象的框架中获得广泛的应用,比如Spring或Spring.NET、Picocontainers等。
服务托管模式与其它方法相比有许多优势。其中一个优势前面已经提到——即服务托管是一种框架,它可以调用代码来改善性能,而无需你亲自进行编排。另一个优势是它能更好地实现开放/封闭原则(OCP)。OCP原则认为类应该是可以扩展的,但是不能修改;而框架的概念正是这个原则的具体表现。
一个服务托管可以托管多个服务——虽然这种情况可能并不常见。我们曾经构建了一个系统,使用的方案规模小到一台计算机即可应付。这真的很方便。如果换另一种方案,那么服务可能就要扩展到多台计算机上,这样你就得应用多个服务托管实例,各个主机托管服务的一部分,而不是整个服务。
服务托管模式已得到了技术供应商的广泛应用,这一点我们可以在下面的技术相关部分看到。
技术相关
这一部分我们将简单地了解一下利用当前的技术实现这个模式的意义。
服务托管是一种基本的SOA架构模式,因此大部分技术都支持这种模式。JAX-WS和Windows Communication Foundation都能用标记语言(XML)进行配置,并使用Web服务器(比如Servlet引擎或IIS)完成大部分的连接工作。
Windows Communication Foundation还提供了一个称为ServiceHost的类。Microsoft的说明文档是这样解释ServiceHost的:“如果你不使用Internet Information Services (IIS)Windows Activation Services (WAS)提供服务,就用ServiceHost类来配置并提供服务。IIS和WAS都能与ServiceHost进行交互。”从根本上说,其内置WCF对ServiceHost的执行与我们所描述的服务托管模式是基本一致的。
如果你要自己实现一个服务托管模式,那么你可以从Spring(或者Spring.NET)、Picocontainers等轻量级容器中学习如何进行连接和实例化。这方面的技术实现并不多,因为服务托管模式实际上是一种相对简单的模式。
轻量级容器与依赖性注射
Spring和其它一些框架被称为“轻量级容器”。这些“轻量级容器”的优点是它们提高了方案的松耦合性和可测试性。这种优点是通过一种非SOA模式——依赖性注射模式(Dependency Injection Pattern)实现的。依赖性注射,正如名字所示,指一个类通过第三方“汇编程序”提供所需的接口。通过依赖性注射技术,类不再依赖于特殊的实现,而只依赖于接口和抽象类。这使可测试性获得了提高,因为你可以使用桩或模拟来为类提供虚拟环境。这同时也提高了灵活性,因为只要它们之间的关系不变,你就可以轻松地改变具体的实现方式。
服务托管模式是一种简单而有效、并且已经获得广泛应用的的模式。
质量属性场景
这一部分是从需求的角度讨论使用模式的架构效益。大多架构需求是根据使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。这些场景也可供其它可以应用模式的情况参考。
使用服务托管模式的主要原因是重用性。由于多个服务需要使用的相同任务只需写一遍代码,因此重用性也随之提高。这还能带来另一个好处,就是可靠性的提高,因为你只需要调试一次。另外,服务托管模式还能提供可移植性的质量属性。由于这个模式的分离问题的效应,因此可移植性也得到了增强。另外由于可以使用标记语言配置服务环境,可移植性又会得到进一步提高。
下面的表2列出了几个场景,可以给你虑使用服务托管模式的理由。
质量属性(第一层) | 质量属性(第二层) | 场景示例 |
重用性 | 减少开发时间 | 开发时,在20分钟内设定新服务的环境。 |
可移植性 | 安装 | 系统必须支持以服务为单位对服务器进行配置。安装过程中,从一个环境切换到另一个环境所用时间应该少于一小时。 |
一旦配置好并开始运行服务,你就得决定服务应该是被动式的(应请求唤醒)或是主动式的(无需等待服务消费者激活,并做一些零工,比如显示状态或处理超时)。正如名字所示,主动式服务模式下,服务是主动运行的,而不是被动的。
模式二:主动式服务
服务的自治性(Autonomous)很重要。自治性能够提高服务之间的松耦合性,并使整体方案产生更好的灵活性。但是自治性服务有什么实际意义呢?有人说,自治性意味着在不同的服务上工作的团队的自治性。这种定义表示,由于各个服务之间只有契约上的关系,因此服务之间几乎没有依赖性。这意味着各团队可以独立工作,专心于自己的服务,而不会互相绊脚。虽然这是一个不错的“功能”,但是同时还有一个更有价值(比如说商业价值)的定义,就是服务是非常自主(self-sufficient )的。下面我们通过一个示例来解释这个定义。
问题
有一家报纸订阅代理机构(比如Ebsco或Blackwell),它需要为客户创建一份申请。申请服务的一项内容是产生一份形式上的清单。要得到这样的一份清单,该机构必须同时有给顾客的折扣率和从各出版商处能够得到的折扣,这样才能计算出这份申请是否有利润。图3就是这样一个流程的简单示意图。
在场景示例中,申请服务需要等待另外两个服务的信息。顾客服务是内部服务,与申请服务是同一系统;但出版商的折扣服务却很可能是外部服务——如果出版商的系统没有联机,那么会对我们的申请服务造成什么影响呢?会造成申请服务无法使用。即使我们花费了天文数字的资金来保证申请服务的容错性,但现在的问题是完全无法使用,因为申请服务是与外部的出版商的服务随时耦合的。因此申请服务不具有真正的自治性。
如何提高服务的独立性以及如何处理暂时性的问题?
上面所描述的问题表明,仅仅根据请求唤醒的被动式服务是有问题的,因为服务可能无法满足依赖于外部服务的契约条件(或服务等级协议)。
一个解决办法是让服务对先前的结果进行缓存,但这只能解决部分问题,因为这样做数据就无法得到及时更新,并且时而也会有缓存失效的情况发生,这时仍然需要连接其它服务。这种方法还有另一个问题,那就是如果传入的请求过多,在处理一个请求的时候,其它的请求就会处于“等待”的状态,这样又会产生资源问题,因为而这些“等待”的请求都需要外部服务的输入。
即使我们解决了前面的缓存问题,我们仍然得处理其它的暂时性事件。暂时性事件包括重复发生,或者与时间相关的一次性事件。比如,生成每月账单或发布股票数据或任何其它重复性的报告都是暂时性事件。一种解决方法是从外部编排服务。这种方法的问题是你得将服务的业务逻辑具体化。但是请记住,封闭的服务层是应用SOA的一个重要原因。因此我们得另寻解决方案。
解决方案
要使服务成为主动式服务至少需要一个主动类(Active Class),这个类可以在边界上,或者服务上,或者两者都有。然后让这个主动类处理暂时性问题和管理自治性。
主动服务模式意味着在服务层上执行“主动类”(见图4)。在Official UML定义中,“主动类”是指“不需要调用方法即可启动自身行为的对象。”这定义对服务也适用。就是说,服务可以有独立的线程来处理循环类事件,比如每月账单或发布状态。主动式服务也可以监控自身的情况,处理超时,甚至可以用来处理请求。
那么,怎么使用主动服务模式来解决我们上面提到的问题呢?就像帕特·森田在《空手道小子》里扮演的宫城先生所说,“最好的防守就是不要在场。”如果你要避免等待另一个服务这种事情发生,那么最好的办法就是不要等待;你可以主动地、周期性地从其它服务获取数据,更新你的缓存。你还能给其它服务减少类似的麻烦,并预先发布自己的变化状态。表面上看起来,缓存数据可能会引起数据重复的问题,但实际上这种情况是不会发生的(详见下面标注)。
缓存与数据重复问题
我想有些人,特别是那些学过数据库的人,看到我说从远程服务主动获取缓存数据的时候,肯定会从椅子上蹦起来质疑我远程数据复制的动机,认为我是不是大脑出了什么问题。然而,在我看来,这已经不是相同的数据了。缓存在服务上的数据是服务的数据,可以用来计算、处理甚至根据服务需要进行修改。当然,你也必须明白缓存数据的服务并不是负责控制数据的。
一个带有计时器的线程基本上足够应付其它暂时性事件了(如果事件少,你可以为每一个事件安排一个定时器;或者定时唤醒,检查哪些事件需要处理并处理它们)。
使用边界组件的线程处理契约相关的暂时性问题是一个好办法(比如,及时发布状态、超时等),而服务线程则可以处理纯业务类的问题,比如发送每月账单或者处理传入的消息队列。
现在我们看一下如何使用主动服务模式重装安排图3的情况。简单重复一下,图3是一个申请服务的流程,它需要从外部的发布服务获取外部数据,并同顾客服务一起为顾客生成一份清单(见图5)。现在我们让申请服务主动地定期获取折扣信息并缓存结果,这样当收到产生一份清单的请求时,申请服务就可以即时计算折扣,更快地返回结果,并且(在处理清单请求时)无需依赖外部服务。使用了主动式服务,请求服务便与其它服务分离了。
主动服务模式差不多就是一种理念,没有太多的技术成分。
技术相关
这一部分讨论如何使用SOA相关技术实现这个模式;然而,因为这里并没有太多的技术成分,因此我们也只是简单的说明一下。
主动服务模式的技术理念就是在有特殊功能的服务或边界组件上使用主动线程。从本质上说,主动服务模式取决于线程技术。你可以用任何语言实现这种线程技术。关键在于,你要用这个线程做什么。在上面的段落里我们用了从其它服务缓存数据并处理重复性报告的例子。
下面的问题就是什么时候应该使用主动服务模式?我们来看一下动机。
质量属性场景
这一部分是从需求的角度讨论使用模式的架构效益。大多架构需求是根据使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。这些场景也可供其它可以应用模式的情况参考。
主动服务是一些其它模式(比如前面提到的分离调用和blogjecting watchdog)的先决条件,而这些模式可以帮助处理质量属性问题,比如可靠性与可用性。并且,即使是单独使用主动服务模式也能满足许多质量属性要求。
通过预先准备的数据,主动式服务可以减少一些潜在的问题。它可以解决期限的问题,因为它能保证服务在期限之前完成任务(比如按时生成每月账单)。另外,主动服务模式还有可用性的优势,因为从其它服务查询并缓存数据意味着减少了服务的可用性对其它服务的依赖性。
下面的表3列出了一些主动服务模式可以发挥作用的场景。
质量属性(第一层) | 质量属性(第二层) | 场景示例 |
性能 | 延迟 | 正常情况下,评估申请的效益只需要2秒不到的时间 |
性能 | 期限 | 未满负载的正常情况下,系统可以每少刷新两次以上股票价格 |
可用性 | 正常运行时间 | 即使断开远程连接,用户仍然可以生成报价单 |
模式三:事务处理服务模式
服务构建的另一个重要属性是:怎么处理从边界组件或服务中得到的信息?事务处理服务模式(Transactional Service Pattern)可以解决这种问题,并且还能解决可靠性问题。
可以把SOA活动简化为服务收到服务消费者要求做某件任务的请求,服务处理请求(可能还会请求其它服务一起做这件任务),然后回应发起请求的服务消费者。图6显示了这样的一个商业系统中的活动场景。前台与订购服务进行对话。订购服务登记订单,把订单发送到供应商,然后通知账单服务。这些事件处理完成后,订购服务向电子商务前台应用发送一条确认信息。这一切看起来井然有序,但万一中途发生错误怎么办?
问题
比如说,订购服务在确认订单与处理的中途产生故障的话,也就是图6中的步骤1.1与2.0之间,会出现什么情况呢?我想顾客应该会坐在舒适的沙发上,喝着茶水,等着邮递员把她订购的东西送过来。但是虽然她在等,订单却已经消失得无影无踪了。
那么如果服务是在报账服务处理订单之间出现故障呢,也就是步骤2.3之前。这种情况下,订购同样会消失——除非订购系统不等待报账便处理了订单,这几乎是不可能的。更糟糕的是,我们已经向供应商发送了订单,供应商已经把账单发了过来,并且货物也随之送了过来,我们还得给货物准备库存。
消息处理过程处处都可能出现前面提到的这些情况。我们可以安慰自己,说大部分情况下系统会正常工作。然而就像莫非定律所说——我们的服务最终必然会在那宗百万美元的订单上垮掉。现在的问题是:
如何让服务可靠地处理请求?
其中一个方案是把这个责任推给服务消费者。在上面提到的场景中,这意味着如果消费者没有在步骤2.5中收到订单确认信息,那么这个订单就是失败的。然而首先,这个方法并不健全,并会降低服务的自治性——服务无法控制消费者,也无法处理一些其它问题。另外,这只能解决部分问题——那些与服务消费者有关的问题。服务之间的相互作用呢?比如在上面的订购场景中提到的——即使在步骤2.1向供应商发送订单以后,仍然可能出现问题。
第二个办法是同步处理消息。同步操作在性能上会产生很大的问题,特别是当服务需要与外部服务、系统或资源交互的时候,因为服务在返回结果之前,整个流程都要等待第三方的回应。更主要的是,实际上这并没有真正解决问题。如果服务在流程中出现故障了,我们无法确认是哪里的故障。我们只知道消息传输出现了故障,而要确定故障环节,则需要服务消费者的帮忙。
表面看来,如果服务能够使用永久性地储存介质(比如到数据库)就可以解决这个问题。我觉得这个方向是不错;但是,这还不够。因为如果服务在储存状态之前出现了故障,传入的消息仍然会丢失,并且服务对此一无所知。还应该注意到,如果使用永久性存储介质,我们虽然可以追踪到在过程的哪一环节出现故障,但是我们无法确定消息是否已经发送到了其它服务。
要解决这些问题,以及整体的可靠性问题,我们需要事务处理服务。
解决方案
使用事务处理服务模式,一次性处理从读取消息到响应的整个流程。
事务处理服务模式的主要组件是消息泵(message pump),见图7。消息泵监听着终端或边界传入的消息。如果接收到消息,消息泵就开始一个事务操作,读取消息,把消息发送到其它组件/类进行处理,处理后,结束事务操作(完成或失败)。如果可以以事务处理的方式发送请求或回应,就可以把这些操作放入到事务处理中,否则你就需要为操作失败准备补偿逻辑。
使用事务处理编程模型的好处是它要么是纯语义学的,要么完全不是,因此不存在边界效应。由于事务的四个属性(ACID),所有的操作和消息都一定会被完全处理完、或完全没有被处理,所以如果有消息离开了服务,那么触发这个行为的传入消息肯定是被完全处理过了。
ACID事务
一个事务是一个完整的任务单位。一个任务单位如果满足ACID所定义的四个属性,那么它就是一个事务。
* 原子:事务中的所有事件都是以一个原子单位的形式发生的(atomic unit)。这些事件要么全部发生,要么全部不发生。
* 一致:不管事务完成与否,事务的资源必需在整个过程保持一致。
* 隔离:所有外部的观察者(不参与事务处理)都不能看到内部的状态。只能在事务处理开始前或完成后查看状态。
* 持久:事务处理过程中做出的改动储存在永久性存储介质中,因此即使系统重启后也不会丢失。
当然,你要选择事务服务模式的重要因素肯定还是性能。由于需要准备、为了持久性而做的输入输出和锁定管理等,事务通常会比较慢。我一般会预告确定目标场景并进行测试,以确保能够得到一个足够好的方案。
实现事务处理服务模式的一种方法是为所有服务间的消息使用事务消息传输。事务消息传输(transactional message transport)使得模式的实现变得非常简单——只要按照前面提到的步骤来就可以了:开始事务、读取、处理、发送、完成。另外一种方法,也是更常见的一种方法,是在接收到消息后把消息放到事务处理资源中(比如队列或数据库),然后向服务消息者发送一条确认消息。但是这种情况下最初的消息不包括在事务处理中,因此你要准备应付服务消费者的多次消息发送。比如,服务消费者没有收到确认消息,于是“又”发送了一条请求消息提取100万美元。
图8是图6的事务处理服务模式。简单重复一下,该场景是一个关于电子商务的前台订购服务。订购服务登记订单,把订单发送到供应商,然后通知账单服务。这些事件处理完成后,订购服务向电子商务前台应用发送一条确认信息。
在图8中,使用事务处理服务模式,步骤2.0到2.5(订购服务的行为)处于同一事务中。这意味着如果你因为故障或其它意外没有处理下订单的消息,那么服务就不会发出任何消息。这是个很让人开心的消息,因为我们不用再写复杂的补偿逻辑了。这里有一个小问题,那就是如果订购服务在步骤1.0到1.2之间出现故障的话会有什么情况。该场景不是100%安全的;它有很小的几率在我们把消息放到队列等待处理的时候出现故障,从而没有发送确认消息。这可能导致重复接收到同样的请求。一个在服务这边处理重复消息的办法是在服务启动时查看消息队列并对所有消息发送确认消息,这种情况下,服务消费者可能会收到多于一条的确认消息。
请注意,在这个示例中,只能在账单处理过程仅仅产生一个发货单的情况下使用单独的事件。如果账单服务还需要处理信用卡,并且订购服务需要得到确认信息才能继续的话,就不能使用单独的事件了。当不能使用单独的事件的时候,需要把过程分成较小的事务,这时整个过程就被称为连续操作。需要把流程分为几个较小的事务的另一个条件是看服务是否是分布式的。
必须注意把事务的范围定到终端/边界和外部的消息发送者是不一样的。虽然从表面看来,这个区别不是很重要;但是实际上它确实很重要——因为前者是增强服务的可靠性而后者是提高系统的耦合性并会给你带来让人头痛的问题。如果你把事务扩展到服务之外,那将是非常大的转变,因为其它的服务是运行在自己的机器的,有它自己的服务等级协议等等。把内部资源暴露到服务信任协议之外是很冒险的做法。
现在我们看看如何实现事务处理服务。
技术相关
这一部分我们将简单的了解一下利用当前的技术实现这个模式的意义。
如果消息传输是对事务敏感的,那么实现事务处理服务就容易得多。大部分ESB(比如Sonic ESB和Iona Artix)都是事务敏感的,另外还有消息中间件(比如MSMQ)、所有JMS实现以及SQL Server Service Broker。如果你在使用事务消息传输,你可以通过仅仅在资源上创建一个事务来实现事务处理服务模式。如果,比如你还要在同一任务单元中进行更新数据库的操作,你可能还需要分布式的事务处理。然后只要从ESB或消息中间件读取消息、处理、发送响应或其它处理过程生成的消息到外部或目标队列,最后一切顺利的话结束事务。
要注意通常要在事务处理中用到多项资源,比如,一个消息队列和一个用于存储消息处理后的任何状态变化的数据库,这时你应该使用分布式事务处理。在.NET 2.0及以上版本中,如果有需要的话,你可以通过打开一个TransactionScope对象(定义于System.Transactions)显式地过渡到分布式事务处理。
如果消息传输不支持事务,只在你把消息存储到了事务存储库(比如队列或表)后有确认信息。你仍然有在没有得到服务消费者的确认的情况下处理事务的风险,因此你得做好再次接收请求的准备,以防确认消息丢失或根本就没有发送。如果出现故障,而处理传入的消息的事务向其它服务发送了消息,你也要准备好补偿逻辑。
毒信息
当我们以事务的方式读取信息时,需要注意分辨并处理毒信息。毒信息是一种有缺陷的信息,它会使服务发生故障,或者使服务在处理这条消息的时候一直产生处理失败的结果。原因在于,如果在事务中读取了一条毒信息,所产生的故障就会使信息返回队列;而毒信息则在队列中等待服务恢复,然后再被服务读取,依此循环。某些信息技术产品可以辩认这种毒信息并将其丢弃。你需要确认的是这些辩论机制到位,并且能够妥善地处理所有故障,或者至少把这些故障交给你亲自处理。
有一种叫WS-ReliableMessaging貌似跟此有关。然而,虽然名字看起来很相似,但是实际上这只是一种能够稳定地点对点传输消息的协议,可以说更像是HTTP的TCP协议,跟持久性或事务处理根本没有一点关系。但许多ESB是事务性质的,能够支持这种协议,因此你可以通过结合标准协议和事务消息处理来得到一个完善的结果。
其它相关的协议有WS-Coordination和其“同宗”的WS-AtomicTransaction和WS-BusinessActivity。现在我们主要来谈谈WS-AtomicTransaction。从根本上说,WS-AtomicTransaction定义了一种编排分布式事务处理的协议。一般来说,我不会建议使用WS-AtomicTransaction,因为它在服务间引入了太多的耦合关系。比如,在图8描述的场景里——我们真的想在等待外部供应商答复的时候锁定资源并且降低我们的订单的优先级吗?
如果使用了事务敏感中间件的话,情况就会有所不同。这时已经不是跨越服务的单一事务,而是分成了三个更小的事务:一个发送服务;内部中间件保障消息的传递;最后一个在中间件与读取者之间——这是与基础设施的耦合,可以从边界组件上分离出来。
质量属性场景
这一部分是从需求的角度讨论使用模式的架构效益。大多架构需求是通过使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。这些场景也可供其它情况作为应用模式的参考。
事务处理服务引入的事务语义可以简化编码和测试。另外,它还能极大地提高服务的可靠性与稳定性。因为其“全有或全无”的属性使得编码变得简单,这让开发人员能够集中精力实现商业价值,而不是考虑一些边界效应或者类似的问题。
下面是几个场景,可以给你考虑使用服务托管模式的理由。
质量属性(第一层) | 质量属性(第二层) | 场景示例 |
可靠性 | 数据损失 | 在任何情况下,凡是经过系统确认的消息都不会丢失 |
易测性 | 覆盖率 | 所有场景都能得到95%甚至更高的测试覆盖率 |
之所以事务处理服务模式能为我们节省编码时间,是因为事务不像非事务代码有那么多的边界效应。还有一种可以节省编写代码时间的模式是工作流化模式(Workflodize Pattern)。
模式四:工作流化模式
我曾经为一家移动公司做过一个项目,构建一个售后服务系统。大家应该都知道,移动公司之间的竞争是非常激烈的。竞争的结果就是,这家公司的销售部门经常要夜以继日地工作才能制定出新的使用方案或捆绑销售计划以提高销售额:比如朋友、亲情、PTT的公司业务、更低的国际电话费用等方案,3.5G网络的捆绑推广等。对于这家公司来说,每周都会有好几种新式应用方案产生。其记账系统是基于Amdocs的,SAP系统应付新方案也很轻松。然而,市场竞争通常都是从销售部门开始的,而不管IT部门的就绪度如何,因此如何尽快地支持新的销售流程就成了迫切的需求。
几乎所有企业的业务需求都是不断变化的——虽然可能不像前面所描述的那般迫切,但它毕竟是存在的。我们必须寻找一种方式让我们的服务适应这些不断变化的过程。
问题
如何提高服务对不断变化的业务流程的适应性?
最容易想到的方法是每次都等待变化的需求,然后根据需求变化开发代码,更新服务。这里有几个问题。首先,为了变更需求,你需要一个完整的开发周期。其次,代码变更意味着系统的很大一部分需要重启——想一想一些诸如此类的问题吧:“我们昨天的计划会不会受到这次更新的影响?”;“会对上个周期我们添加的那个类似的东西产生什么影响?”等等。可以说越多的开发和测试就等于越长的上市时间。在我们的项目中,这意味着实施新的计划需要几个星期的时间,这会让管理部门很不高兴。这也意味着你的工作评定又降了一级,甚至更多。我们当然不能这么做。
一个较好的办法是将应用中比较稳定的部分从经常改变的部分中分离出来。比如在我们的方案中,像顾客姓名、地址等人口统计资料应该就是与销售方案无关的稳定因素。虽然如此,编排稳定的逻辑仍然是一项繁琐且容易出错的任务。或许,我们可以想个更好的办法……
解决方案
在服务中引入一个工作流引擎来处理不稳定的和经常变化的过程、以及编排稳定逻辑(stable logic)。
如图9所示,工作流化模式是在服务中添加一个工作流引擎来驱动业务过程。工作流引擎中包含一个工作流实例(workflow instance)。最基本的形式是每个工作流负责一种请求类型;然而,工作流可以更复杂,处理连续的过程并且有多个接收外部服务请求或数据的入口点。
使用工作流的优势是可以以活动为构建块进行思考,从而更灵活、更轻松地安排流程。以活动流的方式建模过程意味着可以更容易地分辨并重用稳定的部分,直到有变化需求为止。既然活动可以进行自我测试,重用一个活动就代表你不用再进行大量的测试。而灵活地重新安排活动则代表你可以迅速地响应业务需求。
这个能够更容易地(通过工作流)改变服务行为的诱人方案有一个问题:每次行为变更是否需要同时更新契约版本?回答当然是要看情况。我的原则是,对于契约行为来说,如果里氏代换原则成立,那么就不需要添加新的版本。
什么时候更新契约版本——里氏代换原则
里氏代换原则,或契约式设计,是一种面向对象的原则。Barbara Liskoy(里氏)是这样说的:“如果对于每一个类型S的对象o1都有一个类型T的对象o2,使得以T定义的所有程序P在所有o1都被替换为o2的时候程序P的行为没有变化,那么S是T的一个子类型。”简单地说,这就是指子类可以代替父类使用而不会破坏任何使用基类的行为。应用到SOA上这意味着改变服务的内部行为时,如果对于每个契约中定义的操作,前面的情况不变或较弱,而后面的情况(比如请求结果)不变或更强,那么你就不需要创建新的契约版本。换言之,为了保持相同的契约版本,新的服务版本应该与客户对旧的服务版本的期望行为保持一致。
下面我们把示例的场景工作流化,看看工作流是怎么发挥作用的。简单重复一下,该场景主要是关于如何更快地为移动公司引入新的使用方案。在引入新的方案的时候,后台系统通常还没有就绪——一般需要几天甚至几个星期的时间进行改动、测试和部署。而使用工作流的一个优势就是可以在没有后台的人工干预的情况下为新方案提供请求路由支持。比如,我们可以先让客户关系管理(CRM)系统记录某个客户服务的变更,通知技术人员配置网络等,然后等后台系统就绪了,再更新路由把流程指向新系统。此外,正如前面提到的,在这个流程中有许多步骤是稳定的,比如获取客户的人口统计数据(姓名、地址等)、为电话提供附加程序或附件等。这些步骤都是可以被几乎全部销售过程重用的活动或步骤。在这个场景中添加一个工作流可以极大地提高业务响应能力并保持业务敏捷性。如果某个竞争对手启动了一个很受欢迎的新方案,那么这家公司就可以在一天之内回应一个有竞争力的方案。这是真正的有形商业资产。
工作流引擎的另一个优势是能够处理持续的过程。它把涉及多信息交互的全部过程直观地表示出来,使我们更容易对蓝图和过程有一个清楚的了解,因此可以从业务的角度来调试过程。
当然,工作流化也可以与其它模式结合。比如,很容易通过作业调度(几乎所有的工作流引擎都支持)实现主动式服务模式。
流程编排(Orchestrated Choreography)是一种与工作流化密切相关的模式;这两种模式都使用相同的底层技术:使用工作流引擎。不过,虽然底层技术一样,但是不同的架构考虑方式却会导致选择不一样的模式。比如,两者之间一个很明显的不同就是工作流化局限于一个单独的服务中,而流程编排则需要在服务间添加调整性工作流。
技术相关
这一部分我们将简单的了解一下利用当前的技术实现这个模式的意义以及实现模式所涉及的技术。
与工作流化模式相关的技术自然是工作流引擎。当前市场上有许多工作流引擎。微软将Windows Workflow Foundation作为.NET 3.0的一部分,我觉得这会让它在.NET世界中很受欢迎——虽然还有几家其它公司为.NET提供了像Skelta或K2之类的工作流方案。Java自然能得到更多公司的支持,比如IBM、JBoss,以及专业的工作流公司Flux等等。Oracle甚至提供了一个工作流包(数据库WF_Engine)和Java API支持。
大多工作流引擎都有内置的用以修改工作流的可视化设计器。图10是主动服务模式下用以生成报告的Flux的可视化设计器。
使用像图10所示之类的编辑器是修改流程的不错选择,通常你还可以使用XML来定义工作流。还有一些工具,比如开源(BSD许可证)OpenWFE,完全不提供可视化编辑器,只能依靠XML来配置工作流。下面是一个在OpenWFE中编辑流程的示例。
例1:OpenWFE中信贷审批工作流的部分XML实现
<process-definition name="Credit approval">
<sequence>
.
.
.
<participant field-ref="order_value" />
<if>
<greater-than field-value="order_value" other-value="10000" />
<!-- then -->
<sequence>
<participant ref=”supervisor”/>
<subprocess ref=”ReviewAndApproveOrder”/>
</sequence>
<!-- else -->
<subprocess ref="TaskPaypal" />
</if>
.
.
.
</sequence>
</process-definition>
选择工作流引擎——灵活性
编辑工作流可以考虑几个简单的模块,比如活动、异或分支(可能的执行路径之一)、并发分支。要注意有时候会遇到更复杂的场景,像如何在不同步的前提下合并多个执行路径,并且只执行一次后序的活动,还有如何处理一个活动的(各个活动需要同步的情况、活动数量无法预知的情况等等)多个实例,以及许多其它此类问题。这些问题的解决方案就是工作流模式(在“工作流模式页面”上有描述,详见http://is.tm.tue.nl/research/patterns/patterns.htm)。
我的建议是先了解一下引擎支持哪些工作流模式以保证良好的灵活性,而不会在后期走进死胡同。虽然灵活性并不是唯一的选择标准(还得考虑性能、可用性、安全性等),但我觉得作为一个以灵活性为前提而选择的工具而言,灵活性是一个非常重要的标准。
某些工作流引擎,比如Microsoft Biztalk或WebSphere MQ Workflow,相对内部的工作流成本来说,更适合编排内部服务的交互。
质量属性场景
这一部分是从需求的角度讨论使用模式的架构效益。大多架构需求是通过使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。这些场景也可供其它可以应用模式的情况参考。
工作流化的主要优势是能够提高灵活性。设计一个工作流是一个可视化过程(至少大多工作流实现如此),很容易掌握。附加的灵活性优势也能在需求改变时加快上市时间。在我看来,工作流是使服务走向敏捷业务的最重要的工具。
下面是几个场景,可以给你考虑使用工作流模式的理由。
质量属性(第一层) | 质量属性(第二层) | 场景示例 |
灵活性 | 添加过程 | 对于所有预付费方案,添加对新方案的支持只需要两天不到的时间。 |
重用性 | 核心模块 | 每个新方案可以重用90%以上的常用销售过程 |
由于你可以动态地改变服务行为,因此工作流化模式能够提高服务的灵活性;另外还可以提高边界组件模式的灵活性。
模式五:边界组件
最后一个基本模式是边界组件模式。称其它模式为基本模式是因为它们有很大的通用性。但边界组件模式不同,称它为基本模式是因为这是一个实现其它模式的平台。由于边界组件模式是实现其它模式的一个步骤,具体的示例都是适应于在这个边界组件上构建的模式的,所以很难想象一个具体的示例来展示它的必要性。不过,我会尝试通常几个简单的例子和这些例子之间的共性来介绍边界组件。
问题
场景1
我们曾经为一家公司开发了一个海军C4I平台(Military Naval C4I platform)。这个平台有一些可以重用的服务。比如,核心服务之一提供了标准的中央目标视图。平台上第一套工具使用了TIBCO Rendezvous消息设施。后来需要更换完全不同的技术(WSE 3.0 )。这两套工具都使用相同的业务逻辑,但是实现技术不同。
场景2
在另一个项目中(在工作流化模式中提到过),一家移动公司经常需要在一个处理订单的服务中引入新的应用和销售方案,比如朋友和亲情、晚间话费等。由于详细变动都是XML相关,因此这个服务接口是非常稳定的,但是业务逻辑却要为适应新方案而经常变动。
这是一个与场景1截然相反的场景;这里的接口与技术是不变的,而业务逻辑是变动的。
场景3
最后一个场景是许多项目中常见的一种情况。通常系统里会有多个服务。虽然每个服务处理各自不同的业务,但所有这些服务都要执行一些常见的任务,比如在处理请求之前要确认请求是经过验证的,保存审核条目等等。
在这个场景里,我们遇到了一个不是与单个服务直接相关但在各服务之间重复性却是最高的功能——因为即使一个服务是处理订单的,另一个服务是面向顾客的,其记录请求的代码都是基本一样的。
这些场景的共性是每个服务都涉及多个问题(业务逻辑、技术、记录等)。正如我们所见到的,所有这些问题都必须可以根据情况进行变更而不依赖于其它的问题——我们需要实现这种灵活性。因为我们的问题是:
如何让服务、技术和其它交叉问题(比如安全、记录等)等业务方面的问题可以按自己的步调变更而不产生相互的依赖性?
最简单的(或许过分简单了)办法是不要做任何具体的变动。比如,直接把一部分逻辑当作Web服务。这对技术提供商的在线业务来说是很常见的,比如Microsoft (WCF)和 Sun (JAX-WS)提供的教程。然而,由于契约操作与业务逻辑实现直接纠缠在了一起,这给代码维护带来了极大的不便——比如,如果要支持场景1,用这种方法来替换技术可能就会非常困难。
我们可以通过在复制服务上替换新技术来解决前面的在当前服务中替换技术的问题,这种方法也叫“自我克隆(own and clone)”。不过这也会产生维护上的问题,因为你现在有了同一业务逻辑的多个复本,因此你得改动所有复本,并且这还解决不了场景3里要在多个服务上添加记录功能的问题。
如果什么也不做和克隆都行不通,那么我们可能要考虑分开解决各个问题。
解决方案
关注点分离(SoC)在面向对象的设计中是一个为人熟知的概念。其背后的基本原则是单一责任原则(The Single Responsibility Principle),或简称为SRP。SRP认为要改变一个类只能有唯一的原因,这个原因就是责任(responsibility)。我们可以在SOA中应用同一原则,把业务逻辑看作是一个责任,把其它的问题看作是另一个责任,这样我们就得到以下模式:
附加边界组件(Add Edge Component(s)),用以实现服务并提高灵活性、分离业务逻辑与其它问题(比如契约、协议、终端技术和其它交叉问题)。
正如图11所示,添加边界组件的主要原因就是关注点分离。边界组件可以处理所有这些交叉问题以及其它非核心业务问题。这些问题包括负载平衡、格式转换和审计。这样,服务的业务逻辑就交给了另一个专门处理业务逻辑的组件。这种分离支持所有前面提到的场景,因为分离可以允许各个部件自由调整。比如,要支持一项新技术(场景1),只需要添加一个边界组件,但是业务逻辑并不需要更换。如果要改变业务逻辑的行为,就添加一个新的使用方案(场景2),而边界组件则不需要更换。
从某种意义来说,边界组件模式可以为SOA提供外观(fa?ade)、代理、和AOP模式。
我们还要看一下如何解决场景3中服务间的交叉问题。最好的办法是进一步扩展单一责任原则,并且注意边界组件实际上是一个组件,不能把它对应于整个类型的类。比如,你可以应用管道和过滤架构类型,把多个类/组件连接到一起,各个类/组件处理特定的问题,以此来创建传入或传出的流程。比如,图12即是一个边界组件的示例。该示例中,边界组件提供了一个验证过滤器来确保消息有正确的格式。然后是一个转换过滤器把外部契约格式转为内部格式。最后是一个路由过滤器,负责把消息发送到服务的正确组件。这些组件可以在各个服务中根据需求重用,并且能够自由地进行更改。
虽然从一开始就在边界组件和服务之间定义一个内部契约很有吸引力,但是实际上没有理由这么做,除非你必须支持多个外部契约(虽然实现与消费者一对一的契约非常麻烦——见PTP Integration反模式)。如果服务进展并且创建了新的契约版本,像场景1中像添加新技术,那么在需要支持外部老版本的契约时你可能需要添加内部契约。
边界组件非常有用,我在我设计的大部分SOA项目中都引入了这个模式。本书中提到的许多架构模式也都是基于边界组件模式的扩展。
下面我们来看看边界组件模式的技术相关问题。
技术相关
这一部分我们将简单的了解一下利用当前的技术实现这个模式的意义以及实现模式所涉及的技术。
没有任何技术能够像边界组件一样自动处理那么多问题。不过乐观的说,没有任何技术会影响你实现边界组件模式,而且某些技术最终将帮你解决一些让你困惑的问题。
比如,JAX-WS或Windows Communication Foundation (WCF)实际上已经为你实现了边界组件模式,但它们只处理底层的问题,也就是它们称为绑定(binding)的东西。这些问题是在各种WS*标准中提到的;比如WCF可以处理MTOM encoding或安全问题。然而,你还是需要自己编写高层的问题,比如路由和契约转换。这一点我在上面已经提到过。
还可以使用一种有趣的技术,是称为Restlet的Java引擎。Restlet有一些内置的类,使其成为实现边界组件模式的优秀范例。
边界组件范例——Restlet引擎
Noelios Consulting公司的Restlet引擎是一个用以实现RESTful服务的Java库,它有一些内置类(比如filter和router),可以让我们很方便地构建边界组件。请看图13的示例。
图13是一个在订购服务上的可行的边界配置。这个订购服务的契约有两种操作:getLast,返回上一次的订单;和getAll,返回某个客户所有的订单。但是在实际调用业务逻辑之前,我们得先记录它,处理它的状态和情况,然后确定使用了正确的主机,最终调用正确的业务功能。添加一个边界组件可以让我们获得以上效果,并且不会影响到只处理业务请求的业务逻辑。
下面是上述配置的部分代码。
例2:使用Restlet定义的边界组件部分代码
Builders.buildContainer()
.addServer(Protocol.HTTP, portNumeber)
.attachLog("Log Entry")
.attachStatus(true, "webmaster@mysite.org", "http://www.mysite.org")
.attachHost(portNumber)
.attachRouter("/orders/[+")
.attach("/getAll$", getAllRestlet).owner().start();
.attach("/getLast$", getLastOrderRestlet).owner().start();
现在所有的技术都支持边界组件模式,有些甚至已经在内部实现。
质量属性场景
这一部分是从需求的角度讨论使用模式的架构效益。大多架构需求是通过使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。这些场景也可供其它可以应用模式的情况参考。
由边界组件模式与许多质量属性有关。这些属性大多是由共同使用边界组件模式和其它模式产生的结果。然而,有两个质量属性是直接与边界组件模式相关的。第一个是灵活性——增强服务的适应性,提高服务的外部属性,并且不会影响到业务逻辑。第二个是易维护性——关注点分离(SoC)使各组件的行为更易于理解。回想一下上面三个场景——在当前服务中添加新技术、在不改变契约的前提下改变业务行为、以及迅速解决交叉问题——通过边界组件,我们可以在不影响方案的其它部分(或者至少将影响最小化)的情况下解决问题。下表列出了几个使用边界组件的示例场景。
质量属性(第一层) | 质量属性(第二层) | 场景示例 |
易维护性 | 向后兼容性 | 随着契约的变化,服务应该能够使用老版本的契约为消费者提供支持(至少前两个版本) |
灵活性 | 扩展点 | 在将来一年之内,客户将需要SOX compliance和董事会的审计制度 |
边界模式是SOA最基本的架构模式。
总结
最后,来看看我们讨论过的这五个构建服务的SOA基本模式。这些模式分别为:
- 边界组件:将接口(契约)从实现中分离出来以取得灵活性与可维护性
- 服务托管:使用通常包装器来托管服务实例并重用
- 主动式服务:在服务中使用至少一个独立线程来启动
- 事务处理服务:处理事务内部的消息并妥善处理故障
- 工作流化:在服务中添加工作流以提高灵活性