为什么大厂还在用 RPC?不是因为快,是因为不崩

本文通过一个真实的服务雪崩事故,挑战了"RPC 比 HTTP 快"的常见认知。作者用两组 Go 实验量化证明:协议层优化在端到端延迟中仅占个位数百分比,真正的差异在于服务治理能力(超时、熔断、降级)。

封面

所有"RPC 比 HTTP 快"的对比文章,多数拿 HTTP/1.1+JSON 当对手。换成 HTTP/2 之后呢?我跑了实验,协议层的差距在你大部分接口里是个位数百分比。那大厂为什么还在用 RPC?答案不在协议层。

一、那天凌晨三点,我们的服务挂了

凌晨三点二十一分,电话把我吵醒。

值班同学的声音在那头发抖:“订单接口全挂了,下游全在超时,连查询都打不开。”

我抓起电脑接上 VPN,打开监控。一整列接口的 P99 延迟都飙到 30 秒——也就是 Nginx 的网关超时。底下用户正在双十二大促,每秒几千单。我盯着监控大屏,心里只有一个问题:到底是哪个环节先崩的?

那时候我们的架构很简单:所有服务之间用 HTTP/1.1 + JSON 互相调用。Spring Boot + Nginx + Eureka,在内网里跑。说实话,我之前以为这套挺好用的——直到那一晚。

后面复盘,事故链是这样的:

  1. 一个不重要的"积分计算"接口突然变慢(下游 Redis 集群有抖动)
  2. 上游的"下单"服务每次调用都等到超时——但没有超时上限,连接池一格一格被卡住
  3. 卡住之后,“下单"服务的连接池打满,新请求开始排队
  4. 排队太久,Nginx 那一层的健康检查超时,开始把"下单"服务标成不可用
  5. 流量被甩到剩下的实例上,那些实例也因为同样的下游问题,几秒后步入同样的命运
  6. 整条调用链垮了

我们花了 47 分钟把它拉回来——靠的不是什么聪明的工程手段,是手动重启每一个被卡死的服务,把"积分计算"这个不重要的功能直接降级成"返回 0”。

雪崩传播路径示意图 [16:9]

那一晚我没再睡。坐在工位上想了很多事。第二天复盘会上有人提出来:要不要换 RPC 框架?

我当时的第一反应是抗拒——RPC 不就是为了"更快"吗?我们这点流量,“快不快"根本不是瓶颈。改框架的成本谁来承担?

但接下来三个月发生的事,让我把这句话原封不动地咽了回去。

先记住这个画面:接口崩的时候,崩的不是协议,是没有刹车的调用链。


二、所有"RPC 更快"的文章都选错了对手

雪崩之后,我第一件事是研究 RPC。我打开搜索引擎,搜"RPC 比 HTTP 快多少”——好家伙,铺天盖地的文章告诉我"RPC 快 5 倍 / 10 倍 / 20 倍"。

我一篇一篇看。看到第六篇我笑了。

我翻了一圈,绝大多数文章,对手都是 HTTP/1.1 + JSON。

这就像两个人比谁跑得快,一个穿钉鞋,另一个穿拖鞋。然后告诉你:钉鞋跑得真快。

让我把现有的"RPC 更快"论证拆给你看。它们的套路高度一致,就这三招:

第一招:HTTP 头巨大。 他们说,HTTP/1.1 每次请求要发好几百字节的纯文本头部,又是 Content-Type 又是 Accept,开销巨大。 听起来很合理对吧?但你打开 HTTP/2 的 RFC 看看——HPACK 头部压缩在 2015 年就已经标准化。同样的头部,HTTP/2 走二进制 + 静态字典,重复头部通过动态表索引压缩后通常只需 1-2 字节。这一招在 HTTP/2 面前直接报废。

