配色: 字号:
使用异步servlet提升性能
2016-12-09 | 阅:  转:  |  分享 
  
使用异步servlet提升性能

本文发布之后,收到了很多的反馈。基于这些反馈,我们更新了文中的示例,使读者更容易理解和掌握,如果您发现错误和遗漏,希望能给我们提交反馈,帮助我们改进。



本文针对当今webapp中一种常碰到的问题,介绍相应的性能优化解决方案。如今的WEB程序不再只是被动地等待浏览器的请求,他们之间也会互相进行通信。典型的场景包括在线聊天,实时拍卖等——后台程序大部分时间与浏览器的连接处于空闲状态,并等待某个事件被触发。



这些应用引发了一类新的问题,特别是在负载较高的情况下。引发的状况包括线程饥饿,影响用户体验、请求超时等问题。



基于这类应用在高负载下的实践,我会介绍一种简单的解决方案。在Servlet3.0成为主流以后,这是一种真正简单、标准化并且十分优雅的解决方案。



在演示具体的解决方案前,我们先了解到底发生了什么问题。请看代码:



@WebServlet(urlPatterns="/BlockingServlet")

publicclassBlockingServletextendsHttpServlet{



protectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse){

waitForData();

writeResponse(response,"OK");

}



publicstaticvoidwaitForData(){

try{

Thread.sleep(ThreadLocalRandom.current().nextInt(2000));

}catch(InterruptedExceptione){

e.printStackTrace();

}

}

}

此servlet所代表的情景如下:



每2秒会有某些事件发生,例如,报价信息更新,聊天信息抵达等。

终端用户请求对某些特定事件进行监听。

线程暂时被阻塞,直到收到下一次事件。

接收到事件时,处理响应信息并发送给客户端

下面解释一下这个等待场景。我们的系统,每2秒触发一次外部事件。当收到用户请求时,需要等待一段时间,大约是0到2000毫秒之间,直到下一次事件发生.为了演示的需要,此处通过调用Thread.sleep()来模拟随机的等待时间。平均每个请求等待1秒左右。



现在,你可能会觉得这是一个十分普通的servlet。在多数情况下,确实是这样——代码并没有错误,但如果系统面临大量的并发负载时就会力不从心了。



为了模拟这种负载,我用JMeter创建了一个简单的测试,启动2000个线程,每个线程执行10次请求来进行系统压力测试。



请求的URI为/BlockedServlet,部署在Tomcat8.0.30默认配置下,测试结果如下:



平均响应时间:9,492ms

最小响应时间:205ms

最大响应时间:11,368ms

吞吐量:195个请求/秒

Tomcat默认配置的是200个worker线程,再加上模拟的工作量(平均线程休眠1000ms),很好地解释了吞吐量数据-200个线程每秒应该能够完成200次执行周期,平均1秒钟左右.但有一些上下文切换的成本,所以吞吐量为195个请求/秒,很符合我们的预期。



对99.9%的应用来说,这个吞吐量数据看上去也很正常。但看看最大响应时间,以及平均响应时间,就会发现问题实在是太严重了。在最坏情况下客户端居然需要11秒才能得到响应,而预期是2秒,这对用户来说一点都不友好。



下面我们看另一种实现,使用了Servlet3.0的异步特性:



@WebServlet(asyncSupported=true,value="/AsyncServlet")

publicclassAsyncServletextendsHttpServlet{



protectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{

addToWaitingList(reqwww.baiyuewang.netuest.startAsync());

}



privatestaticScheduledExecutorServiceexecutorService=Executors.newScheduledThreadPool(1);



static{

executorService.scheduleAtFixedRate(AsyncServlet::newEvent,0,2,TimeUnit.SECONDS);

}



privatestaticvoidnewEvent(){

ArrayListclients=newArrayList<>(queue.size());

queue.drainTwww.wang027.como(clients);

clients.parallelStream().forEach((AsyncContextac)->{

ServletUtil.writeResponse(ac.getResponse(),"OK");

ac.complete();

});

}



privatestaticfinalBlockingQueuequeue=newArrayBlockingQueue<>(20000);



publicstaticvoidaddToWaitingList(AsyncContextc){

queue.add(c);

}

}

上面的代码稍微有一点复杂,所以我先透露一下此方案的性能表现:响应延迟(latency)只有原来的1/5;而吞吐量(throughput-wise)也提升了5倍。看到这样的结果,你肯定想深入了解第二种方案了吧。



servlet的doGet方法看起来很简单。有两个地方值得提一下:



一是声明servlet,以及支持异步方法调用:



@WebServlet(asyncSupported=true,value="/AsyncServlet")

二是方法addToWaitingList中的细节:



publicstaticvoidaddToWaitingList(AsyncContextc){

queue.add(c);

}

在其中,整个请求的处理只有一行代码,将AsyncContext实例加入队列中。AsyncContext里含有容器提供的request和response对象,我们可以通过他们来响应用户请求.因此传入的请求在等待通知——可能是监视的拍卖组中的报价更新事件,或者是下一条群聊消息。这里需要注意的是,将AsyncContext加入队列以后,servlet容器的线程就完成了·doGet·操作,然后释放出来,可以去接受另一个新请求了。



现在,系统通知每2秒到达一次,当然这部分我们通过static块中的调度事件实现了,每2秒会执行一次newEvent方法.当通知到来时,队列中所有在等待的请求都由同一个worker线程负责处理并发送响应消息。这次的代码,没有阻塞几百个线程来等待外部事件通知,而是用更简洁明了的方法来实现了,把感兴趣的请求放在一个group中,由单个线程进行批量处理。



结果不用说,同样的配置,同样的测试,Tomcat8.0.30服务器跑出了以下结果:



平均响应时间:1,875ms

最小响应时间:356ms

最大响应时间:2,326ms

吞吐量:939个请求/秒

虽然示例是手工构造的,但类似的性能提升在现实世界中却是很普遍的。



现在,请不要急着去将所有的servlet重构为异步servlet。因为这种方案,只在满足某些特征的任务才会得到大量性能提升,比如聊天室,或者拍卖价格提醒之类的。而对于需要请求底层数据库之类的操作,很可能没有性能提升。所以,就像以前一样,我必须重申,我最喜欢的性能优化忠告——请权衡考虑整件事情,不要想当然。



但如果确实符合此方案适应的情景,那我就恭喜你啦!不仅能明显改进吞吐量和延迟,还能在大量的并发压力下表现出色,避免可能的线程饥饿问题。



另一个重要信息是——异步请求的处理终于标准化了。兼容Servlet3.0的应用服务器——比如Tomcat7+,JBoss6或者Jetty8+——都支持这种方案.再也不用陷进那些耦合具体平台的解决方案里,例如WeblogicFutureResponseServlet。

献花(0)
+1
(本文系thedust79首藏)