This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/gRPC/2. gRPC 核心篇/07-HTTP2 传输原理.md
T
2026-05-11 19:02:38 +08:00

18 KiB
Raw Blame History

tags, create time
tags create time
gRPC
HTTP/2
Protocol
Go
Networking
2026-05-11 16:42

HTTP/2 传输原理

概述

gRPC 跑在 HTTP/2 之上,这意味着你天然享受 HTTP/2 带来的所有性能红利:多路复用、头部压缩、服务器推送。但也带来了一些新的心智负担——原来用 TCP 协议栈就能搞定的事,现在又多了一层帧(frame)和流(stream)的概念。理解底层原理才能调试那些"偶发性超时""连接无故断开"的问题。

[!question] 为什么 gRPC 不用 HTTP/1.1? HTTP/1.1 的串行阻塞模型在面对高并发微服务时效率太低——每增加一个并发都要付出一次 TCP 握手开销。而 HTTP/2 在一个 TCP 连接上就能搞定任意数量的并发请求。但代价是你得适应全新的二进制协议层。

[!tip] 先搞清楚这层关系

应用层:Protobuf 消息 → 序列化为字节流
gRPC 层:把字节流包装成 request/response/stream
HTTP/2 层:把 gRPC 数据切分成 frame, multiplex 到 stream 上
TCP 层:可靠的字节流传输

gRPC = Protobuf wire format + HTTP/2 transport + gRPC semantics

HTTP/2 vs HTTP/1.1 对比

特性 HTTP/1.1 HTTP/2
连接数 每主机通常 6 个 任意数量
传输格式 纯文本 二进制
多路复用 ❌ (head-of-line blocking) ✅
头部压缩 ❌ ✅ HPACK
服务器推送 ❌ ✅ PushPromise
头部顺序 明文逐行发送 header block fragments

**核心差异一句话:**HTTP/2 将一切变成了二进制帧(frame),用帧的组合来表达请求、响应和元数据。

[!note] 为什么二进制更好? 纯文本协议(如 HTTP/1.1)需要靠 \r\n 分隔,解析容易出错且浪费带宽。二进制协议用 length + type 字段精确定位每个单元,解析更快也更健壮。代价是——你不能再直接用浏览器看明文了,必须用专门的抓包工具。

HTTP/2 连接建立过程

Connection Preface(连接前置声明)

HTTP/2 要求在真正的数据交换之前,双方先发一段 "preface" 来确认对方支持 HTTP/2:

// Client Preface(客户端必须首先发送,固定字符串)
// PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
// SETTINGS frame (length = 0)

// Server Preface(服务端确认后回复同样长度的 SETTINGS frame)
// SETTINGS frame (length = 0)

这不是什么代码层面的操作——gRPC 客户端内部自动处理。但你可以在 Wireshark 中看到这个 handshake 过程:Client 发 PRI * HTTP/2.0,Server 回复 SETTINGS ACK,然后双方才开始交换业务数据。

[!warning] 常见坑 某些老旧代理(如旧版 Squid / Nginx < 1.3.10)不支持 ALPN 或不认 Preface,会导致 HTTP/2 降级为 HTTP/1.1。遇到偶发的 "connection reset" 时可以检查一下中间件版本。

TLS Handshake + ALPN

如果是 HTTPS 连接(gRPC 生产环境的标配),完整的握手流程是:

sequenceDiagram
    participant C as Client
    participant T as TCP/TLS
    participant S as Server

    Note over C,S: Step 1 — TCP 三次握手
    C->>T: SYN
    T->>S: SYN-ACK
    S->>C: ACK

    Note over C,T: Step 2 — TLS 1.3 握手
    C->>T: ClientHello (ALPN: h2)
    T->>S: ServerHello (selected: h2)
    S->>C: finished

    Note over C,S: Step 3 — HTTP/2 Preface
    C->>S: ClientPreface + SETTINGS
    S->>C: SETTINGS ACK + ServerSettings

    Note over C,S: Step 4 — 开始 multiplexing
    C->>S: HEADERS + DATA (Stream 1)
    C->>S: HEADERS + DATA (Stream 3)
    S->>C: HEADERS + DATA (Stream 2)

