深刻剖析通讯层和 RPC 调用的异步化:二

做者:李林锋
连接:https://www.infoq.cn/article/q3iPeYQv-uF5YsISq62c

深刻剖析通讯层和 RPC 调用的异步化:二

 

1. 异步 RPC 调用的应用场景react

1.1 缩短长流程的调用时延数据库

随着业务分布式架构的发展,系统间的系统调用日趋复杂,以电商的商品购买为例,前台界面的购买操做涉及到底层上百次服务调用,造成复杂的调用链,示例以下:编程

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 1 分布式消息调用链缓存

对于一些逻辑上不存在互相依赖关系的服务,能够经过异步 RPC 调用,实现服务的并行调用,经过并行调用来下降服务调用总耗时,以手游购买道具流程为例,消费次数限制鉴权、帐户余额鉴权和下载记录鉴权三个服务能够经过异步的方式并行调用,来下降游戏道具购买的耗时:网络

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 2 购买道具异步 RPC 调用流程架构

1.2 服务调用耗时波动较大场景并发

对于一些业务场景,服务调用耗时与消息自己、调用的资源对象有关系,例如上传和下载接口,若是下载的资源较多则耗时就会相应的增长。对于这类场景,接口的调用超时时间比较难配置,若是配置过大,服务端自身响应慢以后会拖垮调用方,若是配置太小,万一遇到一个须要较长耗时的 RPC 调用就会超时。经过异步 RPC 调用,就不用再担忧调用方业务线程被阻塞,超时时间能够相应配置大一些,减小超时致使的失败。框架

1.3 第三方接口调用异步

对于大部分的第三方服务调用,都须要采用防护性编程,防止由于第三方故障致使自身不能正常工做。若是采用同步 RPC 方式调用第三方服务,一旦第三方服务的处理耗时增长,就会致使客户端调用线程被阻塞,当超时时间配置不合理时,系统很容易被阻塞。经过异步化的 RPC 调用,能够防止被第三方服务端阻塞,Hystrix 的第三方故障隔离就是采用相似机制,只不过它底层建立了线程池,经过 Hystrix 的线程池将第三方服务调用与业务线程作了隔离,实现了非侵入式的故障隔离。async

1.4 性能和资源利用率提高

对于一个同步串行化调用的系统,大量的业务线程都在等待服务端返回响应,系统的 CPU 使用率很低,可是性能却没法有效提高,这个问题几乎是全部采用同步 RPC 调用的业务都遇到的一个通病。要想充分利用 CPU 资源,须要让业务线程尽量的跑满 CPU,而不是常常性的处于同步等待状态。采用异步 RPC 调用以后,在单位时间内业务线程能够接收并处理更多的请求消息,更充分的利用 CPU 资源,提高系统的吞吐量。

根据一些公开的测试数据,一些业务采用异步 RPC 替换同步 RPC 调用以后,综合性能提高 2-3 倍 +。

2. 异步 RPC 调用实践

2.1 Tomcat + Servlet3.X 的异步化

2.1.1 工做原理

Servlet 异步是指 Servlet 3 规范中提供了对异步处理 Servlet 请求的支持,能够把 HTTP 协议处理线程和业务逻辑执行线程隔离开:

1.Servlet3.0 对异步的支持:Servlet3 以前一个 HTTP 请求消息的处理流程,包括:HTTP 请求消息的解析、Read Body、Response Body,以及后续的业务逻辑处理都是由 Tomcat 线程池中的工做线程处理。Servlet3 以后可让 I/O 线程和业务处理线程分开,进而对业务作隔离和异步化处理。还能够根据业务重要性进行业务分级,同时把业务线程池分类,实现业务的优先级处理,隔离核心业务和普通业务,提高应用可靠性。

2.Servlet3.1 对非阻塞 I/O 的支持:Servlet3.1 之后增长了对非阻塞 I/O 的支持,根据 Servlet3.1 规范中描述:非阻塞 I/O 仅对在 Servlet 中的异步处理请求有效,不然,当调用 ServletInputStream.setReadListener 或 ServletOutputStream.setWriteListener 方法时抛出 IllegalStateException 异常。Servlet3.1 对非阻塞 I/O 的支持是对以前异步化版本的加强,配套 Tomcat8.X 版本。

Tomcat + Servlet3 的异步化处理原理以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 3 Tomcat + Servlet3 异步处理原理

2.1.2 异步化处理流程

关键处理流程以下:

1.声明 Servlet,增长 asyncSupported 属性,开启异步支持,例如 @WebServlet(urlPatterns=“/AsyncLongRunningServlet”,asyncSupported=true)。

2.经过 request 获取异步上下文 AsyncContext, AsyncContext context = request.startAsync(), 相关接口定义以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

