摘要:从一次"微服务间调用用HTTP还是RPC"的技术选型争论出发,深度剖析RPC和HTTP的本质区别。通过Dubbo、gRPC、Feign的实现原理对比,以及序列化、传输协议、服务治理的差异分析,揭秘为什么公司内部用RPC、对外接口用HTTP、以及为什么gRPC比HTTP/1.1快5倍。配合时序图展示调用流程,给出不同场景下的选型建议。
💥 翻车现场
周四下午,技术评审会议。
技术总监:"微服务间调用,你们准备用什么?"
哈吉米:"HTTP啊,Spring Cloud全家桶,Feign调用很方便。"
架构师:"我建议用RPC,Dubbo性能更好。"
哈吉米:"RPC和HTTP有啥区别?不都是远程调用吗?"
架构师:"差别大了!性能、序列化、服务治理都不同。"
会后,哈吉米一脸懵。
哈吉米(疑问):"RPC到底是什么?和HTTP有什么区别?什么时候用RPC,什么时候用HTTP?"
南北绿豆和阿西噶阿西来了。
南北绿豆:"RPC和HTTP经常被混淆,其实它们不是一个层面的概念。"
阿西噶阿西:"来,我给你讲清楚。"
🤔 RPC是什么?
RPC的定义
RPC(Remote Procedure Call):远程过程调用
核心思想:
让远程调用像本地调用一样简单
本地调用:
result = userService.getUserById(123);
远程调用(RPC):
result = userService.getUserById(123); // 看起来一样
实际:
1. 客户端序列化参数(123)
2. 网络传输到服务器
3. 服务器反序列化参数
4. 服务器执行方法
5. 服务器序列化返回值
6. 网络传输回客户端
7. 客户端反序列化返回值
南北绿豆:"RPC的目标是:屏蔽网络通信的细节,让远程调用像本地调用。"
RPC的核心组件
RPC框架的组成:
1. 客户端Stub(桩)
- 封装网络调用细节
- 序列化参数
- 发送请求
- 反序列化结果
2. 服务端Skeleton(骨架)
- 接收请求
- 反序列化参数
- 调用本地方法
- 序列化结果
- 返回响应
3. 注册中心
- 服务注册
- 服务发现
4. 网络传输层
- TCP/HTTP
- 序列化协议(Protobuf、Hessian、JSON)
架构图:
客户端 服务端
│ │
│ result = userService.getUser(123) │
│ ↓ │
│ [客户端Stub] │
│ ↓ │
│ 序列化(123) │
│ ↓ │
│ ┌──网络传输──┐ │
│ │ TCP/HTTP │ │
│ └────────────┘ │
│ ↓ │
│ [服务端Skeleton]
│ ↓
│ 反序列化(123)
│ ↓
│ 调用本地方法
│ getUser(123)
│ ↓
│ 序列化结果
│ ┌──网络传输──┐ │
│ │ 返回结果 │ │
│ └────────────┘ │
│ ↓ │
│ 反序列化 │
│ ↓ │
│ 返回result │
🤔 HTTP是什么?
HTTP的定义
HTTP(HyperText Transfer Protocol):超文本传输协议
特点:
1. 应用层协议(第7层)
2. 基于TCP(可靠传输)
3. 无状态(每次请求独立)
4. 文本协议(可读性好)
请求格式:
GET /api/user/123 HTTP/1.1
Host: www.example.com
Content-Type: application/json
响应格式:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "alice"}
HTTP调用示例
// Spring Cloud Feign(基于HTTP)
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/user/{id}")
User getUser(@PathVariable Long id);
}
// 实际HTTP请求
GET http://user-service/api/user/123 HTTP/1.1
Host: 192.168.1.100:8080
Accept: application/json
// HTTP响应
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "alice", "age": 25}
🎯 RPC vs HTTP:核心区别
对比表
| 特性 | RPC | HTTP |
|---|---|---|
| 概念层次 | 调用方式(应用层概念) | 传输协议(应用层协议) |
| 传输协议 | 可以基于TCP、HTTP | HTTP/1.1、HTTP/2 |
| 序列化 | Protobuf、Hessian、Kryo(二进制) | JSON、XML(文本) |
| 性能 | ⭐⭐⭐⭐⭐ 高 | ⭐⭐⭐ 中 |
| 可读性 | 差(二进制) | 好(文本) |
| 服务治理 | ✅ 丰富(Dubbo) | ⚠️ 需要额外组件 |
| 跨语言 | ⚠️ 看框架 | ✅ 天然支持 |
| 浏览器支持 | ❌ | ✅ |
本质区别
阿西噶阿西:"很多人搞混RPC和HTTP,其实它们不是一个层面的概念。"
HTTP:
- 是一种协议
- 规定了数据格式(请求行、请求头、请求体)
- 应用层协议(OSI第7层)
RPC:
- 是一种调用方式
- 可以基于HTTP实现(如gRPC用HTTP/2)
- 也可以基于TCP实现(如Dubbo)
- 是应用层的概念,不是协议
关系:
RPC ⊄ HTTP
RPC ⊅ HTTP
RPC可以用HTTP实现(gRPC),也可以用TCP实现(Dubbo)
图解:
┌──────────────────────────────────┐
│ RPC(调用方式) │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │基于HTTP/2 │ │ 基于TCP │ │
│ │ (gRPC) │ │ (Dubbo) │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ HTTP(传输协议) │
│ │
│ 用于:浏览器、RESTful API、RPC │
└──────────────────────────────────┘
南北绿豆:"所以问'RPC和HTTP的区别',其实是在问'基于TCP的RPC和基于HTTP的REST API的区别'。"
🎯 Dubbo(基于TCP的RPC) vs Feign(基于HTTP的REST)
Dubbo调用流程
// Dubbo接口定义
public interface UserService {
User getUser(Long id);
}
// 服务端实现
@DubboService
public class UserServiceImpl implements UserService {
@Override
public User getUser(Long id) {
return userMapper.selectById(id);
}
}
// 客户端调用
@DubboReference
private UserService userService;
public void test() {
User user = userService.getUser(123L); // 看起来像本地调用
}
底层流程:
sequenceDiagram
participant Client as 客户端
participant Stub as Dubbo Stub
participant TCP as TCP连接
participant Skeleton as Dubbo Skeleton
participant Server as 服务端
Client->>Stub: 1. userService.getUser(123)
Stub->>Stub: 2. 序列化参数<br/>Hessian(123) → 二进制
Stub->>TCP: 3. 发送TCP包<br/>协议:Dubbo私有协议
TCP->>Skeleton: 4. 接收TCP包
Skeleton->>Skeleton: 5. 反序列化参数<br/>二进制 → 123
Skeleton->>Server: 6. 调用本地方法<br/>getUser(123)
Server->>Skeleton: 7. 返回User对象
Skeleton->>Skeleton: 8. 序列化结果<br/>User对象 → 二进制
Skeleton->>TCP: 9. 发送TCP包
TCP->>Stub: 10. 接收TCP包
Stub->>Stub: 11. 反序列化结果<br/>二进制 → User对象
Stub->>Client: 12. 返回User对象
特点:
- ✅ 基于TCP(长连接,性能好)
- ✅ 二进制序列化(体积小)
- ✅ 服务治理丰富(负载均衡、降级、限流)
Feign调用流程
// Feign接口定义
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/user/{id}")
User getUser(@PathVariable Long id);
}
// 客户端调用
@Autowired
private UserClient userClient;
public void test() {
User user = userClient.getUser(123L);
}
底层流程:
客户端调用:
userClient.getUser(123)
↓
Feign生成HTTP请求:
GET http://user-service/api/user/123 HTTP/1.1
Accept: application/json
↓
从Nacos获取user-service的实例列表
↓
负载均衡选择一个实例(192.168.1.100:8080)
↓
发送HTTP请求
↓
服务端Controller接收:
@GetMapping("/api/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
↓
返回JSON:
{"id": 123, "name": "alice"}
↓
Feign反序列化JSON → User对象
↓
返回给客户端
特点:
- ✅ 基于HTTP(通用性好)
- ✅ JSON序列化(可读性好)
- ⚠️ 文本协议(体积大,性能差)
🎯 性能对比
测试场景
调用:
User user = service.getUser(123L);
返回对象:
{
"id": 123,
"username": "alice",
"phone": "13800138000",
"email": "alice@example.com",
"age": 25
}
序列化体积对比:
| 序列化方式 | 体积 | 说明 |
|---|---|---|
| JSON | 156字节 | 可读性好,体积大 |
| Protobuf | 32字节 | 二进制,体积小 |
| Hessian | 45字节 | 二进制 |
性能测试(10000次调用):
| 方案 | 协议 | 序列化 | 总耗时 | QPS |
|---|---|---|---|---|
| Feign(HTTP/1.1) | HTTP/1.1 | JSON | 8.2秒 | 1219 |
| gRPC(HTTP/2) | HTTP/2 | Protobuf | 1.5秒 | 6666 |
| Dubbo(TCP) | TCP | Hessian | 1.2秒 | 8333 |
性能差距:
- Dubbo比Feign快6.8倍
- gRPC比Feign快5.5倍
🎯 为什么RPC性能更好?
原因1:长连接 vs 短连接
HTTP/1.1(Feign):
每次请求:
1. 建立TCP连接(三次握手,1-2ms)
2. 发送HTTP请求
3. 接收HTTP响应
4. 关闭连接(四次挥手,1-2ms)
或者Keep-Alive(长连接):
1. 复用连接(不关闭)
2. 但有超时时间(通常60秒)
Dubbo(TCP长连接):
启动时建立连接:
1. 建立TCP连接(三次握手)
2. 连接保持(心跳保活)
每次请求:
1. 直接发送数据(复用连接)
2. 接收响应
优势:
- 节省握手/挥手时间
- 连接复用率高
原因2:二进制 vs 文本
HTTP(JSON):
// 请求
POST /api/user/create HTTP/1.1
Host: user-service
Content-Type: application/json
Content-Length: 156
{"username":"alice","phone":"13800138000","email":"alice@example.com"}
// 响应
HTTP/1.1 200 OK
Content-Type: application/json
{"code":0,"data":{"id":123,"username":"alice"}}
体积:
请求:约200字节(HTTP头 + JSON)
响应:约150字节
Dubbo(Hessian):
// 请求(二进制,简化表示)
[Header: 16字节][Body: 45字节]
体积:
请求:61字节
响应:约60字节
体积对比:
Dubbo / HTTP = 60 / 200 = 30%
传输对比:
传输100万次请求:
HTTP(JSON):
请求大小:200字节
总传输:200字节 × 100万 = 200MB
Dubbo(Hessian):
请求大小:60字节
总传输:60字节 × 100万 = 60MB
网络传输节省:(200 - 60) / 200 = 70%
原因3:服务治理
Dubbo的服务治理:
内置功能:
1. 负载均衡(随机、轮询、一致性哈希)
2. 服务降级(失败后降级)
3. 限流(TPS限制)
4. 熔断(失败率过高,熔断)
5. 重试(失败自动重试)
6. 超时控制
7. 版本控制(灰度发布)
8. 分组(环境隔离)
HTTP(Feign):
需要额外组件:
1. 负载均衡:Ribbon
2. 熔断:Hystrix/Resilience4j
3. 限流:Sentinel
4. 服务发现:Eureka/Nacos
问题:
- 组件多,复杂度高
- 需要分别配置
🎯 gRPC:基于HTTP/2的RPC
gRPC的优势
gRPC = RPC + HTTP/2 + Protobuf
特点:
1. 基于HTTP/2(多路复用、二进制帧)
2. Protobuf序列化(体积小)
3. 流式传输(支持双向流)
4. 跨语言(Protocol Buffers跨语言)
gRPC示例
// user.proto
syntax = "proto3";
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
message UserRequest {
int64 id = 1;
}
message UserResponse {
int64 id = 1;
string username = 2;
string phone = 3;
}
客户端调用:
// gRPC客户端
UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
UserRequest request = UserRequest.newBuilder()
.setId(123)
.build();
UserResponse response = stub.getUser(request);
性能:
HTTP/1.1 vs HTTP/2(gRPC):
HTTP/1.1:
- 队头阻塞(一个请求一个TCP连接)
- 文本协议(体积大)
HTTP/2(gRPC):
- 多路复用(一个连接多个请求)
- 二进制帧(体积小)
- 头部压缩(HPACK)
性能提升:5-10倍
🎯 何时用RPC,何时用HTTP?
选型建议
南北绿豆:"根据场景选择。"
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 微服务内部调用 | RPC(Dubbo、gRPC) | 性能好,服务治理强 |
| 对外开放API | HTTP(RESTful) | 通用性好,浏览器支持 |
| 移动端调用 | HTTP(RESTful) | 通用性好,易调试 |
| 跨语言调用 | gRPC | 跨语言支持好 |
| 实时通信 | gRPC(流式) | 支持双向流 |
| 简单场景 | HTTP | 实现简单 |
实际案例
阿里巴巴:
内部:Dubbo(RPC)
- 订单服务 ↔ 库存服务(Dubbo)
- 支付服务 ↔ 账户服务(Dubbo)
对外:HTTP(RESTful)
- 移动App → 开放API(HTTP)
- 第三方接入 → 开放API(HTTP)
Google:
内部:gRPC
- 微服务间调用(gRPC)
对外:HTTP
- Google Maps API(HTTP)
- YouTube API(HTTP)
🎯 完整调用流程对比
Dubbo调用流程
sequenceDiagram
participant Client as 客户端应用
participant Registry as Nacos注册中心
participant Provider as 服务提供者
Note over Client,Provider: 启动阶段
Provider->>Registry: 1. 注册服务<br/>userService, ip:port
Client->>Registry: 2. 订阅服务<br/>userService
Registry->>Client: 3. 返回提供者列表
Note over Client,Provider: 调用阶段
Client->>Client: 4. 本地调用<br/>userService.getUser(123)
Client->>Client: 5. 序列化参数(Hessian)
Client->>Provider: 6. TCP发送(长连接复用)
Provider->>Provider: 7. 反序列化参数
Provider->>Provider: 8. 执行方法
Provider->>Provider: 9. 序列化结果
Provider->>Client: 10. TCP返回
Client->>Client: 11. 反序列化结果
Client->>Client: 12. 返回User对象
Note over Client,Provider: 全程二进制,无HTTP头
Feign调用流程
sequenceDiagram
participant Client as 客户端应用
participant Feign as Feign客户端
participant Ribbon as Ribbon负载均衡
participant HTTP as HTTP请求
participant Controller as 服务端Controller
Client->>Feign: 1. userClient.getUser(123)
Feign->>Ribbon: 2. 获取user-service实例列表
Ribbon->>Ribbon: 3. 负载均衡选择实例
Ribbon->>Feign: 4. 返回:192.168.1.100:8080
Feign->>Feign: 5. 构造HTTP请求<br/>GET /api/user/123
Feign->>HTTP: 6. 发送HTTP请求
HTTP->>Controller: 7. HTTP请求到达
Controller->>Controller: 8. 执行方法
Controller->>Controller: 9. 序列化JSON
Controller->>HTTP: 10. HTTP响应
HTTP->>Feign: 11. 接收响应
Feign->>Feign: 12. 反序列化JSON
Feign->>Client: 13. 返回User对象
Note over Client,Controller: 文本协议,有HTTP头
🎓 面试标准答案
题目:RPC和HTTP有什么区别?
答案:
本质区别:
- RPC:调用方式(远程过程调用)
- HTTP:传输协议(应用层协议)
RPC可以基于HTTP实现(如gRPC),也可以基于TCP实现(如Dubbo)
通常说的"RPC vs HTTP",是指:
- 基于TCP的RPC(如Dubbo)
- 基于HTTP的REST API(如Feign)
具体区别:
| 特性 | RPC(Dubbo) | HTTP(Feign) |
|---|---|---|
| 传输协议 | TCP | HTTP/1.1 |
| 连接方式 | 长连接 | 短连接或Keep-Alive |
| 序列化 | Hessian/Protobuf(二进制) | JSON(文本) |
| 体积 | 小(60字节) | 大(200字节) |
| 性能 | 高(8000 QPS) | 中(1200 QPS) |
| 服务治理 | 丰富(内置) | 需要额外组件 |
| 跨语言 | 看框架 | 天然支持 |
| 调试 | 难(二进制) | 易(文本可读) |
选型建议:
- 内部微服务:RPC(Dubbo、gRPC)
- 对外API:HTTP(RESTful)
- 跨语言:gRPC
- 简单场景:HTTP
🎉 结束语
晚上9点,技术选型会议继续。
哈吉米:"我理解了!RPC和HTTP不是对立的,RPC可以基于HTTP实现。"
南北绿豆:"对,通常说的RPC vs HTTP,其实是Dubbo(基于TCP)vs Feign(基于HTTP)的对比。"
阿西噶阿西:"记住:内部调用用RPC性能好,对外接口用HTTP通用性好。"
哈吉米:"还有gRPC兼顾了两者的优点:基于HTTP/2,但用Protobuf序列化,性能接近Dubbo。"
南北绿豆:"对,没有银弹,根据场景选方案!"
记忆口诀:
RPC是调用方式,HTTP是传输协议
RPC可基于TCP,也可基于HTTP
Dubbo基于TCP长连接,Feign基于HTTP短连接
二进制序列化体积小,文本协议可读性好
内部调用用RPC,对外接口用HTTP