第三章 分布式通信基础:RPC原理与实战

聊到分布式系统,绕不开的一个话题就是RPC。说白了,RPC就是让你可以像调用本地函数一样,去调用远程服务器上的方法。听起来很美好对吧?但背后的坑,我踩过不少。

3.1 RPC的核心三要素

一个完整的RPC调用,本质上要解决三个问题:数据怎么传、传到哪、怎么找到对方。我习惯把这三点拆开来讲。

3.1.1 序列化:把对象变成字节流

你想想看,Java对象在内存里是一堆引用和指针,网络只认二进制流。所以第一步,得把对象“拍扁”成字节数组。这就是序列化。

常见的序列化方案有几种:

  • JDK原生序列化:性能差,体积大,我基本不用
  • JSON/XML:可读性好,但解析慢,适合调试场景
  • Protobuf:Google出品,二进制紧凑,解析快,生产环境首选
  • Hessian:跨语言,但Java版本兼容性有坑

避坑指南:我曾经在一个项目里用了JDK序列化,结果服务升级后,老版本的客户端反序列化新版本的类直接报错。从那以后,我定了个规矩——线上环境绝不用JDK序列化

序列化时要注意什么?我总结三点:

  1. 版本兼容性:字段增删不能导致反序列化失败
  2. 性能开销:序列化时间不能超过总调用时间的10%
  3. 跨语言支持:微服务架构下,不同服务可能用不同语言

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也不是。选哪个,取决于你的场景。但无论选哪个,理解序列化、网络传输、服务寻址这三个核心,你就能在设计分布式系统时游刃有余。