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 实例的个数有限制。

第6章 RPC之道的更多相关文章

  1. 像计算机科学家一样思考python-第1章 程序之道

    1.7调试 程序是很容易出错的.因为某种古怪的原因,程序错误被称为bug,而查捕bug的过程称为调试(debugging). 一个程序中可能出现3种类型的错误:语法错误.运行时错误和语义错误.对它们加 ...

  2. RabbitMQ 原文译06--Remote procedure call(RPC)

    在第三篇文章中, 我们学习了怎么使用队列在多了消息消费者当中进行耗时任务轮询. 但是如果我们想要在远程电脑上运行一个方法,然后等待其执行结果,这就是一个不同的场景,这种就是我们一般讲的RPC(远程过程 ...

  3. The C++ Programming Language 学习笔记 第6章 表达式和语句

    1.关于strcpy函数. 书中说c风格的字符串尽量少用,strcpy这样的函数应该也要少用.这里讲这个函数主要是要通过本章课后练习第十题来讲一下前面提及的要点.巩固一下前几章的知识.写了一段,本来感 ...

  4. 建筑的永恒之道 (C·亚历山大 著)

    永恒之道 建筑或城市只有踏上了永恒之道,才会生机勃勃. 第1章 永恒之道 它是一个唯有我们自己才能带秩序的过程,它不可能被求取,但只要我们顺应它,它便会自然而然地出现. 质 为了探求永恒之道,我们首先 ...

  5. Atitit s2018.2 s2 doc list on home ntpc.docx  \Atiitt uke制度体系 法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别 讯飞科大 语音云.docx \Atitit 代码托管与虚拟主机.docx \Atitit 企业文化 每日心灵 鸡汤 值班 发布.docx \Atitit 几大研发体系对比 Stage-Gat

    Atitit s2018.2 s2 doc list on home ntpc.docx \Atiitt uke制度体系  法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别   ...

  6. 《Netty权威指南》笔记

    第1章 Java的I/O演进之路 1.1 Linux网络I/O模型 fd:file descriptor,文件描述符.linux内核将所有外部设备都看作一个文件来操作,对文件的读写会调用内核提供的命令 ...

  7. 试答卓同学的 iOS 面试题

    卓同学昨天写了一篇文章<4道过滤菜鸟的iOS面试题>.我手痒决定默写一个参考答案.后来发现不认真回答被大家喷成狗,所以决定积极改造,重新做人.下面就是修编之后的答案. 1. struct和 ...

  8. Eratosthenes筛选法计算质数

    <C和指针>第6章第4道编程题: 质数就是只能被1和本身整除的数.Eratosthenes筛选法是一种计算质数的有效方法.这个算法的第一步就是写下所有从2至某个上限之间的所有整数.在算法的 ...

  9. 一个简化的printf函数

    <C和指针>第7章第5道编程题: 实现一个简化的printf函数,它能够处理%d.%f.%s 和 %c 格式码,根据ANSI标准的原则,其他格式码的行为是未定义的.你可以假定已经存在函数 ...

随机推荐

  1. rsync配置教程

    本文默认服务器已经安装了 rsync ! 本文默认服务器已经安装了 rsync ! 本文默认服务器已经安装了 rsync ! 切换到 /etc目录,默认情况下,rsyncd.conf 文件如下: # ...

  2. JVM(10)之 年老代收集器

    开发十年,就只剩下这套架构体系了! >>>   在上一篇博文我们介绍了JAVA新生代收集器,本篇博文我们要讲的就是关于老年代的一些收集器.老年代存活的一般是大对象以及生命很顽强的对象 ...

  3. Beta阶段成果展示——第八组

    Beta阶段成果展示 游戏公网IP:http://119.29.32.204/krad.html(欢迎大家测试!) Beta阶段体现在成果上的工作主要为界面美化,玩家引导,按键封闭等等. 本文将以截图 ...

  4. Vue列表渲染-变异方法

    Vue 包含一组观察数组的变异方法,变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组 所以它们也将会触发视图更新.这些方法如下: push() pop() shi ...

  5. css: IE没法调整那些使用px作为单位的字体大小

    <style type="text/css"> h1{font-size:40px;} h3{font-size:30px;} p{font-size:14xp;} & ...

  6. Tab选项卡 自动切换效果js实现

    try.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> < ...

  7. python基础:10.多线程装饰器模式下的单例模式

    with def __enter__ def __close__ 闭包: 装饰器: 闭包的延迟绑定: 单例模式的应用:

  8. 并行流水线--求 (B+C)*B/2

    public class Msg { public double i; public double j; public String orgStr = null; } import java.util ...

  9. mybatis源码分析之02配置文件解析

    该篇正式开始学习mybatis的源码,本篇主要学习mybatis是如何加载配置文件mybatis-config.xml的, 先从测试代码入手. public class V1Test { public ...

  10. Ext js-02 -官方API文档使用

    官方API文档地址: http://docs.sencha.com/extjs/6.5.3/classic/Ext.html 打开网页如下: 1.选择所使用的Ext js版本,后面offline do ...