3.启动业务逻辑处理线程,并将 AsyncContext 对象传递给业务线程。例如:Executor.execute(()->{context, request, response…})。

4.在业务线程中,经过获取 request 进行业务逻辑处理,完成以后填充 response 对象。

5.业务逻辑处理完成以后,调用 AsyncContext 的 complete() 方法完成响应消息的发送。

2.2 Spring MVC 异步化

2.2.1 工做原理

SpringMVC 3.2+ 版本基于 Servlet3 作了封装,以简化业务使用。它的工做原理以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 4 SpringMVC 异步工做原理

2.2.2 异步的几种实现方式

SpringMVC 支持多种异步化模式,经常使用的有两种:

1.Controller 的返回值为 DeferredResult,在业务 Controller 方法中构造 DeferredResult 对象,而后将请求封装成 Task 投递到业务线程池中异步执行,业务执行完成以后,构造 ModelAndView,调用 deferredResult.setResult(ModelAndView) 完成异步化处理和响应消息的发送。

2.Controller 的返回值为 WebAsyncTask,实现 Callable, 在 call 方法中完成业务逻辑处理,由 SpringMVC 框架的线程池来异步执行业务逻辑(非 Tomcat 工做线程)。

以 DeferredResult 为例,它的异步处理流程以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 5 SpringMVC DeferredResult 工做原理

2.3 Apache ServiceComb 的异步化服务调用

Apache ServiceComb 是一个开箱即用、高性能、兼容流行生态、支持多语言的一站式开源微服务解决方案。它同时支持同步和异步服务调用,下面一块儿分析下它的异步化服务调用机制。

2.3.1 纯 Reactive 模式

纯 Reactive 模式的特色是:

1.异步化接口,消费端不须要同步等待服务提供端返回响应,不会产生阻塞。

2.与传统流程不一样的,全部功能都在 eventloop 中执行,并不会进行线程切换。

3.只要有任务,线程就不会中止,会一直执行任务,能够充分利用 cpu 资源,也不会产生多余的线程切换,去无谓地消耗 cpu。

它的处理流程以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 6 ServiceComb 的 Reactive 工做模式

关键流程解读:

1.异步:橙色箭头走完后,对本线程的占用即完成了,不会阻塞等待应答,该线程能够处理其余任务。

2.当收到远端应答后,由网络数据驱动开始走红色箭头的应答流程。

对应的代码示例以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

经过代码示例能够看出,ServiceComb 的 Reactive 工做模式采用了 JDK8 的 CompletableFuture 做为异步编程模型,利用 CompletableFuture 能够方便的对多个异步操做结果作编排,以及作级联异步操做,功能强大,使用灵活。

纯 Reactive 模式的使用约束:全部在 eventloop 中执行的逻辑,不容许有任何的阻塞动做,包括不限于 wait、sleep、巨大循环、同步查询 DB 等等。实际上就是若是业务的微服务采用了 Reactive,则须要作全栈异步,不然会阻塞 eventloop 线程,致使消息收发出现问题。若是业务的微服务想作异步化,可是因为数据库、缓存等缘由没法实现全栈异步,则能够采用后面介绍的混合 Reactive 模式。

2.3.2 混合 Reactive 模式

混合 Reactive 模式的实现策略以下:

1.服务端接口返回值为 CompletableFuture,这样采用透明 RPC 调用时就能够实现异步化。

2.对于可能产生同步阻塞的业务逻辑代码,采用独立线程池的方式进行处理,防止阻塞平台的 eventloop 线程。

混合 Reactive 模式与纯 Reactive 模式相比,主要有两点差别:

1.存在线程切换。

2.可能致使同步阻塞的业务逻辑放到独立的线程池中执行,纯 Reactive 模式全部业务逻辑都在 eventloop 线程中执行(与 I/O 线程相同)。

它的处理流程以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 7 ServiceComb 的混合 Reactive 工做模式

2.3.3 异步模式的几个特色

相比于其它的微服务框架(RPC 框架),ServiceComb 的 Reactive 有以下几个特色:

对于微服务提供端:

1.producer 是否使用 reactive 与 consumer 如何调用,没有任何联系。

2.当 operation 返回值为 CompletableFuture 类型时,默认此 operation 工做于 reactive 模式,此时若是须要强制此 operation 工做于线程池模式,须要在微服务的配置文件中(microservice.yaml)中明确配置,指定业务线程池。这样业务逻辑的执行就能够由 eventloop 线程(I/O 线程)切换到业务线程。

对于微服务消费端:

1.consumer 是否使用 reactive 与 producer 如何实现,没有任何联系。

