QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

使用异步 Servlet 改进应用性能

  • 2013-11-16
  • 本文字数:3471 字

    阅读完需:约 11 分钟

Nikita Salnikov Tarnovski plumbr 的高级开发者,也是一位应用性能调优的专家,他拥有多年的性能调优经验。近日,Tarnovski撰文谈到了如何通过异步 Servlet 来改进常见的 Java Web 应用的性能问题。

众所周知,Servlet 3.0 标准已经发布了很长一段时间,相较于之前的 2.5 版的标准,新标准增加了很多特性,比如说以注解形式配置 Servlet、web.xml 片段、异步处理支持、文件上传支持等。虽然说现在的很多 Java Web 项目并不会直接使用 Servlet 进行开发,而是通过如 Spring MVC、Struts2 等框架来实现,不过这些 Java Web 框架本质上还是基于传统的 JSP 与 Servlet 进行设计的,因此 Servlet 依然是最基础、最重要的标准和组件。在 Servlet 3.0 标准新增的诸多特性中,异步处理支持是令开发者最为关注的一个特性,本文就将详细对比传统的 Servlet 与异步 Servlet 在开发上、使用上、以及最终实现上的差别,分析异步 Servlet 为何会提升 Java Web 应用的性能。

本文主要介绍的是能够解决现代 Web 应用常见性能问题的一种性能优化技术。当今的应用已经不仅仅是被动地等待浏览器来发起请求,而是由应用自身发起通信。典型的示例有聊天应用、拍卖系统等等,实际情况是大多数时间与浏览器的连接都是空闲的,等待着某个事件来触发。

这种类型的应用自身存在着一个问题,特别是在高负载的情况下问题会变得更为严重。典型的症状有线程饥饿、影响用户交互等等。根据近一段时间的经验,我认为可以通过一种相对比较简单的方案来解决这个问题。在 Servlet API 3.0 实现成为主流后,解决方案就变得更加简单、标准化且优雅了。

在开始介绍解决方案前,我们应该更深入地理解问题的细节。还有什么比看源代码更直接的呢,下面就来看看下面这段代码:

复制代码
@WebServlet(urlPatterns = "/BlockingServlet")
public class BlockingServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
long start = System.currentTimeMillis();
Thread.sleep(2000);
String name = Thread.currentThread().getName();
long duration = System.currentTimeMillis() - start;
response.getWriter().printf("Thread %s completed the task in %d ms.", name, duration);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}

上面这个 Servlet 主要完成以下事情:

  1. 请求到达,表示开始监控某些事件。
  2. 线程被阻塞,直到事件发生为止。
  3. 在接收到事件后,编辑响应然后将其发回给客户端。

为了简化,代码中将等待部分替换为一个 Thread.sleep() 调用。

现在,你可能会觉得这就是一个挺不错的 Servlet。在很多情况下,你的理解都是正确的,上述代码并没有什么问题,不过当应用的负载变大后就不是这么回事了。

为了模拟负载,我通过 JMeter 创建了一个简单的测试,我会启动 2,000 个线程,每个线程运行 10 次,每次都会向 /BlockedServlet 这个地址发出请求。将这个 Servlet 部署在 Tomcat 7.0.42 中然后运行测试,得到如下结果:

  • 平均响应时间:19,324ms
  • 最快响应时间:2,000ms
  • 最慢响应时间:21,869ms
  • 吞吐量:97 个请求 / 秒

默认的 Tomcat 配置有 200 个工作线程,此外再加上模拟的工作由 2,000ms 的睡眠时间来表示,这就能比较好地解释最快与最慢的响应时间了,每个线程都会睡眠 2 秒钟。再加上上下文切换的代价,因此 97 个请求 / 秒的吞吐量基本上是符合我们的预期的。

对于绝大多数的应用来说,这个吞吐量还算是可以接受的。重点来看看最慢的响应时间与平均响应时间,问题就变得有些严重了。经过 20 秒而不是期待的 2 秒才能得到响应显然会让用户感到非常不爽。

下面我们来看看另外一种实现,利用 Servlet API 3.0 的异步支持:

复制代码
@WebServlet(asyncSupported = true, value = "/AsyncServlet")
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Work.add(request.startAsync());
}
}
复制代码
public class Work implements ServletContextListener {
private static final BlockingQueue queue = new LinkedBlockingQueue();
private volatile Thread thread;
public static void add(AsyncContext c) {
queue.add(c);
}
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(2000);
AsyncContext context;
while ((context = queue.poll()) != null) {
try {
ServletResponse response = context.getResponse();
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.printf("Thread %s completed the task", Thread.currentThread().getName());
out.flush();
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
context.complete();
}
}
} catch (InterruptedException e) {
return;
}
}
}
});
thread.start();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
thread.interrupt();
}
}

上面的代码看起来有点复杂,因此在开始分析这个解决方案的细节信息之前,我先来概述一下这个方案:速度上提升了 75 倍,吞吐量提升了 20 倍。看到这个结果,你肯定迫不及待地想知道这个示例是如何做到的吧。

这个 Servlet 本身是非常简单的。需要注意两点,首先是声明 Servlet 支持异步方法调用:

复制代码
<pre dir="ltr">
@WebServlet(asyncSupported = true, value = "/AsyncServlet")

其次,重要的部分实际上是隐藏在下面这行代码调用中的。

