TCP 的 11 个状态:TIME_WAIT 是好的,CLOSE_WAIT 才是 bug

难度 ★★★★★ · 前置:三次握手:双方内核各改了哪些字段 | 一句话结论:TIME_WAIT 是正常关闭流程的收尾,不占文件描述符,只占一个四元组;CLOSE_WAIT 是程序收到对方 FIN 之后没调 close(),内核参数一个都救不了。

去年双十一前压测,监控在群里贴了两台机器。第一台三万个 TIME_WAIT,运维当场慌了,扭头去搜如何清理 TIME_WAIT,最后往 /etc/sysctl.conf 里加了两行。第二台五千个 CLOSE_WAIT,因为数字小一个量级,群里的结论是先观察。两天后挂的是第二台。这事我印象很深,因为加 sysctl 的那个人做事很认真,认真地去修了一个根本不存在的问题,而真正漏水的地方数字更小,看着不像问题,就放过去了。后来我发现这不是个例。搜 TIME_WAIT 过多怎么办,能出来一整页优化方案,搜 CLOSE_WAIT 过多怎么办,结果少一半,而且很多是抄的,原因很朴素,前者看起来吓人,后者不像。而这两件事的严重程度,恰好是反的。

三万个 TIME_WAIT,其实一切正常

第一台是反向代理,它替后面的服务接客,每个请求走完是它先调 close(),所以每个连接都以它作为主动关闭方收尾。主动关闭方会最后留在 TIME_WAIT 六十秒,三万个除以六十,反推出每秒关掉五百个连接,压测前这条链路本来就是几千 QPS,五百这个数字对得上。所以当时那条告警什么也没测出来,它测到的是反代在正常工作。第二台不一样。那五千个 CLOSE_WAIT 的意思是,对端已经挂断了,内核替我们回了 ACK,现在等我们的代码调一次 close(),而代码没调,这些连接就一直躺在那儿,一直躺到进程退出。内核把事情做完了,剩下的是应用层欠的债,内核没法替你还。一句话区分这两个状态,TIME_WAIT 是内核替你做完了事、正在数最后几十秒,CLOSE_WAIT 是该你做的事、你忘了做。

之所以要等,是因为最后那个 ACK

先把 11 个状态摆出来,名字是 RFC 793 定的,Linux 用的完全一样。

状态 谁在等 什么条件下进入
CLOSED 什么都没有 初始 / 彻底结束
LISTEN 服务端等 SYN listen() 之后
SYN_SENT 客户端等 SYN+ACK connect() 之后
SYN_RCVD 服务端等第三次 ACK 收到 SYN、回了 SYN+ACK
ESTABLISHED 双方能收发数据 握手完成
FIN_WAIT_1 主动关闭方等 ACK 自己发了 FIN
FIN_WAIT_2 主动关闭方等对方的 FIN 收到了 FIN 的 ACK
CLOSE_WAIT 被动关闭方等应用 close() 收到对方的 FIN
CLOSING 双方同时发 FIN 发了 FIN 又收到 FIN
LAST_ACK 被动关闭方等最后的 ACK 应用 close()、发了 FIN
TIME_WAIT 主动关闭方等 2MSL 关闭流程走完

这张图里只有一条边的触发者是用户代码,就是 CLOSE_WAIT 到 LAST_ACK,标注写着应用 close。其余的边全是内核在收发包时自动走的,你不动手,图就会停在 CLOSE_WAIT 那一格,一直停着。关闭要四次挥手,是因为 TCP 全双工,两个方向各关各的。

中间那段空白值得盯一会儿,FIN_WAIT_2 和 CLOSE_WAIT 隔着的这段时间,长度完全由应用的代码决定,跟网络无关,跟内核无关。顺带说一句,服务端在 CLOSE_WAIT 里还能发数据,半关闭(half-close)是个正经特性,不是 bug,HTTP 里客户端发完请求 shutdown(SHUT_WR)、服务端继续回响应,用的就是它。问题从来不在半关闭,在于半关闭之后永远不关。

那 60 秒到底在等什么

