HTTP2传输层与流控

一、为什么 gRPC 必须用 HTTP/2

HTTP/1.1 的痛 HTTP/2 的解药
一连接一请求(队头阻塞) Stream 多路复用:一条 TCP 跑千百条并发 RPC
文本协议开销 二进制帧(Frame)分帧
头部重复冗长 HPACK 头压缩(字典+哈夫曼)
无服务端主动推送语义 双向流天然匹配 gRPC 四种模式

二、目标检验③:一次一元调用的 HTTP/2 报文图

协议依据 :官方 PROTOCOL-HTTP2.md(源码走读时以它对照 http_util.go 的常量)。

连接建立(一次)

swift 复制代码
TCP 连接
  ◄──────────────────────────────────────────────►
  Client                                          Server
   │ magic "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" ──► │ 连接前言
   │ ◄─────── SETTINGS 帧(初始窗口/MaxFrameSize) ─ │ 双方交换参数
   │ ──────── SETTINGS(ACK) ────────────────────► │

请求方向(客户端 → 服务端)

ini 复制代码
── HEADERS 帧 (Stream ID=1, END_HEADERS) ──────────────────────────►
     伪头部 + 元数据(HPACK 压缩后):
     :method        POST
     :scheme        http                    (TLS 时是 https)
     :path          /helloworld.Greeter/SayHello   ← 02 篇讲的全名!
     :authority     localhost:50051
     content-type   application/grpc        ← 必须,grpc 的"身份证"
     user-agent     grpc-go/1.85.0
     grpc-timeout   3000m                   ← 05 篇的超时预算(3 秒)
     authorization  bearer xxx              ← 06 篇的 metadata

── DATA 帧 (Stream ID=1) ─────────────────────────────────────────►
     ┌─────────┬─────────────┬───────────────────────────┐
     │ 压缩标志 │ 消息长度     │ Protobuf 编码的消息体        │
     │ 1 byte  │ 4 bytes     │ N bytes                    │
     │ (0/1)   │ uint32 大端  │ (压缩时先 compress 再计长度) │
     └─────────┴─────────────┴───────────────────────────┘
     ★ gRPC 自定义的"长度前缀帧"------protobuf 不自报长度,靠这 5 字节分界
     END_STREAM 标志:一元调用随 DATA 置位;流式调用由客户端发空 DATA 时置位

### 响应方向(服务端 → 客户端)

◄── HEADERS 帧 (Stream ID=1) ────────────────────────────────────
     :status 200                     (HTTP 层 OK;gRPC 错误不看这个!)
     content-type application/grpc
     grpc-encoding gzip              (若压缩)

◄── DATA 帧 (Stream ID=1) ───────────────────────────────────────
     同样的 5 字节前缀 + 响应消息体

◄── HEADERS 帧(trailer, END_STREAM) ─────────────────────────────
     grpc-status  0                  ← gRPC 真正的状态码在这!
     grpc-message xxx                (失败时的描述)
     ★ 这就是 08 篇"多收一次 RecvMsg 等 trailer"的物理对应物

一图记住 :请求 = HEADERS + DATA;响应 = HEADERS + DATA + trailer(HEADERS);gRPC 状态在 trailer;消息靠 5 字节前缀分界。流式调用只是 DATA 帧可以出现 N 次而已。

三、grpc-go 的传输层实现(自研,非 net/http)

internal/transport/ 的核心文件分工:

文件 职责
transport.go 接口定义:ClientTransport(:714)/ServerTransport(:756),帧类型、错误类型
http2_client.go 客户端:建连握手、reader goroutine、keepalive PING 定时器、GOAWAY 处理
http2_server.go 服务端:连接握手、逐帧解析开流、流数配额、GOAWAY(MaxConnectionAge 触发)
controlbuf.go loopyWriter(:522):每连接唯一的写 goroutine + 优先级发送队列
flowcontrol.go 流控窗口账本:trInFlow/outFlow/outStreamContext
bdp_estimator.go BDP 带宽×延迟积估计:动态调大窗口,吃满长肥管道
http_util.go 帧读写工具、头部编码、grpc-timeout/状态解析

用的是 golang.org/x/net/http2 的 Framer 和 HPACK (编码层复用),但连接管理、流调度、应用层逻辑全部自研。原因:net/http 的抽象无法提供 gRPC 需要的精细流控、GOAWAY 语义、keepalive 策略与连接级调优。

