上个月排查一个线上告警,服务调用方一直报 cannot finish rpc call in 30 seconds,我一开始以为是网络抖动,后来顺着链路查下去,发现是服务端 GC 长停顿把一次正常调用拖垮了。排查的过程让我意识到,很多人(包括我自己)天天在用 RPC、gRPC、Protobuf,但真的遇到问题时,对这套分布式通信核心栈的理解还是不够系统。这篇内容我就把 RPC 的本质、gRPC 为什么选 HTTP/2 + Protobuf、编码原理、框架选型,以及几个真实环境里踩过的坑一次讲清楚。适合刚接触微服务、或者已经被 RPC 报错折磨过的后端同学,也适合想搞清楚“HTTP 和 RPC 到底啥关系”的读者。
1. 从超时报错说起:RPC 到底解决的是什么问题
先回到那个 30 秒超时报错。cannot finish rpc call in 30 seconds 这种消息,说明客户端设置了一个 30 秒的超时窗口,但调用没有在这个窗口内拿到结果。这背后其实引出了一个很本质的问题:本地调用和远程调用之间,隔的不是一个函数签名的距离,而是一整张网络。
1.1 本地调用和远程调用之间隔着一整张网
写单机程序时,orderService.getOrder(id) 就是一次普通的函数调用,压栈、传参、返回,毫秒级完成,没有网络参与。但一旦拆成微服务,这个调用就变成了:
- 客户端要把
getOrder这个意图“告诉”另一个进程; - 参数要序列化成字节流;
- 字节流要穿过 TCP 连接,可能经历重传、拥塞、乱序;
- 服务端要解析出是哪个方法、哪些参数;
- 执行完还要把结果序列化传回来。
这一串流程里,任何一环出问题,调用方看到的就是“报错”或“超时”。RPC(Remote Procedure Call,远程过程调用)的价值,就是把这一堆网络细节藏起来,让程序员感觉“我就是在调一个本地方法”。
1.2 RPC 不是“HTTP 之外的某种东西”,而是一种调用方式
很多人会把 RPC 和 HTTP 对立起来,觉得“HTTP 是 REST,RPC 是二进制传输”,这个理解不太准确。
RPC 是一种设计思想:远程调用要像本地调用一样简单。它不限定底层必须用什么协议——gRPC 是基于 HTTP/2 的 RPC,Dubbo 用自定义 TCP 协议,Java RMI 用 JRMP 协议,它们本质上都是 RPC 思想的实现。
HTTP 是一种应用层协议,它既可以承载 RESTful 接口,也可以承载 RPC 风格的消息。所以更合适的说法是:HTTP 是底层的“传输管道”之一,RPC 是管道的“封装方式”。把这两个概念分开,再看各种框架就清晰多了。
1.3 那 30 秒超时到底是怎么来的
我那次排查的链路是这样的:客户端配了 30 秒超时,服务端日志显示请求确实收到了,但处理耗时超过 25 秒。继续往下看,服务端那个实例的 GC 日志里出现了多次秒级停顿,数据库连接池也打满了,大量线程在等连接。
问题不在 RPC 框架本身,而在于:
- 服务端依赖的数据库连接池太小,高峰期排队;
- GC 停顿让线程阻塞;
- 客户端把超时设成了统一的 30 秒,没有按接口区分。
这里有个经验:RPC 超时配置不能“一个值走天下”。读接口、写接口、批量查询接口的耗时特征完全不同,统一配置要么导致频繁误报,要么导致故障长时间无感知。建议按接口的 p99 延迟来设定超时,比如 p99 是 200ms,超时可以设为 1 秒左右;同样,超时报警也要区分“超时但被重试成功”和“超时且失败”两种情况。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆开一次 RPC 调用:从客户端到服务端的完整链路
理解 RPC 最好的方式,是亲手拆一次调用。不管用 gRPC 还是 Dubbo,一条完整的 RPC 调用链路都包含几个固定环节。
2.1 一次调用的 5 个环节
还是拿 orderService.getOrder(id) 举例,走完一次 RPC 调用,实际发生了这些事:
- 客户端调用本地代理(Stub):调用方调用的不是一个真正的远程对象,而是一个本地生成的代理类。代理类负责伪装成“本地服务”,把方法名、参数值打包。
- 序列化:把方法名(或者接口 ID)和参数值转成二进制。gRPC 用 Protobuf,Dubbo 默认用 Hessian2,Java RMI 用 Java 原生序列化。
- 网络传输:序列化后的字节流被写入传输层。gRPC 走 HTTP/2 流,Dubbo 走自定义 TCP 协议。
- 服务端反序列化:服务端接收字节流,还原出方法名和参数,找到对应的服务实现。
- 执行并返回:业务逻辑执行完,结果再走一遍序列化、传输、反序列化,回到客户端。
其中第 2 步和第 4 步直接决定 RPC 的性能上限,因为序列化是最耗 CPU 的环节之一。这也是 Protobuf 能在 gRPC 里站稳脚跟的原因——后面我会单独讲它的编码原理。
2.2 动态代理和 IDL:RPC 的“翻译官”
有了 Stub,客户端代码才能写 service.getOrder(id) 而不是手动拼字节流。这个 Stub 怎么来?两种方式:
- 动态代理:运行时根据接口定义自动生成代理对象。Java 里就是
Proxy.newProxyInstance那一套,Dubbo 老版本大量使用这种方式。 - 代码生成:根据 IDL(Interface Definition Language,接口定义语言)文件在编译期生成客户端和服务端代码。gRPC 用的就是这种方式。
IDL 是 RPC 体系里容易忽略但非常关键的东西。它解决的是“语
