记录一下那天晚上的排查过程。服务在 HTTP/2 下推 SSE,单条事件 4KB,客户端要等 2 秒才收到第一批,降到 HTTP/1.1 后 200ms 就出字。用 curl 的两种协议参数都能稳定复现,服务端日志里 WINDOW_UPDATE 的相关记录很少。

刚开始盯着「反向代理在 HTTP/2 与 HTTP/1.1 之间转换时做了缓冲」,折腾了半个多小时,排掉了。

排查方向其实有限,先把可能性排一排。初始流窗口只有 65535 字节,接收端不主动发 WINDOW_UPDATE。少数情况下是客户端 HTTP/2 实现把接收窗口压得很小。

还有一种情况,应用层每次只写几十字节,Nagle 与延迟 ACK 叠加放大延迟。

先对比两种协议下的首次字节时间

curl -N --http2 -w '%{time_starttransfer}\n' -o /dev/null https://api/stream; curl -N --http1.1 -w '%{time_starttransfer}\n' -o /dev/null https://api/stream

接着抓包看窗口更新帧的频率

tshark -i eth0 -f 'tcp port 443' -Y 'http2.type == 8' -c 20

调大接收窗口后复测首字

nghttp -nv https://api/stream | grep -i window

必要时把流式路径回退到 HTTP/1.1

curl -N --http1.1 https://api/stream

别只看报错消失,验证要多做一步。同一路径在 HTTP/2 下的首字时间与 HTTP/1.1 相差不到 200ms,抓包里能看到持续的窗口更新。

多路复用下窗口是按流算的,代理和客户端各有一份,只调一端经常看不出变化。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。

作者 9823189