☰
告别Linux暗坑:监控面板看不到却会搞垮业务的指标
2026/10/20 2:42:56 网站建设 项目流程

半夜三点,你是被一条报警从床上拽起来的:业务主机负载不高、内存也够,磁盘df -h一看还剩 79% 的空间,但服务就是写不进任何文件。重启应用没用,重启网卡没用,直到你顺手敲了下df -i,才发现 inode 使用率已经 100%。这种“从指标上看一切正常、业务却已经半瘫”的情况,我管它叫 Linux 暗坑。

这类坑最麻烦的地方在于:它们往往不在最显眼的监控面板上。你监控了 CPU、内存、磁盘使用率、带宽,但真正杀死业务的却是另外一批不太起眼的信号。这篇文章把我这些年踩过的、以及帮别人排查时见过的 Linux 暗坑系统梳理一遍,每个坑都会讲清楚三件事:为什么会发生、从哪个指标能提前发现、日常监控里该怎么盯。如果你负责的机器超过十台,建议把这些检查项直接加进你的监控和巡检脚本里。

1. 文件系统视角的三处暗礁

1.1 inode耗尽:有空间,但你写不进任何东西

开头说的那个场景就是 inode 耗尽。inode 是文件系统里用来记录文件元数据的结构,每个文件、目录、软链接都要占用一个 inode。df -h看的是数据块的使用情况,df -i看的才是 inode 使用情况,两块互相独立,一个满了另一个可能还很空闲。

遇到最多的两种写爆 inode 的场景:一种是程序产生海量小文件,比如消息队列的消费位点文件、任务调度系统临时落盘、PHP 会话文件没做清理;另一种是容器环境里 overlayfs 上层目录残留,容器删了但数据没清干净,一层一层堆出几十万个小文件。某次帮朋友看一台跑爬虫的机器,du -sh显示只用了 30 多 G,但df -i已经 100%,一查/tmp下面有 200 多万个 session 文件。

提前发现的路径很明确:把df -i纳入监控,使用率超过 85% 就要告警。这是个纯增量指标,只涨不降,一旦到 100% 连touch一个空文件都做不到,很多 Java 应用会直接抛No space left on device,误导你以为是磁盘满了。

处理方式分两步。临时手段是找出大量小文件的目录,删除或归档;根治手段是让业务方改行为,比如把小文件合并存储、加定时清理任务。这里有个容易踩的细节:ext4 文件系统在格式化时可以指定-i或-N调整 inode 密度,但生产环境已经用了一两年的盘,基本不可能原地扩容 inode,只能迁移数据后重新格式化,成本很高。所以新盘上线时就要估算 inode 数量,宁可多给别抠门。

1.2 文件描述符耗尽:应用还能跑,但你连不上

文件描述符是进程访问文件、socket、管道的凭证,Linux 默认对单进程的限制是 1024,虽然现在很多发行版已经调高了,但你部署的应用未必跟着走。这个问题难发现的原因在于:进程还活着,CPU 也不高,但新连接全部失败,表现是客户端疯狂报Connection refused或Too many open files。

排查命令三板斧:cat /proc/sys/fs/file-nr看全系统 fd 使用量;ls /proc/<PID>/fd | wc -l看单进程 fd 数;ulimit -n看当前 shell 的进程限制。我曾经排查过一个 Java 网关,fd 数涨到 5 万后开始拒绝连接,原因是连接池里的连接被远端重置后,客户端没有及时感知,socket 一直占着 fd 不释放。

监控建议看两个层面:全系统层用file-nr的已分配量与最大值做比值,超过 70% 告警;进程层用监控工具采集每个重要进程的 fd 数量,设置趋势告警而不是固定阈值——比如 10 分钟内增长超过 50% 就报警,很多泄漏都是这样提前抓到的。

还要提醒一个 systemd 的坑:很多人改了/etc/security/limits.conf里nofile,但服务是 systemd 管理的,systemd 会用自己的LimitNOFILE覆盖这个设置。正确做法是在 service 文件里加LimitNOFILE=65535再daemon-reload,否则改半天不生效,线上照样有限制。

1.3 脏页与回写:平时看不见,一断电就教你做人

Linux 会把写操作先进 page cache,再由内核线程异步刷到磁盘,这就是脏页(dirty pages)机制。这么设计是为了性能,但暗坑也在这里:如果系统突然掉电,或者后端存储出问题,没刷下去的脏页就是丢的数据。

