zoukankan      html  css  js  c++  java
  • RPC, Remote Procedure Call 远程过程调用

    总结

    • RPC 是一种思想:是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。
    • RPC要解决的两个问题:
      • 解决分布式系统中,服务之间的调用问题。
      • 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
    • 要实现一个 RPC 框架,只需要把以下三点实现了就基本完成了:
      • Call ID 映射:可以直接使用函数字符串,也可以使用整数 ID。映射表一般就是一个哈希表。
      • 序列化反序列化:可以自己写,也可以使用 Protobuf 或者 FlatBuffers 之类的。
      • 网络传输:TCP最常用; UDP, HTTP2也可以;也可以自己写 Socket。

    1-什么是本地调用

    说起RPC,就不能不提到分布式,这个促使RPC诞生的领域。

    假设你有一个计算器接口,Calculator,以及它的实现类CalculatorImpl,那么在系统还是单体应用时,你要调用Calculator的add方法来执行一个加运算,直接new一个CalculatorImpl,然后调用add方法就行了,这其实就是非常普通的本地函数调用,因为在同一个地址空间,或者说在同一块内存,所以通过方法栈和参数栈就可以实现。

     

    2-什么是远程过程调用(本地调用的解耦改造)

    现在,基于高性能和高可靠等因素的考虑,你决定将系统改造为分布式应用,将很多可以共享的功能都单独拎出来,比如上面说到的计算器,你单独把它放到一个服务里头,让别的服务去调用它。

     
     

    这下问题来了,服务A里头并没有CalculatorImpl这个类,那它要怎样调用服务B的CalculatorImpl的add方法呢?

    有同学会说,可以模仿B/S架构的调用方式呀,在B服务暴露一个Restful接口,然后A服务通过调用这个Restful接口来间接调用CalculatorImpl的add方法。

    很好,这已经很接近RPC了,不过如果是这样,那每次调用时,是不是都需要写一串发起http请求的代码呢?比如httpClient.sendRequest...之类的,能不能像本地调用一样,去发起远程调用,让使用者感知不到远程调用的过程呢,像这样:

    @Reference
    private Calculator calculator;
    
    ...
    
    calculator.add(1,2);
    
    ...
    

    这时候,有同学就会说,用代理模式(TODO)呀!而且最好是结合Spring IoC一起使用,通过Spring注入calculator对象,注入时,如果扫描到对象加了@Reference注解,那么就给它生成一个代理对象,将这个代理对象放进容器中。而这个代理对象的内部,就是通过httpClient来实现RPC远程过程调用的。

    可能上面这段描述比较抽象,不过这就是很多RPC框架要解决的问题和解决的思路,比如阿里的Dubbo。

    总结一下,RPC要解决的两个问题:

    1. 解决分布式系统中,服务之间的调用问题。
    2. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

    3-完整的RPC框架

    在一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。

    3.1 RPC 核心之功能

    RPC 的核心功能是指实现一个 RPC 最重要的功能模块,就是上图中的”RPC 协议”部分:

    一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等。


    下面介绍核心 RPC 框架的重要组成:

    • 客户端(Client):服务调用方。
    • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
    • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
    • 服务端(Server):服务的真正提供者。
    • Network Service:底层传输,可以是 TCP 或 HTTP。
     

    3.2 RPC 核心之三个技术点

    RPC 的核心功能主要由 5 个模块组成,如果想要自己实现一个 RPC,最简单的方式要实现三个技术点,分别是:

    • 服务寻址
    • 数据流的序列化和反序列化
    • 网络传输

    服务寻址

    1-说明:服务寻址可以使用 Call ID 映射。在本地调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。

    所以在 RPC 中,所有的函数都必须有自己的一个 ID。这个 ID 在所有进程中都是唯一确定的。

    客户端在做远程过程调用时,必须附上这个 ID。然后我们还需要在客户端和服务端分别维护一个函数和Call ID的对应表。

    当客户端需要进行远程调用时,它就查一下这个表,找出相应的 Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

    2-实现方式:服务注册中心。要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例。Dubbo 的服务注册中心是可以配置的,官方推荐使用 Zookeeper。

    3-实现案例:RMI (Remote Method Invocation,远程方法调用),也就是 RPC 本身在Java里面的实现方式,网络协议使用的是TCP。

                                                         RMI 架构图

    Registry(服务发现):借助 JNDI (TODO)发布并调用了 RMI 服务。实际上,JNDI 就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。

    RMI 服务在服务端实现之后需要注册到 RMI Server 上,然后客户端从指定的 RMI 地址上 Lookup 服务,调用该服务对应的方法即可完成远程方法调用。

    Registry 是个很重要的功能,当服务端开发完服务之后,要对外暴露,如果没有服务注册,则客户端是无从调用的,即使服务端的服务就在那里。

    序列化和反序列化

    客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。

    但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。

    这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。

    只有二进制数据才能在网络中传输,序列化和反序列化的定义是:

    • 将对象转换成二进制流的过程叫做序列化
    • 将二进制流转换成对象的过程叫做反序列化

    这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

    网络传输

    网络传输:远程调用往往用在网络上,客户端和服务端是通过网络连接的。

    所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把 Call ID 和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。

    只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。

    尽管大部分 RPC 框架都使用 TCP 协议,但其实 UDP 也可以,而 gRPC 干脆就用了 HTTP2。

    TCP 的连接是最常见的,简要分析基于 TCP 的连接:通常 TCP 连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。

    所以,要实现一个 RPC 框架,只需要把以下三点实现了就基本完成了:

    • Call ID 映射:可以直接使用函数字符串,也可以使用整数 ID。映射表一般就是一个哈希表。
    • 序列化反序列化:可以自己写,也可以使用 Protobuf 或者 FlatBuffers 之类的。
    • 网络传输库:可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。 

    3.3 RPC 核心之网络传输协议

    要实现一个 RPC,需要选择网络传输的方式。

    在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议。

    每一种协议对整体的性能和效率都有不同的影响,如何选择一个正确的网络传输协议呢?首先要搞明白各种传输协议在 RPC 中的工作方式。

    基于 TCP 协议的 RPC 调用

    由服务的调用方与服务的提供方建立 Socket (TODO)连接,并由服务的调用方通过 Socket 将需要调用的接口名称、方法名称和参数序列化后传递给服务的提供方,服务的提供方反序列化后再利用反射调用相关的方法。

    ***将结果返回给服务的调用方,整个基于 TCP 协议的 RPC 调用大致如此。

    但是在实例应用中则会进行一系列的封装,如 RMI 便是在 TCP 协议上传递可序列化的 Java 对象。

    基于 HTTP 协议的 RPC 调用

    该方法更像是访问网页一样,只是它的返回结果更加单一简单。

    其大致流程为:由服务的调用者向服务的提供者发送请求,这种请求的方式可能是 GET、POST、PUT、DELETE 等中的一种,服务的提供者可能会根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。

    而调用的具体方法则是根据 URL 进行方法调用,而方法所需要的参数可能是对服务调用方传输过去的 XML 数据或者 JSON 数据解析后的结果,***返回 JOSN 或者 XML 的数据结果。

    由于目前有很多开源的 Web 服务器,如 Tomcat,所以其实现起来更加容易,就像做 Web 项目一样。

    两种方式对比

    • 基于 TCP 的协议实现的 RPC 调用
      • 优点:由于 TCP 协议处于协议栈的下层,能够更加灵活地对协议字段进行定制,减少网络开销,提高性能,实现更大的吞吐量和并发数。
      • 缺点:但是需要更多关注底层复杂的细节,实现的代价更高。同时对不同平台,如安卓,iOS 等,需要重新开发出不同的工具包来进行请求发送和相应解析,工作量大,难以快速响应和满足用户需求。
    • 基于 HTTP 协议实现的 RPC 调用
      • 优点:可以使用 JSON 和 XML 格式的请求或响应数据。而 JSON 和 XML 作为通用的格式标准(使用 HTTP 协议也需要序列化和反序列化,不过这不是该协议下关心的内容,成熟的 Web 程序已经做好了序列化内容),开源的解析工具已经相当成熟,在其上进行二次开发会非常便捷和简单。
      • 缺点:但是由于 HTTP 协议是上层协议,发送包含同等内容的信息,使用 HTTP 协议传输所占用的字节数会比使用 TCP 协议传输所占用的字节数更高。因此在同等网络下,通过 HTTP 协议传输相同内容,效率会比基于 TCP 协议的数据效率要低,信息传输所占用的时间也会更长,当然压缩数据,能够缩小这一差距。

    高性能高可靠的RPC要考虑什么

    要实现一个RPC不算难,难的是实现一个高性能高可靠的RPC框架。

    比如,既然是分布式了,那么一个服务可能有多个实例,你在调用时,要如何获取这些实例的地址呢?

    这时候就需要一个服务注册中心,比如在Dubbo里头,就可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用。

    那么选哪个调用好呢?这时候就需要负载均衡了,于是你又得考虑如何实现复杂均衡,比如Dubbo就提供了好几种负载均衡策略。

    这还没完,总不能每次调用时都去注册中心查询实例列表吧,这样效率多低呀,于是又有了缓存,有了缓存,就要考虑缓存的更新问题,blablabla......

    你以为就这样结束了,没呢,还有这些:

    • 客户端总不能每次调用完都干等着服务端返回数据吧,于是就要支持异步调用;
    • 服务端的接口修改了,老的接口还有人在用,怎么办?总不能让他们都改了吧?这就需要版本控制了;
    • 服务端总不能每次接到请求都马上启动一个线程去处理吧?于是就需要线程池;
    • 服务端关闭时,还没处理完的请求怎么办?是直接结束呢,还是等全部请求处理完再关闭呢?
    • ......

    如此种种,都是一个优秀的RPC框架需要考虑的问题。

    当然,接下来我们还是先实现一个简单的RPC,再在上面一步步优化!

    传送门: 如何实现一个简单的RPC

    RPC 与 消息中间件

    在 OpenStack 中服务与服务之间使用 RESTful API 调用,而在服务内部则使用 RPC 调用各个功能模块。正是由于使用了 RPC 来解耦服务内部功能模块,使得 OpenStack 的服务拥有扩展性强,耦合性低等优点。OpenStack 的 RPC 架构中,加入了消息队列 (例如:RabbitMQ),这样做的目的是为了保证 RPC 在消息传递过程中的安全性和稳定性。

    RPC和消息中间件的角色分担如下图。RabbitMQ详见:RabbitMQ 

    容易混淆的概念

    RPC vs Restful

    其实这两者并不是一个维度的概念,总得来说RPC涉及的维度更广。

    如果硬要比较,那么可以从RPC风格的url和Restful风格的url上进行比较。

    比如你提供一个查询订单的接口,

    • 用RPC风格,你可能会这样写:/queryOrder?orderId=123
    • 用Restful风格呢?Get /order?orderId=123

    RPC是面向过程,Restful是面向资源,并且使用了Http动词。从这个维度上看,Restful风格的url在表述的精简性、可读性上都要更好。

    RPC vs RMI

    严格来说这两者也不是一个维度的。

    RMI是Java提供的一种访问远程对象的协议,是已经实现好了的,可以直接用了。

    而RPC呢?人家只是一种编程模型,并没有规定你具体要怎样实现,你甚至都可以在你的RPC框架里面使用RMI来实现数据的传输,比如Dubbo:Dubbo - rmi协议

    参考

    1. 柳树之,如何给老婆解释什么是RPC,https://www.jianshu.com/p/2accc2840a1b
    2. 李金葵,花了一个星期,我终于把RPC框架整明白了!https://developer.51cto.com/art/201906/597963.htm
  • 相关阅读:
    android: 建立和断开a2dp link 相关方法总结
    Java: volatile和synchronized的区别 (转)
    shell 将变量当命令执行的几种方式
    tcp 自连接(主机的端口和自己建立连接)
    sed使用
    海尔风冷冰箱冷冻室最底层结冰 问题解决
    k8s 传参数的两种方式一种是 环境变量 拼接 另一种说是yaml传参到配置文件的表示怀疑要验证????????????????????????????
    k8s 集群中的etcd故障解决
    dockerfile
    使用VIM/VI给文件加密和解密
  • 原文地址:https://www.cnblogs.com/frankcui/p/12368916.html
Copyright © 2011-2022 走看看