【分布式理论六】分布式调用(4):服务间的远程调用(RPC)
文章目录
- 一、RPC 调用过程
- 二、RPC 动态代理:屏蔽远程通讯细节
- 1. 动态代理示例
- 2. 如何将动态代理应用于 RPC
- 三、RPC 序列化
- 四、RPC 协议编码
- 1. 协议编码的作用
- 2. RPC 协议消息组成
- 五、RPC 网络传输
- 1. 网络传输流程
- 2. 关键优化点
一、RPC 调用过程
RPC(Remote Procedure Call,远程过程调用)是一种让不同网络节点上的服务相互调用的技术。它的核心目标是屏蔽远程调用的复杂性,使远程服务的调用方式如同本地调用一样简单。在分布式系统中,RPC 通过封装底层网络通信细节,提高了服务调用的可用性和开发效率。
RPC 调用流程包括:
- 动态代理:客户端通过代理对象调用远程方法。
- 序列化:将请求数据转换为二进制格式,便于传输。
- 协议编码:增加数据包的协议标识和长度信息。
- 网络传输:通过网络传递数据包。
- 协议解码:服务端解析请求包。
- 反序列化:将二进制数据转换回原始对象。
- 执行方法:调用对应的远程方法并处理请求。
- 响应返回:按照相同的序列化、网络传输等流程将响应结果返回给调用方。
二、RPC 动态代理:屏蔽远程通讯细节
动态代理(Dynamic Proxy)是 Java 提供的一种机制,允许在运行时动态创建代理对象,拦截方法调用,并在调用前后执行额外的逻辑。
在 RPC 场景中,动态代理的主要作用是屏蔽底层的远程通信细节,让客户端可以像调用本地方法一样调用远程服务。
1. 动态代理示例
示例代码:
public interface ServerProvider {
void sayHello(String str);
}
public class ServerProviderImpl implements ServerProvider {
@Override
public void sayHello(String str) {
System.out.println("Hello " + str);
}
}
import java.lang.reflect.*;
/**
- `DynamicProxy` 实现了 `InvocationHandler`,用于拦截方法调用并执行代理逻辑。
- `invoke` 方法中:
1. `method.invoke(realObject, args);` 通过反射调用真实对象的方法。
*/
public class DynamicProxy implements InvocationHandler {
private Object realObject;
public DynamicProxy(Object object) {
this.realObject = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(realObject, args);
}
}
public class Client {
public static void main(String[] args) {
ServerProvider realServer = new ServerProviderImpl();
InvocationHandler handler = new DynamicProxy(realServer);
ServerProvider proxyInstance = (ServerProvider) Proxy.newProxyInstance(
handler.getClass().getClassLoader(),
realServer.getClass().getInterfaces(),
handler);
proxyInstance.sayHello("world");
}
}
通过动态代理,客户端不直接依赖于 ServerProviderImpl
,而是通过接口和代理类进行调用,这样:
- 解耦了客户端和服务端,不需要在客户端硬编码调用远程方法。
- 方便在代理类中加入 RPC 逻辑,比如序列化、网络传输等。
- 增强扩展性,可以在
invoke
方法中添加日志、权限校验、负载均衡等功能。
2. 如何将动态代理应用于 RPC
(1) 在代理类中加入远程调用逻辑
(2) 客户端使用代理调用远程服务
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 构造 RPC 请求
RpcRequest request = new RpcRequest();
request.setMethodName(method.getName());
request.setParameters(args);
// 2. 发送请求到远程服务
RpcResponse response = RpcClient.sendRequest(request);
// 3. 解析响应并返回结果
return response.getResult();
}
ServerProvider serverProvider = (ServerProvider) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{ServerProvider.class},
new RpcDynamicProxy("http://remote-server")
);
serverProvider.sayHello("world");
如果想进一步实现 RPC 的完整流程,可以加入序列化、网络传输、反序列化等模块,搭建一个真正的 RPC 组件!
三、RPC 序列化
序列化是将对象转换成字节流的过程,而反序列化则是恢复对象的过程。常见的序列化方式包括:
- JSON:易读易用,但额外空间开销较大。
- Hessian:二进制格式,序列化后字节数小,性能优于 JSON。
- Protobuf:高效、跨语言支持,适用于大规模分布式应用。
- Thrift:Facebook 开源的高效序列化框架,结合了 RPC 服务框架。
四、RPC 协议编码
1. 协议编码的作用
有了序列化功能,就可以将客户端的请求对象转化成字节流在网络上传输了,这个字节流转换为二进制信息以后会写入本地的 Socket 中,然后通过网卡发送到服务端。从编程角度来看,每次请求只会发送一个请求包,但是从网络传输的角度来看,网络传输过程中会将二进制包拆分成很多个数据包,这一点也可以从 TCP 传输数据的原理看出。拆分后的多个二进制包会同时发往服务端,服务端接收到这些数据包以后,将它们合并到一起,再进行反序列化以及后面的操作。
实际上,协议编码要做的事情就是对同一次网络请求的数据包进行拆分,并且为拆分得到的每个数据包定义边界、长度等信息。
2. RPC 协议消息组成
RPC 协议消息由 消息头 和 消息体 组成:
- 消息头 包含协议标识、数据长度、请求类型等信息。
- 消息体 是序列化后的数据。
协议编码的核心目标是确保数据包正确地分片、合并,并提供必要的描述信息,保障网络传输的可靠性。
消息头部分主要存放消息本身的描述信息,如图所示。
名称 | 描述 |
---|---|
魔术位(magic) | 协议魔术,为解码设计 |
消息头长度(header size) | 用来描述消息头长度,为扩展设计 |
协议版本(version) | 协议版本,用于版本兼容 |
消息体序列化类型(st) | 描述消息体的序列化类型,例如 JSON、gRPC |
心跳标记(hb) | 每次传输都会建立一个长连接,隔一段时间向接收方发送一次心跳请求,保证对方始终在线 |
单向消息标记(ow) | 标记是否为单向消息 |
响应消息标记(rp) | 用来标记是请求消息还是响应消息 |
响应消息状态码(status code) | 标记响应消息状态码 |
保留字段(reserved) | 用于填充消息,保证消息的字节是对齐的 |
消息 Id(message id) | 用来唯一确定一个消息的标识 |
消息体长度(body size) | 描述消息体的长度 |
五、RPC 网络传输
1. 网络传输流程
在 RPC 调用中,服务调用方(Client)需要发送请求给服务提供方(Server),然后等待服务器处理并返回响应数据。在这个过程中,数据在应用程序、操作系统内核、网络传输三个层次之间流动,并涉及多个数据复制操作。
从示意图中可以看出,数据的流转主要分为两部分:
- 请求发送过程(客户端 -> 服务器)
- 响应接收过程(服务器 -> 客户端)
对于请求发送流程(Client -> Server),服务调用方(Client)发起 RPC 请求,其数据流动过程如下:
步骤 | 操作 | 数据位置 |
---|---|---|
1 | 应用程序写入数据 | 业务代码执行RPC调用,将数据写入应用缓冲区(User Space) |
2 | 数据复制到内核缓冲区 | 操作系统将应用缓冲区的数据复制到内核缓冲区(Kernel Space) |
3 | 通过网络发送 | 数据从内核缓冲区被传输到网卡(Network Card),并通过网络协议(如TCP)拆分成数据包发送到远程服务器 |
4 | 服务器接收数据 | 服务器端网卡接收数据包,并将其存入内核缓冲区 |
5 | 数据复制到应用缓冲区 | 服务器的内核将数据复制到应用缓冲区 |
6 | 应用程序读取数据 | 服务器端应用程序从应用缓冲区中获取数据,执行请求逻辑(如数据库查询、业务处理) |
响应接收流程(Server -> Client):服务提供方(Server)处理完请求后,将结果返回给客户端,数据流动过程如下:
步骤 | 操作 | 具体内容 |
---|---|---|
7 | 应用程序写入数据 | 服务器应用程序生成响应数据,并写入应用缓冲区 |
8 | 数据复制到内核缓冲区 | 服务器操作系统将数据从应用缓冲区复制到内核缓冲区,准备发送 |
9 | 通过网络发送 | 服务器的网卡将数据包发送到客户端 |
10 | 客户端接收数据 | 客户端网卡接收数据包,操作系统将其存入内核缓冲区 |
11 | 数据复制到应用缓冲区 | 数据从内核缓冲区复制到应用缓冲区,以便应用程序使用 |
12 | 应用程序读取数据 | 客户端应用程序从应用缓冲区中获取响应数据,完成 RPC 调用 |
2. 关键优化点
RPC 网络传输过程涉及多个阶段,包括数据在应用缓冲区、内核缓冲区、网络传输中的流转。优化 RPC 传输的关键在于减少数据复制、优化网络通信、使用异步 I/O 机制,提高整体性能。
操作 | 具体内容 |
---|---|
减少数据复制 | 采用 零拷贝(Zero-Copy) 技术,如 mmap 、sendfile ,避免数据在用户态和内核态之间频繁复制 |
优化网络传输 | 使用 长连接(Keep-Alive) 避免频繁建立 TCP 连接;采用 批量发送、数据压缩 来减少数据传输的开销 |
异步 I/O 处理 | 使用 异步 I/O(如 Netty、epoll),避免同步阻塞,提高并发处理能力 |
优化缓冲区管理 | 采用 池化缓冲区(Buffer Pool) 避免频繁申请和释放内存 |