关键点是 ALPN(Application-Layer Protocol Negotiation):TLS 握手的 ClientHello 中会附带支持的协议列表(h2 / http/1.1),服务端从中选择一个并在 ServerHello 中返回。如果协商失败,就不会走 HTTP/2。

HTTP/2 帧(Frame)详解

gRPC 的数据以 frame 为单位在连接上传输。每种 frame 有特定的类型码和控制语义:

Frame Type Code 作用 gRPC 中的场景
DATA 0x0 实际载荷 Request / Response body
HEADERS 0x1 头信息(含 HTTP/2 header block) HTTP headers + gRPC metadata
PRIORITY 0x2 设置 stream 优先级 gRPC 不使用
RST_STREAM 0x3 异常终止 Client/Server 主动中断流
SETTINGS 0x4 协商参数 握手阶段交换 max_concurrent_streams 等
PUSH_PROMISE 0x5 服务器推送 gRPC 不使用
PING 0x6 保活探测 keepalive ping
GOAWAY 0x7 优雅关闭 Server 准备停机
WINDOW_UPDATE 0x8 流量控制 调整接收窗口大小
CONTINUATION 0x9 续传 header block 头部太长时的分片传输

每个 frame 的结构固定为 9 字节头部 + 有效载荷:

+-----------------------------------------------+
| Length (24 bits)                               |
+---------------+---------------+---------------+
| Type (8 bits)  | R(1 bit)    | Stream ID(31 bits) |
+---------------+---------------+---------------+
| Payload (... bytes)                            |
+-----------------------------------------------+
  • Length: payload 长度,最大 2^24 - 1 ≈ 16MB
  • Type: frame 类型(上述表格中的 Type Code)
  • R: reserved bit,必须为 0
  • Stream ID: 所属 stream 的 ID,0 表示 connection-level frame
  • Payload: 随类型不同含义各异
flowchart TB
    subgraph Connection ["HTTP/2 Connection"]
        direction TB
        Streams --> S1["Stream 1<br/>HEADERS → DATA → HEADERS → DATA"]
        Streams --> S2["Stream 2<br/>HEADERS → DATA"]
        Streams --> SN["Stream N<br/>HEADERS → DATA"]
    end

    ConnLevel -.-> Settings["SETTINGS / PING / GOAWAY<br/>(Stream ID = 0)"]

    style ConnLevel fill:#A0AEC0,color:#fff
    style Settings fill:#ED8936,color:#fff
    style S1 fill:#00B6BC,color:#fff
    style S2 fill:#4FC08D,color:#fff
    style SN fill:#FFD43B

关键概念:

  • 每个 connection 上有多个 stream,每个 stream 独立收发 data
  • 所有的 frame 都属于某个 stream(除了 connection-level 的 SETTINGS/GOAWAY/PING/WINDOW_UPDATE)
  • gRPC 只用了其中一小部分 frame 类型——它把复杂的 protocol 封装在了内部

Stream 与 Multiplexing

这是 HTTP/2 最重要的特性,也是 gRPC 高性能的核心原因。

机制:

  • 一个 TCP 连接上可以有多个 stream
  • 每个 stream 有唯一的 stream ID(奇数 = client 发起,偶数 = server 发起,ID 从 1 开始递增)
  • stream 之间互不干扰——解决了 HTTP/1.1 的队头阻塞(head-of-line blocking)

Stream ID 分配规则

flowchart LR
    Client["Client"] --- TCP[("TCP Connection")]
    TCP --- Server["Server"]

    subgraph Streams ["Stream IDs"]
        S1["1<br/>Client→Server<br/>Unary call"]
        S3["3<br/>Client→Server<br/>Another call"]
        S5["5<br/>Client→Server<br/>Streaming"]
        S2["2<br/>Server→Client<br/>Response to #1"]
        S4["4<br/>Server→Client<br/>Response to #3"]
        S6["6<br/>Server→Client<br/>Push error"]
    end

    Client --> S1
    Client --> S3
    Client --> S5
    Server --> S2
    Server --> S4
    Server --> S6

    style S1 fill:#00B6BC,color:#fff
    style S3 fill:#00B6BC,color:#fff
    style S5 fill:#00B6BC,color:#fff
    style S2 fill:#4FC08D,color:#fff
    style S4 fill:#4FC08D,color:#fff
    style S6 fill:#4FC08D,color:#fff

