正确认识分布式异构环境下的异步服务和应用场景设计
基础知识
首先,“异步服务”完整的解释应该是客户端以异步通信的方式调用Web服务;而非服务端必需要实现一个异步处理机制的服务端Web服务;
其次,应用架构设计中是否应用“异步服务”完全取决于客户端应用场景所需,服务端应用无需刻意进行任何设计和实现;
因此,基于以上理解,切勿走近实现异步业务场景,必须由服务端配合实现的误区。
异步通信的应用场景
谈为何需要应用异步通信时,首先,需要搞清楚异步通信相对同步而言所产生的价值及收益,其次,要明白什么应用场景需要使用异步通信,是否必须一定要异步通信。异步通信顾名思义就是以多线程的方式对发送请求和接受响应进行分离,从而实现在请求发出后,不需要线程等待,可以充分利用时间、资源处理其他任务,待请求处理完成响应返回时,再次接管线程实现后续业务逻辑处理。一切听起来都很美妙,那么是否所有请求调用都应该作为异步请求调用,并且适用于所有场景呢?其实不然,下一小节分析异步通信的弊端,这里描述一下应用场景,根据多年分布式异构系统ESB总线平台构建的实施经验,归纳为以下三个:
1、 服务端所提供服务相对独立,接口运行稳定,且变更(主动回调)成本代价非常高
2、 服务端所提供服务业务逻辑复杂,处理耗时较长
3、 服务端与客户端部署于同一网络组网,并且相互之间防火墙网络策略透明,网络通信稳定
抛离上述三个场景,均不建议大规模使用异步通信,否则很有可能产生灾难性的故障。因此,从其他方面考虑如何实现异步的场景,正如本文后续章节所建议的方案。
异步通信并非万能
设想考虑一个场景,目前需求需要构建一个机票代购平台的二级代售平台。因此,需要通过各大航空公司获取航班信息和机票信息,但是获取这些信息的服务水平协议(SLA)和服务质量(Qos)完全依赖于航班公司提供的外部服务,由于提交请求的时间、参数不同,很有可能得到操作响应的时间不同,若在节假日高峰期(假设耗时很长、逻辑处理相当复杂)出现等待超时则将严重影响客户满意度,从而流失客户资源。那么异步通信可以多线程进行任务处理,目前的需求场景是否就应该使用异步通信呢?其实答案是否定的,其原因如下:
1、 航班信息接口服务相对稳定,并且为多个代理终端服务,不会因为某一个代理商变更,实现接受请求,进行业务处理,主动回写代理终端接口
2、 航班信息接口服务网络环境与代理终端均在公网,通信都基于TCP/IP,因此如果服务端宕机或网络故障,将导致代理终端出现网络堵塞,同样会出现网络等待超时的异常。
所以,该场景并不适合使用异步通信。而实际上,航班数据接口,代理终端也是通过同步的方式进行实时通信在线查询,并且设置每次会话最大时长和最大错误重试的次数限制。
因此,在未充分考虑应用场景和各系统软硬件背景的情况下,不建议大规模使用异步服务,异步场景在请求发出后,客户端都要维护一个等待回写的进程,服务端完成业务逻辑进行响应时,客户端要关联正确的远程会话,从而执行准确无误的读取。而这样的方式分布式异构平台下,且每个系统均为多机多实例的集群架构时,客户端维护等待进程并关联正确会话信息并不容易,这不仅要考虑网络、各系统可靠性和可用性,还要考虑客户端系统内本身硬件服务器上下文、资源连接、应用会话等,一旦大规模纯粹的异步服务应用场景存在,很有可能失控,导致服务端挂起的进程堵塞,客户端多次尝试回写失败,各方问题排查受阻,反而影响业务正常运转。
所以,建议纯粹的异步通信服务仅仅在时效性要求特别高的少数应用场景使用,并且必须经过各系统模拟真实生产环境中多机多实例集群架构测试,衡量异步回写成功率,从而决定该异步服务是否满足Qos指标,再慎重评估能否接入使用。
JAX-WS异步实现
JAX-WS编程模型相对于JAX-RPC编程模型而言,其中一个优势就是对异步通信调用服务支持。因此,当决定以异步通信的方式调用一个Web服务时,最常用的方式就是按照JAX-WS编程模型开发服务客户端程序。下面介绍一下几个重要环节:
1、 服务客户端支持JDK 5.0
2、 JAX-WS启用JAXB的数据映射,并且配置异步映射,如下代码:
true
3、 一切从WSDL开始,养成良好的习惯。通过服务生产客户端程序的异步调用代码,如下所示:
@WebMethod(operationName = "process")
public Response<com.zte.soa.ucloudapp.helloworld.helloworldbpelprocess.ResponseParameters> processAsync(
@WebParam(partName = "payload", name = "requestParameters", targetNamespace = "http://soa.zte.com/ucloudApp/HelloWorld/HelloWorldBPELProcess")
RequestParameters payload
);
@WebMethod(operationName = "process")
public Future<?> processAsync(
@WebParam(partName = "payload", name = "requestParameters", targetNamespace = "http://soa.zte.com/ucloudApp/HelloWorld/HelloWorldBPELProcess")
RequestParameters payload,
@WebParam(name = "asyncHandler", targetNamespace = "")
AsyncHandler<com.zte.soa.ucloudapp.helloworld.helloworldbpelprocess.ResponseParameters> asyncHandler
);
@WebResult(name = "responseParameters", targetNamespace = "http://soa.zte.com/ucloudApp/HelloWorld/HelloWorldBPELProcess", partName = "payload")
@WebMethod(action = "process")
public ResponseParameters process(
@WebParam(partName = "payload", name = "requestParameters", targetNamespace = "http://soa.zte.com/ucloudApp/HelloWorld/HelloWorldBPELProcess")
RequestParameters payload
);
SayHiEp ss = new SayHiEp(wsdlURL, SERVICE_NAME);
HelloWorldBPELProcess port = ss.getHelloWorldBPELProcessPt();
RequestParameters _payload = new RequestParameters();
_payload.setInput("lishuyi");
{
System.out.println("同步调用 process... ");
ResponseParameters response = port.process(_payload);
System.out.println("process.result=" + response.getOutput());
}
{
System.out.println("异步回调 process...");
ProcessCallBackAsync _asyncHandler = new ProcessCallBackAsync();
Future<?> response = port.processAsync(_payload, _asyncHandler);
while (!response.isDone()) {
Thread.sleep(1000);
}
ResponseParameters output = _asyncHandler.getOutput();
System.out.println("process.result=" + output.getOutput());
}
{
System.out.println("异步无阻塞轮询 process...");
Response<ResponseParameters> response = port.processAsync(_payload);
while (!response.isDone()) {
Thread.sleep(1000);
}
ResponseParameters output = response.get();
System.out.println("process.result=" + output.getOutput());
}
{
System.out.println("异步阻塞轮询 process...");
Response<ResponseParameters> response = port.processAsync(_payload);
ResponseParameters output = response.get(120000, TimeUnit.SECONDS); //wait 2 minutes
System.out.println("process.result=" + output.getOutput());
}
上述代码,分别演示了对一个外部Web服务的四种调用方式:
1) 传统的同步调用
2) 异步回调,需要实现一个自定义的AsyncHandler,并且返回在返回java.util.concurrent.Future的方法中实现响应回写
3) 异步无阻塞轮询,在返回javax.xml.ws.Response的方法中获取响应
4) 异步阻塞轮询,设置最大等待时间
上述代码可以清楚的看到:
a) 是否异步通信的方式调用完全取决于客户端
b) 异步回调与异步轮询都需要设置线程等待
c) 异步回调与异步轮询均基于java.util.concurrent实现
非异步通信方式实现异步场景
既然客户端使用异步通信的方式调用Web服务存在诸多隐患,那么如何解决切实需要异步应用场景呢?这里建议以下两个方案:
1、JMS消息队列
最为有效,并且可以实现服务端与客户端的高可用性和完全解耦,即便某方系统发生故障后,再完成故障恢复或失效转移后,通信数据依旧可以重建,详细方案介绍参考:http://t.cn/zWT9Bap
2、两次同步调用
如果条件允许,尤其是企业内ESB总线上的两个系统通信,那么可以在服务请求客户端再额外实现一个响应接受服务端接口;而在服务请求服务端再额外实现一个响应接受发送客户端程序。那么数据交互模式可以由一个异步的场景,变成两个同步的场景,即客户端在提交完成请求后,服务端马上给予接收到请求的答复反馈;之后服务端可以在闲时进行后台的业务处理,但业务处理完成后再将结果以请求的方式发给调用的客户端,客户端给予服务端收到响应的答复反馈。
BPEL中的异步处理
BPEL(Business Process Execution Language)是JAX-RPC和JAX-WS以外的实现Web Service的另外一套标准规范和实现,并且已纳入OASIS标准。在BPEL规范制定之初,就已经注意到了客户端使用异步通信调用服务的弊端,因此BPEL在实现Web Service的时候,刻意将BPEL划分为Synchronous BPEL Process 和 Asynchronous BPEL Process。其目的就是要客户端程序将同步调用和异步调用严谨的区分开来。对于Synchronous BPEL Process的客户端,BPEL 提供了two-way operation 的调用方式,即一个同步BPEL服务的调用,客户端既有输入也有输出;而对于Asynchronous BPEL Process的客户端,BPEL提供了两个one-way operation 的调用方式,即一个异步的BPEL服务的调用,客户端有一个提交请求的调用,并且还有一个提交获取响应的调用。通过这样的方式,避免同步的BPEL服务设计客户端使用异步通信的方式调用,提前识别出了风险,并且进行的不同方式架构设计。
BPEL原生的API实现方式,对于BPEL服务的two-way operation和one-way operation 方式调用,都可以通过以下三个类进行客户端代码的编写实现:
com.oracle.bpel.client.Locator ----连接BPEL Process Manager
com.oracle.bpel.client.dispatch.IDeliveryService ----定位BPEL服务
com.oracle.bpel.client.NormalizedMessage ---- 提交调用请求,two-way operation 方式对应request方法;one-way operation 方式对应post方法。详细可参考官网介绍:http://docs.oracle.com/cd/B14099_19/integrate.1012/b14448/invoke.htm#BABJJJCC
在我们实际项目中,由于构建企业总线平台,与诸多系统打交道,因此不可能要求所有系统都采用BPEL原生的API实现客户端,强迫各业务系统对BPEL产生依赖,因此我们实际的架构设计采用上述方案提到的“两次同步调用”的方式。即BPEL服务接受到请求后,在其后台进行逻辑处理,待完成后主动将结果回写到客户端提供的接受响应结果的Web Service接口中,代码片段设计如下:
总结
最后用一句广告词形容客户端以异步通信的方式调用Web服务:“异步虽好,可不要贪恋哦!”