简介: 本文介绍如何提升 Java Web 服务性能,主要介绍了三种方法:一是采用 Web 服务的异步调用,二是引入 Web 服务批处理模式,三是压缩 SOAP 消息。重点介绍在编程过程中如何使用异步 Web 服务以及异步调用和同步调用的差异点。本文还示范了如何在项目中使用以上三种方法,以及各种方法所适合的应用场景。
Java Web 服务简介
Web 服务是一种面向服务架构的技术,通过标准的 Web 协议提供服务,目的是保证不同平台的应用服务可以互操作。Web 服务(Web Service)是基于 XML 和 HTTP 通讯的一种服务,其通信协议主要基于 SOAP,服务的描述通过 WSDL、UDDI 来发现和获得服务的元数据。 这种建立在 XML 标准和 Internet 协议基础上的 Web 服务是分布式计算的下一步发展方向,Web 服务为那些由不同资源构建的商业应用程序之间的通信和协作带来了光明的前景,从而使它们可以彼此协作,而不受各自底层实现方案的影响。
JAX-RPC 1.0 是 Java 方面的 Web 服务的原始标准 , 但是由于 JAX-RPC 1.0 对 Web 服务功能的认识有一定的局限,于是 JAX-WS 2.0 应用而生。JAX-WS 2.0 开发工作的主要目标是对各项标准进行更新,成功实现了业界对 JAX-RPC 1.X 的各种期望。此外,JAX-WS 2.0 直接支持 XOP/MTOM,提高了系统附件传送能力以及系统之间的互操作性。
实例剖析 Web 服务性能瓶颈
通过以上简述不难体会到,Web 服务以其 XML + HTTP 的松耦合、平台无关的特性,集万般宠爱于一身,必将成为未来数据共享的基础。但与此同时我们也应当认识到世间完事万物均有其矛盾的两面性:有优点,必将存在缺点,Web 服务亦是如此。就像当初 JAVA 大行其道的时候性能成为其致命诟病一样,Web 服务也同样面临性能问题,似乎“性能问题”天生就是“平台无关”挥之不去的冤家。但问题终归要解决,实践是检验和分析问题的唯一途径,让我们先来创建一个简单的 Web 服务再来审视和分析隐含其中的性能问题。
创建服务
创建服务 Java Bean: 首先我们创建一个尽可能简单的书店服务 Bean,服务的内容只有一个 qryBooksByAuthor,即根据作者 (Author) 查询其名下的书籍 (List<Book>)。
图 1. 书店服务 Bean(BookStoreSrvBean)
服务 Input- 作者 (Author) 的实体类 :
图 2. 作者实体类 (Author)
服务出参 Output- 书籍 (Book) 列表的实体类:
图 3. 书籍实体类 (Book)
至此我们的服务代码已经完成,我们不在此讨论此服务的业务合理性,创建此服务的目的只是举一个尽可能简单的实例以分析 web 服务的性能。
下面的任务就是开发 Web 服务了,手工编写及发布符合规范的 Web 服务过程极为繁琐,在此使用 IBM 的 Rational Software Architect(后面简称 RSA)来进行 Web 服务的服务器端以及客户端的开发。
发布 Web 服务
创建动态 Web 项目 : 发布 Web 服务的前提当然需要一个 J2EE 的 Web 项目,打开 RSA->File->New->Dynamic Web Project, 项目名称为 testWebService, 其余选项根据需要进行选择 ( 注意需要选择加入 Web 项目到 EAR)。创建好的 Web 项目和 EAR 项目效果如下 :
图 4. Web 项目以及应用项目的结构
创建 Web 服务: 选中导入的 com.ibm.test.ws.srv.BookStoreSrvBean,右键 New->Other->Web Service 来创建并发布 Web 服务。创建的时候选择常用的 JAX-WS 标准 , 并选择生成 WSDL 文件。由于 Web 服务的创建不是本文重点,此部分内容暂且省略。服务创建完成之后就可以发布到上一步建好的 Web 项目中了。
创建客户端
使用 RSA,客户端的创建工作将会非常简单:右键点击上面生成的 WSDL 文件 ->Web Services->Generate Client
图 5. 创建客户端界面
在此界面,根据实际情况选择 server,JAX-WS 标准以及 Client 代码的目标项目,然后点击下一步。
图 6. 输入客户端信息
此界面暂时使用默认配置,某些特殊选项将在后面章节进行描述。
客户端调用
由于 JAX-WS 规范大部分的 stub 调用代码是实时生成的,我们只需要修改客户端 WSDL 的 port 就可以用以下代码进行 Web 服务的调用。这里修改 WSDL 端口的目的是让客户端调用 RSA 提供的 TCP/IP Monitor 的虚拟端口,这样我们就可以很轻易地看到 Web 服务实际的调用以及返回的 SOAP 消息了。
客户端调用代码如下 :
图 7. 客户端调用代码
使用 TCP/IP Monitor 看到的 SOAP 消息如下 :
图 8. Web 服务调用产生的 SOAP 消息
Java Web 服务性能分析
从以上实例我们可以看到,Web 服务的调用与传统的 RPC 还是有较大差异的。最大的特点是调用双方使用 XML 格式的 SOAP 规范消息进行传输,这样以文本进行传输的好处是抛弃了私有协议,无论调用双方是何种平台,只要能够构造以及解析 XML 文本,并且存在双方都支持的传输协议,那么调用就成为了可能。而 XML 的日益规范以及 HTTP 协议的普及更是给这两个必要条件提供了坚强的后盾,Web 服务成为未来通用的服务提供标准已是不争的事实。
但是相信使用过 Web 服务的人都曾经经受过其性能不佳的窘境,原因为何我们结合刚才的实例可以分析出以下几点:
● SOAP 文本消息转化导致效率低下
从刚才的 TCP/IP Monitor 监测到的 request 以及 response 的消息我们可以看到,在发送消息时,我们传入了 Author 对象,在实际的调用发生时,这个 Author 对象会被转化成 XML 格式的 SOAP 消息,此消息在到达 Server 端会被解析并重新构造成 Server 端的 Author 对象。Response 也是同理,Books List 也会经历 XML 序列化和反序列化的过程。最糟糕的是,这种过程会在每一次调用的时候都会发生,这种构造以及解析的过程都会极大地消耗 CPU,造成资源的消耗。
●SOAP 文本消息传输导致传输内容膨胀
以 request 参数 Author 为例,必要的信息仅仅是”Bruce Eckel”这几个字节,但转化成 XML 消息后,可以从 SOAP 消息看到,多了很多 SOAP 规范的标签,这些信息会导致需要传输的内容急剧增大,几个字节很可能会变成几千字节。当调用频度和参数内容增多的时候,这种传输内容的膨胀将不是一个可以忽略的影响,它不但会吃掉网络的带宽,还会给 Server 的数据吞吐能力造成负担,后果可想而知。
●同步阻塞调用在某些情况下导致性能低下
同步阻塞调用是指客户端在调用 Web 服务发送 request 后一直处于阻塞状态,客户端线程就会挂起,一直处于等待状态,不能进行其他任务的处理。这样就会造成线程的浪费,如果相应线程占用了一些资源,也不能够及时释放。
这个问题在纯客户端访问 Server 端的情况下并不明显,但如果是两个 Server 端之间进行 Web 服务调用的话,阻塞模式就会成为调用 Server 端的性能瓶颈。
Web 服务性能优化实践
使用异步方式调用 web 服务
先需要强调一点的是,这里的异步方式指的是客户端的异步,无论客户端是同步还是异步,都对服务端没有任何影响。我们期望的理想结果是:当客户端发送了调用请求后不必阻塞等待 server 端的返回结果。最新的 JAX-WS 标准中增加了这一异步调用的特性,更好的消息是,RSA 工具中也对 JAX-WS 的这一特性进行了支持,这样就极大地方便了我们进行异步调用客户端的创建。
其实讲客户端配置为异步模式极其简单,只要在 RSA 生成 Client 端代码时将‘ Enable asynchronous invocation for generated client ’ 选中即可 , 如下图 :
图 9. 异步客户端创建选项
这样在生成的客户端的 BookStoreSrvBeanService 中就会多了 qryBooksByAuthorAsync 的异步方法。既然是异步方法,回调 (Call Back) 就是必不可少的,在下面的异步客户端测试代码中可以看到匿名内部类作为回调 handler 的具体使用方法 :
图 10. 异步客户端调用示例代码
测试代码的输出结果如下:
图 11. 异步调用控制台输出
可以看到,当 Web 服务没有返回时,客户端仍然有机会做自己的输出 :“not done yet, can do something else…”。有些人可能会认为作为客户端此处的输出并无实际意义,但试想如果一个 server 作为客户端去访问一个 Web 服务,如果在服务等待期间能够有机会脱离阻塞状态执行自己需要的代码,甚至可以使用 wait 等方法释放被当前线程占用的资源,那么对于此 server 来说这将是一个对性能提升起到本质作用的因素。
使 web 服务支持批处理模式
● 批处理模式简介
批处理顾名思义是采用一次性处理多条事务的方式来取代一次一条事务的传统处理方式。Java Database Connectivty (JDBC) 中提供了大量的批处理 API 用于优化数据库操作性能,例如 Statement.executeBatch() 可以一次性接收并执行多条 SQL 语句。批处理思想可以方便的移植到 Web 服务调用场景以达到优化 Web 服务调用响应的目的。通过实际 Web 服务调用时间戳分析不难看出网络通讯是 Web 服务性能的瓶颈之一,因此通过减少网络通讯开销来优化 Web 服务性能,批处理模式是其中较为直接的一种实现方式。
●批处理模式适应性
批处理模式虽然作用显著,但是也不适合所有场景。使用批处理模式处理 Web 服务请求时需要考虑一下几点:
1.不同 Web 服务执行时间差异性
不同 Web 服务执行时间不尽相同,因此在同时处理多 Web 服务请求时需要考虑这种时间差异性。一般情况下是等待最长处理时间的 Web 服务执行完毕后汇总所有 Web 服务执行结果从而返回到客户端,因此存在批处理多 Web 服务反而比顺序单次调用 Web 服务消耗更长时间可能性。需要在采用批处理模式前对 Web 服务性能有清晰的了解,尽可能将性能参数相似的 Web 服务纳入批处理,而分别处理执行时间差异较大的 Web 服务。一般建议将性能差异在 30% 以内的多 Web 服务可以考虑纳入批处理。比方说 AccountWebService 中有一个获取用户账户列表的 Web 服务 getUserAccounts,这个 Web 服务执行需要 15 秒,另外 UserWebService 中有一个获取用户目前 pending 的待处理通知 getUserPendingNotifications,这个 Web 服务执行需要 2 秒时间,我们可以看到这两个 Web 服务执行时间差异较大,因此在这种情况下我们不建议将这两个 Web 服务纳入批处理。而 AccountWebService 中有一个增加第三方用户账号的 Web 服务 addThirdPartyNonHostAccount,该 Web 服务执行需要 3 秒,此时就就可以考虑能将 getUserPendingNotifications Web 服务和 addThirdPartyNonHostAccount 放在一个批处理中一次性调用处理。
2.不同 Web 服务业务相关性
一般情况下建议考虑将存在业务相关性的多 Web 服务放入批处理中,只有业务存在相关性的多 Web 服务才会涉及到减少调用次数以提高应用系统性能的需求。比方说用户在增加第三方账号 addThirdPartyNonHostAccount 以后会默认自动发送一条 pending 的 notification 给用户用以提示用户来激活增加的账号,因此这种场景下可以完美的将 addThirdPartyNonHostAccount Web 服务和 getUserPendingNotifications Web 服务放入一个批处理中,在用户增加完三方账号后系统自动刷新 pending notification 区域以提示用户激活账号。UserWebService 中有一个获取用户主账号的 Web 服务 getUserHostAccounts 和获取用户三方账号的 Web 服务 getUserNonHostAccounts,MetaDataService 中有一个获取国家金融机构假期数据的 Web 服务 getFinacialAgencyHolidays,该 Web 服务明显和 getUserHostAccounts,getUserNonHostAccounts 不存在业务上相关性,因此不应该将它们纳入批处理。
3.尽量避免将存在依赖关系的多 Web 服务放入同一个批处理中
将多个存在依赖关系的多 Web 服务放入同一批处理中需要专门考虑、处理多 Web 服务彼此间的依赖关系,进而无法将方便的这些 Web 服务并发执行而不得不串行执行有依赖关系的 Web 服务,最悲观情况下批处理响应时间将是批处理中所有 Web 服务串行执行时间和。原则上即使批处理中 Web 服务间存在依赖关系,通过动态指定依赖关系也可以实现多 Web 服务的批处理调用。但是这样将大大增加批处理实现的技术复杂性,因此不建议如此操作。
4.多线程方式处理批处理 Web 服务请求
批处理模式在服务实现端一般通过多线程处理方法来并发处理多个 Web 服务调用请求。通过集中的解析器解析批处理模式请求,之后针对每一个 Web 服务调用会启动一个单独的线程来处理此 Web 请求,同时会有一个总的线程管理器来调度不同 Web 服务执行线程,监控线程执行进度等。在所有线程执行完成后汇总 Web 服务执行结果返回客户端。
●批处理实现方式
批处理实现方式一般有两种:静态批处理模式,动态批处理模式:
静态批处理模式实现较为简单,但是相对缺乏灵活性。静态批处理的核心思想就是在已有 Web 服务的基础上通过组合封装的方式来得到批处理的目的。举例来说将系统中已有的 Web 服务请求结构组合成一个新的数据对象模型作为 Web 服务批处理请求结构,在客户端进行批处理调用时通过初始化批处理请求数据对象,并将特定的 Web 服务请求对象赋值给批处理请求对象属性的方式。同理在服务实现端在生成批处理响应数据对象时也是通过将具体 Web 服务的响应组合起来生成并返回客户端。
动态批处理模式实现较为复杂,但也能提供更大的操作灵活性。动态批处理模式一般需要应用采用 Java 反射 API 开发具有容器功能的批处理实现框架。客户端可以动态的向容器中增加 Web 服务调用请求,比方说客户端可以动态的将 addThirdPartyNonHostAccount,getUserPendingNotifications 两个 Web 服务加入到这个容器中然后发起一个框架提供的批处理 Web 服务调用请求。该批处理 Web 服务在实现端将解析容器并将其中的各个 Web 服务请求抽取解析并启动独立的线程来处理。
压缩 SOAP
当 Web Service SOAP 消息体比较大的时候,我们可以通过压缩 soap 来提高网络传输性能。通过 GZIP 压缩 SOAP 消息,得到二进制数据,然后把二进制数据作为附件传输。以前常规方法是把二进制数据 Base64 编码,但是 Base64 编码后的大小是二进制数据的 1.33 倍。辛苦压缩的,被 Base64 给抵消差不多了。是否可以直接传输二进制数据呢? JAX-WS 的 MTOM 是可以的,通过 HTTP 的 MIME 规范, SOAP message 可以字符,二进制混合。我们在 client 和 server 端各注册一个 handler 来处理压缩和解压。 由于压缩后的 SOAP 消息附件与消息体中的部分不是基于 MTOM 自动关联的,需要单独处理附件。在生成 client 端和 server 端代码的时候需要 enable MTOM。 Handler 具体代码在本文代码附件中, test.TestClientHanlder, test.TestServerHanlder。 写好了 handler 了之后还要为 service 注册 handler。
客户端 handler 样例代码如下:
客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public
boolean
handleMessage(MessageContext arg0) {
SOAPMessageContext ct = (SOAPMessageContext) arg0;
boolean
isRequestFlag = (Boolean) arg0
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
SOAPMessage msg = ct.getMessage();
if
(isRequestFlag) {
try
{
SOAPBody body = msg.getSOAPBody();
Node port = body.getChildNodes().item(
0
);
String portContent = port.toString();
NodeList list = port.getChildNodes();
for
(
int
i =
0
; i < list.getLength(); i++) {
port.removeChild(list.item(i));
}
ByteArrayOutputStream outArr =
new
ByteArrayOutputStream();
GZIPOutputStream zip =
new
GZIPOutputStream(outArr);
zip.write(portContent.getBytes());
zip.flush();
zip.close();
byte
[] arr = outArr.toByteArray();
TestDataSource ds =
new
TestDataSource(arr);
AttachmentPart attPart = msg.createAttachmentPart();
attPart.setDataHandler(
new
DataHandler(ds));
msg.addAttachmentPart(attPart);
}
catch
(SOAPException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
}
return
true
;
}
|
Web 服务端 handler 样例代码如下:
服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public
boolean
handleMessage(MessageContext arg0) {
SOAPMessageContext ct = (SOAPMessageContext) arg0;
boolean
isRequestFlag = (Boolean) arg0
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
SOAPMessage msg = ct.getMessage();
if
(!isRequestFlag) {
try
{
Object obj = ct.get(
"Attachments"
);
Attachments atts = (Attachments) obj;
List list = atts.getContentIDList();
for
(
int
i =
1
; i < list.size(); i++) {
String id = (String) list.get(i);
DataHandler d = atts.getDataHandler(id);
InputStream in = d.getInputStream();
ByteArrayOutputStream out =
new
ByteArrayOutputStream();
GZIPInputStream zip =
new
GZIPInputStream(in);
byte
[] arr =
new
byte
[
1024
];
int
n =
0
;
while
((n = zip.read(arr)) >
0
) {
out.write(arr,
0
, n);
}
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(
new
ByteArrayInputStream(out.toByteArray()));
SOAPBody body = msg.getSOAPBody();
Node port = body.getChildNodes().item(
0
);
port.appendChild(doc.getFirstChild().getFirstChild());
}
}
catch
(SOAPException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
catch
(SAXException e) {
e.printStackTrace();
}
catch
(ParserConfigurationException e) {
e.printStackTrace();
}
}
return
true
;
}
|
在 web.xml 中 service-ref 部分添加 handler. Server 端 handler 也是同样添加。
配置代码
1
2
3
4
5
6
7
8
9
|
<handler-chains>
<handler-chain>
<handler>
<handler-name>TestClientHandler</handler-name>
<handler-
class
>test.TestClientHandler
</handler-
class
>
</handler>
</handler-chain>
</handler-chains>
|
结束语
以上三种解决方案是根据笔者的经验和分析,针对 Web 服务当前所面临的性能瓶颈进行提出的。并且,这几种解决方案在实际项目使用中都取得了比较好的效果。 综上所述, 在实际项目中,根据不同的需求采用上述方法中一个或者多个组合,可以使 Web 服务性能更加优化。