问题凸现
年关到了商家忙着促销网站忙着推广阿里软件的服务集成平台也面临第一次多方大规模的压力考验根据该平台版本的压力测试结果我们估算了一下现有的推广会带来的压力基本上确定了服务集成平台年底不需要扩容SA(System Administrator系统管理员)为了保险起见还是通过请求方式来做定时的心跳检测保证服务集成平台的可靠性结果阿里旺旺推广开始的第一天SA的报警短信就在几个忙时段不停地发告警但是查看生产环境的服务器状况以及应用状况后看不出有什么问题于是开始怀疑是否告警机制不是很合理几日的访问记录统计报告看过以后发现了几个问题首先由于推广是在IM登录时段集中式的推广因此高峰期比较集中压力也很大而告警发生的时刻也是那些时候另外发现那些推广使用的API的处理时间比较长同时还有些出现了问题这几天除了服务集成平台告警以外那些API服务器也在告警因此可以看出问题应该是由于API提供商响应速度慢而拖累了服务集成平台的处理能力监控机制在高峰情况下没有得到及时的响应就认为是服务器已经处于无效状态
其实这类问题在我们现在的应用体系架构中常常出现原因是现在很少再有纯粹封闭式应用对数据库的依赖对存储的依赖对第三方系统的依赖等等这也让我回忆到在前一阵子参加的安全会议中腾迅的安全技术团队的负责人说安全现在最大的问题就在于合作的第三方的安全不受控而引发的安全潜在影响Web应用未尝不是从最基本的事务处理要小粒度不要在事务中包含第三方依赖到心跳检测容错方案的制定等都已经让我们对这方面的问题有所注意但是往往这类问题不是局部设计可以看到的如果没有一个总体架构设计者对于全局的把握协调和防范那么问题出现并且带来的影响将会很大
从前对于服务集成平台的压力测试主要是在ISP服务基本正常的情况下做的但是这次问题的暴露就要求我们在第三方依赖出现边界问题时及时做出一些措施或者改进设计
问题分析以及解决方案
问题原因
Http请求处理的阻塞方式 后端服务处理时间过长服务质量不稳定 Web Container接受请求线程资源有限
解决方案
改阻塞方式为非阻塞方式来处理请求 设置后端超时时间主动断开连接回收资源 修改容器配置增加线程池大小以及等待队列长度
解决方案一是最难做到的后面的篇幅将描述对于这方面技术的探索
解决方案二比较容易允许各个ISP设置自己API容许的最大超时时间
解决方案三Tomcat和JBoss在Connector中有两个参数配置(maxThreads和acceptCount)可以做调整
第一个方案其实和JDK 支持的NIO是一种想法只是我们在Socket中都已经采用过了而在Http请求处理中因为要依赖于Web Container开发商的实现所以至今还没有被广泛应用不过在开源社区已经有用Mina实现的Http协议处理的框架需要注意的是现在Web应用对于Web请求高效处理的需求仅仅是很小的一方面其实还有很多类似于安全缓存监控等等附加功能也占据着很重要的地位
Servlet 规范经过快一年的推广已经被各大Web Container厂商所接受Tomcat JBoss Jetty 都宣称自己对Servlet 作了较好的支持而在Servlet 中最广为关注的一个特性就是异步服务处理Servlet(Async Servlet)这点也是解决我目前面临问题的最好手段
Servlet 与服务异步处理
Servlet 主要的新特性分成四部分内嵌式的使用模式Annotation的支持Async Servlet的支持和安全提升内嵌式的使用很早就在Jetty中被实现也成为Jetty的优势之一Annotation也只能说是锦上添花的部分而安全暂时没有怎么用到所以最关心的还是Async Servlet部分Async Servlet到底是什么样的概念这里就大致描述一下在Servlet 规范中对它的介绍
支持 Comet(彗星)最早期的Http请求就是无状态的请求和响应所有的数据一次性在请求后返回给客户端由客户端渲染后来发展到AJAX页面的请求和渲染由全局变成了局部而Comet适合事件驱动的Web应用和对交互性和实时性要求很强的应用通过建立客户端和服务端的长连接通道在一次请求后可以主动推送服务端数据的变更情况到客户端长连接建立的策略有两种Http Streaming和Http Long Polling前者客户端打开一个单一的与服务器端的 HTTP 持久连接服务器通过此连接把数据发送过来客户端对它们进行增量处理后者由客户端向服务器端发出请求并打开一个连接这个连接只有在收到服务器端的数据之后才会关闭服务器端发送完数据之后就立即关闭连接客户端则马上再打开一个新的连接等待下一次的数据 支持Suspending a request通过在ServletRequest中增加suspendresumecomplete等其将Http请求处理的block模式转变成为not block模式同时支持对于状态的查询(suspendresumetimeout) 请求处理过程中支持事件机制响应也支持状态查询
图 异步服务请求基本流程
现实中的异步服务处理 Tomcat 的异步服务处理
这里使用的是Tomcat 版本在Tomcat中对于异步处理描述在Advanced IO中作了说明主要分成两部分Comet的支持和异步输出
Comet的支持作用分成两部分请求读数据的非阻塞响应处理的异步执行前者可以防止在大流量数据上传过程中信道空闲等待的资源浪费后者用于在处理请求时依赖于第三方或者本身处理比较耗时的情况下悬挂起请求处理线程提高请求处理能力完成处理后异步输出结果
Servlet不再是原来对于几个标准的Http请求类型的方法实现而是对于事件响应的处理Comet定义了个基础的事件
EventTypeBEGIN:客户端建立起连接时激发的事件可以用于资源初始化 EventTypeREAD:有数据可以被读入的事件(熟悉NIO的事件模式应该可以了解) EventTypeEND:请求处理结束时激发的事件可以用于资源清理 EventTypeERROR:当请求处理出现问题时激发的事件(IO异常超时等)
还有一些子事件类型例如超时就属于ERROR的子事件类型可以在事件处理中更加精确地定位事件类型
必需的配置在serverxml中配置如下(红色部分)
<Connector port= protocol=yote
connectionTimeout=
redirectPort= />
实际代码范例如下
//CometProcessor接口必需被实现一旦实现以后则该Servlet在配置好以后不会再调用servicegetpost等方法的实现
public class SIPCometTomcatServlet extends HttpServlet implements CometProcessor
{
@Override
//事件处理响应方法实现
public void event(CometEvent event) throws IOException ServletException
{
if (eventgetEventType() == CometEventEventTypeBEGIN)
{
//设置事件超时时间
eventsetTimeout( * )
//另起线程处理后台工作异步返回结果事件响应将不等待后台处理直接返回
new Handler(eventgetHttpServletRequest()eventgetHttpServletResponse())start()
}
else if (eventgetEventType() == CometEventEventTypeERROR)
{
//结束事件回收requestresponse资源
eventclose()
}
else if (eventgetEventType() == CometEventEventTypeEND)
{
eventclose()
}
}
//另起一个线程异步处理请求
class Handler extends javalangThread
{
private HttpServletResponse response;
private HttpServletRequest request;
public Handler(HttpServletRequest requestHttpServletResponse response)
{
thisresponse = response;
thisrequest = request;
}
@Override
public void run()
{
try
{
String id;
id = requestgetParameter(id)
if (id == null)
id = no id;
Threadsleep()
PrintWriter pw = responsegetWriter()
pwwrite(id)
pwflush()
} catch (Exception e)
{
eprintStackTrace()
}
}
}
}
使用过程中的一些总结
事件响应框架将服务的请求由完整的一次服务处理切割成为细粒度的多事件处理为请求多阶段并行处理提供了框架基础 Event对象在事件处理方法结束后就被回收了但是request和response在事件处理完以后还可以继续使用因此可以看出原来的阻塞式的方式已经可以通过事件的切分成为非阻塞的方式 没有提供Servlet 中描述的suspendresumecomplete方法无法主动控制request的异步处理上面的代码可以看出我只使用了Begin方法启动了一个线程但是由于无法主动地结束请求因此在向客户端返回数据以后还要等到超时才会结束这次会话(看了Tomcat的代码也想模仿close的动作但是由于它使用了protected无法获取封装的request对象因此无法释放资源)当然也可以通过客户端配合由客户端主动发起再次的数据传输激发READ事件来结束会话这么做对客户端的依赖比较强同时也增加了客户端的处理复杂度 Tomcat支持异步输出在APR或者NIO的模式下Tomcat支持在系统压力增大的时候支持异步回写大文件数据
总体上来说实现了部分对于Comet的支持但是没有对异步服务流程作很好的支持无法在开发中使用(简单顺畅的使用)
JBoss的异步服务处理
JBoss 版本配置和使用与Tomcat 类似没有什么差异
JBoss 刚刚发布了RC版本对于异步服务处理作了很大的改动与Tomcat配置很不同这里具体的说一下JBoss中的异步服务使用
JBoss 已经将Tomcat中的HttpNioProtocol给删除了取而代之的是JBoss自己servlet包内增加的一个HttpEventServlet接口这个接口和Tomcat的CometProcessor类似
首先必须配置JBoss内置的Web容器为APR模式也就是配置jbosswebsar下面的serverxml中Connector 如下
<Connector protocol=yote port= address=${jbossbindaddress}
connectionTimeout= redirectPort= />
其次异步服务处理的Servlet必须实现HttpEventServlet接口接口只有一个方法就是事件处理方法public void event(HttpEvent event)事件定义与Tomcat稍有不同在BEGINERRORREADEND基础上增加了TIMEOUTEOFEVENTWRITE四个事件同时去掉了SubType
TIMEOUT其实是从原来的Error的SubType分离出来的这个方法是在最后一次处理事件到当前时间超过设定的超时时间而被激发的同时TIMEOUT被激发并不会关闭请求处理流程必须显示调用事件的close方法才会结束会话 EOF事件将会在客户端主动断连的情况下被触发就好比IE窗口在请求过程中被关闭就会被触发 EVENT事件在事件对象被调用resume的时候被激发按照原意应该最好可以附带上一些自定义信息来做一些工作但是我自己使用过程中还没有发现有什么好的办法可以在事件中附带信息到事件处理中 WRITE方法在调用isWriteReady方法时被激发可以在网络出现问题或者繁忙的时候异步等待输出
再则JBoss的事件对象还支持几个方法来实现异步处理以及Comet机制方法如下
close方法表示一次请求处理的结束会告知客户端没有数据返回了同时也会激发END事件 setTimeout方法设置连接超时时间(单位毫秒)计算超时是从最近的事件处理时间开始记录的如果发生超时则会激发TIMEOUT事件 isReadReady方法如果连接有数据可以读取则返回true如果这个方法返回falseservlet还试图去读去数据则会阻塞 isWriteReady方法如果返回true则连接可以无阻塞的写出数据如果返回falseservlet必须停止写数据如果强制写出则可能会发生IO错误或者会采用异步输出当客户端的输出通道可用以后则会激发write事件 suspend方法suspend连接处理线程直到timeout发生或者resume被调用实际上意味着servlet在suspend以后不再收到READ事件READ事件将会在后台被不断的激发除非被suspend resume方法会激发event事件可以利用这个方法来结束异步处理同时也可以激活因为suspend停止的read事件同时也可以在resume以后再调用suspend方法注意这里未必是要求必须先suspend以后再resume eventrequestresponse在事件响应过程中都可以被使用但是线程不安全同时在调用了close以后requestresponse资源会被释放可以通过对event对象做同步来保证线程安全的问题当READ事件和END事件都发生的时候首先会完成READ事件然后再去完成END
具体的实现代码如下
public class SIPCometJBossServlet extends HttpServlet implements HttpEventServlet
{
@Override
public void event(HttpEvent event) throws IOException ServletException
{
switch (eventgetType())
{
//will be called at the beginning of the processing of the connection
case BEGIN:
{
eventsetTimeout( * )//设置超时时间
//eventsuspend()//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:
{
eventclose()
break;
}
//End may be called to end the processing of the request
case END:
{
//eventclose()//可以写也可以不写因为进入这个方法也就是调用了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:
{
eventclose()//如果不主动关闭Timeout方法会被循环调用会话不会结束
break;
}
//The end of file of the input has been reached and no further data is available
case EOF:
{
eventclose()
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:
{
eventclose()//作为resume方法调用后主动释放连接资源的一种手段
break;
}
//Write is sent if the servlet is using the isWriteReady method
case WRITE:
{
break;
}
}
}
class Handler extends javalangThread
{
private HttpEvent event;//event的生命周期已经不限制于事件处理方法因此随时可以关闭请求处理
private HttpServletResponse response;
private HttpServletRequest request;
public Handler(HttpEvent event)
{
thisevent = event;
thisresponse = eventgetHttpServletResponse()
thisrequest = eventgetHttpServletRequest()
}
@Override
public void run()
{
try
{
String id;
id = requestgetParameter(id)
if (id == null)
id = no id;
Threadsleep()
//危险!!!其实eventresponserequest都是线程不安全的因此此时可能response已经被释放需要同步住event的对象来操作效率可能会降低
PrintWriter pw = responsegetWriter()
pwwrite(id)
pwflush()
eventresume()//发送结束调用resume方法进入event方法结束请求处理
} catch (Exception e)
{
eprintStackTrace()
}
}
}
}
使用总结
对于Servlet描述的异步服务处理有了较好的支持 事件方法比较丰富但是对于可定义事件支持不够完善 对象并发控制需要开发者自己设计权衡多线程处理的高效以及资源争夺的消耗
下面对异步服务处理Servlet和普通Servlet做了一下简单的性能测试
首先我原本想用ab来做一下简单的压力测试即可但是ab好像对于apr模式下的测试支持的不好一压就报错(apr_poll: The timeout specified has expired ())也可能是自己不会用吧因此就自己写了一段测试代码来做测试
测试场景如下
两类Servlet都可以设置处理时Hold的时间来达到消耗连接数的目的测试客户端可以设置并发多少用户每个用户发起多少次请求下表就是测试的结果
这里设置的是Servlet都hold秒钟APR启动时配置的最大连接数为默认的个
客户端设置 普通Servlet总耗时(ms) 异步Servlet总耗时(ms) 普通Servlet单个线程耗时(ms) 异步Servlet单个线程耗时(ms)
并发线程每个线程执行次请求
并发线程每个线程执行次请求
并发线程每个线程执行次请求
并发线程每个线程执行次请求 retrying requestconnect reject retrying requestconnect reject
从上表可以看出就纯粹从处理效率来说采用事件处理方式在线程切换过程中存在着一定的损失但是就我们使用异步请求处理的本意来看对于在高并发下对后端依赖无法避免的性能损耗情况下异步请求解决了连接耗尽的问题
最后再来看我在测试过程中用JProfiler来截取的一些线程创建和使用状况
上图是最初的线程创建情况还没有任何请求被发送到服务端因此线程池也没有开任何一个连接
这是普通的Servlet在压力测试下的线程状况线程就开到了最大值图中由于程序来Hold请求处理线程出现了红色阻塞和黄色等待同时客户端已经开始出现拒绝连接的错误下图就是错误的截图
上图是异步服务处理Servlet在压力测试开始的情况可以发现它的http线程还是但是其他事件处理线程在不断增长下图已经增长到了多个线程(这里需要注意的就是这种异步处理资源申请没有设置上限因此对于资源消耗来说也是比较大的同时要防范攻击性请求造成服务端垮掉)
结语
多线程分布式计算Erlang等这些编程方式框架设计语言其实都在实现这一个理论那就是分而治之多线程是站在单应用的角度去考虑解决方案分布式计算是在多机协作考虑解决方案Erlang在单机多处理器的角度去考虑解决方案但彼此的理念都是一样将能够分割的不相关联的独立任务并行处理最终实现最优化的处理效果
对于服务集成平台是否采用这种技术我自己还没有最终的决定首先就如上面的测试结果来看有得还是有失的其次这种并发异步处理带来的多线程维护控制复杂度也需要考虑到成本中Jetty的开发者对于是否将异步服务处理Servlet来交由开发者控制而不是容器本身来控制表示出了反对意见的确这样复杂的控制交给开发者来处理会增加开发者的学习成本以及维护成本