上个月底一台跑着 Nginx 加 Node 应用的机器突然开始返回 502,日志里隔三差五冒出一行 Too many open files。当时第一反应是文件句柄不够,去服务器上执行 ulimit -n,返回 65535,心里瞬间踏实了。可服务重启后该报错照旧,十分钟后 502 变成大面积 502,连接直接被内核掐断。
折腾了一个多小时才发现,问题出在 systemd 管的 Node 服务上:limits.conf 里改的 ulimit 对 systemd 启动的服务根本不管用,得在 service 文件里单独写 LimitNOFILE 才行。而这台机器上真正卡住它的,其实是另一个数字:内核的 fs.nr_open 只有 1048576,看起来很大,但系统级上限和进程级上限叠加起来,再加上连接没及时回收,句柄很快就见底了。
这次踩坑让我意识到,很多「服务器优化」文章只讲 ulimit -n 65535 这一句,漏掉了 systemd 和内核参数这两层。真正线上要调优,得把这三层一起看。
想直接上手调自己的机器,又不想每次都开一堆终端窗口切换着敲命令的,可以试试这台服务器上的 UltimateShell(远程服务器管理工具箱):多标签会话、批量下发命令和常用运维脚本都能在一个窗口里完成,排查参数的时候来回切会话的效率会高很多。
Too many open files 这个错误有三个可能来源,位置不同,排查手段完全不同:
第一层是内核级。整个系统能打开的文件总数由 fs.file-max 控制,单个进程能打开的最大数量由 fs.nr_open 兜底。这两个值在 /proc/sys/fs/ 下,可以用 cat /proc/sys/fs/file-nr 看到当前分配数、分配总量和峰值。
第二层是进程级。也就是 ulimit 的 nofile 项,也就是常说的文件句柄上限。这个值每个进程独立,SSH 登录后执行 ulimit -n 看到的软限制,以及 ulimit -Hn 看到的硬限制,都属于这一层。
第三层是服务级。服务怎么被拉起来,决定了它能拿到多少句柄。systemd 启动的服务、supervisord 托管的服务、Docker 容器里的进程,各自遵循不同的限制规则。这层最容易被忽略,也是我这台机器翻车的根源。
先看内核参数。fs.file-max 是全系统文件句柄的分配上限,fs.nr_open 是单个进程句柄数的硬天花板。默认配置下 nr_open 通常是 1048576,也就是单个进程最多约 100 万个句柄,理论上足够绝大多数业务。
我一般会先确认这两个值:
sysctl fs.file-max fs.nr_open
如果发现 nr_open 比业务需要的句柄数还小(比如要给每个连接分配句柄的网关类服务),那就得往上抬。修改方式是写进 sysctl 配置目录,而不是直接用 sysctl -w,因为后者重启就丢:
echo "fs.file-max = 2097152" > /etc/sysctl.d/99-server.conf
echo "fs.nr_open = 2097152" >> /etc/sysctl.d/99-server.conf
sysctl --system
注意 fs.nr_open 的上限受编译内核时的 NR_OPEN 常量约束,设再大也会被截断,所以别把数字写得过于夸张,够用就行。
ulimit 有软限制(soft limit)和硬限制(hard limit)两个概念。软限制是进程当前实际能用的值,可以在自己有权限的情况下往下调;硬限制是软限制的天花板,只有 root 才能往上抬,而且抬到硬限制之后,这个会话里的普通用户就再也调不回去了。
改法有三种,作用域各不相同:
第一种是临时改,只影响当前终端会话及其子进程:ulimit -n 65535,退出登录就失效。
第二种是全局改,写 /etc/security/limits.conf,对 PAM 认证的登录会话生效:
* soft nofile 65535
* hard nofile 65535
root soft nofile 65535
root hard nofile 65535
第三种是给某个用户单独设,用 username 替换前面的星号。
关键点在于第二种和第三种只管 PAM 登录会话。systemd 启动的服务不走 PAM,所以你在 limits.conf 里写一百行,服务该多少还是多少。
这台机器上的 Node 服务是一个 systemd unit,正确的做法是给它加一个 drop-in 覆盖文件,而不是去动 limits.conf:
mkdir -p /etc/systemd/system/node-api.service.d
vi /etc/systemd/system/node-api.service.d/override.conf
内容是:
[Service]
LimitNOFILE=65535
LimitNPROC=65535
然后重载并重启服务:
systemctl daemon-reload
systemctl restart node-api
改完怎么确认生效?看进程自己的 limits 文件,这比看配置文件靠谱得多:
cat /proc/$(pgrep -f node-api | head -1)/limits | grep -i "open files"
输出里的 Max open files 那一行,如果 soft 和 hard 都是 65535,说明这次真的落到进程上了。如果还是默认的 1024 或者 4096,说明覆盖文件没被读取,常见原因是 unit 名字写错、drop-in 路径不对,或者改完没执行 daemon-reload。
顺便说一句,容器里的进程同理。Docker 的 --ulimit nofile=65535:65535 是唯一靠谱的方式,容器内改 /etc/security/limits.conf 完全无效,因为容器里没有 PAM,也没有 systemd。
文件句柄撑不住,很多时候根因不在句柄本身,而在连接被占着不放。几个常配的参数:
net.core.somaxconn = 4096,这是 accept 队列的上限,内核 5.4 之前的默认值常常只有 128,高并发下新连接会被直接丢弃,表现就是偶发连接超时。
net.ipv4.tcp_max_syn_backlog = 8192,半连接队列长度,防 SYN Flood 也能顺带改善突发流量下的握手成功率。
net.ipv4.ip_local_port_range = 10000 65535,客户端能用 ephemeral port 的范围。默认值 32768 到 60999 只有约 28000 个,做代理、压测客户端、爬虫这类短连接多的场景很容易把端口耗尽,表现是 connect 报 Cannot assign requested address。
net.ipv4.tcp_tw_reuse = 1,允许复用 TIME_WAIT 状态的 socket,在短连接为主的场景下能明显缓解端口不够用的问题。注意它只对出向连接有意义,作用有限,别指望它包治百病。
net.core.netdev_max_backlog 和 net.ipv4.tcp_max_orphans,处理网卡收包堆积和孤儿 socket 堆积,上行带宽大或者大量连接被强杀时值得调。
下面这几条基本能覆盖八成的场景,我一般按顺序敲:
ss -s 看当前 socket 总量、TCP 连接状态分布,能一眼看出是不是 TIME_WAIT 堆积成山。
cat /proc/sys/fs/file-nr 看系统级文件分配情况,allocated 接近 max 就危险了。
ls /proc/$(pgrep -f 你的服务名)/fd | wc -l 数进程当前实际占用的句柄数,比查配置直观得多。
lsof -p 进程PID | wc -l 进一步看这些句柄都是些什么文件,绝大多数情况下能发现是某个日志文件、socket 或者 pipe 没关。
cat /proc/sys/net/ipv4/ip_local_port_range 确认端口范围,有时候「Too many open files」其实是端口耗尽在伪装。
第一,改完 limits.conf 没重启 SSH 会话,以为不生效,其实 PAM 只在登录时读一次配置,新开的会话立刻就生效了,但当前会话没变。
第二,硬限制没提就写软限制。ulimit -n 65535 在硬限制是 4096 的会话里会直接报 operation not permitted,得先 ulimit -Hn 抬硬限制。
第三,把所有服务的 LimitNOFILE 都设成 65535 就算完事。句柄是内存开销,一个进程几十万句柄是要吃内存的,盲目调大反而可能把内存吃满。真正该做的是先查清楚句柄泄漏在哪,而不是把天花板掀掉。
第四,sysctl -w 改完就宣布完事。机器重启或者某些面板重启服务后参数全丢,恢复原形,必须写进 /etc/sysctl.d/ 下的文件并执行 sysctl --system 才叫改完。
我的收尾动作一般是这五步:
一、sysctl --system 确认内核参数落盘,用 sysctl 命令回读一遍。
二、systemctl daemon-reload 加 restart 之后,cat /proc/PID/limits 看进程的 Max open files。
三、ss -s 看连接状态,确认 TIME_WAIT 和 CLOSE_WAIT 没有异常堆积,特别是 CLOSE_WAIT,多了说明应用侧漏了 close。
四、用压测工具按目标并发打一轮,观察 fd 使用量和错误率,别只在低并发下自我感觉良好。
五、重启服务器做一次冷启动验证,这一步能抓出所有「只在当前会话有效」的伪配置,我那台机器就是冷启动之后才彻底老实。
整套东西捋下来其实就一句话:文件句柄的上限是分层的,内核管天花板、ulimit 管会话、systemd 管服务,你要调的是这三层的合集,而不是在 SSH 里敲一句 ulimit -n 65535 就宣布完工。少调一层,服务就用真实的低限值在跑,出问题的时候还特别难查因为配置文件明明写着 65535,看起来特别专业。