答案是一个更根本的问题,既然最后那个 ACK 已经发出去了,为什么还要待着不走。因为发出去不等于对方收到,而对方收到也没法再回你一个 ACK,那就成了 Ack 套 Ack,跟两将军问题一个道理,无穷递归下去没有终点,所以协议只能选一个经验值,等足够久,久到我有把握。等这 2 倍 MSL 是为了两件事。一是给最后那个 ACK 留重传的机会,如果它丢了,被动方还停在 LAST_ACK 会重发 FIN,这时候主动方如果已经走了,内核只能回一个 RST,被动方会认为自己是被异常中断的,而不是正常关闭。(这个细节我是看了很久的抓包才转过弯来的,之前一直以为 TIME_WAIT 纯粹是个历史包袱)二是让这条连接路上的旧数据包彻底死掉,MSL 是一个包在网络里能活的最大时间,等两倍它,可以保证这条四元组上的残留包已经全部消失。不等会怎样,想象旧连接有个 seq=100 的包被网络延迟了,新连接正好也用这个四元组,seq 也爬到了 100 附近,新连接会认领这个包,数据流当场被污染,RFC 1337 专门讨论过这件事,标题用的是 TIME-WAIT Assassination,暗杀。这就是 TIME_WAIT 必须绑在四元组上而不是绑在进程上的原因,它保护的是这条地址加端口的路,不被前一个住户的遗物影响。

60 秒这个数字,调不动

网上流传最广的一条是调小 tcp_fin_timeout 就能减少 TIME_WAIT。这条是错的,我早期也信过。tcp_fin_timeout 管的是 FIN_WAIT_2。

1
sysctl net.ipv4.tcp_fin_timeout
1
net.ipv4.tcp_fin_timeout = 60

它跟 TIME_WAIT 一点关系都没有,TIME_WAIT 的时长在 Linux 里是个编译进内核的常量,include/net/tcp.h 里的 TCP_TIMEWAIT_LEN,值是 60 * HZ,改它得重新编译内核,没有 sysctl 入口。网上那些调优方案里还有一条是开 tcp_tw_recycle,这个参数在 4.12 内核里已经被删掉了,删掉的原因是它在 NAT 环境下会随机丢连接,它按源 IP 缓存时间戳,而 NAT 后面一群客户端共用一个 IP,时间戳看起来倒退的包就被直接扔掉,这类故障极其难查,因为丢包是随机的,看不出来规律。后来还有人问我为什么某个云厂商的机器上这台连不上那台连不上,最后查到是他们的自定义镜像里留了这个参数,如果你在 4.12 以上的内核上还看得到它,那大概是有人手动打的补丁,别用。

真正的成本是端口

TIME_WAIT 复用的资源,网上说法乱七八糟,实测一遍比较省事。

项目 是否占用
文件描述符 不占。进程 close() 之后 fd 已经还了
进程 / 线程 不占
内存 占一个 inet_timewait_sock(slab 里的 tw_sock_TCP,objsize 200 多字节,含 slab 管理开销实际每个 300 字节上下)
四元组 占。这是唯一的成本

三万个 TIME_WAIT 吃不到 10MB 内存,可以忽略,具体数用 grep tw_sock /proc/slabinfo 或者 slabtop -o | grep tw_sock 自己看。真正会卡住你的是端口,一个进程作为客户端去连别人的时候,源端口从下面这个范围里挑。

1
sysctl net.ipv4.ip_local_port_range
1
net.ipv4.ip_local_port_range = 32768\t60999

两万八千个可用端口,如果每个关闭后的连接都要占满六十秒,那么对着同一个目的 IP:端口,新建连接的上限是每秒四百六十六次。

1
28000 / 60 ≈ 466 次/秒

超了这个数就是一行报错。

1
connect: Cannot assign requested address

注意这个极限是按每个目的四元组算的,连十个不同的后端上限就变成四千六,反过来如果你的流量全打在一个网关上,那对不起了,四千六也没有。这也是为什么网关一个节点上堆几万个 TIME_WAIT 很常见,它就是那个所有流量都打在一个目的地址的角色。

tcp_tw_reuse 治的是端口,不是数量

1
sysctl net.ipv4.tcp_tw_reuse

默认值是 2,意思是只对 loopback 生效,常见的优化是设成 1。它允许内核在创建出向连接时,复用还在 TIME_WAIT 状态的四元组,前提是开了时间戳(tcp_timestamps=1,默认就开)并且新连接的时间戳严格大于旧的,有这三个限制。

  1. 只对出向连接生效,也就是你自己 connect() 出去的。入向连接完全不管。
  2. 必须开 tcp_timestamps,复用安全性的依据就是它。
  3. 不会减少 ss 里看到的 TIME_WAIT 数量。记录还在,只是能被复用。

我刚接手这套服务那会儿设完这个参数,敲 ss 一看数字没变,还以为没生效,又去查了半天日志。(那一个小时基本白花,查到最后发现自己从一开始就把这个参数当成了清理开关)它治的是端口耗尽,不是状态数量,如果你看到三万个 TIME_WAIT 就去设它,大概率是白设,除非这台机器同时还有 Cannot assign requested address。

