servlet异步
这篇文章将描述一种性能优化技术,该技术适用于与现代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是上面描述的应用程序的外观示例:
- 请求到达,宣布有兴趣监视某些事件
- 线程被阻塞,直到事件到达
- 收到事件后,响应将被编译并发送回客户端
为了简单起见,我们将等待部分替换为对Thread.sleep()的调用。
现在,您可能会认为这是一个完全正常的servlet。 在许多情况下,您是完全正确的–在应用程序面临大量负载之前,代码没有错。
为了模拟此负载,我在JMeter的帮助下创建了一个相当简单的测试,在该测试中,我启动了2,000个线程,每个线程运行10次迭代,以对/ BlockedServlet的请求轰炸应用程序。 在现成的Tomcat 7.0.42上使用已部署的servlet运行测试,我得到以下结果:
- 平均响应时间:19,324毫秒
- 最小响应时间:2,000毫秒
- 最大响应时间:21,869 ms
- 吞吐量:97个请求/秒
Tomcat的默认配置有200个工作线程,再加上将模拟工作替换为2,000ms睡眠周期这一事实很好地说明了最小和最大响应时间– 200秒中的每个线程应该能够完成100个睡眠周期,每个2秒。 最重要的是,加上上下文切换成本,达到的97个请求/秒的吞吐量非常接近我们的预期。
对于99.9%的应用程序而言,吞吐量本身看起来不会太差。 但是,从最大响应时间(尤其是平均响应时间)来看,问题开始变得更加严重。 在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);}@Overridepublic void contextInitialized(ServletContextEvent servletContextEvent) {thread = new Thread(new Runnable() {@Overridepublic 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();}@Overridepublic void contextDestroyed(ServletContextEvent servletContextEvent) {thread.interrupt();}
}
这部分代码稍微复杂一点,所以也许在我们开始深入研究解决方案细节之前,我可以概述一下该解决方案在延迟方面的性能提高了约75倍,在吞吐量方面的性能提高了约20倍 。 掌握了此类结果的知识后,您应该更加有动力去理解第二个示例中的实际情况。
servlet本身看起来确实很简单。 但是,有两个事实值得概述,第一个事实声明了该Servlet支持异步方法调用:
@WebServlet(asyncSupported = true, value = "/AsyncServlet")
第二个重要方面隐藏在以下行中
Work.add(request.startAsync());
其中整个请求处理都委托给Work类。 使用AsyncContext实例存储请求的上下文,该实例保存由容器提供的请求和响应。
现在,第二个也是更复杂的类–以ServletContextListener实现的Work开始看起来更简单。 传入的请求只是在实现中排队等待通知-这可能是对受监控拍卖的更新出价,或者是群聊中所有请求都在等待的下一条消息。
通知到达时-再次简化为仅在Thread.sleep()中等待2,000ms,队列中所有被阻止的任务都由一个负责编译和发送响应的工作线程处理。 我们没有阻塞数百个线程来等待外部通知,而是以一种更简单,更简洁的方式实现了这一点-将兴趣组批处理在一起,并在单个线程中处理请求。
结果不言而喻–在具有默认配置的相同Tomcat 7.0.24上进行的相同测试得出以下结果:
- 平均响应时间:265毫秒
- 最小响应时间:6毫秒
- 最长响应时间:2,058毫秒
- 吞吐量:1,965请求/秒
此处的具体情况很小且综合,但在实际应用中可以实现类似的改进。
现在,在您将所有servlet重写为异步servlet之前-稍等片刻。 该解决方案可以完美地用在部分用例上,例如群聊通知和拍卖行价格警报。 对于请求在唯一数据库查询完成后等待的情况,您很可能不会从中受益。 因此,与往常一样,我必须重申我最喜欢的与性能相关的建议–衡量所有事情。 什么都不要猜。
但是在问题确实适合解决方案形状的情况下,我只能称赞它。 除了对吞吐量和延迟的明显改进之外,我们还优雅地避免了在高负载下可能出现的线程不足问题。
另一个重要方面–异步请求处理方法最终实现了标准化。 独立于您最喜欢的Servlet API 3.0 –兼容应用程序服务器(例如Tomcat 7 , JBoss 6或Jetty 8),您可以确定该方法有效。 不再为不同的Comet实现或与平台相关的解决方案(例如Weblogic FutureResponseServlet)而费力 。
翻译自: https://www.javacodegeeks.com/2013/10/how-to-use-asynchronous-servlets-to-improve-performance.html
servlet异步