复制代码
<pre dir="ltr">
Work.add(request.startAsync());

整个请求处理都被委托给了 Work 类。请求上下文是通过 AsyncContext 实例来保存的,它持有容器提供的请求与响应对象。

现在来看看第 2 个,也是更加复杂的类,Work 类实现了 ServletContextListener 接口。进来的请求会在该实现中排队等待通知,通知可能是上面提到的拍卖中的竞标价,或是所有请求都在等待的群组聊天中的下一条消息。

当通知到达时,我们这里依然是通过 Thread.sleep() 让线程睡眠 2,000ms,队列中所有被阻塞的任务都是由一个工作线程来处理的,该线程负责编辑与发送响应。相对于阻塞成百上千个线程以等待外部通知,我们通过一种更加简单且干净的方式达成所愿,通过批处理在单独的线程中处理请求。

还是让结果来说话吧,测试配置与方才的示例一样,依然使用 Tomcat 7.0.24 的默认配置,测试结果如下所示:

  • 平均响应时间:265ms
  • 最快响应时间:6ms
  • 最慢响应时间:2,058ms
  • 吞吐量:1,965 个请求 / 秒

虽然说这个示例很简单,不过对于实际项目来说通过这种方式依然能获得类似的结果。

在将所有的 Servlet 改写为异步 Servlet 前,请容许我多说几句。该解决方案非常适合于某些应用场景,比如说群组通知与拍卖价格通知等。不过,对于等待数据库查询完成的请求来说,这种方式就没有什么必要了。像往常一样,我必须得重申一下——请通过实验进行度量,而不是瞎猜。

对于那些不适合于这种解决方案的场景来说,我还是要说一下这种方式的好处。除了在吞吐量与延迟方面带来的显而易见的改进外,这种方式还可以在大负载的情况下优雅地避免可能出现的线程饥饿问题。

另一个重要的方面,这种异步处理请求的方式已经是标准化的了。它不依赖于你所使用的 Servlet API 3.0,兼容于各种应用服务器,如 Tomcat 7、JBoss 6 或是 Jetty 8 等,在这些服务器上这种方式都可以正常使用。你不必再面对各种不同的 Comet 实现或是依赖于平台的解决方案了,比如说 Weblogic FutureResponseServlet。

就如本文一开始所提的那样,现在的 Java Web 项目很少会直接使用 Servlet API 进行开发了,不过诸多的 Web MVC 框架都是基于 Servlet 与 JSP 标准实现的,那么在你的日常开发中,是否使用过出现多年的 Servlet API 3.0,使用了它的哪些特性与 API 呢?

2013-11-16 04:019326
用户头像

发布了 88 篇内容, 共 264.3 次阅读, 收获喜欢 8 次。

关注

评论 1 条评论

发布
用户头像
Thread.sleep(2000); 实际指的是什么?
2019-07-24 12:52
回复
没有更多了
发现更多内容

从零手写react-router

helloworld1024fd

JavaScript

React中常见的TypeScript定义实战

xiaofeng

React

【GitHub60K+Star】12W字Java后端技术总结,助力2023年春招

程序知音

程序员 java面试 面试技巧 后端技术 Java面试八股文

一文总结JavaScript手写面试题

helloworld1024fd

JavaScript

深入浅出文件系统新形态

焱融科技

云计算 高性能 文件存储

React源码解读之更新的创建

flyzz177

React

那些年面挂的js手写题

helloworld1024fd

JavaScript

现在加入写作社区,惊喜等你开启!

InfoQ写作社区官方

热门活动

web前端培训有哪些比较好?

小谷哥

前端培训机构学习比较好的方法

小谷哥

大数据培训自学怎么样?

小谷哥

前端线下培训和线上培训学习哪个更好?

小谷哥

从零开始实现一个Promise

helloworld1024fd

JavaScript

安防小间距LED显示屏的解决方案是什么

Dylan

LED显示屏 户外LED显示屏 led显示屏厂家

从延迟处理讲起,JavaScript 也能惰性编程?

掘金安东尼

前端 11月月更

react hook 源码完全解读

flyzz177

React

React-hooks+TypeScript最佳实战

xiaofeng

React

java程序员培训和自学的区别

小谷哥

【LeetCode】验证栈序列Java题解

Albert

算法 LeetCode 11月月更

为何大企业都纷纷选择低代码做数字化转型?

优秀

数字化 低代码开发

React源码中的dom-diff

夏天的味道123

React

【云服务器】云服务器哪家好用便宜服务好?

行云管家

云计算 企业上云 云服务器 行云管家

React核心技术浅析

夏天的味道123

React

想开发DAYU200,我教你

华为云开发者联盟

开发 华为云 开发板 企业号十月 PK 榜 富设备

React生命周期深度完全解读

夏天的味道123

React

9个GaussDB常用的对象语句

华为云开发者联盟

数据库 后端 华为云 企业号十月 PK 榜

热备与冷备分别是什么意思?怎么通俗理解?

行云管家

高可用 热备 冷备

React-diff原理及应用

xiaofeng

React

什么是 HTML 语义化,有什么好处

肥晨

11月月更 HTML语义化 语义化标签

分享10个降低PCB成本的技巧!可收藏

华秋PCB

PCB PCB打样 PCB设计

React源码解读之任务调度

flyzz177

React

使用异步Servlet改进应用性能_语言 & 开发_张龙_InfoQ精选文章