RPC 与通信模式:同步调用的陷阱
本文基于模型知识整理(生成时未联网核对),关键结论建议对照经典文献复核。
一句话定义
RPC 让跨节点调用看起来像本地函数调用,但这份"透明"是伪装的:序列化成本、网络故障、版本兼容问题都会从抽象缝隙里漏出来。
为什么重要
RPC 是微服务时代的默认通信原语。不清楚它泄漏了哪些复杂性,就会写出"把远程调用当函数调用用"的代码:无超时、无重试预算、接口不兼容直接上线炸了下游。理解 RPC 的边界,才能在它与消息队列(kp-008)之间做出正确的通信选型。
前置知识
核心概念
- RPC(Remote Procedure Call):客户端经 stub/代理把调用编码(序列化)为请求,传输到服务端解码(反序列化)执行,再把结果编回传回。
- 同步 vs 异步:同步 RPC 阻塞等待响应;异步 RPC/future 立即返回句柄。
- 序列化格式:文本型(JSON,人类可读、体积大、类型弱)vs 二进制型(Protobuf/Thrift/Avro,schema 驱动、紧凑、可向前/向后兼容)。
- schema 演进:加字段必须可缺省、不重用旧字段编号、只加不删的兼容纪律。
- 流式 RPC:gRPC 的 streaming(服务端流/客户端流/双向流)把"一次连接多次推送"变成一等公民。
原理与机制
"透明"是错觉。Birrell & Nelson 当年提出 RPC 的目标是像本地调用;但以下差异无法隐藏:① 耗时从纳秒级变成毫秒级且方差巨大;② 可能失败在"参数已生效"之后(kp-006 未知态);③ 两端代码版本可能不同(部署不同步期)。因此成熟的 RPC 框架都把超时、重试、熔断、负载均衡做成框架层默认(gRPC 的 deadline propagation、重试策略配置),而不是留给业务。
向前/向后兼容是长生命周期系统的生死线。规则(以 Protobuf 为例):字段编号一旦使用不可复用;新增字段用 optional/默认值;枚举只增不删;删除字段先标记 reserved。违反任何一条,滚动升级期间就会出现解析失败或字段串位。
超时传播(deadline propagation):上游剩余预算应随请求下传(gRPC deadline),避免"上游已放弃、下游还在全力算"的算力浪费——这是级联超时治理的基础设施。
直观类比
RPC 像打电话托人办事:你以为"一说他就办",实际上可能占线(拒绝)、串线(乱码)、对方记错了事(版本不兼容)、办完了你没听清回话(响应丢失,kp-006)。打电话前先想好"没打通怎么办",才是合格的委托人。
实例或案例
- gRPC:基于 HTTP/2 + Protobuf,四类流式接口,云原生默认 RPC;Kubernetes 组件间大量使用。
- Thrift / Avro:Hadoop 生态常用,Avro 以 schema 注册中心 + 编解码耦合解决演进。
- JSON API 的坑:无 schema 导致"对方少发一个字段"上线才炸;大 JSON 序列化的 CPU 开销成为瓶颈,普遍需要迁移二进制。
常见误区
- 误区一:"RPC 透明的,先用再说"。必须显式配置超时/重试/熔断;框架默认值不等于适合你的业务。
- 误区二:"接口加了字段不会破坏老客户端"。只有遵守 schema 演进纪律(可缺省、不复用编号)才成立。
- 误区三:"同步链路越长越好读"。链路深度即风险乘法(kp-002 扇出放大),深链路应考虑事件化(kp-008)。
与其他知识点的关系
自测题
- 为什么说 RPC 的"本地调用感"是一种伪装?
答:耗时数量级不同、存在未知态结果、两端版本可能不同步,这三点让远程调用本质上无法等同本地调用,必须按网络故障编程。
- Protobuf 删字段为什么要先 reserved?
答:防止字段编号被新字段复用后,旧消息数据被新代码解析成完全不同的语义,造成静默数据错乱。
- deadline propagation 解决什么问题?
答:把上游剩余超时预算传给下游,避免上游已超时放弃而下游仍持续消耗资源,控制级联浪费。
延伸阅读
- Andrew Birrell & Bruce Nelson, "Implementing Remote Procedure Calls"(ACM TOCS, 1984)。
- gRPC 官方文档中 deadline/retry 配置章节。
- Martin Kleppmann《DDIA》第 4 章(编码与演化)。