一句话结论(30s)
HTTP 和 RPC 不是二选一的替代品,而是同一架构里不同层的最优解:对外 API 用 HTTP/REST(通用、可读、生态兼容),内部微服务间用 RPC/gRPC(二进制高性能、强类型、低延迟)。因为本质上这是在「通用性 vs 性能」「人类友好 vs 机器友好」之间做权衡——网关面向五花八门的外部调用方需要通用性,内部调用方受控、调用链深需要性能。
核心原理(2min)
- HTTP(资源导向):URL + Method 表达意图,JSON 文本序列化,自描述性强、生态成熟(CDN/网关/防火墙全支持),适合对外 API 与跨组织通信。
- RPC(方法导向):像调本地函数一样调远程服务,protobuf 二进制序列化 + IDL 代码生成,编译时类型安全,体积比 JSON 小 3-10 倍,适合内部微服务与高性能场景。
- HTTP/2 缩小差距:二进制帧 + 流多路复用 + HPACK 头压缩,让 HTTP 性能接近传统 RPC;gRPC 正是构建在 HTTP/2 之上(用它的传输效率,再叠加 IDL/拦截器/超时重试等 RPC 体系)。
- 选型结论:网关层对外 REST,内部服务间 gRPC,各取所长。
底层深入(5-10min)
两种设计范式
HTTP 和 RPC 代表了服务间通信的两种设计范式:
- HTTP:为 B/S 架构(浏览器/服务器)而生,强调通用性、可读性、生态兼容性。资源导向(URL + Method),任何客户端都能理解
- RPC:为 C/S 架构(客户端/服务器)而生,强调性能、类型安全、开发效率。方法导向(函数调用),像调用本地方法一样调用远程服务
思考:HTTP 和 RPC 的本质区别到底在哪?表面看是「文本 JSON vs 二进制 protobuf」,但更根本的是语义模型不同——HTTP 用「资源」(URL 指向的资源 + Method 表达动作)描述世界,RPC 用「方法」(像调本地函数那样调用一个具名过程)描述世界。所以选型前先问一句:你的调用方更关心「操作了什么资源」(对外、通用),还是更关心「调用了什么能力」(对内、性能)?答案不同,选型就不同。
HTTP:万维网的通用语言
RESTful 哲学
HTTP 最初设计用于传输超文本文档,但在 RESTful 架构的推动下,它成为了一套通用的服务间通信协议:
GET /users/123 → 获取用户信息
POST /users → 创建用户
PUT /users/123 → 完整更新用户
PATCH /users/123 → 部分更新用户
DELETE /users/123 → 删除用户
HTTP 的资源导向设计使其拥有极强的自描述性——仅通过 URL + Method 就能大致理解接口意图。这对大型组织中的跨团队协作和 API 公开至关重要。
HTTP/1.1 的性能局限
队头阻塞:HTTP/1.1 的持久连接(Keep-Alive)允许在单个 TCP 连接上发送多个请求,但响应必须按发送顺序返回(Pipelining)。如果第一个请求处理慢(数据库查询耗时),后续所有响应都被阻塞。浏览器通过 6-8 个并发连接缓解此问题,但增加了连接开销和服务端资源消耗。
文本协议开销:HTTP/1.1 的请求和响应头都是文本格式,每次请求都携带大量重复的元数据(User-Agent、Accept-Encoding、Cookie 等),这些几百字节的头部在大量请求时是显著的开销。
无强类型:HTTP 的请求体和响应体通常是 JSON,缺乏编译时的类型检查。服务端和客户端需要各自手动序列化/反序列化,容易出现字段名拼写错误、类型不匹配等运行时问题。
思考:为什么 HTTP 会有「队头阻塞 + 文本开销」这两个先天短板?因为它设计之初是为传输「超文本文档」服务的,请求互相独立、以可读性为先,从没想过要被高并发的服务间调用反复打。理解了它的出身,就能理解为什么后来会出现 HTTP/2 和 RPC 来「打补丁」——一个在传输层做优化,一个干脆换一种语义模型。
RPC:让远程调用像本地方法
核心思想
RPC(Remote Procedure Call)的核心思想是透明化网络通信。从开发者的视角看,调用远程服务与调用本地函数完全一样:
// 调用本地方法
User user = userService.getUserById(123);
// 调用远程 RPC 服务(看起来一样)
User user = remoteUserService.getUserById(123);
框架在底层自动处理序列化、网络传输、反序列化、异常转换。这个”看起来一样”的承诺非常强大——它极大降低了分布式系统开发的心智负担。
思考:RPC 凭什么能让远程调用「看起来像本地方法」?靠的是契约 + 代码生成:先用 IDL(如 .proto)把接口和方法签名固定下来,框架再据此生成 client/server 两端的 stub,把序列化、网络收发、异常转换全部藏进 stub 里。但「透明」也是一种「伪透明」——网络延迟、超时、失败这些分布式问题并不会因为看起来像本地调用就消失,反而更容易被忽略。
gRPC:基于 Protocol Buffers 的现代 RPC
gRPC 使用 Protocol Buffers(protobuf)作为接口定义语言(IDL)和序列化格式:
// user.proto
service UserService {
rpc GetUserById (GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
int64 user_id = 1;
}
message GetUserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
protobuf 编译后生成强类型的客户端和服务端 stub。调用方拿到的是编译时类型安全的接口,不会有字段名错误或类型不匹配的问题。
protobuf 的二进制编码相比 JSON 具有显著优势:
- 体积更小:字段名不传输(用字段编号 1, 2, 3 替代),整数用变长编码(Varint),通常比 JSON 小 3-10 倍
- 解析更快:二进制格式无需词法分析和字符串转义,直接按字段编号定位
- 向后兼容:新增字段只需加新的编号,旧客户端忽略不认识的编号
思考:为什么 protobuf 比 JSON 小 3-10 倍?因为 JSON 每次都要把字段名(“name”、“email” 这样的字符串)原样传输,而 protobuf 用字段编号(1、2、3)替代字段名,整数还用变长编码(Varint)。字段名再短也是字节,字段编号却可以短到 1 个字节——这就是「机器友好」和「人类友好」在序列化上的具体差别。
Thrift 与 Dubbo
Apache Thrift(Facebook 开源)是另一个流行的 RPC 框架,支持多种传输协议(TCP、HTTP)和序列化格式(Binary、Compact、JSON)。其设计更注重可扩展性——协议栈的每一层(传输、协议、处理器、服务端/客户端)都可以独立替换。
Dubbo(阿里巴巴开源)是 Java 生态中最广泛使用的 RPC 框架之一,为服务治理而设计:自动服务注册与发现(通过 ZooKeeper/Nacos)、负载均衡(随机/轮询/一致性哈希)、失败重试、熔断降级——这些不是 RPC 协议本身的功能,而是 RPC 框架提供的分布式系统基础设施。
HTTP/2.0:缩小差距
HTTP/2.0 引入了多项改进,使其性能接近传统 RPC:
二进制帧层:HTTP/2 将 HTTP 消息分解为二进制帧(HEADERS frame、DATA frame),而非 HTTP/1.1 的文本流。所有帧在单个 TCP 连接上多路复用。
流多路复用:多个请求-响应可以在同一个 TCP 连接上交错传输,每个流有独立的流 ID。接收端按流 ID 将帧组装回完整消息。这解决了 HTTP/1.1 的队头阻塞问题(但 TCP 层面的队头阻塞仍然存在)。
头部压缩(HPACK):HTTP/2 通过 HPACK 算法压缩请求/响应头。静态表预定义了 61 个常见头字段,动态表在连接期间学习重复出现的自定义头。后续请求可以只发送索引引用而非完整头字符串,极大减少了冗余传输。
服务端推送:服务端可以主动向客户端发送资源(如 CSS、JS),无需等待客户端请求。虽然实际使用不如预期广泛,但在受控环境(CDN 边缘节点)中有独特价值。
gRPC + HTTP/2
gRPC 正是构建在 HTTP/2 之上的——它利用了 HTTP/2 的流多路复用、二进制帧和头部压缩。但 gRPC 不仅是一个传输协议,更是一套完整的 RPC 体系(IDL + 代码生成 + 拦截器 + 超时/重试/熔断 + 负载均衡)。HTTP/2 提供了传输层的效率,gRPC 在此之上提供了开发体验层的便利。
微服务架构中的分层策略
现代微服务架构中的典型分层:
外部流量(浏览器、移动 App、第三方)
│
▼
API Gateway(HTTP/REST — 通用、人类可读、生态兼容)
│
▼
内部微服务(gRPC/Thrift — 高性能、强类型、低延迟)
网关层对外使用 HTTP/REST,因为:
- 外部调用方多种多样(浏览器、iOS、Android、第三方 SDK)
- HTTP 生态极其成熟(CDN、负载均衡器、API 管理工具、防火墙全部支持)
- JSON 可读性好,便于调试和文档化(Swagger/OpenAPI)
内部服务间使用 RPC,因为:
- 调用方都是受控的内部服务,不存在生态兼容性问题
- 性能要求高(微服务间的调用链可能很深,每次调用的开销被逐级放大)
- protobuf 的 IDL 强制接口契约,减少跨团队沟通中的歧义
- 服务治理需求(注册发现、负载均衡、熔断)由 RPC 框架统一解决
对比总结
| 维度 | HTTP/REST | gRPC (RPC) |
|---|---|---|
| 接口定义 | OpenAPI/Swagger(文档驱动) | Protocol Buffers(IDL 驱动,代码生成) |
| 序列化 | JSON(文本,人类可读) | Protocol Buffers(二进制,机器最优) |
| 传输协议 | HTTP/1.1 或 HTTP/2 | HTTP/2(原生多路复用) |
| 类型安全 | 运行时检查 | 编译时(代码生成) |
| 浏览器兼容 | 原生支持 | 需 gRPC-Web 代理 |
| 调试友好 | 极高(curl、浏览器 DevTools) | 中等(需 grpcurl 等专用工具) |
| 性能(大负载) | 中等 | 高(二进制序列化 + 多路复用) |
| 生态成熟度 | 极成熟 | 成熟(但不如 HTTP 广泛) |
| 适用场景 | 对外 API、跨组织通信、BFF | 内部微服务、高性能场景、多语言环境 |
思考:回到最初的问题——HTTP 和 RPC 是替代品吗?不是。表格里的每一条差异(类型安全、调试友好、浏览器兼容……)都指向同一个结论:它们在不同的「信任半径」里最优。对外边界要通用、要能被陌生客户端理解,用 HTTP;对内受控环境要性能、要契约,用 RPC。选型 = 判断你站在信任半径的哪一侧。
选择 HTTP 还是 RPC,本质上是在通用性与性能、人类友好与机器友好之间做权衡。在一个成熟的微服务架构中,两者不是互斥的——它们是同一架构中不同层的最优解。
章末提问
Q1:HTTP 和 RPC 的本质区别是什么?
结论先行:本质区别不是「文本 vs 二进制」,而是语义模型不同——HTTP 是资源导向(URL + Method),RPC 是方法导向(具名函数调用)。因为 HTTP 为 B/S 架构设计、面向陌生调用方,强调通用性和可读性;RPC 为 C/S 架构设计、面向受控调用方,强调性能和类型安全。二进制序列化、代码生成只是这一本质在实现层的投影。
Q2:为什么内部微服务用 RPC 而不用 HTTP?
结论先行:因为内部调用方受控、调用链深、性能与契约要求高,RPC 的强类型 IDL + 二进制序列化 + 服务治理正好匹配。因为内部没有「生态兼容」负担(不必讨好陌生客户端),却有「开销逐级放大」的压力(深调用链下每次序列化/解析开销都会被逐级放大);protobuf 的 IDL 还能强制接口契约、减少跨团队沟通歧义。
Q3:HTTP/2 解决了什么?没解决什么?
结论先行:HTTP/2 用二进制帧 + 流多路复用 + HPACK 解决了 HTTP/1.1 的应用层队头阻塞和头部冗余,但没解决 TCP 层的队头阻塞。因为多路复用是在单个 TCP 连接上按流 ID 交错传输帧,一旦 TCP 丢包重传,整条连接上的所有流都要等待;彻底解决要靠 HTTP/3 换用 UDP(QUIC)。
Q4:gRPC 和 HTTP/2 是什么关系?
结论先行:gRPC 是构建在 HTTP/2 之上的一套完整 RPC 体系,HTTP/2 只提供传输层效率。因为 gRPC 复用了 HTTP/2 的流多路复用、二进制帧、头部压缩,又叠加了 IDL + 代码生成 + 拦截器 + 超时/重试/熔断等 RPC 层能力;所以「用 HTTP/2」不等于「有 RPC 体系」。
Q5:选型时怎么在 HTTP 和 RPC 之间做决策?
结论先行:看信任半径——对外边界用 HTTP/REST,内部受控环境用 RPC。因为对外要通用、可读、生态兼容(CDN/网关/防火墙全支持),对内要性能、类型安全、服务治理;成熟架构里两者不互斥,网关层对外 REST、内部服务间 gRPC,各取所长。