第6章 RPC之道

  6.1 认识RPC

    分布式、微服务的架构思维中都不能缺少 RPC 的影子

     RPC(Remote Procedure Call)远程过程调用。通过网络在跨进程的两台服务器之间传输信息,我们使用的时候不用关心网络底层的实现,通过RPC调用远程服务就像本地调用系统内部方法一样方便。

     在 OSI 网络通信模型中,RPC跨越了传输层和应用层,使开发分布式应用程序变得非常方便。

     RPC基本的调用过程如下图。客户端发起一个 RPC请求,本地调用 client stub 负责将调用的接口、方法和参数按照事先约定好的协议进行序列化,然后由 RPC 框架的 RPCRuntime 实例通过 socket 传输到远程服务器上。

     远程服务器端 RPCRuntime 实例收到请求后再通过 server stub 进行反序列化,发起最终的 server method 调用。

      

 

      一个良好的 RPC框架要兼具可靠性和易用性,可靠性方面要保证 I/O、序列化等准确处理,还要考虑网络的不确定性,心跳、网络闪断等因素;易用性方面要考虑超时与重试机制的控制,同步和异步调用的使用等。以上都是考量一个 RPC框架好坏的标准。

      目前有很多优秀的开源 RPC框架,比如国内的 Dubbo(阿里)、Motan(新浪微博)等。

  6.2 RPC是如何实现通信的

    在两台服务器之间进行通信,首先必备的条件是要有一个网络通信基础,即建立网络连接,其次在内存中的数据如果要经过网络传输,则必须先序列化为字节流,最后调用RPC的时候一定不会期望接口增多系统也跟着变复杂,希望有一个代理做这件事情。

    下面从动态代理、反射、序列化、网络编程等方面去理解 RPC 的实现原理。

     6.2.1  动态代理

  代理指我们要做一件事不用亲自去做,找一个代理,只跟这个代理打交道,让这个代理去处理各种事情。动态指我们要代理做10件事情,将来也可能做20件事情,这个不是固定的,是可以动态增加的。程序设计上也需要这种机制,一个系统现在需要调用订单、商品、用户的接口,随着业务的不断发展,该系统还需要调用促销、物流等业务方的接口。我们不希望让所有的外部接口都嵌入这个系统,希望找一个代理去做调用接口的事情,代理逻辑的代码和业务是无关联的,不会因为接口的增多导致这个系统逐渐庞大。

  以 JDK自带的动态代理机制为例来说明如何做到动态代理。JDK有一个重要的类 java.reflect.Proxy,用这个类就可以生成代理类,它的主要方法是 newProxyInstance 代码如下:

  

 

 

   有三个参数,第一个参数是类加载器对象(代理对象的类加载器,加载代理类到 JVM 方法区),第二个参数是接口(代理类需要实现的接口,可以指定多个接口),第三个参数是调用具体的处理器类实例(具体要处理的逻辑都在这个类实例里面)。

  查看 JDK源码,newProxyInstance主要做的事情如下

  

 

 

     这样根据传入的不同接口,我们就可以获取不同的业务处理逻辑对象,而且是在运行的过程中实现的,达到动态代理的目的。

    上面仅以 JDK 原生的支持机制为例来阐述动态代理的实现方式,原生的动态代理有个缺点就是只能针对接口进行代理。另外还有一些比较好的开源的实现,比如字节码方式的 CGLIB,CGLIB可以针对类进行代理。

 

     6.2.2 反射

      反射是指计算机程序在运行时(Runtime)可以访问、检测和修改它本身状态或行为的一种能力。

      上面介绍动态代理过程中有一个类没有讲到,那就是 InvocationHandler 类,它是一个接口,所有代理类都必须实现这个接口,代理类需要完成的具体逻辑实现都在 InvocationHandler 接口类中。

       

 

 

     传入远程服务名和方法名,通过反射自动定位到需要被调用的方法,再传入入参,从而进行 RPC调用

     6.2.3 序列化

      序列化的目的是将内存中数据体转换为字节流,因此在网络传输前需要先进行序列化。涉及网络通信,就要考虑序列化之后的内容大小,序列化和反序列化的耗时,以及对 CPU 的影响。所有提到的这些因素都会影响一次 RPC调用的时间。

      常见的序列化框架有 Hessian、Protobuf、Thrift(更是一个 RPC框架)等。

     6.2.4 网络编程

      我们使用动态代理解决了代理逻辑代码和业务隔离的问题,通过反射实现了自动定位到具体的远程方法,序列化为网络传输做好了准备。还需要一个通道把内容通过网络发送出去。

      RPC框架一般以 TCP 协议为基础,因为它需要可靠的通信来保障每一次调用的顺利进行。我们熟悉的网络代码类似下面的示例。

      

    现在大多数 RPC 框架底层的网络通信都使用 Netty 将基础的网络代码进行了封装,我们在使用 RPC 的时候并不会注意到这些内容。

  6.3 一次RPC调用的时间

  用户组调用了订单组提供的一个接口 methodA(),这是再正常不过的两个系统交互的现象,但出现一个问题:用户组这边的方法性能监控显示这个接口的 TP99性能在 500ms,订单组监控的该接口的 TP99 性能在 100ms,这个差距有点大。

  那么一次 RPC 调用时间都去哪里了?

    一次正常的调用统计的耗时如下图:

 

 

  一次正常的调用统计的耗时主要包括:①调用端 RPC 框架执行时间 + ②网络发送时间 + 3服务端 RPC 框架执行时间 + ④服务端业务代码时间

  ①调用端调用的时候 RPC 框架会先拦截业务请求,同时将对象序列化,在收到响应的时候会反序列化。这时框架耗时主要与 CPU、JVM 运行情况相关,序列化耗时主要和传输对象复杂程度相关。

  ②网络时间就是数据包在网络传输过程中的时间,包括请求+响应,耗时主要与数据包大小、网络情况相关。

  3.服务端主要是队列等待时间,包括请求拦截 + 反序列、响应的序列化;如果 RPC 框架用了队列,则可能有一定的等待时间,成熟的 RPC 框架都会支持队列的方式,不然就是默认将线程当队列使用了。

  ④服务端业务代码处理业务逻辑时间,通常是监控系统收集的服务端耗时(上面提到订单组的系统监控主要就是指这段时间)

 

  另外,如果是一些异常请求(例如服务端线程池已满,客户端超时重试等),其实根本没有执行服务端的业务代码,服务端未记录耗时,但调用端已经记录了这些耗时。针对这种现象还要看下有无异常情乱。

  定位方法:

    (1)首先想到的就是要分析网络情况,查看网络延迟是否严重,是否有 TCP 重传,TCP重传次数不能太大。

    (2)分析服务端和调用端的运行情况,查看是否压力较大,比如 CPU使用率、CPU负载、内存占用大小等。

    (3)查看传输对象是否很大、很复杂,这个对序列化有很大的影响。

    (4)如果服务端有队列,则试着减少队列,或者改为固定线程池,线程特别多,可以试试减少线程大小。

    (5)控制 CPU 使用率不要太高,尽量不超过 80%,该扩容的时候就要扩容。另外大部分业务都是 I/O密集型,并非计算密集型,这里有公式,核数*7,然后取百分比,比如 4C 的容器,那么 2.8% 的使用率属于正常水平,当达到 80% 的时候就要注意了,如果不是业务正常的量上来了,那么可能有线程阻塞。

    (6)有可能的化,可以以单次耗时为准,查看单次耗时的差距,从而确定是否是某些参数的请求导致的耗时比较长。

    (7)查看服务端是否有 full gc,因为会发生 STW。如果频繁 young gc 也不是好事,无论 young 还是 full 都会 STW,只是耗时长短问题。

      下图所示为采样一天时间内的 fg 次数,这是一个比较正常的情况,如果平均耗时较大,那么肯定会影响当时的调用响应时间。  

     

 

 

     (8)排除了以上种种因素,还没定位到原因,就需要尝试如下方法:在服务端通过 tcpdump 抓包,用 wireshark 分析 RPC 请求在服务端的耗时,定位是服务端还是调用端的耗时长,然后进一步确定原因。

 

  6.4 异步RPC

    成熟的RPC 框架都会支持异步调用、异步监听、callback调用,下面为3中异步方式的用法及注意事项

    6.4.1 异步调用

      有一个功能需要调用3个接口来满足业务需求,这3个接口的耗时如下:

        * A接口(耗时400ms)

        * B接口(耗时 200ms)

        * C接口(耗时 700ms)

        如果使用普通的同步调用,完成这个功能需要的总耗时为:400ms+200ms+700ms=1300ms,如果采用异步调用,那么总耗时将是耗时最长的那个接口的耗时,即 700ms。

        示例代码如下:

        

    RPC的异步调用是指客户端发起请求之后,不必等待获取结果,而是返回 null 值,同时可以获取一个 Future 对象,然后从 Future 中去获取结果,如上面代码所示,这样客户端在调用的时候不需要启动多个线程就可以并行调用多个远程服务接口。  

    6.4.2 异步监听

    有时候我们发起一个调用请求后,并不想通过 Future 的 get 获取结果(因为“get”的时候是阻塞的),而是希望调用请求之后可以去干其他的事情,通过一个监听去侦测,当有结果返回的时候直接去获取结果,然后进行逻辑处理。

      比如下面的代码示例,有一个监听类 TestResponseListener,里面的 handleResult 方法负责处理结果数据

  

 

    

 

      发送请求后不需要再调用 getFuture 方法。当有结果返回的时候,会自动调用事先注入好的上面代码示例中的 TetsResponseListener 方法,发起异步回调的示例代码如下:

 

    

 

 

     在使用异步监听的时候,建议最好限制发送的频率,发送太快会导致内存溢出等问题。

 

 

     

 

    6.4.3 callback调用

     成熟的RPC框架基本都支持前面两中方式的调用,callback方式,RPC是以TCP全双工的协议进行通信的,基于长连接,服务端便具备了可以“调用”客户端 callback 函数的能力。

      有的 RPC框架也会支持 callback 调用方式,使用方法示例如下:

      

 

     如果在服务端接口里面完成一个业务逻辑有 3 个过程(A-->B-->C),那么在这 3 个过程中可以分别调用 callback 方法,形成"一次调用,多次通知"的机制,这一点在异步监听中是没有办法实现的,异步回调更像是“一次性买卖”。

    在高并发场景下建议使用第二种方式实现异步监听,因为 callback 方式客户端会对 Callback 实例的个数有限制。

 

posted @ 2019-12-10 13:06  veggiegfei  阅读(520)  评论(0编辑  收藏  举报