HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

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

关注

评论 1 条评论

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

最新大文件传输解决方案,一站式解决网站文件上传下载速度慢问题

镭速

大文件传输

文心一言 VS 讯飞星火 VS chatgpt (149)-- 算法导论12.2 4题

福大大架构师每日一题

福大大架构师每日一题

结算系统开发,直销系统开发,交易所开发,dapp开发,公链开发 商城开发,

Geek_8da502

安全加密传输大文件对金融行业的意义以及重要性

镭速

大文件传输 加密传输大文件

Microsoft Word LTSC 2021 for mac v16.79.2永久激活版

mac

word 苹果mac Windows软件 文字处理软件

11 | 排序(上):为什么插入排序比冒泡排序更受欢迎

鲁米

大数据 - MapReduce:从原理到实战的全面指南

快乐非自愿限量之名

数据库 大数据 工作原理

智能联动第三方告警中心,完美实现故障响应全闭环

观测云

人工智能 监控 智能告警

Ulysses for Mac(Markdown文本编辑软件) 33中文激活版

mac

文本编辑器 苹果mac Windows软件 Ulysses

特权账号管理之医疗行业篇

尚思卓越

数据库 运维 网络安全

Amoro 试用&贡献活动 | 10月社区评选揭晓

Amoro Community

大数据 开源 湖仓一体

腾讯云大数据ES荣获信通院优秀案例奖,云原生实力再获认可!

腾讯云大数据

ES

数实融合!低代码推动工业数字化转型走“深”向“实”

优秀

低代码 数字化转型 工业数字化

开源 | Spark Commiter 深度解读:Apache Spark Native Engine

网易数帆

大数据 spark 开源 Gluten

一文读懂 ChatGPT 工作原理

秃头小帅oi

低代码 AIGC ChatGPT

Netty源码学习7——netty是如何发送数据的

不在线第一只蜗牛

学习 源码 Netty

2023Q4 私有化版本发布,和鲸 ModelWhale 持续赋能大科研、高校教改的 AI for Science

ModelWhale

人工智能 云计算 数据分析 超算 私有化部署

又添三位“信伙伴”,亚信安慧AntDB数据库与南京一鸣、广东鸿数、北京数见完成兼容互认

亚信AntDB数据库

数据库 AntDB AntDB数据库

【哈尔滨学院主办】第三届高性能计算与通信工程国际学术会议(HPCCE 2023)

搞科研的小刘

通信 计算机 高性能计算 通信工程

Spring Boot 项目代码混淆实战:保护代码安全,防止泄露

深入解析Linux进程管理机制

EquatorCoco

Linux 运维

人工智能与供应链行业融合:开启智能化供应链的新时代

不在线第一只蜗牛

人工智能 供应链 智能化

释放潜能:IT外包服务对业务增长的强大推动

Ogcloud

外包 IT 外包公司 外包项目 IT 运维

Java医院绩效考核系统源码支持二次开发

源码星辰

Java

分享一个LCD驱动框架

不在线第一只蜗牛

教程 开发框架 lcd

阿里巴巴中国站按关键字搜索商品 API 的调用频率限制是多少?

技术冰糖葫芦

API 开发

如何实现通过API接口对商品信息进行实时更新和维护?

技术冰糖葫芦

API 文档

安全测试工具Burpsuit和OWASP ZAP使用入门指南

快乐非自愿限量之名

测试工具 安全测试 入门指南

理解意图,加速迈向L4高度自智网络

鲸品堂

意图识别 自智网络 12 月 PK 榜

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