运营反馈好几个地推团队在写字楼地下车库、电梯间等弱网环境下,加载 App 首页的商品高清大图经常卡死死活加载不出来。抓包排查发现,用户的 HTTP/2 TCP 连接一旦发生高达 10% 以上的随机丢包,就会直接触发 TCP 慢启动和拥塞控制,整个多路复用的 TCP 通道全都因为头部阻塞(Head-of-Line Blocking)动弹不得。也就是说,一个大图切片丢了包,后面所有的小接口数据、静态资源全部得在缓冲区里排队死等。

面对这种典型的移动端弱网大坑,指望优化 H2 连接或者缩减包体积已经是杯水车薪,最彻底的搞定办法干脆就是直接上 HTTP/3 (QUIC)。QUIC 协议基于 UDP 传输,每个流都是独立的,单个流丢包只影响它自己,根本不会连累其他连接通道,心智负担瞬间归零。

虽然目前 Nginx 从 1.25.0 版本开始已经原生支持了 HTTP/3,但实际折腾过程中,各种坑层出不穷,尤其是在 UDP 丢包调优和证书握手配置上。这里直接贴出我们在生产环境实测跑通的最优配置。

首先是 Nginx 主配置文件的 server 块定义:

server {
    # 监听常规的 IPv4/IPv6 HTTPS 端口
    listen 443 ssl;
    listen [::]:443 ssl;

    # 开启 HTTP/3 QUIC 监听,reuseport 必须在同一个 IP/Port 组的其中一个 listen 处声明
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    server_name api.example.com;

    # 证书配置,QUIC 必须使用 TLS 1.3
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_conf_command Options PrioritizeChaCha;

    # 开启 Gzip 和 Brotli 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;

    # HTTP/3 相关头部宣告,告诉浏览器支持并尝试升级到 h3
    # ma 表示缓存有效期(秒),此处设为 24 小时
    add_header Alt-Svc 'h3=":443"; ma=86400';
    add_header X-Frame-Options "SAMEORIGIN";

    # 针对 HTTP/3 的特有缓冲区和连接参数调优
    http3 on;
    http3_max_concurrent_streams 128;
    http3_stream_buffer_size 128k;
    
    # 启用 QUIC 连接保持和快速恢复
    quic_gso on;
    quic_retry on;

    location / {
        proxy_pass http://backend_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

很多人配完 Nginx 发现依然走的是老旧的 TCP H2 协议,90% 的原因都是被防火墙拦截了,或者系统的 UDP 缓冲区直接把包给扔了。

第一步,别忘了在服务器上用 iptables 放行 443 端口的 UDP 流量,光放行 TCP 是没用的:

# 放行常规 HTTP/S 的 TCP 端口
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# 必须显式放行 UDP 443 端口,否则 QUIC 无法建连
iptables -A INPUT -p udp --dport 443 -j ACCEPT

# 保存防火墙规则并重启服务
netfilter-persistent save
netfilter-persistent reload

第二步是针对 Linux 内核的 UDP 丢包调优。默认情况下,Linux 系统的 UDP 接收缓冲区(rmem)非常小,遇到高并发或者网络突发状况,系统的 socket 缓冲区直接被打满,内核就会开始疯狂地丢弃 UDP 数据包。这会导致 QUIC 不断重传,性能直接崩溃。

我们需要修改 /etc/sysctl.conf,强行调大系统全局的 Socket 读写缓冲区最大限制:

# 设置最大 Socket 读缓冲区大小为 16MB
net.core.rmem_max = 16777216
# 设置最大 Socket 写缓冲区大小为 16MB
net.core.wmem_max = 16777216
# 针对 UDP 协议的内存限制调优(单位为页数,通常 1页 = 4KB)
net.ipv4.udp_mem = 65536 131072 262144

修改完之后执行 sysctl -p 重新加载内核参数。我们在办公室用抓包工具把丢包率强行拉到 15% 模拟弱网,结果升级了 HTTP/3 + Linux 内核调优后的首屏大图加载成功率从原本的 40% 直接飙升到了 95% 以上,卡死重试的心智负担彻底消除了。

不过在生产上强推 HTTP/3 的时候,我们发现某些省份的小运营商会恶意限速或者直接封锁 UDP 443 端口,导致浏览器回退到 H2。你们有遇到过这种 QUIC 被运营商阻断的骚操作吗?都是怎么做探测并进行灰度切换的?