在云服务器上跑网关、消息消费者或者高并发的 Java/Go 服务,流量一上来日志里就刷出 Too many open files,新连接建不起来,文件也打不开。这个报错看起来简单,但“改了 ulimit 还是不生效”的情况非常多。本文以 CVM 上常见的 systemd 托管服务为例,梳理排查顺序和几个容易踩的坑。
Linux 对文件描述符(fd)有两层限制,报错文字也不同:
报错 | errno | 含义 | 对应参数 |
|---|---|---|---|
Too many open files | EMFILE | 单个进程打开的 fd 超过了它的 RLIMIT_NOFILE |
|
Too many open files in system | ENFILE | 整个系统已分配的 fd 达到上限 |
|
绝大多数线上问题是第一种:进程级上限太低,或者程序本身在泄漏 fd。
不要在自己的 SSH 会话里执行 ulimit -n 来判断,那只是当前 shell 的值。要看出问题的那个进程:
PID=$(pgrep -f my-service | head -1)
# 进程实际生效的软/硬限制
grep "Max open files" /proc/$PID/limits
# 当前已打开的 fd 数量
ls /proc/$PID/fd | wc -l如果已打开数量贴着软限制,基本可以确定是进程级上限触顶。系统级的情况看 /proc/sys/fs/file-nr,三列分别是已分配数、空闲数(新内核恒为 0)和 fs.file-max:
cat /proc/sys/fs/file-nr/etc/security/limits.conf 由 PAM 的 pam_limits 模块在登录会话时应用,只影响 SSH、su 等通过 PAM 登录启动的进程。由 systemd 启动的服务不经过 PAM,所以这里改得再大,服务进程也看不到。
systemd 服务要在 unit 里配置。推荐用 drop-in,不直接改厂商的 unit 文件:
sudo systemctl edit my-service在打开的编辑器中写入:
[Service]
LimitNOFILE=65535然后重启服务并复核:
sudo systemctl restart my-service
grep "Max open files" /proc/$(systemctl show -p MainPID --value my-service)/limits没有单独配置的服务使用 /etc/systemd/system.conf 里的 DefaultLimitNOFILE。较新的 systemd 上游默认是软限制 1024、硬限制 524288,发行版可能另有修改,以实际查询为准:
systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft注意软限制 1024 对高并发服务明显不够,这是“服务器配置很高却报 fd 不足”的常见原因。
prlimit 可以直接修改运行中进程的限制,不需要重启,适合先止血:
sudo prlimit --pid $PID --nofile=65535:65535
grep "Max open files" /proc/$PID/limits这只对当前进程有效,服务重启后恢复原值,事后一定要补上 unit 配置。另外,进程的硬限制不能超过内核参数 fs.nr_open,设置过大会直接失败。
如果把上限调到 65535 之后,fd 数量仍然持续增长直到再次触顶,问题就不是配置,而是程序没有释放资源。先按类型统计:
sudo lsof -p $PID | awk 'NR>1 {print $5}' | sort | uniq -c | sort -rn | headIPv4/IPv6/sock 占大头:多半是连接没关。再看连接状态:sudo ss -tanp | grep "pid=$PID," | awk '{print $1}' | sort | uniq -c大量 CLOSE-WAIT 说明对端已经关闭,而本进程没有调用 close,常见于 HTTP 客户端响应体没关闭、连接池使用不当。
REG(普通文件)占大头:用 ls -l /proc/$PID/fd 看具体指向哪些文件,通常能直接定位到忘记关闭的日志或临时文件。可以每隔几分钟记录一次 ls /proc/$PID/fd | wc -l,只增不减就是泄漏,和业务高峰同步起落则更可能是正常的并发需求。
Docker 容器内进程的上限默认继承自容器运行时(dockerd/containerd)自身的设置,与宿主机登录会话无关;也可以在 /etc/docker/daemon.json 的 default-ulimits 里统一指定。单个容器需要单独设置时,在启动参数里指定:
docker run --ulimit nofile=65535:65535 ...Kubernetes 中则取决于容器运行时的配置,排查方法相同:进入容器后看 /proc/1/limits。
即使系统上限调高了,使用 select() 的老程序仍然只能处理编号小于 FD_SETSIZE(通常为 1024)的 fd,超过后会出现异常甚至内存越界。遇到这种情况,调整 ulimit 没有用,需要把代码改为 poll 或 epoll。
排查顺序可以归纳为:
/proc/<pid>/limits 和 /proc/<pid>/fd 看真实上限和用量;LimitNOFILE,不要指望 limits.conf;prlimit 止血,事后补配置;原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。