而且你想想,这个时代谁还在生产环境用纯 HTTP/1.1?除了一些老遗留系统和外部 API,几乎所有现代微服务网关——Envoy、Nginx 1.9.5+、ALB(对外入口侧默认 HTTP/2,到后端 upstream 需显式配置)、API Gateway——基本都在用 HTTP/2。你拿 HTTP/1.1 的头部开销出来吓人,约等于拿 32 位系统说事——理论上没错,现实里离我们已经很远。

第二招:JSON 序列化慢。 他们说,文本协议比二进制协议解析慢一个数量级。 这话本身没错。但 JSON 的对手不是 HTTP,是序列化方案。HTTP 完全可以走 Protobuf、可以走 MessagePack、可以走任何二进制协议——只是大多数人懒得换罢了。把"序列化方案"算到"协议"头上,本身就是偷换概念。

打个比方。你说"开高铁比开汽车快"——这成立。但你说"开高铁比开汽车快,因为高铁烧的是电"——这就不对了。烧电是动力来源,不是速度差距的根本原因;快是因为它跑在专线轨道上。把"序列化方式"算到"协议"头上,犯的就是这种错。HTTP 完全可以载二进制,gRPC 跑在 HTTP/2 上就是最好的证明。

第三招:连接握手开销大。 他们说,HTTP 每次请求要重新握手。 这一招最离谱。Keep-Alive 是 1997 年的 HTTP/1.1 RFC 写进去的,连接复用早就是默认行为。任何一个稍微正经一点的 client 都不会每次重新建连。

你打开 Java 的 Apache HttpClient、Go 的 http.Transport、Python 的 requests.Session——它们底层全都是连接池,全都是 Keep-Alive。会让你"每次重新握手"的写法,是 PHP 早期那种 file_get_contents 短连接调用——但那已经是十几年前的故事了。

三招都打偏。那为什么这些文章看起来还挺有道理?

因为他们对比的是 2010 年的 HTTP,对比的是没人用心配过的客户端,对比的是教科书里那张"OSI 七层"的简化图。他们没在拿你今天用的东西做对比。

而你今天用的东西是什么?

  • gRPC = HTTP/2 + Protobuf + 一套 IDL
  • Dubbo = TCP + Hessian/Protobuf + 注册中心(Dubbo 3.x 起主流版本默认已改用基于 HTTP/2 的 Triple 协议,兼容 gRPC)
  • 现代 HTTP 服务 = HTTP/2 + Protobuf 或 JSON + Keep-Alive

你看,gRPC 本身就是 HTTP/2。这两个东西在协议层面已经是一家人了。

“选错对手"对比图 [3:2]

那 RPC 和 HTTP 的边界到底在哪?

不在协议层。在框架层。

但要论证这一点,光说不够。我得让数据说话。


三、实测:协议优化的真实收益只有 5%

我写了两组实验。第一组对比三种协议组合的吞吐和延迟,第二组拆解一次真实调用的各环节耗时。代码全部开源,你可以自己跑。

实验一:三种协议组合,谁更快?

我用 Go 起了三个服务,跑同一个接口(返回一个用户对象),用 32 个并发 worker 持续打 5 秒:

方案 QPS P50 延迟 P99 延迟
HTTP/1.1 + JSON 104,841 276µs 919µs
HTTP/2 + JSON 77,007 404µs 848µs
HTTP/2 + 二进制 77,804 403µs 837µs

实验配置:macOS / Go 1.26.2 / 同主机 loopback / 32 worker / 5s。响应大小:HTTP/1.1+JSON 179 B,HTTP/2+二进制 84 B。代码在 evidence/code/e1-protocol-bench/,可复现。

HTTP/1.1 + JSON 的 QPS 居然最高——这不是我特意挑的反差数据。同主机 loopback 消除了网络往返延迟,HTTP/2 多路复用的核心价值(减少 RTT 等待)在这个场景根本无法体现;叠加 HTTP/2 帧处理的额外 CPU 开销,导致同机测试 QPS 反不如 HTTP/1.1。换到真实跨机网络环境,HTTP/2 会反超回来,但反超的幅度也就 10~20%,不是数量级。这组数据更重要的结论是实验本身的局限——loopback 场景不是 HTTP/2 的主场,别拿这组数字去反推生产决策。

