Update 计算机网络 - 传输层.md

补充 CLOSE_WAIT 堆积的成因与排查,以及第三次握手的 ACK 全部丢失时服务器的处理。
This commit is contained in:
CyC2018
2026-11-02 01:31:14 +08:00
parent c7e61e37c7
commit 33c096ea0d
+13 -7
View File
@@ -114,9 +114,9 @@ B 照常回复 SYN+ACK。A 并没有在等这个连接,回复 RST(复位报
每个握手报文都可能丢失,丢失后靠超时重传恢复(Linux 6.10 实测,见[附录 A2](#a2-握手报文丢失)):
- 第一次握手的 SYN 丢失:客户端收不到 SYN+ACK,超时后重传 SYN。Linux 6.5 之前按 tcp_syn_retries(默认 6)重传,间隔从 1 秒起每次翻倍,约 127 秒后放弃;6.5 起前几次重传改为固定间隔 1 秒(tcp_syn_linear_timeouts,默认 4),实测共重传 10 次,134 秒后 connect() 返回超时错误;
- 第一次握手的 SYN 丢失:客户端收不到 SYN+ACK,超时后重传 SYN。Linux 6.5 之前按 tcp_syn_retries(默认 6)重传,间隔从 1 秒起每次翻倍,约 127 秒后放弃;6.5 起增加了 tcp_syn_linear_timeouts(默认 4)次按固定间隔的重传,前 5 次重传的间隔都是 1 秒,之后再翻倍,共重传 10 次,内核文档给出约 131 秒后放弃,实测 134 秒后 connect() 返回超时错误;
- 第二次握手的 SYN+ACK 丢失:客户端以为 SYN 丢了,重传 SYN;服务器收不到第三次握手,也重传 SYN+ACK(tcp_synack_retries 默认 5 次,间隔 1、2、4、8、16 秒),任何一方的重传到达,连接都能建立;
- 第三次握手的 ACK 丢失:客户端发出 ACK 后已经进入 ESTABLISHED,服务器仍处于 SYN_RCVD,继续重传 SYN+ACK,客户端每收到一次就再回复一次 ACK。客户端发送的数据报文段也带有 ACK,服务器收到后同样会建立连接;如果这些 ACK 都丢失、客户端也一直不发数据,服务器重传 5 次后放弃,之后客户端再发数据会收到 RST。
- 第三次握手的 ACK 丢失:客户端发出 ACK 后已经进入 ESTABLISHED,服务器仍处于 SYN_RCVD,继续重传 SYN+ACK,客户端每收到一次就再回复一次 ACK。客户端发送的数据报文段也带有 ACK,服务器收到后同样会建立连接;如果这些 ACK 都丢失、客户端也一直不发数据,服务器重传 5 次后放弃,删除这个半连接,并不通知客户端,之后客户端再发数据会收到 RST。
<div align="center"> <img src="pics/tcp-third-ack-loss.png" width="660px"/> </div><br>
@@ -140,13 +140,15 @@ B 照常回复 SYN+ACK。A 并没有在等这个连接,回复 RST(复位报
- A 收到后发出确认,进入 TIME_WAIT 状态,等待 2 MSL(Maximum Segment Lifetime,最大报文段寿命)后释放连接;
- B 收到 A 的确认后释放连接,进入 CLOSED 状态。
B 在半关闭状态下发来的数据,A 能否收到取决于 A 怎样关闭:A 调用 shutdown(SHUT_WR) 只关闭发送方向时,仍然可以读;A 调用的是 close() 时,应用已经不能再读,Linux 收到 B 的新数据后回复 RST,RFC 9293 也建议这样做,以表明数据丢失了。
FIN 和数据一样,丢失后由发送方超时重传。A 在 FIN_WAIT_2 中等待 B 的 FIN,如果 A 的应用已经调用了 close(),而 B 迟迟不发 FIN,Linux 会在 tcp_fin_timeout(默认 60 秒)后直接释放这个连接。
### 四次挥手的原因
TCP 是全双工的,两个方向要分别关闭。A 的 FIN 只表示 A 不再发送数据,B 可能还有数据没发完,所以 B 先回复确认,进入 CLOSE_WAIT 继续发送,等应用发完并调用 close() 后再发出自己的 FIN。握手时 B 的 SYN 和确认可以一起发出,挥手时 B 的确认和 FIN 通常隔着一段时间,只能分开发送,所以多了一次。
如果 B 收到 FIN 时已经没有数据要发送,应用立即调用 close(),B 的确认和 FIN 就可以合并在一个报文段中,挥手只有三次。Linux 收到 FIN 后,确认会延迟几十毫秒再发送,应用在这期间调用 close() 就会合并(Linux 6.10 实测,见[附录 A3](#a3-三次挥手)):
如果 B 收到 FIN 时已经没有数据要发送,应用立即调用 close(),B 的确认和 FIN 就可以合并在一个报文段中,挥手只有三次,A 在 FIN_WAIT_1 中收到这个报文段后直接进入 TIME_WAIT。Linux 收到 FIN 后,确认会延迟几十毫秒再发送,应用在这期间调用 close() 就会合并(Linux 6.10 实测,见[附录 A3](#a3-三次挥手)):
```text
B 读到 EOF 后立即 close() B 读到 EOF 后 1 秒再 close()
@@ -185,9 +187,11 @@ ss -tan | awk 'NR > 1 {print $1}' | sort | uniq -c
- 在发起连接的一方开启 net.ipv4.tcp_tw_reuse,允许新连接复用处于 TIME_WAIT 的端口,它依靠 TCP 时间戳选项识别旧连接的报文(Linux 默认值为 2,只对回环地址生效,设为 1 才对所有出站连接生效);
- 扩大本地端口范围 net.ipv4.ip_local_port_range。
Linux 没有提供修改 TIME_WAIT 时长的参数,缩短它也会让旧连接的报文段更容易混进新连接。另一个参数 tcp_tw_recycle 在 NAT 环境下会丢弃正常的连接,Linux 4.12 已将其移除。
Linux 没有提供修改 TIME_WAIT 时长的参数,缩短它也会让旧连接的报文段更容易混进新连接。另一个参数 tcp_tw_recycle 在 NAT 环境下会丢弃正常的连接:开启后内核按对端 IP 缓存最近收到的时间戳,缓存未过期时,时间戳回退超过容忍范围的 SYN 会被丢弃,而 NAT 后面多台主机的时间戳互不相关。Linux 4.10 起每个连接的时间戳还加了随机偏移,同一台主机的连接也可能被误丢,4.12 将这个参数移除。
CLOSE_WAIT 出现在被动关闭的一方,表示对方已经关闭,本方的应用还没有调用 close()。连接大量停在 CLOSE_WAIT 通常是程序的问题,例如异常分支没有关闭连接、从连接池借出的连接没有归还,这些连接会一直占用文件描述符,积累到上限后出现 Too many open files。排查时用 ss -tanp state close-wait 找到所属进程,再检查代码。
CLOSE_WAIT 出现在被动关闭的一方,表示对方已经关闭,本方的应用还没有调用 close()。连接大量停在 CLOSE_WAIT 通常是程序的问题,例如异常分支没有关闭连接、从连接池借出的连接没有归还,或者处理线程被没有超时的慢调用占住,排队的连接一直等不到处理。这些连接会一直占用文件描述符,积累到上限后出现 Too many open files。
Linux 没有专门限制 CLOSE_WAIT 停留时长的超时,tcp_fin_timeout 只作用于已经没有进程引用的 FIN_WAIT_2 连接,调整它不能解决 CLOSE_WAIT 堆积。排查时用 ss -tanp state close-wait 找到所属进程,再检查代码;Java 进程可以用 jstack 查看处理线程阻塞在哪里。
## TCP 可靠传输 🔥
@@ -335,9 +339,11 @@ TCP 面向字节流,只保证字节按顺序到达,不保留应用每次写
产生的原因主要有以下几种:
- 数据先进入接收缓冲区,应用读取时缓冲区里有多少就读出多少,最多读满自己的缓冲区;
- 发送方的 Nagle 算法会把多次写入的小数据合并成一个报文段发送;
- 发送方的 Nagle 算法在还有未确认的数据时,把之后多次写入的小数据攒在一起,等到已发出的数据全部得到确认,或者攒够一个完整的报文段再发送;
- 超过最大报文段长度(Maximum Segment Size, MSS)的消息会被分成多个报文段,可能分多次读到。
关闭 Nagle 算法(TCP_NODELAY)只改变发送的时机,接收方读得慢时,缓冲区中的多条消息仍会被一次读走,所以不能代替应用层的分帧。
UDP 面向报文,一次读取得到的就是对方一次发送的一个数据报,不存在粘包。需要注意的是,接收缓冲区比数据报小时,多出的部分会被丢弃,不会留到下一次读取。
解决方法是在应用层约定消息的边界,常用以下三种:
@@ -386,7 +392,7 @@ static String receive(DataInputStream in) throws IOException {
HTTP/3 使用的 QUIC 就是在 UDP 上实现的可靠传输,并改进了 TCP 的几处设计:
- 包号只增不减,丢失的数据放进一个新编号的包中重传。收到确认时能分清确认的是哪一次发送,RTT 测得更准;TCP 重传时沿用原来的序号,无法区分;
- 数据按流(Stream)组织,每个流按帧中的偏移量单独排序,一个流丢包不影响其它流;
- 数据按流(Stream)组织,每个流按帧中的偏移量单独排序,一个包丢失时,只有包中带有数据的流会因缺数据而暂停交付;
- 一个确认帧可以携带很多个已收到的区间,TCP 的选择确认(SACK)受首部长度限制,通常最多携带 3 个区间。
QUIC 选择在 UDP 上重新实现,而不是改进 TCP,是因为 TCP 实现在操作系统内核中,改动要等操作系统升级才能普及;防火墙、NAT 等中间设备还会检查甚至改写 TCP 首部,不认识的新选项可能被丢弃。QUIC 运行在用户态,随浏览器和 App 一起更新,并且加密了大部分首部,中间设备无法干预。QUIC 的握手和连接迁移见 [HTTP/3](HTTP.md#http3)。