概述
Socket是网络编程的基础,它提供了一种机制让两个不同程序间能够建立连接并相互通信。然而Socket这种面向字节流的读/写接口的方式与通常设计单机应用程序常用的过程调用方式非常不同。
1984年Birrell和Nelson 提出了RPC(Remote Procedure Call)机制,它允许程序调用其他机器上的过程代码,看起来就像是进行本地过程调用一样。虽然远程调用和本地调用在底层实现上差异巨大,但RPC提供了统一的调用语义。
RPC 的两个主要优势是:
- 程序员可以使用熟悉的过程调用语义来调用远程函数。
- 网络通信的细节被封装在Stub函数中,开发者无需关心底层的套接字、端口、数据格式等问题,从而简化了分布式系统的开发
如何实现RPC
本地函数调用的过程包括:参数入栈、调用指令跳转、执行函数体、返回值出栈等。RPC借鉴了这一模型,但其实现依赖于操作系统提供的网络通信机制(如Socket),并通过Stub封装参数序列化、网络传输、线程等待等过程,模拟出y一种如同本地调用的体验。
-
客户端Stub(Proxy):与远程过程具有相同接口声明,负责将参数序列化(marshaling)、发送请求、接收响应并反序列化结果。
-
服务端Stub(Skeleton):负责注册服务、接收请求、反序列化参数、调用本地实现、序列化结果并返回。
实现中的挑战
-
远程调用语义
不同于本地过程调用精确调用一次(exactly once)的语义,大多数RPC通常提供以下语义:
- At least once:请求可能被重发,远程过程可能执行多次。
- At most once:请求不会重发,可能不执行。
- Exactly once:理想语义,但难以实现
对于用户来说,使用哪种语义取决于远程过程的幂等性(idempotent)。可以多次运行而不会产生不良副作用的函数称为幂等函数;多次运行会产生不良副作用的函数称为非幂等函数。推荐在设计远程过程的时候最好保证其幂等性,当然有时候这是不可能的。
-
Marshaling(序列化)
所有RPC的请求和响应必须序列化为字节流才能在网络传输,并客户端和服务器之间还必须将字节序列的格式标准化,这样服务器才能解析其接收到的数据,反之亦然。常见的格式包括:
- 显式类型:如 JSON、XML,包含字段名和类型。
- 隐式类型:如 Protocol Buffers,依赖双方预定义结构。
-
服务查找
客户进程如何建立与托管远程程序的服务的网络连接是执行RPC的前提。RPC通常依赖命名服务(Name Service)来实现服务发现。另外一个问题是如何唯一标识远程过程,通常采用为每个远程过程设置一个唯一ID,用于唯一标识服务器上一个远程过程。命名服务器接受客户端提供的唯一ID并返回实现这些功能的服务的主机和端口号。服务启动时,服务端stub将向RPC命名服务器注册其接口。
-
错误处理
作为分布式基础组件的RPC必须处理部分故障,RPC必须处理网络中断、超时、协议不匹配、版本不一致、认证失败等问题,确保系统健壮性。
-
性能
RPC的开销远高于本地调用,涉及序列化、网络传输、线程阻塞等,通常来说RPC来说要慢数千倍,因此应合理设置超时、避免频繁调用、优化数据结构。
-
安全性
对于本地过程调用来说,其安全性完全依托于操作系统。但是对于RPC来说,由于消息要穿越不安全的网络环境进行通信,如何保证消息不被劫持和篡改,以及如何验证客户端和服务端的合法性都需要考虑。
RPC实现中的关键能力
-
Marshaling
客户端Proxy对于客户进程来说就是一个与真正远程过程拥有相同接口的本地程序,它会将参数与被调用函数等信息打包成适合网络传输的字节数组,这个过程称为marshaling。
附加元数据包括方法名、版本号、超时时间、调用链 ID(用于分布式追踪)
附加的元数据和参数会一起进行序列化为字节数组,常见的序列化格式包含:
- 二进制协议:Protocol Buffers(高效压缩)、MessagePack(轻量级)、Thrift(跨语言支持)
- 文本协议:JSON(易读性)、XML(兼容传统系统)
序列化库的选择会影响RPC通信的速度和效率。二进制协议体积小、解析快(如 Protobuf 性能比 JSON 高 5-10 倍),文本协议便于调试。
-
传输协议和链接管理
传输协议用于将序列化后的数据发送到远程系统。对于传输协议的选择,不同的RPC框架有不同的取舍
协议类型 典型场景 示例框架 TCP 高性能、可靠传输 Dubbo HTTP/2 多路复用、流支持 grpc UDP 实时性优先(如游戏、流媒体) QUIC RPC需要处理连接创建、维护和断开等工作,这里涉及到连接池、重试策略、超时控制以及其它网络相关的问题。
- 连接池:复用长连接,避免频繁握手(如 gRPC 默认使用 HTTP/2 多路复用)。
- 超时控制:客户端设置全局或单次调用超时(如 500ms)。
- 重试策略:指数退避(Exponential Backoff)、熔断机制(Circuit Breaker)。
- 负载均衡:客户端负载均衡(如 Round Robin、一致性哈希)或服务端 LB(如 Nginx)
-
服务定义和stub生成
在某些RPC框架中,特别是那些使用强类型数据结构的框架(如gRPC),服务接口需要通过IDL显示定义。这些定义通常提供有关可用方法、输入参数和返回类型的信息。然后通过rpc编译器根据这些定义自动生成客户端和服务端的stub函数。IDL和rpc编译器是rpc框架的通常选择。
-
服务发现
RPC命名服务提供注册并查询绑定信息,通过命名服务,允许服务端ip和端口是动态的。
方式 实现 示例 客户端发现 客户端直接查询注册中心 ZooKeeper、Consul 服务端发现 通过负载均衡器代理请求 Nginx、Envoy 健康检查
- 主动心跳检测(如 Consul 的 TTL 检查)。
- 被动响应检测(如 HTTP /health 端点)。