Skip to content
Charles Shao
Go back

RPC 框架(开篇)· 原理与 gRPC/Thrift/Dubbo

views

单体拆成微服务之后,进程之间最常走的通信方式不是「调一个 HTTP 接口试试看」,而是 RPC(Remote Procedure Call):调用方像调本地方法一样写出 budgetService.deduct(...),框架帮你完成寻址、序列化、网络、超时与错误处理。看起来很甜,底下却是一整层比 REST 更「协议化」的栈——做错一层,轻则延迟抖动,重则雪崩。

广告实时链路尤其苛刻:一次竞价里可能同步打出「定向 → 出价 → 预算扣减 → 频次」好几跳 RPC,每跳多几毫秒,总 deadline 就被吃光。这一篇是微服务与 RPC 系列的开篇:先把一次 RPC 从代理到传输拆开,再对照业界三条主流路线 gRPC / Thrift / Dubbo,最后讲清客户端侧几乎人人踩过的负载均衡与超时重试。

本文是微服务与 RPC 系列的第 1 篇(开篇 · 原理与框架对照)。 全系列 4 篇:

  1. RPC 框架(开篇)· 原理与 gRPC/Thrift/Dubbo(本篇)
  2. 服务注册与发现深挖
  3. 服务网格 Service Mesh 深挖
  4. API 网关与分布式追踪

一句话定位:RPC 框架把「远程调用」伪装成「本地方法」——代理层骗过你的编程直觉,真正决定性能与正确性的是序列化、协议帧、传输以及 Cluster 上的负载均衡/超时/重试策略。

TL;DR

Table of contents

Open Table of contents

1. 为什么微服务里几乎必谈 RPC

拆服务之后,一次「扣预算」不再是同 JVM 内的方法调用,而是跨机器的网络往返。你可以继续用裸 HTTP+JSON,但广告竞价、投放控制这类路径对延迟、类型安全、多路复用、连接复用、超时语义更苛刻——于是 RPC 框架把这些横切能力标准化。

和「手写 HTTP Client」比,成熟 RPC 至少多给了四样东西:

  1. 契约:IDL / 接口定义驱动 codegen,字段兼容有规矩,减少「同学你 JSON 字段名又改了」;
  2. 连接与多路复用:长连接池 +(在 HTTP/2 上)单连接多流,避免每次调用三次握手;
  3. 统一的错误与超时模型:框架异常、业务异常、Deadline 传播有约定;
  4. 可插拔治理:Filter / interceptor 挂载鉴权、日志、限流、Trace。

RPC 要解决的核心问题可以列成清单:

  1. 怎么找到对端(服务发现,下一篇);
  2. 怎么把对象变成字节再还原(序列化);
  3. 怎么在字节流上切出「一次请求/响应」(协议,呼应 Netty 粘包拆包);
  4. 出错/超时/多副本时怎么选人、怎么重试(Cluster 治理)。

本篇先啃 2–4;第 1 点留到注册发现篇;Mesh 与网关如何接管治理,在第 3、4 篇展开。

Fallacies of distributed computing 里的前几条——网络可靠、延迟为零、带宽无限、网络是安全的——每一次「像调本地方法」的 RPC,其实都在假装这些谬误不存在。框架的价值,是把谬误显式化并治理,而不是假装它们消失了。

2. 一次 RPC 的分层栈

一次 RPC 调用的分层栈示意图。左右两栏对比客户端与服务端:客户端自上而下为 Stub/Proxy(接口代理)、Serialization(Protobuf/Hessian/JSON)、Protocol Framing(消息头/请求 ID/压缩)、Transport(HTTP/2·TCP·Netty);服务端对应为 Skeleton/Dispatcher、Deserialization、Protocol Decode、Transport Accept。传输层中间双向箭头连接网络请求与响应。底部注释说明上层只看见调本地方法,真正跨进程的是下面三层,Netty Codec Pipeline 常嵌在协议与传输之间。

代理骗过直觉;性能与正确性藏在序列化、协议与传输。

2.1 代理(Stub / Proxy)

IDL 或接口定义生成客户端 Stub:业务只调方法,Stub 打包参数、填方法名/服务名、挂上超时与附件(attachment),再交给下层。服务端 Skeleton 按方法签名派发到实现类——对业务代码来说,远程与本地的边界被抹平了;对框架作者来说,边界全在下层。

代理层还常挂 Filter / Interceptor 链:出入站各跑一圈,典型挂载点包括鉴权、权限、访问日志、熔断、Metrics、Trace 注入。这和 Servlet Filter、Netty Pipeline 是同一心智:横切关注点不要污染业务方法

2.2 序列化

方案特点典型场景
Protobuf紧凑、强 schema、跨语言gRPC 默认;高 QPS 路径
Hessian2Java 友好、动态经典 Dubbo
Thrift binaryIDL 驱动多语言存量系统
JSON可读、调试友好网关边缘、低 QPS

序列化挑错的症状很直接:CPU 高、包体大、GC 抖。广告竞价链路热路径优先二进制 schema;调试排障可以另开 JSON 旁路,别混在同一条热 RPC 上。

兼容性也是序列化问题:Protobuf 靠字段号与「只增不改语义」的纪律;Hessian 更活但也更容易「一端升级、另一端 silently 读错」。跨团队接口要有 兼容性门禁(lint / 兼容性测试),不要只靠 code review 眼力。

2.3 协议帧

协议层至少要解决:

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 同步、异步与流式

