最专业的八方代购网站源码!

资讯热点
HTTP服务异步转换实践

发布时间:2020-7-14 分类: 电商动态

背景

我们有一个在门户级别提供HTTP服务的应用程序。由于业务的复杂性,用户请求的处理涉及对后端远程服务的多次调用。为了实现简单,目前以同步方式完成,即在处理请求时,它将占用用于逻辑操作和同步远程调用的容器线程。这种开发方法的好处是直观且开发成本低,但它们也引入了一些稳定性和资源浪费问题。对于我们的HTTP服务,同步的实现带来了以下三个问题。

下游服务超时导致的服务可用性问题。请求超时的一部分将导致HTTP服务线程池已满,导致其他请求无法获取线程资源并失败。

性能问题,串行执行多次远程服务调用,导致服务响应时间过长。

容量问题,有限的服务吞吐量。每个请求都占用一个线程很长时间,导致线程未得到充分利用。

为了解决这些问题,结合当前使用的技术堆栈和适应成本,我们对HTTP服务进行了异步转换。

溶液

回调地狱,在异步编程中已知,已经阻止了许多学生。当业务复杂时,各种回调都嵌套在一起,使得代码更容易出错并且不易理解。业界还有许多提供异步编程支持的框架。有三个想法:

光纤路径

光纤可以被视为轻量级用户线程,与操作系统的调度机制分离,并计划在应用程序级别进行管理。因为它只维护基本的执行堆栈信息而不立即分配执行资源,所以它可以轻松创建数千个光纤(受内存大小限制)并使用极少的线程调度光纤调度。执行。这个方向的代表是微信团队的开源libco和语言级支持的Go语言。 Libco挂钩底层IO相关系统功能,并通过底层IO事件驱动光纤调度。遇到同步网络调用时,libco会自动注册回调侦听器并放弃CPU。当IO事件完成或随着时间的推移,光纤会自动恢复,然后安排执行。它的实现机制确定它非常适合依赖于耗时的IO服务的实现。它是微信数万电话的基石。不幸的是,libco是一个高效的c/c ++协程库,它没有在JVM上实现。

Quasar在JVM顶部实现光纤机制。基本上,异步代码可以基于Quasar的类库以同步模式编写。在实际执行代码之前,相关字节码以编译或仪器代理的形式编织。从头开始引入光纤仍然是一个不错的选择。对于现有项目的转换,有必要将现有的线程类修改为光纤类,这需要在底层更改大量中间件。此外,业界发表的经验较少,后续可以继续关注其发展。

演员模特

Actor模型并不是一个真正的新概念。近年来出现了越来越受欢迎的趋势。 Actor模型中的核心概念是Actor实体。每个Actor实体负责逻辑计算。传统的并发编程基于共享内存的方式来实现多个线程之间的通信。演员不共享数据,也不直接沟通。相反,他们发送或接受邮箱/队列中的消息以实现通信。演员由消息驱动。正式由于发送方和接收方的分离,Actor具有固有的并发特性,可以优化IO等待问题,而不考虑actor之间的同步问题和接收消息的Actor的无限制调度。 Scala,Golang等在语言级别支持Actor模型。在新版本的Scala中,推出Akka以完成Actor模型并具有Java版本。但是,有必要引入一个新的API来将现有业务代码块转换为Actor模型,该模型对现有代码进行了大量更改。

RX

Rx也是一种编程模型,它试图提供统一的异步编程接口包来操作可观察的数据流。它吸收了函数式编程的优秀思想,实现了观察者和迭代器模式的精致实现。目前流行的语言基本上有相应的实现。例如,RxJava类库提供了java版本的实现,并且RxJava已成功应用于Netulx Zuul项目。 Rx看起来更像是编程思维的突破。它提供了统一的函数式编程接口,以简化异步程序的编写,并在内部通过回调机制,它可以获得比Actor更好的响应速度。在研究过程中,我们发现它还需要对现有代码进行重大更改,并将之前的同步模式转换为函数式编程风格。

总的来说,上述一些优秀的框架不能立即用于我们的项目,引入成本仍然很高。结合现有技术架构并且产品快速迭代,我们对HTTP服务进行了轻量级异步转换。此转换引入了基于图形的执行引擎,以解决服务之间的复杂依赖关系并集中管理异步状态。结合Servlet 3.0,它提供了一个用于请求和释放tomcat容器线程的接口,充分利用了Servlet容器线程资源。最后,spring mvc的异步模块连接这两个异步机制,以达到完全堆栈异步的目的。

原理分析

Servlet从3.0开始添加,并添加了异步规范。 Spring mvc还支持从3.2开始的异步Servlet 3.0。对于现有技术的堆栈,可以通过以下代码说明全栈异步化:

如您所见,orderService.createOrderAsync(request)调用不会等待在发出请求后返回结果,而是立即返回。监听器在返回的未来对象上注册。最后返回DeferredResult。 Spring mvc会在收到DeferredResult的返回结果时调用(当然它也可以是WebAsyncTask和Callable)

AsyncContext context=HttpServletRequest.startAsync(req,response);

