问题分析以及解决方案:
问题原因:
1. Http请求处理的阻塞方式。
2. 后端服务处理时间过长,服务质量不稳定。
3. Web Container接受请求线程资源有限。
解决方案:
1. 改阻塞方式为非阻塞方式处理请求。
2. 设置后端超时时间,主动断开连接,回收资源。
3. 修改容器配置,增加线程池大小以及等待队列长度。
解决方案一是最难做到的,后面的篇幅讲描述对于这方面技术的探索。
解决方案二比较容易,允许各个ISP设置自己API容许的最大超时时间。
解决方案三Tomcat,JBoss在Connector中有两个参数配置(maxThreads和acceptCount)可以做调整。
第一个方案其实和Jdk1.5支持的NIO就是一种想法,只是我们在Socket中都已经采用了,而在Http请求处理中要依赖于Web Container开发商的实现所以至今还没有被广泛应用,不过在开源社区已经有用Mina实现的Http协议处理的框架,但是现在的Web应用高效的Web请求处理仅仅是很小的一方面,还有很多类似于安全,缓存,监控等等附加功能也占据着很重要的地位。
Servlet 3规范经过快一年的推广,已经被各大Web Container厂商所接受,Tomcat6、JBoss5、Jetty7都宣称自己对Servlet3作了较好的支持,而在Servlet3中最广为关注的一个特性就是异步服务处理Servlet(Async Servlet),这点也是解决我目前面临问题的最好的手段。
Servlet 3 与服务异步处理:
Servlet 3主 要的新特性分成四部分:内嵌式的使用模式,Annotation的支持,Async Servlet的支持,安全提升。内嵌式的使用很早就在Jetty中被实现,也成为Jetty的优势之一,Annotation也只能说是锦上添花的部 分,安全暂时没有怎么用到,最关心的还是Async Servlet部分。Async Servlet到底是什么样的概念,这里就大致描述一下在Servlet3规范中的介绍:
1. 支 持 Comet(彗星)。最早期Http请求就是无状态的请求和响应,所有的数据一次性在请求后返回给客户端由客户端渲染。后来发展到AJAX,页面的请求和 渲染由全局变成了局部。而Comet适合事件驱动的 Web 应用和对交互性和实时性要求很强的应用,通过建立客户端和服务端的长连接通道,在一次请求后可以主动推送服务端数据的变更情况到客户端。长连接建立的策略 有两种:Http Streaming和Http Long Polling。前者客户端打开一个单一的与服务器端的 HTTP 持久连接。服务器通过此连接把数据发送过来,客户端增量的处理它们。后者由客户端向服务器端发出请求并打开一个连接。这个连接只有在收到服务器端的数据之 后才会关闭。服务器端发送完数据之后,就立即关闭连接。客户端则马上再打开一个新的连接,等待下一次的数据。
2. 支 持Suspending a request。通过在ServletRequest中增加suspend,resume,complete将Http请求处理的block模式转变成为 not block模式,同时支持对于状态的查询(suspend,resume,timeout)。
3. 请求处理过程中支持事件机制。响应也支持状态查询。
图 异步服务请求基本流程
现实中的异步服务处理:
Tomcat 的异步服务处理
这里使用的是Tomcat 6.0.14版本。在Tomcat中对于异步处理描述在Advanced IO中作了说明,主要分成两部分:Comet的支持和异步输出。
Comet的支持作用分成两部分:请求读数据的非阻塞,响应处理的异步执行。前者可以防止在大流量数据上传时在传输过程中信道空闲等待的资源浪费,后者用于在处理请求时,依赖于第三方或者本身处理比较耗时的情况下,悬挂起请求处理线程,提高请求处理能力,完成处理后异步输出结果。
Servlet不再是原来对于几个标准的Http请求类型的方法实现,而是对于事件响应的处理。Comet定义了4个基础的事件:
1.EventType.BEGIN:客户端建立起连接时激发的事件,可以用于资源初始化。
2.EventType.READ:有数据可以被读入的事件。(熟悉NIO的事件模式应该可以了解)
3.EventType.END:请求处理结束时激发的事件,可以用于资源清理。
4.EventType.ERROR:当请求处理出现问题时激发的事件。(IO异常,超时等)
还有一些子事件类型,例如超时就属于ERROR的子事件类型,可以在事件处理中更加精确的定位事件类型。
必需的配置:在server.xml中配置如下(红色部分):
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443" />
实际代码范例如下:
//CometProcessor接口必需被实现,一旦实现以后,则该Servlet在配置好以后不会再调用service,get,post等方法的实现。
publicclass SIPCometTomcatServlet extends HttpServlet implementsCometProcessor
{
@Override
//事件处理响应方法实现
publicvoid event(CometEvent event) throws IOException, ServletException
{
if (event.getEventType() == CometEvent.EventType.BEGIN)
{
//设置事件超时时间
event.setTimeout(10 * 1000);
//另起线程处理后台工作,异步返回结果,事件响应将不等待后台处理直接返回
new Handler(event.getHttpServletRequest(),event.getHttpServletResponse()).start();
}
elseif (event.getEventType() == CometEvent.EventType.ERROR)
{
//结束事件,回收request,response资源
event.close();
}
elseif (event.getEventType() == CometEvent.EventType.END)
{
event.close();
}
}
//另起一个线程异步处理请求。
class Handler extends java.lang.Thread
{
private HttpServletResponse response;
private HttpServletRequest request;
public Handler(HttpServletRequest request,HttpServletResponse response)
{
this.response = response;
this.request = request;
}
@Override
publicvoid run()
{
try
{
String id;
id = request.getParameter("id");
if (id == null)
id = "no id";
Thread.sleep(5000);
PrintWriter pw = response.getWriter();
pw.write(id);
pw.flush();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
使用的一些总结:
1. 事件响应框架将服务的请求由完整的一次服务处理切割成为细粒度的多事件处理,为请求多阶段并行处理提供了框架基础。
2. Event对象在事件处理方法结束后就被回收了,但是request和response在事件处理完以后还可以继续使用,因此可以看出原来的阻塞式的方式已经可以通过事件的切分成为非阻塞的方式。
3. 没有提供Servlet3中描述suspend,resume,complete方法,无法主动控制request的异步处理。上面的代码可以看出我只使用了Begin方法启动了一个线程,但是由于无法主动地结束请求,因此在向客户端返回数据以后还要等到超时才会结束这次会话。(看了Tomcat的代码,也想模仿close的动作但是由于它使用了protected无法获取封装的request对象,因此无法释放资源)。当然也可以通过客户端配合,由客户端主动发起再次的数据传输激发READ事件来结束会话。这么做对客户端的依赖比较强,同时也增加了客户端的处理复杂度。
4. Tomcat支持异步输出:在APR或者NIO的模式下,Tomcat支持在系统压力增大的时候,支持异步回写大文件数据。
总体上来说实现了部分对于Comet的支持,但是没有对异步服务流程作很好的支持,无法在开发中使用(简单顺畅的使用)。
测试场景如下:
两类Servlet都可以设置处理时Hold的时间,来达到消耗连接数的目的。测试客户端可以设置并发多少用户,每个用户发起多少次请求。下表就是测试的结果:
这里设置的是Servlet都hold1秒钟,APR启动时配置的最大连接数为默认的200个。
客户端设置 |
普通Servlet总耗时(ms) |
异步Servlet总耗时(ms) |
普通Servlet单个线程耗时(ms) |
异步Servlet单个线程耗时(ms) |
100并发线程,每个线程执行1次请求 |
263866 |
274430 |
2638 |
2744 |
300并发线程,每个线程执行1次请求 |
550718 |
617082 |
1835 |
2056 |
100并发线程,每个线程执行10次请求 |
1087747 |
1207920 |
10877 |
12079 |
300并发线程,每个线程执行10次请求 |
retrying request,connect reject |
5193644 |
retrying request,connect reject |
17312 |
从上表可以看出,就纯粹从处理效率来说,采用事件处理方式在线程切换过程中存在着一定的损失,但是就我们使用异步请求处理的本意来看,对于在高并发下对后端依赖无法避免的性能损耗情况下,异步请求解决了连接耗尽的问题。
后语:
多线程、分布式计算、erlang其实这些编程方式、框架设计、语言都在实现这一个理论,那就是分而治之,多线程是站在单应用的角度去考虑解决方案,分布式计算是在多机协作考虑解决方案,erlang在单机多处理器的角度去考虑解决方案。但彼此的理念都是一样,将能够分割的不相关联的独立任务并行处理,最终实现最优化的处理效果。
对于服务集成平台是否采用这种技术,我自己还没有最终的决定,首先就如上面的测试结果来看,有的还是有失的,其次这种并发异步处理带来的多线程维护控制复杂度,也需要考虑到成本中。Jetty的开发者对于是否将异步服务处理Servlet来交由开发者控制而不是容器本身来控制表示出了反对意见,的却将这样复杂的控制交给开发者来处理会增加开发者的学习成本以及维护成本。