RPC 到底是什么:微服务内部调用的首选,以及它藏起来的三个坑

大家好,我是晚安code。

这篇不打算重复那些「定义 + 通信流程 + 框架对比」的套路,我想聊的是:RPC 的甜头来自「像本地调用一样」,它所有的坑也全部来自「它毕竟不是本地调用」。

一、RPC 是什么:一次调用,怎么就跑到了另一台机器上

RPC(Remote Procedure Call,远程过程调用):让调用方像调用本地方法一样,去调用另一台机器上的方法,把寻址、序列化、网络收发这些事全部藏进框架里。类比:你在外卖软件上下单点一份牛肉面,只要说清楚要什么,骑手怎么找路、厨房在哪栋楼、配送箱保不保温,都不用你管。

这个类比得往下再走一步,因为这里正好是第一个分岔口------外卖送到你手上,和你在楼下自己煮一碗面,中间差着的东西,恰恰是 RPC 隐藏掉的全部复杂度。

序列化(Serialization):把内存里的对象转成能在网络上传输的字节流,接收端再还原成对象,后者叫反序列化。类比:把一台组装好的家具拆成平板打包寄走,收件人照着图纸拼回去------拆和拼都不难,难的是图纸不能有歧义。

一次 RPC 调用,代码上只有一行,实际跑起来是这么一条链:

1)你写的 userService 其实不是实现类,是一个动态代理生成的桩,它长得像接口,干的却是打包的活。

2)代理把「接口名 + 方法名 + 参数」序列化成一段二进制。网络只认字节,不认 Java 对象,这一步绕不开。

3)二进制顺着一条已经建好的长连接发出去。注意是长连接------每次调用都重新握手,那是 HTTP/1.1 时代的做法。

4)对端的解码器把字节还原成对象,查路由表找到真正的实现类,反射调用它。

5)返回值再反过来走一遍序列化和网络,最后你的 getUser(id) 拿到了一个 User 对象。

图里我特意把「动态代理」放在最前面。很多人以为 RPC 的魔法发生在网络上,其实第一步就发生了:那行代码从写下的一刻起,就不是本地调用。

二、RPC 到底解决了什么问题(它最早真不是为了性能)

搜 RPC 的文章,十篇里有九篇会说「为了性能」。这个说法不算错,但顺序反了。

RPC 这个概念的源头是 1980 年代的 Bruce Jay Nelson,他在论文里给的目标是「简单、高效、通用」------注意,简单排在高效前面。而微服务时代我们真正被它救过的,也不是那点吞吐量,是下面这三件具体的事。

第一件:不用每个团队自己重写一遍连接池。

三个团队各自用 HttpClient 封装了一套调用层。半年后的结果是:A 团队的重试是无限次,B 团队的重试是三次但不做退避,C 团队压根没超时。同一个上游服务抖了一下,三家的表现完全不同。后来统一 RPC 框架进来,第一件砍掉的事就是这三套各写各的。

序列化、收发线程、连接池、超时、状态机------这些是「业务之外」的技术劳动。一个团队写一遍是本事,十个团队写十遍是浪费。

第二件:调用方不该关心对端在哪台机器上。

服务实例会扩容、会缩容、会上线、会挂掉。如果调用方靠配置文件写死 IP,那运维每加一台机器,就得推一次配置重启一批应用。RPC 框架配一个**服务发现(Service Discovery)**就解决了:实例启动时把自己注册到注册中心,调用方从本地缓存里拿地址列表,节点变化时自动同步。类比:你不用背下全城每一家面馆的地址,打开外卖软件它自己知道谁在营业。

第三件:把「这是远程」这件事从业务代码里抹掉。

如果不封装,你每调一个远端方法都要手写一段「拼 URL、发请求、判状态码、解析 JSON、异常处理」。业务逻辑被稀释得只剩三成,剩下七成在跟网络打交道。RPC 把这七成收进了框架。

三、为什么用 RPC,而不是 HTTP、MQ 或者 WebSocket

先把一个常见的说法掰正:HTTP 是一种应用层协议,而 RPC 是一类调用模型。「HTTP 和 RPC 谁更强」这个问法本身就不成立,gRPC 的底座就是 HTTP/2。它们真正的分野在建模方式上------HTTP/REST 面向资源,RPC 面向方法。

GET /users/1001 描述的是「我要拿编号 1001 这个资源」;UserService.getUser(1001) 描述的是「我要执行 getUser 这个方法」。你的微服务边界是按业务能力切的,而业务能力天然是「一个个能调用的方法」,不是「一堆资源目录」。这是 RPC 在内部调用里更顺手的第一层原因。

