深入RPC,更好使用RPC,须从RPC框架整体性能考虑问题。得知道如何提升RPC框架的性能、稳定性、安全性、吞吐量及如何在分布式下快速定位问题。RPC框架如何压榨单机吞吐量?
1 前言
TPS一直上不去,压测时CPU压到40%~50%就再也压不上去,TPS也不提高,咋办?
看业务逻辑,在执行较为耗时的业务逻辑基础上,又同步调用了好几个其它服务。由于这几个服务的耗时较长,导致服务业务逻辑耗时也长,CPU大部分时间都在等待,没得到充分利用,因此CPU利用率和服务吞吐量上不去。
3 RPC调用吞吐量的影响因素
根本原因:由于处理RPC请求较耗时,且CPU大部分时间都在等待而没有去计算,导致CPU利用率不够。好比一个人干活,但他没规划好时间,且长时间都闲着,当然也就完不成太多工作。
导致RPC请求耗时的原因主要在RPC框架本身吗?除非在网络较慢或使用方使用不当,否则大多情况,刨除业务逻辑处理的耗时时间,RPC本身处理请求的效率就算在较差环境也不过ms级。可以说RPC请求耗时大部分是业务耗时,如业务逻辑中有访问DB执行慢SQL的操作。所以,大多情况,影响RPC调用吞吐量原因就是业务逻辑处理慢,CPU大部分时间都在等待资源。
找到根因,对症下药,
3 提升单机吞吐量的方案
响应式开发就是为提升业务处理的吞吐量。提升吞吐量的关键:“异步”。我们的RPC框架要做到完全异步化,实现全异步RPC。试想一下,如果我们每次发送一个异步请求,发送请求过后请求即刻就结束了,之后业务逻辑全部异步执行,结果异步通知,这样可以增加多么可观的吞吐量?
效果不用我说我想你也清楚了。那RPC框架都有哪些异步策略呢?
4 调用端如何异步?
最常用方式就是返回Future对象的Future或入参为Callback对象。Future是最简单的一种异步方式。
发起一次异步请求,并从请求上下文拿到一个Future,之后就可调用Future#get获取结果。
业务逻辑中调用好几个其它服务,若同步调用,假设调用4个服务,每个服务耗时10ms,则业务逻辑执行完至少耗时40ms。采用Future,连发4次异步请求并拿到4个Future,由于异步调用,耗时几乎忽略不计,之后统一调用这几个Future#get。业务逻辑执行完的时间理想情况10ms,耗时整整缩短到原四分之一,即吞吐量可能提升4倍。
5 RPC框架Future异步实现
一次RPC调用本质:调用端向服务端发一条请求消息,服务端收到消息后进行处理,处理后响应给调用端一条响应消息,调用端收到响应消息后再处理,最后将返回值给到动态代理。
对调用端,向服务端发送请求消息与接收服务端响应消息,是两个完全独立过程,大多数情况下都不在一个线程进行。是不是说RPC框架的调用端,对RPC调用的处理逻辑,内部实现就是异步的?是的。
对RPC框架,无论同步 or 异步调用,调用端内部实现都是异步。
调用端发的每条消息都有个唯一标识,调用端向服务端发请求消息前,会先创建一个Future,并存储消息标识与这Future的映射,动态代理所获得返回值最终就是从这Future中获取。当收到服务端响应消息,调用端会根据响应消息的唯一标识,通过映射找到对应Future,将结果注给那个Future,再处理,最后动态代理从Future得到返回值。
- 同步调用,不过是RPC框架在调用端处理逻辑中主动执行Future#get,让动态代理等待返回值
- 异步调用,则是RPC框架没有主动执行Future#get,用户可以从请求上下文得到这Future,自己决定何时执行Future#get
Future示意图
现在你应该很清楚RPC框架是如何实现Future方式的异步了。
6 RPC调用全异步
Future异步是调用端异步的一种方案,那服务端是否需异步,有何实现方案?
RPC服务端接收到请求的二进制消息后,据协议拆包解包,之后将完整消息解码并反序列化,得到入参后,再通过反射执行业务逻辑。生产环境中这些操作都在哪个线程执行?
当然不在一个线程,对二进制消息数据包拆解包的处理是一定在处理网络I/O的线程,若网络通信框架使用Netty,则对二进制包处理在IO线程,而解码与反序列化过程一般也在IO线程处理。
服务端业务逻辑应交给专门业务线程池处理,以防止由于业务逻辑处理过慢而影响网络I/O处理。
我们配置的业务线程池的线程数都有限,业务线程池的线程数一般只会配置到200,因为大多情况下线程数配置到200还不够用,说明业务逻辑该优化。但若碰到特殊业务场景,让配置的业务线程池打满了。
案例
启动一个服务,业务逻辑处理得就是较慢,当访问量逐渐变大,业务线程池很容易打满,吞吐量不理想,这时CPU利用率也很低。咋办?
调大业务线程池的线程数?有更好方案吗?服务端业务处理逻辑异步是个好方案。
调大业务线程池的线程数
勉强可解决这问题,但对RPC框架,往往都有多个服务共用一个线程池情况,即使调大业务线程池,较耗时服务很可能还会影响其它服务。最佳方案:能让业务线程池尽快释放,就需RPC框架支持服务端业务逻辑异步处理。
7 服务端业务逻辑异步方案
较难处理,因为服务端执行完业务逻辑后,要对返回值序列化并编码,将消息响应给调用端,但若是异步处理,业务逻辑触发异步后方法就执行完了,来不及将真正结果进行序列化并编码后响应给调用端。
就要RPC框架提供一种回调方式,让业务逻辑可异步处理,处理完后调用RPC框架回调接口,将最终结果通过回调响应给调用端。
可让RPC框架支持CompletableFuture,实现RPC调用在调用端与服务端之间完全异步,发布一个RPC服务,服务接口定义返回值CompletableFuture对象。
调用过程
- 服务caller发起RPC调用,直接拿到返回值CompletableFuture对象,之后无需任何额外与RPC框架相关操作(如Future方式时需通过请求上下文获取Future的操作),直接就可异步处理
- 在服务端业务逻辑,创建一个返回值CompletableFuture对象,之后服务端真正业务逻辑可在一个线程池中异步处理,业务逻辑完成之后,再调用这CompletableFuture对象的complete方法,完成异步通知
- 调用端在收到服务端发过来的响应后,RPC框架再自动调用调用端拿到的那个返回值CompletableFuture对象的complete方法
- 一次异步调用完成
通过CompletableFuture,RPC框架可真正做到在调用端与服务端间完全异步,同时提升调用端与服务端的两端的单机吞吐量,并且CompletableFuture是Java8原生支持,业务逻辑中没有任何代码入侵性。
8 总结
影响RPC调用的吞吐量主要原因:服务端的业务逻辑比较耗时,并且CPU大部分时间都在等待而没有去计算,导致CPU利用率不够,而提升单机吞吐量的最好办法就是使用异步RPC。
RPC框架的异步策略主要是调用端异步与服务端异步。调用端的异步就是通过Future方式实现异步,调用端发起一次异步请求并且从请求上下文中拿到一个Future,之后通过Future的get方法获取结果,如果业务逻辑中同时调用多个其它的服务,则可以通过Future的方式减少业务逻辑的耗时,提升吞吐量。服务端异步则需要一种回调方式,让业务逻辑可以异步处理,之后调用RPC框架提供的回调接口,将最终结果异步通知给调用端。
另外,我们可以通过对CompletableFuture的支持,实现RPC调用在调用端与服务端之间的完全异步,同时提升两端的单机吞吐量。
其实,RPC框架也可以有其它的异步策略,比如集成RxJava,再比如gRPC的StreamObserver入参对象,但CompletableFuture是Java8原生提供的,无代码入侵性,并且在使用上更加方便。如果是Java开发,让RPC框架支持CompletableFuture可以说是最佳的异步解决方案。
9 FAQ
提升RPC调用吞吐量,还有啥解决方案?
RPC调用方式
sync
默认方式,但这只是『方法』内部同步,在RPC框架内部还是异步处理。
future 方式
消费者得到 future,自行决定何时获取返回结果。
callback 方式
调用端无需同步处理响应结果,可直接返回。最后返回结果在回调线程里异步处理。
oneway 方式
调用端发送请求之后,无需接受响应
Dubbo 2.7后使用 CompletableFuture 提升异步处理能力,支持以上四种方式。
CPU大部分时间都在等待,并未得到充分利用,因此CPU利用率和服务吞吐量当然上不去了。对于这段话,其实线程处于等待状态时,不占用CPU资源。更准确的描述:浪费宝贵线程资源,大量线程处等待状态,可能(不是一定)导致CPU利用率低。
使用异步的时候返回的速度变快了,但是后台所需要的线程数会变少吗?,线程池我理解还是被打满?
异步对于服务提供方来说,RPC线程所要处理的事情就变少了。
压榨单机吞吐量的秘诀是异步化,针对RPC框架异步化分成:
- 调用端,本就是异步化,毕竟通过网络发生完请求消息,后面就是黑盒,此时可选择不让RPC调用端框架拿服务端的响应消息,让调用端应用自行选择何时拿。这样调用端就能发送更多的请求消息,提高吞吐量
- 服务端异步化,核心在于重分利用单机服务端的资源,避免CPU闲置,业务处理线程处于等待状态
若“业务线程池的线程数配置到200”,线程池被打满了,若单纯增加线程数量有用吗?200个线程都处理不了的话,配置到300或500不是只会增加CPU上下文切换时间吗?
可能用处不大,需提高接口性能或者扩容解决。