RPC (Remote Procedure Call) 架构概览

1 RPC 简介

RPC (Remote Procedure Call) 即远程过程调用,它是一种通过网络从远程计算机上请求服务的机制。RPC 通过网络通信将调用请求发送至远程计算机后,利用远程计算机的系统资源执行这部分程序,最终返回执行结果。

  • 远程过程

    远程过程不同于本地过程,本地过程可以理解为本地函数调用,发起方与被调用方都在同一个地址空间或内存空间内,因此可以直接通过指针访问。而远程过程是把进程内的部分程序逻辑放到其他机器上,也就是所谓的 “业务拆解”,如此一来每个服务便具备了独立性、扩展性,变得更易维护。

    这种在远程机器上提供的服务被称为远程过程。

  • 过程调用

    过程调用即我们平常见到的方法调用。

“过程调用” 与 “远程过程” 相配合,便实现了调用过程的跨机器、跨网络控制传输。

2 RPC 需要解决的问题

RPC 的出现的确为分布式系统构建带来了便利,但与此同时分布式系统本身的问题也暴露了出来。

  • 延迟问题

    第一个问题就是延迟问题,最直观的表现就是响应时间变长:用户的一次点击事件可能需要经过多个服务处理,每个服务都被部署在不同的机器上,这种跨机器、跨网络的进程间通信更容易出现网络延迟。此外,数据编/解码带来的性能损耗也会拖慢响应速度。

    要解决这个问题,就得投入更大的网络带宽以及更强的硬件设备。

  • 地址空间被隔离

    内存地址只在同一台机器上有效,在一台机器内,可以通过内存共享实现地址空间不被隔离,但是在跨机器场景下,地址空间是被完全隔离的。

    比如在使用指针时,本地地址空间的指针在另一台机器上是无法使用的,所以需要 RPC 通过编程范式对开发者隐藏这种区别。而作为开发者,也应该清楚 RPC 框架下的开发不可以直接使用原始指针。

  • 局部故障

    在分布式架构中,不同的服务部署在不同的机器上,而且每个服务也会部署多个节点。当某服务的其中一个或几个节点发生故障时,若没有一个合适的发现机制,流量依旧请求到了故障节点,就会造成响应失败。

    为了解决这个问题,我们可以引入注册中心。注册中心可以发现并屏蔽掉发生故障的节点,但是注册中心也使故障类型变得模糊,定位问题的过程会更加复杂。

    除了发现问题、定位问题难度的上升,注册中心在解决局部故障上的难度也不小:因为出现局部故障后,需要保证整个集群的处理结果是一致的。比如需要通过分布式事务来保证整个集群所有节点写入数据是一致的,不会因为局部故障而出现故障节点写入数据失败但是非故障节点写入成功,导致数据不一致的情况。

  • 并发问题

    在分布式架构中,每个服务都有多个节点,如果多个节点同时对某个服务发起调用,就会产生并发问题。与本地多线程调用不同,分布式架构无法做到完全控制调用顺序,因为每个节点在不同的机器上,它们发起调用的时间没有被统一管控,也无法管控。

3 RPC 架构核心组件

RPC 技术发展至今,底层核心组成部分始终没有很大变化。无论是几十年前的 CORBA,还是如今流行的 Doubbo、gRPC 等,它们基本都由以下五个部分组成:

  • Consumer (服务调用方);
  • Consumer-Stub (调用方本地存根);
  • RPC Runtime (RPC 通信者);
  • Provider-Stub (提供方本地存根);
  • Provider (服务提供方)。

3.1 服务调用方

服务调用方也叫服务消费者 (Consumer),它的职责之一是指定需要调用的接口的全限定名和方法,将调用的方法需要的参数提供给 Consumer-Stub。职责之二是从 Consumer-Stub 中获取执行的结果。

3.2 服务提供方

服务提供方 (Provider) 用于执行接口实现的方法逻辑,也就是为 Provider-Stub 提供方法的具体实现。

3.3 本地存根

