网站内部相关文章:

What is rpc?

使用场景

RPC 最早起源于 60 年代, 此时计算机非常昂贵和庞大, 此时服务器的带宽, 计算能力, 并发性能等原因使得服务一般使用分布式系统, 分布式系统本质上就多进程协作, 进程之间需要进行相互通信, 例如将计算外包给分布式系统中的某一个服务器, 此时远程调用的概念被提出来.

  • RFC674: 提出远程调用, 试图定义一种通用方法, 用于解决分布式系统中多个计算节点的通信问题
  • RFC684: RFC674 的补充说明, 规定了过程调用为原语操作, 远程调用的故障问题, 同步等待消息返回等问题
  • RFC707: 讨论 telnet/ftp 等远程调用服务的资源共享问题, 过程调用的滥用问题, 提出建立一个通用的模型来进行远程调用

从此, 一个标准的 RCP 远程调用流程如下:

rpc-call

虽然 RPC 的提出时间远远早于 SOAP, Restful, 但是其在现在仍然使用较为广泛, 对比 RPC 和 Restful 两者:

  • 在追求性能的场合, 特别是分布式内网环境, 更适合用于基于TCP/IP的 RPC 协议
  • 在追求兼容多语言的场合, 更适合 Restful 服务

至于原因见下面关于 RPC 的介绍, 关于 Restful 的介绍见站内相关文档.

定义

RPC-Remote Procedure Call Protocol, 远程过程调用, 其是一种允许两个实体 A/B 通过通用的请求/响应机制的通用通道进行通信的设计范例. 假如有如下需求:

1
存在两台服务器 A, B, 其中应用部署在 A 上,此时希望调用 B 上的应用提供的函数/方法, 此时需要网络来表达调用的语义以及传达的函数.

则可以根据共享内存来实现 socket 通信, 利用基于 socket 的 RPC 等来实现上述需求, 所有上述的方法实际上都是通过 IPC(进程间通信) 的另外一种方式. RPC 的提出也是跟随需求的不断变化而最终成型:

  • 最初需求 1: A B 两个进程之间需要进行数据交换, 此时需要利用共享内存进行 IPC, 需求完成.
  • 中间需求 2: A B 之间需要进行更加复杂的交互, 此时则指定一个协议来对内容编码解码, 根据协议内容作出决策, 例如 TCP, 需求完成.
  • 最终需求 3: 发现 A B 之间基于协议通信过于复杂, 试图优化协议, 将函数参数和调用的函数名称作为协议的一部分, 其中返回值也类似, RPC 提出并用以解决该需求.

RPC 整体表现的特性就是:

  • object invok(params), 序列化 params 到中间格式
  • 利用远程服务器的invok函数进行处理, 将返回的值解码成 object 对象.

在整个调用过程中, 层层抽象, 将复杂的协议编解码和数据传输封装到了一个函数中.

RPC 发展历史

那么第一个问题: 为何一开始要使用 RPC? 其本身有什么优势呢?

  • 使用 rpc 作为数据交换协议, 可将网络传输层的内容隐藏, 从而将精力专注于逻辑上,基于 RPC 能够在分布式系统中, 有效的将不同的功能应用分离开来, 降低程序的耦合.

既然 RPC 这么好, 那么他为何在HTTP通信接口架构阶段被替代呢?RPC 的缺点是什么?

  • 两军问题: RPC 毕竟是跨主机调用, 类似 Socket 通信, 容易受网络影响, 可能会超时或者调用失败
  • 参数问题: 参数的顺序, 参数的传递, 特别是指针类型参数的传递问题
  • 全局变量: 本地调用随时使用, 类似全局变量产生不好的编程规范
  • 性能问题: 类似两军问题
  • 异常处理: 这是 RPC 频繁讨论的问题, 故障或错误后怎么恢复?重试还是抛出异常? 在非远程调用方式中, 对于异常的处理可以更加方便, 这个在现在的微服务中也频繁碰到, 对于通信的微服务, 如果发生异常情况, 该怎么进行妥善的处理, 这是一个非常关键的问题.
  • 幂等问题