还有一条路:直接发 RST

前面说的都是怎么和 TIME_WAIT 共处,真不想让它出现,只有一个办法,不让连接走正常关闭。SO_LINGER 设成 {1, 0} 然后 close(),内核会直接发 RST 把连接掐掉,跳过 FIN/ACK 四次挥手,也就没有 TIME_WAIT。

1
2
3
struct linger lg = { .l_onoff = 1, .l_linger = 0 };
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof lg);
close(fd); // 发 RST,不是 FIN

代价有三条,都得认。

  • 对端收到 ECONNRESET,它缓冲区里还没被读走的数据全部丢掉。这就是「RST 会吃掉数据」。
  • 丢掉了 TIME_WAIT 那两分钟的保护,链路上残留的旧包可能串到下一个同四元组的连接里。
  • 中间设备(NAT、负载均衡)看到 RST 会立刻释放会话表项,看起来是好事,但也意味着没机会做优雅收尾。

所以服务端永远不要这么干,真要用的场景是客户端,连接已经没意义了(对端挂了、超时了),继续走正常关闭纯属浪费一个端口两分钟,很多 HTTP 客户端在超时后就是这么清理的。顺带说一句 tcp_max_tw_buckets,默认值是 NR_FILE * 2,TIME_WAIT 超过这个数之后内核会直接掐掉新的,并在 dmesg 里打一行 TCP: time wait bucket table overflow。看到这行说明你的端口已经在耗尽的边缘,tcp_max_tw_buckets_ub 或者调大它只是把问题往后推。

另一边:CLOSE_WAIT 是代码里的漏

写到这里回到第二台机器。CLOSE_WAIT 的含义精确到有点残忍,对方关掉了它的写方向,我回了 ACK,现在等我的应用调 close()。没有任何内核参数能让它消失,能做的只有三件事,改代码、加连接池上限、加监控。关于它占不占 fd,我见过有人争论,占的,CLOSE_WAIT 的 socket 还挂在进程的 fd 表里,因为 close() 还没调,所以这类泄漏的第一后果往往不是连接数告警,是 ulimit -n 先被打满,然后 accept 报下面这个。

1
accept: EMFILE: Too many open files

到这一步,服务已经在持续拒绝新连接了,而监控看到的 CLOSE_WAIT 数字可能才几百。

三种漏法

第一种,最常见的,忘了 close。

1
2
3
4
5
6
7
while True:
conn, addr = sock.accept()
data = conn.recv(1024)
if not data:
continue # 这里漏了
handle(conn)
conn.close()

客户端正常关闭时 recv 返回空字节,代码 continue 往下走,conn 从来没有被 close(),Python 有引用计数,这个对象可能在下一轮被回收,那是运气,Go 没有 GC 帮你关文件,Java 的 finalize 贡献了无数个 CLOSE_WAIT。正确写法是让上下文管理器兜底。

1
2
3
4
5
with conn:                 # socket 支持上下文管理器,异常也会关
data = conn.recv(1024)
if not data:
return
handle(conn)

第二种,异常路径没走 finally。

1
2
3
4
5
conn, _ := ln.Accept()
if err := handle(conn); err != nil {
return // 忘了 conn.Close(),panic 也一样
}
conn.Close()

Go 里 defer conn.Close() 要紧跟 Accept() 写,不要等到用完之后再补,这一点我是在一次线上泄漏里才真正记住的,之前总觉得写哪儿都一样。第三种,连接池没有淘汰,HTTP 客户端池里服务端先关了连接,客户端这边就是 CLOSE_WAIT,健康的长连接池会定期清理,不健康的会一直攒到打满。

全机的分布,一行命令数出来

统计这件事,一行命令就能看全机分布。

1
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
1
2
3
142 ESTAB
38 TIME-WAIT
4 LISTEN

ss -tan 里的状态名是缩写的,而且带连字符(TIME-WAIT 不是 TIME_WAIT),写过滤条件的时候要注意这一点。

1
2
ss -tan state time-wait | wc -l
ss -tan state close-wait | wc -l

找到 CLOSE_WAIT 属于哪个进程。

1
ss -tanp state close-wait
1
2
State      Recv-Q  Send-Q  Local Address:Port      Peer Address:Port  Process
CLOSE-WAIT 0 0 10.0.3.14:8080 10.0.5.9:51234 users:(("java",pid=2210,fd=218))

fd=218 就是那个没被关掉的描述符,拿着 PID 去 jstack 或者 pstack,能看到线程卡在哪儿。想自己造一个出来看,写个收到 FIN 但不 close 的服务。