E1 协议对比表 [3:2]

更有趣的是 HTTP/2 + JSON vs HTTP/2 + 二进制的对比:P50 差距只有 1µs——百万分之一秒。把 JSON 换成二进制压缩到 47% 体积,端到端延迟几乎看不出来。

为什么?因为响应本身才 179 字节。你省下 95 字节,在万兆网卡上根本不够看。

那序列化本身呢?我专门跑了一组:

操作 JSON 二进制
编码 50000 次平均 253ns 115ns
解码 50000 次平均 1.367µs 217ns

二进制确实更快——快了一个数量级。但单位是纳秒。换算到端到端,这点差距完全被网络抖动淹没。

到这里你应该有个直觉:协议层的优化空间,比你想象的小得多。

但直觉不算证据。我得给你看真正的拆解。

实验二:一次调用的时间都花在哪了?

第二个实验把端到端调用拆成可测量的几段。业务逻辑用 sleep 10ms 模拟,代表轻量级场景(这是本实验的基准假设,代表轻量级 DB 查询;真实生产 P99 常见 50-200ms,见下方敏感性分析)。

跑 1000 次取中位数:

环节 中位耗时 占总耗时
请求序列化(JSON Marshal) 2.79µs 0.02%
网络栈 + 协议处理 1.91ms 15.96%
业务逻辑(DB / 下游调用) 10ms 83.64%
响应反序列化(JSON Unmarshal) 24.25µs 0.20%
端到端 P50 11.96ms 100%

实验代码:evidence/code/e2-latency-breakdown/

序列化加反序列化加起来,0.22%

那协议层(网络栈+协议处理)的 16% 看起来挺可观——但别急。这个 16% 是建立在"业务只要 10ms"这种轻量场景下的。真实业务有多重?我做了敏感性分析:

业务逻辑耗时 协议+序列化占比
10ms 16.19%
20ms 8.82%
50ms 3.73%
100ms 1.90%

业务越重,协议层占比越小。

端到端延迟堆叠条形图 [3:2]

而典型微服务的业务逻辑(不算下游调用)通常 20~50ms。在你大部分接口里,协议层优化的天花板是个位数百分比。

你花一周时间把所有接口从 JSON 切到 Protobuf——也许能省下 5% 的端到端延迟。 而你的下游服务慢了 50ms——直接把整条链路打崩。

哪个更值得你关心?

这就是为什么我看到那些"RPC 比 HTTP 快 10 倍"的标题想笑。不是说他们撒谎——他们确实测出了协议层的差距——而是他们测对了一个没人在乎的指标。

这一章不是说协议优化没价值。10w QPS 量级以上的核心链路,省 5% 是真金白银。问题是,绝大多数业务系统连 1w QPS 都到不了——你优化协议,是给空气省时间。


四、真正的胜负手是出问题的那一刻

回到我开头讲的那个雪崩之夜。

复盘那天我画了一张图。把整个事故的传播路径画出来之后,我盯着它发呆——没有一个环节是被"协议慢"杀死的

杀死系统的是这几个东西:

超时没有上限——下游一慢,上游连接池就成了无底洞,慢慢被卡死。熔断器不存在,“积分计算"连续失败几百次,调用方还在继续发。一个非核心接口出了问题,没有任何机制把它从主链路隔离出去。下游越慢,上游排队越积越多,雪上加霜。

这四个东西是什么?是服务治理

而 RPC 框架——我说的是接入公司内部治理 SDK(或 Service Mesh 层)之后的真实 RPC 框架,gRPC、Dubbo(Triple)、Tars(腾讯生态)、Thrift——给你的是什么?

这些能力,在接入治理 SDK 后都有。