监控上容易被忽略的两个文件:/proc/meminfo里的Dirty和Writeback字段。Dirty 表示等待写回磁盘的脏页总量,Writeback 表示正在写回的量。正常情况下 Dirty 应该在一个较低水平波动,如果长时间维持在高位甚至持续增长,说明磁盘写能力跟不上业务写入速度,或者回写线程出了问题。

如果你管理的是数据库主机或跑着核心状态服务的机器,我的建议是把vm.dirty_ratio、vm.dirty_background_ratio调低一些。默认背景回写阈值是 10%,比率是 20%,对服务器来说偏激进。适当调低到 5% 和 10%,会换来写入延迟更平滑、掉电丢失窗口更小,代价是稍微牺牲一点突发写性能。对于有备用电池的 RAID 卡环境,可以保留默认值,因为写缓存有电池保护,掉电不会直接丢数据。

2. 内存与交换空间的隐形黑洞

2.1 Swap换入换出:内存看着够,服务却慢得像蜗牛

很多人看内存只看 used 和 free,忽略了一个关键问题:内存够不够,不取决于总量,而取决于内存回收压力。当系统内存紧张时,内核会把不常用的内存页换到 swap,这个过程本身不致命,致命的是频繁的 swap in / swap out。

判断标准就一条:vmstat 1里 si 和 so 两列。如果 si/so 长期非零且数值较大,说明内存回收非常频繁,即使你还有不少空闲内存也可能发生——这种情况常见于 cgroup 内存限制导致的回收、页缓存被过度清理、或者程序内存访问模式差导致大量缺页。

还有一个被忽略的坑:swap 指标里swappiness参数。常年有人把vm.swappiness=0当成性能优化,但对某些场景反而有害。比如 Redis 这类自己管理内存的应用,swappiness 设置过高会让 Redis 的内存页被换出,导致延迟抖动;但如果你把它设成 0,在内存真的不够时,内核会优先杀死进程而不是用 swap 缓冲,OOM 风险反而更大。折中方案是设 10 左右,既减少 swap 使用倾向,又保留一定的缓冲能力。

监控上不要只盯 swap 总量,要看 si/so 的速率和持续时长。如果 si/so 连续 5 分钟非零,就需要查是哪个进程在大量申请内存,或者是不是 cgroup 限制压得太紧。

2.2 上下文切换爆表:CPU 很闲,吞吐就是上不去

上下文切换是 CPU 从一个进程/线程切换到另一个的过程,切换本身有成本,频繁切换等于把 CPU 时间花在了“换人”而不是“干活”上。vmstat的 cs 列可以看到每秒上下文切换次数,正常服务器可能在几千到一两万,如果上了几十万,即使 CPU 使用率只有 20%,业务吞吐也会非常难看。

高频切换通常有三个来源:线程数量过多且互相竞争锁、短连接服务的线程反复创建销毁、或者协程/线程池配置不合理导致大量唤醒。我遇到过一个诡异案例:一个 Java 服务 CPU 使用率才 15%,QPS 却一直上不去,vmstat一看 cs 飙到 40 万,pidstat -w定位到是 GC 线程和业务线程在锁上互相争抢,调整锁粒度后 QPS 直接翻倍。

监控建议:把 cs 列和当前运行队列长度(vmstat 的 r 列)放在一起看。cs 很高但 r 也很高,说明 CPU 确实忙不过来;cs 很高但 r 很低,说明任务切换里有一大部分是无效的空转,重点排查锁竞争和线程模型。

2.3 平均负载与 CPU 的错位:你看到的数字会骗人

top右上角的 load average 是很多人判断系统压力的第一指标,但这里的暗坑在于:平均负载是运行队列中进程数的滑动平均,它不仅包含 CPU 可运行队列,还包含处于不可中断睡眠状态的进程。也就是说,一个进程卡在磁盘 IO 上,也会拉高负载,而 CPU 使用率可能看起来很正常。

真实案例:某数据库备机 load average 到了 30,但 CPU 只有 5% 使用率。从监控面板看是一台“很闲”的机器,实际是底层存储出问题,IO 请求全部阻塞在等待中,备份进程一直卡在不可中断状态。如果不看iostat的util、await和vmstat的wa列,这个问题可能要把人折磨好几天。

所以监控负载必须三件套配合:load average 要跟 CPU 的 us/sy/wa 联合看,wa 高说明 IO 瓶颈,sy 高说明系统调用或锁竞争,us 高才算真正的 CPU 计算压力。只发一个 load 告警打死也不知道问题在哪。

3. 网络层的三套路边的雷

3.1 连接跟踪表又满了:流量没到服务,包就被丢了

