上周有个客户跟我说,站点首屏慢,可 ping 看着挺正常,服务器带宽也没跑满。我先跑了 iperf3、看了监控面板,都没问题。后来我把 curl 的时间线打出来,才注意到一件事:从 TCP 连接到第一个业务响应之间,有一段近乎"空白"的等待,正好是一个来回的距离。日本线路 RTT 大概 41 毫秒,这一段空白就是 41 毫秒。
这不是配置问题,是协议本身的开销。传统 TCP 要先三次握手建立连接,客户端在收到 SYN-ACK 之前一个字节的业务数据都发不出去。也就是说,跨洋或者跨境访问时,首个请求天生就要多等一个 RTT,TLS 握手还要再叠上去。
如果你手上的机器正好需要跑在低延迟链路上做接口、代理或者跨境业务,选机器时别只看带宽和 CPU:digitalvirt(原生IP精品线路) 这类主打 CN2 GIA、9929、CMI 等优化回程和低延迟线路的产品,和本文要聊的场景是一类问题;cstonecloud(多机房多线路云服务器) 也提供 CN2 GIA、9929、CMIN2 等多种线路组合,可以按自己实际跑的链路去挑。

Fast Open 到底省在哪:把数据塞进 SYN 包
Fast Open 的思路不复杂:服务端在第一次完整握手的时候,顺手发一个加密 cookie 给客户端;客户端下次建连时,把这个 cookie 放进 SYN 包里,同时把第一批业务数据也塞进去,服务端验证 cookie 通过就直接收数据,省掉那次等待。
关键点有三个。第一,客户端必须先有一次成功的连接拿到 cookie,所以第一次请求不吃亏,只是从第二次开始生效;第二,cookie 是加密且有时效的,中间设备动不了它,但某些 NAT、防火墙会顺手把 SYN 里的数据丢掉,那就退化成普通握手,不会出错,只是没收益;第三,这套机制只作用在 TCP 层,和 TLS 是不是 1.3、是不是 0-RTT 是两码事,别混在一起讲。

我的实测:日本线路上,首包从 412ms 降到 371ms
测试环境是一台东京机房的机器,客户端在国内,ping 稳定在 41 毫秒左右。我用 curl 连续跑 50 次,取首字节时间和 total 时间的中位数。
关闭的时候:curl 的 time_connect 约 83ms(一个 RTT 加 SYN/ACK 排队),time_starttransfer 中位数 412ms。打开 net.ipv4.tcp_fastopen=3 之后,time_connect 降到 42ms 附近,time_starttransfer 中位数 371ms,正好少了一个 RTT。抖动也变了:关闭时首包时间的 p99 是 468ms,打开后 p99 掉到 401ms。
这 41 毫秒值不值钱,得看业务。如果是个后台数据同步任务,基本无所谓;如果用户点一下要等首屏,那这 41 毫秒就是白扔的。对于跨境电商接口、SaaS 中转、小程序后端这种一个请求里链路占比不高的场景,收益其实是叠加的一次完整请求里往往不止一次建连,代理转发的场景更明显。

怎么开:内核参数加 Nginx 配置,两步
第一步是内核参数,net.ipv4.tcp_fastopen 这个值有四个状态:0 是完全关闭,1 表示本机作为客户端启用,2 表示本机作为服务端启用,3 是两边都开。做服务端就设 2,做客户端调工具就设 1,通吃就设 3。改完记得 sysctl -w 立刻生效,同时写进 /etc/sysctl.conf 防止重启丢。
第二步是 Nginx 侧。较老的 Nginx 只认 fastopen=1,1.25.1 之后可以写成 fastopen=256 这类数字,含义是允许同时发起多少个 Fast Open 连接,超过这个数的连接退回普通握手,防止被当成放大攻击的入口。写在 listen 那一行上,比如 listen 443 ssl fastopen=256 http2;。改完 nginx -t 检查语法再 reload。
客户端侧不用改服务端代码,curl 加 --tcp-fastopen 就行,浏览器原生不支持,得靠系统层面的 TCP Fast Open 开关(Linux 上是 /proc/sys/net/ipv4/tcp_fastopen)。另外要注意,客户端必须先跟同一台服务端完成过一次握手才拿得到 cookie,所以测试的时候别只测第一次连接,那必然看不到差异。

那个坑:0-RTT 数据可能被重放
Fast Open 的第一批数据是"抢跑"的,服务端在握手完全确认之前就收了。这带来一个安全上的固有风险:如果中间有攻击者能伪造 IP 并阻断原包的到达,服务端会以为重传,于是同一份早期数据被执行两次。对于普通的 HTTP GET 请求这无所谓,但对于下单、扣款这类写操作,这就是典型的重放攻击。
实际处理办法是分层看:业务层靠幂等键防重(同一个请求带唯一 ID,服务端去重),协议层如果是 TLS 1.3 的 0-RTT,那本身也有重放风险,敏感接口不该依赖早期数据。所以我的做法是:Fast Open 开着,但所有写接口都走独立的、带幂等键的请求路径,不去碰早期数据的语义。别为了省 41 毫秒,把金额搞重了。
还有两个常见的"开了没效果"的情况:一是中间链路有设备剥掉 SYN 里的数据(比如某些老旧的防火墙或者 NAT 网关),二是客户端应用根本没启用 Fast Open 支持,cookie 拿不到。这时候可以抓包确认 SYN 包的 payload 长度是不是非零,非零就说明数据确实发出去了。
最后说说值不值得开
如果你的业务在国内访问日本、香港或者美西的机器,链路 RTT 在 30 毫秒以上,且对首包敏感,那 Fast Open 基本属于配置一次、长期受益的改动,收益就是稳定省下那一个 RTT。如果你的服务主要在同城或者同一机房内部,或者对首包不敏感,那它的存在感会非常低,开不开都行。
顺带说一句,这类改动真正要注意的是"别只看省了多少"。我在测的时候同时看了连接建立时的 CPU 占用和并发连接数,Fast Open 并没有明显增加负担,反倒因为少了一次往返减少了半开连接的堆积。真正需要盯的是那个 fastopen=数字 的上限,别图省事写太大。
动手顺序我建议这样:先 sysctl 打开服务端参数,在同一台机器上用 curl --tcp-fastopen 验证数据确实进了 SYN;确认没问题再加 Nginx 的 listen 配置,然后 reload;最后才考虑把客户端也打开。这样出问题的时候好定位是哪一层。


喜欢
顶
难过
囧
围观
无聊




