TCP Fast Open 想把“握手一个往返 + 请求应答一个往返”压成一个往返:第一次连接拿一个 cookie,以后的连接把数据直接塞进 SYN。这篇先在 macOS 上实测它在线上长什么样、能省多少时间、公网上谁支持,再用 root 权限补上密钥轮换和“客户端为什么悄悄不用了”,最后在 Linux 容器里对照 Linux 的行为、重放和被中间设备丢弃的情形。
net.inet.tcp.fastopen: 3 net.inet.tcp.fastopen_backlog: 10 listen() 之前对监听套接字 setsockopt(TCP_FASTOPEN, 1):EINVAL listen() 之后 setsockopt(TCP_FASTOPEN, 1):成功,读回 1 Python 的 socket 模块里有 TCP_FASTOPEN: True;有 MSG_FASTOPEN: False(Linux 用 sendto + MSG_FASTOPEN,macOS 用 connectx,这里通过 ctypes 调用 libc 的 connectx)
net.inet.tcp.fastopen=3;服务端要在 listen() 之后设 TCP_FASTOPEN;客户端用 connectx 带数据(Python 需要 ctypes)。
服务端收到的数据 [b'hello0', b'hello1', b'hello2', b'hello3', b'hello4', b'hello5', b'hello6', b'hello7'] 第 1 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 2 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 3 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 4 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 5 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 6 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 7 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] 第 8 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无;SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO']
用一个客户端从没用过的目的地做对照:8 次连接的 SYN 都是 0 字节、带长度 2 的 TFO 选项请求 cookie,服务端不回 TFO 选项,数据在握手后才发。
服务端收到的数据 12 条,全部正确:True
12 个连接的 SYN 载荷(字节):[0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
12 个 SYN 的 TFO 选项长度:[[2], [10], [10], [10], [10], [10], [10], [10], [10], [10], [10], [10]](2 = 只请求 cookie;10 = 带 8 字节 cookie)
第 2 个连接(SYN 里带数据的第一个)的全部包:
+ 0.00 ms C→S SYN 载荷 7 字节 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'] ← TFO 选项长度 [10]
+ 0.26 ms S→C SYN+ACK 载荷 0 字节 选项 ['MSS', 'WS', 'TS', 'SACKok']
+ 0.32 ms C→S ACK 载荷 0 字节 选项 ['TS']
+ 0.34 ms S→C ACK 载荷 0 字节 选项 ['TS']
+ 0.47 ms S→C PSH+ACK 载荷 10 字节 选项 ['TS']
+ 0.56 ms C→S FIN+ACK 载荷 0 字节 选项 ['TS']
+ 0.58 ms S→C ACK 载荷 0 字节 选项 ['TS']
+ 55.64 ms S→C FIN+ACK 载荷 0 字节 选项 ['TS']
+ 55.77 ms C→S ACK 载荷 0 字节 选项 ['TS']
它的 SYN+ACK 的 ack = SYN 的 seq + 8(1 个 SYN + 7 字节数据)第一个连接请求 cookie(TFO 选项长 2);之后的连接 SYN 带 8 字节 cookie(选项长 10)和数据,SYN+ACK 的 ack 确认了数据。
普通 connect + send :逐次(ms):[202, 200, 201, 202] connectx(TFO) :逐次(ms):[205, 101, 100, 103] connectx 这 4 次的 SYN 载荷(字节):[0, 5, 5, 5] 普通 connect + send 中位 202 ms;connectx 里 SYN 带数据的 3 次中位 101 ms,SYN 没带数据的 1 次中位 205 ms;省下 101 ms ≈ 1.0 个 RTT(RTT = 2 × 50 ms)
50 ms 单向延迟下,普通连接 202 ms,带数据的 TFO 连接 101 ms。
connectx 第 1 次:SYN 载荷 56 字节,SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'](TFO 选项长度 [10]);SYN+ACK 选项 ['MSS', 'SACKok', 'TS', 'WS'](TFO 选项长度 []),ack = SYN 的 seq + 57 connectx 第 2 次:SYN 载荷 56 字节,SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'](TFO 选项长度 [10]);SYN+ACK 选项 ['MSS', 'SACKok', 'TS', 'WS'](TFO 选项长度 []),ack = SYN 的 seq + 57 connectx 第 3 次:SYN 载荷 56 字节,SYN 选项 ['MSS', 'WS', 'TS', 'SACKok', 'TFO'](TFO 选项长度 [10]);SYN+ACK 选项 ['MSS', 'SACKok', 'TS', 'WS'](TFO 选项长度 []),ack = SYN 的 seq + 57 connectx 到收到响应第一批字节:[267, 243, 248] ms;响应开头 [b'HTTP/1.1 200'];普通 connect + send:[386, 378, 476] ms
example.com 接受 SYN 里的 56 字节请求并确认,首字节比普通连接快约一个 RTT。
设置 K1:net.inet.tcp.fastopen_key: -> 换成 K2:net.inet.tcp.fastopen_key: -> 服务端收到的数据 [b'hello0', b'hello1', b'hello2', b'hello3', b'hello4', b'hello5', b'hello6'],全部正确:True 第 1 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 e317342e 第 2 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 e317342e;SYN+ACK 的 TFO 选项 无,数据被接受 第 3 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 e317342e;SYN+ACK 的 TFO 选项 无,数据被接受 第 4 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 e317342e;SYN+ACK 的 TFO 选项 533701b8,数据没被接受(ack = seq + 1) ← 换密钥之后 第 5 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 533701b8;SYN+ACK 的 TFO 选项 无,数据被接受 ← 换密钥之后 第 6 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 533701b8;SYN+ACK 的 TFO 选项 无,数据被接受 ← 换密钥之后 第 7 个:SYN 载荷 6 字节,TFO 选项长度 [10],cookie 前 4 字节 533701b8;SYN+ACK 的 TFO 选项 无,数据被接受 ← 换密钥之后
换密钥之后,旧 cookie 被拒,SYN+ACK 的 ack 只有 seq + 1 并带来新 cookie,数据在握手后重发,不丢;下一个连接起用新 cookie。
连接总数 40(前 3 个无丢包;其后 30 个在 30% 丢包下;之后每 10 秒一次);丢包阶段 connectx/recv 失败 0 次
无丢包第 1 个:0B/TFO2
无丢包第 2 个:7B/TFO10
无丢包第 3 个:7B/TFO10
丢包第 1 个:6B/TFO10
丢包第 2 个:6B/TFO10 → 0B/无TFO
丢包第 3 个:6B/TFO10
丢包第 4 个:6B/TFO10 → 0B/无TFO
丢包第 5 个:6B/TFO10 → 0B/无TFO
丢包第 6 个:0B/无TFO
丢包第 7 个:0B/无TFO
丢包第 8 个:0B/无TFO
丢包第 9 个:0B/无TFO
丢包第 10 个:0B/无TFO
丢包第 11 个:0B/无TFO
丢包第 12 个:0B/无TFO
丢包第 13 个:0B/无TFO
丢包第 14 个:0B/无TFO
丢包第 15 个:0B/无TFO
丢包第 16 个:0B/无TFO
丢包第 17 个:0B/无TFO
丢包第 18 个:0B/无TFO
丢包第 19 个:0B/无TFO
丢包第 20 个:0B/无TFO
丢包第 21 个:0B/无TFO
丢包第 22 个:0B/无TFO
丢包第 23 个:0B/无TFO
丢包第 24 个:0B/无TFO
丢包第 25 个:0B/无TFO
丢包第 26 个:0B/无TFO
丢包第 27 个:0B/无TFO
丢包第 28 个:0B/无TFO
丢包第 29 个:0B/无TFO
丢包第 30 个:0B/无TFO
关掉丢包之后:
+ 0 s:0B/无TFO
+ 10 s:0B/无TFO
+ 20 s:0B/无TFO
+ 30 s:0B/无TFO
+ 40 s:0B/无TFO
+ 50 s:0B/无TFO
+ 60 s:0B/无TFO
+ 70 s:0B/无TFO
+ 80 s:0B/无TFO
+ 90 s:0B/无TFO
+ 100 s:0B/无TFO
+ 110 s:0B/无TFO
+ 120 s:0B/无TFO
+ 130 s:0B/无TFO
+ 140 s:0B/无TFO
+ 150 s:0B/无TFO
+ 160 s:0B/无TFO
+ 170 s:0B/无TFO
+ 180 s:0B/无TFO
+ 190 s:0B/无TFO
+ 200 s:0B/无TFO
+ 210 s:0B/无TFO
+ 220 s:0B/无TFO
+ 230 s:0B/无TFO
+ 240 s:0B/无TFO
+ 250 s:0B/无TFO
+ 260 s:0B/无TFO
+ 270 s:0B/无TFO
+ 280 s:0B/无TFO
+ 290 s:0B/无TFO
+ 300 s:0B/无TFO
+ 310 s:0B/无TFO
+ 320 s:0B/无TFO
+ 330 s:0B/无TFO
+ 340 s:0B/无TFO
+ 350 s:0B/无TFO30% 丢包下,SYN 带数据的重传 3 次之后,客户端对该目的地不再发 TFO 选项;丢包关掉后至少 40 分钟没有恢复。
同一目的地、新端口:0B/无TFO;0B/无TFO;0B/无TFO
新目的地 127.77.0.22:0B/TFO2;6B/TFO10;6B/TFO10
+ 1 s:0B/无TFO
+ 61 s:0B/无TFO
+ 121 s:0B/无TFO
+ 181 s:0B/无TFO
+ 2281 s:0B/无TFO
+ 2341 s:0B/无TFO
+ 2401 s:0B/无TFO
共轮询 41 次,覆盖 2401 秒;最后一次 SYN 载荷 0 字节;没有恢复停用是按目的地址记的:同一目的地换端口仍不尝试,别的目的地不受影响;之后每 60 秒试一次,共 41 次、覆盖 2401 秒,没有恢复。
清掉缓存前 ip tcp_metrics 里的 TFO 记录:1 条带 fo_cookie
清掉后:0 条带 fo_cookie
耗时(ms):普通 connect + send ['313.5', '215.0', '213.0', '212.5'];TFO(第 1 次取 cookie)['214.1', '103.7', '110.7', '108.7', '108.8'];清缓存后 TFO ['214.8', '102.3']
普通 4 次中位 214 ms;TFO 里 SYN 带数据的 4 次中位 109 ms
每个连接的 SYN(按时间顺序:4 个普通、5 个 TFO、2 个清缓存后):
第 1 个:SYN 载荷 0 字节,TFO 选项长度 无,cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无
第 2 个:SYN 载荷 0 字节,TFO 选项长度 无,cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无
第 3 个:SYN 载荷 0 字节,TFO 选项长度 无,cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无
第 4 个:SYN 载荷 0 字节,TFO 选项长度 无,cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 无
第 5 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 a407c2fe
第 6 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据被接受
第 7 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据被接受
第 8 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据被接受
第 9 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据被接受
第 10 个:SYN 载荷 0 字节,TFO 选项长度 [2],cookie 前 4 字节 -;SYN+ACK 的 TFO 选项 a407c2fe
第 11 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据被接受
服务端收到的数据:n0 n1 n2 n3 t0 t1 t2 t3 t4 f0 f1同样 100 ms 往返,Linux 的 TFO 连接中位 109 ms,普通连接 214 ms;ip tcp_metrics flush 清掉缓存后又要重新取 cookie。
3 次耗时(ms):['217.3', '216.1', '213.6']
第 1 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据没被接受(ack = seq + 1)
第 2 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据没被接受(ack = seq + 1)
第 3 个:SYN 载荷 2 字节,TFO 选项长度 [10],cookie 前 4 字节 a407c2fe;SYN+ACK 的 TFO 选项 无,数据没被接受(ack = seq + 1)
服务端(7001)收到的数据:x0 x1 x2Linux 客户端把 cookie 和数据发给没开 TFO 的端口:数据没被确认,握手后重发,不丢不重,耗时与普通连接一样。
(容器网卡开着发送校验和卸载,抓到的出站包里 TCP 校验和是占位值,直接重发会被服务端丢弃——第一次尝试时服务端计数 TcpInCsumErrors = 2,重发的包没有生效;下面是重新计算校验和之后的结果) 抓到的 SYN:载荷 7 字节,TFO 选项长度 [10] 服务端内核计数 TcpExtTCPFastOpenPassive(收到有效 cookie 的 SYN 并建立子连接的次数)增加了 5(1 次原始 + 重放) 服务端应用收到的记录(9 条):['order-0', 'order-0', "ERR ConnectionResetError(104, 'Connection reset by peer')", 'order-0', "ERR ConnectionResetError(104, 'Connection reset by peer')", 'order-0', "ERR ConnectionResetError(104, 'Connection reset by peer')", 'order-0', "ERR ConnectionResetError(104, 'Connection reset by peer')"] 其中应用读到 order-0 的有 5 次,另有 4 条是随后收到客户端 RST 的报错(客户端内核没有对应的套接字,对重放引发的 SYN+ACK 回 RST)
原样重发抓到的 SYN,服务端应用又读到同一份数据:5 次读取里有 4 次来自重放。所以只适合幂等请求。
tcp_fastopen_blackhole_timeout_sec = 0
丢弃期间 5 次耗时(ms):['1269.1', '1227.7', '1228.9', '1227.0', '1231.1'];撤掉规则后再 3 次:['106.0', '106.2', '103.0']
第 1 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B, +1.1s/0B
第 2 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B, +1.0s/0B
第 3 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B, +1.0s/0B
第 4 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B, +1.0s/0B
第 5 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B, +1.0s/0B
第 6 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B
第 7 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B
第 8 个连接的 SYN(相对时间 s / 载荷):+0.0s/2B
客户端内核计数(从本组容器启动起累计,其中第十节的 3 次 + 本节的 5 次 = 8 次 SYN 里的数据没被对端确认):TcpExtTCPFastOpenActiveFail 8;TcpExtTCPFastOpenBlackhole 0;
服务端收到的数据:d0 d1 d2 d3 d4 e0 e1 e2带数据的 SYN 被丢掉时,Linux 约 1 秒后重传无数据的 SYN,每个连接多等约 1.0 秒;这个内核里的黑洞检测没启用,连续 5 次都是这样。