排查了三个小时,Too many open files 的罪魁祸首竟然不是 ulimit

凌晨零点,生产环境的告警钉钉群炸了。

PostgreSQL 容器疯狂报错:

FATAL:  could not open file "global/pg_filenode.map": Too many open files
LOG:  out of file descriptors: Too many open files; release and retry

依赖它的 dolphin-1 应用服务同步崩溃,用户开始投诉。

我冲进服务器,第一反应——这是经典的 ulimit 问题。

第一轮:改 ulimit,无效

进入 PostgreSQL 容器:

docker exec -it <container> bash
ulimit -n          # 1024

容器内单个进程的 fd 数不过几十到几百,但限制只有 1024。看起来就是这个问题。

我在 docker-compose 里加上 ulimits.nofile 配置,重启容器。

报错依旧。

第二轮:查宿主机,限制充足

ulimit -n                          # 65536
cat /proc/sys/fs/file-max          # 9223372036854775807
cat /proc/sys/fs/file-nr           # 7712    0       9223372036854775807

宿主机资源充足得很,完全不是瓶颈。我顺手把 NFS 客户端的 slot table 也调大了:

echo 128 > /proc/sys/sunrpc/tcp_slot_table_entries

还是无效。

两个小时的排查,改了三个配置,零效果。

第三轮:翻开存储链路,真相浮出水面

我开始从底层往上翻:

mount | grep nfs
# 10.1.0.10:/var/nfs/shared/容器 on /opt/.dockerd type nfs ...

mount | grep bindfs
# /opt/.dockerd/zoo/elephant /opt/dockerd/zoo/elephant fuse.bindfs ...

存储链路是这样的:

NFS 服务端 → NFS 客户端 (/opt/.dockerd) → bindfs (FUSE) → /opt/dockerd → Docker Volume → PostgreSQL

中间有一层 bindfs——一个用户空间的 FUSE 文件系统,用来做权限映射。

我检查 bindfs 进程的 fd 数:

for pid in $(pgrep bindfs); do
  echo "bindfs PID $pid: $(ls /proc/$pid/fd 2>/dev/null | wc -l)"
done

输出:

bindfs PID 1128: 54
bindfs PID 1138: 5
bindfs PID 1159: 1024    ← 异常!正好是软限制
bindfs PID 1164: 12

PID 1159,对应 elephant 挂载点,fd 数正好是 1024——触达软限制上限。

再看它的 limits:

cat /proc/1159/limits | grep "Max open files"
# Max open files  1024  524288  files

软限制 1024,硬限制 524288。问题就卡在软限制上。

根因:不是 PostgreSQL 的 ulimit,是 bindfs 的 ulimit

PostgreSQL 容器内的 ulimit -n 是 65536,进程自身远未触达。但当 PostgreSQL 打开一个数据文件时,请求经过 Docker Volume → bindfs → NFS 这条链路。bindfs 作为 FUSE 代理,每打开一个文件,自身需要持有一个 fd 来转发访问。

数据库并发高时,bindfs 的 fd 数触达自己的软限制 1024,无法继续代理文件打开请求,向 PostgreSQL 返回 EMFILE(Too many open files)。

PostgreSQL 报了错,但错不在 PostgreSQL。错在它脚底下那个透明的代理层。

这就像你打电话投诉客服说"我打不通电话",客服查了自己的线路一切正常——因为真正出问题的是中间那根你根本不知道存在的转接线。

修复

应急(立即生效,不重启)

for pid in $(pgrep bindfs); do
  prlimit --pid $pid --nofile=65536:65536
done

一分钟后,PostgreSQL 恢复正常,所有报错消失。

永久(重启后仍生效)

mkdir -p /etc/systemd/system.conf.d/
cat > /etc/systemd/system.conf.d/limits.conf << 'EOF'
[Manager]
DefaultLimitNOFILE=65536
EOF
systemctl daemon-reload

注意:这里用的是 systemd 的 system.conf,不是 /etc/security/limits.conf——因为 systemd 服务(包括 bindfs 这种 mount 单元)根本不读 PAM 的 limits.conf。这个坑我踩了,希望你不用再踩。

ulimit 的继承链路

搞明白这条链路,以后遇到类似问题可以直接定位:

内核(nr_open) → systemd(PID 1) → system.conf/服务单元 → 具体进程
                      ↓
                 PAM/limits.conf → 登录 shell/用户进程
                      ↓
                 Docker daemon.json → 容器默认
  • /etc/security/limits.conf 只对 PAM 认证的登录 session 有效
  • systemd 服务的 limits 由 system.conf 或 service 单元文件控制
  • 容器的默认 limits 由 Docker daemon 配置决定

三者互不相干。你改了 limits.conf,systemd 服务完全无感。

经验教训

  1. "Too many open files" 不一定是进程自身的 ulimit 问题

当存储链路中有 FUSE 层(bindfs、sshfs 等)时,错误可能来自 FUSE 守护进程。排查时必须检查完整 IO 链路中的每个环节,而不是只看报错进程本身。

  1. bindfs + 数据库 = 高风险组合

bindfs 是用户空间文件系统,每个文件操作都有额外的 fd 开销和延迟。数据库(PostgreSQL、MySQL 等)的高频随机 IO 很容易触发 FUSE 层瓶颈。数据库数据目录请直接使用本地磁盘或块存储,不要走 bindfs。

  1. systemd 服务不吃 limits.conf

这个知识点看起来很基础,但我在凌晨两点排查时还是踩了。limits.conf 只管 PAM 登录 session,systemd 服务有自己的 limits 继承链。改错了文件,等于没改。

  1. 排查思路的转变:从"改限制"到"查链路"

前两个小时我一直在"改限制"——改容器 ulimit、改宿主机参数、改 NFS 配置。这些改动全部无效,因为瓶颈不在这些地方。当我转而"查链路"——一层一层揭开存储路径时,五分钟就锁定了根因。遇到资源限制类报错时,先查链路,再改限制。

后话

凌晨三点半,告警恢复,用户投诉安抚完毕。我在运维手册里加了一页:

Docker 容器报 "Too many open files" 时,排查路径:

  1. 检查容器的 ulimit(基础)
  2. 检查宿主机系统限制(进阶)
  3. 检查存储链路中的每一层——NFS、bindfs、FUSE(关键)
  4. 检查 systemd 的 limits 继承链(隐藏)

压垮 PostgreSQL 的,不是它自己打开的文件太多,而是它脚底下那个没人注意到的 bindfs 代理层打开了太多文件。而 bindfs 的软限制,又是从 systemd PID 1 那里继承来的——一个 1024 的默认值,安静地传递给了一个安静的守护进程,最终在一个安静的深夜,炸掉了一个安静的数据库。

最后修改:2026 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