郜启凯,李 莹,邓水光
(浙江大学 计算机科学与技术学院,浙江 杭州 310013)
随着云计算、移动互联网等技术的迅猛发展,网络空间中发布的在线服务数量正在成倍地增长,开发人员能够快速获取到满足自身需求的第三方服务。但第三方服务的质量往往不受控制,而跨组织服务间的适配[1]也更为复杂,这使得针对跨组织间服务合作场景的高可靠性服务编排技术[2]成为研究热点。
现有的较为流行的服务编排框架(如Netflix的conductor[注]https://github.com/Netflix/conductor。和Uber的cadence[注]https://github.com/uber/candence。)尽管在异步和分布式场景中表现出优秀的性能,但大部分都是基于服务编制(service orchestration)[3]的单核心框架,这种框架往往缺乏灵活性,不适合在跨组织项目中部署。与此同时,服务网格[4-5]概念的兴起激起了人们对于服务代理的兴趣,使用额外的统一化代理[6-7]在应用层完成类似TCP协议在网络层所作的工作,屏蔽不同服务间的差异,以此来简化服务编排设计和业务流程实现的技术成为了服务计算领域的热点之一。
根据以上背景,如何快速、低成本地实现服务编排,对一个跨组织服务编排项目的上线速度和成本构成了重要影响,而传统的中心控制组件方式的编排框架显然很难适应这一需求。因此,本文研究了一种基于业务流程管理和标记法(Business Process Managemen Notation, BPMN)[8]标准的服务编排(service choreography)框架,其核心为一个服务代理引擎。框架向服务提供一个服务代理功能,将服务编排的实现和业务逻辑剥离,使得开发者能够专注于业务本身,而将服务间通信的任务交给代理完成。
应用服务编排技术,需具备以下两个条件:
(1)服务的数据交换格式需要受到统一标准的约束,因此服务使用者在进行编排设计时不用花费过多的精力来考虑参数适配问题。
(2)网络中应当有支持动态“服务-实例”转换查询[9]的控制服务,这一服务让使用者能够以服务名称或类型为基本元素来设计工作流。
以上两个条件的目的是使框架下的工作流易于设计、维护和扩展。希望本文框架能够像TCP/IP系列协议所做的那样,以一定的性能损失为代价来屏蔽异构服务之间的差异,在整个服务网络中建立可靠的交互途径,从而提高整个系统的可用性。
在一个现代服务应用场景中,服务被认为是数量众多但质量不可控的。这就意味着,一方面,针对功能需求的服务发现[10]是可行的,另一方面,对于某个具体的服务实例,本文总是认为它是不可靠的。
如果在服务编排时,文中将活动对应到具体的服务实例,则工作流在执行时将会经常性地因为某个实例不可用而进入阻塞。因此,工作流中的活动应当是面向服务功能而不是面向服务实例的,只有在工作流执行到活动的时候,应用才进行“活动-服务实例”的转换,来获取当前可用/最优的服务实例。
本文的服务代理引擎采用递归调用模式(如图1),对于主持者来说,在发起服务时只需要发送一份交互消息便可执行完整个工作流,这有利于开发者专注于业务逻辑代码。
(1)服务编排脚本 服务编排脚本用来描述一个遵循BPMN 2.0规范的子集的工作流,带有完整的工作流和参数适配信息。本文设计了相应的工具来从一份符合要求的工作流 XML描述中自动产生对应的脚本。
(2)主持者 一次工作流执行任务的发起者,提供一份服务编排脚本,主持者即可发起工作流任务。除发起任务和接受结果外,主持者还需要在本地维护一个状态信息栈用于过程控制,并在任务异常时进行终止、补偿和重定向操作。
(3)参与者 工作流执行中被调用的服务。
(4)控制服务 提供“服务-实例”转换查询和路由信息。
当服务来自不同提供者时,服务的接口往往也有区别,如果不进行封装,代理引擎将很难轻松地协调各个服务间的通信和调用。因此,本文提出一种标准消息模式[11],用以包装通信数据。该模式只允许任务中存在活动的双端交互,且交互两端的活动只能使用通信消息进行交互,或者使用通知消息向主持者提交状态信息。
1.2.1 交互消息
交互消息在一次调用流程中作为执行说明,参与者在完成自身任务后通过转发交互消息来推进工作。在一个交互消息(如图2a)中,Message名称由调用发起者在发起时产生,依据一定的策略使得该名称在整个网络中唯一。
交互消息M定义为一个六元组:
M=(X,H,JM,P,E,W)。
(1)
式中:X表示脚本xml,H为主持者地址host,JM表示消息被转发次数jump,P代表前置活动数据prior,E为活动过期的时间expire,W表示权重数weight。
X的加入使得代理引擎有能力在没有中心控制器的情况下独立正确地执行工作流步骤;W的作用为活动许可,初始值为工作流中最大路径数,在每个实际分支处会变为当前分支的最大路径数。如果遇到分支,交互消息的权重值会减小,并在聚合时计算收到的同名消息的权重和,只有当权重等于当前分支的最大可能路径数时,活动才会被许可。
1.2.2 通知消息
通知消息N(如图2b)被定义为一个四元组:
N=(T,S,JN,I)。
(2)
式中:T为枚举子类型type,S表示通知发送活动service,JN为当前活动的步数jump,I为携带信息info。
T包含Receive,Success和Fail 3种子类型,用于表明当前消息的子类型。其中Receive子类型用于确认接受消息,Success用于确认任务完成,Fail子类型用于确认任务失败。当T=Receive时,I的内容为发送者所接受到的消息的来源,当T=Fail时,携带失败返回信息,当T=Success时,I为所发送M中的P的副本,该副本被用于状态保存,使工作流在中断时能够从断点处恢复。
注册到控制服务的服务需要事先使用平台提供的代理引擎软件开发工具包(Software Development Kit,SDK)封装自己的接口,该SDK主要用于消息解析,参数自动适配,消息装配和转发。代理引擎采用分层模型(如图3),对于业务逻辑代码而言,服务实例只需要调用SDK中的对应方法发送和接受数据,服务间交互就像是直接发送到目标的,剩余的工作将交给代理引擎完成。
1.3.1 主持者
当启用一个工作流时,引擎接受一个脚本解析并验证合法性(如控制服务启用者是否具有所有包含服务的使用权限);然后主持者启动状态监听程序,构造交互消息,依据脚本注入参数,设定JM=1;最后从控制服务处查询下一活动的可用实例并发送交互消息。
状态监听程序的行为由图4主持者状态机说明:
(1)若设定的等待时间到达则终止监听并报超时;
(2)若收到Receive通知则更改等待时间为发送者注册的服务最大耗时;
(3)若收到Success通知则更改等待时间为设定的网络延迟等待时间,将P副本更新至状态栈中;
(4)若收到Fail通知,报告Fail异常并终止监听;
(5)若工作流已执行到结束事件,则从状态信息栈中读取所需数据并提交。
1.3.2 参与者
在参与者处建立等待队列、就绪队列和结果保留队列3个存储结构。引擎将自动抛弃存储结构中超时的数据,结果保留队列的超时时间会被自动延长2倍。其中,等待队列用于当前活动存在多个前置活动,且只有部分前置活动完成时存储相关信息;就绪队列用于存储待执行的活动;结果保留队列则用于存储活动的执行结果,之所以设立结果保留站,是为了在通知消息遭遇网络问题时能够快速响应补偿操作所发的消息而避免重复执行同一个任务。
参与者的行为如图5所示。当参与者接受到一个交互消息时,首先检查该消息是否超时,未超时则向H发送Receive通知,否则直接丢弃。然后引擎会查询结果保留队列,若存在同名且JM相等的任务,则执行快速响应策略,立即返回处理结果通知消息。下一步,引擎将查询等待队列,若存在同名且JM相等的任务,则将P的参数注入任务,更新E为较小者,计算新的W,检查任务是否已就绪,若已就绪则将其转入就绪队列,并结束;若不存在同名且JM相等的任务,则解析脚本,依据脚本生成一个任务对象并将P的参数注入,检查任务是否已就绪,若已就绪则将其转入就绪队列,若未就绪,则将任务转入等待队列。
引擎会在本地服务处于可用状态时主动将就绪任务队首提交给本地服务处理,并将任务转至结果保留队列。本地服务完成任务后向引擎提交输出,并按以下策略执行通知操作:
(1)若处理异常,构建Fail通知发送给主持者;
(2)若处理成功,从脚本解析中获取后续活动信息,构建交互消息并注入参数,将JM+1,向控制服务查询可用实例,将交互消息发送给所有后续活动,同时构建Success通知发送给H。
根据以上模型,已发射的工作流中的活动将具备以下几种状态:等待、阻塞、就绪、正在执行、完成、失败、超时。由于预估了执行延迟,“正在执行”状态可以合并至“就绪”状态以减少状态通信。为便于过程监控,活动主持人将维护一个工作流状态信息栈,记录每一个活动的当前状态信息。此外,在一个分布式系统中,网络条件也是决定一个工作能否正确执行的决定因素之一,因此本文设计了路径染色方案,用于判断网络状况并决定补偿方案。主持者依据表1的活动状态规则更新状态信息栈中活动的执行状态,并依据表2的网络状态规则更新工作流路径状态。
表1 活动状态
在引擎中,文本设计了异常处理策略,用来在服务存在异常时进行处理,避免偶发的网络不稳定导致整个工作流终止。异常处理策略分为消息补偿和服务重定向[12],将由主持者依据状态栈自动执行。
表2给出了路径颜色和转移条件的对应关系。在黄色路径上,主持者会读取状态栈中的参数列表,将自己模拟为前置活动重发交互消息,新的交互消息将会按照预设定值增大超时时间戳;若在红色路径上,主持人将向管理平台重新查询可用服务实例,并重新发送补偿消息。服务重定向后路径转为黑色,当一次补偿操作失败或者工作流超时后,整个工作流将被立即终止。若不是因为工作流超时而终止,主持者需要向所有阻塞和就绪的活动发送包含过期时间戳的交互消息,以通知所有参与者该工作流已被终止。
图6给出了一个包含一个并行网关的简单工作流的状态信息栈结构,当Activity 3所发的交互消息在超时前为被Activity 4通知Receive时(可能时交互消息丢失,也可能时Receive通知消息丢失),主持者将在状态信息栈中提取信息并伪装成Activity 3向Activity 4重发交互消息。当Activity 4收到重发的交互消息时,若活动仍未超时,则将面临两个状况:①结果保留队列存在同名且同jump任务,则立即重发Success通知并更新保留时间;②结果保留栈不存在同名且同jump任务,则执行正常流程。
表2 网络状态
本文利用模拟服务网络来模拟真实的执行情况。表3说明了实验中的模拟服务的具体情况,这些服务会在区间范围内延迟随机的时间并输出预置结果。每个服务有200 ms的基础延迟,最终延迟为基础延迟加上动态延迟范围内的随机结果。其中A~E为原始服务,F、G、H分别作为B、CD、E的替代服务。服务将根据本文中设置好的阶梯概率(如表4),依据路径流量选择概率并随机忽略收到的消息来模拟网络不稳定[13]。
表3 服务说明
表4 阶梯延迟
实验包含1个实验组A和1个对照组B,分别在3种不同情况下执行100 000次。其中,实验组A使用文中提出的服务编排框架,工作流超时时间设置为路径上服务的平均延迟的和的2倍,对照组则使用了Conductor框架,未设置异常处理功能。3种情况分别为:①排他网关(图7a)和1%服务异常概率;②排他网关和0.01%服务异常概率;③并行网关(图7b)和0.01%服务异常概率。
性能评价指标为工作流平均执行时长和成功率。其中,工作流平均执行时长为全部成功完成的工作流执行时长的均值。
在搭建实验时,使用Conductor框架时搭建一个简单工作流的耗时以小时为单位,而使用本文提出的框架时仅需要数分钟制作BPMN模型即完成了工作。这无疑证明了本文所提框架的易用性。
实验结果(如表5)显示,相对于没有补偿操作的服务编排引擎,本文所提出的引擎付出平均完成时间增加约4.1%~6.5%的代价,使得在不稳定情况下的服务完成率获得16.508%~20.804%的提升。其中对照组B的消息补偿和服务重定向次数表示可执行补偿或重定向而未执行的次数。
实验1和实验2对比,本文所提出的引擎对于服务质量差的情况具有较好的抗干扰性;实验2和实验3对比,发现多执行路径任务的分支会显著影响平均时间和成功率。
表5 实验结果数据
实验表明,本文所提出的服务编排引擎在简化服务编排设计的情况下,引入了补偿操作的概念,对工作流在不稳定情况下提升执行成功率具有显著的效果。
本文所提到的服务编排技术成功实现了一个去中心化的服务编排框架,通过网络代理机制使得分散的服务能够以脱离中心控制器的方法独立完成服务间的协调工作,补偿机制的加入以可接受的性能损失为代价增强了整个服务网络的鲁棒性,减小了跨组织合作时服务编排的成本增长。服务实例的变动并不会影响工作流的执行,而编排设计也无需考虑目标服务的可用性。
无论是商业化的服务提供平台,还是自有分布式服务管理平台,应用该技术将在初期付出一部分代价,但在后续的更新和维护场景中,简化的服务编排模式能够明显缩短服务更新周期,使得整个应用的扩展更为灵活。
本文提出的编排技术尽管在编排流程简化和抗干扰性上展现了显著的效果,但性能方面的因素并未在之前的工作中作为重点进行研究,预计复杂工作流在执行时将会面临较大的延迟代价。因此,基于未来服务将是多且不可靠的基本原则,该项技术还有以下几个提升方向:
(1)增强分支处理决策能力,提高编排引擎在递归调用时的性能。
(2)精细化业务流程编排[14]以适应可伸缩微服部署[15],拓宽框架的应用场景。
(3)为控制服务添加服务推荐算法[16],动态地向使用者提供质量更高的服务实例,提高一次执行的成功率,缩短平均执行时间。