远程过程调用协议 RPC

RPC(Remote Procedure Call)

传输(Transport)
TCP 协议是 RPC 的 基石,一般来说通信是建立在 TCP 协议之上的,而且 RPC 往往需要可靠的通信,因此不采用 UDP。

这里重申下 TCP 的关键词:面向连接的,全双工,可靠传输(按序、不重、不丢、容错),流量控制(滑动窗口)。

另外,要理解 RPC 中的嵌套 header+body,协议栈每一层都包含了下一层协议的全部数据,只不过包了一个头而已,如下图所示的 TCP segment 包含了应用层的数据,套了一个头而已。

RPC 框架可选择的 I/O 模型严格意义上有 5 种,这里不讨论基于 信号驱动 的 I/O(Signal Driven I/O)。这几种模型在《UNIX 网络编程》中就有提到了,它们分别是:

传统的阻塞 I/O(Blocking I/O)
非阻塞 I/O(Non-blocking I/O)
I/O 多路复用(I/O multiplexing)
异步 I/O(Asynchronous I/O)

同步模型(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路复用IO(multiplexing IO)
信号驱动式IO(signal-driven IO)
异步IO(asynchronous IO)

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

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

1. RPC

http://wulc.me/2019/01/16/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%AC%94%E8%AE%B0(2)-RPC%20and%20threads/

1.1. RPC 基本概念

RPC(Remote Procedure Call) 的概念很好理解,类比函数调用,只是两个函数不在一个内存空间,不能直接调用,需要通过网络进行远程调用。RPC 的调用过程如下,图片摘自 Remote Procedure Calls

需要明确的一点是 RPC 只是一个概念,因此广义上任意实现远程调用的方法都可称为 RPC(如 http),而区别于各个 RPC 的实现(RPC 框架)在于其实现的协议的不同,而最基本的协议包含编码协议和传输协议。

RPC是建立在Socket之上的

1.2. 开篇:RPC 要解决的核心问题和在企业服务中的地位

随着企业 IT 服务的不断发展,单台服务器逐渐无法承受用户日益增长的请求压力时,就需要多台服务器联合起来构成「服务集群」共同对外提供服务。同时业务服务会随着产品需求的增多越来越肿,架构上必须进行服务拆分,一个完整的大型服务会被打散成很多很多独立的小服务,每个小服务会由独立的进程去管理来对外提供服务,这就是「微服务」。

当用户的请求到来时,我们需要将用户的请求分散到多个服务去各自处理,然后又需要将这些子服务的结果汇总起来呈现给用户。那么服务之间该使用何种方式进行交互就是需要解决的核心问题。

RPC 就是为解决服务之间信息交互而发明和存在的。

1.3. 什么是 RPC ?

RPC (Remote Procedure Call)即远程过程调用,是分布式系统常见的一种通信方法,已经有 40 多年历史。当两个物理分离的子系统需要建立逻辑上的关联时,RPC 是牵线搭桥的常见技术手段之一。除 RPC 之外,常见的多系统数据交互方案还有

  1. 分布式消息队列、
  2. HTTP 请求调用、
  3. 数据库和分布式缓存
    ...
    等。

其中 RPC 和 HTTP 调用是没有经过中间件的,它们是端到端系统的直接数据交互。HTTP 调用其实也可以看成是一种特殊的 RPC,只不过传统意义上的 RPC 是指长连接数据交互,而 HTTP 一般是指即用即走的短链接。

RPC 在我们熟知的各种中间件中都有它的身影。Nginx/Redis/MySQL/Dubbo/Hadoop/Spark/Tensorflow 等重量级开源产品都是在 RPC 技术的基础上构建出来的,我们这里说的 RPC 指的是广义的 RPC,也就是分布式系统的通信技术。RPC 在技术中的地位好比我们身边的空气,它无处不在,但是又有很多人根本不知道它的存在。

1.4. RPC 关键

1.4.1. 编码协议(消息协议)

编码协议表明了该如何将要传递的参数等信息打包好;常见的有基于文本编码的 xml、 json,也有二进制编码的 protobuf、binpack,也可自定义协议。

1.4.2. 传输协议

而传输协议则表明如何将打包好的数据传输到远端;如著名的 gRPC 使用的 http2 协议,也有如 dubbo 一类的自定义报文的tcp协议(精简了传输内容)等。

1.5. 多种 RPC 框架

对于一个 RPC 框架,实现中最关注以下三点:
1. Call ID映射:即告诉远程服务器要调用的是哪个函数或应用
2. 序列化和反序列化:即上面的编码协议
3. 网络传输:即上面的传输协议

类似于计算机网络中的各种协议一样,这些协议是比较繁琐的且通用的,因此产生了很多 RPC 框架来完成这些协议层面的东西,而除了上面提到的最基本的编码协议和传输协议,成熟的 rpc 框架还会实现额外的策略, 如服务注册发现、错误重试、服务升级的灰度策略,服务调用的负载均衡等。通过 RPC 框架,在编码时能够像本地调用一样使用 RPC。

比较有名的 RPC 框架:
1. gRPC
2. dubbo

1.6. rpc 历史

远程过程调用发展历程

ONC RPC (开放网络计算的远程过程调用),

OSF RPC(开放软件基金会的远程过程调用) CORBA(Common Object Request Broker Architecture公共对象请求代理体系结构) DCOM(分布式组件对象模型),COM+ Java RMI .NET Remoting XML-RPC,SOAP,Web Service PHPRPC,Hessian,JSON-RPC Microsoft WCF,WebAPI ZeroC Ice,Thrift,GRPC Hprose早期的 RPC第一代 RPC(ONC RPC,OSF RPC)不支持对象的传递。CORBA 太复杂,各种不同实现不兼容,一般程序员也玩不转。DCOM,COM+ 逃不出 Windows 的手掌心。RMI 只能在 Java 里面玩。.NET Remoting 只能在 .NET 平台上玩。XML-RPC,SOAP,WebService 冗余数据太多,处理速度太慢。 RPC 风格的 Web Service 跨语言性不佳,而 Document 风格的 Web Service 又太过难用。 Web Service 没有解决用户的真正问题,只是把一个问题变成了另一个问题。 Web Service 的规范太过复杂,以至于在 .NET 和 Java 平台以外没有真正好用的实现,甚至没有可用的实现。 跨语言跨平台只是 Web Service 的一个口号,虽然很多人迷信这一点,但事实上它并没有真正实现。PHPRPC基于 PHP 内置的序列化格式,在跨语言的类型映射上存在硬伤。通讯上依赖于 HTTP 协议,没有其它底层通讯方式的选择。内置的加密传输既是特点,也是缺点。虽然比基于 XML 的 RPC 速度快,但还不是足够快。Hessian二进制的数据格式完全不具有可读性。官方只提供了两个半语言的实现(Java,ActionScript 和不怎么完美的 Python 实现),其它语言的第三方实现良莠不齐。支持的语言不够多,对 Web 前端的 JavaScript 完全无视。虽然是动态 RPC,但动态性仍然欠佳。虽然比基于 XML 的 RPC 速度快,但还不是足够快。JSON-RPCJSON 具有文本可读性,且比 XML 更简洁。JSON 受 JavaScript 语言子集的限制,可表示的数据类型不够多。JSON 格式无法表示数据内的自引用,互引用和循环引用。某些语言具有多种版本的实现,但在类型影射上没有统一标准,存在兼容性问题。JSON-RPC 虽然有规范,但是却没有统一的实现。在不同语言中的各自实现存在兼容性问题,无法真正互通。Microsoft WCF,WebAPI它们是微软对已有技术的一个 .NET 平台上的统一封装,是对 .NET Remoting、WebService 和基于 JSON 、XML 等数据格式的 REST 风格的服务等技术的一个整合。虽然号称可以在 .NET 平台以外来调用它的这些服务,但实际上跟在 .NET 平台内调用完全是两码事。它没有提供任何在其他平台的语言中可以使用的任何工具。ZeroC Ice,Thrift,GRPC初代 RPC 技术的跨语言面向对象的回归。仍然需要通过中间语言来编写类型和接口定义。仍然需要用代码生成器来将中间语言编写的类型和接口定义翻译成你所使用的编程语言的客户端和服务器端的占位程序(stub)。你必须要基于生成的服务器代码来单独编写服务,而不能将已有代码直接作为服务发布。你必须要用生成的客户端代码来调用服务,而没有其它更灵活的方式。如果你的中间代码做了修改,以上所有步骤你都要至少重复一遍。Hprose无侵入式设计,不需要单独定义类型,不需要单独编写服务,已有代码可以直接发布为服务。具有丰富的数据类型和完美的跨语言类型映射,支持自引用,互引用和循环引用数据。支持众多传输方式,如 HTTP、TCP、Websocket 等。客户端具有更灵活的调用方式,支持同步调用,异步调用,动态参数,可变参数,引用参数传递,多结果返回(Golang)等语言特征,Hprose 2.0 甚至支持推送。具有良好的可扩展性,可以通过过滤器和中间件实现加密、压缩、缓存、代理等各种功能性扩展。兼容的无差别跨语言调用支持更多的常用语言和平台支持浏览器端的跨域调用没有中间语言,无需学习成本性能卓越,使用简单

1.7. Nginx 与 RPC

Ngnix 是互联网企业使用最为广泛的代理服务器。它可以为后端分布式服务提供负载均衡的功能,它可以将后端多个服务地址聚合为单个地址来对外提供服务。如图,Django 是 Python 技术栈最流行的 Web 框架。

Nginx 和后端服务之间的交互在本质上也可以理解为 RPC 数据交互。也许你会争辩说 Nginx 和后端服务之间使用的是 HTTP 协议,走的是短连接,严格上不能算是 RPC 调用。

你说的没错,不过 Nginx 和后端服务之间还可以走其它的协议,比如 uwsgi 协议、fastcgi 协议等,这两个协议都是采用了比 HTTP 协议更加节省流量的二进制协议。如上图所示,uWSGI 是著名的 Python 容器,使用它可以启动 uwsgi 协议的服务器对外提供服务。

uwsgi 通讯协议在 Python 语言体系里使用非常普遍,如果一个企业内部使用 Python 语言栈搭建 Web 服务,那么他们在生产环境部署 Python 应用的时候不是在使用 HTTP 协议就是在使用 uwsgi 协议来和 Nginx 之间建立通讯。

Fastcgi 协议在 PHP 语言体系里非常常见,Nginx 和 PHP-fpm 进程之间一般较常使用 Fastcgi 协议进行通讯。

1.8. Hadoop 与 RPC

在大数据技术领域,RPC 也占据了非常重要的地位。大数据领域广泛应用了非常多的分布式技术,分布式意味着节点的物理隔离,隔离意味着需要通信,通信意味着 RPC 的存在。大数据需要通信的量比业务系统更加庞大,所以在数据通信优化上做的更深。

比如最常见的 Hadoop 文件系统 hdfs,一般包括一个 NameNode 和多个 DataNode,NameNode 和 DataNode 之间就是通过一种称为 Hadoop RPC 的二进制协议进行通讯。

1.9. TensorFlow 与 RPC

在人工智能领域,RPC 也很重要,著名的 TensorFlow 框架如果需要处理上亿的数据,就需要依靠分布式计算力,需要集群化,当多个分布式节点需要集体智慧时,就必须引入 RPC 技术进行通讯。Tensorflow Cluster 的 RPC 通讯框架使用了 Google 内部自研的 gRPC 框架。

1.10. HTTP 调用其实也是一种特殊的 RPC

HTTP1.0 协议时,HTTP 调用还只能是短链接调用,一个请求来回之后连接就会关闭。HTTP1.1 在 HTTP1.0 协议的基础上进行了改进,引入了 KeepAlive 特性可以保持 HTTP 连接长时间不断开,以便在同一个连接之上进行多次连续的请求,进一步拉近了 HTTP 和 RPC 之间的距离。

当 HTTP 协议进化到 2.0 之后,Google 开源了一个建立在 HTTP2.0 协议之上的通信框架直接取名为 gRPC,也就是 Google RPC,这时 HTTP 和 RPC 之间已经没有非常明显的界限了。所以在后文我们不再明确强调 RPC 和 HTTP 请求调用之间的细微区别了,直接统一称之为 RPC。

1.11. HTTP VS RPC (普通话 VS 方言)

HTTP 与 RPC 的关系就好比普通话与方言的关系。要进行跨企业服务调用时,往往都是通过 HTTP API,也就是普通话,虽然效率不高,但是通用,没有太多沟通的学习成本。但是在企业内部还是 RPC 更加高效,同一个企业公用一套方言进行高效率的交流,要比通用的 HTTP 协议来交流更加节省资源。整个中国有非常多的方言,正如有很多的企业内部服务各有自己的一套交互协议一样。虽然国家一直在提倡使用普通话交流,但是这么多年过去了,你回一趟家乡探个亲什么的就会发现身边的人还是流行说方言。

如果再深入一点说,普通话本质上也是一种方言,只不过它是官方的方言,使用最为广泛的方言,相比而言其它方言都是小语种,小语种之中也会有几个使用比较广泛比较特色的方言占比也会比较大。这就好比开源 RPC 协议中 Protobuf 和 Thrift 一样,它们两应该是 RPC 协议中使用最为广泛的两个。

1.12. 换个角度看世界

如果两个子系统没有在网络上进行分离,而是运行在同一个操作系统实例之上的两个进程时,它们之间的通信手段还可以更加丰富。除了以上提到的几种分布式解决方案之外,还有共享内存、信号量、文件系统、内核消息队列、管道等,本质上都是通过操作系统内核机制来进行数据和消息的交互而无须经过网络协议栈。

但在现代企业服务中,这种单机应用已经非常少见了,因为单机应用意味着单点故障 —— “一人摔跤全家跌倒”。业务子系统往往都需要经物理网络栈进行隔离,因此分布式解决方案在要求高可用无间断服务的企业环境里便大有作为,这也让 RPC 迎来自己大放异彩的时代。

前文提到的分布式子系统交互方案,除了 RPC 技术之外还有数据库、消息队列和缓存。但其实这三者本质上是 RPC 技术的一个应用组合。我们可以将数据库服务理解为下面这张图:

可以看出,子系统和数据库之间的交互也是通过 RPC 进行的,只不过这里是三个子系统之间复杂的组合消息交互罢了。如果再深入进去,你会发现,这里的数据库不是那种单机数据库,而是具备主从复制功能的数据库,比如 MySQL。在互联网企业里一般都会使用这种主从读写分离的数据库。一个业务子系统将数据写往主库,主库再将数据同步到从库,然后另一个业务子系统又从从库里将数据取出来。这时又可以进一步将它们看成是四个子系统之间进行的更加复杂的 RPC 数据交互。

2. 基础篇:深入理解 RPC 交互流程

本节我们开始讲解 RPC 的消息交互流程,目的是搞清楚一个简单的 RPC 方法调用背后究竟发生了怎样复杂曲折的故事,以看透 RPC 的本质。

上图是信息系统交互模型宏观示意图,RPC 的消息交互则会深入到底层。

RPC 是两个子系统之间进行的直接消息交互,它使用操作系统提供的套接字来作为消息的载体,以特定的消息格式来定义消息内容和边界。

RPC 的客户端通过文件描述符的读写 API (read & write) 来访问操作系统内核中的网络模块为当前套接字分配的发送 (send buffer) 和接收 (recv buffer) 缓存。

# coding: utf-8
# server

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("localhost", 8080))
sock.listen(1)  # 监听客户端连接
while True:
    conn, addr = sock.accept()  # 接收一个客户端连接
    print(conn.recv(1024))  # 从接收缓冲读消息 recv buffer
    conn.sendall(b"world")  # 将响应发送到发送缓冲 send buffer
    conn.close() # 关闭连接

