背景我们有一个在ingress层提供HTTP服务的应用。随着业务的复杂性,一个用户请求的处理涉及到对后端远程服务的多次调用。为了简单起见,目前采用同步的方式进行,即在处理一个请求的过程中,会占用一个容器线程进行逻辑操作和同步远程调用。这种开发方式的优点是直观,开发成本低,但也带来了一些稳定性和资源浪费的问题。对于我们的HTTP服务来说,同步的实现带来了以下三个问题。下游服务超时导致的服务可用性问题。部分请求超时会导致HTTP服务线程池爆满,导致其他请求无法获取线程资源而失败。性能问题,多次调用远程服务串行执行,导致服务响应时间长。容量问题,服务吞吐量有限。每个请求长时间占用一个线程,导致线程未被充分利用。为了解决这些问题,结合目前使用的技术栈和适配成本,我们对HTTP服务进行了异步改造。解决方案异步编程中著名的CallbackHell让很多同学望而却步。当业务复杂时,各种回调相互嵌套,使得代码更容易出错,难以理解。业界也有很多框架提供异步编程支持。有以下三种思路:Fibers可以看作是轻量级的用户线程,脱离了OS的调度机制,在应用层进行调度管理。由于它只维护基本的执行栈信息,不立即分配执行资源,因此可以轻松创建数千个纤程(受内存大小限制),并通过极少的线程实现完成纤程的调度。这个方向的代表有微信团队开源的libco,语言层面支持的Go语言。libcohook底层IO相关的系统函数,通过底层IO事件驱动fibers的调度和执行。当遇到同步网络请求时,libco会自动注册一个回调监听器并让出CPU。当IO事件完成或超时后,fiber会自动恢复,然后调度执行。它的实现机制决定了它非常适合依赖于耗时IO服务的实现。承载着微信***通话的基石。不幸的是,libco是一个高效的c/c++协程库,它并没有在JVM上实现。Quasar在JVM之上实现了纤程机制。基本上,基于Quasar的类库,可以用同步的方式编写异步代码。在代码真正执行之前,以编译或InstrumentAgent的形式编织相关的字节码。从头开始引入纤维仍然是一个不错的选择。对于现有项目的改造,需要将现有的thread类修改为fiber类,这需要我们底层的中间件改动很多。另外,目前业界公布的使用经验较少,后续可继续关注其发展。Actor模型Actor模型并不是一个新概念。近年来,有越来越受欢迎的趋势。Actor模型中的一个核心概念是Actor实体。每个Actor实体负责一个逻辑计算。传统的并发编程都是基于共享内存来达到多线程间通信的目的。Actor之间不共享数据,也不直接通信,而是在mailbox/queque中发送或接收消息,达到通信的目的。Actor由消息驱动。形式上由于发送方和接收方的分离,是的,Actor具有与生俱来的并发特性。它可以在不考虑Actor之间同步问题的情况下,调度执行接收消息的Actor,从而优化IO等待问题。Scala、Golang等在语言层面支持Actor模型。在新版本的Scala中,引入了Akka来完成Actor模型,还有Java版本。但是需要引入新的API,将现有的业务代码块改造为Actor模型,并对现有代码进行较大改动。RXRx也是一种编程模型,它试图提供一个统一的异步编程接口包来操作一个可观察的数据流。它吸取了函数式编程的优秀思想,将观察者模式和迭代器模式实现的淋漓尽致。目前流行的语言基本上都有相应的实现。例如RxJava类库提供了java版本的实现,RxJava已经成功应用在Netflix的Zuul项目中。Rx看起来更像是一种编程思维的突破。它提供统一的函数式编程接口,简化异步程序的编写。同时,它内部也使用了回调机制,以获得比Actor更好的响应速度。在我们的研究中,我们发现它还需要对现有代码进行重大更改,并将之前的同步模式转换为函数式编程风格。综合来看,上面的一些优秀的框架并不能马上用到我们的项目中,引进成本还是很高的。结合现有的技术架构和产品快速迭代的环境,我们对HTTP服务进行了轻量级的异步改造。在这次转型中,引入了Graph-BasedExecutionEngine来解决服务之间复杂的依赖关系,集中管理异步状态。结合Servlet3.0,提供了请求和释放tomcat容器线程的接口,充分利用了Servlet容器线程资源。***,通过springmvc的异步模块把这两种异步机制串联起来,达到了全栈异步的目的。原理分析Servlet从3.0开始加入了异步规范。SpringMVC从3.2开始也支持异步Servlet3.0。对于现有的技术栈,全栈异步的实现可以用下面一段代码来说明:可以看到,orderService.createOrderAsync(request)的调用并没有在请求发送后等待返回结果,但立即返回。在返回的未来对象上注册了一个侦听器。***返回延迟结果。当springmvc收到返回结果为DeferredResult(当然也可以是WebAsyncTask和Callable)时,会调用AsyncContextcontext=HttpServletRequest.startAsync(req,response);获取上下文,然后退出容器线程。当createOrderAsync完成并获得结果时,将调用在future上注册的侦听器并开始执行。这里忽略一些中间处理,直接在DeferredResult上设置RPC结果。springmvc获取到执行结果后,调用Servet的上下文context.dispatch()通知容器继续进行后续操作,比如重新进入springmvc拦截器的完整流程,最后将结果输出到客户端。整个过程可以用下图来表示:图中的三个框表示将整个请求打散,分三个阶段执行。***框和第二个框之间,表示正在执行RPC服务。此时处理请求的线程已经被释放。它可以继续接受其他请求。当RPC服务返回值或超时时,它会在单独的线程池中调用已注册的侦听器。最后在第三个框通知Servlet容器继续执行interceptor.complete。通过回调通知机制,CPU将得到充分利用。避免启动等待IO完成的宝贵线程。Graph-BasedExecutionEngine的真实业务场景要比上面的代码复杂的多。例如,订单业务一般依赖于用户、报价、支付、折扣等服务。服务之间存在依赖关系,比如黑名单服务验证提交订单。还有一些服务是对等关系,相互之间没有依赖关系,可以并行调用,以减少服务的整体响应时间。如下图所示,这是一个普通的服务依赖:图中的A、B、C没有依赖关系,实际上是可以并行执行的。C服务不关心返回结果,所以调用通知发出后即可结束。D服务需要等待A的结果,E需要等待B和D的执行结果。如果用传统的异步编程,看起来是这样的:可以看到服务依赖隐藏在代码行之间,业务逻辑穿插在每个回调中,中间引入ListeableFuture
