`
xiaofengtoo
  • 浏览: 492942 次
  • 性别: Icon_minigender_1
  • 来自: xiamen
社区版块
存档分类
最新评论

Web请求异步处理降低应用依赖风险

    博客分类:
  • java
阅读更多

 

问题凸现

年关到了,商家忙着促销,网站忙着推广,阿里软件的服务集成平台也面临第一次多方大规模的压力考验。根据该平台5.3版本的压力测试结果,我们估算了一下现有的推广会带来的压力,基本上确定了服务集成平台年底不需要扩容。SA(System Administrator,系统管理员)为了保险起见还是通过请求方式来做定时的心跳检测,保证服务集成平台的可靠性。结果阿里旺旺推广开始的第一天,SA的报警短信就在几个忙时段不停地发告警,但是查看生产环境的服务器状况以及应用状况后看不出有什么问题,于是开始怀疑是否告警机制不是很合理。几日的访问记录统计报告看过以后,发现了几个问题,首先由于推广是在IM登录时段集中式的推广,因此高峰期比较集中,压力也很大,而告警发生的时刻也是那些时候;另外发现那些推广使用的API的处理时间比较长,同时还有些出现了问题,这几天除了服务集成平台告警以外,那些API服务器也在告警;因此可以看出问题应该是由于API提供商响应速度慢而拖累了服务集成平台的处理能力,监控机制在高峰情况下没有得到及时的响应,就认为是服务器已经处于无效状态。

 

其实这类问题在我们现在的应用体系架构中常常出现,原因是现在很少再有纯粹“封闭式”应用,对数据库的依赖,对存储的依赖,对第三方系统的依赖等等。这也 让我回忆到在前一阵子参加的安全会议中,腾迅的安全技术团队的负责人说安全现在最大的问题就在于合作的第三方的安全不受控而引发的安全潜在影响。Web应 用未尝不是,从最基本的事务处理要小粒度,不要在事务中包含第三方依赖,到心跳检测,容错方案的制定等,都已经让我们对这方面的问题有所注意。但是往往这 类问题不是局部设计可以看到的,如果没有一个总体架构设计者对于全局的把握、协调和防范,那么问题出现并且带来的影响将会很大。

 

从前对于服务集成平台的压力测试主要是在ISP服务“基本正常”的情况下做的,但是这次问题的暴露就要求我们在第三方依赖出现边界问题时,及时做出一些措施或者改进设计。

问题分析以及解决方案

问题原因:

  1. Http请求处理的阻塞方式。
  2. 后端服务处理时间过长,服务质量不稳定。
  3. Web Container接受请求线程资源有限。

解决方案:

  1. 改阻塞方式为非阻塞方式来处理请求。
  2. 设置后端超时时间,主动断开连接,回收资源。
  3. 修改容器配置,增加线程池大小以及等待队列长度。

解决方案一是最难做到的,后面的篇幅将描述对于这方面技术的探索。
解决方案二比较容易,允许各个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 与服务异步处理

Servlet 3主要的新特性分成四部分:内嵌式的使用模式、Annotation的支持、Async Servlet的支持和安全提升。内嵌式的使用很早就在Jetty中被实现,也成为Jetty的优势之一,Annotation也只能说是锦上添花的部 分,而安全暂时没有怎么用到,所以最关心的还是Async Servlet部分。Async Servlet到底是什么样的概念,这里就大致描述一下在Servlet 3规范中对它的介绍:

  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
//等方法的实现。

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();
            }
        }
    }
}
 

使用过程中的一些总结:

  1. 事件响应框架将服务的请求由完整的一次服务处理切割成为细粒度的多事件处理,为请求多阶段并行处理提供了框架基础。
  2. Event对象在事件处理方法结束后就被回收了,但是request和response在事件处理完以后还可以继续使用,因此可以看出原来的阻塞式的方式已经可以通过事件的切分成为非阻塞的方式。
  3. 没 有提供Servlet 3中描述的suspend,resume,complete方法,无法主动控制request的异步处理。上面的代码可以看出我只使用了Begin方法启 动了一个线程,但是由于无法主动地结束请求,因此在向客户端返回数据以后还要等到超时才会结束这次会话。(看了Tomcat的代码,也想模仿close的 动作但是由于它使用了protected无法获取封装的request对象,因此无法释放资源)。当然也可以通过客户端配合,由客户端主动发起再次的数据 传输激发READ事件来结束会话。这么做对客户端的依赖比较强,同时也增加了客户端的处理复杂度。
  4. Tomcat支持异步输出:在APR或者NIO的模式下,Tomcat支持在系统压力增大的时候,支持异步回写大文件数据。

总体上来说实现了部分对于Comet的支持,但是没有对异步服务流程作很好的支持,无法在开发中使用(简单顺畅的使用)。

JBoss的异步服务处理

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 如下:

<Connector protocol="org.apache.coyote.http11.Http11AprProtocol" port="8080" 
address="${jboss.bind.address}" connectionTimeout="2000" redirectPort="8443" />

其次异步服务处理的Servlet必须实现HttpEventServlet接口,接口只有一个方法,就是事件处理方法:public void event(HttpEvent event) 。事件定义与Tomcat稍有不同,在BEGIN,ERROR,READ,END基础上增加了TIMEOUT,EOF,EVENT,WRITE四个事件,同时去掉了SubType。

  1. TIMEOUT其实是从原来的Error的SubType分离出来的,这个方法是在最后一次处理事件到当前时间超过设定的超时时间而被激发的,同时TIMEOUT被激发并不会关闭请求处理流程,必须显示调用事件的close方法才会结束会话。
  2. EOF事件将会在客户端主动断连的情况下被触发,就好比IE窗口在请求过程中被关闭就会被触发。
  3. EVENT事件在事件对象被调用resume的时候被激发,按照原意应该最好可以附带上一些自定义信息来做一些工作,但是我自己使用过程中还没有发现有什么好的办法可以在事件中附带信息到事件处理中。
  4. WRITE方法在调用isWriteReady方法时被激发,可以在网络出现问题或者繁忙的时候异步等待输出。

再则,JBoss的事件对象还支持几个方法来实现异步处理以及Comet机制,方法如下:

  1. close方法:表示一次请求处理的结束,会告知客户端没有数据返回了,同时也会激发END事件。
  2. setTimeout方法:设置连接超时时间(单位毫秒),计算超时是从最近的事件处理时间开始记录的,如果发生超时,则会激发TIMEOUT事件。
  3. isReadReady方法:如果连接有数据可以读取则返回true,如果这个方法返回false,servlet还试图去读去数据,则会阻塞。
  4. isWriteReady方法:如果返回true,则连接可以无阻塞的写出数据,如果返回false,servlet必须停止写数据,如果强制写出,则可能会发生IO错误或者会采用异步输出。当客户端的输出通道可用以后,则会激发write事件。
  5. suspend方法:suspend连接处理线程直到timeout发生或者resume被调用,实际上意味着servlet在suspend以后不再收到READ事件,READ事件将会在后台被不断的激发,除非被suspend.
  6. resume方法:会激发event事件,可以利用这个方法来结束异步处理。同时也可以激活因为suspend停止的read事件,同时也可以在resume以后再调用suspend方法。注意,这里未必是要求必须先suspend以后再resume。
  7. event,request,response 在事件响应过程中都可以被使用,但是线程不安全,同时在调用了close以后,request,response资源会被释放,可以通过对event对象 做同步来保证线程安全的问题。当READ事件和END事件都发生的时候,首先会完成READ事件,然后再去完成END。

具体的实现代码如下:

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();
            }
        }
    }
}
 

使用总结:

  1. 对于Servlet描述的异步服务处理有了较好的支持。
  2. 事件方法比较丰富,但是对于可定义事件支持不够完善。
  3. 对象并发控制需要开发者自己设计,权衡多线程处理的高效以及资源争夺的消耗。

下面对异步服务处理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来交由开发者控制而不是容器本身来控制表示出了反对意见,的 确,这样复杂的控制交给开发者来处理会增加开发者的学习成本以及维护成本。


作者介绍: 岑文初,就职于阿里软件公司研发中心平台一部,任架构师。当前主要工作涉及阿里软件开发平台服务框架(ASF)设计与实现,服务集成平台(SIP)设计与实现。没有什么擅长或者精通,工作到现在唯一提升的就是学习能力和速度。个人Blog为:http://blog.csdn.net/cenwenchu79

 

转载:InfoQ中文站  备份一个。

 

 

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

    大型WEB网站架构深入分析_异步处理.docx

    异步系统能迅速返回初步响应,即使操作尚未完成,这在Web应用中尤其重要,因为它降低了用户的等待时间。 1.1.3.3 负载平衡 异步处理天生支持负载平衡,通过队列机制,可以根据需要动态调整处理节点,实现服务的...

    前端大数据导入,异步处理

    在安全方面,由于前端直接处理用户上传的文件,可能存在XSS(跨站脚本攻击)和CSRF(跨站请求伪造)的风险。因此,必须验证文件类型,限制文件大小,并且在接收数据时进行严格的校验和过滤。 总结来说,前端大数据...

    异步处理代码

    在JavaScript中,异步处理广泛应用于Node.js服务器端编程和浏览器端的AJAX请求。使用`setTimeout`、`setInterval`、`fetch`、`async/await`等函数可以实现异步操作。例如,`async/await`语法使得异步代码更接近同步...

    登录的完整代码涉及3个框架 和异步请求

    SpringMVC是Spring的一个模块,专门用于处理Web请求,它将模型、视图和控制器分离,增强了代码的组织性和可维护性。MyBatis则是一个持久层框架,简化了数据库操作,通过XML或注解的方式将SQL语句与Java代码关联。 1...

    最全的Spring MVC注解例子,异步请求,错误处理

    - 这个文件名可能代表了一个示例项目,它可能包含了各种Spring MVC注解的实例,用于演示如何使用Spring MVC进行Web开发,包括异步请求的处理和错误管理策略。 总的来说,这个压缩包提供了学习和实践Spring MVC注解...

    httpUnit完整的web工程实例,包括源码和依赖包,数据页面具有异步数据

    在本项目中,我们关注的是一个基于HTTPUnit的Web工程实例,它包含了源码和必要的依赖包,能够处理异步数据加载。这个工程是用Java 1.8或更高版本编译并运行的,旨在帮助开发者理解如何在Web应用程序中进行HTTP请求...

    AJAX异步处理原理分析

    总结,AJAX异步处理是现代Web开发的重要技术,它使得网页更加动态和交互性强。通过理解其工作原理和应用场景,开发者可以有效地提升网站性能和用户体验。然而,同时要注意解决可能出现的问题,如跨域限制和SEO兼容性...

    Python-一个现代快速简单的异步python3web框架

    在Python Web开发中,异步功能变得越来越重要,因为它们能够提高服务器处理并发请求的能力,进而提升应用程序的响应速度。 异步编程是Python 3.5及更高版本中的一个重要特性,主要通过asyncio库来实现。在Web框架中...

    java web ajax异步提交方式上传文件

    Java Web中的Ajax异步提交方式上传文件是一种常见且高效的用户交互技术,它允许用户在不刷新整个页面的情况下发送数据到服务器并接收响应。本项目利用Maven构建,前端使用jQuery库以及jQuery-form插件,后端则采用...

    Android 请求 WebAPI的案例

    本案例"Android请求WebAPI"将详细讲解如何在Android应用中实现这一功能。WebAPI通常指的是基于HTTP协议的RESTful API,允许客户端(如Android应用)通过HTTP方法(GET、POST、PUT、DELETE等)获取或操作服务器资源。...

    Springboot实现高吞吐量异步处理详解(适用于高并发场景)

    异步处理器负责处理异步请求,异步处理队列负责存储异步请求,异步处理结果存储器负责存储异步处理结果。使用Springboot异步处理架构可以实现高吞吐量异步处理,并且可以提高服务器的性能和可扩展性。 技术要点3: ...

    如何在WPF应用程序中通过HttpClient调用Web API

    在WPF应用程序中,异步调用Web API是一项重要的技术,因为它可以避免UI线程阻塞,从而提高用户体验。在.NET框架中,HttpClient类提供了异步调用Web API的能力。本文将详细介绍如何在WPF应用程序中使用HttpClient异步...

    基于hibernate_validator的异步表单校验框架依赖文件

    5. **异步处理**:对于异步验证,可以使用Ajax请求将表单数据发送到服务器,然后在服务器端执行验证并返回结果,前端接收到结果后展示错误信息。 **四、扩展与定制** Hibernate Validator允许自定义校验注解和校验...

    Rust使用 reqwest 发起 GET/POST 异步请求.pdf

    Tokio是一个异步运行时,提供了执行异步代码的基础设施,reqwest就依赖于Tokio来实现其异步请求功能。 在使用reqwest发起异步HTTP GET或POST请求之前,需要在项目配置文件Cargo.toml中添加reqwest和Tokio的依赖项。...

    一般应用程序中如何处理Web页面的Session问题

    ### 一般应用程序中如何处理Web页面的Session问题 在Web开发过程中,Session管理是一项至关重要的技术,它确保了用户在多次请求之间的状态得以保存。本文将深入探讨在使用一般应用程序处理Ajax请求时如何有效地获取...

    ajax异步处理

    **Ajax异步处理详解** Ajax(Asynchronous ...综上所述,Ajax异步处理是现代Web应用中不可或缺的一部分,它提升了用户体验,实现了动态加载和无刷新操作。理解并熟练运用Ajax技术,是每个前端开发者必备的技能。

    ajax技术在web模式开发中的应用研究

    传统的Web应用依赖于页面之间的完全重载来响应用户的动作,这种方式不仅用户体验较差,还增加了服务器的压力。为了解决这些问题,一种名为AJAX的技术应运而生。AJAX(Asynchronous JavaScript and XML)是一种用于...

Global site tag (gtag.js) - Google Analytics