Author:放翁(文初)
Date: 2010/6/28
Email:fangweng@taobao.com
围脖:
前话
在前面的文章中,先给出了Web服务请求异步处理的压力测试报告,从数据角度描述了支持Web请求异步化的容器在不同并发用户下的处理能力及性能消耗。本文从概念的角度对于应用系统异步化,Web服务请求异步化和Web请求异步化规范及实现三方面做一个介绍,为系统异步化改造做好基础准备。(同样,文中大部分都是个人意见和想法,非完全正确,欢迎讨论)
应用系统异步化
Web服务请求异步化也是应用系统异步化的一种,因此首先谈一下对于应用系统异步化的一些看法和认识。
随着系统不断积累和发展,系统模块化是必然趋势,而模块之间的耦合性和依赖会直接影响系统的稳定性和可用性,因此系统异步化概念就产生了。异步化和其他技术一样,不是“万能药”,在适合的场景扬长避短才能够体现它的优势,提升系统的可用性和效率。
异步化要素:
1. 会话。
异步模式下,请求和结果不在一次交互中,因此需要通过会话的设计方式(增加会话码)来保证请求和结果的一一对应。另一种方式可以不需要会话,但是要求请求和结果是保证顺序的。(这种设计方式会使得系统的资源复用受到限制,同时容错很难保证,容易“串”,早先用NIO去实现低版本的Memcached客户端协议就存在没有会话号的问题)
2. Callback or Stateful Result。结果产生如何通知请求方,有两种手段:a. Callback,通过服务方主动推送的方式将结果推送给服务调用者。(如果是进程内,可以通过实现定义的回调接口方式,如果是进程外,可以通过注册URL或者WebService等方式来实现)。b. Stateful Result,是通过调用方不断轮询执行结果,当服务提供者服务处理完毕后,修改Result的状态。优劣不在此赘述。
异步化特点:
1. 系统间可用性和处理能力松耦合。
AàB(A系统依赖于B系统)
A的可用性最差情况是sum(A系统自身可用性,B系统可用性)。
A系统的处理能力是min(A系统处理能力,B系统处理能力)
作用:差别化系统设计,A系统可能是前端系统要求高处理能力,B系统是后段系统,要求高一致性,此时两个系统如果异步方式依赖,A可以设计的较为轻量化,在高并发下有很好的吞吐量。B系统可以设计的有足够的容错和备份机制,在效率上适当放低要求。(我们时常在设计系统的时候谈CAP原则,不同系统和流程不同阶段对于这三个因素的要求都是不同的,因此通过异步的方式防止由于对于其他系统的依赖导致本系统的CAP无法有效的权衡)
2. 资源的有效利用。
异步模式天生就需要事件驱动模型支持,而事件驱动模型在高并发情况下对资源管理和使用十分有效。NIO设计就是典型的异步模式,基于对信道事件的监听和分发,最大程度上复用信道,复用接收和发送缓存等相关资源,提高资源利用率,增强系统的服务能力。
3. 系统复杂度增加。
错误暴露及时性降低。当B系统出现问题时,A系统知晓情况被动,整个流程问题暴露及时性降低。因此要求B系统和A系统作好更多地容错,异常检查工作。(例如流程超时处理机制等)
4. 整体业务流程处理时间增大。
由于B反馈给A的结果是异步化,因此A就有了上述两种方式去获取结果:Pull和Push,Push就是B主动回调A的服务(及时性较强,不过仍有部分时间消耗),Pull的间隔时间决定了A获得结果可能存在的延时性。
异步化场景:
1. 模块间依赖,系统间依赖。
这里描述的是粒度,异步化最终是对一个流程中的部分环节的弱化:一种就是弱化非关键路径来保证关键路径的可用性和效率。另一种就是弱化整体流程的及时性和一致性,提高部分环节的处理能力和可用性。但不论哪一类弱化都是基于一定的业务粒度,模块应该说是最小的业务粒度,在模块内部设计就要求紧耦合。
2. 资源决定一切
异步的使用场景往往是为了节省资源,但是节省带来的成本就是复杂度,当资源本身就不是问题(并发不高,资源足够)的情况下,无需要选择异步方式来增加复杂度,同时反而降低了可用性和稳定性。
3. 整体观与局部观
AàB,A依赖于B,此时采取异步化的方式,A的处理能力得到提高,大量的请求被提交到B上,B由于请求堆积,导致性能下降甚至崩溃,那么其实对于整体处理流程来说并没有随着A处理能力曲线上升而上升,因此此类优化没有协调好局部和整体。
AàB AàC,A依赖于B,A依赖于C,异步改造A和B之间的关系,但是A还是受限于C,那么此类优化未必有效果,同时增加了复杂度。(这里就要关注整体流程的关键路径,关键路径的优化才是有效的优化)
Web服务请求异步化
随着Servlet3.0的日趋成熟和各个Web容器厂商的支持,Web服务请求异步化在很多领域开始慢慢被使用和熟悉。
Web服务请求异步化与NIO的概念是不同的(Web服务请求异步化可以基于BIO的底层交互也可以基于NIO的底层交互),Web服务请求异步化是业务层的异步设计,与普通的应用系统异步化差别在于Web请求有固定的规范,请求流程和接口较为固定,同时请求底层的资源管理交由Web容器处理,因此在第一部分中所谈到的异步的优势劣势,适用场景同样适合Web服务请求的异步化。
这里也顺带谈一下为什么NIO在Web领域里面被没有像后台系统一样被得到广泛使用:
a. 信道复用的投入产出比例。Http请求是无状态的请求应答模式,信道复用概率不像内部后台系统那么高,再加之业务时间占整体流程时间绝对大的比例,那么投入产出不成比例。
b. Web请求受限于Servlet规范的生命周期管理,导致前段无论如何异步,在服务处理过程中都是同步阻塞模式,因此异步不彻底,无法体现NIO的优势。
三种Web请求模型的演进:
1. Thread Per Connection。在BIO的模型下,每次连接都会被分配一个线程,线程负责底层数据接收发送,业务处理。
2. Thread Per Request。在NIO的模型下,将不再为连接单独分配线程,而为每一次请求事件发生创建线程,处理业务,对于底层的数据发送和接收资源做到了共享,同时数据通道得到了共享。
3. Thread Per Service Event。在Web请求异步处理模型下,底层数据处理可以依赖于第二个处理模型,上层业务处理将业务状态对象独立于业务处理流程,通过事件驱动模式来分阶段触发业务各阶段处理,为每一次事件处理创建线程和资源(通常是资源池),最终提高资源利用率。
介绍一下四种场景下的Web请求处理:
角色介绍:(在下面的场景中会涉及到一些角色,有些隶属于容器,有些是外部服务和资源)
:并发用户。T代表有T个并发用户。
Conn Thread Pool :连接线程池,属于Web容器的一部分,作为响应和处理请求的线程资源获取来源。
Conn Thread:线程连接,从线程连接池中获取到的线程。
Service Provider:业务实现者。可以是本系统的业务实现,也可以是其他系统的业务实现,可以是异步方式的返回也可以是同步方式返回结果。(N)代表本身业务处理需要N个时间单位。
Worker Thread Pool:外部工作线程池,可以接收处理“耗时”的业务流程。
1. 非异步化Web请求处理
从这个场景可以看出,在非异步化Web请求的容器中,不论后端服务是否采取异步化,由于请求本身需要在一次阻塞式交互中返回,那么连接线程池中的线程在后端服务异步化的同时依然Hold没有被释放,当前端并发量增加的时候,容器的吞吐量就会成为瓶颈(就算后端服务能力还有很大的剩余)。
Resource表示消耗的资源:在T个并发用户下,需要消耗T个连接线程资源。
Response表示响应处理时间:N个时间单位。
2. 异步化Web请求处理,后端服务提供者为阻塞模式。
这种模式下,增加了一个工作者线程池,在做后端服务处理的时候,工作线程池的线程取代了连接线程池的线程,在工作者线程获得了挂起的异步上下文以后, 释放了连接线程池的线程,当业务执行完毕以后,工作者线程通过异步上下文,提交返回结果,最后释放自身资源。
Resource:少量ConnThread + T个WorkThread。
Response:N + workerThead消耗(创建,异步调用等)
可以看到,对于后端没有支持异步化的情况下,仅仅前端容器异步化能够起到效果的前提是:1. WorkThread很轻量化,消耗资源远小于连接池线程资源。2.WorkerThread消耗占整体消耗的很小一部分,甚至可以忽略。此时通过用轻量级线程池替换容器连接线程池可以较好的提高效率和资源利用率。
3. 异步化Web请求处理,后端服务提供者为非阻塞模式。(Push & complete mode)
Push & complete mode指的是对于后端服务结果的反馈是Service Provider主动push给服务调用者,在Web请求异步化过程中,返回请求结果是直接通过调用异步上下文的complete事件来触发commite的。(想对于resume的唤醒重入方式)
此场景服务提供者支持异步化处理业务请求,因此连接线程池的线程负责处理最轻量一些操作,然后将业务请求转交给服务提供者,同时将异步上下文传给服务提供者的处理线程,就此连接池连接资源释放。当服务完成后,服务端线程通过异步上下文获取到输出对象,将处理结果直接返回给客户。
Resource:少量Conn Thread。
Response:N个时间单位。
可以看到容器请求异步化结合后端服务体系异步化能够起到最好的效果,但有一点是要注意的,前端线程池在没有异步化以前吞吐量取决于它的线程池大小和后端服务处理速度,而当异步化后,吞吐量取决于它的线程池大小和异步化带来的消耗,明显并发服务能力得到了很大的提升(特别是后端服务耗时严重的时候),这样就意味着会有更多的服务请求流向后端,当后端处理能力无法支撑的时候,那么N那个时间单位就会上升,同时稳定性也会产生问题,因此反而会起副作用,因此这种模式需要评估后端服务能力,保证异步化后服务质量依旧。
4. 异步化Web请求处理,后端服务提供者为非阻塞模式。(Pull & Complete mode)
Pull & complete mode指的是对于后端服务结果的反馈是Service Provider被动的等待其他监控线程定时pull结果对象并比对结果状态确定是否完成,在Web请求异步化过程中,返回请求结果是直接通过调用异步上下文的complete事件来触发commite的。(想对于resume的唤醒重入方式)
这个场景和前一个场景差别在于对于结果的获取方式,同样对于后台服务来说,前端系统的业务处理不会侵入到它的业务代码(Push方式会要求Service Provider回调或者直接提交结果到客户端)。这种场景需要增加一个结果队列,连接线程池中的线程责任就是调用后端服务,然后将Future结果和异步上下文作为服务结果放入队列。由一个小的工作者线程池定时检查任务执行情况,当执行通过时,直接取出结果集,将结果通过异步上下文输出到客户端。
Resource:少量的Conn Thread + 部分worker Thread
Response:N + 异步化消耗的时间(结果轮询消耗的时间)
就系统本身来说,稳定性和可用性有所“折扣”,增加了对于队列和工作者线程的依赖。
5. 异步化Web请求处理,后端服务提供者为非阻塞模式。(Push & resume mode)
Push & resume mode指的是对于后端服务结果的反馈是Service Provider主动push给服务调用者,在Web请求异步化过程中,在业务处理结束后,重入当前同样的Servlet中(带上结果),由Servlet在新的请求中返回结果给客户端。(在Servlet3.0中是允许dispatch到不同的Servlet中,这样带来的灵活性就比较高了,在jetty的continuation中只允许重入当前请求的Servlet)
重入机制会给容器带来一定的压力,一次请求在容器这边变成了两次或者多次请求,同时对于Servlet中的业务代码需要去关注是否是原始请求还是被模拟的重入的请求,区别化对待。
Resource:部分的Conn Thread。
Response:N + redispatch time。
连接线程消耗要比普通的complete来的多,同时消耗时间也比complete模式来的大。
Web请求异步化规范及实现
当前实现Web请求异步化的容器有Jetty6,Jetty7,Tomcat7.其中Jetty6支持他特有的Continuation机制,jetty7支持Continuation和Servlet3.0,Tomcat7支持Servlet3.0.后面就从Jetty的角度去介绍Continuation机制,再比较Continuation与Servlet3.0的差异。
Continuation
Jetty可以使用BIO或者NIO的底层来支持Continuation,不过就效果来说肯定是NIO的效果好,这里给出两个图(Jetty的NIO的类结构图和Continuation的交互图),从中可以看到这两块设计的实现。
Jetty NIO 类图
Jetty作为外部容器或者嵌入式容器入口都是Server,Server中包含了Connector(这块实现的不同就决定了是用BIO还是NIO的模式,这里描述的是NIO模式,因此Connector是SelectorChannelConnector),ThreadPool成为整个系统中的线程资源池,用来完成事件驱动模型的各种需求。为了提高性能,NIO模式下的Connector包含多个Selector(即SelectSet)和Acceptor,通过Acceptor循环检测来触发多个Selector检查IO事件,当有请求产生的时候创建SelectChannelEndPoint来分配必要的资源处理请求(与前面描述的Thread Pre Request是一致的,确切的说是One Resource Pre Request)。
Continuation 交互图
图片看不清楚可以去flickr上看( )
1. 用户发起请求。
2. NIO的Selector接收到了IO事件创建了Endpoint分配了相应的资源。
3. 同时将Endpoint作为一个任务封装后插入到线程池队列中,等待工作线程执行请求处理。(IO事件处理到此结束)
4. 工作线程池中线程执行请求处理。
5. 先读取请求数据。
同步模式:
6.1 -6.4调用内部的handler串行化处理请求,最后返回处理结果。
异步模式:
7.1 执行Handler的业务逻辑。
7.2 挂起请求,进入异步模式。
7.3 创建异步事件置入到Request中。(一来用于容器后续判断当前请求是否处于同步模式,是否需要提交response,另一方面用于异步事件的超时检测)
7.4 在Servlet的原生命周期的方法中(service,doget,dopost …)创建新的线程去执行业务操作,并且将Continuation传递给线程用于后续complete或者resume来提交业务处理结果或则重新分发进入Servlet。
7.5 结束常规的Servlet生命周期。
7.6 容器判断,如果是异步模式,则将异步事件放入到timeoutTasks这个链状超时事件队列中,如果没有启动异步模式,则提交结果,回收请求处理资源。
7.7.1 工作线程执行业务处理。
7.7.2 执行完毕业务处理以后调用Continuation的complete或者resume方法来提交处理结果。
7.7.3 complete或者resume方法调用将产生事件被放入到了线程池队列中。
7.7.4 执行complete或者resume事件,调用事件宿主Endpoint的分发请求的处理。(可以理解为重新模拟执行了一次服务端的dispatch,也就是重新执行一次handler链,不过中间结果数据已经完全不同)
7.7.5 如果没有complete则重新回到7.7.1
7.7.6 如果complete 则返回结果,回收资源。
7.8.1 Endpoint会循环检查异步事件是否已经超时(查看timeoutTasks)。
7.8.2 如果出现超时,则封装超时事件放入线程池队列等待执行。
7.8.3 线程池执行超时处理,返回结果,回收资源。
Servlet3.0 与 Continuation的差异
可以说Jetty团队在Servlet3.0没有成为正式规范之前就参考了它的设计理念,因此本质上来说两者没有太大的区别,唯一的几个区别点在于:
1. Continuation和AsynContext分别是两个体系的异步上下文载体。
2. Continuation resume机制没有Servlet3灵活,Servlet3可以支持dispatch到内部任意的Service上。
异步化在客户端
前面一致介绍异步化在服务端的应用,其实在客户端的应用可以提高客户端的连接能力及容错能力(加长Timeout时间也不会导致连接耗尽),Jetty已经支持客户端异步化,使用比较简单。
后话
这些是刚开始,接下来对于应用实际的改造(TOP现有管道化流程的异步化尝试)会找到异步化的优势和软肋。如何用好异步化对于高并发的多模块或者多依赖系统来说是很关键的,是一把双刃剑,需要有足够能力的人去把控,这个人需要的不仅是教条,更多的是经验。