|
凌晨三点,工程我盯着 Wireshark 里那条重传了七次的工程 SYN 包,心里骂了一句:又是工程 MTU 惹的祸。对面那台嵌入式设备死活不认 1500 字节的工程 MSS,硬生生把三次握手拖成了慢动作回放。工程这就是工程 TCP,一个你以为懂、工程其实每天都在打脸的工程东西。 很多人把 TCP 当成“可靠传输”四个字就完事了。工程扯淡。工程可靠是工程靠代价换来的。每个字节都有序号,工程每个包都要 ACK,工程发出去的工程数据得在内存里排队等着被确认。内核里那个叫 sk_buff 的工程结构体,说白了就是快递单加包裹,丢一个就得重发,重发还得看拥塞窗口的脸色。 我第一次被 TCP 教做人,是在一个跨机房同步项目里。两台机器 ping 延迟 0.3ms,理论带宽 10Gbps,结果 scp 跑出来只有 200Mbps。查了半天,罪魁祸首是初始拥塞窗口——Linux 3.0 之前默认才 4 个 MSS,RFC 6928 之后提到 10 个。就这么点事,卡了整整一代工程师的脖子。你算算,10 个 MSS 按 1460 字节算,也就 14.6KB,RTT 0.3ms 的情况下,理论吞吐量撑死 390Mbps。想跑满?等慢启动翻倍吧,翻到 10MB 窗口得十几个 RTT。跨机房那点延迟,全喂给慢启动了。 TCP 的拥塞控制才是真正的水下冰山。 Reno、CUBIC、BBR,名字听着像乐队,干的事都是猜网络什么时候堵。Reno 靠丢包判断,丢了就砍一半窗口——这逻辑放今天就是耍流氓。Wi-Fi 丢包太正常了,信号干扰、信道竞争,跟拥塞半毛钱关系没有。结果 Reno 一看丢包就缩,明明带宽空着也不敢发。CUBIC 稍微聪明点,用三次函数找窗口拐点,但骨子里还是丢包驱动。直到 Google 搞出 BBR,直接测带宽和 RTT,不鸟丢包。我在生产环境切过 BBR,跨太平洋线路吞吐量从 300Mbps 跳到 2.3Gbps。但 BBR 也不是神仙,跟 CUBIC 混跑的时候抢带宽抢得跟饿狼似的,公平性一塌糊涂。 再说个反直觉的:TCP 不保证送达。它只保证“要么按序送到,要么告诉你断了”。你 write 了 100 字节,内核返回 100,只说明数据进了发送缓冲区,不代表对面收到了。真要确认,得等 ACK 回来,或者干脆等连接关闭。我见过有人写金融交易系统,以为 send 返回成功就完事,结果对端进程崩了,数据卡在中间路由器上,钱没了。TCP 的可靠性是端到端的,中间任何一跳都可能把包吃了,它只能靠超时重传硬扛。超时时间怎么算?RTO 最小值 Linux 里是 200ms,基于 RTT 平滑和方差估算。RTT 抖动大的线路,RTO 能飙到几秒,那几秒里连接跟死了没区别。 还有个坑是 TIME_WAIT。主动关闭方要等 2MSL,Linux 默认 60 秒。高并发短连接场景,比如 Nginx 反代,几万个 TIME_WAIT 堆在那,端口耗尽直接拒绝服务。调 tcp_tw_reuse 能缓解,但前提是时间戳选项开启,否则可能把旧连接的延迟包当成新连接的,数据就串了。我亲眼见过一个电商大促,LVS 后面几千台机器,就因为 TIME_WAIT 没调优,零点刚过集体雪崩。 写 TCP 代码最怕自作聪明。有人为了“优化”自己实现 ACK 延迟,有人关掉 Nagle 算法结果小包满天飞。Nagle 和延迟 ACK 放一起就是经典死锁:你等 ACK 再发,我等数据再 ACK,互相干瞪眼。所以 HTTP 里才有 TCP_NODELAY,浏览器和服务器都设上,宁可多发几个小包也别卡着。 说到底,TCP 是四十多年前的设计,能活到今天靠的是足够笨、足够稳。它不完美,慢启动慢、拥塞控制瞎猜、TIME_WAIT烦人。但你要真把它当黑盒,它就在你最忙的时候给你一记闷棍。我的习惯是:任何长连接服务上线前,先抓包看握手、看窗口缩放、看 SACK 有没有开。别信“默认配置就行”,内核参数是给通用场景调的,你的场景未必通用。 凌晨四点,我把 MSS 钳到 1400,重传没了,握手一次过。关掉 Wireshark,心想:TCP 这东西,你越觉得它简单,它越要教你做人。 |