服务每隔几天就崩 Too many open files:一次 Linux 文件描述符泄漏排查复盘

一个后台服务平时很稳,可每隔三四天就集体发病:新请求连不进来、日志写不动,满屏 Too many open files。重启立刻恢复,过几天又原样复发。把 ulimit 调大一倍后,故障没消失,只是把复发周期从三四天拖长到六七天——这说明有东西在持续增长,调大上限只是给了它更长的时间涨到新上限。排查梳理:Linux 一切皆文件,socket 网络连接和管道也算文件也占文件描述符;Too many open files 是进程持有的 fd 到了上限,先 ls /proc/PID/fd 数一下;查 fd 别只数总量要按类型和状态归类,看清堆在 socket 还是磁盘文件;大量连接长期卡在 CLOSE-WAIT 几乎可以断定是代码漏了 close,这是 fd 泄漏最经典的铁证;盯 fd 数量趋势,在区间上下波动是限制太小可以调,只涨不跌一路爬升是泄漏;调大 ulimit 后故障只推迟没消失这个现象本身就证明是泄漏不是限制小;fd 上限层层套,进程 ulimit、limits.conf、系统级 fs.file-max;systemd 启动的服务不读 limits.conf 必须在 service 文件里配 LimitNOFILE;进程的 fd 上限在启动那一刻就定死改了配置必须重启进程;fd 泄漏的根治在应用代码,用 try-with-resources defer with 这类语言级保证关闭的写法别靠记性;正确解法是先堵泄漏定位漏 close 的代码、再配合理 ulimit、连接池配上限、把 fd 使用量纳入监控,以及一套文件描述符排查纪律。

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

避坑清单

  1. Linux 一切皆文件,socket 网络连接和管道也算文件也占用文件描述符,不只是磁盘上的文件
  2. Too many open files 是这个进程持有的文件描述符数量到了上限,先 ls /proc/PID/fd 数一下
  3. 查 fd 别只数总量,要按类型和状态归类,看清是堆在 socket 还是磁盘文件上,方向立刻就清楚
  4. 大量连接长期卡在 CLOSE-WAIT 状态,几乎可以断定是代码漏了 close,这是 fd 泄漏最经典的铁证
  5. 盯 fd 数量的趋势:在一个区间上下波动是限制太小可以调,只涨不跌一路爬升是 fd 泄漏
  6. 调大 ulimit 后故障只是推迟没有消失,这个现象本身就证明是泄漏不是限制小,调多大都没用
  7. fd 上限层层套:进程 ulimit 软硬限制,limits.conf 永久配置,系统级 fs.file-max 总闸
  8. systemd 启动的服务不读 limits.conf,必须在 service 文件里配 LimitNOFILE 才生效
  9. 进程的 fd 上限在它启动那一刻就定死了,改了配置必须重启进程才会用上新值
  10. fd 泄漏的根治在应用代码,用 try-with-resources defer with 这类语言级保证关闭的写法,别靠记性

总结

这次"每隔几天就撞穿文件数上限"的事故,纠正了我一个关于"打开"的、藏得极深的直觉。在我的脑子里,"打开一个文件"或者"建立一个连接",一直是一个【瞬间的动作】。我按下这个动作,拿到我要的东西——文件的内容、连接的另一端——然后这个动作就【结束了】。它在我的认知里,像是伸手去开一下灯:开灯这个动作本身,做完就做完了,不会在世界上留下任何需要我后续操心的东西。正因为我把"打开"理解成一个用完即逝、不留痕迹的瞬间动作,所以当报错说"打开的文件太多"时,我的第一反应才那么自然:既然每次打开都是即开即用即走的,那"太多",就只能是同一时刻【正在进行】的打开太多了——是并发太高,是上限太小。我从没想过,一次"打开",会在动作结束之后,还【遗留下一个东西】。可复盘到根上,我才看清,我漏掉的恰恰就是这个"遗留下来的东西"。在 Linux 里,你每"打开"一次,内核并不是默默帮你做完就算了——它会发给你一个文件描述符,一个小小的整数。这个 fd 不是一张用完即弃的票根,它是一个【持续占用的、有名有姓的资源】,是内核记在你这个进程头上的一笔"账"。只要你不明确地把它"关上"(close),这笔账就一直挂在那里,这个 fd 就一直被你占着。所以"打开"这个动作,根本不是我以为的那个会自动收尾的瞬间;它其实是一笔【借贷】——你借走了一个 fd,而内核默认你将来一定会来还。一个进程能借的 fd 是有限的,这就是那个上限。我的服务,在某一个异常分支里,借了 socket 这个 fd,却因为代码没写到位,永远没有执行那句"归还"。于是它一笔一笔地借,一笔都不还,那些没还的账,以 CLOSE-WAIT 的形态,静静地、一个一个地堆积起来。它们不是"正在被使用"的连接——对端早就走了——它们只是一具具我忘了收殓的尸体,却依然实实在在地占着我的 fd 名额。我那把"打开是瞬间动作"的尺子,根本量不出这种"动作早已结束、占用却永久存续"的东西。我去调大 ulimit,本质上是在说"那就让我能借更多吧"——可一个借了从不还的人,你给他再高的额度,他也终有一天会把额度借穿,你给的,只是他把额度耗尽所需要的那点时间而已。这次最大的收获,是我意识到,我对"资源"这个概念的理解,长期停留在一个太天真的层面:我只关心"获取"的那一刻——我得到了我想要的没有——却从不为"获取"这个动作所【创造出来的、需要被对称地销毁】的那个东西负责。获取和释放,本该是一对必须配对出现的动作,像呼和吸;可在我的心智里,我只记得"呼",把"吸"当成了某种会自动发生的、不需要我操心的事。一个不会自己消失的东西,如果我不主动去消灭它,它就会永远累积——这个朴素到近乎可笑的道理,我是付出了一次周期性崩溃的代价才真正记住的。所以下一次,当我的代码里写下任何一个"获取"——打开文件、建立连接、申请内存、加一把锁、拿一个池里的对象——我会强迫自己在写下它的同一时刻,就立刻回答一个问题:这个东西,将在【哪一行代码】、被【谁】、确定无疑地还回去?如果这个问题我答不上来,或者答案是"程序员记得的话就会还",那我就已经亲手埋下了一颗定时炸弹。——很多缓慢累积、终将爆炸的故障,根源都不在那个爆炸的瞬间,而在很久以前,某一次只有"借"、却从来没有配上"还"的、看起来无比平常的"打开"。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

接口偶尔慢 5 秒:一次 Linux DNS 域名解析排查复盘

2026-5-20 22:31:28

Linux教程

磁盘还剩四成却报 No space left:一次 Linux inode 耗尽排查复盘

2026-5-20 22:39:58

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索