摘要:从一次"微服务改造后性能下降50%"的架构问题出发,深度剖析HTTP在内部服务调用中的性能瓶颈。通过HTTP文本协议的开销、连接复用的局限、以及服务治理的缺失对比,揭秘为什么公司内部用RPC而不是HTTP、为什么Dubbo比Spring Cloud Feign快5倍、以及阿里为什么从HSF到Dubbo再到gRPC的演进。配合抓包对比协议开销,给出微服务架构的通信协议选型指南。
💥 翻车现场
2024年3月,哈吉米的公司开始微服务改造。
技术总监:"用Spring Cloud全家桶,Feign做服务调用。"
哈吉米:"好的!"
改造前(单体应用):
// 本地调用
@Service
public class OrderService {
@Autowired
private StockService stockService;
public void createOrder(Order order) {
// 本地方法调用
stockService.decrease(order.getProductId());
}
}
// 性能:
接口响应时间:50ms
其中库存扣减:1ms(本地调用)
改造后(微服务):
// 远程调用(Feign + HTTP)
@FeignClient(name = "stock-service")
public interface StockClient {
@PostMapping("/stock/decrease")
Result decrease(@RequestParam Long productId);
}
@Service
public class OrderService {
@Autowired
private StockClient stockClient;
public void createOrder(Order order) {
// 远程HTTP调用
stockClient.decrease(order.getProductId());
}
}
// 性能:
接口响应时间:120ms
其中库存扣减:70ms(远程HTTP调用)← 慢了70倍
性能对比:
| 调用方式 | 库存扣减耗时 | 接口总耗时 | 性能下降 |
|---|---|---|---|
| 本地调用 | 1ms | 50ms | - |
| HTTP远程调用 | 70ms | 120ms | 下降58% |
哈吉米:"卧槽,微服务化后性能下降了一半?"
架构师看了看性能报告。
架构师:"HTTP做内部服务调用,开销太大了。改用Dubbo试试。"
改用Dubbo后:
// Dubbo调用
@DubboReference
private StockService stockService;
public void createOrder(Order order) {
stockService.decrease(order.getProductId());
}
// 性能:
接口响应时间:65ms
其中库存扣减:15ms(Dubbo调用)← 比HTTP快4.6倍
哈吉米:"Dubbo比Feign快这么多?HTTP为什么慢?"
南北绿豆和阿西噶阿西来了。
南北绿豆:"HTTP是为浏览器设计的,不是为内部服务调用设计的。"
阿西噶阿西:"来,我给你讲讲为什么内部调用需要RPC。"
🤔 HTTP的5个性能问题
问题1:文本协议开销大
HTTP请求:
POST /stock/decrease HTTP/1.1
Host: stock-service
Content-Type: application/json
User-Agent: Apache-HttpClient/4.5.13
Accept: application/json
Content-Length: 28
Connection: keep-alive
{"productId":1001,"num":1}
总大小:约300字节
- HTTP头:约250字节
- 数据:28字节
Dubbo请求(二进制):
[Dubbo协议头: 16字节]
[方法名: "decrease"]
[参数: 1001, 1]
总大小:约50字节
- 协议头:16字节
- 数据:34字节(Hessian序列化)
开销对比:
HTTP:300字节
Dubbo:50字节
HTTP的开销是Dubbo的6倍
南北绿豆:"HTTP的文本协议头占了大部分空间,对于内部调用,这些头信息大多是浪费。"
问题2:JSON序列化性能差
序列化性能对比(100万次):
| 序列化方式 | 序列化耗时 | 反序列化耗时 | 总耗时 |
|---|---|---|---|
| JSON | 2.5秒 | 3.2秒 | 5.7秒 |
| Hessian | 0.8秒 | 1.2秒 | 2.0秒 |
| Protobuf | 0.5秒 | 0.7秒 | 1.2秒 |
| Kryo | 0.4秒 | 0.6秒 | 1.0秒 |
性能差距:JSON比Protobuf慢4.75倍
体积对比(User对象):
User user = new User(123L, "alice", "13800138000", "alice@example.com", 25);
// JSON
{"id":123,"username":"alice","phone":"13800138000","email":"alice@example.com","age":25}
大小:92字节
// Hessian
[0x4f, 0x00, 0x7b, ...]
大小:45字节
// Protobuf
[0x08, 0x7b, 0x12, ...]
大小:32字节
体积对比:
JSON : Hessian : Protobuf = 92 : 45 : 32
问题3:连接管理效率低
HTTP/1.1 Keep-Alive:
特点:
- 连接复用(不是每次请求都建连接)
- 但有超时时间(通常60秒)
- 超时后关闭,下次重建
问题:
- 频繁创建/销毁连接
- 连接数不可控
Dubbo长连接:
特点:
- 启动时建立连接池
- 连接持久保持(心跳保活)
- 不会超时关闭
好处:
- 连接复用率高
- 不频繁创建/销毁
- 性能稳定
连接数对比:
100个订单服务实例 → 10个库存服务实例
HTTP(每个请求1个连接):
峰值连接数:100 × 10 = 1000个(短时间大量连接)
Dubbo(连接池):
连接数:100 × 2 = 200个(每个实例2个长连接)
连接数减少:80%
问题4:缺少服务治理
HTTP(Feign)需要额外组件:
功能需求:
1. 负载均衡 → 需要Ribbon
2. 熔断降级 → 需要Hystrix/Resilience4j
3. 限流 → 需要Sentinel
4. 重试 → 需要Spring Retry
5. 超时控制 → 需要配置
6. 服务发现 → 需要Eureka/Nacos
问题:
- 组件多(学习成本高)
- 配置复杂(每个组件单独配置)
- 维护成本高
Dubbo内置服务治理:
内置功能:
1. 负载均衡(随机、轮询、一致性哈希、最少活跃)
2. 集群容错(失败重试、快速失败、安全失败)
3. 限流降级(TPS限流、并发限流)
4. 超时控制(方法级超时)
5. 异步调用
6. 泛化调用
7. 结果缓存
8. 参数验证
好处:
- 一站式解决方案
- 配置简单
- 功能丰富
配置对比:
# Feign配置(需要多个组件)
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
ribbon:
ConnectTimeout: 1000
ReadTimeout: 3000
# Dubbo配置(一个配置搞定)
dubbo:
consumer:
timeout: 3000
retries: 2
loadbalance: random
cluster: failover
问题5:协议解析开销
HTTP解析:
// HTTP响应解析(文本协议)
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: 50\r\n\r\n" +
"{\"code\":0,\"data\":{\"id\":123}}";
// 解析步骤:
1. 解析状态行(HTTP/1.1 200 OK)
2. 解析响应头(逐行解析,遇到\r\n分割)
3. 解析JSON(反序列化)
开销:
- 字符串分割
- 正则匹配
- JSON解析
Dubbo解析(二进制):
// Dubbo响应(二进制)
[魔数: 2字节][消息类型: 1字节][状态: 1字节][请求ID: 8字节][数据长度: 4字节][数据]
// 解析步骤:
1. 读取固定位置的字段(偏移量固定)
2. 反序列化数据(Hessian)
开销:
- 字节读取(快)
- 二进制反序列化
解析性能对比:
解析100万次响应:
HTTP + JSON:
- 解析HTTP头:1.5秒
- 解析JSON:3.0秒
- 总计:4.5秒
Dubbo + Hessian:
- 解析协议头:0.3秒
- 解析Hessian:0.8秒
- 总计:1.1秒
性能差距:4倍
🎯 HTTP适合什么场景?
HTTP的优势
阿西噶阿西:"HTTP也不是一无是处,它有自己的优势。"
优势1:通用性好
HTTP:
- 所有语言都支持
- 浏览器原生支持
- 跨平台、跨语言
Dubbo:
- 需要客户端SDK
- 浏览器不支持
- 跨语言支持有限
优势2:可读性好
HTTP:
文本协议,可读性强
curl http://api.example.com/user/123
{"id":123,"name":"alice"} ← 一眼能看懂
Dubbo:
二进制协议
tcpdump抓包:
0x2f 0x3a 0x8b 0x9c ... ← 看不懂
优势3:易调试
HTTP:
- Postman测试
- curl命令测试
- 浏览器F12查看
- 抓包工具(Wireshark、Charles)
Dubbo:
- 需要专门工具(dubbo-admin)
- 需要泛化调用
- 调试复杂
优势4:防火墙友好
HTTP:
- 端口:80(HTTP)、443(HTTPS)
- 防火墙通常放行
- 公司网络、云服务商都支持
Dubbo:
- 自定义端口(如20880)
- 可能被防火墙拦截
- 需要额外配置
🎯 什么时候用HTTP?什么时候用RPC?
选型指南
南北绿豆:"根据场景选择。"
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 公司内部微服务 | RPC(Dubbo、gRPC) | 性能好、服务治理强 |
| 对外开放API | HTTP(RESTful) | 通用性好、易接入 |
| 移动端调用 | HTTP | 浏览器/App原生支持 |
| 跨语言调用 | gRPC(RPC的一种) | Protobuf跨语言 |
| Web前端调用 | HTTP | 浏览器只支持HTTP |
| 内网穿透 | HTTP | 防火墙友好 |
| 高性能场景 | RPC | 低延迟、高吞吐 |
| 简单场景 | HTTP | 实现简单 |
大厂实践
阿里巴巴:
内部:
- HSF(早期)
- Dubbo(开源)
- 现在:Dubbo + gRPC
对外:
- 开放平台API:HTTP(RESTful)
- 淘宝开放平台、支付宝开放平台
Google:
内部:
- gRPC(基于HTTP/2的RPC)
对外:
- Google Maps API:HTTP
- YouTube API:HTTP
腾讯:
内部:
- tRPC(自研RPC框架)
对外:
- 微信开放平台:HTTP
- 腾讯云API:HTTP
规律:
内部调用:
RPC(性能优先)
对外接口:
HTTP(通用性优先)
🎯 性能数据对比
真实压测(10000次调用)
测试场景:
// 接口
User getUser(Long id);
// 返回对象
User(id=123, username="alice", phone="13800138000", ...)
压测结果:
| 方案 | 协议 | 序列化 | 总耗时 | QPS | 平均响应 |
|---|---|---|---|---|---|
| Feign(HTTP/1.1) | HTTP/1.1 | JSON | 8.2秒 | 1219 | 0.82ms |
| Feign(HTTP/2) | HTTP/2 | JSON | 3.5秒 | 2857 | 0.35ms |
| gRPC | HTTP/2 | Protobuf | 1.5秒 | 6666 | 0.15ms |
| Dubbo(Hessian) | TCP | Hessian | 1.2秒 | 8333 | 0.12ms |
| Dubbo(Protobuf) | TCP | Protobuf | 1.0秒 | 10000 | 0.10ms |
| 本地调用 | - | - | 0.01秒 | 1000000 | 0.00001ms |
性能排名:
本地调用 > Dubbo(Protobuf) > Dubbo(Hessian) > gRPC > Feign(HTTP/2) > Feign(HTTP/1.1)
Dubbo比Feign快:8333 / 1219 = 6.8倍
开销分析
Feign(HTTP/1.1 + JSON)的开销:
单次调用的耗时分解:
1. 序列化参数(JSON):0.03ms
2. 构造HTTP请求(拼接字符串):0.05ms
3. 建立TCP连接(或复用):0.02ms(复用)
4. 发送HTTP请求:0.10ms
5. 网络传输:0.20ms
6. 接收HTTP响应:0.10ms
7. 解析HTTP头:0.05ms
8. 反序列化JSON:0.05ms
9. 返回结果:0.02ms
总计:0.62ms
主要开销:
- JSON序列化/反序列化:0.08ms
- HTTP头处理:0.10ms
- 网络传输:0.20ms
Dubbo(TCP + Hessian)的开销:
单次调用的耗时分解:
1. 序列化参数(Hessian):0.01ms
2. 构造Dubbo请求(二进制):0.01ms
3. 复用长连接:0ms
4. 发送请求:0.05ms
5. 网络传输:0.20ms(和HTTP一样)
6. 接收响应:0.05ms
7. 解析协议头:0.01ms
8. 反序列化Hessian:0.02ms
9. 返回结果:0.01ms
总计:0.36ms
主要开销:
- 网络传输:0.20ms(占比56%)
对比:
Feign:0.62ms
Dubbo:0.36ms
差距:0.26ms
差在哪?
- JSON序列化慢:0.08ms vs 0.03ms
- HTTP头处理慢:0.10ms vs 0.02ms
- 总计多出:0.15ms
看起来不多?
但:
微服务调用链路:
订单服务 → 库存服务 → 支付服务 → 账户服务 → 通知服务
调用5次:
Feign:0.62ms × 5 = 3.1ms
Dubbo:0.36ms × 5 = 1.8ms
差距:1.3ms(42%)
如果QPS=10000:
每秒浪费:1.3ms × 10000 = 13秒(13个CPU秒)
阿西噶阿西:"在微服务调用链路中,这些微小的差距会被放大!"
🎯 HTTP能优化吗?
优化方案
方案1:HTTP/2
HTTP/2的改进:
1. 二进制帧(不是文本)
2. 多路复用(一个连接多个请求)
3. 头部压缩(HPACK)
性能提升:
HTTP/1.1:1219 QPS
HTTP/2:2857 QPS
提升:2.3倍
方案2:gRPC(HTTP/2 + Protobuf)
gRPC = HTTP/2 + Protobuf
优势:
1. HTTP/2的速度
2. Protobuf的体积
3. 跨语言支持
性能:
gRPC:6666 QPS
接近Dubbo的性能(8333 QPS)
🎓 面试标准答案
题目:有HTTP为什么还要RPC?
答案:
HTTP的问题(内部调用场景):
-
文本协议开销大
- HTTP头250字节,数据28字节
- 开销是数据的9倍
-
JSON序列化慢
- 比二进制序列化慢4-5倍
-
连接管理效率低
- Keep-Alive有超时
- 频繁创建/关闭连接
-
缺少服务治理
- 需要额外组件(Ribbon、Hystrix)
- 配置复杂
-
性能差
- QPS:1219(HTTP/1.1)
- 比RPC慢5-7倍
RPC的优势(内部调用场景):
-
二进制协议
- 协议头16字节
- 开销小
-
高效序列化
- Hessian、Protobuf
- 快4-5倍
-
长连接池
- 连接持久保持
- 不频繁创建/销毁
-
内置服务治理
- 负载均衡、熔断、限流
- 一站式方案
-
性能好
- QPS:8333(Dubbo)
- 比HTTP快6-7倍
选型建议:
- 内部微服务:RPC(Dubbo、gRPC)
- 对外API:HTTP(RESTful)
- 前端调用:HTTP
- 跨语言:gRPC
混合使用:
- 内部:Dubbo(高性能)
- 对外:HTTP(通用性)
- 前端:HTTP
🎉 结束语
一周后,哈吉米把核心服务改成了Dubbo。
哈吉米:"内部服务用Dubbo后,性能提升了6倍,接口响应时间从120ms降到65ms!"
南北绿豆:"对,HTTP适合对外接口,RPC适合内部调用,各有所长。"
阿西噶阿西:"记住:不是HTTP不好,而是HTTP不是为内部服务调用设计的。"
哈吉米:"还有gRPC兼顾了性能和跨语言,是个不错的选择。"
南北绿豆:"对,没有银弹,根据场景选方案,内外有别!"
记忆口诀:
HTTP文本协议开销大,JSON序列化性能差
连接复用效率低,服务治理需组件
RPC二进制协议快,长连接池性能好
内置服务治理强,内部调用RPC选
对外接口用HTTP,内外有别是关键