
平时接到最多的问题之一就是“接口用 HTTP 不就行了吗搞个 RPC 是不是脱裤子放屁”这话乍一听没毛病毕竟 HTTP 是应用层事实标准网页、小程序、开放平台到处都是 HTTP 接口跨语言、跨设备、浏览器直接调调试工具一抓一大把。但真到微服务大规模落地或者你负责的系统每天要处理几千万次服务间调用时HTTP 和 RPC 的差距会迅速从“概念之争”变成“线上事故”。HTTP 解决的是“资源传输”的问题RPC 解决的是“方法调用”的问题两者看着重叠其实出发点完全不同。这篇文章就把这件事彻底讲透顺便聊聊实际项目里我在 RPC 和 HTTP 之间做选择、迁移、排障时踩过的一些坑适合后端开发、架构师和所有正在纠结“要不要上 RPC”的团队参考。1. HTTP 和 RPC根本不是一道二选一的选择题1.1 一个看的是“资源”一个看的是“动作”HTTP 协议从诞生那天起设计目标就是“传输超文本文档”。后来 REST 风格被广泛接受大家把接口抽象成资源用 GET 拿资源、POST 创建资源、PUT 覆盖资源、DELETE 删资源这套思路在浏览器生态和开放 API 里非常自然。你在地址栏输入一个 URL本质是在定位一个资源然后对这个资源做增删改查。浏览器、CDN、缓存、HTTP 状态码全都是围绕资源设计出来的。RPC 的出发点完全反过来RPC 全称是 Remote Procedure Call目标是让远程调用尽量像本地函数调用一样自然。你不需要去想“我请求的是哪个资源”你只需要关心“我调用了哪个方法、传了什么参数、拿回什么结果”。比如你调用getUserById(id)这就不是一个对/users/{id}资源的 GET 请求而是一个明确的“执行动作”。动作可能涉及多个资源的协同修改也可能没有任何资源语义比如healthCheck()、rebuildIndex()。用 HTTP 硬去表达“动作”就得自己约定路径和动词时间一长接口风格会非常割裂。1.2 RPC 要解决的是“本地调用远程方法”这一整套问题RPC 真正的难点不只是“发个请求”而是让一个进程像调用本地函数那样调用另一个进程里的函数。这背后至少需要解决几个问题服务怎么发现、方法怎么寻址、参数怎么序列化、响应怎么反序列化、连接怎么管理、超时怎么处理、失败怎么重试、单点怎么负载均衡。这些功能如果全写在业务代码里系统会变成一坨互相纠缠的“胶水逻辑”。所以你去看成熟的 RPC 框架比如 gRPC、Apache Thrift、Dubbo它们不是简单定义了一个协议格式而是附带了一整套服务治理机制。服务注册与发现、健康检查、负载均衡、熔断降级、链路追踪框架层面全给你留了接口。HTTP 本身只负责“把报文从一个点送到另一个点”至于这个点后面有没有服务列表、哪个实例健康、要不要降级HTTP 协议一概不管。1.3 一张表看懂两者核心差异维度HTTP/RESTRPC设计视角面向资源天然适合 Web 生态面向方法天然适合服务间逻辑调用典型协议HTTP/1.1、HTTP/2文本头文本/JSON 体自定义或二进制协议如 Protobuf、Thrift、Hessian序列化以 JSON/XML 为主人类可读体积偏大二进制为主体积小、解析快但肉眼不可读连接模型HTTP/1.1 靠 Keep-Alive 复用HTTP/2 有二进制分帧和多路复用RPC 框架普遍自己维护连接池、长连接、多路复用服务治理基本没有内置需要网关、注册中心、监控组件拼装框架普遍内置服务注册发现、负载均衡、超时重试、熔断跨语言能力极强任何语言都能手写 HTTP 请求取决于 IDL 和生成代码生态支持也还行但更依赖框架调试与工具链浏览器、Postman、curl 全都有一般需要专用 CLI、控制台或可视化面板这张表不是告诉你 RPC 一定高级而是说明两者的成本结构不一样。HTTP 的代价是“通用但笨重”RPC 的代价是“高效但有绑定”。你做选择的时候本质上是在选择一套成本结构。2. 为什么只有 HTTP 不够用2.1 HTTP 协议“天生”不是为了远程函数调用设计的HTTP 有一套非常固定的语义请求行、请求头、空行、报文主体。它把“状态”放在动词和状态码里把“内容”放在 body 里。这套东西服务人类阅读没问题服务机器之间的高频方法调用就有很多别扭的地方。举个很常见的例子服务 A 想调用服务 B 的batchCreateOrder这个动作本质是一次方法调用但用 HTTP 表达时你得先争论是 POST/orders/batch还是 PUT/orders要不要自定义X-Operation头错误码是放到 HTTP status code 还是 body 里的code字段。讨论着讨论着团队里每个人维护的接口风格都不一样了。RPC 直接在 IDL 里定义rpc batchCreateOrder(BatchCreateOrderRequest) returns (BatchCreateOrderResponse)语义清晰也不会有人问你该用什么动词。另一个问题是方法签名。HTTP 对参数类型几乎没有约束传入 JSON 的字段类型、是否必填、嵌套结构全靠接口文档约束。一旦文档落后于代码联调时就会出现“你传 string 我收 int”这种低级错误。而 IDL 是强约束的生成代码后编译期就能发现类型不匹配这在大团队协作里价值非常大。2.2 序列化和性能的差距会被高并发放大很多从 HTTP 迁移到 RPC 的团队都能肉眼看到性能提升核心差异主要在序列化和连接管理上。HTTP 接口最常见的序列化格式是 JSON。JSON 人类可读但解析开销大体积也大。一个字段名userName在 JSON 里要占 8 个字节在 Protobuf 里可能只占一个字段编号的几个 bit。高并发场景下几万个字段名在 GC 压力和时间消耗上的差异是惊人的。我在一个压测里对比过同样一屏订单数据JSON 序列化后的 body 有 12KBProtobuf 序列化后只有 3.4KB这个差距在百兆网卡的机器群集里会直接反映成吞吐量差异。HTTP/1.1 还有一个问题一个连接同时只能处理一个请求队头阻塞严重。虽然 HTTP/2 用二进制分帧和多路复用解决了传输层队头阻塞但很多企业内网服务还是 HTTP/1.1Nginx、网关默认配置也没完全开启 HTTP/2。RPC 框架干脆不走这条路直接基于 TCP 或 QUIC 自己控制连接池和多路复用连接利用率更高帧头更小延迟更低。这也是为什么一些延迟敏感场景比如证券行情、实时协同、物联网设备指令下发内部调用普遍选 RPC。物联网方向的 ThingBoard RPC 就是一个典型例子平台要通过 RPC 通道远程给设备下发命令如果用 HTTP 轮询实时性和效率都很差用长连接 RPC 是更合理的选择。2.3 服务治理能力需要额外组件补齐如果你的服务只有两三个HTTP 裸调完全够用。但当你拆出几十个微服务每个服务多副本部署问题就来了调用方怎么知道被调方的实例 IP 列表某个实例正在发版怎么不把流量发过去某个实例超时变多要不要降级这些问题用 HTTP 也能解决但要自己拼组件。需要一个注册中心、一个负载均衡器、一套健康检查系统、一套熔断限流库、一个分布式链路追踪系统。好消息是这些组件都能找到开源方案坏消息是你要自己去集成、升级、排障而且每换一个语言栈这些集成工作基本要重来一遍。RPC 框架把这些能力内置在框架里或者通过统一 SDK 提供。比如 Dubbo 和 Spring Cloud Alibaba 的集成里注册中心、配置中心、熔断限流都是开箱即用的。gRPC 虽然没有内置注册中心但生态里有 etcd、Consul、Kubernetes 的 resolver你只是写几行配置而不是自己造一套服务发现系统。这就是为什么微服务架构里 RPC 成了默认选项不是因为 HTTP 不能做而是用 RPC 省下的工程成本太明显。2.4 但 HTTP 仍然不可替代的场景说完 RPC 的优势也得说公道话。HTTP 的跨语言力、生态普适性和可调试性是 RPC 很难替代的。你要给第三方开发者开放 API就不可能要求对方引入你的 Protobuf 文件、生成一堆 stub 代码。他们只需要一份 OpenAPI/Swagger 文档拿着 Postman 就能调起来。浏览器、小程序、移动端 App 的服务端通信也基本都是 HTTP/HTTPS。虽然可以用 gRPC-Web 和 HTTP/2 做克制但生态成熟度、缓存机制、CDN 加速、HTTP 状态码语义还是 HTTP 最顺手。团队内部如果大多是“页面-接口”模式长期以资源交换为主强行上 RPC 纯粹是给自己加戏。所以准确的说法是HTTP 负责对外和对边缘RPC 负责对内和对高密度调用。两者不是替代关系而是分层关系。3. 什么时候选 RPC、什么时候留在 HTTP3.1 对外开放 API留在 HTTP只要是面向外部开发者的 Open API、面向 Webhook 的推送接口、面向 SPA 或小程序的 BFF 层老老实实留在 HTTP。原因很简单对方不一定认识你的服务发现机制也不愿意安装你的 SDK。外部 API 最重要的是稳定、可调试、跨语言。HTTP JSON OpenAPI 文档是当前生态里兼容性最好的一套组合。哪怕性能差一点外部 API 的 QPS 通常要比内部调用低一两个数量级而且慢一点也远好于对方调不了。真遇到极端性能需求可以在 HTTP 层做缓存、做 CDN、做网关优化完全没有必要让外部用户去适配你的 RPC 序列化格式。3.2 内部服务互联考虑 RPC内部服务之间的调用尤其是核心链路上的高频调用是我推荐优先考虑 RPC 的地方。这类调用有三个特点第一调用方和被调方都在同一个技术体系内有能力引入统一 SDK第二调用量和延迟敏感度都很高用户下单、支付、库存检查、价格计算多几次网络往返都会直接影响响应时间第三需要精细治理比如版本灰度、接口级限流、熔断降级这些用 RPC 框架实现比用 HTTP 裸调配合网关要自然得多。举个例子我维护过一个交易系统早期内部调用全是 HTTP JSON压测时 3000 QPS 就把网关的 CPU 打到 80%body 解析和网络开销占了大量成本。后来把核心链路切到基于 Protobuf 的 RPC 框架同样机器配置跑到 8000 QPS 才出现瓶颈而且接口的 IDL 契约让服务之间联调时少了大量“字段对不上”的扯皮。3.3 混合架构让 RPC 和 HTTP 各司其职很多成熟团队并不是二选一而是做成两层内部服务之间用 RPC暴露给外部或前端的统一走 HTTP 网关。这个模式在 e-commerce、金融、IoT 领域都很常见。内部用 gRPC 或 Dubbo网关层负责把外部 HTTP 请求转换成内部 RPC 调用再转发到对应服务。这样做的好处是内外隔离。网关层做鉴权、流控、协议转换、敏感信息脱敏内部服务不用关心外部 API 的兼容性专心把业务逻辑做好。等哪天要改内部接口只要保持 IDL 兼容网关层同步更新即可外部用户的接口契约可以不变。如果你的团队已经拆了微服务但对内部 HTTP 调用的治理越来越痛这个混合模式是渐进演进的最优解。4. 实操视角从 HTTP 切换到 RPC 的关键环节4.1 定义一个 RPC 接口先想清契约从 HTTP 迁移到 RPC最直接的接触点是“写 IDL”。拿 gRPC 举例你要先写一个 proto 文件syntax proto3; package order.v1; service OrderService { rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse); rpc GetOrder(GetOrderRequest) returns (GetOrderResponse); } message CreateOrderRequest { string order_id 1; string user_id 2; repeated Item items 3; } message Item { string sku_id 1; int32 quantity 2; }这段定义里order.v1是包名1、2、3是字段编号。第一次写 proto 的人很容易忽略字段编号的重要性。字段编号一旦确定就尽量不要改因为它是线上传输格式的一部分改编号会让旧版本进程解析新数据时错乱。新增字段要用新编号并且设置合理的默认值这样老进程遇到未知字段也不会崩。HTTP 接口写个PostMapping就完事了RPC 接口却要把请求和响应都建模得清楚一开始可能觉得麻烦但长期看是省事。接口即文档生成出来的代码就是强类型约束比一堆 Markdown 文档靠谱得多。4.2 序列化与连接复用决定了你省下来的性能有多少切到 RPC 后连接管理基本都在框架内部完成。以 gRPC 为例它默认基于 HTTP/2底层有连接池和流复用能力。不知道这点的人会把 gRPC 当成“又一个 HTTP”在每个请求里新建连接性能当然好不起来。真正要注意的是不要混用连接池和线程池的资源水位。一次 RPC 调用会占用一个连接上的流同时占用一个工作线程。如果业务方法里存在同步阻塞比如调数据库用了 JDBC 同步连接RPC 线程会被卡住连接池没满但线程池满了就会出现“连接正常、请求排队”的现象。这里和 HTTP 里 Keep-Alive 的道理一样连接复用能省掉大量 TCP 握手但不是让你无限制调大连接数连接数过大反而会打满机器的文件描述符和内存。序列化方面我常用的经验是对性能不敏感但对调试敏感的场景比如 BFF 层的内部接口先用 JSON 过度没问题对核心链路上的高频请求直接上 Protobuf。Protobuf 解析速度快但也意味着返回的是二进制线上排障时要靠反射工具或解析插件不然你抓个包全是乱码。4.3 超时、重试和熔断设置不当比 HTTP 死得更惨很多团队迁移 RPC 后遇到的第一个线上事故就是超时设置不合理。没有超时下游一卡上游全卡超时太短慢查询频繁发起重试直接把下游打挂。HTTP 接口时代大家习惯用网关统一配置 timeout而在 RPC 框架里超时控制散落在各个服务之间必须每个调用方都设。我见过一个真实报错cannot finish rpc call in 30 seconds。调用方设的超时是 30 秒下游一个报表查询在高峰期要跑 40 秒每次调用都超时但调用方没有配置失败降级导致流量反复重试数据库连接被占满。后来把报表查询拆成预计算超时时间从 30 秒降到 3 秒加上了失败快速返回系统才稳下来。超时不是越长越好而是要基于你的 P99 耗时来设定通常取 P99 耗时乘以 3 到 5至少留足网络抖动和 GC 停顿的空间。再提一个很容易被忽略的点重试一定要考虑幂等性。HTTP 时代大家还会想一想“前端重试会不会重复下单”到了 RPC 调用里框架默认支持重试反而更容易忽略。你调用的下游如果是个扣库存接口重试 n 次就可能超卖 n-1 次。所以凡涉及资金、库存、状态变更的 RPC 调用要么业务侧做幂等要么关闭自动重试只在创建操作上手动重试。熔断降级的配置同样重要。RPC 框架一般都有熔断器你配置的是“错误比例超过某个阈值就开启熔断”前提是你得有一个合理的着色规则。这个阈值要根据业务容忍度定不能拍脑袋写 50%。有一次我把阈值调到 60%结果下游故障时还是有一半流量打到它身上下游恢复变慢熔断出现抖动。后来改成错误率超过 30% 且持续 10 秒就熔断再给熔断时间设 30 秒系统才平滑降级。4.4 一次典型排障30 秒超时和网关 502排障是 RPC 日常里最消耗人的一部分。我挑两个高频问题讲讲你以后大概率也会遇到。第一个是超时丢请求。现象是日志里出现大量deadline exceeded或cannot finish rpc call in 30 seconds但下游日志显示请求已经处理成功。这种“响应丢失”通常是连接被回收或代理层写超时导致的。排查时先看调用链的耗时分位再看代理层和负载均衡的连接 idle 时间。如果连接静态复用时间过长后端有连接回收机制中间一次请求刚好卡在连接关闭窗口就会丢响应。解决方式是缩短空闲连接发现间隔、在客户端加上连接探活或者对关键接口关闭连接空闲回收。第二个是网关 502。你可能会看到类似unexpected status 502 bad gateway: unknown error的日志甚至 URL 还是http://127.0.0.1:1572这种本地转发地址。这通常不是下游服务真的挂了而是代理进程或本地转发层工作异常。排查路径先绕过本地代理直接访问下游看是否正常如果能通再看代理进程的并发连接数和错误日志如果代理本身没有报错就要查是不是转发时把请求体或关键 header 丢掉了。这里有个很隐蔽的坑参数校验错误会在网关层被误报成 502。我遇到过一种场景上游暴露的是 HTTP 接口内部代理层负责把 HTTP 请求转换成 RPC 调用结果代理层少传了一个上游要求的字段下游返回 HTTP 400代理层没有正确透传反而把它包装成 502。所以看到 502 时别只看状态码要去看完整响应体里的upstream_status和cause那才是真正的根因。像reasoning_content必须回传这种字段在代理层很容易被过滤掉如果下游有严格校验就会直接报错。处理办法是确保代理层的字段映射和下游协议保持一致任何透传字段都不能凭感觉裁剪。5. 我的经验引入 RPC 前要想清楚的几件事5.1 RPC 不是银弹别为赶时髦引入它我见过有团队只有三个服务调用量一天才几万次却上了完整的 RPC 框架加注册中心加配置中心结果光维护 SDK 版本就累得半死。RPC 框架的学习成本、SDK 升级成本、协议排障成本在一开始会显著高于 HTTP。如果你的服务数量少、调用量不高、团队也没精力运营治理体系坚持 HTTP OpenAPI 是更理性的选择。RPC 真正的收益来自规模化后的治理能力和性能提升规模没到收益就是纸面收益。5.2 接口设计要像做长期契约一样封版RPC 和 HTTP 最大的差别之一就是 RPC 接口升级非常暴露兼容性风险。HTTP 新增一个字段老客户端通常会忽略但 RPC 里如果 IDL 里删掉一个字段、改一个字段编号、或改了类型老进程反序列化时可能直接报错。所以 RPC 接口一定要有版本意识包名或服务名加 v1/v2字段只增不改禁止复用废弃编号。就算你只在内部用也要把“前后兼容”作为接口改动的底线。5.3 可观测性必须一开始就做切到 RPC 以后不能再靠“打日志看时间”来排障了。最好引入链路追踪把每次 RPC 调用的 traceId、spanId、耗时、状态码串起来。没有链路追踪的 RPC 环境线上一个问题可能要翻十几个服务日志才能定位到是哪个环节超时。RPC 框架本身会提供拦截器就在拦截器里把指标接出去一开始可能觉得多写几行代码等真出问题的时候它就是你救命的工具。我个人现在做技术选型时会先问三个问题这个接口是给谁调的调用量能到什么级别团队有没有精力维护一套独立协议体系三个问题回答清楚HTTP 还是 RPC 的答案基本就出来了。回想早期在系统里闭着眼睛用 HTTP 调内部接口的日子真没少为超时和字段对齐吃苦反过来我也见过不少团队在服务还很小时就建了一堆 RPC 基础设施最后光处理框架兼容就耗掉大把精力。所以别问“哪个更好”要问“你现在缺的是哪一层能力”——缺性能和多路复用就认真上 RPC缺生态兼容和调试便利就踏实留在 HTTP。这个选择没有标准答案但想清楚这几层你至少不会选错。