分类: Java
2009-03-06 10:32:30
年关到了,商家忙着促销,网站忙着推广,阿里软件的服务集成平台也面临第一次多方大规模的压力考验。根据该平台5.3版本的压力测试结果,我们估算了一下现有的推广会带来的压力,基本上确定了服务集成平台年底不需要扩容。SA(System Administrator,系统管理员)为了保险起见还是通过请求方式来做定时的心跳检测,保证服务集成平台的可靠性。结果阿里旺旺推广开始的第一天,SA的报警短信就在几个忙时段不停地发告警,但是查看生产环境的服务器状况以及应用状况后看不出有什么问题,于是开始怀疑是否告警机制不是很合理。几日的访问记录统计报告看过以后,发现了几个问题,首先由于推广是在IM登录时段集中式的推广,因此高峰期比较集中,压力也很大,而告警发生的时刻也是那些时候;另外发现那些推广使用的API的处理时间比较长,同时还有些出现了问题,这几天除了服务集成平台告警以外,那些API服务器也在告警;因此可以看出问题应该是由于API提供商响应速度慢而拖累了服务集成平台的处理能力,监控机制在高峰情况下没有得到及时的响应,就认为是服务器已经处于无效状态。
其实这类问题在我们现在的应用体系架构中常常出现,原因是现在很少再有纯粹“封闭式”应用,对数据库的依赖,对存储的依赖,对第三方系统的依赖等等。这也让我回忆到在前一阵子参加的安全会议中,腾迅的安全技术团队的负责人说安全现在最大的问题就在于合作的第三方的安全不受控而引发的安全潜在影响。Web应用未尝不是,从最基本的事务处理要小粒度,不要在事务中包含第三方依赖,到心跳检测,容错方案的制定等,都已经让我们对这方面的问题有所注意。但是往往这类问题不是局部设计可以看到的,如果没有一个总体架构设计者对于全局的把握、协调和防范,那么问题出现并且带来的影响将会很大。
从前对于服务集成平台的压力测试主要是在ISP服务“基本正常”的情况下做的,但是这次问题的暴露就要求我们在第三方依赖出现边界问题时,及时做出一些措施或者改进设计。
问题原因:
解决方案:
解决方案一是最难做到的,后面的篇幅将描述对于这方面技术的探索。
解决方案二比较容易,允许各个ISP设置自己API容许的最大超时时间。
解决方案三Tomcat和JBoss在Connector中有两个参数配置(maxThreads和acceptCount)可以做调整。
第一个方案其实和JDK 1.5支持的NIO是一种想法,只是我们在Socket中都已经采用过了,而在Http请求处理中因为要依赖于Web Container开发商的实现,所以至今还没有被广泛应用,不过在开源社区已经有用Mina实现的Http协议处理的框架。需要注意的是,现在Web应用对于Web请求高效处理的需求仅仅是很小的一方面,其实还有很多类似于安全、缓存、监控等等附加功能也占据着很重要的地位。
Servlet 3规范经过快一年的推广,已经被各大Web Container厂商所接受,Tomcat 6、JBoss 5、Jetty 7都宣称自己对Servlet 3作了较好的支持,而在Servlet 3中最广为关注的一个特性就是异步服务处理Servlet(Async Servlet),这点也是解决我目前面临问题的最好手段。
Servlet 3主要的新特性分成四部分:内嵌式的使用模式、Annotation的支持、Async Servlet的支持和安全提升。内嵌式的使用很早就在Jetty中被实现,也成为Jetty的优势之一,Annotation也只能说是锦上添花的部分,而安全暂时没有怎么用到,所以最关心的还是Async Servlet部分。Async Servlet到底是什么样的概念,这里就大致描述一下在Servlet 3规范中对它的介绍:
图 异步服务请求基本流程
这里使用的是Tomcat 6.0.14版本。在Tomcat中对于异步处理描述在Advanced IO中作了说明,主要分成两部分:Comet的支持和异步输出。
Comet的支持作用分成两部分:请求读数据的非阻塞,响应处理的异步执行。前者可以防止在大流量数据上传过程中,信道空闲等待的资源浪费,后者用于在处理请求时,依赖于第三方或者本身处理比较耗时的情况下,悬挂起请求处理线程,提高请求处理能力,完成处理后异步输出结果。
Servlet不再是原来对于几个标准的Http请求类型的方法实现,而是对于事件响应的处理。Comet定义了4个基础的事件:
还有一些子事件类型,例如超时就属于ERROR的子事件类型,可以在事件处理中更加精确地定位事件类型。
必需的配置:在server.xml中配置如下(红色部分):
protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443" />
实际代码范例如下:
//CometProcessor接口必需被实现,一旦实现以后,则该Servlet在配置好以后不会再调用service,get,post等方法的实现。 public class SIPCometTomcatServlet extends HttpServlet implements CometProcessor { @Override //事件处理响应方法实现 public void event(CometEvent event) throws IOException, ServletException { if (event.getEventType() == CometEvent.EventType.BEGIN) { //设置事件超时时间 event.setTimeout(10 * 1000); //另起线程处理后台工作,异步返回结果,事件响应将不等待后台处理直接返回 new Handler(event.getHttpServletRequest(),event.getHttpServletResponse()).start(); } else if (event.getEventType() == CometEvent.EventType.ERROR) { //结束事件,回收request,response资源 event.close(); } else if (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 public void 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(); } } } }
使用过程中的一些总结:
总体上来说实现了部分对于Comet的支持,但是没有对异步服务流程作很好的支持,无法在开发中使用(简单顺畅的使用)。
JBoss 4.2.3版本配置和使用与Tomcat 6类似,没有什么差异。
JBoss 5刚刚发布了RC版本,对于异步服务处理作了很大的改动,与Tomcat配置很不同,这里具体的说一下JBoss5中的异步服务使用。
JBoss 5已经将Tomcat中的Http11NioProtocol给删除了,取而代之的是JBoss自己servlet包内增加的一个HttpEventServlet接口,这个接口和Tomcat的CometProcessor类似。
首先,必须配置JBoss内置的Web容器为APR模式,也就是配置jbossweb.sar下面的server.xml中Connector 如下:
其次异步服务处理的Servlet必须实现HttpEventServlet接口,接口只有一个方法,就是事件处理方法:public void event(HttpEvent event)
。事件定义与Tomcat稍有不同,在BEGIN,ERROR,READ,END基础上增加了TIMEOUT,EOF,EVENT,WRITE四个事件,同时去掉了SubType。
再则,JBoss的事件对象还支持几个方法来实现异步处理以及Comet机制,方法如下:
具体的实现代码如下:
public class SIPCometJBossServlet extends HttpServlet implements HttpEventServlet { @Override public void event(HttpEvent event) throws IOException, ServletException { switch (event.getType()) { //will be called at the beginning of the processing of the connection case BEGIN: { event.setTimeout(100 * 1000);//设置超时时间 //event.suspend();//resume之前不必要一定使用suspend new Handler(event).start(); break; } //Error will be called by the container in the case //where an IO exception or a similar unrecoverable error occurs case ERROR: { event.close(); break; } //End may be called to end the processing of the request case END: { //event.close();//可以写也可以不写,因为进入这个方法也就是调用了close方法,起码暂时还不知道有其他什么入口 break; } //This indicates that input data is available, //and that at least one read call can be made without blocking case READ: { break; } //The connection timed out according to the timeout value which has been set //,but the connection will not be closed unless the servlet uses the close method of the event case TIMEOUT: { event.close();//如果不主动关闭,Timeout方法会被循环调用,会话不会结束 break; } //The end of file of the input has been reached, and no further data is available case EOF: { event.close(); break; } //Event will be called by the container after the resume() method is called, //during which any operation can be performed, including closing the connection using the close() method. case EVENT: { event.close();//作为resume方法调用后主动释放连接资源的一种手段 break; } //Write is sent if the servlet is using the isWriteReady method case WRITE: { break; } } } class Handler extends java.lang.Thread { private HttpEvent event;//event的生命周期已经不限制于事件处理方法,因此随时可以关闭请求处理 private HttpServletResponse response; private HttpServletRequest request; public Handler(HttpEvent event) { this.event = event; this.response = event.getHttpServletResponse(); this.request = event.getHttpServletRequest(); } @Override public void run() { try { String id; id = request.getParameter("id"); if (id == null) id = "no id"; Thread.sleep(5000); //危险!!!其实event,response,request都是线程不安全的,因此此时可能response已经被释放,需要同步住event的对象来操作,效率可能会降低 PrintWriter pw = response.getWriter(); pw.write(id); pw.flush(); event.resume();//发送结束调用resume方法,进入event方法,结束请求处理 } catch (Exception e) { e.printStackTrace(); } } } }
使用总结:
下面对异步服务处理Servlet和普通Servlet做了一下简单的性能测试。
首先我原本想用ab来做一下简单的压力测试即可,但是ab好像对于apr模式下的测试支持的不好,一压就报错(apr_poll: The timeout specified has expired (70007)),也可能是自己不会用吧,因此就自己写了一段测试代码来做测试。
测试场景如下:
两类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 |
从上表可以看出,就纯粹从处理效率来说,采用事件处理方式在线程切换过程中存在着一定的损失,但是就我们使用异步请求处理的本意来看,对于在高并发下对后端依赖无法避免的性能损耗情况下,异步请求解决了连接耗尽的问题。
最后再来看我在测试过程中用JProfiler来截取的一些线程创建和使用状况:
上图是最初的线程创建情况,还没有任何请求被发送到服务端,因此线程池也没有开任何一个连接。
这是普通的Servlet在压力测试下的线程状况,线程就开到了200最大值,图中由于程序来Hold请求处理线程出现了红色阻塞和黄色等待,同时客户端已经开始出现拒绝连接的错误。下图就是错误的截图:
上图是异步服务处理Servlet在压力测试开始的情况,可以发现它的http线程还是200,但是其他事件处理线程在不断增长。下图已经增长到了3000多个线程。(这里需要注意的就是这种异步处理资源申请没有设置上限,因此对于资源消耗来说也是比较大的,同时要防范攻击性请求造成服务端垮掉)。
多线程、分布式计算、Erlang等这些编程方式、框架设计、语言其实都在实现这一个理论,那就是分而治之,多线程是站在单应用的角度去考虑解决方案,分布式计算是在多机协作考虑解决方案,Erlang在单机多处理器的角度去考虑解决方案。但彼此的理念都是一样,将能够分割的不相关联的独立任务并行处理,最终实现最优化的处理效果。
对于服务集成平台是否采用这种技术,我自己还没有最终的决定,首先就如上面的测试结果来看,有得还是有失的,其次这种并发异步处理带来的多线程维护控制复杂度,也需要考虑到成本中。Jetty的开发者对于是否将异步服务处理Servlet来交由开发者控制而不是容器本身来控制表示出了反对意见,的确,这样复杂的控制交给开发者来处理会增加开发者的学习成本以及维护成本。