第三章 分布式通信基础:RPC原理与实战
聊到分布式系统,绕不开的一个话题就是RPC。说白了,RPC就是让你可以像调用本地函数一样,去调用远程服务器上的方法。听起来很美好对吧?但背后的坑,我踩过不少。
3.1 RPC的核心三要素
一个完整的RPC调用,本质上要解决三个问题:数据怎么传、传到哪、怎么找到对方。我习惯把这三点拆开来讲。
3.1.1 序列化:把对象变成字节流
你想想看,Java对象在内存里是一堆引用和指针,网络只认二进制流。所以第一步,得把对象“拍扁”成字节数组。这就是序列化。
常见的序列化方案有几种:
- JDK原生序列化:性能差,体积大,我基本不用
- JSON/XML:可读性好,但解析慢,适合调试场景
- Protobuf:Google出品,二进制紧凑,解析快,生产环境首选
- Hessian:跨语言,但Java版本兼容性有坑
避坑指南:我曾经在一个项目里用了JDK序列化,结果服务升级后,老版本的客户端反序列化新版本的类直接报错。从那以后,我定了个规矩——线上环境绝不用JDK序列化。
序列化时要注意什么?我总结三点:
- 版本兼容性:字段增删不能导致反序列化失败
- 性能开销:序列化时间不能超过总调用时间的10%
- 跨语言支持:微服务架构下,不同服务可能用不同语言
3.1.2 网络传输:数据怎么跑过去
序列化完的数据,得通过网络发出去。这里有两个选择:
| 传输方式 | 特点 | 适用场景 |
|---|---|---|
| TCP | 可靠、有序、但需要自己处理粘包 | 对可靠性要求高的场景 |
| HTTP | 基于TCP,但多了协议头开销 | 需要穿透防火墙、调试方便 |
| UDP | 不可靠、无序、但速度快 | 实时性要求高、可容忍丢包 |
我个人习惯用TCP做RPC底层传输。为什么?因为RPC本身就需要保证请求-响应的可靠性。但要注意,TCP的粘包问题得自己处理。我一般会在消息头里加一个4字节的长度字段,这样接收方就能准确切分消息。
小技巧:网络传输的瓶颈往往不在带宽,而在连接数。我建议用连接池复用TCP连接,避免每次调用都三次握手。
3.1.3 服务寻址:怎么找到对方
客户端知道要调哪个接口,但不知道IP和端口。这就需要服务发现了。常见的做法有两种:
- 直连模式:配置文件里写死IP,适合测试环境
- 注册中心模式:服务启动时注册到ZooKeeper/Nacos/Consul,客户端从注册中心拉取
嗯,这里要注意。注册中心不是万能的。我曾经遇到过一个线上事故:注册中心挂了,所有服务调用都超时。后来我加了一层本地缓存——即使注册中心不可用,客户端也能用缓存的路由信息继续工作。
3.2 HTTP vs RPC:到底选哪个
很多新手会问:RPC和HTTP不都是远程调用吗?有什么区别?
我直接说结论:RPC是面向服务的,HTTP是面向资源的。
具体差异看这张表:
| 对比维度 | HTTP(RESTful) | RPC |
|---|---|---|
| 协议 | 应用层协议,基于HTTP | 传输层协议,通常基于TCP |
| 数据格式 | JSON/XML,可读性好 | 二进制(Protobuf等),紧凑高效 |
| 性能 | 有HTTP头开销,相对慢 | 轻量级,性能高 |
| 接口定义 | URL + HTTP方法(GET/POST等) | 接口IDL文件(如.proto) |
| 适用场景 | 对外API、开放平台 | 内部微服务间调用 |
你想想看,如果对外暴露API,用RPC的话,客户端还得引入你的IDL文件,多麻烦。但内部服务间调用,追求的是性能和低延迟,RPC明显更合适。
注意:别把RPC和HTTP对立起来。gRPC就是基于HTTP/2的RPC框架,它既用了HTTP的标准化特性,又保留了RPC的高性能。
3.3 gRPC入门:Google的RPC框架
说到RPC框架,gRPC是绕不开的。我最早接触gRPC是在2016年,当时被它的跨语言能力和性能惊艳到了。
3.3.1 为什么选gRPC
gRPC有几个核心优势:
- 基于HTTP/2:支持多路复用、头部压缩、双向流
- 默认使用Protobuf:序列化快、体积小、强类型
- 跨语言:官方支持10+种语言
- 流式通信:支持服务端流、客户端流、双向流
3.3.2 一个简单的gRPC示例
先定义服务接口,写一个.proto文件:
syntax = "proto3";
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
然后生成代码。以Java为例,服务端实现:
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {
String message = "Hello, " + req.getName();
HelloResponse response = HelloResponse.newBuilder()
.setMessage(message)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
客户端调用:
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);
HelloResponse response = stub.sayHello(HelloRequest.newBuilder().setName("World").build());
System.out.println(response.getMessage());
个人经验:gRPC的流式通信特别适合实时推送场景。我之前做一个监控系统,用gRPC的双向流实现了服务端实时推送告警,客户端也能随时调整订阅条件,体验非常好。
3.3.3 gRPC的坑与避让
gRPC虽好,但也不是银弹。我遇到过几个问题:
- 负载均衡:gRPC默认基于HTTP/2的长连接,传统四层负载均衡(如LVS)无法感知请求级别。需要客户端自己做负载均衡,或者用Envoy这类七层代理
- 调试困难:Protobuf是二进制格式,不能像JSON那样直接curl调试。我一般会开一个gRPC反射服务,配合grpcurl工具
- 版本兼容:.proto文件变更后,所有依赖方都得重新生成代码。建议用proto的向后兼容特性,只加字段不改字段
嗯,最后说一句。RPC不是银弹,HTTP也不是。选哪个,取决于你的场景。但无论选哪个,理解序列化、网络传输、服务寻址这三个核心,你就能在设计分布式系统时游刃有余。