深入 HTTP/2 传输原理¶
gRPC 之所以没有自研全新的私有传输协议,而是坚定地选择 HTTP/2(RFC 7540) 作为底层传输层,是因为 HTTP/2 拥有标准化的多路复用、天然穿透企业级防火墙/代理(通过 80/443 端口)、成熟的 TLS 加密以及健全的生态支持。
1. HTTP/2 核心概念分层¶
在理解 gRPC 传输机制前,必须厘清 HTTP/2 的四个核心实体:
graph TD
Conn["1. Connection (物理 TCP 连接)"] --> Stream1["Stream 1 (逻辑双向流)"]
Conn --> Stream3["Stream 3 (逻辑双向流)"]
Conn --> Stream5["Stream 5 (逻辑双向流)"]
Stream1 --> MsgReq["Message (请求消息)"]
Stream1 --> MsgResp["Message (响应消息)"]
MsgReq --> FrameH1["HEADERS 帧"]
MsgReq --> FrameD1["DATA 帧 (分片 1)"]
MsgReq --> FrameD2["DATA 帧 (分片 2)"]
- 连接(Connection):客户端与服务端之间建立的单条物理 TCP 连接。
- 流(Stream):在单个连接内并发运行的双向字节流通道。每个 Stream 拥有唯一的 31 位整数 ID:
- 客户端发起的 Stream ID 为奇数(1, 3, 5, 7...)。
- 服务端发起的 Stream ID 为偶数(2, 4, 6...,在 gRPC 中少用)。
- 消息(Message):逻辑上的一个完整 RPC 请求或响应,由一个或多个帧组成。
- 帧(Frame):HTTP/2 通信的最小独立单元。
2. HTTP/2 帧结构与关键帧类型¶
所有 HTTP/2 帧都拥有一个固定的 9 字节通用帧头:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
- Length (24 bits):帧负载(Payload)的字节长度(默认最大不超过 16,384 字节)。
- Type (8 bits):帧类型。
- Flags (8 bits):控制标志位(如
END_STREAM标识当前流传输结束;END_HEADERS标识头部接收完毕)。 - Stream ID (31 bits):指定该帧所属的虚拟流编号。
gRPC 中关键的 HTTP/2 帧类型¶
| 帧类型 (Type) | 值 | gRPC 中的作用 |
|---|---|---|
HEADERS |
0x1 |
启动 RPC 调用;传递请求路径、Metadata、以及作为 Trailers 传递状态码 |
DATA |
0x0 |
承载实际经过 Protobuf 序列化后的二进制 Payload |
SETTINGS |
0x4 |
建立连接时协商参数(如 MAX_CONCURRENT_STREAMS、初始流控窗口等) |
WINDOW_UPDATE |
0x8 |
HTTP/2 流量控制机制,告知对端可以继续发送多少字节 |
RST_STREAM |
0x3 |
异常终止或显式取消流(当调用 cancel() 或发生 Deadline 超时时发送) |
PING |
0x6 |
测量往返时延(RTT)及 Keepalive 心跳探活 |
GOAWAY |
0x7 |
优雅停机宣告,通知对端不要再在该连接上新建流 |
3. HPACK 头部压缩算法¶
在微服务频繁调用中,HTTP 头部中的 authorization、content-type 等字段往往高度重复。HTTP/2 使用 HPACK(RFC 7541) 压缩算法:
graph LR
H["Header 键值对"] --> StaticTable["静态字典表 (预置 61 个高频头部)"]
H --> DynamicTable["动态字典表 (连接内自适应学习缓存)"]
H --> Huffman["霍夫曼编码 (进一步压缩未命中字符)"]
- 静态表(Static Table):预置了 61 个常用的 HTTP 键值对(例如
:method: POST在表中编号为 3)。传输时仅需发送索引数值 3(1 个字节),而无需传输 12 字节的字符串。 - 动态表(Dynamic Table):在连接生命周期内,双方根据已经传递过的自定义 Metadata 动态构建索引。后续相同的 Token 或自定义 Header 仅需传递一个微小的索引编号。
4. 为什么 HTTP/2 的负载均衡需要特殊考量?¶
传统的 L4(四层网络负载均衡,如 LVS 或普通 TCP 模式的 NGINX)只在 TCP 连接建立时分发流量:
graph TD
Client["客户端"] --> L4["四层负载均衡器 (TCP Proxy)"]
L4 -->|长连接一旦建立便不再切换| PodA["服务后端 Pod A (承受 100% 流量)"]
L4 -.->|无流量| PodB["服务后端 Pod B (闲置)"]
L4 -.->|无流量| PodC["服务后端 Pod C (闲置)"]
由于 gRPC 在单个 TCP 连接上可以不间断地复用发送成千上万个 RPC,若采用 L4 代理,所有后续请求将全部被倾泻在同一个固定后端节点上,造成集群负载极不均衡。
因此,gRPC 在微服务生产治理中,必须采用: 1. L7 七层负载均衡代理(如 Envoy、Traefik、NGINX gRPC 模块),代理能够解析 HTTP/2 帧并按 RPC 流(Stream)粒度分发; 2. 客户端负载均衡(Client-Side Load Balancing):客户端直接解析后端多个 Pod IP,在本地构建连接池轮询发起 Stream。