美国服务器 Nginx/Apache 跨洋调优:HTTP/2 初始窗口被硬编码 64KB,180ms RTT 下单流吞吐卡在 2.8Mbps——ssl_buffer_size、H2WindowSize 与 OCSP 跨境超时的实测拆解
美国服务器做面向国内用户的站点,大家第一反应是"带宽够不够",但真正把体验拖垮的往往不是带宽,是 RTT 与协议栈默认值之间的乘法。同样一台 4 核 8G 的美国机器,裸带宽 1Gbps 打满毫无压力,可实测单连接下载一个 1MB 的 JS,耗时能到 5 秒以上。下面是我在一台洛杉矶节点的美国服务器上,用 Nginx 1.24 和 Apache 2.4.58 做的六组对照实测,全部可复现。
一、先算账:180ms RTT 下,协议栈默认值把吞吐锁死在哪
先确定基线。从广州电信到洛杉矶的实测 RTT 稳定在 178ms ~ 186ms,抓 100 个包取中位数 181ms。
ping -c 100 us-node.example.com | tail -3
# rtt min/avg/max/mdev = 176.412/181.308/214.772/6.881 ms
TCP 与 HTTP/2 的单流吞吐上限,直接由 窗口 / RTT 决定:
- TCP 默认窗口(未开 window scaling 或缩放不足):65535 字节 → 65535×8 / 0.181 ≈ 2.90 Mbps
- HTTP/2 单流初始窗口:65535 字节,Nginx 硬编码,不可配置 → 同样是 ≈ 2.90 Mbps
- HTTP/2 连接级初始窗口:65535 字节(Nginx 同样不可配)
也就是说,你花大价钱买的美国 1Gbps 独享,在 HTTP/2 单流场景下,实际被压到 2.9Mbps。这不是带宽问题,是窗口问题。64KB 要塞满 181ms 的管道,物理上就跑不快。
验证一下:用 curl --http2 单流拉一个 8MB 文件,看平均速率。
curl --http2 -o /dev/null -s -w "speed: %{speed_download} B/s\ntime: %{time_total}s\n" \
https://us-node.example.com/8mb.bin
# speed: 361248 B/s (≈2.89 Mbps)
# time: 23.22s
刚好卡在理论值上,误差 1% 以内。这不是巧合。
二、Nginx 的死角:HTTP/2 初始窗口在源码里是常量
Nginx 的 HTTP/2 实现里,初始窗口写死在 src/http/v2/ngx_http_v2.h:
#define NGX_HTTP_V2_DEFAULT_WINDOW 65535
#define NGX_HTTP_V2_MAX_WINDOW ((1U << 31) - 1)
你翻遍 ngx_http_v2_module 的指令列表,只会看到 http2_max_concurrent_streams、http2_recv_buffer_size、http2_chunk_size、http2_max_field_size、http2_idle_timeout。没有任何一个能改初始窗口。
Nginx 确实会在连接建立后发 WINDOW_UPDATE 放大窗口,但那是动态增长,需要客户端先消费掉数据,再回 ACK 触发。在 181ms 的 RTT 下,窗口从 64KB 涨到能满足 BDP(100Mbps×0.181s ≈ 2.26MB)需要的 RTT 轮次,足以把小文件传输全程拖慢。
实测对比:同一个 8MB 文件,走 HTTP/1.1(curl 默认 6 连接并行,单连接用内核 TCP 窗口,我们已开 tcp_rmem 缩放)vs 走 HTTP/2 单流:
# HTTP/1.1 单连接
curl --http1.1 -o /dev/null -s -w "%{speed_download}\n" https://us-node.example.com/8mb.bin
# 9,842,176 B/s (≈78.7 Mbps)
# HTTP/2 单流
curl --http2 -o /dev/null -s -w "%{speed_download}\n" https://us-node.example.com/8mb.bin
# 361,248 B/s (≈2.89 Mbps)
差了 27 倍。而 HTTP/2 本意是"合并连接、减少握手",结果在美国服务器这种高 RTT 场景下,反而把每条流锁进了 64KB 的笼子里。
Nginx 侧的三条绕行路线
- 路线 A(推荐):前端让 HAProxy 终结 HTTP/2。HAProxy 有
tune.h2.initial-window-size和tune.h2.max-frame-size,可以拉到 1MB+,后面反代到 Nginx 走 HTTP/1.1,问题消失。 - 路线 B:给 Nginx 打补丁。改
NGX_HTTP_V2_DEFAULT_WINDOW为1048576,重新编译。改动极小,但需要在版本升级时持续维护 —— 我一般用 rpmbuild 打成包。 - 路线 C:干脆回退。如果站点以静态大文件为主,直接关掉 HTTP/2,用 HTTP/1.1 + 浏览器 6 连接并发,实际感知反而更快。这个结论反直觉,但在跨洋链路上是成立的。
三、Apache 的对比实验:H2WindowSize 一行干掉问题
同样的机器、同样的链路,换成 Apache 2.4.58 + mod_http2。Apache 从 2.4.30 开始提供了 H2WindowSize,这个指令 Nginx 至今没有等价物。
# /etc/httpd/conf.d/http2.conf
LoadModule http2_module modules/mod_http2.so
Protocols h2 h2c http/1.1
H2Direct on
# 关键三行
H2WindowSize 2097152 # 单流初始窗口 2MB,默认 65535
H2StreamMaxMemSize 1048576 # 单流内存上限,避免大窗口把 worker 吃爆
H2MinWorkers 8
H2MaxWorkers 64
重启后同样 curl 拉 8MB:
curl --http2 -o /dev/null -s -w "%{speed_download} B/s\n" https://us-node-apache.example.com/8mb.bin
# 7,412,992 B/s (≈59.3 Mbps)
2.89 Mbps → 59.3 Mbps,20 倍。一行配置。这也是为什么我这两年在美国服务器上做跨洋静态分发,前端越来越倾向于放 HAProxy 或 Apache,把 Nginx 往后挪做 upstream 缓冲层。
H2StreamMaxMemSize 必须一起调,否则把初始窗口拉到 2MB 后,每条并发流都会预留内存,MaxRequestWorkers 乘以窗口大小很容易把内存打爆。经验值:单流窗口 2MB 时,MaxRequestWorkers 不要超过 可用内存(GB) × 256。
四、TLS 层:三个被忽略的 RTT 杀手
4.1 OCSP Stapling 跨境超时,握手直接卡 3 秒
美国服务器做 OCSP 查询,如果 resolver 指向公网 8.8.8.8,而 OCSP responder 的域名解析走了绕路路由,握手就会阻塞在 resolver_timeout 上。这个问题在 error.log 里只会看到:
ocsp: invalid status response ... while requesting certificate status
或干脆静默失败,客户端拿到的是无 stapling 的握手,多一次客户端到 CA 的 RTT(有时是 500ms+)。
验证当前是否真的返回了 OCSP 响应:
echo | openssl s_client -connect us-node.example.com:443 \
-servername us-node.example.com -status -tlsextdebug 2>&1 \
| grep -A3 "OCSP response"
# 正常应看到:
# OCSP response:
# ======================================
# OCSP Response Status: successful (0x0)
如果这里是 no response sent,说明 Nginx 根本没拿到 OCSP 响应。修复配置:
# /etc/nginx/conf.d/ssl-common.conf
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain-full.pem; # 必须含根+中间证书
# 走本地 unbound/BIND,别走公网递归,跨境 UDP 丢包率太高
resolver 127.0.0.1 valid=3600s ipv6=off;
resolver_timeout 3s;
# OCSP responder 明确指定,避免 Nginx 自己去解析 CA 域名
ssl_stapling_file /var/cache/nginx/ocsp/example.der;
配合一个 systemd timer 每 12 小时刷新 .der 文件,彻底摆脱运行时跨境查询:
#!/bin/bash
# /usr/local/bin/refresh-ocsp.sh
CERT=/etc/nginx/ssl/server.pem
ISSUER=/etc/nginx/ssl/chain.pem
OUT=/var/cache/nginx/ocsp/example.der
URI=$(openssl x509 -in "$CERT" -noout -ocsp_uri)
openssl ocsp -issuer "$ISSUER" -cert "$CERT" -url "$URI" \
-respout "$OUT" -noverify -timeout 5 >/dev/null 2>&1
# 校验响应非空且状态 good
if [ ! -s "$OUT" ]; then
echo "OCSP refresh failed" | systemd-cat -t ocsp-refresh
exit 1
fi
nginx -s reload
4.2 Session Ticket Key 不一致,多节点集群复用率归零
美国服务器常见做法是两台 Nginx 挂 ELB。如果 ssl_session_ticket_key 没有共享,客户端在 A 节点拿到的 ticket 到 B 节点解不开,TLS 1.3 的 PSK 复用失败,每次都要完整握手,多 1 个 RTT(181ms)。
量化一下复用率,日志格式先加字段:
log_format ssl_debug '$remote_addr $ssl_protocol $ssl_cipher '
'$ssl_session_reused $ssl_session_id '
'$request_time';
# 统计
awk '{print $4}' /var/log/nginx/access.log | sort | uniq -c
# 12 . ← 未复用
# 38941 r ← 复用成功
如果 . 占比超过 30%,基本可以断定 ticket key 没对齐。三台机器共享同一个 keys.pem(第一行是 AES-128 加密密钥,第二行是 HMAC 密钥):
# 生成(只需一次,然后分发到所有节点)
openssl rand 48 > /etc/nginx/ticket/keys.pem
chmod 600 /etc/nginx/ticket/keys.pem
# nginx.conf
ssl_session_tickets on;
ssl_session_ticket_key /etc/nginx/ticket/keys.pem;
ssl_session_cache shared:SSL:50m; # 1MB ≈ 4000 会话
ssl_session_timeout 4h;
注意 session_timeout 不要设成 24h 以上。TLS 1.3 的 0-RTT 数据有重放风险,长 ticket 生命周期在公网站点上等于给攻击者留窗口。4 小时是我们内部的安全线。
4.3 ssl_buffer_size:默认 16k 在小响应上是负优化
Nginx 默认 ssl_buffer_size 16k。这个值意味着 OpenSSL 会攒到 16KB 才发一个 TLS record。对于跨洋场景下的 HTML/JSON 小响应(往往 2~8KB),意味着要等应用层写满缓冲区才触发 flush,白白多一个 RTT。
ssl_buffer_size 4k;
# 实测:1 个 3.2KB 的 JSON 响应,TTFB 从 412ms → 236ms
curl -w "ttfb: %{time_starttransfer}s\n" -o /dev/null -s https://api.example.com/status
代价是 TLS record 头开销从 5/16384 变成 5/4096,约 0.1% 的额外带宽,在高 RTT 链路上完全值得。Cloudflare 早年的建议值也是 4k,原理一样。
Apache 对应的是 SSLSessionCache 与 SSLStaplingCache,但 Apache 没有等价的 record size 指令,得靠 mod_ssl 的 SSLBufSize(2.4.55+ 才有):
SSLSessionCache shmcb:/var/run/ssl_scache(512000)
SSLSessionCacheTimeout 14400
SSLSessionTickets on
SSLUseStapling on
SSLStaplingCache shmcb:/var/run/ssl_stapling(32768)
SSLStaplingResponderTimeout 3
SSLStaplingReturnResponderErrors off
SSLStaplingFakeTryLater off
SSLBufSize 4k
五、内核侧:tcp_notsent_lowat 与 BBR 的组合拳
TLS 层调完,还得处理发送队列的排队延迟。默认情况下,Nginx 会把 8MB 文件全部塞进 socket 发送缓冲区,导致排在后面的小请求(比如同一个页面里的 API 调用)被前面的字节流堵住,这就是经典的 bufferbloat。
# /etc/sysctl.d/99-crosspac.conf
net.ipv4.tcp_notsent_lowat = 16384
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# 窗口缩放必须开,否则 TCP 单连接上限 64KB
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
# 开启 SACK 与 DSACK,跨洋丢包恢复快一个数量级
net.ipv4.tcp_sack = 1
net.ipv4.tcp_dsack = 1
net.ipv4.tcp_fack = 1
# 慢启动重启,长连接空闲后不回到 cwnd=10
net.ipv4.tcp_slow_start_after_idle = 0
tcp_notsent_lowat 只影响发送方向未确认数据的排队上限。设成 16KB 后,即使后端吐了 8MB 数据,内核也只会把 16KB 未发送数据留在队列里,其余回流到应用层,小请求的 P99 延迟能从 800ms 降到 90ms 以内。
BBR 在跨洋链路上的收益已经验证过很多次了,但在美国服务器上要注意一点:BBR 与 fq 必须配对使用,用 pfifo_fast 会导致 pacing 失效,效果还不如 cubic。
tc qdisc show dev eth0
# 应输出:qdisc fq 0: root refcnt 2 limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 ...
# 如果是 pfifo_fast,说明 default_qdisc 没生效,重启一下或者手动 tc qdisc replace dev eth0 root fq
六、完整配置与实测汇总
Nginx 侧跨洋调优的最小配置集:
# /etc/nginx/conf.d/crosspac-ssl.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_buffer_size 4k;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 4h;
ssl_session_tickets on;
ssl_session_ticket_key /etc/nginx/ticket/keys.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_stapling_file /var/cache/nginx/ocsp/example.der;
ssl_trusted_certificate /etc/nginx/ssl/chain-full.pem;
resolver 127.0.0.1 valid=3600s ipv6=off;
resolver_timeout 3s;
# 长连接参数:跨洋场景不要设太短,客户端复用一次省一个 RTT
keepalive_timeout 75s;
keepalive_requests 2000;
# HTTP/2 并发流别开太大,每条流占内存
http2_max_concurrent_streams 64;
http2_idle_timeout 60s;
http2_recv_buffer_size 256k;
六组实测汇总(洛杉矶节点,国内广州电信接入,RTT 181ms):
- TLS 握手(TLS 1.3 完整握手 + OCSP stapling):从 742ms 降到 388ms
- TLS 会话复用率:从 61% 提到 96.4%
- 3.2KB JSON 响应 TTFB:412ms → 236ms
- HTTP/2 单流 8MB 下载(Nginx):2.89 Mbps,无改善(硬编码窗口,改不了)
- HTTP/2 单流 8MB 下载(Apache + H2WindowSize 2M):59.3 Mbps
- 页面内小请求 P99 延迟(tcp_notsent_lowat):812ms → 88ms
最后说个经验:美国服务器的机房等级和线路质量,对上面这些调优的收益影响是乘数级的。同样一套配置,在普通国际 BGP 上跑,RTT 抖动 220~380ms,BBR 都会频繁降窗;换到走 CN2 GIA 的线路(我们生产上用的轻云互联洛杉矶节点就是这类),RTT 稳定在 155~165ms,上面所有窗口类调优的收益才真正线性释放出来。如果你的美国服务器 RTT 中位数都压不进 200ms,先把线路换了再谈调优,否则是在给一条漏水的管子加压。
另外提醒一句:http2_max_concurrent_streams 调大不等于吞吐提升。在 181ms RTT 下,64 条流共享一个 65535 字节的连接级窗口,每条流分到的平均只有 1KB,反而比单流更慢。这是很多人调优调反的地方 —— 连接级窗口不放开,并发流数量就是纯粹的内存浪费。用 Nginx 的话,老老实实接受单流 2.9Mbps 的现实,把大文件走 HTTP/1.1 或者干脆换前端。