这篇文章,我们将通过几个简单脚本,亲手模拟一场典型的“句柄泄漏事故”,让你直观理解其发生机制与复现方法。
在上一篇文章中,我们分析了一个常见的线上故障:
Too many open files → 服务拒绝连接
很多开发者会疑惑:这个问题究竟是如何触发的?能否在本地环境进行复现?
本文我们将直接使用几个简易脚本,亲手复现一场“句柄泄漏事故”。
什么是文件描述符(FD)

在 Linux 系统中,一切资源皆被视为文件:
- 普通文件
- 网络 Socket
- Pipe(管道)
- 设备节点
这些资源均通过文件描述符(FD)进行统一管理。
每个进程可使用的 FD 数量都有上限:
ulimit -n
常见配置值:
- 1024(系统默认)
- 65535(经过优化后的典型值)
一旦 FD 被耗尽,便会触发:
Too many open files
实验一:最简单的 FD 泄漏场景
演示脚本(未主动关闭文件):
# fd_leak_file.py
import time
fds = []
while True:
f = open("/tmp/test_fd.txt", "w")
fds.append(f) # ❗没有关闭
print("opened:", len(fds))
time.sleep(0.01)
运行实验:
ulimit -n 10000
python3 fd_leak_file.py
opened: 9993
opened: 9994
opened: 9995
opened: 9996
opened: 9997
Traceback (most recent call last):
File "file_os.py", line 7, in
OSError: [Errno 24] Too many open files: '/tmp/test_fd.txt'
# 检查文件描述符
[root@localhost ~]# ll /proc/4013078/fd -ltr
...
l-wx------ 1 root root 64 3月 24 22:27 9993 -> /tmp/test_fd.txt
l-wx------ 1 root root 64 3月 24 22:27 9994 -> /tmp/test_fd.txt
l-wx------ 1 root root 64 3月 24 22:27 9995 -> /tmp/test_fd.txt
l-wx------ 1 root root 64 3月 24 22:27 9996 -> /tmp/test_fd.txt
监控截图:

由此可见,只要持续打开文件而不关闭,FD 必然被耗尽。
实验二:模拟 Socket 连接泄漏
该场景更贴近实际服务:
# fd_leak_socket.py
import socket
import time
sockets = []
while True:
s = socket.socket()
s.connect(("127.0.0.1", 80))
sockets.append(s) # ❗不关闭
print("socket:", len(sockets))
time.sleep(0.01)
运行现象:
#脚本执行情况
socket: 9997
Traceback (most recent call last):
File "fd_leak_socket.py", line 8, in
File "/usr/lib64/python3.7/socket.py", line 151, in __init__
OSError: [Errno 24] Too many open files
#检查句柄情况
[root@localhost ~]# ls -ltr /proc/1831260/fd
...
lrwx------ 1 root root 64 3月 25 16:29 104 -> 'socket:[1039854483]'
lrwx------ 1 root root 64 3月 25 16:29 103 -> 'socket:[1039854482]'
lrwx------ 1 root root 64 3月 25 16:29 102 -> 'socket:[1039854481]'
lrwx------ 1 root root 64 3月 25 16:29 101 -> 'socket:[1039854480]'
lrwx------ 1 root root 64 3月 25 16:29 100 -> 'socket:[1039854479]'
lrwx------ 1 root root 64 3月 25 16:29 10 -> 'socket:[1039845307]'
lrwx------ 1 root root 64 3月 25 16:29 1 -> /dev/pts/0
lrwx------ 1 root root 64 3月 25 16:29 0 -> /dev/pts/0
lrwx------ 1 root root 64 3月 25 16:30 9999 -> 'socket:[1040097407]'
lFD 数量持续增长,最终程序报错:

实验三(重点):触发 Pipe 泄漏
该场景正是许多线上故障的根源:
# fd_leak_pipe.py
import os
import time
pipes = []
while True:
r, w = os.pipe()
pipes.append((r, w)) # ❗不关闭
print("pipe:", len(pipes))
time.sleep(0.01)
验证步骤:
首先找到进程 PID
ps -ef | grep fd_leak_pipe
查看 FD 数量
[root@localhost ~]# ls -ltr /proc/1923060/fd
...
l-wx------ 1 root root 64 3月 25 16:36 9722 -> 'pipe:[1040687058]'
lr-x------ 1 root root 64 3月 25 16:36 9721 -> 'pipe:[1040687058]'
l-wx------ 1 root root 64 3月 25 16:36 9720 -> 'pipe:[1040687057]'
lr-x------ 1 root root 64 3月 25 16:36 9719 -> 'pipe:[1040687057]'
ls /proc//fd | wc -l
你会发现:
- 已打开的文件描述符数量持续攀升
- 短时间内即逼近系统上限
查看 FD 类型:
ls -l /proc//fd | grep pipe
pipe:[xxxx]
pipe:[xxxx]
pipe:[xxxx]
该现象与众多线上生产故障完全吻合。
实验四:模拟真实服务场景(子进程泄漏)
此场景最贴近实际生产环境:
# fd_leak_subprocess.py
import subprocess
import time
procs = []
while True:
p = subprocess.Popen(
["echo", "hello"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
procs.append(p) # ❗不 wait、不关闭
print("proc:", len(procs))
time.sleep(0.01)
该实验会导致:
- Pipe 数量急剧增加
- 子进程不断堆积
- 文件描述符迅速膨胀引发爆炸
与真实服务(如文档转换、后台任务执行)的故障模式高度一致。
为什么会发生泄漏?
根本原因只有一个:资源被申请后,始终未进行释放。
文件描述符(FD)泄漏可归纳为四大类:
- 文件打开后未关闭
- Socket 连接未释放
- Pipe 管道未关闭
- 子进程未等待(导致管道残留)
正确写法(避免泄漏):
import os
for i in range(10000):
r, w = os.pipe()
os.close(r)
os.close(w)
或对子进程做:
p = subprocess.Popen(...)
p.wait()
如何快速判断是否存在泄漏?
方法1:观察 FD 数量是否持续增长
watch -n 1 "ls /proc//fd | wc -l"
方法2:按类型统计 FD 占比
ls -l /proc//fd | awk '{print $NF}' | cut -d: -f1 | sort | uniq -c
方法3:查看 Socket 状态
ss -antp
实验总结
上述几个实验印证了一个道理:文件描述符泄漏并非复杂难题,本质上是“忘记关闭资源”导致的。
但在生产环境中,由于:
- 并发量高
- 调用链复杂
- 业务链路漫长
极易演变成灾难性后果:
FD耗尽 → 无法 accept → 服务雪崩
如果你在线上遇到:
- 服务突然拒绝连接
- 日志频繁出现Too many open files
第一件该做的事:
ls /proc//fd | wc -l
亲手动手模拟一遍,远比阅读十篇理论文章更有效。
