有HTTP为什么要有RPC?

摘要:从一次"微服务改造后性能下降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

性能对比

调用方式库存扣减耗时接口总耗时性能下降
本地调用1ms50ms-
HTTP远程调用70ms120ms下降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万次):

序列化方式序列化耗时反序列化耗时总耗时
JSON2.5秒3.2秒5.7秒
Hessian0.8秒1.2秒2.0秒
Protobuf0.5秒0.7秒1.2秒
Kryo0.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)性能好、服务治理强
对外开放APIHTTP(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.1JSON8.2秒12190.82ms
Feign(HTTP/2)HTTP/2JSON3.5秒28570.35ms
gRPCHTTP/2Protobuf1.5秒66660.15ms
Dubbo(Hessian)TCPHessian1.2秒83330.12ms
Dubbo(Protobuf)TCPProtobuf1.0秒100000.10ms
本地调用--0.01秒10000000.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的问题(内部调用场景):

  1. 文本协议开销大

    • HTTP头250字节,数据28字节
    • 开销是数据的9倍
  2. JSON序列化慢

    • 比二进制序列化慢4-5倍
  3. 连接管理效率低

    • Keep-Alive有超时
    • 频繁创建/关闭连接
  4. 缺少服务治理

    • 需要额外组件(Ribbon、Hystrix)
    • 配置复杂
  5. 性能差

    • QPS:1219(HTTP/1.1)
    • 比RPC慢5-7倍

RPC的优势(内部调用场景):

  1. 二进制协议

    • 协议头16字节
    • 开销小
  2. 高效序列化

    • Hessian、Protobuf
    • 快4-5倍
  3. 长连接池

    • 连接持久保持
    • 不频繁创建/销毁
  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,内外有别是关键