1
2
3
4
5
6
7
8
9
# server_cw.py
import socket, time
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 9200))
s.listen(5)
conn, _ = s.accept() # 只接一个,然后就不管了
print('accepted, not closing; sleeping')
time.sleep(120)

客户端连上就主动关。

1
2
3
4
5
# client_cw.py
import socket, time
c = socket.create_connection(('127.0.0.1', 9200))
c.close() # 主动关闭 → 服务端进 CLOSE_WAIT
time.sleep(0.5)
1
ss -tanp 'sport = :9200'
1
2
State      Recv-Q  Send-Q  Local:Port        Peer:Port
CLOSE-WAIT 0 0 127.0.0.1:9200 127.0.0.1:51234

它就这么挂着,一直挂到进程退出,顺手验证它确实占着 fd。

1
ls -l /proc/$(pgrep -f server_cw)/fd | grep socket
1
2
lrwx------ 1 user user 64 ... 3 -> socket:[24510]
lrwx------ 1 user user 64 ... 4 -> socket:[24511]

两个 socket,一个是监听的那个,一个就是卡在 CLOSE_WAIT 的那个。TIME_WAIT 的六十秒也能直接看到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python3 -c "
import socket
s = socket.socket()
s.bind(('127.0.0.1', 9300))
s.listen(1)
c = socket.create_connection(('127.0.0.1', 9300))
conn, _ = s.accept()
c.close(); conn.close()
"

for i in 1 2 3; do
ss -tan state time-wait 'sport = :9300 or dport = :9300' | tail -n +2
sleep 20
done

第一次看到一条 TIME-WAIT,二十秒后还在,四十秒后还在,六十秒后没了,这里先 close() 的是客户端,所以只有它是主动关闭的一方,走完 FIN_WAIT_1、FIN_WAIT_2,最后停在 TIME_WAIT 等满六十秒。服务端是被动关闭,收到 FIN 先进 CLOSE_WAIT,它那个 close() 只是补一个 FIN,走 LAST_ACK 直接到 CLOSED,不经过 TIME_WAIT。想看得更精确,ss -tin 会把它剩多少秒都打出来。

1
ss -tin 'sport = :9300'
1
\t timer:(timewait,23sec,0) ...

全机的总量在 /proc/net/sockstat 里。

1
cat /proc/net/sockstat
1
2
sockets: used 187
TCP: inuse 42 orphan 0 tw 128 alloc 189 mem 12

那个 tw 128 就是。上限由 tcp_max_tw_buckets 控制,超了内核会直接销毁记录并打一行日志。

1
sysctl net.ipv4.tcp_max_tw_buckets
1
kernel: TCP: time wait bucket table overflow

看到这行说明关闭速率远超预期,要么真出问题了,要么有人在压测。最后一条我建议做成巡检项。

1
ss -tanp | awk -v p="$(pgrep -o nginx)" '$0 ~ "pid="p","'

CLOSE_WAIT 超过 100 就 P1,TIME_WAIT 不用告警,除非同时出现 Cannot assign requested address。(这条规则我在两个团队里都推过,第二个团队至今还在用,第一个团队据说把它扩成了整站规范)

收一下

  • 11 个状态里只有两个值得看,而且方向是反的。TIME_WAIT 是正常收尾,CLOSE_WAIT 是代码漏了 close()。
  • TIME_WAIT 等 2MSL 有两个硬理由:让最后的 ACK 能重传,让旧包死透。RFC 1337 讲的是不等会怎样。
  • 六十秒写死在 TCP_TIMEWAIT_LEN,tcp_fin_timeout 管的是 FIN_WAIT_2,别弄混。
  • 它不占 fd,每个占几百字节内存(grep tw_sock /proc/slabinfo 可查),真正的成本是四元组。两万八千端口除以六十,对同一个目的地址每秒 466 次。
  • tcp_tw_reuse=1 只对出向连接生效、要开时间戳、不减少数量。
  • tcp_tw_recycle 在 4.12 已移除,在 NAT 下会随机丢连接。
  • CLOSE_WAIT 只有一个原因,应用没 close()。它会吃掉 fd,先打满 ulimit -n 再让 accept 报 EMFILE。

我在这件事上前后搞反过两次,一次是给别人加 sysctl,一次是自己漏了 defer,修 TIME_WAIT 是替内核操心,修 CLOSE_WAIT 才是替自己操心。接下来该聊服务器怎么扛住更多连接了,fork、线程池、select、epoll 四条路,各自在什么规模上开始撑不住。