RPC 会带来空间地址被隔离的问题,在远程调用的过程中,Consumer 端的地址空间中的任何一个内存地址在 Provider 端都是没有意义的。除了内存地址无法匹配,在不同的机器上还可能出现位宽不同、处理器大小端不同、编译环境导致的结构体系内存布局不同、字符串编码不同等情况,这些情况会导致远程调用的过程中 Consumer 端的方法调用无法像本地调用一样正确匹配到真实的方法实现,从而导致调用失败。所以在远程调用过程中,Consumer 端发起的方法调用让 Provider 端精确的知道自己应该执行哪个方法就是必须要解决的问题,Stub 的职责便是抽象这个调用过程。

本地存根 (Stub) 分为服务调用方本地存根 (Consumer-Stub) 和服务提供方本地存根 (Provider-Stub) 两种:

  • Consumer-Stub

    Consumer-Stub 与 Consumer 同属消费端,它们存在于同一台机器上。

    Consumer-Stub 会接受 Consumer 的方法调用,然后解析被调方法的方法名、参数等信息,然后整理、组装这些数据,将整理好的数据以指定的协议进行序列化,打包成可以传输的消息体,交给 RPC Runtime。

    Consumer-Stub 除了处理消费者提供的方法和参数,还会处理服务提供方返回的结果,它会将 RPC Runtime 返回的数据反序列化成服务调用方需要的数据并传递给 Consumer。

    从服务调用者的角度来看,Consumer-Stub 隐藏了远程调用的实现细节,就像是远程服务的一个代理对象,可以让服务调用者感觉自己在调用本地方法一样。

  • Provider-Stub

    Provider-Stub 与 Provider 都属于服务提供端,它们也一起位于同一台机器上。

    当 Provider 端的 RPC Runtime 收到请求包后,交由 Provider-Stub 进行参数等数据的转化。

    Provider-Stub 会重新转换客户端传递的数据,以便在 Provider 端的机器上找到对应的方法,传递正确的参数数据,最终正确地执行实际方法的调用。

    等方法执行完毕后,Provider 会将执行结果返回给 Provider-Stub,Provider-Stub 再将数据序列化、打包,经由 RPC Runtime 返回给服务调用方。

3.4 RPC Runtime

RPC 依赖互联网传递数据,远程过程调用的本质就是远程通信,所以 RPC 必不可缺的就是通信模块,RPC Runtime 的职责便是承载服务之间的通信。

RPC Runtime 肩负数据包的传输、重传、确认、路由和加密等职责,在 Consumer 端和 Provider 端都会有一个 RPC Runtime 实例,负责双方的通信。

4 RPC 调用过程

RPC 调用过程可以分为四个阶段:

  1. 服务暴露过程;
  2. 服务发现过程;
  3. 服务引用过程;
  4. 方法调用过程。

4.1 服务暴露过程

服务暴露过程发生在 Provider 端,根据服务是否暴露到远程可以将其分为“服务暴露到本地”和“服务暴露到远程”两种。

  • 本地暴露

    在一台机器上,一个应用服务可以认为是机器上的一个进程。当 Provider 进程启动后,RPC Runtime 会监听一个端口以对外提供服务(例如 Dubbo 默认监听的 20880 端口),此时一个 Consumer 如果想要访问我们新建的 Provider,则需要手动指定 Provider 的 Host 与 Port,否则是没办法知道这个新增的 Provider 的具体访问信息的,这种暴露方式称为“本地暴露”。

  • 远程暴露

    如果 Provider 进程启动后,紧接着将自己的 IP、端口等信息注册到公共的注册中心,如此一来所有的 Consumer 都可以通过这个注册中心发现新添加的 Provider 的连接信息,进而实现自动连接,这种暴露方式便是“远程暴露”。

    Provider 端的应用服务信息包括 Provider 的地址、端口、应用服务需要暴露的接口定义等信息。Provider 除了会在应用服务启动时将服务信息注册到注册中心,还会与注册中心保持心跳连接,如果 Provider 端某个节点异常下线,注册中心在一段时间内如果没有收到心跳,就会将该节点从服务列表中移除,以防 Consumer 将请求发送到异常节点。

    通过注册中心管理服务的地址信息,实现了 Consumer 对服务变动的动态感知,如此一来客户端不再需要显式的配置服务端地址,只需连接到注册中心即可,而注册中心集群地址往往是相对固定的。