注:gRPC 裸框架本身提供 deadline、拦截器、客户端侧负载均衡,不内置熔断/限流/降级。这些治理能力来自:①公司内部封装的治理 SDK(如 tRPC-Go 的内置治理能力)②Sentinel/Resilience4j 独立集成③Istio/Envoy Service Mesh。以下讨论基于公司内部封装的 gRPC 治理 SDK 场景。

让我把同样的雪崩场景,搬到接入治理 SDK 的 RPC 框架下推演一遍:

以下是基于工程常识的场景推演,不是某次真实事故的实测复盘——但雪崩演进的典型路径就是这样。

纯 HTTP(无治理)的故障演进:

T+0s   下游 Redis 抖动,「积分计算」开始变慢
T+5s   上游连接池开始堆积,未完成请求 1000 → 5000
T+15s  连接池打满,新请求直接报错 "no available connection"
T+30s  Nginx 健康检查失败,把上游实例标记下线
T+45s  剩余实例承接全部流量,依次进入相同状态
T+60s  整条调用链不可用

注意第二步——“连接池开始堆积”。这是雪崩的起点。HTTP/1.1 的默认 client 行为,在大多数语言里都没有强制的"等待上限”。你可以加 timeout,但要有人记得加。你的整个团队,里里外外几十个调用点,每一个都加对了吗?这是个赌局,赌输的代价就是某天凌晨三点的电话。

RPC 框架(接入治理 SDK)的故障演进:

T+0s   下游 Redis 抖动,「积分计算」开始变慢
T+2s   SDK 强制的超时上限触发,调用方快速失败
T+5s   服务治理层识别连续失败,「积分计算」被熔断
T+5s   后续请求直接走 fallback 逻辑(返回默认积分 0)
T+10s  下游恢复,熔断器进入 half-open,自动恢复
T+15s  全链路恢复正常

差别在有没有刹车

更深一层:差别在默认行为

HTTP 的默认行为是"无脑等到死”——除非你主动设超时、主动加重试、主动配熔断、主动接监控。这些动作每一个都不难,但加起来就是一套需要长期维护的体系。问题是,你团队里新来的同学知道为什么这么写吗?三年后业务长出十倍体量的时候,这套手工拼装的治理还能跟上吗?

接入内部治理 SDK 的 RPC 框架,它的设计哲学是"假设下游会出问题"——SDK 要求超时显式设置(没有全局默认值兜底),服务治理层识别连续失败后触发熔断,限流和降级开箱即用。它把"治理"从"工程师的自觉",变成了"框架的契约"。

HTTP 默认行为 vs RPC 框架默认行为 [3:2]

HTTP 这套不是不能加刹车——你可以装 Spring Cloud,可以接 Sentinel,可以自己写中间件。但你得知道你需要刹车,得有人去配,得有人去维护。

而内置治理 SDK 的 RPC 框架默认就给你装好了。你的下游一定会出问题,所以它提前帮你假设它会出问题。

这才是大厂选 RPC 的真正原因。不是因为它"快 5ms"——是因为它在出事的时候给你一根救命稻草。

雪崩演进双场景对比 [3:2]

那次选型,我们最后选了什么?

雪崩之后,我们组开了三次会讨论框架。会上有人主张直接上 Dubbo,有人主张换 gRPC,还有人主张就在 HTTP 上接 Sentinel。

最后我们的决策不是技术比拼,是这两个判断:

判断一:我们需要的不是「快」,是「可控」。 我们的业务量根本不到协议优化能体现差距的水平。但我们需要熔断、需要限流、需要降级、需要服务发现、需要负载均衡——这些东西自己写一遍,不仅麻烦,还容易写错。

判断二:内置 vs 自建,成本差几个数量级。 Spring Cloud 看起来"也能做"——但你试过自己接一遍 Eureka + Hystrix(现已停止维护,社区主推 Resilience4j)+ Ribbon + Zuul + Sleuth + Zipkin 吗?光把这套调通就要两周,跑稳定要半年,每次升级又是一波踩坑。 gRPC + 公司内部治理 SDK,接入文档走完,接入周期从两周压缩到两天——大部分踩坑的坑,中间件团队替你踩完了。

