单体拆成微服务之后,进程之间最常走的通信方式不是「调一个 HTTP 接口试试看」,而是 RPC(Remote Procedure Call):调用方像调本地方法一样写出 budgetService.deduct(...),框架帮你完成寻址、序列化、网络、超时与错误处理。看起来很甜,底下却是一整层比 REST 更「协议化」的栈——做错一层,轻则延迟抖动,重则雪崩。
广告实时链路尤其苛刻:一次竞价里可能同步打出「定向 → 出价 → 预算扣减 → 频次」好几跳 RPC,每跳多几毫秒,总 deadline 就被吃光。这一篇是微服务与 RPC 系列的开篇:先把一次 RPC 从代理到传输拆开,再对照业界三条主流路线 gRPC / Thrift / Dubbo,最后讲清客户端侧几乎人人踩过的负载均衡与超时重试。
本文是微服务与 RPC 系列的第 1 篇(开篇 · 原理与框架对照)。 全系列 4 篇:
- RPC 框架(开篇)· 原理与 gRPC/Thrift/Dubbo(本篇)
- 服务注册与发现深挖
- 服务网格 Service Mesh 深挖
- API 网关与分布式追踪
一句话定位:RPC 框架把「远程调用」伪装成「本地方法」——代理层骗过你的编程直觉,真正决定性能与正确性的是序列化、协议帧、传输以及 Cluster 上的负载均衡/超时/重试策略。
TL;DR
- 一次 RPC ≈ 四层:Stub/Proxy(代理)→ Serialization(序列化)→ Protocol Framing(协议)→ Transport(传输);对端镜像反向拆开、派发到业务方法。
- 代理让业务无感远程;序列化决定 CPU 与包体大小;协议决定请求关联、多路复用、压缩与错误语义;传输决定连接管理与吞吐(常建在 Netty Codec Pipeline 上)。
- 同步 / 异步 / 流式是三种调用语义:同步占线程换简单,异步释放线程,流式适合推送与大结果集。
- gRPC:HTTP/2 + Protobuf,跨语言默认解,Unary/Stream 原生;治理多依赖外部(发现、Mesh、自定义 LB)。
- Thrift:强 IDL、多语言、可插拔传输/协议;生态里「Cluster 治理」要自己拼,偏「协议+codegen」。
- Dubbo:Java 微服务开箱 Cluster(路由/容错/负载均衡),序列化与协议可插拔;近年 Triple 对齐 HTTP/2 生态。
- 负载均衡在客户端挑实例:随机/轮询/加权/一致性哈希;要和注册中心的健康摘除联动(见注册发现)。
- 超时要端到端透传(Deadline);重试只对幂等或明确可重试错误,并设预算——无脑 FailOver 是雪崩制造机(见依赖韧性)。
- AdTech 血泪:竞价扣预算 RPC 默认 FailOver 重试 3 次,下游抖动时流量 4× → 线程池打满 → 超时更多 → 正反馈雪崩。
Table of contents
Open Table of contents
1. 为什么微服务里几乎必谈 RPC
拆服务之后,一次「扣预算」不再是同 JVM 内的方法调用,而是跨机器的网络往返。你可以继续用裸 HTTP+JSON,但广告竞价、投放控制这类路径对延迟、类型安全、多路复用、连接复用、超时语义更苛刻——于是 RPC 框架把这些横切能力标准化。
和「手写 HTTP Client」比,成熟 RPC 至少多给了四样东西:
- 契约:IDL / 接口定义驱动 codegen,字段兼容有规矩,减少「同学你 JSON 字段名又改了」;
- 连接与多路复用:长连接池 +(在 HTTP/2 上)单连接多流,避免每次调用三次握手;
- 统一的错误与超时模型:框架异常、业务异常、Deadline 传播有约定;
- 可插拔治理:Filter / interceptor 挂载鉴权、日志、限流、Trace。
RPC 要解决的核心问题可以列成清单:
- 怎么找到对端(服务发现,下一篇);
- 怎么把对象变成字节再还原(序列化);
- 怎么在字节流上切出「一次请求/响应」(协议,呼应 Netty 粘包拆包);
- 出错/超时/多副本时怎么选人、怎么重试(Cluster 治理)。
本篇先啃 2–4;第 1 点留到注册发现篇;Mesh 与网关如何接管治理,在第 3、4 篇展开。
Fallacies of distributed computing 里的前几条——网络可靠、延迟为零、带宽无限、网络是安全的——每一次「像调本地方法」的 RPC,其实都在假装这些谬误不存在。框架的价值,是把谬误显式化并治理,而不是假装它们消失了。
2. 一次 RPC 的分层栈
代理骗过直觉;性能与正确性藏在序列化、协议与传输。
2.1 代理(Stub / Proxy)
IDL 或接口定义生成客户端 Stub:业务只调方法,Stub 打包参数、填方法名/服务名、挂上超时与附件(attachment),再交给下层。服务端 Skeleton 按方法签名派发到实现类——对业务代码来说,远程与本地的边界被抹平了;对框架作者来说,边界全在下层。
代理层还常挂 Filter / Interceptor 链:出入站各跑一圈,典型挂载点包括鉴权、权限、访问日志、熔断、Metrics、Trace 注入。这和 Servlet Filter、Netty Pipeline 是同一心智:横切关注点不要污染业务方法。
2.2 序列化
| 方案 | 特点 | 典型场景 |
|---|---|---|
| Protobuf | 紧凑、强 schema、跨语言 | gRPC 默认;高 QPS 路径 |
| Hessian2 | Java 友好、动态 | 经典 Dubbo |
| Thrift binary | IDL 驱动 | 多语言存量系统 |
| JSON | 可读、调试友好 | 网关边缘、低 QPS |
序列化挑错的症状很直接:CPU 高、包体大、GC 抖。广告竞价链路热路径优先二进制 schema;调试排障可以另开 JSON 旁路,别混在同一条热 RPC 上。
兼容性也是序列化问题:Protobuf 靠字段号与「只增不改语义」的纪律;Hessian 更活但也更容易「一端升级、另一端 silently 读错」。跨团队接口要有 兼容性门禁(lint / 兼容性测试),不要只靠 code review 眼力。
2.3 协议帧
协议层至少要解决:
- 请求关联:每个调用一个
requestId,异步回包能对上; - 切帧:长度字段/定界,避免 TCP 粘拆包(见 Netty 编解码);
- 元数据:超时剩余、trace header、序列化类型、压缩算法;
- 错误语义:业务异常 vs 框架异常(超时、序列化失败、找不到方法)。
gRPC 把大量语义放进 HTTP/2 headers 与 trailer(如 grpc-status);Dubbo 经典协议则是自定义二进制头 + body。无论哪条路,「一次业务调用」必须在字节流上可切、可关联、可取消。
2.4 传输
gRPC 绑在 HTTP/2(多路复用、流控、头部压缩);Dubbo 经典走 Netty TCP 长连接,新版 Triple 也可走 HTTP/2;Thrift 可配 socket/framed 等。传输层还管连接池、心跳、背压——这里的坑和连接与 I/O 模型、Netty 线程模型是同一家族。
实践上要盯:连接数是否随实例数爆炸、空闲连接是否被中间 NAT 掐断、慢消费者是否把窗口砸满。传输层不健康时,上层再调超时参数也只是「更快地失败」,根因仍在连接与线程。
2.5 同步、异步与流式
- 同步 RPC:调用线程阻塞等结果——写起来最简单,线程池一堵全家堵;
- 异步 RPC:返回 Future/
CompletableFuture,释放调用线程,适合扇出聚合; - 流式(Streaming):客户端流、服务端流、双向流——适合日志推送、大结果集分页、长会话。
广告场景里,同步扣预算仍常见(强一致、短路径);异步扇出常用于并行拉用户画像/创意特征。别在同步链路上做重计算,那是把对方的线程池当成自己的算力。
3. 三条框架路线对照
差别不在「能不能远程调用」,而在「框架内建多少治理」vs「交给基建」。
3.1 gRPC
- 强项:跨语言一致体验;流式 RPC(服务端推、双向流)天然适配推送/长会话;生态与 K8s、Istio、OpenTelemetry 咬合紧。
- 弱项:服务发现、复杂路由、多活单元化,往往要你自己接 resolver / Mesh / 自定义 LB。
- 适用:多语言微服务、云原生默认栈、需要 stream 的场景。
gRPC 的负载均衡历史上常落在客户端(lookaside / 自定义 resolver)或 sidecar;别以为「用了 gRPC 就自动有均匀流量」——没配 resolver 时,它可能只傻傻打到第一个地址。
3.2 Apache Thrift
- 强项:成熟 IDL、多语言 codegen、传输与协议可组合(
TBinaryProtocol+TFramedTransport等)。 - 弱项:现代「微服务治理全家桶」不如 Dubbo/gRPC+Mesh 完整,团队常自己造一层封装。
- 适用:已有 Thrift 资产、或多语言强约束且暂不上 Mesh。
Thrift 适合「我们要一份严谨 IDL,运行时自己掌控」的团队;若你指望它自带灰度路由与动态配置,多半会失望,需要外挂配置中心与发现组件。
3.3 Apache Dubbo
- 强项:Cluster 层内建——Directory(服务目录)、Router(条件/标签路由)、LoadBalance、Cluster 容错(Failover/Failfast/Failsafe/Forking…)、Filter 链扩展点完备,Java 栈落地快。
- 弱项:历史上偏 Java;跨语言靠 Triple / gRPC 互通补齐,心智比「纯 gRPC」略杂。
- 适用:以 Java 为主的微服务、需要开箱路由/灰度/容错。
Dubbo 的扩展点(SPI)是双刃剑:能快速加鉴权、单元路由、压测流量染色;也能让团队变成「Filter 动物园」——每个业务线挂一串私货,最后谁也说不清一次调用经过了哪些层。治理扩展要平台统一,业务只留极少必需的。
一句话:协议选型看语言与流式需求;治理选型看你想把复杂度放在 SDK 还是 Mesh(第 3 篇)。
3.4 接口演进与多版本共存
线上服务很少「一次切干净」。常见共存策略:
- Protobuf:只加字段、不改语义;废弃字段保留号段;用
reserved防止复用; - URL / 包名版本:
budget.v1与budget.v2并行,网关按客户端版本分流; - 双写/双读过渡期:新老接口同时服务,用指标确认旧调用归零再下线。
广告主侧 SDK 升级极慢,服务端必须比客户端更保守——宁可多撑一个废弃字段三个季度,也不要在大促前强切。
3.5 选型速查
| 诉求 | 更倾向 |
|---|---|
| 多语言 + K8s + Istio | gRPC |
| Java 为主、要开箱路由灰度 | Dubbo |
| 存量 Thrift IDL / 多语言老系统 | Thrift + 自建薄封装 |
| 极致延迟、同机房同构 | 成熟 SDK(Dubbo/gRPC)直连,慎上额外 hop |
| 强合规 mTLS + 多语言一致治理 | Mesh 为中心,RPC 只管契约 |
选型不是一次性决议。广告团队常见演进:Java 单体时代 Dubbo → 多语言渗透上 gRPC → 合规要求推 Mesh mTLS → 入口治理收拢到网关。每一阶段保留上一阶段仍高效的部分,比「推翻重来」便宜一个数量级。
4. 负载均衡、超时与重试
重试是放大镜:好的时候恢复快,坏的时候把故障放大成雪崩。
4.1 负载均衡
常见策略:
- 随机 / 加权随机:实现简单,短期可能不均;
- 轮询 / 加权轮询:更均匀,注意慢实例拖后腿(head-of-line);
- 一致性哈希:按 key(如用户、广告主)粘滞,利于缓存命中与「同广告主串行化」;
- 最短响应 / P2C(Power of Two Choices):偏向更快实例,但要对「刚恢复的冷实例」有预热保护,否则流量瞬时打爆新节点。
策略再炫,候选集必须干净:不健康实例要尽快从注册视角摘掉(下一篇)。负载均衡与负载均衡专题同一套数学,只是 RPC 通常发生在客户端进程内;Mesh 则把同一决策挪到 Sidecar(第 3 篇)。
多机房场景还要:同城优先、跨城降级——别把哈希策略做成「一半流量跨洲」。多活就近接入见 数据中心设计。
4.2 超时与 Deadline
只设「客户端等 200ms」不够——下游还可能再调下游。正确姿势是 端到端 Deadline:入口(或网关)定下绝对截止时间,沿调用链透传剩余时间;链路任一环超时立刻失败,避免「已经注定超时还在占线程」。
常见反模式:
- 每一跳都设固定 200ms,三跳理论最多 600ms,入口 SLO 却是 150ms;
- 只设 socket timeout,不设业务 deadline,连接活着但业务已无意义;
- 超时后客户端走了,服务端还在算——应用层要支持取消(gRPC cancel / context cancel)。
4.3 重试
三条铁律:
- 默认 Failfast:超时/连接失败直接抛,别自动连打三次;
- 仅幂等读或带幂等键的写才 FailOver;预算扣减、下单、计费禁止无脑重试;
- 设预算:
maxAttempts、总体超时、以及对「同实例 vs 换实例」的明确策略;配合熔断、舱壁(见依赖韧性)。
可重试错误要白名单化:连接拒绝、明确的 UNIMPLEMENTED/NOT_FOUND 通常不要狂重试;UNAVAILABLE、幂等读上的超时才考虑有限次。hedged request(同时打两个副本取最快)更是流量放大器,只给只读且尾延迟不可接受的路径,并严格限流。
4.4 错误模型:别把一切都打成 Exception
清晰的错误分类能决定上游行为:
| 类型 | 例子 | 上游该做 |
|---|---|---|
| 可重试基础设施错误 | 连接拒绝、短暂 UNAVAILABLE | 有限次换实例(若幂等) |
| 明确业务拒绝 | 余额不足、参数非法 | 禁止重试,直接失败/降级 |
| 超时 | Deadline exceeded | 看幂等与预算;常应失败快 |
| 认证/权限 | UNAUTHENTICATED | 修复身份,不要循环重试 |
把「余额不足」和「连接断开」丢进同一个 RpcException,重试逻辑必然误伤。gRPC status code、Dubbo 业务异常 vs 框架异常,都要在团队约定里写死。
参考
-
gRPC Authors. gRPC Documentation:HTTP/2、Protobuf、流式 RPC 与官方概念模型。
-
Apache Dubbo. Dubbo Architecture & Cluster:Directory / Router / LoadBalance / Cluster 容错的一手说明;本站深挖见 Dubbo 系列。
-
Apache Thrift. Thrift Protocol & Transport:IDL、协议与传输组合的官方文档。
-
Google. API Design Guide — Deadline:端到端超时透传的设计共识。
-
Peter Deutsch. The Eight Fallacies of Distributed Computing:理解「本地方法幻觉」的经典清单。