广告场景里,同步扣预算仍常见(强一致、短路径);异步扇出常用于并行拉用户画像/创意特征。别在同步链路上做重计算,那是把对方的线程池当成自己的算力。

3. 三条框架路线对照

gRPC 与 Dubbo 协议栈与治理能力对照图。左栏 gRPC:HTTP/2 多路复用、Protobuf/IDL stub、Unary/Stream、治理依赖外部组件;右栏 Dubbo:TCP/Triple、多序列化可插拔、同步异步泛化与 Filter 链、内建 Cluster(路由容错负载均衡)。底部说明选型直觉:跨语言/云原生默认 gRPC,Java 要开箱治理常选 Dubbo,Thrift 介于其间。

差别不在「能不能远程调用」,而在「框架内建多少治理」vs「交给基建」。

3.1 gRPC

gRPC 的负载均衡历史上常落在客户端(lookaside / 自定义 resolver)或 sidecar;别以为「用了 gRPC 就自动有均匀流量」——没配 resolver 时,它可能只傻傻打到第一个地址。

3.2 Apache Thrift

Thrift 适合「我们要一份严谨 IDL,运行时自己掌控」的团队;若你指望它自带灰度路由与动态配置,多半会失望,需要外挂配置中心与发现组件。

3.3 Apache Dubbo

Dubbo 的扩展点(SPI)是双刃剑:能快速加鉴权、单元路由、压测流量染色;也能让团队变成「Filter 动物园」——每个业务线挂一串私货,最后谁也说不清一次调用经过了哪些层。治理扩展要平台统一,业务只留极少必需的。

一句话:协议选型看语言与流式需求;治理选型看你想把复杂度放在 SDK 还是 Mesh(第 3 篇)。

3.4 接口演进与多版本共存

线上服务很少「一次切干净」。常见共存策略:

广告主侧 SDK 升级极慢,服务端必须比客户端更保守——宁可多撑一个废弃字段三个季度,也不要在大促前强切。

3.5 选型速查

诉求更倾向
多语言 + K8s + IstiogRPC
Java 为主、要开箱路由灰度Dubbo
存量 Thrift IDL / 多语言老系统Thrift + 自建薄封装
极致延迟、同机房同构成熟 SDK(Dubbo/gRPC)直连,慎上额外 hop
强合规 mTLS + 多语言一致治理Mesh 为中心,RPC 只管契约

选型不是一次性决议。广告团队常见演进:Java 单体时代 Dubbo → 多语言渗透上 gRPC → 合规要求推 Mesh mTLS → 入口治理收拢到网关。每一阶段保留上一阶段仍高效的部分,比「推翻重来」便宜一个数量级。

4. 负载均衡、超时与重试

RPC 客户端侧负载均衡、超时与重试分层示意图。横向流水线:业务代码 invoke stub → Cluster/容错(FailOver·Failfast)→ LoadBalance(随机·轮询·一致性哈希)→ 选中 Endpoint 并设超时/Deadline → Transport 发出。下方三枚强调卡片:重试只做幂等写或明确可重试错误码;超时要端到端透传 Deadline/Context;熔断与限流参见 dependency-resilience。

重试是放大镜:好的时候恢复快,坏的时候把故障放大成雪崩。

4.1 负载均衡

常见策略:

策略再炫,候选集必须干净:不健康实例要尽快从注册视角摘掉(下一篇)。负载均衡与负载均衡专题同一套数学,只是 RPC 通常发生在客户端进程内;Mesh 则把同一决策挪到 Sidecar(第 3 篇)。

多机房场景还要:同城优先、跨城降级——别把哈希策略做成「一半流量跨洲」。多活就近接入见 数据中心设计

4.2 超时与 Deadline

只设「客户端等 200ms」不够——下游还可能再调下游。正确姿势是 端到端 Deadline:入口(或网关)定下绝对截止时间,沿调用链透传剩余时间;链路任一环超时立刻失败,避免「已经注定超时还在占线程」。

常见反模式:

4.3 重试

三条铁律:

  1. 默认 Failfast:超时/连接失败直接抛,别自动连打三次;
  2. 仅幂等读或带幂等键的写才 FailOver;预算扣减、下单、计费禁止无脑重试;
  3. 设预算maxAttempts、总体超时、以及对「同实例 vs 换实例」的明确策略;配合熔断、舱壁(见依赖韧性)。

可重试错误要白名单化:连接拒绝、明确的 UNIMPLEMENTED/NOT_FOUND 通常不要狂重试;UNAVAILABLE、幂等读上的超时才考虑有限次。hedged request(同时打两个副本取最快)更是流量放大器,只给只读且尾延迟不可接受的路径,并严格限流。

4.4 错误模型:别把一切都打成 Exception

清晰的错误分类能决定上游行为:

类型例子上游该做
可重试基础设施错误连接拒绝、短暂 UNAVAILABLE有限次换实例(若幂等)
明确业务拒绝余额不足、参数非法禁止重试,直接失败/降级
超时Deadline exceeded看幂等与预算;常应失败快
认证/权限UNAUTHENTICATED修复身份,不要循环重试

把「余额不足」和「连接断开」丢进同一个 RpcException,重试逻辑必然误伤。gRPC status code、Dubbo 业务异常 vs 框架异常,都要在团队约定里写死。

参考


views
Share this post on:

Previous Post
服务注册与发现深挖
Next Post
分布式 ID 深挖 · 雪花算法及其变体