排查了三个小时,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: 12PID 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 服务完全无感。
经验教训
- "Too many open files" 不一定是进程自身的 ulimit 问题
当存储链路中有 FUSE 层(bindfs、sshfs 等)时,错误可能来自 FUSE 守护进程。排查时必须检查完整 IO 链路中的每个环节,而不是只看报错进程本身。
- bindfs + 数据库 = 高风险组合
bindfs 是用户空间文件系统,每个文件操作都有额外的 fd 开销和延迟。数据库(PostgreSQL、MySQL 等)的高频随机 IO 很容易触发 FUSE 层瓶颈。数据库数据目录请直接使用本地磁盘或块存储,不要走 bindfs。
- systemd 服务不吃 limits.conf
这个知识点看起来很基础,但我在凌晨两点排查时还是踩了。limits.conf 只管 PAM 登录 session,systemd 服务有自己的 limits 继承链。改错了文件,等于没改。
- 排查思路的转变:从"改限制"到"查链路"
前两个小时我一直在"改限制"——改容器 ulimit、改宿主机参数、改 NFS 配置。这些改动全部无效,因为瓶颈不在这些地方。当我转而"查链路"——一层一层揭开存储路径时,五分钟就锁定了根因。遇到资源限制类报错时,先查链路,再改限制。
后话
凌晨三点半,告警恢复,用户投诉安抚完毕。我在运维手册里加了一页:
Docker 容器报 "Too many open files" 时,排查路径:
- 检查容器的 ulimit(基础)
- 检查宿主机系统限制(进阶)
- 检查存储链路中的每一层——NFS、bindfs、FUSE(关键)
- 检查 systemd 的 limits 继承链(隐藏)
压垮 PostgreSQL 的,不是它自己打开的文件太多,而是它脚底下那个没人注意到的 bindfs 代理层打开了太多文件。而 bindfs 的软限制,又是从 systemd PID 1 那里继承来的——一个 1024 的默认值,安静地传递给了一个安静的守护进程,最终在一个安静的深夜,炸掉了一个安静的数据库。