当你的机器开启了防火墙或使用 NAT 时,内核会用nf_conntrack维护一张连接跟踪表,每个经过的连接都占用一条记录。这张表有上限,默认值在不同发行版上不一样,有的只有 65536 或 262144。一旦 track 表满,所有新建连接都会被直接丢弃,表现就是“服务还能 ping 通,但业务端口死活连不上”。

难排查的地方在于:ss -lnt看监听端口正常,应用日志也干干净净,因为包根本没到达应用层。这种情况下你要看conntrack -S或者/proc/sys/net/netfilter/nf_conntrack_count,跟最大值做对比。曾经有个客户报障说“网络时不时抽风”,查了很久发现是他们的 DNS 解析用的是短连接,查询量一大 conntrack 表瞬间填满,连 ssh 都登不上。

监控指标很简单:当前跟踪条数除以最大值。超过 60% 就要关注,超过 80% 必须告警。解决方式有两个方向:调大nf_conntrack_max,但每条记录大约 300 多字节内存,调太大会增加内存开销;更有效的是减少连接数,比如把短连接改成连接池、加大 DNS 缓存时间、调整 TCP 超时参数让条目尽快释放。注意:调整 conntrack 参数后重启会失效,必须写入/etc/sysctl.conf。

3.2 TIME_WAIT 和 CLOSE_WAIT:一个被高估,一个被低估

TIME_WAIT 被讨论得最多,其实它远没有大家想的那么可怕。TIME_WAIT 是主动关闭方在收到对端 FIN 后进入的状态,设计目的是确保最后的 ACK 能被可靠送达,同时防止旧连接的数据包干扰新连接。大量 TIME_WAIT 表示系统主动关闭了大量连接,会占用本地端口,但通过调整net.ipv4.ip_local_port_range扩大可用端口范围,或者用连接池复用连接,基本都能缓解。那句流传很广的net.ipv4.tcp_tw_reuse=1确实有用,但注意它只适用于发起连接的一方,而且内核版本新一些的 Linux 默认就开启了这个行为。

真正需要你紧张的是 CLOSE_WAIT。CLOSE_WAIT 表示对端已经关闭连接,但本地应用还没调用close(),本质上就是连接泄漏。如果ss -s里看到 CLOSE_WAIT 数量持续增长,说明应用代码里有 socket 没有正确关闭,常见于读取响应后没有关闭流、异常分支里没释放连接。这个泄漏最终会占满 fd 或端口,吞掉整个服务的可用连接。

监控建议用ss -ant | awk '{print $1}' | sort | uniq -c统计状态分布,CLOSE_WAIT 超过连接总数的 5% 且持续上涨就要告警。处理 CLOSE_WAIT 没有捷径,只能排队等开发改代码。临时缓解手段是调短应用层空闲超时,让对应连接早点被回收。

3.3 网卡错误包与重传率:链路质量的第一道警报

业务超时、延迟抖动这类问题,很多人第一反应是看服务端负载,却忽略了物理链路上可能已经出现故障。ip -s link能显示网卡收发的错误包、丢包、冲突,ss -ti能看到每条 TCP 连接的重传情况。我之前排查过一次间歇性延迟升高:应用日志显示偶尔有请求耗时超过 5 秒,服务端 CPU、内存、磁盘全正常,最后是ethtool -S eth0发现rx_crc_errors和rx_errors在持续增长,加上光模块收发功率异常,判断是光纤链路老化导致偶发丢包。

监控上要盯两条线:物理层看网卡的 errors/dropped/overruns 计数,只要非零且增长就说明有问题;传输层看重传率,或者看ss -ti输出里retr计数。重传率超过 1% 就要警惕,超过 2% 基本可以断定链路或对端有问题。

这里有个容易踩的坑:网卡的 dropped 不一定是链路问题,也可能是环形缓冲区太小导致收包来不及处理,ethtool -G eth0 rx 4096可以调大,但调大也有内存开销和中断延迟的副作用。要把计数器增长趋势和当时系统 CPU 软中断占用结合起来判断,不能一概而论。

4. 时间与任务调度:最容易被忽视的“定时炸弹”

4.1 时钟偏移:证书过期、日志错乱、集群脑裂的起点

时间偏移是个典型的“平时没事,出问题就是大事”的暗坑。当集群里某台机器的时钟比其它机器快了几分钟,一系列诡异现象就会冒出来:Kerberos 认证失败、TLS 证书验证报错(因为证书有效期判断依赖本地时间)、分布式系统脑裂、日志时间线错乱导致排查难度暴涨。