我们选了 gRPC。整个治理栈都在 SDK 里,不需要自己拼装。

那之后没再发生过类似的雪崩。不是因为系统变快了——P50 延迟其实差不多——而是因为任何一个下游开始有问题,链路会自己止血。


五、决策框架:你到底该不该选 RPC

讲到这里,应该可以给你一个落地的判断标准了。我把这两年看过的、做过的选型总结成一张决策表。

先回答这四个问题,再决定要不要换 RPC:

问题 建议方案
你的服务调用关系是网状的,超过 5 个内部服务互相调? ✅ RPC
你担心某个下游慢/挂会拖垮整个链路? ✅ RPC
你需要在服务实例增减时自动发现/负载均衡? ✅ RPC
你的接口需要给非内部团队(前端/外部合作方)调用? ✅ HTTP

如果前三个全是 RPC,第四个是 HTTP——上 RPC,省心。 如果只有第四个——继续用 HTTP,REST 风格反而更直观。 两边都有?典型的双协议并存架构:内部用 gRPC,对外暴露 HTTP/REST 网关层。这是大多数中型公司的最终形态。

RPC/HTTP 决策流程图 [3:2]

几个常见的判断误区,顺便也说一下:

误区一:“我们流量小,用不上 RPC。"——错。RPC 的核心价值不是承载大流量,是控制小故障不要演变成大故障。哪怕你只有几百 QPS,只要你的服务调用链路超过三跳,就有雪崩风险。

误区二:“切 RPC 要花两个月,我们没时间。"——切框架是有成本,但对比的是"出一次大事故的代价”。我们那次 47 分钟的故障,业务损失够养一个 5 人小组工作半年。这套治理能力你可以自建,但半年踩坑加持续维护的代价,往往超过直接接入框架的成本。

误区澄清 [1:1]


六、回到那一晚

回到我开头那个凌晨三点。

那一晚之后我学到的最重要的一件事是:工程师对"性能"的迷恋,常常掩盖了真正的工程问题。

我们花了好几年优化接口、压榨 SQL、研究序列化协议——所有这些加起来,可能还不如一行 WithTimeout(3 * time.Second) 的价值大。

不是说优化没意义。是说优先级搞反了

让你的服务跑得更快,是锦上添花。让你的服务在出问题的时候不要一起死掉——这是命。

这两年带过几个新同学,每次让他们做服务设计的复盘,问题都集中在一个地方:他们对"快"特别敏感,对"崩"特别迟钝。看到 P99 慢了 10ms 就着急去调,看到没设超时却觉得"反正大概率不会触发”。

可工程的现实是反过来的——大概率不会出事的事,一旦出事就是大事。

下次再看到"RPC 比 HTTP 快多少"的标题,先问一句:它在衡量什么。答案往往已经说明了作者在不在乎你真正的问题。

选 RPC 不是因为它跑得快,是因为它在你最需要的时候,告诉你:“出问题了,我替你刹车。”

收尾情绪图 [3:2]


附录:实验代码和原始数据

本文 2 组实验的代码已开源:

GitHub:zhiyulab-evidence/rpc-vs-http

  • e1-protocol-bench/:E1 协议层吞吐与延迟对比(HTTP/1.1+JSON vs HTTP/2+JSON vs HTTP/2+二进制),可复现 46.9% 体积压缩和 1µs P50 差距
  • e2-latency-breakdown/:E2 端到端调用延迟分解(序列化/网络+协议/业务逻辑各环节占比),可复现 83.64% 业务逻辑占比和敏感性分析数据

每个子目录都有独立说明,二进制编译产物不入库,跑实验前自己 go build

原文发布于 止语Lab


关于止语Lab

一个工程师的深度技术笔记。

不写入门教程,不追热点。只写那些真正折腾过、想通了的东西。

了解更多 →