这也是 RPC 逐渐会被 REST 等替代的缘由, 随着万维网技术的发明, 互联网技术的不断普及, RPC 逐渐被淡化, SOAP 技术被提出以替代 RPC, 再后续随着 web1.0, web2.0, web3.0 出现, XML/REST 开始替代前两者.

但是, 随着互联网指数增长, 微服务架构的提出, 分布式系统架构开始随处可见, 而此时基于 RESTFul 的缺点在分布式环境中被放大:

  • 仅仅支持请求和响应
  • 单个请求获取多个资源的难度较大
  • HTTP 动词无法呈现一些复杂的操作
  • 基于 JSON 和 XML 的消息冗余(非二进制), 极大的消耗性能

在微服务架构通信见, HTTP 明显有点杀猪用宰牛刀或者功能不匹配的感觉, 微服务架构大部分都是在内网环境下, 微服务架构之间的通信不需要如 HTTP 协议那么可扩展, 泛用性, 微服务架构更加数据交互效率, 数据流通的简易性, 数据流通的稳定性, 这是特点都是 RESTFul 部分放弃的功能. 当然, 有些 RPC 调用仍然是基于 HTTP 之上的封装.

注意, 如果粗暴的对服务架构进行细分的话, 可以简单的分为如下三个阶段:

  • 单体架构阶段: 单项目, 单服务器, 整体部署
  • HTTP 接口通信阶段: 模块化, 前后端分离, 重要模块之间基于 API 进行通信
  • 微服务阶段: RPC, 消息队列承载的微服务架构

微服务中 RPC

现代 RPC 服务一般指代基于TCP/IP协议上开发的二进制协议服务, 大部分 HTTP 服务一般指代基于HTTP1.X协议上开发的 REST 服务(大部分), 那么现代 RPC 服务有什么优势呢?

  • 序列化方式: 针对二进制协议做序列化和反序列化, 相比 HTTP 基于文本(JSON)的序列化和反序列化, 明显传递数据更加高效
  • 报文长度: RPC 协议提供的功能更少, 更加专注, 其传输的报文头更小, 当然有些 RPC 调用仍然是基于 HTTP, 这里就不再阐述
  • 连接复用: RPC 服务一般都是长连接, 当然 HTTP1.1 以及 HTTP2.0 也支持

通过使用 RPC, 我们至少解决了如下问题:

  1. 通过 IP 直接寻址到目标主机(RPC 远程调用), 不需要走中间负载均衡(LB)和 DNS 解析服务, 大大提高了服务效率, 当然这也不一定是绝对的, 这里硬编码 IP 和端口会被下面的注册中心替代, 从而提高服务扩展性.
  2. 限制调用方式, 减少结果变量, 易于追踪和排查问题
  3. 基于 HTTP1.1 或者 HTTP2.0 的连接复用(当然也可以三层的 TCP/UDP), 提高的连接效率
  4. 更加强调服务容错和服务治理, 提供更加可靠的应用层通信方式, 这明显是 HTTP 天生就部分放弃的特性

第四点中提到 RPC 中的服务容错和服务治理, 他们是什么呢?

容错和治理

  1. RPC 熔断机制, 类似 TCP 的拥塞控制, 每一个服务实例都有一个熔断器, 若目标服务多次请求超时或服务不可用时触发熔断, 被熔断之后实例不再参与软负载选举流程, 后续由定时器进行健康检查并逐渐放大流量知道正常
  2. RPC 超时重试, 该操作请勿在非幂等接口使用, 否则可能造成不可预知后果
    • 间隔重试: 无实时要求环境
    • 立即重试
  3. RPC 接口限流, 对目标服务进行保护
    • 主动限流: QPS 限制
    • 被动限流: 结合熔断机制进行限流
  4. 其他, 比如异常返回, 服务冒充等

