流控与限额:HTTP/2 窗口、消息大小与服务端限流
服务端被打挂的原因往往不是"请求太多",而是三道闸门缺了一道:HTTP/2 流控窗口决定一条连接在途能有多少未确认字节,消息大小与并发流上限决定单请求与单连接的规模边界,服务端限流决定超出容量时是排队、拒绝还是等死。本章给出这三层的机制、参数与建议值。
HTTP/2 流控:两级窗口
HTTP/2 的流量控制是"接收方给发送方发额度":初始窗口 65535 字节(64KB),接收方通过 WINDOW_UPDATE 追加。gRPC 在此之上做带宽延迟积(bandwidth-delay product,BDP)估算并动态调窗,所以高延迟链路不会因 64KB 窗口而吞吐受限。
两级窗口必须分清:
| 窗口 | 参数 | 约束对象 |
|---|---|---|
| 流级(stream) | grpc.InitialWindowSize | 单条 RPC 的在途未确认字节 |
| 连接级(conn) | grpc.InitialConnWindowSize | 该连接上所有流合计的在途字节 |
srv := grpc.NewServer(
grpc.InitialWindowSize(1<<20), // 每流 1MB(默认 64KB)
grpc.InitialConnWindowSize(4<<20), // 连接级 4MB,建议为流级的 2~4 倍
grpc.MaxRecvMsgSize(16<<20), // 单条消息接收上限 16MB
grpc.MaxSendMsgSize(16<<20), // 与接收上限一致,否则"我发得出、对方收不下"
grpc.MaxConcurrentStreams(1000), // 单连接并发流上限,也是客户端建池的估算依据
)
内存估算必须算清:连接级窗口是常驻的,而"流级窗口 × 并发流"才是大头。1MB/流 × 1000 并发流 = 1GB,所以调窗前先算"单机可用内存 ÷(每流窗口 × 目标并发流)"。默认参数不是性能上限,而是"不会 OOM"的保守值,按上面的账自己调,别照抄网上的大数字。
客户端侧的调用级上限:
conn, _ := grpc.NewClient(target,
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(16<<20), // 默认 4MB
grpc.MaxCallSendMsgSize(16<<20),
),
)
服务端限流:三级令牌桶 + 明确拒绝语义
用拦截器加令牌桶(golang.org/x/time/rate),按"全局 → 方法 → 用户"三级限:
type Limiter struct {
global *rate.Limiter // 整机保护,最后一道闸
method map[string]*rate.Limiter // 按 FullMethod 分配配额
perUser *sync.Map // 按 uid 限速,防单用户刷爆
}
func limitUnaryServer(l *Limiter) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
if isHealthCheck(info.FullMethod) { // 健康检查与指标必须豁免
return handler(ctx, req)
}
if !l.allow(ctx, info.FullMethod) {
// 明确告诉客户端"是你太快了,不是服务坏了",并给出建议重试间隔
st := status.New(codes.ResourceExhausted, "rate limit exceeded")
st, _ = st.WithDetails(&errdetails.RetryInfo{
RetryDelay: durationpb.New(200 * time.Millisecond),
})
return nil, st.Err()
}
return handler(ctx, req)
}
}
- 返回
ResourceExhausted而不是Unavailable:第 10 章的重试策略默认不会重试它,避免"限流 → 重试 → 更严重限流"的正反馈; - 带上
RetryInfo细节,客户端才有依据做"遵守服务端建议"的退避; - 单机配额要按副本数折算:单机 1000 QPS、10 个副本就是对下游 10000 QPS,需要精确全局限额时用集中式配额(Redis 令牌桶)在拦截器里统一判断。
并发上限:别让排队变成雪崩
QPS 限流挡不住"慢请求堆积"——每个请求都合法,但都在等下游,goroutine 与内存无限增长。用有界并发兜底:
sem := make(chan struct{}, 512) // 单机在途 RPC 上限
func inflightUnaryServer() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
select {
case sem <- struct{}{}:
defer func() { <-sem }()
return handler(ctx, req)
default:
return nil, status.Error(codes.ResourceExhausted, "server busy") // 满即快速失败
}
}
}
上限建议按 目标 QPS × 平均耗时 再乘 48 倍余量:2000 QPS × 20ms = 40 个在途,取 256512 就够。超过上限宁可拒绝,也不要让整机因为 goroutine 与内存爆炸而彻底不可用。
流式调用的背压
- 服务端流式:不要在服务端写无界队列。
Send会因 HTTP/2 窗口耗尽而阻塞,这个阻塞就是天然背压,让它阻塞即可;但要配合第 07 章的 ctx 取消,否则一个不消费的客户端会长期占住 goroutine; - 双向流:客户端要控制发送速率并周期性
Recv,否则窗口互锁(双方都在等对方读)会死锁; - 大消息优先切
stream传输,而不是靠调大MaxRecvMsgSize硬塞一个几百 MB 的 message。
参数建议值
| 参数(服务端) | 默认 | 建议值 | 说明 |
|---|---|---|---|
InitialWindowSize | 64KB | 256KB~1MB | 大消息/跨机房调大,注意内存 |
InitialConnWindowSize | 64KB | 1~4MB | 取流级窗口的 2~4 倍 |
MaxRecvMsgSize | 4MB | 4~16MB | 按最大业务消息定,不要设成 -1 |
MaxSendMsgSize | 无限制 | 与接收上限相同 | 防发出对手收不下的消息 |
MaxConcurrentStreams | 百级 | 500~2000 | 同时决定客户端连接池大小 |
MaxHeaderListSize | 偏大 | 8~16KB | metadata 的边界,见第 09 章 |
| 在途 RPC 上限 | 无 | QPS × 耗时 × 4 | 有界 semaphore,超限快速失败 |
| 单用户 QPS | 无 | 压测峰值的 2~3 倍 | 防单用户刷爆,超限返回 ResourceExhausted |
| 允许排队时长 | 无 | ≤ 50ms | 只允许极短排队,长排队要拒绝 |
常见坑
received message larger than max:发送端没超限、接收端MaxRecvMsgSize太小,报ResourceExhausted。收发两侧上限要成套设置并写进接口文档。- 只调窗口不算内存:
InitialWindowSize设 4MB、并发 2000 流,等于每连接预留 8GB,先 OOM 再谈性能。 - 拒绝用错状态码:返回
Unavailable会让客户端按第 10 章策略重试,把限流打成雪崩;ResourceExhausted+RetryInfo才是正确姿势。 - 健康检查被限流:探针失败导致 Pod 反复重启,故障率反升。所有限流器与并发闸门都要留白名单。
- 单机限额当全局限额:下游按单机容量算就会被打穿,多副本场景要按副本数折算或集中式配额。
WaitForReady(true)配限流:调用方排队等就绪,请求堆在客户端,超时集中爆发;限流场景应保持 fail-fast。- 无界排队:任何"先塞进 channel 慢慢处理"的设计都必须带上限与超时,否则限流就变成了延迟放大器。
小结:HTTP/2 是两级窗口流控,InitialWindowSize 管单流、InitialConnWindowSize 管整连接,调窗前按"每流窗口 × 并发流"算内存;消息规模用 MaxRecvMsgSize/MaxSendMsgSize(建议 4~16MB,收发两侧一致);服务端用令牌桶做"全局/方法/用户"三级限流并返回 ResourceExhausted + RetryInfo,再用有界 semaphore 卡住并发(上限约为 QPS × 耗时 × 4),超出即快速失败;流式调用让 Send 阻塞传导背压,不要自己加无界队列。