第二层是工程上的具体差别,列成表更清楚:

对比维度 HTTP/REST RPC 框架(gRPC / Dubbo)
建模方式 面向资源:/users/1001 面向方法:UserService.getUser(id)
数据格式 文本 JSON,Header 偏大 二进制 Protobuf / Hessian,报文紧凑
连接方式 HTTP/1.1 多为短连接 + Keep-Alive 长连接 + 多路复用 + 连接池
服务发现 依赖 DNS 或网关转发 内置注册中心,实例上下线自动感知
对外兼容 浏览器、第三方直接可用 需要 SDK 或 IDL,浏览器不直接支持
调试成本 低,curl 就能打 中,得先有 .proto 和生成的桩

**那什么时候该用 HTTP,什么时候该用 RPC?**我的划分很土,但用了几年没出过问题:对外用 HTTP,内部用 RPC,异步用 MQ,推送用 WebSocket。

边界画清楚之后有个附带好处:团队吵「用哪个」的次数明显变少了。给第三方开放的接口,你必须用 HTTP,因为这由对方的接入成本决定,不由你的偏好决定;反过来,订单服务调库存服务这种内部一跳,用 HTTP 就是给自己找麻烦。

有两个判断句我想单独拎出来说,因为它们和网上的主流说法相反:

**「HTTP 更简单,所以内部调用也应该用 HTTP」------这句话只在接口数量少、团队规模小的时候成立。**接口数一旦过百,没有契约约束的 JSON 就会变成一场灾难:字段名靠口口相传,类型靠运行时才发现,改一个字段没人知道影响了谁。

**MQ 和 RPC 不是替代关系,是两种时间观。**RPC 是你现在就要答案,MQ 是你先收下这件事、回头再处理。下单后扣积分这种「必须成功但可以晚一点」的操作,用 RPC 硬扛只会把两个服务绑在一条命上。

四、怎么应用:RPC 藏起来的三件事

这一节是全文我最想让你记住的部分。前面说了半天「像本地调用一样方便」,现在得说硬币的另一面。

第一件:它看起来是本地调用,但它会超时。

本地方法调用只有三种结局:返回、抛异常、死循环。RPC 有第四种:没有结局。请求发出去了,对方可能正在 GC、可能网络在丢包、可能对端进程已经挂了但 TCP 连接还没断。你的线程就那么挂着,一直挂着。

我见过最典型的一次事故:一个查询接口,上游给它的超时是 30 秒,它给下游的也是 30 秒。下游卡住的时候,上游连接池被占满,整个服务雪崩。正确的做法是给超时做预算,而不是给每一跳都配一个「默认值」------上游总共 800ms 的容忍度,那给下游最多 300ms,剩下 500ms 留给自己的处理和网络往返。超时不是越大越安全,越大越危险。

第二件:它看起来只会执行一次,但它可能重试。

「网络超时」这四个字是有歧义的。它可能是请求没发到,也可能是请求发到了、对方处理完了、但响应回来的路上丢了。你这边看到的结果一样,服务端那边的状态却完全不同。所以无脑重试一个「扣库存」的接口,就等着用户投诉吧。

幂等(Idempotent):同一个操作执行一次和执行多次,对系统状态的影响完全相同。类比:电梯的「关门」按钮你按几下都一样,门就是关上------它不因为你多按了三次就关三次。

工程上的落点很简单:调用方生成一个全局唯一的请求 ID 带上去,服务端拿它做去重,处理过的直接返回上次的结果。这件事必须在设计接口的时候就想,事后补的代价通常是改数据库。

第三件:它看起来像函数,但接口设计不该照着本地函数来。

这条最容易踩,因为它不报错。有些团队用 RPC 用得跟没用一样------一个接口叫 doSomething,参数是一个大的 JSON 字符串,返回值也是 JSON 字符串,业务字段全靠字符串里解析。这就麻烦了:你把 RPC 当成了 HTTP 用,那么 RPC 的好处你一样没拿到。

所以 RPC 的接口要切得细一点、语义明确一点:getOrderById 和 listOrdersByUser 就是两个接口,不该为了「少写两个方法」硬合并成一个 queryOrder。接口数量少不等于设计好,语义清楚才算好。

正确的用法长这样------先用 IDL 写契约,再生成调用桩,调用方拿到的是一份有类型的接口:

五、RPC 真正的好处:它把跨进程调用变回一份可检查的合同

知道了坑在哪,再回头看好处,你会发现它们其实是同一件事的两面。

好处一:契约是编译期的。