(例外:Server.ServeHTTP(server.go:1141) 允许把 gRPC 挂到 net/http 服务器上与他人共享端口,注释明确标注 EXPERIMENTAL 且性能/特性受限。)

四、单写者模型:loopyWriter(controlbuf.go:522)

scss 复制代码
 应用侧(任意多条流,任意多 goroutine)
     │ SendMsg → 写入 controlBuf(并发安全队列)
     ▼
 ┌─────────── controlBuf(按优先级组织) ───────────┐
 │  高优先级:流控帧/SETTINGS ACK/PING 回应          │
 │  中优先级:HEADERS                                │
 │  普通优先级:各流的 DATA                          │
 └───────────────────────┬────────────────────────┘
                         │ 唯一消费者
                  loopyWriter.run() (controlbuf.go:595,每连接 1 个 goroutine)
                    loop {
                      取当前可发的最高优先级项
                      检查目标流的 HTTP/2 发送窗口余额
                      ├─ 够: 聚合成帧(可合并多个小消息) → framer 写出
                      └─ 不够: 记账挂起,先发别的;收到 WINDOW_UPDATE 再唤醒
                      处理 keepalive PING 到期(客户端)
                    }

设计收益:

  1. 无锁写 socket------多流并发不再竞争连接;
  1. 优先级调度------流控记账、PING 回应永远插队在大块数据之前,避免"队头的大文件饿死控制帧";
  1. 小消息聚合------多次 SendMsg 的数据可合进一个帧,减少系统调用。

五、流控:HTTP/2 窗口 + BDP

两级窗口

scss 复制代码
连接级窗口(整条 TCP 的在途字节上限)
   └── 流级窗口(单条 RPC 的在途字节上限,默认初始 64KB,可配)

发送方视角: 每发 DATA,窗口余额减少
接收方视角: 应用读走数据后发 WINDOW_UPDATE 帧"还款"

背压传导链 :服务端 handler 处理慢 → 不读 RecvMsg → 不发 WINDOW_UPDATE → 客户端 loopyWriter 窗口耗尽挂起 → 客户端 SendMsg 开始阻塞 → 上游收不到推送压力自然减速。整条链路无需任何额外代码,这就是 03 篇"流式铁律"背后的机制。

BDP 估计器(bdp_estimator.go)

高延迟大带宽链路(跨机房)默认 64KB 窗口严重吃不满管道。BDP 估算器通过给 ACK 打时间戳测 RTT 与吞吐,动态向对端申请放大窗口 (WINDOW_UPDATE 发大增量)。这解释了一个现象:抓包能看到窗口越涨越大。

六、keepalive 在传输层的样子

  • 客户端 http2_client.go:定时器到期 → 无活动则发 PING(带数据) → 等 PONG;Timeout 内无回应对应 keepalive.ClientParameters.Timeout,判死连接;
  • 服务端 http2_server.go:收到 PING → 立即 PONG;同时执法 ------两次 PING 间隔 < MinTime 且无豁免 → 发 GOAWAY 断连;
  • MaxConnectionAge 到期 → 服务端主动 GOAWAY → 客户端收到后把该 transport 标记退役,重连会走 balancer 重新 pick------滚动重启的优雅感来源。
相关推荐
Bazingga1 小时前
同一个脑子,不同的身体:桌面办公智能体全景拆解
后端
用户302822530681 小时前
你的 API 只返回 30 条,为什么数据库还是被打满了?
数据库·后端·架构
Hespethorn1 小时前
MQTT 的四个工程暗礁:遗嘱、保留消息、ACL 与主题设计
后端·物联网
打工仔折腾 AI1 小时前
Spring AI + Qwen 周报生成实战:从原始日志到人工确认卡
人工智能·后端·python·spring·elasticsearch·ai agent 实战
杨文说AI与数字化1 小时前
分时预约系统的库存模型:为什么'加个预约功能'必然出现超卖与假满
后端
Darling噜啦啦1 小时前
# MySQL + Milvus = PostgreSQL?一张表搞定 AI Agent 的长时记忆(附完整代码)
后端·postgresql·orm
用户713874229001 小时前
在 .NET 中使用 AWS Secrets Manager:Config 与 Client 详解
后端
福大大架构师每日一题1 小时前
2026-10-16:交替数列的最大元素。用go语言,给定三个整数 n、s、m。现在要构造一个包含 n 个整数的序列,其中第一个数必须等于 s。序列中相邻数字的
后端
yunwei371 小时前
eBPF 入门实践教程:BPF Token,安全的委托式权限与程序加载
linux·后端·性能优化