告别Linux暗坑:监控面板看不到却会搞垮业务的指标
半夜三点你是被一条报警从床上拽起来的业务主机负载不高、内存也够磁盘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 文件里加LimitNOFILE65535再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.swappiness0当成性能优化但对某些场景反而有害。比如 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_reuse1确实有用但注意它只适用于发起连接的一方而且内核版本新一些的 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 $8Z能数出僵尸进程数量。注意僵尸进程用kill -9杀不掉因为进程已经死了你只能杀它的父进程让 init/systemd 接管并回收。在容器场景里孤儿进程尤其容易堆积建议在容器的主进程启动脚本里加上trap信号处理机制或者直接用配置了reaper功能的容器运行时。监控上加一个简单的计数告警僵尸进程数量超过 20 或 50 就报警根据机器规格调整。低于这个数一般不碍事但持续增长就是另一个信号。5.3 systemd 服务疯狂重启你以为的守护其实是加速消亡systemd 的Restartalways本是好事但它有个暗坑如果服务启动即崩溃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 NR1 $585 {print $1, $5} echo 2. 磁盘空间/ 和 /var/log df -h | awk NR1 $6/ $580 {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 $1Z | 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 这些都没有覆盖那它顶多算一个花架子。把这些坑补上你值班时的生活质量会明显上一个台阶。