360SDN.COM

使用异步servlet提升性能

来源:百知教育   2018-09-14 16:56:53    评论:0点击:

如今的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。

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权