4.2 服务发现过程

服务发现过程发生在 Consumer 端,服务发现的过程也就是“寻址过程”,Consumer 端如果要发起 RPC 调用,首先需要知道自己想要调用的服务有哪几个 Provider,服务发现的方式分为“直联式”和“注册中心式”两种。

  • 直联式

    服务消费者可以根据服务暴露的地址和端口直接连接远程服务,但是每次服务提供者的地址或端口更改了,服务消费者都需要手动重新配置。这种方式不建议在生产环境使用,仅可作为测试。

    如果 Provider 只暴露到了本地,那么 Consumer 只能通过该模式连接 Provider。

  • 注册中心式

    服务消费者通过注册中心发现服务,也就是从注册中心动态的获取服务提供者的地址和端口。

    如果 Provider 暴露到了远程,则 Consumer 最好选择注册模式连接 Provider。

4.3 服务引用过程

服务引用过程发生在服务发现之后,当 Consumer 端通过服务发现获取所有服务提供者的地址后,通过负载均衡策略选择其中一个服务提供者的节点进行服务引用。服务引用的过程就是与某一个服务节点建立连接(双方 RPC Runtime 建立网络连接),以及在 Consumer 端创建接口代理的过程。

4.4 方法调用过程

当服务引用完成后,Consumer 端便可进行方法调用了,整个调用过程如下:

RPC runtime 示意图
  1. Consumer 以本地调用方式(往往是接口方式)调用服务,它会将需要调用的方法、参数类型、参数列表传递给服务 Consumer-Stub;
  2. Consumer-Stub 收到调用后,将方法、参数等数据序列化成可以通过网络传输的消息体,并将该消息体传递给 Consumer 端的 RPC Runtime;
  3. Consumer 端的 RPC Runtime 通过 Socket 将消息发送到 Provider 端,由 Provider 端的 RPC Runtime 接收;
  4. Provider 端的 RPC Runtime 收到消息后,将其传递给 Provider-Stub;
  5. Provider-Stub 将收到的消息反序列化,然后解析出服务调用的方法、参数类型和参数列表,并调用 Provider 的服务;
  6. Provider 执行对应的方法后,将执行结果返回给 Provider-Stub;
  7. Provider-Stub 将结果序列化,打包成可传输消息体,传递给 Provider 端的 RPC Runtime;
  8. Provider 端的 RPC Runtime 通过 Socket 将消息发送到 Consumer 端,由 Consumer 端的 RPC Runtime 接收;
  9. Consumer 端的 RPC Runtime 将收到的消息传递给 Consumer-Stub;
  10. Consumer-Stub 根将消息反序列化,然后将得到的结果传递给 Consumer。

5 RPC 框架

RPC 框架是为了实现 RPC 而衍生出来的产物,它是一个可复用的软件架构解决方案。要实现一次 RPC 调用,需要实现 Stub 以及 RPC Runtime 等组件,其中涉及网络编程、序列化和反序列化等内容,RPC 框架就是为开发者提供了一个封装了 RPC 所需的能力的、开箱即用”的工具软件。

随着微服务架构的兴起,服务类型与服务数量也随之增加,服务治理相关需求也越来越大。根据是否提供服务治理能力,可以将 RPC 框架分为两类,一类是仅提供 RPC 调用能力的框架;一类是除了提供基本的 RPC 调用能力,还承载了各种服务治理能力的框架。

  • 第一类框架的代表有:gRPC、Thrift 等;
  • 第二类框架的代表有:Dubbo、Motan、Feign 等。