检查手段:timedatectl看同步状态,chronyc tracking或ntpq -p看 offset 值。注意的一件事是虚拟机和容器场景,宿主机开启时间同步后,虚拟机内又跑了一个 NTP 客户端,两边可能会产生冲突,导致时间一步跳变而不是平稳追平。

监控建议:offset 超过 100ms 告警(高精度交易类场景要更严格到 10ms 甚至 1ms),同时监控 NTP 同步进程本身是否存活。如果你用的是 chrony,还要关注Leap status是否为Normal,如果是Not synchronised说明它已经失去了时间源,这种状态可能持续很久而没人发现。

4.2 定时任务:你以为它每天都在跑,其实它早就停了

cron 大概是运维里最没有存在感、但又最容易断的环节。常见坑包括:机器在某次维护后没开机导致任务错过执行时间、系统时间漂移导致原定凌晨 2 点的任务挪到了白天高峰期、“cron 日志被系统清掉了从来没人看”导致任务静默失败好几周。

我见过最典型的一个事故:某个数据同步任务依赖脚本里的环境变量,但 crontab 里执行bash script.sh时没有继承登录 shell 的环境变量,脚本一直报command not found,数据链路断了两周才被人发现。事后加了一句source /etc/profile才解决。

监控上有两个建议:一是把关键任务的执行结果主动写到特定日志文件或发到消息队列,由监控系统采集,不能只依赖/var/log/cron;二是写 crontab 时养成固定习惯——重定向输出到独立日志文件、脚本开头声明解释器路径、关键任务加锁防止重入、任务内容保持幂等。任何一个习惯都能在关键时刻救你一命。

4.3 logrotate:日志膨胀到把磁盘撑爆,往往不是日志本身的错

很多应用日志管理靠的是logrotate,它由 cron 触发,配置文件在/etc/logrotate.d/下。暗坑在于:logrotate 默认是按天轮转,如果你的 cron 服务没跑、或者轮转时间设置成了日志写入高峰,就可能出现“日志文件已经被旋转了,但应用还在往旧 inode 写”的诡异情况,表现为磁盘空间没省下来,日志内容还在持续增长但文件看起来已经不存在。

更隐蔽的一个问题:某些应用(经典的如 Java 的 log4j 1.x,以及一些不会主动重新打开文件句柄的守护进程)在 logrotate 切割日志后,仍然把数据写进旧文件句柄,新日志文件始终是空的。这种情况下你会在磁盘上看到大量.1、.2后缀的日志文件,而主日志文件刚建出来就一直是 0 字节。

监控建议:对日志目录的总体积设上限,配合find /var/log -name "*.log" -size +1G之类的大日志排查;遇到应用不释放句柄的情况,可以在 logrotate 配置里加copytruncate参数,或者强制应用配置按天生成带日期格式的日志文件名。copytruncate有极小的概率丢几行日志,但对大多数非核心服务来说可以接受。

5. 内核与进程信号:事故现场的“第一现场”

5.1 dmesg里的“临终遗言”:应用日志干净,不代表没出事

当应用进程突然挂掉,大家习惯先去翻应用日志,但有一个地方经常被忽略:内核环形缓冲区,也就是dmesg。OOM killer 杀进程的记录、硬件 IO 报错、文件系统异常、soft lockup —— 这些信息只出现在内核日志里,应用自身往往毫不知情。

典型场景:数据库实例无征兆 crash,应用日志显示“连接被强制关闭”,查半天找不到原因,实际上dmesg -T | tail -50里赫然写着某个 IO 设备有超时或错误,文件系统直接进入了只读状态。又比如 Java 进程突然消失,dmesg里一条Out of memory: Kill process直接锁定真凶。

监控建议:不要等出问题了才去敲dmesg。部署 node-exporter 或 telegraf 时,把内核错误、OOM 事件、磁盘 IO 错误、网卡设备错误这类关键字采集出来,接入告警。定期执行journalctl -k -b -p err也能帮你扫一眼当前启动周期里有没有内核级错误。这条建议价值极高,因为它往往能把几小时的排查缩短到几分钟。

5.2 僵尸进程的无声累积:父进程不管,系统就替你养着

僵尸进程就是已经退出、但还没有被父进程回收的进程。少量几个没关系,但大量僵尸进程说明父进程有问题——可能没有正确调用wait(),或者父进程本身卡死了。最直接的影响是 PID 号被耗尽,新进程无法创建,系统表现为“什么命令都执行不了”。