要点:

  • Stream ID 严格递增,Client 永远用奇数,Server 永远用偶数
  • 即使一个 stream 已经完成(发送了 END_STREAM flag),它的 ID 也不会回收重用
  • 理论上单条连接最多可以有 2^31 个 stream(受 Stream ID 字段限制)

代码示例

同一个 conn 上可以并发创建多个 stream,无需额外 TCP 连接:

conn, _ := grpc.Dial("localhost:50051", ...) // 只有一个 TCP 连接
client := pb.NewUserServiceClient(conn)

// 这三个 call 在同一个连接的不同 stream 上并行执行
go client.GetUser(ctx1, &pb.GetUserRequest{Id: 1}) // → Stream 1
go client.GetUser(ctx2, &pb.GetUserRequest{Id: 2}) // → Stream 3
go client.GetUser(ctx3, &pb.GetUserRequest{Id: 3}) // → Stream 5

HPACK 头部压缩

HTTP/2 使用 HPACK 算法对头部进行压缩,主要靠两个手段:

  1. 静态字典 — RFC 预定义了 61 个常用 header(如 :method, :path, content-type),直接通过索引引用
  2. 动态表 — 运行时新增的 key-value 对会被缓存,后续请求只需引用索引号
  3. Huffman 编码 — 对无法查表的字符串做变长编码

gRPC 强制要求 hpack table size >= 4096 bytes。好处是 gRPC 的请求头部通常只有几十个字节,相比 HTTP/1.1 每次都要带完整的 User-Agent/Content-Type 等大很多倍。

flowchart LR
    A["原始 Header:<br/>:method=POST<br/>:path=/user.v1.UserService/CreateUser<br/>content-type=application/grpc<br/>grpc-timeout=30s"] --> B["HPACK Encoder"]

    B -->|"索引引用"| D[":method → :POST (static table idx 2)"]
    B -->|"动态表插入"| E[":path → full string<br/>(dynamic table idx 1)"]
    B -->|"Huffman 编码"| F["grpc-timeout=30s<br/>(Huffman: 5 bytes)"]

    D --> G["Header Block Fragment<br/>(~15 bytes)"]
    E --> G
    F --> G

    style A fill:#FFD43B
    style B fill:#00B6BC,color:#fff
    style G fill:#4FC08D,color:#fff

[!example] HPACK 的实际压缩效果

// HTTP/1.1 头部(每次完整发送,约 200+ bytes)
POST /user.v1.UserService/CreateUser HTTP/1.1
Host: localhost:50051
Content-Type: application/grpc
Grpc-Timeout: 30s
User-Agent: grpc-go/1.50.0
Te: trailers

// HTTP/2 + HPACK(连续多次调用,压缩后可能只剩十几字节)
HEADERS: {idx 2} {dynamic-table: :path=/user.v1.UserService/CreateUser} {huffman: 30s}
DATA: <protobuf payload>

注意第二次调用时,:method=POST 和 content-type=application/grpc 都可以直接从 static table 引用——不需要再发送这些字符串。

Flow Control 流量控制

HTTP/2 有两层 flow control,各自独立工作:

层级 范围 默认窗口 控制方式
Connection Level 整个 TCP 连接 65535 bytes WINDOW_UPDATE frame
Stream Level 单个 stream 继承 connection level WINDOW_UPDATE frame

当接收方缓冲区快满时,发送方会收到 WINDOW_UPDATE 被"堵住"——这就是 HTTP/2 的 backpressure。gRPC 在此基础上又加了一层自己的 flow control:

gRPC 配置项 说明 默认值
MaxReceiveMessageSize 单次可接收的最大消息 4 MB
MaxSendMessageSize 单次可发送的最大消息 math.MaxInt32

[!warning] 这是一个常见的坑! 当你的 protobuf message 超过 4MB 时,你会看到类似这样的错误:

