1. 事故回放:业务告警一堆,服务器却显示“一切正常”
那天晚上大概十一点半,手机上连续弹出十几条告警,全是同一个集群的“健康检查失败”“HTTP 5xx 比例超阈”。我打开监控面板扫了一圈,第一反应是“这不对劲”:CPU 使用率不到 8%,内存还剩 60% 多,磁盘 IO 也近乎一条直线,没有任何容器被 OOM Kill。这个集群上跑的是公司的 JS 反爬校验服务,核心逻辑是用 Node.js 执行一组 JavaScript 挑战与指纹判定,负载模型属于典型的网络 IO 密集型,平时 CPU 不太高是正常的,可一个请求也回不了,就完全说不通了。
当时我先试着 SSH 登录服务器。结果更诡异:TCP 三次握手能通,但输入密码之后要卡十几秒才进到 shell,敲个 top 都要等半天。换句话说,机器没有“死”,内核还活着,网络栈也能响应连接请求,可就是什么都慢,新请求也处理不了。再看集群里的节点状态,负载均衡器已经开始把流量从这台机器上摘除,于是剩余机器压力迅速翻倍,眼看着也要被拖垮。
这基本就是大家常说的“服务器假死”现场。它不是 CPU 被打满或者内存耗尽那种一眼能看穿的问题,而是某些内核资源到了临界点之后,整个系统服务水平断崖式下降。我后来把所有日志和内核信息翻了一遍,锁定到一个平时几乎没人会盯的参数:fs.file-max。就是这个控制“整个 Linux 能打开多少文件句柄”的全局上限,把整个 JS 反爬服务给推到了悬崖边上。
先说一下这套服务为什么容易踩这个坑。JS 反爬校验服务自身的业务链路非常长:访客浏览器发起带用户行为特征的请求到 Node.js 网关,Node 需要读取请求体里的 JS 执行结果,可能还要去 Redis 里取会话状态,同时要向后端风控服务发起 HTTP 调用做联合判定,再把最终校验结果写回日志系统。每一条请求链路都会打开新的 socket 连接,而 Node.js 的异步模型决定了它在高并发下会同时维持非常多的连接。某些爬虫方发的请求还会故意打开长连接不关闭,或者不断用新的 user-agent 和指纹来试探,这会让每个请求占用的连接和临时文件更难以被及时回收。
这台出事的服务器配置是 8C16G,跑了两个 Node.js 实例,对外承担 JS 挑战的校验任务。系统内核版本是 5.4 的长期维护版,默认的 fs.file-max 内核会根据内存大小自动给一个估算值,我在那台机器上查到的值是 1048576。这个数字看起来很大,但在极端流量下,如果进程级限制和连接回收都没有兜底,一天之内用光并不是不可能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 初期排查:监控没错,是排查方向错了
先说我踩过的弯路。当时看到 CPU 和内存都没问题,我一度把怀疑重点放在应用代码死锁和 Node.js 事件循环阻塞上,甚至怀疑是不是某个版本的依赖包因为 DNS 解析超时卡住了整个进程。于是我先让运维把流量切干净,然后通过带外管理控制台连进去跑了 journalctl -u node-service 和 Node 的堆快照分析,结果是:进程没崩、事件循环延迟正常、GC 也没异常。
2.1 健康检查为什么失败?先从“文件打开失败”开始查
真正让我转向的是一条应用日志:Node.js 进程在某个瞬间开始疯狂输出 EMFILE: too many open files。这是操作系统在尝试创建新的文件描述符时失败时抛出的错误。Node.js 的 HTTP server 在 accept 新连接、读写文件、建立出站 socket 时都会触发这个错误。一旦监听端口无法 accept,服务端的请求队列马上就会灌满,表现为健康检查接口返回 503。
我当时在服务器上执行的第一组命令是这样的:
bash复制# 查看系统级文件句柄整体水位
cat /proc/sys/fs/file-nr
# 查看系统级上限
cat /proc/sys/fs/file-max
# 查看指定进程当前打开的句柄数量
ls /proc/<PID>/fd | wc -l
# 查看进程级软硬限制
cat /proc/<PID>/limits | grep -i nofile
file-nr 的输出是三列数字,含义分别是:系统已经分配的文件句柄数、尚未使用的句柄数、以及系统最大句柄数。正常状态下第一列会远小于第三列。但当时我们看到的第一列值已经逼近第三列,也就是说整个系统的文件句柄池即将见底。
再配合 for pid in /proc/[0-9]*; do echo $pid $(ls $pid/fd 2>/dev/null | wc -l); done | sort -k2 -n | tail,就能一眼看出哪些进程是“吃句柄大户”。结果毫无悬念,两个 Node.js 进程累计占用了差不多 80% 的已分配句柄。这时候排查方向才终于从“代码死锁”转向了“文件描述符泄漏与耗尽”。
2.2 dmesg 里的关键证据,比任何监控面板都诚实
Linux 内核在很多资源即将耗尽时会往内核日志里写警告。dmesg -T 的尾部能看到类似这样的记录:
text复制[Thu Oct 17 23:19:41 2025] fs: file-max limit 1048576 reached
虽然这条日志不一定每次都出现,不同内核版本的打印位置略有差异,但只要看到 file-max limit reached 或 `too many o