# coding: utf-8
# client

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("localhost", 8080))  # 连接服务器
sock.sendall(b"hello")  # 将消息输出到发送缓冲 send buffer
print(sock.recv(1024))  # 从接收缓冲 recv buffer 中读响应
sock.close() # 关闭套接字...

2.1. RPC 的消息协议

json 这种直观的消息协议的可读性非常棒,但是它的缺点也很明显,有太多的冗余信息。比如每个字符串都使用双引号来界定边界,key/value 之间必须有冒号分割,对象之间必须使用大括号分割等等。这些还只是冗余的小头,最大的冗余还在于连续的多条 json 消息即使结构完全一样,仅仅只是 value 的值不一样,也需要发送同样的 key 字符串信息。

消息的结构在同一条消息通道上是可以复用的,比如在建立链接的开始 RPC 客户端和服务器之间先交流协商一下消息的结构,后续发送消息时只需要发送一系列消息的 value 值,接收端会自动将 value 值和相应位置的 key 关联起来,形成一个完成的结构消息。在 Hadoop 系统中广泛使用的 avro 消息协议就是通过这种方式实现的,在 RPC 链接建立之处就开始交流消息的结构,后续消息的传递就可以节省很多流量。

消息的隐式结构一般是指那些结构信息由代码来约定的消息协议,在 RPC 交互的消息数据中只是纯粹的二进制数据,由代码来确定相应位置的二进制是属于哪个字段。比如下面的这段代码

https://www.grpc.io/docs/


如果你觉得这篇文章对你有帮助,不妨请我喝杯咖啡,鼓励我创造更多!