使用异步servlet提升性能

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

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

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

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

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

@WebServlet(urlPatterns = "/BlockingServlet")
public class BlockingServlet extends HttpServlet {

  protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    waitForData();
    writeResponse(response, "OK");
  }

  public static void waitForData() {
    try {
      Thread.sleep(ThreadLocalRandom.current().nextInt(2000));
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

此 servlet 所代表的情景如下:

  • 每2秒会有某些事件发生, 例如, 报价信息更新, 聊天信息抵达等。
  • 终端用户请求对某些特定事件进行监听。
  • 线程暂时被阻塞, 直到收到下一次事件。
  • 接收到事件时, 处理响应信息并发送给客户端

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

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

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

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

  • 平均响应时间: 9,492 ms
  • 最小响应时间: 205 ms
  • 最大响应时间: 11,368 ms
  • 吞吐量: 195 个请求/秒

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

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

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

@WebServlet(asyncSupported = true, value = "/AsyncServlet")
public class AsyncServlet extends HttpServlet {

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    addToWaitingList(request.startAsync());
  }

  private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

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

  private static void newEvent() {
    ArrayList clients = new ArrayList<>(queue.size());
    queue.drainTo(clients);
    clients.parallelStream().forEach((AsyncContext ac) -> {
      ServletUtil.writeResponse(ac.getResponse(), "OK");
      ac.complete();
    });
  }

  private static final BlockingQueue queue = new ArrayBlockingQueue<>(20000);

  public static void addToWaitingList(AsyncContext c) {
    queue.add(c);
  }
}

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

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

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

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

二是方法 addToWaitingList 中的细节:

  public static void addToWaitingList(AsyncContext c) {
    queue.add(c);
  }

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

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

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

  • 平均响应时间: 1,875 ms
  • 最小响应时间: 356 ms
  • 最大响应时间: 2,326 ms
  • 吞吐量: 939 个请求/秒

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

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

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

另一个重要信息是 —— 异步请求的处理终于标准化了。兼容 Servlet 3.0 的应用服务器 —— 比如 Tomcat 7+, JBoss 6 或者 Jetty 8+ —— 都支持这种方案. 再也不用陷进那些耦合具体平台的解决方案里, 例如 Weblogic FutureResponseServlet

原文链接: https://plumbr.eu/blog/java/how-to-use-asynchronous-servlets-to-improve-performance

翻译人员: 铁锚 http://blog.csdn.net/renfufei

翻译时间: 2016年12月08日

时间: 2024-10-13 00:06:56

使用异步servlet提升性能的相关文章

使用异步HTTP提升客户端性能(HttpAsyncClient)

大家都知道,应用层的网络模型有同步.异步之分. 同步,意为着线程阻塞,只有等本次请求全部都完成了,才能进行下一次请求. 异步,好处是不阻塞当前线程,可以“万箭齐发”的将所有请求塞入缓冲区,然后谁的请求先完成就处理谁. 大家也注意到了,同步模式阻塞的只是“线程”.实际上,在异步模式流行之前,人们也经常用多线程的方式处理并发请求.然而,随着数据规模的不断加大,线程开销所带来的CPU.内存剧增,因此这种方法的应用比较有限. 近几年来,随着异步处理方案在node.js.Nginx等系统中的成功应用,异步

关于servlet3.0中的异步servlet

刚看了一下维基百科上的介绍,servlet3.0是2009年随着JavaEE6.0发布的: 到现在已经有六七年的时间了,在我第一次接触java的时候(2011年),servlet3.0就已经出现很久了,但是到现在,里边的一些东西还是没有能够好好地了解一下 最近在研究java的长连接,在了解jetty中的continuations机制的时候也重新了解了一下servlet3.0中的异步servlet机制,通过看几个博客,加上自己的一些测试,算是搞明白了一些,在这里记录一下: 在服务器的并发请求数量比

提升性能-事件委托技术

--- title: 提升性能——事件委托技术 date: 2016-05-11 22:13:43 tags: [javascript,improving performance, font-end] --- 提升页面性能之事件委托技术 (整理摘选自<Javascript高级程序设计>)概述 利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件. 实际例子 HTML部分 <ul id = "mylinks"> <li id = "g

Struts2、SpringMVC、Servlet(Jsp)性能对比 测试

Struts2.SpringMVC.Servlet(Jsp)性能对比 测试 . Servlet的性能应该是最好的,可以做为参考基准,其它测试都要向它看齐,参照它. 做为一个程序员,对于各个框架的性能要有一个基本的认知,便于选型时做出正确的决策. 在测试中发现了什么也不要大喊大叫,因为这些都是Java程序员的基础知识. 人人都要了解. ----------------------------------------------------------------------------------

paip.提升性能3倍--使用栈跟VirtualAlloc代替堆的使用.

#----为什么要设计堆栈,它有什么独特的用途? 为了性能 ....  堆比栈的性能 也有的说法为了编程容易...这个是错误的.因为使用堆+func也能实现编程简单地.. #----为什么stack 比堆快,stackAccess 要快两到三倍 主要的2点::  使用堆额外的操作多,而且机器硬件上直接支持栈操作.. 堆栈都是一段内存条中的内存区域,感觉上,应该上没有多大的访问速度差别..但是,实际上,还是有很大的的速度效率区别.. 1.存取路径短1倍. 堆的分配/释放都要比栈要慢的多 结论:可以

Android ViewPager Fragment使用懒加载提升性能

?? Android ViewPager Fragment使用懒加载提升性能 Fragment在如今的Android开发中越来越普遍,但是当ViewPager结合Fragment时候,由于Android ViewPager内在的加载机制,导致一个比较严重的加载性能问题,具体来说,假设一个ViewPager中有n多个Fragment,那么ViewPager在初始化阶段将一次性的初始化FragmentPagerAdapter中的至少3个Fragment(如果Fragment多于3),创建和加载Fra

Other - 02 - Servlet学习笔记 - 异步Servlet

异步Servlet Servlet默认情况下都是同步的,但是Servlet可以进行异步的调用. /** * Servlet implementation class MainServlet */ @WebServlet(value = "/MainServlet", asyncSupported = true) public class AsynServlet extends HttpServlet { private static final long serialVersionUI

通过jdbc使用PreparedStatement,提升性能,防止sql注入

为什么要使用PreparedStatement? 一.通过PreparedStatement提升性能 Statement主要用于执行静态SQL语句,即内容固定不变的SQL语句.Statement每执行一次都要对传入的SQL语句编译一次,效率较差.     某些情况下,SQL语句只是其中的参数有所不同,其余子句完全相同,适用于PreparedStatement.PreparedStatement的另外一个好处就是预防sql注入攻击     PreparedStatement是接口,继承自State

使用异步存储提升 Web 应用程序的离线体验

localForage 是一个 JavaScript 库,通过使用简单的.类似 localStorage 风格的 API 实现异步存储,帮助你提升 Web 应用程序的离线经验(通过 IndexedDB 或 WebSQL). localForage 同时支持回调和 Promises 模式两个 API,你可以根据自己的喜好进行选择. 您可能感兴趣的相关文章 Web 开发中很实用的10个效果[附源码下载] 精心挑选的优秀jQuery Ajax分页插件和教程 12款经典的白富美型 jQuery 图片轮播