写点什么

使用异步 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:019309
用户头像

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

关注

评论 1 条评论

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

python 函数二三事

AIWeker

Python python小知识 7月月更

详细页返回列表保留原来滚动条所在位置

小恺

7月月更

【愚公系列】2022年7月 Go教学课程 004-Go代码注释

愚公搬代码

7月月更

MMAP

北洋

Andriod 7月月更

场景化面试:关于分布式锁的十问十答

面试官问

分布式锁

小程序容器可以发挥的价值

Geek_99967b

小程序 小程序容器

Spring你牛个啥,我承认刚才说话我声音有点大

zxhtom

7月月更

刷个算法,结果第一题就蚌埠住了~~

为自己带盐

算法 力扣 7月月更

深入理解计算机系统(CSAPP)第1章计算机系统漫游

小明Java问道之路

计算机基础 csapp 计算机结构 7月月更 解读

Promise

Jason199

Promise 7月月更

如何组织一场实战攻防演练

穿过生命散发芬芳

攻防演练 7月月更

图解网络:TCP三次握手背后的原理,为啥两次握手不可以?

wljslmz

TCP 三次握手 网络协议 网络技术 7月月更

看抖音直播Beyond演唱会有感

Empty

一文读懂简单查询代价估算

华为云开发者联盟

数据库 后端 查询引擎

使用 RepositoryProvider简化父子组件的传值

岛上码农

flutter ios 安卓 移动端开发 7月月更

【刷题记录】1. 两数之和

WangNing

7月月更

分布式算法入门之 Paxos 算法

宇宙之一粟

Basic paxos 7月月更

synchronized 和 ReentrantLock

zarmnosaj

7月月更

不要再手动批量替换了,使用python AST模块批量替换

阿呆

Python AST 批量替换

Ubuntu 20.04 安装 Chisel

贾献华

7月月更

牛客java选择题每日打卡Day7

京与旧铺

7月月更

从 1.5 开始搭建一个微服务框架——调用链追踪 traceId

悟空聊架构

日志 链路追踪 traceId 悟空聊架构 7月月更

AI金榜题名时,MLPerf榜单的份量究竟有多重?

脑极体

XaaS 陷阱:万物皆服务(可能)并不是IT真正需要的东西

雨果

云服务 xaas DaaS 本地服务

systemd-resolved 开启 debug 日志

程序员与厨子

ubuntu 运维 DNS systemd-resolved

如何开发引入小程序插件

Geek_99967b

小程序插件

国内低代码开发平台靠谱的都有哪些?

AIRIOT

低代码 物联网 低代码,项目开发

让开发效率飞速提升的跨端方案

Geek_99967b

小程序 跨端 小程序容器

微服务链路风险分析

阿泽🧸

7月月更 链路风险分析

华为云ModelArts文本分类–外卖评论

逝缘~

深度学习 华为云 7月月更

Java方向~~0基础小白如何快速脱离0offer的苦海!

KEY.L

7月月更

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