这是我认为 RPC 最被低估的价值。写完 .proto 或者接口定义,代码生成器会给你一份带类型的桩。字段类型改了、方法签名变了,调用方编译就过不去。**这不是「节省时间」,这是把一整类线上事故提前到了本地。**用裸 HTTP + JSON 的时候,字段对了错了,只有跑起来才知道。

好处二:跨语言是真的跨语言。

服务 A 用 Java,服务 B 用 Go,服务 C 用 Rust,只要它们认同一份 IDL,互相调用就不用为对方改写架构。这一点在收购、合并、多团队并行的时候价值特别明显------你不用劝另一个团队换语言。

好处三:服务治理是内建的,不是外挂的。

负载均衡、失败重试、熔断降级、灰度路由、链路追踪、实例上下线无感------这些东西如果自己基于 HTTP 搭,每一项都是一个独立项目。RPC 框架把它们做成了配置项。

好处四:长连接扛得住高并发。

对内部的短平快调用来说,每次请求都走一遍三次握手是纯浪费。长连接 + 多路复用把这块开销压掉了。这也是为什么秒杀、Feed 流这类场景,内部链路基本清一色是 RPC。

截至 2026 年 10 月,主流选择基本就三个:gRPC (HTTP/2 + Protobuf,多语言、支持流式,跨语言场景首选)、Dubbo (Java 生态里服务治理最完整的,triple 协议在语义上和 gRPC 对齐)、Thrift(老牌,多语言,但生态热度不如前两个)。选型的顺序我建议是:先看团队主语言,再看要不要跨语言,最后才比性能------因为对绝大多数业务来说,这三者的性能差距都不是瓶颈。

可能有人会问:gRPC 底层就是 HTTP/2,那「内部用 RPC 不用 HTTP」这句话是不是自相矛盾?

不矛盾。协议是运输工具,RPC 是说怎么建模、怎么治理。gRPC 用 HTTP/2 当底座,但它给你的是 IDL 契约、代码生成、流式语义和一套治理能力------这些东西 HTTP/2 本身并不提供。你完全可以手写 HTTP/2 请求,只是那样你就得自己把 RPC 框架又实现一遍。
可能有人会问:我们团队接口不到 20 个,是不是没必要上 RPC 框架?

大概率是的。接口少、团队小的时候,裸 HTTP + 一份约定好的 JSON schema 跑得又轻又快,引入 RPC 框架反而增加了构建和调试成本。我的经验阈值是「服务数过 10 个、或者有跨团队调用」------到这个时候,寻址和契约的一致性会开始收税。

六、写在最后:RPC 让你忘记网络,但你不能真的忘

回到开头那个 userService.getUser(id)。

RPC 做的事情,本质上是在工程上给「分布式」这件事做了一次精致的包装:它让你写代码的时候可以假装没有网络。这个假装是有价值的,它让业务逻辑重新变成业务逻辑。

但假装归假装,线上不会陪你演戏。每一次「像本地调用」的舒适,背后都该有一次「它不是本地调用」的清醒------超时写了没有,重试幂等了没有,这个接口的语义是不是切得够清楚。

把这三件事做到,RPC 才真的帮你省事;做不到,它就只是把一次故障从三天前挪到了上线当天。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你们项目里的 RPC 调用,超时和幂等这两件事,是设计的时候就定好的,还是出事之后补上的?

相关推荐
晚安code1 小时前
微服务架构和单体架构的区别:拆之前你得先想清楚这件事
微服务·架构
code_slave(码畜)20 小时前
微服务架构落地:基础服务 —— 报表服务(中篇:元数据、查询引擎、缓存与权限落地)
spring boot·spring cloud·缓存·微服务·架构
code_slave(码畜)21 小时前
微服务架构落地:基础服务 —— 报表服务(AI 集成篇:AI 增强报表能力)
人工智能·spring boot·spring cloud·微服务·架构
code_slave(码畜)1 天前
微服务架构落地:公共中间件层总览——不承载业务,只承载稳定性
spring boot·spring cloud·微服务·中间件·架构
Frank_refuel1 天前
分布式RPC框架实战(二):服务端模块划分与底层设计
分布式·网络协议·rpc
别动我齐刘海1 天前
简历技术栈全面复习总结
c++·人工智能·深度学习·学习·tcp/ip·算法·rpc
code_slave(码畜)1 天前
微服务架构落地:基础服务 —— 报表服务(上篇:定位、边界与整体架构)
java·spring boot·spring cloud·微服务·架构
starzy19901 天前
Flink Rpc通信源码级详解:从RpcService到动态代理的完整调用链路
大数据·rpc·flink
贾斯汀frank2 天前
Spring Boot + K8s + Istio 与 Spring Cloud:微服务治理能力对比
spring cloud·微服务·istio