grpc: received message larger than max (5242880 vs 4194304) on XXX

修复方式:在 dial 或 server 选项中设置更大的 limit。

grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(10*1024*1024))

注意:这里设置的单位是 bytes。设太大也有风险——对方可能发一个巨型 payload 打爆你的内存。

与 HTTP/2 Flow Control 的关系

很多人会把两层 flow control 混淆。简单理解:

  • HTTP/2 Window 管的是「网络缓冲区」——防止发送方压垮接收方的 TCP 队列
  • gRPC Message Size 管的是「应用层内存」——防止 protobuf unmarshal 时 OOM

两层都配好了才是真正的安全。

[!tip] 调优建议

  • HTTP/2 Window:大多数场景不需要手动改,除非出现大量 WINDOW_UPDATE 延迟
  • MaxReceiveMessageSize:按业务需要设定,但不要设为无上限;配合上游 LB 的 payload limit 一起考虑

连接生命周期

stateDiagram-v2
    [*] --> Idle
    Idle --> Connecting: TCP connect
    Connecting --> Connected: OK
    Connecting --> ErrorFailed: Failed
    ErrorFailed --> Closed

    Connected --> TLSEncrypted: TLS + ALPN (if secure)
    Connected --> Plaintext: Insecure mode

    TLSEncrypted --> SettingsSent: Send HTTP/2 Preface + SETTINGS
    Plaintext --> SettingsSent: Send HTTP/2 Preface + SETTINGS

    SettingsSent --> Ready: Receive SETTINGS ACK
    SettingsSent --> ErrorSettingsTimeout: Timeout
    ErrorSettingsTimeout --> Closed

    Ready --> Active: Create Streams
    Active --> HalfClose: Stream done (END_STREAM)
    HalfClose --> GoAwayReceived: Server sends GOAWAY
    GoAwayReceived --> DrainPending: Wait for active streams
    DrainPending --> Closed: All pending done

    state Active {
        StreamOpen --> Sending
        StreamOpen --> Receiving
        Sending --> Done
        Receiving --> Done
    }

    note right of ErrorFailed
        DNS 解析失败
        TCP connect timeout
        TLS cert invalid
    end note

    note right of ErrorSettingsTimeout
        对方未在规定时间内
        回复 SETTINGS ACK
    end note

各阶段要点:

  1. Connecting — TCP 三次握手。如果目标地址不可达或被防火墙拦截,会在这里报 connection refused
  2. TLS + ALPN — 对于 secure 连接,先用 TLS 加密并协商出 h2 协议。证书过期或 host mismatch 会在这一步失败
  3. Settings exchange — 双方交换 window size、max concurrent streams 等参数。这是 HTTP/2 的正式握手点
  4. Ready — 可以开始创建 stream 了
  5. Active — 正常业务阶段,stream 可随时创建和销毁
  6. GOAWAY — 服务端通知即将关闭(如滚动重启)。已创建的 stream 还能继续完成,新 stream 不能再用这条连接
  7. Closed — 连接彻底关闭,下次调用时 gRPC 会自动重连(reconnect policy)

[!note] GOAWAY vs RST_STREAM 的区别

  • GOAWAY: connection-level,告诉对方"这条连接以后不能新建 stream 了",属于优雅关闭
  • RST_STREAM: stream-level,仅终止单个 stream,不影响同连接上的其他 stream

对开发者的实际影响

1. 连接数管理

不需要手动连接池。gRPC 的 grpc.ClientConn 已经做了连接复用和自动重建:

// 一个 conn 对象就够了,内部自动管理连接数和重连
conn, _ := grpc.Dial("target", grpc.WithTransportCredentials(...))
// 所有 client share this conn
client1 := pb.NewService1Client(conn)
client2 := pb.NewService2Client(conn)

[!tip] 连接池误区 很多从 HTTP/1.1 转过来的开发者会习惯性建连接池。但在 gRPC 里一个 Dial 就够——gRPC 内部维护了一个连接管理器,会根据负载情况自动增减连接。

2. Keepalive 配置