2.当前只支持透明 RPC 模式,使用 JDK 原生的 CompletableFuture 来承载此功能 ompletableFuture 的 when、then 等等功能均可直接使用。

对于 ServiceComb,不管服务端定义的接口是同步仍是异步的,消费端均可以采用异步的方式调用它,对具体细节感兴趣的读者能够到 ServiceComb 官网下载 Demo 示例学习。

2.3.4 I/O 线程和业务线程的交互优化

ServiceComb 微服务的完整线程模型以下图所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 8 I/O 线程和业务线程交互

ServiceComb 经过线程绑定技术来减小锁竞争,提高性能:

1.业务线程在第一次调用时会绑定某一个网络线程, 避免在不一样网络线程之间切换, 无谓地增长线程冲突的几率。

2.业务线程绑定网络线程后, 会再绑定该网络线程内部的某个链接, 一样是为了不线程冲突。

2.4 gRPC 的异步化

gRPC 的服务调用有三种方式:

  1. 同步阻塞式服务调用,一般实现类是 xxxBlockingStub(基于 proto 定义生成)。
  2. 异步非阻塞调用,基于 Future-Listener 机制,一般实现类是 xxxFutureStub。
  3. 异步非阻塞调用,基于 Reactive 的响应式编程模式,一般实现类是 xxxStub。

2.4.1 基于 Future 的异步 RPC 调用

业务调用代码示例以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

调用 GreeterFutureStub 的 sayHello 方法返回的不是应答,而是 ListenableFuture,它继承自 JDK 的 Future,接口定义以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

将 ListenableFuture 加入到 gRPC 的 Future 列表中,建立一个新的 FutureCallback 对象,当 ListenableFuture 获取到响应以后,gRPC 的 DirectExecutor 线程池会调用新建立的 FutureCallback,执行 onSuccess 或者 onFailure,实现异步回调通知。

接着咱们分析下 ListenableFuture 的实现原理,ListenableFuture 的具体实现类是 GrpcFuture,代码以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

获取到响应以后,调用 complete 方法:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

将 ListenableFuture 加入到 Future 列表中以后,同步获取响应(在 gRPC 线程池中阻塞,非业务调用方线程):

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

获取到响应以后,回调 callback 的 onSuccess,代码以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

除了将 ListenableFuture 加入到 Futures 中由 gRPC 的线程池执行异步回调,也能够自定义线程池执行异步回调,代码示例以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

2.4.2.Reactive 风格异步 RPC 调用

业务调用代码示例以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

构造响应 StreamObserver,经过响应式编程,处理正常和异常回调,接口定义以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

将响应 StreamObserver 做为入参传递到异步服务调用中,该方法返回空,程序继续向下执行,不阻塞当前业务线程,代码以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

下面分析下基于 Reactive 方式异步调用的代码实现,把响应 StreamObserver 对象做为入参传递到异步调用中,代码以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

当收到响应消息时,调用 StreamObserver 的 onNext 方法,代码以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

当 Streaming 关闭时,调用 onCompleted 方法,以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

经过源码分析能够发现,Reactive 风格的异步调用,相比于 Future 模式,没有任何同步阻塞点,不管是业务线程仍是 gRPC 框架的线程都不会同步等待,相比于 Future 异步模式,Reactive 风格的调用异步化更完全一些。

2.4.3 异步双向 streaming 调用

gRPC 的通讯协议基于标准的 HTTP/2 设计,除了普通的 RPC 调用,还支持 streaming 调用。

客户端发送 N 个请求,服务端返回 N 个或者 M 个响应,利用该特性,能够充分利用 HTTP/2.0 的多路复用功能,在某个时刻,HTTP/2.0 链路上能够既有请求也有响应,实现了全双工通讯(对比单行道和双向车道),示例以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 9 双向 streaming 模式

proto 文件定义以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

业务代码示例以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

构造 Streaming 响应对象 StreamObserver并实现 onNext 等接口,因为服务端也是 Streaming 模式,所以响应是多个的,也就是说 onNext 会被调用屡次。

经过在循环中调用 requestObserver 的 onNext 方法,发送请求消息,代码以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

requestObserver 的 onNext 方法实际调用了 ClientCall 的消息发送方法,代码以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

对于双向 Streaming 模式,只支持异步调用方式。

2.4.4 总结

gRPC 服务调用支持同步和异步方式,同时也支持普通的 RPC 和 streaming 模式,能够最大程度的知足业务的需求。

对于 streaming 模式,能够充分利用 HTTP/2.0 协议的多路复用功能,实如今一条 HTTP 链路上并行双向传输数据,有效的解决了 HTTP/1.X 的数据单向传输问题,在大幅减小 HTTP 链接的状况下,充分利用单条链路的性能,能够媲美传统的 RPC 私有长链接协议:更少的链路、更高的性能:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

