2023 年,我维护的一个后台服务,出了一个很有"节奏感"的故障。这个服务平时稳得很,可每隔几天,它就会突然集体发病:新的请求连不进来,日志也写不动了,满屏都是同一句报错——Too many open files。我第一次见,照着字面意思理解:"打开的文件太多了"。我心想,这服务也没读写多少文件啊,哪来的"太多"?我重启了一下,世界瞬间清净——服务恢复如初,稳稳当当。可过了三四天,同样的故障,又一次原样上演。我又重启,又恢复,又过几天,又崩。它就像上了发条一样,精确地、周期性地复发。我第一反应是"限制太小了",于是我把那个能开文件的上限调大了一倍。然而调大之后,故障没有消失——它只是把复发的周期,从三四天,拖长到了六七天。这个现象让我背后一凉:如果只是"限制小",调大了就该彻底好了;可它只是"晚一点"再犯——这说明有个东西在【持续不断地增长】,我调大上限,只不过是给了它更长的时间去涨到新的上限而已。我这才意识到,问题根本不是"文件太多",而是有什么东西,我打开了,却【从来没有关上】,它在一天天地堆积。这件事逼着我把 Linux 的文件描述符、ulimit、/proc/PID/fd、fd 泄漏这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个长期运行的后台 Java 服务
事故现象:
- 服务平时正常,每隔三四天集体发病
- ★ 新请求连不进来,日志写不动,报 Too many open files
- 重启立刻恢复,过几天又原样复发
- ★ 把 ulimit 调大一倍后,复发周期从 3 天变成 6 天
现场排查:
# 1. ★ 抓一条完整报错
$ tail -f app.log
java.net.SocketException: Too many open files
at java.net.ServerSocket.accept(...)
# ^^^^^^^^^^^^^^^^^^^^ ★ 注意:报错出在 accept
# —— 是"接受一个网络连接"时,开不出新的了
# 2. ★ 看这个进程当前开了多少个文件描述符(fd)
$ ls /proc/8866/fd | wc -l
65530 # ★ 直逼上限
# 3. 看这个进程的 fd 上限是多少
$ cat /proc/8866/limits | grep "open files"
Max open files 65536 65536 # ★ 上限就是 65536,已快满
# 4. ★ 关键一步:fd 都开着些什么
$ ls -l /proc/8866/fd | awk '{print $NF}' | \
sed 's/[0-9]*$//' | sort | uniq -c | sort -rn
64900 socket:[ # ★ 6 万多个 socket!
30 /www/app/logs/app.log
8 anon_inode:[eventpoll]
3 /dev/null
# ★ 真相浮现:6 万多个 fd,几乎全是 socket(网络连接)
# 5. ★ 这些 socket 是什么状态
$ ss -tanp | grep 8866 | awk '{print $1}' | sort | uniq -c
64880 CLOSE-WAIT # ★ 全是 CLOSE-WAIT!
22 ESTAB
# ★ CLOSE-WAIT:对端已经关了连接,我方却迟迟没 close
根因(后来想清楚的):
1. ★ 在 Linux 里,"打开一个文件"会得到一个【文件
描述符(fd)】—— 一个小整数,代表这个打开的资源。
而 socket(网络连接)、管道,在 Linux 里【也算
文件】,也占用 fd。
2. ★ 每个进程能同时持有的 fd 数量,有【上限】
(ulimit -n)。开一个占一个,这是有限资源。
3. 我的服务,每处理完一个连接,因为代码里的一个
分支漏掉了 close —— socket 用完【没有关闭】。
fd 就这样一个一个【只借不还】,持续泄漏。
4. ★ 泄漏的 socket 停在 CLOSE-WAIT 状态:对端关了,
等我方 close,可我方永远不 close —— 它就一直
占着一个 fd,不生不灭。
5. ★ 调大 ulimit 为什么没用:泄漏没停,只是上限
更高了,涨到新上限需要更久 —— 治标不治本。
开的不是太多,是开了不还;根子是代码漏了 close。
修复 1:Too many open files——先认清"文件"和"fd"
# === ★ 先纠正一个根本误解:这里的"文件"是什么 ===
# === ★ Linux 的核心哲学:一切皆文件 ===
# "Too many open files" 里的"文件",不只是磁盘上的
# .txt、.log。在 Linux 的世界观里,下面这些【全都
# 算"文件"】,打开它们都要占一个 fd:
# - 普通磁盘文件
# - ★ socket(网络连接)—— 本文的主角
# - 管道 pipe
# - 设备(/dev/null 等)
# - epoll / eventfd 等内核对象
# ★ 所以"打开的文件太多",真实含义往往是
# "打开的【网络连接】太多" —— 和读写磁盘没关系。
# === ★ 什么是文件描述符(fd)===
# 进程每打开一个上述"文件",内核就发给它一个
# 【文件描述符】—— 一个小整数(0、1、2、3...)。
# 进程之后读写这个资源,都靠这个数字来指代。
# - 0 = 标准输入 1 = 标准输出 2 = 标准错误
# - 3 往后,是程序自己打开的文件、连接...
# ★ fd 是【有限的资源】:每个进程能同时持有多少个,
# 有一个明确的上限。开一个,占一个名额。
# === ★ 直接看一个进程持有的所有 fd ===
# /proc//fd/ 这个目录,列出该进程当前持有的
# 每一个 fd,每个都是一个软链,指向它代表的东西:
$ ls -l /proc/8866/fd | head
lrwx------ 0 -> /dev/pts/0
lrwx------ 1 -> /dev/null
lrwx------ 3 -> socket:[284013] # ★ 一个网络连接
lrwx------ 4 -> /www/app/logs/app.log # ★ 一个磁盘文件
lrwx------ 5 -> socket:[284099]
# ★ 数一数有多少个:
$ ls /proc/8866/fd | wc -l
# === ★ 报错 accept 给的提示 ===
# 我这次报错出在 ServerSocket.accept() —— "接受新连接"。
# accept 一个新连接,要为这个连接【分配一个新 fd】。
# fd 名额满了,accept 就失败 —— 这就是为什么
# "新请求连不进来"。
# === 认知 ===
# ★ Too many open files = 这个进程持有的 fd 到顶了。
# 而 fd 的大头,常常是 socket,不是磁盘文件。
# 排查第一步:ls /proc/PID/fd,看 fd 到底是些什么。
修复 2:一个进程开了多少 fd、都开了些什么
# === ★ 把"这个进程的 fd 都用在哪了"查清楚 ===
# === 方法 1:ls /proc/PID/fd —— 最直接 ===
$ ls /proc/8866/fd | wc -l # 当前持有多少个 fd
65530
# === ★ 方法 2:按"类型"归类,看 fd 花在哪 ===
# 这一步最关键 —— 不是看"多少",是看"多在哪一类":
$ ls -l /proc/8866/fd | awk '{print $NF}' | \
sed 's/\[[0-9]*\]//' | sort | uniq -c | sort -rn
64900 socket: # ★ 绝大多数是 socket
30 /www/app/logs/app.log
8 anon_inode:[eventpoll]
3 pipe:
# ★ 一眼看出:fd 全堆在 socket 上 -> 是连接没关,
# 不是文件没关。方向立刻就清楚了。
# === ★ 方法 3:lsof —— 更详细的列表 ===
$ lsof -p 8866 | head
COMMAND PID USER FD TYPE ... NAME
java 8866 app 3u IPv4 ... TCP host:8080->1.2.3.4:51000 (CLOSE_WAIT)
java 8866 app 4u IPv4 ... TCP host:8080->1.2.3.4:51001 (CLOSE_WAIT)
# ★ lsof 能直接看到每个 socket 的对端地址和状态。
$ lsof -p 8866 | wc -l # 统计总数
# 按状态统计 socket:
$ lsof -p 8866 | grep -o '(.*)' | sort | uniq -c | sort -rn
64880 (CLOSE_WAIT) # ★ 6 万多个卡在 CLOSE_WAIT
# === ★ 方法 4:ss —— 专看网络连接,比 lsof 快 ===
$ ss -tanp | grep pid=8866 | awk '{print $1}' | sort | uniq -c
64880 CLOSE-WAIT
22 ESTAB
# ★ ss 在连接数巨大时,比 lsof 快得多。
# === ★ CLOSE-WAIT 是什么,为什么它是泄漏的铁证 ===
# TCP 连接关闭是【四次挥手】。当对端先关闭:
# - 我方内核收到对端的 FIN,把连接置为 CLOSE-WAIT;
# - 然后【等我方的程序调用 close()】,才继续挥手。
# ★ CLOSE-WAIT 的意思就是:"对端已经走了,就等你
# 这边的代码 close 了"。
# ★ 如果大量连接长期卡在 CLOSE-WAIT —— 几乎可以
# 断定:你的代码漏了 close()。这是 fd 泄漏最经典、
# 最一眼可辨的信号。
# === 认知 ===
# ★ 查 fd 问题,别只数总量。要按【类型】和【状态】
# 归类:堆在 socket 还是文件?socket 卡在什么状态?
# 一归类,"是不是泄漏、漏在哪"立刻就清楚了。
修复 3:ulimit——fd 上限是怎么一层层卡的
# === ★ fd 的上限,不止一个,是层层叠叠的好几道 ===
# === ★ 第一道:进程级 ulimit(soft / hard)===
$ ulimit -n # 看当前 shell 的软限制
1024 # ★ 默认常常只有 1024!
$ ulimit -Hn # 看硬限制
4096
# ★ 两个限制的关系:
# - soft(软限制):实际生效的值。进程开 fd,就是
# 撞这个数。
# - hard(硬限制):soft 能调到的天花板。普通用户
# 只能把 soft 往上调到 hard,调不过 hard。
# ★ 临时调大(只对当前 shell 和它的子进程有效):
$ ulimit -n 65536
# === ★ 看某个【正在运行】的进程实际的限制 ===
# ulimit 看的是当前 shell。要看那个出问题的进程:
$ cat /proc/8866/limits | grep "open files"
Limit Soft Limit Hard Limit
Max open files 65536 65536
# ★ 这才是那个进程【真正】在用的上限。
# 进程的限制,在它【启动那一刻】就定死了 ——
# 你之后改 /etc/security/limits.conf 也不影响
# 已经在跑的它,必须重启进程才会用上新值。
# === ★ 第二道:永久配置 /etc/security/limits.conf ===
# ulimit 命令是临时的,重登录就没。要永久生效:
$ vim /etc/security/limits.conf
app soft nofile 65536
app hard nofile 65536
# ^用户 ^软/硬 ^项目 ^值
# ★ 对用户 app 的所有登录会话生效(需重新登录)。
# === ★ 第三道:systemd 服务,limits.conf 不管用!===
# ★ 一个大坑:如果服务是用 systemd 启动的,它【不读】
# limits.conf。要在 service 文件里单独配:
$ vim /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=65536 # ★ systemd 服务的 fd 上限
$ systemctl daemon-reload
$ systemctl restart myapp
# ★ 很多人改了 limits.conf 不生效,就是栽在这 ——
# 服务是 systemd 拉起来的,根本没走 limits.conf。
# === ★ 第四道:系统级总上限 fs.file-max ===
# 上面几道是【单个进程】的限制。整台机器所有进程
# 加起来能开多少 fd,还有个总闸:
$ cat /proc/sys/fs/file-max
6815744
$ cat /proc/sys/fs/file-nr # 看当前全系统用了多少
12800 0 6815744
# ★ 永久改:在 /etc/sysctl.conf 里写 fs.file-max=...
# === ★ 确认到底哪道限制在卡你 ===
# 进程报 Too many open files,挨个对:
# - cat /proc/PID/fd 数量,是不是接近 /proc/PID/limits?
# -> 是,卡在【进程级】限制。
# - file-nr 是不是接近 file-max?
# -> 是,卡在【系统级】总限制。
# ★ 我这次:进程 fd 65530,limits 65536 —— 卡在
# 进程级。和系统总量无关。
# === 认知 ===
# ★ fd 上限是【进程 ulimit -> limits.conf/systemd
# -> 系统 file-max】层层套的。报错时先定位是哪
# 一层在卡 —— 改错了层,白忙活。
修复 4:分清"限制太小"和"fd 泄漏"
# === ★ 这是整件事最关键的一个判断 ===
# === 两种情况,长得像,根上完全不同 ===
# 情况 A:★ 限制确实太小
# 业务量大,这个进程【正常就需要】很多并发连接,
# 而 ulimit -n 还是默认的 1024 —— 名额本来就不够。
# 特征:fd 数量【高,但稳定】,在某个值上下波动,
# 不会无限往上爬。
# -> 解法:把 ulimit 调到合理的值(修复 5)。
#
# 情况 B:★ fd 泄漏(本文这次)
# 代码开了 fd 不关,fd 数【只涨不跌,无限增长】,
# 迟早撞穿任何上限。
# 特征:fd 数量【单调上升】,重启清零后又慢慢爬。
# -> 解法:调大 ulimit【没用】,必须回代码堵泄漏。
# === ★ 一招分清:盯着 fd 数量看一段时间 ===
# 别只看一个瞬间值。每隔一会儿采一次,看【趋势】:
$ while true; do
echo "$(date +%T) fd=$(ls /proc/8866/fd | wc -l)"
sleep 60
done
14:00:01 fd=41200
14:01:01 fd=41950
14:02:01 fd=42710 # ★ 每分钟稳定涨几百
14:03:01 fd=43480
# ★ 判断:
# - fd 数在一个区间里【上下波动】 -> 情况 A,限制小。
# - fd 数【只涨不跌、一路爬升】 -> ★ 情况 B,泄漏。
# 我这次:fd 每分钟稳定净增几百个,从不回落 ——
# 板上钉钉的泄漏。
# === ★ 再一个铁证:看 CLOSE-WAIT 数量的趋势 ===
$ watch -n30 "ss -tan | grep -c CLOSE-WAIT"
# ★ CLOSE-WAIT 持续累积、从不下降 —— 泄漏实锤。
# 健康的服务,CLOSE-WAIT 应该是个很小的、波动的数。
# === ★ 为什么"调大 ulimit"会骗过你 ===
# 调大上限后,泄漏【还在以同样的速度进行】,只是
# 从 0 涨到新上限,需要更久。所以你会看到:
# "故障消失了几天" —— 然后它【一定会回来】。
# ★ "调大限制后,故障只是推迟、没有消失" —— 这个
# 现象本身,就是"这是泄漏、不是限制小"的铁证。
# 我就是看懂了这一点,才扭转了排查方向。
# === 认知 ===
# ★ 撞到 fd 上限,先别急着调大。先盯 fd 数量的
# 【趋势】:波动 = 限制小,可以调;单调上涨 =
# 泄漏,调多大都没用,得回代码。
修复 5:正确解法——堵住泄漏,再合理配额
# === ★ 解法:先堵泄漏(治本),再配合理 ulimit(治标)===
# === ★ 解法 1:定位泄漏的代码位置 ===
# 已知泄漏的是 socket、卡在 CLOSE-WAIT。CLOSE-WAIT
# 说明"对端关了,我方没 close"。回代码查:
# - 所有 socket / 连接,用完是否【一定】被 close?
# - ★ 重点查【异常分支】:try 里打开了连接,catch
# 里抛了异常,close 没被执行到 —— 最经典的泄漏。
# - 各类资源(连接、流、Channel),是否都放在
# finally 里关、或用 try-with-resources / using /
# defer / with 这种【语言保证一定关闭】的写法。
# ★ 内核帮不上忙,泄漏的根永远在应用代码里。
# === ★ 解法 2:用工具锁定是哪个连接没关 ===
# lsof 看泄漏 socket 的对端,往往能反推是哪段逻辑:
$ lsof -p 8866 | grep CLOSE_WAIT | awk '{print $NF}' | \
sort | uniq -c | sort -rn | head
64000 ->10.0.2.30:6379 (CLOSE_WAIT) # ★ 对端是 Redis!
# ★ 立刻缩小范围:泄漏的是【访问 Redis 的那段代码】,
# 多半是 Redis 连接用完没归还连接池/没 close。
# === ★ 解法 3:给资源用"语言级的关闭保证" ===
# 别再依赖"记得手动 close" —— 人一定会忘。用语言
# 提供的、能【保证关闭】的结构:
# - Java:try-with-resources
# - Go:打开后立刻 defer Close()
# - Python:with 语句
# - C++:RAII,析构里关
# ★ 用了这些,close 不再依赖程序员的记性,异常路径
# 也会自动关 —— 这才是根治 fd 泄漏的写法。
# === ★ 解法 4:连接池要配上限和回收 ===
# 用连接池(数据库、Redis、HTTP)时:
# - 配 maxActive(池子最大连接数)—— 池子本身就是
# fd 数量的一道闸。
# - 配【借出去的连接超时回收】,防止某个连接被借走
# 后忘了还,永久占着。
# === ★ 解法 5:把 ulimit 配到合理值(治标,但必要)===
# 堵住泄漏后,也要给进程配一个【与真实并发匹配】的
# ulimit。一个高并发服务,1024 确实太小:
# - 普通用户/会话:改 /etc/security/limits.conf
app soft nofile 65536
app hard nofile 65536
# - ★ systemd 服务:改 service 文件(limits.conf 不管用)
[Service]
LimitNOFILE=65536
# ★ 配额是给"正常的高并发"留够余量,不是用来
# "容忍泄漏"的 —— 这点要分清。
# === ★ 解法 6:把 fd 使用量纳入监控 ===
# fd 泄漏是【缓慢累积】的,不监控,只能等它撞上限
# 爆炸。监控关键进程的:
$ ls /proc//fd | wc -l # fd 使用数
# 对比它的上限,算占用率,超过比如 80% 就告警。
# ★ 再监控 CLOSE-WAIT 总数 —— 它异常增长,就是
# 泄漏的早期信号,能让你在爆炸【之前】就发现。
# === 验证 ===
$ while true; do
echo "$(date +%T) fd=$(ls /proc/8866/fd|wc -l) \
cw=$(ss -tan|grep -c CLOSE-WAIT)"
sleep 60
done
# ★ 修复后:fd 数在一个区间稳定波动、不再单调上涨,
# CLOSE-WAIT 维持在很小的数 —— 泄漏才算真堵上。
修复 6:文件描述符排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 一切皆文件:socket、管道也占 fd,不只是磁盘文件 ===
# === 2. ★ Too many open files = 进程持有的 fd 到顶了 ===
$ ls /proc//fd | wc -l
# === 3. ★ 查 fd 先按类型/状态归类,看堆在 socket 还是文件 ===
$ ls -l /proc//fd | awk '{print $NF}' | sed 's/\[[0-9]*\]//' | sort | uniq -c
# === 4. ★ 大量 CLOSE-WAIT = 代码漏了 close,泄漏铁证 ===
$ ss -tan | grep -c CLOSE-WAIT
# === 5. ★ 盯 fd 数量趋势:波动=限制小可调,单调涨=泄漏 ===
# === 6. ★ 调大 ulimit 后故障只推迟没消失,就是泄漏不是限制小 ===
# === 7. fd 上限层层套:进程 ulimit -> limits.conf/systemd -> file-max ===
$ cat /proc//limits | grep "open files"
# === 8. ★ systemd 服务不读 limits.conf,要配 LimitNOFILE ===
# === 9. ★ 泄漏的根治在代码:用 try-with-resources/defer/with 保证关闭 ===
# === 10. 排查"Too many open files"的步骤链 ===
$ ls /proc//fd | wc -l # ① 持有多少 fd
$ cat /proc//limits # ② 上限是多少
$ ls -l /proc//fd | 按类型归类 # ③ fd 堆在哪类
$ 盯一段时间看 fd 趋势 # ④ 波动还是单调涨
$ 单调涨 -> lsof 找对端 -> 回代码堵 close # ⑤ 根治
# 按这个顺序,"Too many open files"基本能分清真假、能根治。
命令速查
需求 命令
=============================================================
看进程持有多少 fd ls /proc/<PID>/fd | wc -l
看进程的 fd 上限 cat /proc/<PID>/limits | grep "open files"
fd 按类型归类 ls -l /proc/<PID>/fd | awk '{print $NF}' | sort | uniq -c
看进程打开的文件详情 lsof -p <PID>
统计 socket 状态分布 ss -tanp | grep pid=<PID>
数 CLOSE-WAIT 连接 ss -tan | grep -c CLOSE-WAIT
看当前 shell 的 fd 软限制 ulimit -n
看 fd 硬限制 ulimit -Hn
临时调大 fd 上限 ulimit -n 65536
看全系统 fd 总上限 cat /proc/sys/fs/file-max
看全系统 fd 当前用量 cat /proc/sys/fs/file-nr
永久配置(用户) 编辑 /etc/security/limits.conf
永久配置(systemd 服务) service 文件里写 LimitNOFILE=
口诀:Too many open files 是 fd 到顶,socket 也算文件也占 fd
盯趋势:波动是限制小可调大,单调涨是泄漏调多大都没用回代码堵 close
避坑清单
- Linux 一切皆文件,socket 网络连接和管道也算文件也占用文件描述符,不只是磁盘上的文件
- Too many open files 是这个进程持有的文件描述符数量到了上限,先 ls /proc/PID/fd 数一下
- 查 fd 别只数总量,要按类型和状态归类,看清是堆在 socket 还是磁盘文件上,方向立刻就清楚
- 大量连接长期卡在 CLOSE-WAIT 状态,几乎可以断定是代码漏了 close,这是 fd 泄漏最经典的铁证
- 盯 fd 数量的趋势:在一个区间上下波动是限制太小可以调,只涨不跌一路爬升是 fd 泄漏
- 调大 ulimit 后故障只是推迟没有消失,这个现象本身就证明是泄漏不是限制小,调多大都没用
- fd 上限层层套:进程 ulimit 软硬限制,limits.conf 永久配置,系统级 fs.file-max 总闸
- systemd 启动的服务不读 limits.conf,必须在 service 文件里配 LimitNOFILE 才生效
- 进程的 fd 上限在它启动那一刻就定死了,改了配置必须重启进程才会用上新值
- fd 泄漏的根治在应用代码,用 try-with-resources defer with 这类语言级保证关闭的写法,别靠记性
总结
这次"每隔几天就撞穿文件数上限"的事故,纠正了我一个关于"打开"的、藏得极深的直觉。在我的脑子里,"打开一个文件"或者"建立一个连接",一直是一个【瞬间的动作】。我按下这个动作,拿到我要的东西——文件的内容、连接的另一端——然后这个动作就【结束了】。它在我的认知里,像是伸手去开一下灯:开灯这个动作本身,做完就做完了,不会在世界上留下任何需要我后续操心的东西。正因为我把"打开"理解成一个用完即逝、不留痕迹的瞬间动作,所以当报错说"打开的文件太多"时,我的第一反应才那么自然:既然每次打开都是即开即用即走的,那"太多",就只能是同一时刻【正在进行】的打开太多了——是并发太高,是上限太小。我从没想过,一次"打开",会在动作结束之后,还【遗留下一个东西】。可复盘到根上,我才看清,我漏掉的恰恰就是这个"遗留下来的东西"。在 Linux 里,你每"打开"一次,内核并不是默默帮你做完就算了——它会发给你一个文件描述符,一个小小的整数。这个 fd 不是一张用完即弃的票根,它是一个【持续占用的、有名有姓的资源】,是内核记在你这个进程头上的一笔"账"。只要你不明确地把它"关上"(close),这笔账就一直挂在那里,这个 fd 就一直被你占着。所以"打开"这个动作,根本不是我以为的那个会自动收尾的瞬间;它其实是一笔【借贷】——你借走了一个 fd,而内核默认你将来一定会来还。一个进程能借的 fd 是有限的,这就是那个上限。我的服务,在某一个异常分支里,借了 socket 这个 fd,却因为代码没写到位,永远没有执行那句"归还"。于是它一笔一笔地借,一笔都不还,那些没还的账,以 CLOSE-WAIT 的形态,静静地、一个一个地堆积起来。它们不是"正在被使用"的连接——对端早就走了——它们只是一具具我忘了收殓的尸体,却依然实实在在地占着我的 fd 名额。我那把"打开是瞬间动作"的尺子,根本量不出这种"动作早已结束、占用却永久存续"的东西。我去调大 ulimit,本质上是在说"那就让我能借更多吧"——可一个借了从不还的人,你给他再高的额度,他也终有一天会把额度借穿,你给的,只是他把额度耗尽所需要的那点时间而已。这次最大的收获,是我意识到,我对"资源"这个概念的理解,长期停留在一个太天真的层面:我只关心"获取"的那一刻——我得到了我想要的没有——却从不为"获取"这个动作所【创造出来的、需要被对称地销毁】的那个东西负责。获取和释放,本该是一对必须配对出现的动作,像呼和吸;可在我的心智里,我只记得"呼",把"吸"当成了某种会自动发生的、不需要我操心的事。一个不会自己消失的东西,如果我不主动去消灭它,它就会永远累积——这个朴素到近乎可笑的道理,我是付出了一次周期性崩溃的代价才真正记住的。所以下一次,当我的代码里写下任何一个"获取"——打开文件、建立连接、申请内存、加一把锁、拿一个池里的对象——我会强迫自己在写下它的同一时刻,就立刻回答一个问题:这个东西,将在【哪一行代码】、被【谁】、确定无疑地还回去?如果这个问题我答不上来,或者答案是"程序员记得的话就会还",那我就已经亲手埋下了一颗定时炸弹。——很多缓慢累积、终将爆炸的故障,根源都不在那个爆炸的瞬间,而在很久以前,某一次只有"借"、却从来没有配上"还"的、看起来无比平常的"打开"。
—— 别看了 · 2026