生产环境务必调优 keepalive 参数,否则可能被负载均衡器或 K8s ingress 切断连接:

grpc.WithKeepaliveParams(keepalive.ClientParameters{
    Time:                10 * time.Second,
    Timeout:             5 * time.Second,
    PermitWithoutStream: true,  // 即使没有 active stream 也发 ping
})

grpc.KeepaliveParams(keepalive.ServerParameters{
    Time:    10 * time.Second, // PING 间隔
    Timeout: 20 * time.Second, // 无响应则断开
})

[!warning] PermitWithoutStream 的作用 当这个值为 false 时(默认),如果当前没有任何活跃的 RPC(比如空闲等待期),gRPC 不会发 keepalive ping——此时中间设备恰好会杀掉空闲连接。生产环境建议设为 true。

生产环境的推荐配置取决于你的中间件策略:

组件 典型 idle timeout 建议 keepalive Time
AWS ALB 60s 25s
Nginx proxy_pass 75s (proxy_read_timeout) 30s
K8s kube-proxy (iptables) 根据 conntrack 25s
Cloud Load Balancer varies 20–30s

3. MaxMessageSize

遇到 "message too large" 错误时,第一反应应该是检查这个限制,而不是怀疑代码逻辑。

4. 调试技巧

工具 用途 常用命令
grpcurl CLI 工具,支持直接调用 gRPC 方法 grpcurl -plaintext -d '{"id": 42}' localhost:50051 user.v1.UserService.GetUser
nghttp HTTP/2 抓包分析,能看到 frame 级别细节 nghttp -v http://localhost:50051
Wireshark 原始帧级别的诊断 filter: tcp.port == 50051 && http2
GRPC_VERBOSITY=DEBUG GRPC_TRACE=all Go 内置的 verbose trace,打印内部事件 export GRPC_VERBOSITY=DEBUG && export GRPC_TRACE=http2,transport,subchannel
# 快速测试一个 gRPC endpoint
grpcurl -plaintext -d '{"id": 42}' localhost:50051 user.v1.UserService.GetUser

# 查看服务提供的全部方法
grpcurl -plaintext localhost:50051 list

# 查看某个 service 的详细定义
grpcurl -plaintext localhost:50051 describe user.v1.UserService

5. 常见问题排查速查表

现象 可能原因 排查方向
偶发性 DEADLINE_EXCEEDED HTTP/2 Window 满了被 backpressure 堵住 Wireshark 看 WINDOW_UPDATE 延迟
连接间歇性断开 Keepalive 没开或时间太长,LB 杀了空闲连接 检查 PermitWithoutStream 和 LB timeout
"message larger than max" Proto message 超过 4MB 限制 调大 MaxCallRecvMsgSize
PROTOCOL_ERROR / INTERNAL 中间代理篡改了 HTTP/2 frame 检查 Nginx / Envoy 配置,确保启用 http2
某个 stream 卡住但不报错 Server 端 handler 阻塞或未回写 用 GRPC_TRACE=stream,transport 定位
DNS 解析慢导致首次调用超时 gRPC 内置 resolver 异步解析但首次调用不等 提前预热连接或用固定 IP

附录:gRPC 协议分层速览

block-beta
    columns 1
    block:App
        columns 1
        A1["Protobuf Message<br/>(序列化后的 byte[])"]
    end
    space
    block:GRPC
        columns 1
        B1["gRPC Frame<br/>(Wire type + Length + Payload)"]
    end
    space
    block:HTTP2
        columns 1
        C1["DATA Frame"]
        C2["HEADERS Frame"]
        C3["WINDOW_UPDATE Frame"]
    end
    space
    block:Transport
        columns 1
        D1["HTTP/2 Stream<br/>(multiplexed over one TCP)"]
    end
    space
    D2["TCP Socket"]

    A1 --> B1
    B1 --> C1
    B1 --> C2
    B1 --> C3
    C1 --> D1
    C2 --> D1
    C3 --> D1
    D1 --> D2

    style A1 fill:#FFD43B
    style B1 fill:#00B6BC,color:#fff
    style D1 fill:#4FC08D,color:#fff

关联笔记