第 16 章 TCP 连接管理
连接是 TCP 造出来的抽象:握手造它,挥手拆它,而线上绝大多数"连接建立失败/端口耗尽"故障,都落在这两个过程的状态与队列里。
学习目标
读完本章,你应当能够:
- 逐字段说明 TCP(Transmission Control Protocol,传输控制协议)报文头每个字段被哪个机制使用;
- 从零推导"为什么三次握手不能压缩成两次",说清历史 SYN 造成"幽灵连接"的完整场景;
- 区分 SYN 队列(半连接队列)与 accept 队列(全连接队列),用
ss -lnt定位连接建立失败发生在哪一步; - 说明
tcp_syncookies的原理与代价;给出TIME_WAIT必须存在且必须为2MSL的两条理由,并评估各种"减少 TIME_WAIT"手段的边界; - 画出 TCP 11 状态机全图,填出"某状态收到某标志位"的反应表。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
| MTU/MSS 的层次归属与分片 | 见第 15 章对应小节 |
| 端口与四元组、NAT(Network Address Translation)对四元组的影响 | 见第 15 章对应小节 |
fd → 打开文件表 → inode 三层结构、dup 的共享语义 | 见第 7 章对应小节 |
| RTO(Retransmission Timeout)与重传 | 见第 17 章对应小节 |
16.1 TCP 报文头全字段解读
教材讲过报头"有什么字段",面试真正问的是每个字段在哪个机制里被消费。
| 字段 | 宽度 | 被哪个机制使用 | 不设/设错的后果 |
|---|---|---|---|
| 源端口 / 目的端口 | 各 16 bit | 四元组定位与多路分解(demultiplexing) | 无法区分同主机并发连接;端口耗尽故障的直接来源 |
| 序号(Sequence Number) | 32 bit | 字节流编号、确认、重排、去重、SACK(Selective ACK) | 无法检测乱序与重复;重传判断不出对端收到多少 |
| 确认号(Acknowledgment Number) | 32 bit | 累积确认;仅 ACK 置位时有效 | 发送方只能靠超时判丢包,吞吐塌陷 |
| 数据偏移(Data Offset) | 4 bit | 报文头长度,单位 4 字节,最小 5(20 字节) | 无法定位选项区与载荷边界,选项机制整体失效 |
| 保留位 + 标志位 | 3 + 9 bit | URG/ACK/PSH/RST/SYN/FIN 六大标志 + CWR/ECE/NS | 建连、拆除、复位全靠它们;ECE/CWR 是 ECN(Explicit Congestion Notification)的信道 |
| 窗口(Window) | 16 bit | 接收窗口 rwnd,流量控制;配合窗口扩大因子可左移 | 只能表达 65535 字节窗口,长肥管道跑不满 |
| 校验和(Checksum) | 16 bit | 覆盖伪首部 + TCP 头 + 载荷 | 无法发现链路层未检出的比特错误 |
| 紧急指针(Urgent Pointer) | 16 bit | 仅 URG=1 时指向紧急数据末尾 | URG 在实践中几乎无人实现,select 异常集合已废弃 |
| 选项(Options) | 变长,补齐到 4 字节 | MSS、窗口扩大(Window Scale)、SACK Permitted、时间戳 | 只在握手期协商,协商失败即退化为默认行为 |
| |
窗口字段的语义要记牢:它通告的是本端还能接收多少字节(接收缓冲区空位),不是"我打算发多少"。应用读走多少,窗口就回涨多少;回涨通知靠不带数据的纯 ACK——这就是延迟确认与零窗口探测的舞台(见第 17 章)。
注意
SYN 与 FIN 各占 1 个序号。由此推出两个高频细节:第三次握手的 ACK 号是"服务端 ISN + 1";挥手时 FIN 的确认号也要 +1。报头固定 20 字节 + 选项最多 40 字节,正是"为什么不能把时间戳、SACK、窗口扩大全部塞进去"的根因,也是 MSS = MTU − 20 − 20 = 1460(以太网)的来源。
16.2 三次握手:每一步究竟在做什么
“SYN、SYN+ACK、ACK"是结论,面试要的是每步交换的信息与它解决的问题。
| |
第一步(SYN, seq = x):客户端宣告自己的初始序号(Initial Sequence Number, ISN),请求序号同步。它不确认任何东西——服务端还没开口。
第二步(SYN+ACK, seq = y, ack = x + 1):两件事压进一个报文——宣告服务端 ISN y,并确认客户端 ISN x。此刻服务端已单方面认为连接建立,进入 SYN_RCVD 并分配接收缓冲区等资源。
第三步(ACK, seq = x + 1, ack = y + 1):客户端确认服务端 ISN,把连接推到 ESTABLISHED,并让服务端知道客户端接受了 y。
为什么双方各自独立编号、必须交换 ISN。 全双工连接有两条独立的字节流,各自需要起点,才能计算确认号、判断乱序与重排。
为什么 ISN 必须随机化、不能从 0 开始。 两条独立理由,面试要都答:
- 防止旧连接的报文被新连接误收。 四元组会被复用(客户端端口释放后再用、NAT 复用映射、服务端重建同名连接)。若 ISN 从 0 每轮递增,上一轮某个迟到报文的序号恰好落进新连接的接收窗口,就会被当作合法数据交付应用层。随机化把这一概率压到约
1/2³²。 - 防止序列号猜测攻击。 ISN 若可预测,攻击者无需看到握手报文即可盲猜有效序号,从而伪造数据注入或伪造
RST打断连接——在没有认证机制的 TCP 上这是致命的。
第三次握手能否携带数据? 协议允许,服务端也会正常缓存。但默认不发:服务端 cwnd 初值只有 1 个 MSS 量级(见第 17 章),第二次报文一旦因 SYN 队列溢出被丢,客户端的数据会随重传的 ACK 一起重发,白费带宽;更根本的是客户端此刻还不确定服务端是否接受连接。
为什么不是四次。 把第二步拆成"单独回 ACK"+“单独发 SYN“语义完全等价,但白白多一个 RTT。第二步能合并的依据:SYN 与 ACK 是同一报头里的两个独立标志位,同时置位不违反任何约束,接收方也无歧义(ack 号永远指向上次收到的对端序号,seq 号是本端新序列的起点)。凡两件事能无损放进一个报文,协议设计都会合并——这是读 TCP 的通用规律。挥手侧同理会出现"少于四次"的情况,见 16.6.1。
重要
正确表述:“两次握手让服务端单方面完成状态迁移,而连接是双方的共同状态;第三次握手是客户端把自己的迁移结果通报给服务端,使双方对’连接已存在’达成一致。"——这个句式可以直接迁移到挥手题上。
16.3 为什么不能是两次握手
题目假设:客户端发 SYN,服务端回 SYN+ACK 后即进入 ESTABLISHED,不等第三次确认。
| |
关键点:两次握手模型下,服务端无法区分"新连接请求"与"早已作废的历史连接请求”——它对两者做出同样的承诺、分配同样的资源。而客户端本地没有任何相关状态,会忽略或 RST 掉那个 SYN+ACK,结果是服务端持有一批客户端根本不承认的连接。第三次 ACK 才是客户端对"我确实想建这条连接"的承诺:历史 SYN 送到时,客户端内核因找不到对应 socket 而回 RST,服务端收到即清理。只有"客户端确认了服务端 ISN"这一事实能筛掉幽灵连接。
小心
两个会立刻被追问的错答:说成"两次握手不能保证可靠传输”——可靠性归第 17 章的重传机制,与轮次无关,会被反问"那三次握手就不丢包了吗”;说成"为了防 SYN Flood”——因果反了,防历史连接误建是设计动机,SYN Flood 是后来针对该设计的攻击。
16.4 SYN 队列与 accept 队列
内核为 listen socket 维护两个独立队列,这是排查"连接建立失败"的核心。
| |
| 项目 | SYN 队列(半连接) | accept 队列(全连接) |
|---|---|---|
| 存放对象 | 收到 SYN、未完成第三次握手的连接 | 握手已完成、等应用 accept() 的连接 |
| 长度上限 | min(tcp_max_syn_backlog, somaxconn, backlog) | min(backlog, net.core.somaxconn) |
| 溢出行为 | 默认丢弃新 SYN,不回 RST,客户端自行重传 | 看 net.ipv4.tcp_abort_on_overflow:0 = 忽略第三次 ACK(服务端悄悄丢弃);1 = 回 RST 让客户端立刻失败重试 |
| 观测量 | netstat -s 的 SYNs to LISTEN sockets dropped | netstat -s 的 listen overflows |
ss -lnt 列 | 不直接显示 | Recv-Q = 当前队列长度,Send-Q = 上限 |
ss -lnt 在 LISTEN 下的 Recv-Q/Send-Q 语义与其他状态完全不同,是最容易读错的地方:
| |
警告
tcp_abort_on_overflow = 0(默认)的表现极具迷惑性:客户端 connect() 成功返回,ss 里也能看到 ESTABLISHED,但服务端内核在 accept 队列满时把这个已握完手的连接悄悄丢掉,随后它因服务端不认账而被 RST。现象是"偶发连接被重置,重试即成功"。排查第一步是 netstat -s | grep -i listen 加 ss -lnt 的 Recv-Q。
16.5 SYN Flood 与防御
攻击机理直接利用 16.4 的队列:用伪造源 IP 的海量 SYN 填满 SYN 队列,服务端为每个表项分配资源并按 tcp_synack_retries 重传 SYN+ACK,而真实客户端的 SYN 因队列满被丢弃。tcp_syncookies 的原理是:不分配状态,把状态编码进 ISN。
| |
- 无状态是它的全部价值:SYN 队列被"绕过"而非"扩容",因此能抗住任意大的洪流。
- 代价一:选项协商退化。 服务端 ISN 被占用来编码 cookie,MSS、窗口扩大因子、SACK、时间戳都无法随 cookie 编码,只能放弃。这就是"syncookies 不能与某些 TCP 选项共存"的确切含义——它用长肥管道与 SACK 的性能换抗洪能力。
- 代价二:cookie 编码空间有限,时间戳计数器粒度决定了 cookie 有效期(约 64 秒量级),超时的第三次
ACK会被拒。
| 参数 | 作用 | 取值建议与理由 |
|---|---|---|
tcp_syncookies | 队列溢出时改用无状态 cookie 应答 | 1(按需启用),避免常态下损失选项协商;2 为无条件启用 |
tcp_syn_retries | 客户端 SYN 重传次数,决定 connect() 多久失败 | 默认 6,总退避约 127 秒,业务上通常过久,需配合应用层连接超时 |
tcp_synack_retries | 服务端 SYN+ACK 重传次数 | 默认 5,退避约 1 + 2 + 4 + 8 + 16 + 32 秒,决定半连接表项存活时长,调小可加快回收 |
tcp_max_syn_backlog | SYN 队列上限 | 默认 256~1024,高并发服务端须上调,否则先于 accept 队列溢出 |
net.core.somaxconn / tcp_abort_on_overflow | accept 队列的全局上限 / 队列满时是否回 RST | somaxconn 5.4+ 默认 4096;tcp_abort_on_overflow=0 会造成"客户端成功、服务端丢弃"的幽灵连接 |
注意
更彻底的方向是把握手负担外移:负载均衡设备做 SYN 代理(客户端先与 LB 握手,LB 再与后端建连),或改用 QUIC(见第 18 章)——QUIC 把传输握手与 TLS 握手合并,并用地址验证令牌/无状态重试包达到类似 syncookies 的效果,且不必牺牲选项协商。
16.6 四次挥手与 TIME_WAIT
16.6.1 每一步的语义与状态迁移
| |
FIN 的语义仅是"我没有数据要发了",它占一个序号,但不关闭接收方向。B 进 CLOSE_WAIT 后仍可继续发数据,A 必须继续 ACK——这就是半关闭(half-close),也是 CLOSE_WAIT 堆积的物理基础:B 的应用不调 close(),该状态就永远停留。
为什么不一定四次。 ① ACK 与 FIN 若同时就绪,B 会合并成 FIN+ACK,四次退化为三次(延迟确认加剧这种合并);② 同时关闭时双方都发 FIN,各自经 CLOSING 进 TIME_WAIT;③ 一方设了 SO_LINGER(见 16.7)或进程崩溃时直接回 RST,四次挥手被完全跳过——这是抓包里"突然 RST“的常见来源。
16.6.2 四个"堆积状态"的辨析与排查
| 状态 | 出现在哪一侧 | 含义 | 排查方法 |
|---|---|---|---|
CLOSE_WAIT | 被动关闭方(已收 FIN 并回 ACK,等本端应用 close()) | 应用层没关闭 fd | ss -tan state close-wait 找 fd → lsof -p <pid> 定位 → 查连接池 release 是否被 finally/try-with-resources 覆盖。必定是代码 bug,改内核参数无效 |
LAST_ACK | 被动关闭方(已发 FIN,等对端最后 ACK) | 等对端确认本端 FIN | 通常瞬时。大量停留说明对端不回 ACK(对端崩溃或被防火墙丢弃),属网络/对端问题 |
TIME_WAIT | 主动关闭方 | 正常关闭,等 2MSL 后释放 | 堆积说明本端是主动关闭方,见 16.6.3、16.6.4 |
FIN_WAIT_2 | 主动关闭方(已收 ACK,等对端 FIN) | 对端迟迟不发 FIN | 对端 CLOSE_WAIT 侧应用没关连接;FIN_WAIT_2 有超时,由 tcp_fin_timeout(默认 60 s)控制 |
一句话记住分辨线:CLOSE_WAIT 是应用没关(bug),TIME_WAIT 是内核在等(正常或端口压力)。
16.6.3 TIME_WAIT 为什么存在,且必须是 2MSL
先澄清 MSL(Maximum Segment Lifetime):报文能在网络中存活的最长时间。RFC 793 建议 2 分钟;Linux 实现中 TCP_TIMEWAIT_LEN 固定为 60 秒,即 2MSL = 60 s,对应实现假设的 MSL = 30 s。tcp_fin_timeout 不控制 TIME_WAIT 时长(它控制 FIN_WAIT_2),这是最常见的读错参数。TIME_WAIT 有两条互相独立的理由:
理由一:保证最后一个 ACK 能到达对端。
| |
若 A 发完最后一个 ACK 就 CLOSED 并释放四元组,B 重传的 FIN 到达时 A 只能回 RST,B 会把已成功传输的数据判为异常终止;TIME_WAIT 让 A 保留状态以重发 ACK,并在收到重复 FIN 时重置计时器。
理由二:让本次连接的旧报文在网络中全部消失。 考虑四元组复用:关闭后立刻用同样的 (源IP, 源端口, 目的IP, 目的端口) 建连,若上一轮某个被延迟的报文仍在网络中,其序号落进新连接的接收窗口,就会被当作新数据交付应用层。2MSL 的物理含义正是:一个报文从发出到被网络丢弃最多花 MSL,对端对它做出反应(如重传)再花 MSL,故 2MSL 后本轮连接的所有报文及其所有可能的响应都必然消失。
重要
记忆锚:理由一防"对端收不到 ACK 而误判失败”,理由二防"旧报文污染同四元组的新连接"。前者关于本次连接能否优雅结束,后者关于下一次连接能否正确开始。只答一条必被追问"还有吗"。
16.6.4 TIME_WAIT 过多的解法与代价
先判断量级是否真异常。本地端口由 net.ipv4.ip_local_port_range 决定(默认约 32768~60999,约 2.8 万个)。若服务是客户端角色(调外部接口、做代理转发),TIME_WAIT 消耗本地端口,到几万量级即报 Cannot assign requested address;若服务是服务端角色(固定监听端口),TIME_WAIT 不消耗端口,只占内存表项。
| 手段 | 真实作用 | 代价与边界 |
|---|---|---|
SO_REUSEADDR | 对 TIME_WAIT 数量毫无影响。只做两件事:允许 bind 处于 TIME_WAIT 的地址(重启不必等 2MSL),允许同一端口绑定不同本地 IP | 它不让 TIME_WAIT 变少,只让"绑定"不被拒。当成减少 TIME_WAIT 的手段是根因性误解 |
tcp_tw_reuse | 允许主动发起连接的一方,在时间戳(tcp_timestamps=1)开启时复用 TIME_WAIT 中的四元组建新连接 | 只对主动发起方有效;必须开时间戳(复用后旧报文的序号保护由 PAWS 承担);不改架构,只做缓解 |
缩短 tcp_fin_timeout | 只缩短 FIN_WAIT_2 的等待上限 | 完全不影响 TIME_WAIT 时长(Linux 由内核常量 TCP_TIMEWAIT_LEN 固定)。当解法属彻底误读 |
| 连接池化 / 长连接 | 从根上减少连接创建次数,因而减少 TIME_WAIT 总量 | 需处理空闲超时、对端半关闭、失效连接重建(见第 18 章)。唯一从源头解决的方向 |
| 让对端主动关闭 | 把 TIME_WAIT 推给对端 | 需协议层协商,且对端运维未必接受 |
tcp_tw_recycle | 已废弃并移除 | 见下方警示 |
小心
net.ipv4.tcp_tw_recycle 在 4.12 及之后的内核中已被彻底移除,在旧内核上也绝不应启用。它依赖"同一源 IP 的时间戳必须单调递增",而 NAT 设备后的多个客户端共用一个源 IP、各自时间戳并不同步,服务端会把时间戳更小的正常 SYN 当作过期报文直接丢弃——NAT 后的客户端出现随机、大面积连不上。
16.7 半关闭、SO_LINGER 与端口复用选项
| 操作 | 作用 | 能否继续读 | 对连接的影响 |
|---|---|---|---|
shutdown(fd, SHUT_WR) | 只发 FIN,关闭发送方向 | 能,接收方向仍可读 | 连接进入半关闭(FIN_WAIT_1/FIN_WAIT_2),fd 仍被占用 |
shutdown(fd, SHUT_RD) | 关闭接收方向,对端再发数据则回 RST | 不能 | fd 仍被占用 |
shutdown(fd, SHUT_RDWR) | 双向关闭并发 FIN | 不能 | 触发完整挥手 |
close(fd) | fd 引用计数减 1;减到 0 才发 FIN | 取决于是否还有其他副本 | 释放 fd;引用计数未归零则连接不会关闭 |
最后一行是高频陷阱:fork 或 dup 出来的 fd 共享同一打开文件表项,一方 close 只减引用计数,连接依旧活着(见第 7 章三层结构)。SO_LINGER 四种组合——定义 struct linger { int l_onoff; int l_linger; }:
l_onoff | l_linger | close() 行为 | 用途与风险 |
|---|---|---|---|
| 0 | 忽略 | 默认:后台异步发完剩余数据并正常四次挥手 | 绝大多数场景 |
| 1 | 0 | 丢弃发送缓冲区未发数据,直接发 RST | 强制跳过四次挥手、不产生 TIME_WAIT;代价是对端可能丢数据并被报"连接被重置" |
| 1 | >0 | close() 阻塞至多 l_linger 秒,等数据发完并收到对端 ACK;超时退化为发 RST | 适合关闭前必须确保送达的场景;风险是阻塞调用线程,在事件循环里会拖垮整个 loop |
| 1 | 负值 | 未定义/视实现而定,不应使用 | — |
警告
“把 SO_LINGER 的 l_linger 设 0 来消灭 TIME_WAIT"是流传最广的偏方之一。它确实让主动关闭方发 RST 而不进 TIME_WAIT,但代价是:① 发送缓冲区里未确认的数据被静默丢弃;② 对端按异常终止处理,可能触发重试或告警;③ 绕过了理由二,旧报文污染新连接的风险重新出现。
SO_REUSEADDR 与 SO_REUSEPORT 的差异:前者管能不能 bind(允许绑定 TIME_WAIT 地址、允许同一端口绑定不同本地 IP);后者管谁来 accept——允许多个 socket(甚至不同进程)绑定完全相同的地址与端口,内核按四元组哈希在它们之间做负载均衡,每个进程有独立队列,从而消除 accept 惊群(thundering herd)与锁竞争。注意 SO_REUSEPORT 要求同一连接始终分到同一 socket,故哈希必须稳定;它只解决分发,不解决"谁来关闭连接”。
16.8 TCP 状态机全图与状态×标志位反应表
| |
11 种状态是 CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、CLOSING、LAST_ACK、TIME_WAIT。TCP 是事件驱动的有限状态机:事件 = 收到的标志位组合 + 本端应用调用(open/send/close)+ 超时。
| 当前状态 | 收到 ACK | 收到 FIN | 收到 RST |
|---|---|---|---|
CLOSED | 回 RST | 回 RST | 忽略 |
LISTEN | 忽略 | 忽略 | 忽略 |
SYN_SENT | 忽略(未置 SYN 的 ACK 无效) | 建连后再处理 | connect() 失败,→ CLOSED |
SYN_RCVD | 移入 accept 队列,→ ESTABLISHED | 先建连再走 CLOSE_WAIT | → CLOSED |
ESTABLISHED | 更新发送窗口 | 回 ACK,→ CLOSE_WAIT | 异常终止,→ CLOSED |
FIN_WAIT_1 | 确认我方 FIN:已收对端 FIN 则 → TIME_WAIT,否则 → FIN_WAIT_2 | 回 ACK,→ CLOSING 或 TIME_WAIT | → CLOSED |
FIN_WAIT_2 | 忽略 | 回 ACK,→ TIME_WAIT | → CLOSED |
CLOSE_WAIT | 忽略 | 忽略(已在关闭中) | → CLOSED |
CLOSING | 确认我方 FIN,→ TIME_WAIT | 重发 ACK | → CLOSED |
LAST_ACK | → CLOSED | 重发 ACK | → CLOSED |
TIME_WAIT | 忽略 | 重发 ACK 并重置 2MSL 计时器 | 提前 → CLOSED |
所有非 LISTEN、非 SYN_SENT 的非 CLOSED 状态收到裸 SYN 都属异常,统一回 RST;LISTEN 收到 SYN 回 SYN+ACK 并进入 SYN_RCVD,SYN_SENT 收到对端 SYN 即"同时打开",回 SYN+ACK 后进入 SYN_RCVD。
TIME_WAIT 行加粗项正是理由一在状态机层面的落地:收到对端重传的 FIN 时不回 RST,而是重发 ACK。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 不能两次 | 两次握手只让服务端单方面迁移状态,历史 SYN 会造成客户端不认的幽灵连接 | 只背"防止旧连接请求",说不出旧 SYN 如何产生 |
| 不能四次 | 服务端 SYN 与对客户端 SYN 的 ACK 可放进同一报文,拆开无收益 | 说"四次也行,只是三次更快",漏掉"不违反任何约束" |
| 两个队列 | SYN 队列溢出丢 SYN;accept 队列溢出看 tcp_abort_on_overflow,默认 0 时连接"成功后消失" | 把两者混成一个 backlog,忽略 somaxconn 取小值 |
CLOSE_WAIT / TIME_WAIT | 前者是被动关闭方应用没调 close()(代码 bug);后者是主动关闭方的正常等待 | 把两者一起归为内核参数问题 |
TIME_WAIT 的 2MSL | ① 能重发最后一个 ACK;② 让旧报文消失,避免污染新四元组 | 只说一条;把 tcp_fin_timeout 当成控制 TIME_WAIT 的参数 |
| 端口复用选项 | SO_REUSEADDR 只管"能否 bind",不减 TIME_WAIT;SO_REUSEPORT 管内核级分发 | 把 SO_REUSEADDR 当成 TIME_WAIT 优化手段 |
tcp_tw_recycle | 因 NAT 时间戳假设错误已被移除 | 按旧博客去开启 |
自测题
Q1(推导) 请在不使用"三次握手"这个词的前提下,解释为什么服务端在第二个报文之后不能认为连接已建立,并给出一个能让错误模型暴露的资源耗尽场景。
答案与解析
核心论点是连接是双方的共同状态,不是单方的承诺。第二个报文只完成了"服务端告知自己的 ISN 并确认了客户端 ISN",服务端据此进入 ESTABLISHED;但客户端尚未确认服务端的 ISN,也无法分辨该报文是回应本次请求还是上一轮的迟到回应。若模型在此结束,服务端为每条收到的 SYN 都分配了接收缓冲区与表项,而客户端可能根本没有对应状态。
资源耗尽场景:伪造源 IP 发送大量 SYN。两次模型下服务端对每个 SYN 立即完成建连并分配资源,无法用"客户端是否回应第三次确认"筛除伪造请求——因为根本没有第三次。常见错误:答"两次不可靠,数据可能丢失",可靠性归第 17 章重传机制,与轮次无关。追问:① “客户端收到 SYN+ACK 后崩了,三次握手不也一样消耗服务端资源吗”→ 用 SYN_RCVD 超时回收与 syncookies 回答;② “RST 在这里起什么作用”→ 客户端无对应 socket 时内核回 RST,服务端收到即清理。
Q2(判断) 某 Java 服务出现大量 CLOSE_WAIT,运维建议调大 tcp_max_syn_backlog 与 somaxconn。判断该建议是否有效,并说明你会怎么定位。
答案与解析
无效,方向性错误。 CLOSE_WAIT 表示"本端已收到对端 FIN 并回了 ACK,正等本端应用调用 close()",其唯一出口是应用层 close();两个队列参数只影响连接建立阶段的表项长度。
定位路径:ss -tan state close-wait 找连接与本地端口 → ss -tanp 或 lsof -p <pid> 映射到进程与 fd → 审查连接获取/释放代码。典型根因有三:① 连接池 close/release 不在 finally 或 try-with-resources 中,异常路径漏释放;② 连接池 maxIdle/maxLifetime 与对端空闲超时不匹配,对端先关、本端不感知;③ finally 里只提交事务而没归还连接到池。常见错误:把它与 TIME_WAIT 一起归为内核参数问题。追问:① “CLOSE_WAIT 占用 fd 吗”→ 占用,这正是它最终引发 Too many open files 的原因;② “调 ulimit -n 能否缓解”→ 只是把爆炸时间推后,属掩盖问题。
Q3(计算) 某服务端 tcp_timestamps=0,ip_local_port_range 为默认 32768~60999。它作为主动发起方每秒向同一后端的同一端口建立 5000 条短连接,且由本端主动关闭。判断是否会出现本地端口耗尽,并说明 tcp_tw_reuse 能否救场。
答案与解析
可用本地端口约 60999 − 32768 + 1 ≈ 28232 个。本端主动关闭 → 每条连接进 TIME_WAIT 并占住其四元组 60 秒(TCP_TIMEWAIT_LEN = 60 s)。稳态下处于 TIME_WAIT 的四元组数约 5000 × 60 = 300000,远超 28232,约 5.6 秒就会耗尽端口空间。要注意四元组是 (源IP, 源端口, 目的IP, 目的端口),若后端有多个地址/端口,占用会按目的端组合分摊,瓶颈变成 可用源端口数 × 目的端组合数;本题限定同一后端同一端口,故必然耗尽。
tcp_tw_reuse 不能救场:题目已设 tcp_timestamps=0,而它要求时间戳开启(依赖 PAWS 保证复用后旧报文不被误收);即便开启时间戳,它也只在发起新连接时允许复用,属缓解而非根治。真正的解法是连接池/长连接(把 5000 次/秒的建连降到接近零),辅以扩大 ip_local_port_range、增加后端地址多样性。常见错误:答"把 tcp_fin_timeout 调小",它控制 FIN_WAIT_2,不影响 TIME_WAIT 的 60 秒。追问:① “为什么 tcp_tw_reuse 需要时间戳”→ 复用同一四元组后,旧连接的迟到报文可能落入新连接窗口,PAWS 用时间戳单调性拒收;② “SO_REUSEADDR 有帮助吗”→ 只对"服务端重启时 bind 失败"有帮助。
Q4(设计) 要上线一个"每台机器每秒对下游发起上万次 HTTP 短连接"的网关。请给出减少连接管理开销的方案,按收益排序并说明每个方案的 TCP 层依据。
答案与解析
按收益排序:① 连接池 + 长连接(keep-alive),把建连与拆连的次数从每秒上万降到接近零,直接消除 TIME_WAIT 总量与握手 RTT(依据 16.6.4);② 降低主动关闭方压力,与下游协商空闲超时,连接池 maxLifetime 必须小于对端空闲超时,避免使用被对端静默关闭的连接(依据 16.6.2);③ 扩大可用四元组空间,调整 ip_local_port_range 并在下游增加 (IP, 端口) 组合(依据 Q3 计算);④ SO_REUSEPORT 做接入侧多进程负载均衡,若同时对外提供入口,多进程各自 listen 同端口、由内核按四元组哈希分发,可消除惊群(16.7);⑤ 谨慎使用 tcp_tw_reuse,仅"本端主动发起 + 时间戳开启"有效且依赖 PAWS;⑥ 明确不要用 tcp_tw_recycle(已移除,NAT 场景必炸)与 SO_LINGER 置 0(丢数据且破坏 TIME_WAIT 的保护语义)。
常见错误:把 SO_REUSEADDR 排在前面——它只解决"重启时 bind 失败"与"多 IP 同端口"。追问:① “长连接会不会有时间戳回绕风险”→ 超过 24.8 天的连接需靠时间戳/序号回绕保护,故应设 maxLifetime 定期重建;② “改 HTTP/2 多路复用有没有帮助”→ 有,但那是第 18 章主题。
Q5(辨析) 面试官问:"TIME_WAIT 是为了让最后一个 ACK 能重发,那我调小 tcp_fin_timeout 不就缩短了吗?“请指出其中的两处错误。
答案与解析
错误一:tcp_fin_timeout 不控制 TIME_WAIT。 它控制 FIN_WAIT_2 的存活上限(对端迟迟不发 FIN 时多久放弃),默认 60 秒;TIME_WAIT 时长由内核常量 TCP_TIMEWAIT_LEN(60 秒,即 2MSL,对应实现假设的 MSL = 30 s)决定,用户态无法通过 sysctl 修改。
错误二:把 TIME_WAIT 的理由只归结为"重发 ACK"。 还有第二条独立理由:让本轮连接的所有报文在网络中消失,避免污染使用相同四元组的新连接;即使能保证最后一个 ACK 一定送达,第二条理由依然要求等待约 2MSL。加分回答:可补"把主动关闭方角色转移到可控一侧是常见工程做法;彻底消除 TIME_WAIT 的方向是减少建连次数(连接池)或换用多路复用方案(QUIC,见第 18 章)"。常见错误:答"调小 tcp_fin_timeout 会让 TIME_WAIT 更快释放,但有风险”——把参数名与风险绑定,恰好暴露没读 man 7 tcp。追问:① “MSL 与 2MSL 是什么关系”→ MSL 是单程报文最大存活时间,2MSL 覆盖一去一回;② “为什么 Linux 用 60 秒”→ 内核把 TCP_TIMEWAIT_LEN 定为固定值,是网络半径与端口压力之间的折中。
面试高频问题与陷阱
| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应本章小节 |
|---|---|---|---|
| 为什么 TCP 三次握手不能改成两次? | 是否理解"连接是双方共同状态" | 错误回答:只答"防止旧连接请求"就停住,说不出旧 SYN 怎么产生、为什么客户端会回 RST。加分:完整给出"延迟 SYN + 连接已关闭 + 客户端无状态"场景链 | 16.3 |
| 为什么三次握手不是四次? | 是否理解标志位可合并 | 错误回答:“四次也行,只是三次更快”——漏了"不违反任何约束"这一层,也没提同时打开与延迟确认捎带 | 16.2、16.6.1 |
| TCP 的初始序号为什么要随机化,不能从 0 开始? | 序号空间与安全性 | 错误回答:只说"为了安全"。加分:两条理由分开讲(旧报文误收 + 序列号猜测攻击),并给出概率量级约 1/2³² | 16.2 |
| 第三次握手可以携带数据吗? | 协议细节与工程取舍 | 错误回答:“不可以,SYN 不能带数据”——混淆了第一次与第三次。加分:协议允许但默认不发,因服务端 cwnd 初值小、丢包会连带重传数据 | 16.2 |
ss -lnt 里 LISTEN 状态的 Send-Q 和 Recv-Q 是什么意思? | 两个队列与观测 | 错误回答:“收发缓冲区大小”——那是 ESTABLISHED 下的含义。加分:Recv-Q 是 accept 队列当前长度、Send-Q 是上限,贴上限即溢出 | 16.4 |
CLOSE_WAIT 很多是什么原因,怎么处理? | 状态语义与排查 | 错误回答:“调内核参数"或"提高 ulimit"。加分:断言这是应用没调 close() 的代码 bug,并给出连接池 finally/try-with-resources 的定位路径 | 16.6.2 |
TIME_WAIT 为什么必须有,为什么是 2MSL 而不是 1MSL? | 本章第一号难点 | 错误回答:只说"重发 ACK”。加分:第二条理由(旧报文消失,防污染同四元组新连接)+ 说明 MSL 是单程、2MSL 覆盖一去一回 | 16.6.3 |
SO_REUSEADDR 能不能减少 TIME_WAIT? | 参数真实语义 | 错误回答:“能,开启后 TIME_WAIT 就少了”。正确:它只解决 bind 被 TIME_WAIT 拒绝的问题,数量不变 | 16.6.4、16.7 |
tcp_tw_recycle 为什么被移除? | 是否读过真实内核变更 | 错误回答:“因为它不安全"这类空话。加分:NAT 后多客户端共源 IP、时间戳不同步,服务端按时间戳丢弃正常 SYN 导致随机连不上,4.12 起已移除 | 16.6.4 |
syncookies 是怎么防 SYN Flood 的,有什么代价? | 无状态化思想 | 错误回答:“它扩大了半连接队列”。加分:状态编码进 ISN 的 MAC;代价是选项协商退化、cookie 有效期有限 | 16.5 |
shutdown(SHUT_WR) 和 close 有什么区别? | 半关闭与 fd 引用计数 | 错误回答:“没区别”。加分:shutdown 只关发送方向、接收仍可读;close 是减引用计数,dup/fork 共享时可能根本不发 FIN | 16.7 |
SO_REUSEADDR 和 SO_REUSEPORT 的区别是什么? | 是否分清两个易混选项 | 错误回答:“都是为了端口复用,差不多”。加分:前者管能否 bind,后者是内核按四元组哈希做负载均衡、消除惊群 | 16.7 |
服务端大量连接处于 SYN_RCVD 说明什么? | 半连接状态与攻击识别 | 错误回答:“正常现象,不用管”。加分:区分三种成因——客户端不回第三次 ACK(可能有防火墙丢包)、真实 SYN Flood、tcp_max_syn_backlog 偏小,并给出 netstat -s 对应计数器 | 16.4、16.5 |
| 同时关闭(simultaneous close)时状态怎么走? | 状态机是否真的画过 | 错误回答:“不可能同时关闭”。正确路径:FIN_WAIT_1 → CLOSING → TIME_WAIT,前提是双方都成为主动关闭方 | 16.6.1、16.8 |
延伸阅读
- RFC 793《Transmission Control Protocol》——状态机定义(含状态转移图)、ISN 选择、
TIME_WAIT与MSL的原始表述。 - RFC 1122《Requirements for Internet Hosts — Communication Layers》4.2.2 节——TCP 实现的最低要求,含半关闭与
TIME_WAIT的强制项。 - RFC 6298《Computing TCP’s Retransmission Timer》——握手期
SYN重传与 RTO 初值(1 s)的规范来源。 - RFC 7323《TCP Extensions for High Performance》——窗口扩大因子、时间戳与 PAWS、SACK 选项,是理解"syncookies 牺牲了什么"的必备背景。
- Linux 手册页
man 7 tcp、man 7 socket——tcp_tw_reuse/tcp_fin_timeout/tcp_syncookies/SO_LINGER/SO_REUSEPORT的权威语义,注意区分官方文档与网上偏方。 - Linux 内核文档
Documentation/networking/ip-sysctl.rst——tcp_max_syn_backlog、tcp_abort_on_overflow与 accept 队列溢出的精确行为。 - 《TCP/IP 详解 卷 1:协议》第 17~18 章——连接建立与终止的报文级时序,可作抓包对照(第 19 章讲怎么读这些报文)。