1 Servlet在高并发请求时存在的问题
现今的Web项目大都使用框架开发,而不是采用Servlet直接进行开发,但是框架本质上还是基于传统的JSP与Servlet进行设计的,因此Servlet依然是很重要的JavaEE标准组件。
大部分的web请求都是从浏览器端发出的,现今有越来越多的请求是从应用发出的,而不需要客户端不断刷新[1],比如web聊天室,拍卖系统等,是由应用自身发起通信,也叫做服务端推技术[2-4]。这些应用往往都是高负载的,很多用户并发使用Servlet,而此时如果Servlet中存在某段代码是需要较长时间运行的(比如获取数据库连接,读取文件系统数据等),就会导致当前线程阻塞,这样短时间内访问web服务器的线程增加会很快,当达到服务器设置的峰值时,其他请求就只能处于“饥饿状态”。
遇到阻塞我们自然能够想到的就是分配一个线程,让代码异步执行。Servlet3.0提供了一个这样的特性,许多Web服务器对其有很好的支持,比如Tomcat7及以上版本。下面就分析一下这个新特性在面对多线程和耗时任务时与传统Servlet的对比。
2 传统Servlet与异步Servlet的开发及性能对比
2.1 传统的Servlet处理
我们知道Servlet是由Sun公司提出的动态Web资源服务器开发技术, Servlet通过HTTP协议与Web浏览器进行信息交互,交互完成后Servlet将生成响应返回给Web服务器[5-6]。
在第一次请求Servlet时会创建一个新的Servlet实例, 此后无需实例化任何新对象, 创建出Servlet对象会一直驻留在内存中为该Servlet访问服务[7-9]。下面是一个典型Servlet的处理请求的方法,模拟了一个长时间处理任务:
其中,我们用线程睡眠10秒钟来模拟长时间任务执行。我们发出请求之后会在响应页面显示:Thread http-bio-8081-exec-3 completed the task in 10000 ms。
2.2 同时访问Servlet解析
下面我们来模拟很多请求同时到达访问该Servlet,来看响应结果是怎样的?
我们使用工具JMeter来模拟并发(模拟60个线程同时发出请求),响应结果如下:
我们设置tomcat7服务器的线程池最大线程数目默认是20,显然我们模拟的60个线程数超过了限制,短时间内迅速超过了20,所以新来的线程处于等待状态,上图看出每秒钟处理的线程吞吐量为2个,平均响应每个请求将近20秒,响应时间几乎是我们期待的2倍。
这和tomcat本身对线程的处理机制有关系的,当请求到达后,tomcat派发工作线程给予处理,在整个过程中该线程一直被占用,处于繁忙状态,一直到返回响应,如图:
我们再来看tomcat对异步Servlet处理过程,示意图如下:
将容器线程池和业务线程池分离开。在处理长时间的业务操作的时候,把这个操作移动到业务线程池中进行,释放容器线程,使得容器线程处理其他任务,在业务逻辑执行完毕之后,再去通知tomcat容器线程池来继续后面的操作,这个操作应该是把处理结果commit到客户端或者是dispatch到其他Servlet上。
3 开发异步Servlet过程
首先Servlet, @WebServlet的属性asyncSupported 值设置为true。
通过ServletRequest.startAsync方法获取AsyncContext的实例。
当在支持异步处理的Servlet或过滤器中调用请求对象的startAsync()方法时,该次请求会离开容器所分配的线程,这意味着必须响应处理流程会返回,也就是若有过滤器,也会依序返回(也就是各自完成FilterChain的doFilter()方法),但最终的响应被延迟。
注意此处虽然即使不使用AsyncContext而使用线程也可以异步处理,但是线程run方法运行时可能响应已经结束,也就是本次连接已经断开,异步处理方法的返回结果将无法在页面中呈现,所以一般都采用该方法的返回值作为参数传递给我们开发的业务线程。
步骤1:
由于实际实现委托给另一个线程,我们应该有一个线程池实现。我们可以一个通过Executors framework 创建线程池和使用Servlet context listener来初始化线程池。
步骤2:
编写一个可运行的实现,我们将进行重处理,然后使用AsyncContext对象发送请求到另一个资源或使用ServletResponse编写响应对象。一旦处理完成,我们通过AsyncContext.complete()方法通知容器异步处理完成。
步骤3
步骤1中已经给AsyncContext对象注册了监听,Servlet 3.0还为异步处理提供了一个监听器[10],下面就是监听器(实现AsyncListener接口)里面的具体实现代码,我们可以使用它检测异步处理到什么阶段,从而做一些相关处理。比如在异步处理结束的时候即onComplete时做一些清理工作。比如关闭输出流及其他资源。
我们用jmeter依旧模拟60个线程并发,重复上面的测试得到结果如下:
上图看出每秒钟处理的线程吞吐量为5.4个,平均响应每个请求10秒左右,响应时间跟我们设置的10秒处理时间相差无几。通过上面的实验可以看出,异步处理对于超过web容器的maxThread的并发量请求的响应比较及时,因为web容器一般都有线程池,而且都有一定数量的限制,而Servlet异步将耗时的代码交给了我们自己开发的业务线程,让出了tomcat分配的工作线程,从而使其他web请求得到了及时响应。
3 结束语
本文通过tomcat服务器对客户端请求异步特性的Servlet的工作机制分析,得知tomcat为本次请求分配的工作线程会将任务交给业务线程处理,而工作线程本身进入tomcat的线程池,可以继续响应别的请求,这样就增加了系统的吞吐量。并用JMeter进行了多线程压力测试,能够从结果看出开启异步支持的Servlet在面对高并发请求时能够明显减少响应时间和提高吞吐量。