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:019323
用户头像

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

关注

评论 1 条评论

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

刘强:作业帮给OceanBase提了九条意见

OceanBase 数据库

数据库 oceanbase

全球首个开发者村“开村”!数字之光在何处点亮?

白洞计划

叹服!阿里自述SpringCloud微服务:入门+实战+案例

做梦都在改BUG

Java 架构 微服务 Spring Cloud

面试被Spring Cloud拿捏?莫慌,阿里人用五个模块讲明白了SpringCloud微服务架构

做梦都在改BUG

Java 架构 微服务 Spring Cloud

膜拜!阿里人用10W字面经把Java面试官拿下了

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

SpringBoot 项目解决跨域的几种方案

做梦都在改BUG

Java Spring Boot

小红书如何应对万亿级社交网络关系挑战?图存储系统 REDtao 来了!

小红书技术REDtech

云原生 存储 图数据库 跨云多活

广立微大数据平台全线升级 为芯片全生命周期保驾护航

科技热闻

Wallys AP controllers devices/PQ4019 and IPQ4029 chipsets support 20 km remote transmission

Cindy-wallys

IPQ4019 ipq4029

Github高赞!Alibaba最新亿级并发系统架构(2023 版全彩小册)

Java你猿哥

Java 架构 分布式 高并发 架构设计

太强了!阿里人用138个案例讲明白了Spring全家桶+Docker+MQ

做梦都在改BUG

Java spring 微服务 Spring Cloud Spring Boot

即时通讯技术文集(第14期):WebSocket精华文章合集 [共15篇]

JackJiang

网络编程 即时通讯 IM

一条SQL如何被MySQL架构中的各个组件操作执行的

华为云开发者联盟

sql 开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜

从0到100:小区物业报修小程序开发笔记

CC同学

面试官:如何保证 RabbitMQ 的消息可靠性

做梦都在改BUG

Java 面试 RabbitMQ 消息队列 消息中间件

chatGPT是割韭菜的镰刀还是创业的新风口? | 社区征文

迷彩

AIGC 生成式人工智能 三周年征文 三周年连更

八股MQ001——为什么需要使用MQ?

Codyida

后端

多种文件清理:Disk Cleanup Pro 激活版

真大的脸盆

Mac Mac 软件 磁盘清理 清理工具

SpringBoot自动配置原理详解

做梦都在改BUG

Java Spring Boot

Spring Security 中的基本认证过滤器链

Java架构历程

Java spring security 三周年连更

分享:集群吞吐量以1抵5,车企MySQL八大痛点的解决方案

OceanBase 数据库

数据库 oceanbase

数说热点 | 跟着《长月烬明》起飞,今年各地文旅主打的就是一个听劝

MobTech袤博科技

GPIO实验-主芯片GPIO输出实验

鸿蒙之旅

OpenHarmony 三周年连更

Zero-ETL、大模型和数据工程的未来

Baihai IDP

人工智能 大模型 数据工程 企业号 5 月 PK 榜 LLMs

实战解读:隐钥科技数据库加密解决方案及场景化解析

Lily

第四范式开源强化学习研究通用框架,支持单智能体、多智能体训练,还可训练自然语言任务!训练速度提升17%

Geek_32eb82

病假单|病假条|体检报告|诊断证明书|病历证明|医院化验单|ct报告|b超单|怀孕检查

病假条病假单

从0开始:活动打卡小程序开发笔记

CC同学

SAPUI5 本地工程中的键值对 sapux - true 的作用

汪子熙

前端开发 SAP Fiori SAP UI5 三周年连更

硬核!阿里自爆虐心万字面试手册,Github上获赞89.7K

做梦都在改BUG

Java 程序员

OceanBase 4.0(小鱼)入选2023数字中国建设峰会“十大硬核科技”!

OceanBase 数据库

数据库 oceanbase

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