如果想更深入地了解 RPC ,就必须从 RPC 框架的整体性能上去考虑问题了。如何去提升 RPC 框架的性能、稳定性、安全性、吞吐量,以及如何在分布下场景下快速定位问题等等。
今天我们就先来讲讲,RPC 框架是如何压榨单机吞吐量的。
如何提升单机吞吐量?
记得之前业务团队反馈过这样一个问题:我们的 TPS 始终上不去,压测的时候 CPU 压到 40% - 50% 就再也压不下去了,TPS 也不会提高,问我们这里有没有什么解决方案可以提升业务的吞吐量?
那是什么影响到了 RPC 调用的吞吐量呢?
其实根本原因就是由于处理 RPC 请求比较耗时,并且 CPU 大部分的时间都在等待而没有去计算,从而导致 CPU 的利用率不够。所以说,在大多数情况下,影响到 RPC 调用的吞吐量的原因也就是业务逻辑处理慢了,CPU 大部分时间都在等待资源。
该如何去提升单机吞吐量?
要提升吞吐量,其实关键就两个字:异步。
那 RPC 框架都有哪些异步策略呢?
调用端如何异步?
最常用的方式就是返回 Future 方式,或者入参为 Callback 对象的回调方式。
连接发送4次异步请求并且拿到4个 Future,如下图所示,我们的吞吐量可能提升4倍!
那 RPC 框架的 Future 方式异步又该如何实现呢?
对于 RPC 框架,无论是同步调用还是异步调用,调用端的内部实现都是异步的。调用端发送的每条消息都有一个唯一的消息标识,并且在发送请求消息前会先创建一个 Future,并会存储这个消息标识与这个 Future 的映射,动态代理所获得的返回值最终就是从这个 Future 中获取的;
当收到服务端响应的消息时,调用端会根据响应消息的唯一标识,找到对应的 Future,最后动态代理从 Future 中获取正确的返回值。
所谓的同步调用,不过是 RPC 框架在调用端的处理逻辑中主动执行了这个 Future 的 get 方法,让动态代理等待返回值;而异步调用则是 RPC 框架没有主动执行这个 Future 的 get 方法,用户可以从请求上下文中得到这个 Future,自己决定什么时候执行这个 Future 的 get 方法。
如何做到 RPC 调用全异步?
上面的 Future 异步,是调用端的一种异步方式,那么服务端呢?有什么实现方式?
其实我们可以让 RPC 框架支持 CompletableFuture,实现 RPC 在调用端与服务端之间完全异步。
CompletableFuture 是 Java8 原生支持的。整个调用过程会分为这样几步:
- 服务调用方发起 RPC 调用,直接拿到返回值 CompletableFuture 对象,直接就可以进行异步处理;
- 在服务端的业务逻辑中创建一个返回值 CompletableFuture 对象,之后服务端真正的业务逻辑完全可以在一个线程池中异步处理,业务逻辑完成后再调用这个 CompletableFuture 对象的 complete 方法,完成异步通知;
- 调用端在收到服务端发送过来的响应之后,这样一次异步调用就完成了。
通过对 CompletableFuture 的支持,RPC 框架可以真正地做到在调用端与服务端之间完全异步,同时提升了调用端与服务端两端的单机吞吐量,并且 CompletableFuture 是 Java8 原生支持,业务逻辑中没有任何代码入侵性。
课后思考
对于 RPC 调用提升吞吐量这个问题,你是否还有其它解决方案?你还能想到哪些 RPC 框架的异步策略?
为什么需要考虑安全问题?
要搞清楚这个问题,我们可以先看一个完整的 RPC 应用流程。
我们一般是先由服务提供方定义好一个接口,并把这个接口的 Jar 包发布到私服上去,然后在项目中去实现这个接口,最后通过 RPC 提供的 API 把这个接口和其对应的实现类完成对外暴露,如果是 Spring 应用的话直接定义成一个 Bean 就好了。到这儿,服务提供方就完成了一个接口的对外发布了。
这里其实存在一个安全隐患问题,因为私服上所有的 Jar 坐标我们所有人都可以看到,只要拿到了 Jar 的坐标,就可以把发布到私服的 Jar 引入到项目中完成 RPC 调用了。
调用方之间的安全保证
我们只需要给每个调用方设定一个唯一的身份,每个调用方在调用之前都先来服务提供方这登记下身份,只有登记过的调用方才能继续放行,没有登记过的调用方一律拒绝。
那在 RPC 里面我们该怎么实现呢?
我们把进行调用接口登记的地方姑且称为“授权平台”。在调用方启动初始化接口的时候,带上授权平台上颁发的身份去服务提供方认证下,当认证通过后就认为这个接口可以调用。
(每次调用服务方都去请求授权平台,这样是授权平台对高可性要求就比较高了)
那服务提供方验证对照的数据来自哪儿?总不能又去请求授权平台吧?
加密算法里面有一种叫做不可逆加密算法 HMAC,服务提供方应用里面放一个用于 HMAC 签名的私钥,在授权平台上用这个私钥为申请调用的调用方应用进行签名,这个签名生成的串就变成了调用方唯一的身份。服务提供方在收到调用方的授权请求之后,只要验证下这个签名跟调用方应用信息是否对应得上就行了。这样集中式授权的瓶颈也就不存在了。
服务发现也有安全问题吗?
服务提供方把接口 Jar 发布到私服上,如果有人拿到这个 Jar 发布出来一个服务提供方,这样就导致调用方通过服务发现拿到的服务提供方 IP 地址集合里面会有那个伪造的提供方。
我们可以这样来解决:当服务向注册中心注册的时候,注册中心可以在收到注册请求时,验证下请求过来的应用是否跟接口绑定的应用一样,只有相同才允许注册,否则就返回错误信息给注册的应用,从而避免假冒的服务提供者对外提供错误服务。
课后思考
如果想要对接口里部分方法作授权认证,该怎么做呢?
分布式环境下定位问题有哪些困难?
分布式系统有着较为复杂的依赖关系,我们很难判断是哪个环节出现的问题,而且在大型的分布式系统中,往往会有跨部门、跨团队合作的情况,在排查问题的时候会面临非常高的沟通成本。
如何做到快速定位问题?
方法1:借助合理封闭的异常信息
哪类异常引起的问题(序列化问题或网络超时问题),是调用端还是服务端出现的异常,调用端与服务端的 IP 是什么,以及服务接口与服务分组都是什么等等。
由此可见,一款优秀的 RPC 框架要对异常进行详细地封装,还要对各类异常进行分类,每类异常都要有明确的异常标识码,并整理成一份简明的文档。
方法2:借助分布式链路跟踪
分布式链路跟踪有 Trace 与 Span 的概念。Trace 就是代表整个链路,每次分布式都会产生一个 Trace,每个 Trace都有它的唯一标识 TraceId,在分布式链路跟踪系统中,就是通过 TraceId 来区分每个 Trace 的。
Span 就是代表了整个链路中的一段链路,也就是说 Trace 是由多个 Span 组成的。在一个 Trace 下,每个 Span 也都有它的唯一标识 SpanId,而 Span 是存在父子关系的,Span2 的父 Span 就是 Span1。如下图所示:
RPC 在整合分布式链路跟踪需要做的最核心的两件事就是埋点和传递。
所谓埋点,就是分布式链路跟踪系统要想获得一次分布式调用的完整的链路信息,就必须对这次分布式调用进行数据采集,而采集这些数据的方法就是通过 RPC 框架对分布式链路跟踪进行埋点。
所谓传递,就是批上游调用端将 Trace 信息与父 Span 信息传递给下游的服务端,由下游触发埋点,对这些信息进行处理,在分布式链路跟踪系统中,每个子 Span 都存有父 Span 的相关信息以及 Trace 的相关信息。
思考题
分布式环境下,你还知道哪些快速定位问题的方法?
答:skywalking、Zipkin 等开源分布式链路跟踪。