现代 RPC 角色

  1. 服务注册和服务发现: 通过注册中心来管理服务注册和服务发现, 从而自动的向服务调用者告知服务提供者的服务信息, 其中有三个重要的角色:
  • 注册中心: 用于服务端注册远程服务, 客户端发现服务, 一般通过 zookeeper 等开源框架实现, 一些大厂会自研相关框架
  • 服务端: 对外提供后台服务, 将自己的服务信息注册到注册中心
  • 客户端: 从注册中心获取远程服务的注册信息, 然后进行远程调用

那么, 服务注册的具体操作是怎样的呢? 服务发现是怎样的一个流程呢? 如何感知服务的健康度呢?

  • a. Register: 记录服务的 IP, PORT, 调用方式(协议, 序列化方式), 例如在 zookeeper 中创建一个 znode 节点
  • b. Discover: 调用方会定时通过注册中心获取提供服务的节点列表, 后续根据负载均衡算法自动选择某一个节点来提供服务
  • c. Detect: 临时节点(相当于服务的一份对外连接服务节点) + 长连接确保调用方能够即使发现服务状态, 主动下线通知 + 心跳检测来感知服务状态
  1. 完整的 RPC 流程
  • a. 注册中心, 客户端, 服务端都已经成功通信并感知到对方, 他们通过服务发现和服务注册来进行
  • b. Client 通过调用模块同本地系统的 stub 进行通信
  • c. stub 中包含 RPC 协议的序列化, 协议编码, 网络传输等动作
  • d. Server stub 接收消息并进行协议解码, 反序列化, 然后调用服务器处理模块

流程和案例

Process

整个 RPC 调用过程:

  • Client 调用特定过程函数, 并等待返回, 基于网络, 发送请求到 server
  • Server 执行收到的请求, 返回执行结果, 并等待下次调用
  • Client 收到响应之后, 继续下次执行.

整个逻辑类似 HTTP 的请求响应或者 socket 通信, 只不过使用不同的框架, 基于不同的协议, 其中类似 socket 的通信机制, Client 可以阻塞/非阻塞式调用, 服务端可以使用专门的线程处理所有请求, 类似 accept.

订单支持系统

下面我们以电商支付系统中的”支付”为例讲解一次完整的 RPC 调用, 其流程大体同 1.1 节的示例图:

  1. 订单系统本地调用角色: PayA, Client Stub
  2. 支付系统服务器角色: PayS, Server stub
  3. 处于两者中间的网络通信模块
  4. PayA 产生订单, 进行一次本地调用: 调用 Client Stub, Client Stub 对 PayA 透明, 对调用参数进行打包和序列化, 通过网络通信发送到服务器
  5. Server Stub 在接收到消息之后进行反序列化, 将参数等消息传递给 PayS 开始进行真正的支付逻辑, 在处理完成之后再将结果回传给 Client stub, 最终再返回给 payA
  6. 在整个过程中 PayA 的感知: 调用本地模块, 开始支付, 等待支付结果, 其他过程对其都是透明的

整个流程示意图如下:

rpc-pay

RPC Protocol

  1. 协议格式
  • Request Msg: 标识, 类型, 客户端标识, 远程过程标识, 参数集合
  • Response Success: 标识, 类型, 回复类型(successful), 回复的内容
  • Response Failed: 标识, 类型, 回复类型(unsuccessful), 失败原因

其中远程过程标识包含: 程序标识, 版本, 过程标识.

  1. 协议角色

  2. Request Protocol: 服务端仅仅接收请求并执行, 类似 send/to

  3. Request/Reply protocol: 服务端接收请求并执行, 返回执行结果, 继续下一个请求

  4. Request/Reply/Acknowledge-Reply protocol: 服务端接收请求并执行, 返回执行结果, 客户端收到结果后发送 ACK 响应, 服务端在收到响应之后才会继续下一次调用.

  5. 长轮询式的调用: 客户端定时向服务端发送探测包; 服务定时返回响应包

引用