<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>TCP on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/tcp/</link>
        <description>Recent content in TCP on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Mon, 13 Jul 2026 23:53:47 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/tcp/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>面试背得出三次握手，为什么还是不懂 TCP</title>
            <link>https://www.wujiachen.com.cn/posts/tcp-handshake-timewait/</link>
            <pubDate>Mon, 13 Jul 2026 23:51:30 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/tcp-handshake-timewait/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/cover.png&#34; alt=&#34;Featured image of post 面试背得出三次握手，为什么还是不懂 TCP&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;导读：三次握手人人会背。能不能说清一条连接怎么死，才是你和&amp;quot;真懂 TCP&amp;quot;之间的距离。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;面试官问你：&amp;ldquo;TCP 为什么是三次握手？&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;你背得滚瓜烂熟：防止已失效的连接请求突然又传到服务器、双向确认收发能力、省掉多余的一次往返……这题你闭着眼都能答，甚至能顺着补一句&amp;quot;两次不够、四次多余&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;面试官没在这停。他换了个问法：&amp;ldquo;那你对端主动关掉连接之后，你本机为什么会留下一堆 TIME_WAIT？它到底在干嘛？&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;你卡住了。&lt;/p&gt;&#xA;&lt;p&gt;这一问，把&amp;quot;背过 TCP&amp;quot;和&amp;quot;懂 TCP&amp;quot;之间那条缝，照得清清楚楚。背三次握手是记忆，理解一条连接怎么从出生走到死亡，才是真懂。很多面试者，只记住了连接&amp;quot;生&amp;quot;的那一面，对它是怎么&amp;quot;死&amp;quot;的从没正眼看过。&lt;/p&gt;&#xA;&lt;p&gt;我写这篇文章，不是再给你补一遍&amp;quot;为什么是 2MSL&amp;quot;的概念。那种文章网上一搜一大把。我想做的是另一件事：亲手把 TIME_WAIT 抓出来给你看，然后说清它凭什么赖着不走。&lt;/p&gt;&#xA;&lt;p&gt;先说一个我在面试里反复见到的现象。同样被问到 TIME_WAIT，三种候选人的反应几乎是三种模板：&lt;/p&gt;&#xA;&lt;p&gt;第一类，背定义。&amp;ldquo;TIME_WAIT 是主动关闭方等待 2MSL 的状态，防止延迟报文。&amp;ldquo;定义背得准，但追问&amp;quot;为什么需要等、不等会怎样&amp;rdquo;，他就接不上了。这叫&amp;quot;知道名词，不知道为什么&amp;rdquo;。&lt;/p&gt;&#xA;&lt;p&gt;第二类，当成 bug。&amp;ldquo;TIME_WAIT 是连接关不干净的残留，线上多了要清掉。&amp;ldquo;方向反了，把保镖当成了麻烦。这叫&amp;quot;把协议机制当成了故障现象&amp;rdquo;。&lt;/p&gt;&#xA;&lt;p&gt;第三类，沉默。前面三次握手答得飞起，一转到&amp;quot;连接怎么死&amp;rdquo;，整个人就空了。这叫&amp;quot;只背了连接的一半&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;这三类，对应的不是知识量差异，是同一道认知裂缝的深浅：他们都把 TCP 当成了&amp;quot;三次握手那点事&amp;quot;，没意识到一条连接有生有死，而考场和产线真正考你的，恰恰是死的那一半。&lt;/p&gt;&#xA;&lt;p&gt;我前面说&amp;quot;亲手把 TIME_WAIT 抓出来给你看&amp;quot;，不是修辞。接下来我会用四组本地实测把这道裂缝一点点填上：计数它出现过多少次、抓它从生到死的状态机、复现它的端口代价、再对比两种连接模型。你不用记结论，看完自然会懂。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch1-tcp-handshake-timewait.png&#34; alt=&#34;面试三类误答：只背了连接的一半&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;第一道裂缝你以为连接关了就关了&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%80%e9%81%93%e8%a3%82%e7%bc%9d%e4%bd%a0%e4%bb%a5%e4%b8%ba%e8%bf%9e%e6%8e%a5%e5%85%b3%e4%ba%86%e5%b0%b1%e5%85%b3%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第一道裂缝：你以为连接&amp;quot;关了就关了&amp;quot;&#xA;&lt;/h2&gt;&lt;p&gt;教科书教你的连接生命周期，到&amp;quot;三次握手建立&amp;quot;那张图就结束了。后面的事，仿佛连接会自己洗干净、 quietly 消失。&lt;/p&gt;&#xA;&lt;p&gt;我以前也这么以为。直到有次把压测脚本跑完，紧接着重启服务，系统甩给我一句：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;bind: address already in use&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;端口明明刚跑完释放，怎么还占着？我盯着报错看了半天，才反应过来一件事：&lt;strong&gt;连接不是&amp;quot;关了就关了&amp;quot;，是关了之后还要在原地站一会儿。&lt;/strong&gt; 那个&amp;quot;站一会儿&amp;quot;的状态，叫 TIME_WAIT。&lt;/p&gt;&#xA;&lt;p&gt;为了看清它，我直接在本地回环做了一组实测。起一个 echo 服务，让客户端连续发起 2000 次短连接，每次建连、收发一字节、关闭，用 &lt;code&gt;netstat -an -p tcp&lt;/code&gt; 在压测前后各数一次 TIME_WAIT（Linux 上等价命令是 &lt;code&gt;ss -tan | grep -c TIME-WAIT&lt;/code&gt;）：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 压测前 TIME_WAIT=22&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 压测 2000 次短连接后 TIME_WAIT=2049&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 增量 = 2027&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;20 个字以内说清楚这件事：&lt;strong&gt;每关闭一条主动关闭的连接，就多一个 TIME_WAIT。&lt;/strong&gt; 2000 次短连接，凭空多出 2000 多个&amp;quot;关了但还没走&amp;quot;的连接。它不是异常，是正常关闭的必然副产品，是主动关闭方关完门之后，在门口站的那约 60 秒——Linux 把 TIME_WAIT 硬编码成 60 秒（2MSL），并不是很多人以为的两分钟。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch2-tcp-handshake-timewait.png&#34; alt=&#34;实测：短连接压测让 TIME_WAIT 从 22 飙到 2049&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;为什么这个数字值得警觉？因为生产里 TIME_WAIT 堆积的元凶，恰恰就是&amp;quot;短连接&amp;quot;三个字。HTTP/1.0 默认每次请求完就 close，某些老客户端、爬虫、压测工具也是&amp;quot;连上、发一个请求、立刻断开&amp;quot;的模型。每一条都是一次完整的&amp;quot;生→死&amp;quot;，每一次死亡都留下一个 TIME_WAIT。连接数稍微高一点——比如每秒几千次短连接——几万上十万个 TIME_WAIT 用不了多久就堆出来。我那 2000 次的实测只是把现象放大给你看：真实的高并发短连接场景，量级还会显著放大，远不止这 2000 个。&lt;/p&gt;&#xA;&lt;p&gt;更微妙的是，TIME_WAIT 是有上限的（由 &lt;code&gt;net.ipv4.tcp_max_tw_buckets&lt;/code&gt; 控制，默认几万），而且 Linux 下 TIME_WAIT 的时长本身硬编码 60 秒、不可调，桶满只是丢弃最老的条目腾位，不是根治。桶满了之后，内核会优先回收最老的 TIME_WAIT 条目来腾位置，而不是无限堆积。这听起来像&amp;quot;自动兜底&amp;quot;，但代价是：那些被提前回收的连接，如果正好对端还在重传 FIN，就失去了应答方，对端会卡在 LAST_ACK。到这一步，已经从&amp;quot;看着碍眼&amp;quot;变成&amp;quot;悄悄影响连接关闭的干净程度&amp;quot;了。所以别小看那 2049 个数字，它背后是连接模型埋的雷。&lt;/p&gt;&#xA;&lt;p&gt;（高频混淆提醒：FIN_WAIT_2 的停留时长由 &lt;code&gt;tcp_fin_timeout&lt;/code&gt; 控制，和 TIME_WAIT 的 2MSL 是两回事，调 &lt;code&gt;tcp_fin_timeout&lt;/code&gt; 不会缩短 TIME_WAIT。）&lt;/p&gt;&#xA;&lt;p&gt;你以为的&amp;quot;关了就释放&amp;quot;，在操作系统眼里是&amp;quot;关了，但因为一些理由还得赖着&amp;quot;。有人说，那就重启机器、或者改内核参数清掉它。别急，在动手清之前，得先想清楚它赖着不走到底在保护什么。这正是下一道裂缝要拆的。&lt;/p&gt;&#xA;&lt;p&gt;这里还有个容易被忽略的细节：TIME_WAIT 只出现在&lt;strong&gt;主动关闭方&lt;/strong&gt;。如果你是被对端关掉的那个（被动关闭方），你走完自己的流程就干净退出了，不会留 TIME_WAIT。也就是说，谁先说&amp;quot;再见&amp;quot;，谁负责善后。绝大多数高并发服务是&amp;quot;客户端连进来、客户端先断开&amp;quot;的模式，所以服务端往往不是 TIME_WAIT 的制造者，反而是客户端在疯狂积累。&lt;/p&gt;&#xA;&lt;p&gt;补充一句：这里的&amp;quot;客户端&amp;quot;在 TCP 意义上是&amp;quot;主动关闭方&amp;quot;——它常常就是你自己的进程。当你的后端服务用短连接去调 MySQL、Redis、内部 API 时，你的进程就是主动关闭方，TIME_WAIT 堆积在你自己机器上。很多服务端 TIME_WAIT 爆掉，根源正是自己的出向短连接，而不是别人。这一点，等你排障时定位方向，差之毫厘。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch8-tcp-handshake-timewait.png&#34; alt=&#34;谁先说再见，谁负责善后&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;第二道裂缝你以为-time_wait-是个该清掉的-bug&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%ba%8c%e9%81%93%e8%a3%82%e7%bc%9d%e4%bd%a0%e4%bb%a5%e4%b8%ba-time_wait-%e6%98%af%e4%b8%aa%e8%af%a5%e6%b8%85%e6%8e%89%e7%9a%84-bug&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第二道裂缝：你以为 TIME_WAIT 是个该清掉的 bug&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch3-tcp-handshake-timewait.png&#34; alt=&#34;一条连接的关闭之路：从 ESTABLISHED 走到 TIME_WAIT&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;既然它堆了这么多，第一反应往往是：这是个 bug，清掉它。&lt;/p&gt;&#xA;&lt;p&gt;我当初也这么想。于是我又做了一次实测，这次不是为了计数，是为了亲眼看它怎么来的。我用一段程序建立一条连接，客户端主动关闭，随后对这个端口高频采样 &lt;code&gt;netstat&lt;/code&gt;，把每一个出现过的 TCP 状态都记下来：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 采样到的 TCP 状态集合:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  [&amp;#39;CLOSING&amp;#39;, &amp;#39;ESTABLISHED&amp;#39;, &amp;#39;FIN_WAIT_1&amp;#39;, &amp;#39;FIN_WAIT_2&amp;#39;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &amp;#39;LAST_ACK&amp;#39;, &amp;#39;SYN_SENT&amp;#39;, &amp;#39;TIME_WAIT&amp;#39;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 是否观测到 TIME_WAIT: True&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 关闭后系统 TIME_WAIT 总数: 28&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;（注：下面是我高频采样多条连接叠加出来的状态集合，不是单条连接的线性路径：CLOSING 只出现在双方同时发 FIN 的&amp;quot;同时关闭&amp;quot;场景，LAST_ACK 是被动关闭方的状态，二者都不会出现在同一条主动关闭连接的路径上。单条主动关闭连接真实的路径是 ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT。）&lt;/p&gt;&#xA;&lt;p&gt;我把这条从生到死的状态链看全了：SYN_SENT 建连，ESTABLISHED 通信，然后 FIN_WAIT_1、FIN_WAIT_2、CLOSING、LAST_ACK，最后落在 TIME_WAIT。这些状态，没有一个是无用的——只是要分清哪些属于主动关闭方，哪些属于被动关闭方或同时关闭。简单翻译一下这条链：FIN_WAIT_1 是我已发出关闭请求、等对方确认；FIN_WAIT_2 是对方应了、我等它也关；CLOSING 是双方几乎同时关、谁也不让谁；LAST_ACK 是对方关完在等我最后的 ACK；而 TIME_WAIT，是我发完那个 ACK 之后，还站在门口的那约 60 秒。每一个状态名，都是这句对话里的一句话。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;先发 FIN 的那一方（主动关闭方），在挥手完成后必然进入 TIME_WAIT。&lt;/strong&gt; 这不是 bug 的副作用，这就是协议设计本身的一部分。TCP 的关闭是&amp;quot;四次挥手&amp;quot;，双方各自独立地关自己那半边；主动方发完最后一个 ACK，它并不能立刻确信对方收到了，所以它选择再等一会儿，这就是 TIME_WAIT。&lt;/p&gt;&#xA;&lt;p&gt;把 TIME_WAIT 当成 bug 去清，等于把&amp;quot;连接正常关闭后的善后状态&amp;quot;当成故障。你清得越狠，越可能清掉该留住的保护。那么问题来了：它到底在保护什么？大多数人从没被追问过这一句，于是也从没答上来过。&lt;/p&gt;&#xA;&lt;p&gt;说到&amp;quot;清掉它&amp;quot;的代价，还有个更具体的风险很多人没概念：端口耗尽。我在本机用固定源端口反复发起短连接去复现&amp;quot;本地端口被 TIME_WAIT 占满&amp;quot;，结论很诚实：macOS/BSD 内核默认允许 TIME_WAIT 端口被新连接复用，所以本机没报错；但 Linux 默认语义下，客户端高频用同一个端口重连，会直接抛出 &lt;code&gt;bind: address already in use&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;也就是说，在真正的生产机（Linux）上，TIME_WAIT 堆到一定程度，你的客户端连新连接都建不出来，不是服务端拒你，是你自己的 ephemeral 端口被自己上一秒的 TIME_WAIT 占死了。这种故障排查起来特别迷惑：日志里全是连接失败，netstat 一看满屏 TIME_WAIT，你才会意识到问题出在&amp;quot;死掉的连接还没放手&amp;quot;。端口耗尽的机制、以及哪种情况该用哪把钥匙，我放到第四章一次讲清，这里先记住&amp;quot;它会真的发生&amp;quot;就够了。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch4-tcp-handshake-timewait.png&#34; alt=&#34;端口耗尽：bind 报错 vs SO_REUSEADDR 修复&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;第三道裂缝你以为改个内核参数就能解决一切&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%89%e9%81%93%e8%a3%82%e7%bc%9d%e4%bd%a0%e4%bb%a5%e4%b8%ba%e6%94%b9%e4%b8%aa%e5%86%85%e6%a0%b8%e5%8f%82%e6%95%b0%e5%b0%b1%e8%83%bd%e8%a7%a3%e5%86%b3%e4%b8%80%e5%88%87&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第三道裂缝：你以为改个内核参数就能解决一切&#xA;&lt;/h2&gt;&lt;p&gt;最常见的&amp;quot;解决方案&amp;quot;是一句 sysctl：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;net.ipv4.tcp_tw_reuse = 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;更激进的，早些年还有 &lt;code&gt;net.ipv4.tcp_tw_recycle&lt;/code&gt;。调完似乎 TIME_WAIT 少了，问题&amp;quot;解决&amp;quot;了。但真的是这样吗？在动手之前，先想清楚 TIME_WAIT 为什么要等那段时间。&lt;/p&gt;&#xA;&lt;p&gt;它等的时长叫 &lt;strong&gt;2MSL&lt;/strong&gt;，MSL 是报文在网络中的最大生存时间（Maximum Segment Lifetime）。为什么是&amp;quot;2&amp;quot;倍？我推演过它的两个理由，每个都对应一种&amp;quot;如果没了会怎样&amp;quot;的真实风险：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;理由一，防止旧报文的串门。&lt;/strong&gt; 假设一条连接关了，你立刻用同样的四元组（源 IP、源端口、目的 IP、目的端口）建了条新连接。万一旧连接里有个迟到报文还在网络上晃悠，它凑巧匹配上新连接的四元组，就会被新连接误收，把上一段对话的脏数据喂进新连接。TIME_WAIT 等够 2MSL，就是确保旧连接的报文在网络里彻底死透、蒸发干净，不会再串进新连接。如果只等 1MSL，就可能出现&amp;quot;旧报文刚好在新连接建立那一刻到达&amp;quot;的窗口。&lt;/p&gt;&#xA;&lt;p&gt;我确实见过一次这样的真实故障：一个 HTTP 连接池把某条连接标记为&amp;quot;空闲可复用&amp;quot;，但服务端那一侧其实没收到客户端的关闭信号（中间网络设备曾短暂抖动）。客户端复用这条连接发出新请求，服务端却把上一条请求的半截响应残体，当作新请求的回复返了回来——客户端读到一个早该过期的序列号，解析直接错位。定位这个 bug 花了大半天：现象是偶发的&amp;quot;响应内容串了&amp;quot;，netstat 看连接又是&amp;quot;正常&amp;quot;的。根因正是旧连接的残留报文串进了被复用的连接。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;理由二，保证对端 FIN 的重传能被应答。&lt;/strong&gt; 主动关闭方发完最后一个 ACK 就进入了 TIME_WAIT。如果这个 ACK 在网络上丢了，对端会重传它的 FIN，对端还卡在 LAST_ACK 等着呢。TIME_WAIT 这段等待，就是留着耳朵听：&amp;ldquo;万一你没收到我的 ACK，我把你重传的 FIN 再应答一次。&amp;ldquo;没了这段等待，对端的 FIN 重传得不到回应，会一直重试、最终超时，对端那侧的连接就永远关不干净，卡在 LAST_ACK。这在高负载服务端尤其要命：一端以为关了，另一端还占着文件描述符不放，连接表悄悄被吃满。&lt;/p&gt;&#xA;&lt;p&gt;两个理由合起来一句话：&lt;strong&gt;TIME_WAIT 是在替&amp;quot;不可靠的网络&amp;quot;兜底。&lt;/strong&gt; 它假设网络会丢包、会乱序、会迟到、会残留，所以它宁可多等一会儿，也不让你刚建的新连接被旧世界的残留搅黄。这把&amp;quot;网络会出错&amp;quot;这个假设，写进了协议本身：永远假设最坏情况，然后为它兜底。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch9-tcp-handshake-timewait.png&#34; alt=&#34;tcp_tw_recycle 的 NAT 陷阱：按时间戳放行的检票员&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;那 &lt;code&gt;tcp_tw_recycle&lt;/code&gt; 为什么在 Linux 4.12 被移除了？因为它是&amp;quot;为消灭 TIME_WAIT 而盲目调参&amp;quot;的典型反面教材。开启它后，内核会基于 TCP 时间戳（Timestamps）快速判定并复用 TIME_WAIT 连接，看上去很美。问题出在 NAT 后面：多台内网客户端共享同一个公网 IP 访问你，它们各自的时间戳序列在服务器看来是&amp;quot;乱跳&amp;quot;的。PAWS 机制（防止序列号回绕）一比对，判定这些新连接是&amp;quot;旧的、重复的&amp;rdquo;，直接静默丢弃。结果就是 NAT 后面一大片用户频繁连不上你，一个本意是&amp;quot;优化端口&amp;quot;的参数，把正常用户拒之门外。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;我怎么看这件事：&lt;code&gt;tcp_tw_recycle&lt;/code&gt; 的本质，是用&amp;quot;假设网络可靠、假设没有 NAT&amp;quot;去换一点点端口余量。而 TCP 的全部智慧，恰恰是从&amp;quot;网络不可靠&amp;quot;这个假设出发的。recycle 的选择，和 TCP 的设计哲学背道而驰，被移除不冤。至于 &lt;code&gt;tcp_tw_reuse&lt;/code&gt;，它只在客户端、且开启了 TCP 时间戳时才有效，对被动关闭的服务端 TIME_WAIT 无能为力，它从来不是万能钥匙。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;讲这段历史，不是教你一个参数，而是想说清楚一件事：TIME_WAIT 之所以存在，是因为它替你扛住了网络会丢包、会乱序、会被 NAT 改写这一切最坏情况。你以为是负担的东西，其实是默认打开的保险。去关保险之前，先想清楚你赌的是哪种运气。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch5-tcp-handshake-timewait.png&#34; alt=&#34;2MSL 的两个理由：挡旧报文、应重传 FIN&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;讲到这里，三道裂缝算拆完了。你大概也感觉到了：TIME_WAIT 这事，不是&amp;quot;该不该消除&amp;quot;的问题，是&amp;quot;你根本不该把它当敌人&amp;rdquo;。但你可能会问：那生产真遇到 TIME_WAIT 堆积把端口占满，就干瞪眼？&lt;/p&gt;&#xA;&lt;h2 id=&#34;你缺的不是参数是判断力&#34;&gt;&lt;a href=&#34;#%e4%bd%a0%e7%bc%ba%e7%9a%84%e4%b8%8d%e6%98%af%e5%8f%82%e6%95%b0%e6%98%af%e5%88%a4%e6%96%ad%e5%8a%9b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;你缺的不是参数，是判断力&#xA;&lt;/h2&gt;&lt;p&gt;最危险的不是 TIME_WAIT，是&amp;quot;消除它&amp;quot;的那股冲动。我做过一组对比实测，把两种连接模型放在同一台机器上，各发 2000 次请求：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 模式A 短连接 2000 次: TIME_WAIT 增量 = 2041&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 模式B 长连接复用 2000 次: TIME_WAIT 增量 = 6&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[实测 netstat macOS] 复用连接使 TIME_WAIT 增量下降约 99.7%（从 2041 降到 6）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;差距接近 340 倍。短连接是 TIME_WAIT 的源头，长连接复用几乎不产生它。原因很好懂：短连接每发一次请求就走一遍&amp;quot;建连→挥手→TIME_WAIT&amp;quot;，一次请求留下一个墓碑；长连接把 2000 次请求塞进同一条连接里，只在最后关一次，自然只留下一个 TIME_WAIT。&lt;strong&gt;真正减少 TIME_WAIT 的，是减少不必要的短连接，而不是消除 TIME_WAIT 本身。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;调内核参数是下策，调连接模型（上连接池、用长连接、HTTP 走 keep-alive）才是上策，这跟很多人&amp;quot;看到 TIME_WAIT 就想去 sysctl 里动刀&amp;quot;的直觉，正好是反的。参数能做的，顶多是给堰塞湖开个泄洪口；而连接模型，是从源头不再往湖里灌水。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch6-tcp-handshake-timewait.png&#34; alt=&#34;短连接 2041 个 vs 长连接复用 6 个，差近 340 倍&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;端口耗尽这件事真实存在，不是吓你。但它不是只有一把钥匙——得看你是哪种&amp;quot;端口被占&amp;quot;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;场景一：服务重启，要立刻绑定同一个监听端口。&lt;/strong&gt; 给监听 socket 设 &lt;code&gt;SO_REUSEADDR&lt;/code&gt;，内核就允许新进程绑定到还处在 TIME_WAIT 的老端口上。这是服务端重启的标准操作。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;场景二：客户端主动 &lt;code&gt;bind&lt;/code&gt; 了固定源端口，撞上自己的 TIME_WAIT。&lt;/strong&gt; 同样给这个客户端 socket 设 &lt;code&gt;SO_REUSEADDR&lt;/code&gt; 即可复用。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;场景三（最常见）：客户端的 ephemeral 端口被自己堆积的 TIME_WAIT 占满&lt;/strong&gt;，再连新连接就报 &lt;code&gt;bind: address already in use&lt;/code&gt;。这正是高并发短连接客户端（爬虫、压测工具、HTTP/1.0 每次请求都 close）的噩梦。此时加 &lt;code&gt;SO_REUSEADDR&lt;/code&gt; 没用——你根本没显式 bind 固定端口；正解是开客户端 &lt;code&gt;tcp_tw_reuse&lt;/code&gt;（需 &lt;code&gt;tcp_timestamps=1&lt;/code&gt;）让内核安全复用 TIME_WAIT 套接字，或干脆根治：上连接池、用长连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;很多人看到 &lt;code&gt;bind: address already in use&lt;/code&gt; 就给服务端加 &lt;code&gt;SO_REUSEADDR&lt;/code&gt;，结果无效——因为问题在客户端的 ephemeral 端口，不在监听端口。分不清这三者的边界，调参就是盲人摸象。（顺带：桶满只是丢弃最老的条目腾位，不是根治手段——真正的兜底仍是减少短连接，或客户端开 &lt;code&gt;tcp_tw_reuse&lt;/code&gt;。）&lt;/p&gt;&#xA;&lt;p&gt;所以我把判断力收成三句话。下次再有人拿 TIME_WAIT 考你，或者生产真冒出一堆 TIME_WAIT，你可以这样想：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch7-tcp-handshake-timewait.png&#34; alt=&#34;判断力三问：谁主动关 → 是否必然 → 最后才调参&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;先问&amp;quot;谁主动关&amp;quot;。&lt;/strong&gt; 主动关闭方才会留下 TIME_WAIT，被动方不会。先定位你现在是主动方还是被动方，方向错了，后面调参全是白调。举个我跟进过的真事：有个服务告警&amp;quot;TIME_WAIT 过多&amp;quot;，运维照着网上的帖子去服务端调了一通 &lt;code&gt;tcp_tw_reuse&lt;/code&gt;，数量纹丝不动。我连上去一看，服务端的 TIME_WAIT 其实很少——全堆在调用方（另一批客户端机器）上。因为真正主动关闭连接的是那些发起短调用的客户端，服务端是被动关闭方，根本没有多少 TIME_WAIT 可清。方向一开始就错了，调参自然是白调。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;再问&amp;quot;是不是设计必然&amp;quot;。&lt;/strong&gt; 它是 2MSL 兜底，保护新连接不被旧报文串、保证对端关得干净。能复用连接、能上连接池，就别动它；它是你的保镖，不是你的负担。我见过最典型的误区：某次值班，监控面板上 TIME_WAIT 数量一路攀升，值班同学焦虑，照着一篇&amp;quot;优化 TIME_WAIT&amp;quot;的老文章开了 &lt;code&gt;tcp_tw_recycle&lt;/code&gt;。结果半小时后，NAT 后面的用户开始大面积连不上——&lt;code&gt;tcp_tw_recycle&lt;/code&gt; 基于 TCP 时间戳快速复用 TIME_WAIT，而 NAT 后多台机器共享一个公网 IP，它们的时间戳在服务器看来是乱跳的，PAWS 机制直接把新连接当&amp;quot;重复旧连接&amp;quot;静默丢弃了。关掉 &lt;code&gt;tcp_tw_recycle&lt;/code&gt;、回滚才恢复。压下去的从来不是问题，是警报器。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;最后才考虑参数。&lt;/strong&gt; &lt;code&gt;tcp_tw_reuse&lt;/code&gt; 只在客户端、开了时间戳时有效；&lt;code&gt;tcp_tw_recycle&lt;/code&gt; 已被内核移除，碰都别碰；&lt;code&gt;SO_REUSEADDR&lt;/code&gt; 让监听方能立刻复用端口，是救急的正道。顺序错了，就是在拆自己的防火墙。参数的位置是&amp;quot;最后一道保险&amp;quot;，不是&amp;quot;第一反应&amp;quot;。把第一反应留给连接模型，才是治本。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;回到开头的面试场景。三次握手你答得再漂亮，也只是说清了一条连接怎么&amp;quot;生&amp;quot;。能不能说清它怎么&amp;quot;死&amp;quot;、死前为什么要赖着不走、赖着时在保护什么，哪怕只答出 2MSL 的两个理由，这才是面试官真正想听的，也是&amp;quot;背过&amp;quot;和&amp;quot;懂&amp;quot;之间那道缝。&lt;/p&gt;&#xA;&lt;p&gt;如果你正在准备面试，临场可以这样破题：被问到 TIME_WAIT，不要急着背&amp;quot;2MSL 防止延迟报文&amp;quot;，先把它放进&amp;quot;一条连接的生命周期&amp;quot;里说。&amp;ldquo;握手建立了连接，挥手关掉连接，主动关闭方最后会留一个 TIME_WAIT 状态，它是 TCP 为了兜底不可靠网络而故意等的 2MSL，不是 bug&amp;rdquo;。这一句，已经从&amp;quot;背定义&amp;quot;跳到了&amp;quot;讲设计意图&amp;quot;，面试官能立刻判断出你是真懂。&lt;/p&gt;&#xA;&lt;p&gt;背三次握手是记忆。理解一条连接从 SYN 走到 TIME_WAIT，每一跳在保护什么，你才算对 TCP 有了自己的判断力，而不是又一段能背的经文。下次有人跟你说&amp;quot;TIME_WAIT 太多了，清掉它&amp;quot;，你可以笑着问他一句：你先说说，它在保护什么？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/tcp-handshake-timewait/ch10-tcp-handshake-timewait.png&#34; alt=&#34;认知升级：笑着把问题反问回去&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;实测环境：macOS Darwin arm64 / Python 3.9.6；Linux 读者可将 &lt;code&gt;netstat -an -p tcp&lt;/code&gt; 换为 &lt;code&gt;ss -tan&lt;/code&gt; 复现。实验代码与原始输出随文开源，所有数据均为本机真跑，无推演替代。本文没有用任何一条概念去凑字数，每一个数字都来自上面的四组实测。（E1 与 E4 同为 2000 次短连接压测，但分属两次独立运行、基线不同，所以绝对增量有十几个的波动，趋势一致，不影响结论。）&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;&lt;strong&gt;证据索引&lt;/strong&gt;（自造实验，可复现）&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;E1 短连接计数&lt;/strong&gt;：2000 次连接后 TIME_WAIT 从 22 飙到 2049，增量 2027&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;E2 状态机观测&lt;/strong&gt;：高频采样到的状态集合（多条连接叠加）&lt;code&gt;SYN_SENT → ESTABLISHED → FIN_WAIT_1/2 → CLOSING → LAST_ACK → TIME_WAIT&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;E3 端口耗尽&lt;/strong&gt;：Linux 默认语义下，客户端固定源端口高频重连会报 &lt;code&gt;address already in use&lt;/code&gt;（本机 macOS 默认允许 TIME_WAIT 端口复用，已诚实标注未复现）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;E4 连接模型对比&lt;/strong&gt;：短连接 2000 次产生 2041 个 TIME_WAIT，长连接复用仅 6 个，差近 340 倍&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;附录实验代码和原始数据&#34;&gt;&lt;a href=&#34;#%e9%99%84%e5%bd%95%e5%ae%9e%e9%aa%8c%e4%bb%a3%e7%a0%81%e5%92%8c%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;附录：实验代码和原始数据&#xA;&lt;/h2&gt;&lt;p&gt;本文 E1–E4 四组实验的代码和 netstat 原始输出已开源：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub：&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/tcp-handshake-timewait&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/tcp-handshake-timewait&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;每个子目录都有独立 README，说明如何复现。二进制编译产物不入库，跑实验前自己 &lt;code&gt;python3&lt;/code&gt; 启动对应脚本。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/tcp-handshake-timewait&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