图 10 传统 RPC 和双向 streaming 模式的对比

gRPC 的网络 I/O 通讯基于 Netty 构建,服务调用底层统一使用异步方式,同步调用是在异步的基础上作了上层封装。所以,gRPC 的异步化是比较完全的,对于提高 I/O 密集型业务的吞吐量和可靠性有很大的帮助。

3. 异步化的一些技术难点

3.1.1 异步异常传递

当采用异步编程以后,异步抛出的异常传递给调用方会变得很是困难,例如 Runnable, 当异步执行它时,异常须要在 run 方法中捕获和处理,不然会致使线程跑飞,run 方法中的异常是没法回传到调用方的。

使用 JDK8 的 CompletableFuture 以后,它的经常使用方法参数基本是 Lambda 表达式,因为函数接口中的方法一般不容许检查期异常,在表达式中发生的异常没法回传给调用方,相比于之前同步调用能够将异常抛给调用方处理的方式有很大差别。

异步异常的解决策略:

1.若是异步的编程模型基于 JDK8 的 CompletableFuture,能够经过 whenComplete 对返回值的异常进行非空判断,当异常非空时,进行异常逻辑处理,相关接口以下:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

也能够经过 exceptionally 方法来处理异步执行发生的异常,相关接口以下所示:

深刻剖析通讯层和 RPC 调用的异步化:二

 

 

2.异步回调(Lambda 表达式)代码块中的异常处理有两种策略:1)必定要经过 exceptionally 方法或者 whenComplete 对异常进行捕获处理,不然会致使 Lambda 表达式异常退出,后续操做被忽略,最终致使业务逻辑跑飞。2)运行期异常,一般是没法抛出来由调用方处理的,须要在发生异常的地方就地捕获和处理。

3.1.2 超时控制

异步代码块(Lambda 表达式)中可能会涉及到多种业务逻辑操做,例如:

1.数据库、缓存、MQ 等中间件平台调用。

2.第三方接口调用。

3.级联嵌套其它微服务调用。

对于异步的超时控制,建议策略以下:

1.对单个原子的中间件、第三方接口、微服务作超时控制。

2.不建议直接对异步代码块(Lambda 表达式)总体作超时控制,例如包装出一个支持异步超时的 CompletableFuture,主要缘由以下:

  1. 超时并不能确保中断当前正在执行的业务逻辑,例如同步 Redis 缓存调用。
  2. 若是超时发生时,正好又发起了一次异步 RPC 调用,建立了一个新的 CompletableFuture,外层超时以后,已经建立的 CompletableFuture 异步回调仍然可能会被执行,这会带来各类混乱。
  3. 因为异步代码块(Lambda 表达式)中的业务逻辑可能会很是复杂,因此超时以后的补偿操做很是困难。例如充值操做已经成功了,可是外层调用方超时失败了,这会给后续业务的处理带来不少困难,由于超时发生时调用方并不知道异步代码块中的哪些操做被执行,哪些没被执行。

没有超时控制以后,要确保 CompletableFuture 可以正常或者异常的结束,不然会致使 CompletableFuture 积压,最终发生 OOM。

3.1.3 上下文传递

在传统的同步 RPC 调用时,业务每每经过线程变量来传递上下文,例如:TraceID、会话 Session、IP 等信息。异步化以后,因为潜在的线程切换和线程被多个消息交叉复用,一般不建议继续使用线程变量传递上下文。

想学习Java工程化、分布式架构、高并发、高性能、深刻浅出、微服务架构、Spring,MyBatis,Netty源码分析等技术能够加群:479499375,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们,欢迎进群一块儿深刻交流学习。

异步化以后,上下文传递的建议策略:

1.若是是 Lambda 表达式,能够直接引用局部变量,经过变量引用的方式将上下文信息传递到 Lambda 表达式中,后续能够经过方法传参等层层传递下去。

2.在全部发生线程切换的地方,显式的进行上下文信息的拷贝和清理,特别须要注意的是隐式线程切换,例如 Hystrix,底层会本身启线程池。

3.建议经过调用级的消息上下文来作参数传递,每一个上下文都关联一次 RPC 调用,调用完成以后自动清理掉。

4.异步化以后,须要排重点查全部使用 ThreadLocal 的地方,一般状况下都会存在问题,须要作改造。

3.1.4 异步回调地狱问题

若是使用的是 JDK8 的 CompletableFuture,它支持对异步操做结果作编排以及级联操做,可以比较好的解决相似 JS 和传统 Future-Listener 的回调地域问题,感兴趣的读者能够体会下 CompletableFuture 的异步化接口。

相关文章
相关标签/搜索