用top或ps aux | awk '$8=="Z"'能数出僵尸进程数量。注意僵尸进程用kill -9杀不掉,因为进程已经死了,你只能杀它的父进程,让 init/systemd 接管并回收。在容器场景里,孤儿进程尤其容易堆积,建议在容器的主进程启动脚本里加上trap信号处理机制,或者直接用配置了reaper功能的容器运行时。

监控上加一个简单的计数告警:僵尸进程数量超过 20 或 50 就报警,根据机器规格调整。低于这个数一般不碍事,但持续增长就是另一个信号。

5.3 systemd 服务疯狂重启:你以为的守护,其实是加速消亡

systemd 的Restart=always本是好事,但它有个暗坑:如果服务启动即崩溃,systemd 会按策略反复拉起它,形成“每 3 秒启动一次、持续一整晚”的重启循环。这种循环有三个副作用:日志被大量重复启动信息淹没、依赖该服务的下游全部跟着抖动、CPU 在反复 fork/exec 中被白白烧掉。

排查手段:systemctl status xxx能直接显示Active: activating (auto-restart)和NRestarts计数,journalctl -u xxx里如果全是同一条启动报错刷屏,基本可以确认是启动崩溃循环。监控上建议给 NRestarts 设告警,比如一小时内重启次数超过 5 次就报警。

处理技巧也很实用:发现服务在疯狂重启时,先systemctl stop停掉它再排查日志,不然你每排查一步它都会重启一次,日志里只能看到一堆“刚启动又失败”的噪音。很多人在这一步栽过跟头。

6. 把暗坑变成例行巡检:一份能用起来的体检清单

前面讲了这么多暗坑,核心结论很简单:不要只监控“看起来重要”的指标,要监控“出问题时真正能救你”的指标。基于上面的坑,我整理了一份最小化巡检清单,可以直接做成脚本,配合 cron 每天跑一次或接入你的监控系统。

#!/bin/bash # 日常巡检:输出异常项 # 用法:直接执行,会自动列出可疑项 echo "==== 1. inode 使用率 ====" df -i | awk 'NR>1 && $5>85 {print $1, $5}' echo "==== 2. 磁盘空间(/ 和 /var/log) ====" df -h | awk 'NR>1 && $6=="/" && $5>80 {print $6, $5}' echo "==== 3. 全系统文件描述符 ====" used=$(cat /proc/sys/fs/file-nr | awk '{print $1}') max=$(cat /proc/sys/fs/file-nr | awk '{print $3}') echo "fd: $used / $max" echo "==== 4. conntrack 连接跟踪表 ====" count=$(sysctl net.netfilter.nf_conntrack_count 2>/dev/null | awk '{print $3}') maxct=$(sysctl net.netfilter.nf_conntrack_max 2>/dev/null | awk '{print $3}') [ -n "$count" ] && [ -n "$maxct" ] && echo "conntrack: $count / $maxct" echo "==== 5. 负载与 CPU 等待 ====" uptime vmstat 1 2 | tail -1 | awk '{print "r列:", $1, "cs列:", $12, "wa列:", $16}' echo "==== 6. 时钟偏移 ====" chronyc tracking 2>/dev/null | grep -E "System time|Leap status|Stratum" || echo "chronyd 未运行" echo "==== 7. 僵尸进程数 ====" ps -e -o stat,ppid,pid,comm | awk '$1=="Z"' | wc -l echo "==== 8. 内核错误/OOM 记录 ====" dmesg -T 2>/dev/null | grep -iE "oom|soft lockup|call trace|i/o error" | tail -5 || echo "无" echo "==== 9. 网卡错误计数 ====" ip -s link | grep -A 2 "RX:" | grep -E "errors|dropped" || echo "无" echo "==== 10. systemd 异常重启数 ====" systemctl --failed --no-legend | grep -v "^$" || echo "无失败服务"

这个脚本不追求华丽,目的就是让你每周或每天花 10 秒扫一眼,提前暴露那些暗坑。实际用的时候,我建议把“进程 fd 增长趋势”“CLOSE_WAIT 计数”“dmesg 错误级别事件”这三项接入到告警体系,因为它们最容易被忽略、后知后觉的代价也最大。

最后说点个人体会。干这行久了会发现,真正搞垮业务的往往不是高并发、内核优化之类听起来很高端的话题,而是这些“平时不用管、出事就要命”的角落。每次处理完一起事故,多问一句“当初有没有指标能提前拦住它”,然后把对应检查加到监控里,才是运维经验沉淀的正循环。监控系统装得再漂亮,如果连 inode、conntrack、dmesg、CLOSE_WAIT 这些都没有覆盖,那它顶多算一个花架子。把这些坑补上,你值班时的生活质量会明显上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询