获取上下文然后退出容器线程。当createOrderAsync完成结果时,将调用将来注册的侦听器以开始执行。这里,忽略了一些中间处理,并且直接在DeferredResult上设置了RPC结果。通过调用Servet

的上下文调用执行结果后的Spring mvc

Context.dispatch();

通知容器继续后续操作,例如重新进入spring mvc拦截器的完整进程,最后将结果输出到客户端。整个过程可以用下图表示:

图中的三个框表示整个请求被分解并分三个阶段执行。第二个框的第一个框表示正在执行RPC服务。此时,处理请求的线程已被释放。它可以继续接受其他请求。当RPC服务具有返回值或超时时,将在单独的线程池中引发已注册的侦听器。最后通知servlet容器以在第三个框中继续interceptor.complete。通过回调通知机制,CPU将得到充分利用。避免启动有价值的线程以等待IO完成。

基于图形的执行引擎

真实的业务场景比上面的代码复杂得多。例如,订单业务,一般依靠用户,报价,付款,优惠和其他服务。服务之间存在依赖关系,例如黑名单服务验证传递提交订单。还存在点对点的服务,并且彼此之间没有依赖关系,并且可以并行调用以减少服务的总体响应时间。如下所示,这是一个常见的服务依赖:

在图中,A,B和C没有依赖关系,实际上可以并行执行。 C服务不关心返回结果,因此呼叫通知将被发送并且可以结束。 D服务需要等待A的结果,并且E需要等待B和D的执行结果。使用传统的异步编程,这可能是它的样子:

可以看出,服务的依赖关系隐藏在代码行之间,业务逻辑散布在每个回调中,而ListeableFuturefutureBT在中间引入以管理异步状态。不容易阅读和维护。为此,我们提供了基于图形的执行引擎(GBEE)。 GBEE的主要目标是解决以下问题:

(1)管理服务之间的依赖关系

服务之间的依赖关系与业务代码分离,服务之间的依赖关系由有向非循环图数据结构描述。图中的每个节点都保存其前一个(后驱动)节点。每个节点执行的先决条件是其所有前任节点都已完成。

(2)统一注册回调

每个节点都可以覆盖回调以注册自己的侦听器。一般用于转换结果,记录监控。回调由执行者统一注册。避免在代码嵌套中注册侦听器。

(3)使用异步事件驱动的执行

异步事件侦听器在GBEE中统一注册,并在事件发生时驱动回调,或者在条件成熟时调用下一个节点的执行。

具体做法:

(1)将业务逻辑分成多个节点,每个节点负责特定的业务逻辑执行,但没有任何状态,例如启动异步RPC调用和返回ListenableFuture。

(2)通过配置文件定义依赖关系管理

每个Node定义自己的父节点,这意味着依赖节点。 Spring本身提供了服务的依赖管理功能。因此,其依赖关系定义如下:

(3)提供执行器基于图形的执行器,负责统一注册监听器和管理异步状态。

在每个请求到达后,可以通过上面的依赖关系配置构建基于图的执行程序:

Graph将找到根节点,并且多个根节点可以同时并行。

Apply(node,context)是一个递归调用。每次执行当前节点时,都会主动探测父节点是否可以作为自己的节点执行:

基于图形的执行程序将业务代码与底层异步机制分离,使每个节点更专注于自己的业务。

后记

迁移特定服务时,将来会遇到一些常见问题。

(1)公司的RPC服务主要发送到dubbo,这使得使用公司的基本组件可以很容易地使用异步调用。

(2)使用tomcat 6仍然有很多应用程序在线,tomcat 7支持Servlet 3,你应该将应用程序升级到tomcat 7.

(3)web.xml配置有几个重要的配置。

为了让spring mvc实际启用异步支持,除了需要激活org.springframework.web.servlet.DispatcherServlet的异步选项外,即:true

您还需要在此servlet之前为所有过滤器设置async-supported为true。只要中间没有设置过滤器,后者设置无效。在后续开发中,如果添加过滤器,则还必须对其进行配置。

(4)ThreadLocal问题。

现有系统的一些常见上下文参数通过ThreadLocal传递。在异步转换之后,代码并不总是在请求线程中执行。这使通过ThreadLocal传递的变量无效。我们采用了两种方法来解决,一种是转换一些业务代码,以参数的形式传递。另一种是在HttpServletRequest的Attribute中存储一些公共变量。在异步上下文中维护对HttpServletRequest的引用。然后通过工具类直接从HttpServletRequest中提取公共变量。

(5)异常处理

在同步代码中,我们通常会自定义一些业务异常。捕获这些业务异常后,我们会根据异常合理性和状态代码执行一些业务逻辑。由ListeableFuture继承的Future接口指定在异步计算期间抛出的所有异常都封装在ExecutionException中。此时,在同步代码中捕获,就无法捕获ExecutionException。此时,业务代码需要修改特定类型的捕获,然后通过Exception.getCause()获取原始异常。这个部分可以由基于图形的执行引擎统一处理。转换原始异常后,将调用节点的onException。

« 在设计过程中不可忽视的交互设计的关键点是什么? | 如何设计更好的日期选择器? »