1. 性能监控与排查的整体思路
服务器半夜报警,CPU 跑满 100%,登录上去敲 top,进程列表一片红,但你不知道从哪下手。这个场景我见了太多次,也是从“会敲命令”到“会做系统管理”的分水岭。这篇是 Linux 系统管理高级教程的第六篇,主题定在性能监控与调优实战。我不讲安装之类的基础操作,直接面对生产环境里最常见的 CPU、内存、磁盘 IO 和日志问题,给你一套可以复用的排查方法,而不是背一堆命令参数。
先说明一个观点:性能监控的核心不是命令,是流程。你现在就可以把 top、vmstat、free 倒背如流,但如果没有清晰的分析路径,遇到复杂故障照样一头雾水。这篇博客的编排就是一个流程:先看整体水位,再逐步深入到具体进程,最后靠日志锁定事件。后面每一章会沿这条主线展开,你在实际生产里遇到问题,也建议按这个顺序走,能省掉大量无效操作。
1.1 先定“基线”再谈“调优”
我非常反对一上来就翻内核参数。很多管理员看到负载一高,第一反应是去调 sysctl,或者干脆重启服务,这种做法等于不看心电图直接给病人开药。真正合理的顺序是先采集一段时间的“正常数据”,把系统日常运行的负载、内存占用、磁盘 IO 压在哪个量级搞清楚,然后以这个基线去对比异常时期的数据,差异处往往就是问题的入口。
比如我管理过的某台业务服务器,平时 CPU 使用率稳定在 30% 左右,某天突然冲到 95%。那不用多想,要么是业务流量发生变化,要么是某个进程出现死循环。反过来,如果这台服务器的 CPU 本来就长期跑在 90%,那偶尔跳到 95% 反而只是业务高峰的正常波动,不值得大惊小怪。没有基线,所有的“异常”都是在猜;有了基线,排查才有方向。
基线的获取不用复杂工具。最简单的方式是写一个轻量采集脚本,把关键指标定时落盘。我这里提供一个可用的模板,你可以根据自己的场景调整采样间隔:
#!/usr/bin/env bash # 每天按日期生成一个日志文件,每小时由 cron 调用一次 LOG_DIR=/var/log/sysmon/monthly mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/$(date +%F).log" { echo "===== snap: $(date '+%F %H:%M:%S') =====" uptime free -m iostat -x 1 2 2>/dev/null df -h } >> "$LOG_FILE" 2>&1配合 cron 里的0 * * * * /usr/local/bin/sysmon.sh,一个月后你就拥有了一份按小时采样的系统历史水位。别嫌这个办法土,很多高级工具崩溃或没部署的时候,恰恰是这种最笨的数据救了你。我后来能快速定位几次线上问题,全靠几个月前随手写的这个脚本。
1.2 工具选型:从整体水位到单进程定位
Linux 自带的工具链其实非常完整,绝大多数问题根本不需要装额外的商业监控软件。我的习惯是先做外部观测,再做内部观测。外部观测用 uptime、free、iostat、sar 这类工具看整体水位;内部观测用 pidstat、strace、perf 这类工具定位到具体进程和内核调用。
这里有个很关键的选型原则:工具要按“问题类型”选,而不是按“别人推荐”选。查 CPU 使用率排行用 top 就够了;查 CPU 上下文切换飙升要配合 vmstat 和 pidstat;查内存不够用,先拿 free 确认总量,再用 ps 按内存排序看单个进程;磁盘 IO 整体水位用 iostat,定位谁在读写则用 iotop。混用工具不只是浪费操作成本,更会把判断搞复杂,因为每个工具的视角和粒度都不同。
我自己的生产环境里,首选保留的是 sysstat 包里的 sar。它能把 CPU、内存、IO、网络的历史数据自动落盘,回看过去 7 天的数据非常方便。很多新同学不知道,sar本身只是一个读取工具,真正干活的是后台的sadc数据采集进程。平时它安安静静地积累数据,等故障发生后再去翻,比临时抱着 top 干瞪眼有效得多。
1.3 监控阈值速查,怎么用才不误报
很多人问“负载多少算高”,这个问题没有绝对答案,但在云主机和物理机混用的场景里,我的经验值是这样:CPU 使用率持续 15 分钟高于 85%,或者 load average 长期超过 CPU 核数的 4 倍,就已经值得关注。内存方面,可用内存持续低于总内存的 20%,而且 swap 一直在动,说明内存已经吃紧。磁盘方面,iostat 里的 %util 超过 80%,同时 await 超过 30ms,基本可以判断磁盘是瓶颈。
这些阈值不是金科玉律,不同硬件和业务模式会有差异,但作为第一道粗筛非常有效。我建议你把上面的数字写成一个监控小脚本,每 5 分钟检查一次,触发条件就发告警。注意设置告警时一定要加“持续时间”条件,比如连续 3 次采样都超标才触发,否则业务流量瞬时会制造大量无意义告警,把真正的风险淹没在噪音里。
2. CPU 与进程监控调优实战
2.1 load average 和 CPU 使用率不是一回事
遇到 CPU 飙高,很多人第一眼就看 top 里的 %CPU。这个动作没问题,但太局限。top 的 %CPU 只告诉你谁在消耗 CPU,没有告诉你系统为什么感觉卡。要回答“为什么”,得把 load average 和进程状态一起看。
load average 的三个数字(1 分钟、5 分钟、15 分钟)反映的是过去一段时间处于可运行状态和不可中断睡眠状态的进程平均数。这里最容易踩的坑,就是把 load 高直接等同于 CPU 忙。不可中断睡眠状态(也就是 D 状态)的进程通常是在等磁盘 IO 或者等内核锁,这时候 load 会高,但 CPU 可能很闲。我遇到过一台机器 load 飙到 20,用 mpstat 看 CPU 占用却不到 30%,最后发现是磁盘阵列有异常,大量写请求都卡在等待队列里。如果你只盯着 CPU%,这个方向就完全错了。
所以我在排查 load 异常时,第一件事永远是区分“CPU 密集”还是“IO 等待”。判断方法不复杂,看进程状态就行。用 top 按“D”状态过滤,如果发现一堆进程处在 D 状态,而 CPU 空闲率却不低,基本就是 IO 出问题;如果 D 状态很少,r 列排队很多,那就是纯粹的 CPU 争抢。这一步做对了,后面的排查才有意义。
2.2 vmstat 读数:r、b、cs 的判别价值
vmstat 是性能排查里最被低估的工具,输出简洁,信息密度却很高。我的固定用法是vmstat 1 5,每秒采样一次,连续采 5 次,重点看 r、b、cs、si、so 这几列。
r 列表示正在排队等待 CPU 的进程数,b 列表示处于不可中断睡眠状态的进程数。如果 r 很大,b 基本为 0,说明是 CPU 资源争抢;如果 b 经常非零,CPU 却有空闲,说明大概率是 IO 等待。这两列能帮你快速定位主方向。另外,us 和 sy 的比例也有参考价值:sy 占比长期超过 30%,说明系统调用或内核态处理负担过重,可能是锁竞争、频繁中断或系统调用太多。
上下文切换的异常更隐蔽。很多高性能应用性能突然下降,不是 CPU 不够,而是上下文切换太频繁。vmstat 里的 cs 列每秒几千次是正常范围,但如果压测时 cs 从 5000 跳到 5 万,同时 CPU 的 sy 占比升高,就要怀疑是不是线程数开太多、锁竞争严重,或者定时器太密集。此时用pidstat -w 1看哪个进程每秒的 cswch(主动切换)和 nvcswch(被迫切换)高,顺着那条线就能找到元凶。
2.3 进程优先级调整的正确姿势
调整进程优先级有两种常规手段:nice 命令在启动进程时设定,renice 在进程运行中修改。优先级范围从 -20 到 19,数字越小优先级越高。生产环境改优先级要格外小心,把核心进程调成 -5 以下,可能造成其他业务延迟,反而放大问题。
我一般在两类场景才会动 nice:一是批处理脚本和关键守护进程共存时,把批处理任务 renice 到 10 以上,让它别抢核心业务的 CPU;二是某个前端服务需要优先响应时,把它的进程优先级稍微调高,给关键线程留出 CPU。操作命令不复杂,renice -n 10 -p 12345就可以直接修改。需要注意的是,普通用户只能调高 nice 值(降低优先级),要调低必须用 root 权限,这也是系统层面的安全设计。
2.4 顺着 CPU 使用率追到具体调用链
单进程 CPU 高,很多人习惯直接 kill 重启。这是最省事但也是最危险的解法,因为根因还在,进程拉起来没多久又会复发。更稳妥的做法是:先确认进程身份,再判断是用户态忙还是内核态忙,最后定位到函数或线程级别。
如果进程用户态 CPU 高,可以先用pidstat -p PID 1确认趋势,再上perf top -p PID看热点函数。如果热点集中在一个明显的函数上,比如某些正则表达式处理、加解密算法或者序列化逻辑,就可以回查代码或配置。如果内核态 CPU 高,用perf record -g -p PID抓调用栈,重点看是不是频繁触发软中断、内存分配、文件系统锁。这些动作不复杂,但能把问题从“换台机器”升级成“修复根因”,这才是高级系统管理该有的水平。
3. 内存与交换空间诊断优化
3.1 看懂 free:别被 used 和 available 欺骗
内存排查是最容易被数据误导的领域。free -h一看 free 列只有几十兆,很多人就以为内存不足,实际上 Linux 的内存管理哲学是“内存不用白不用”,空闲内存会尽量用作 page cache。真正需要关注的指标是 available,它表示在不触发 swap 的情况下,还能给新进程分配多少内存。
以我管理过的一台 Web 服务器为例,free 命令显示 used 占了 96%,但 available 还有 30%。业务跑得很稳,swap 也很少动。这说明内存并没有真正紧张,只是大部分文件缓存没法被统计成“空闲”。如果照着 used 去扩容或者去清缓存,就是白白浪费成本。判断内存够不够,我只认两条:available 是否长期低于总内存的 20%,以及 swap 的 si/so 是否持续非零。
除了 free,还可以用ps aux --sort=-rss | head快速排出当前物理内存占用最高的进程。想更准确一点,就同时看/proc/meminfo里的 CommitLimit 和 Committed_AS,它们反映的是系统承诺给进程的虚拟内存总量。如果 Committed_AS 长时间超过 CommitLimit,说明过度分配情况比较严重,未来可能出现内存分配失败的隐患。
3.2 swappiness 要按场景调,不能照抄
关于 swappiness,这个参数控制内核回收匿名内存与文件缓存的倾向。文档默认值通常是 60,很多优化文章喜欢直接改成 0 或 10,理由是减少 swap 使用。但从我的经验看,无脑改低在某些场景反而有害。
如果一台机器大量运行 Java 这类堆内存应用,把 swappiness 调得过低,内核会更不愿意回收匿名页面。一旦出现突发内存需求,系统可能直接在分配时卡顿,甚至触发 OOM Killer。反过来,对于高并发读的缓存服务器,调低 swappiness 可以减少不必要的换页,确实有效。正确做法是结合 available 和 swap 变化趋势来综合判断,而不是照抄网上的参数。
修改命令是sysctl -w vm.swappiness=10,只对当前运行生效;要让重启后也生效,需要写进 /etc/sysctl.conf。任何内核参数修改前,一定先备份原值,并记录修改时间和原因。很多线上事故不是改错了,而是改完忘了为什么改,回滚时无从下手。
3.3 内存泄漏的定位思路:连续观察 RSS
内存泄漏定位也有不少误区。ps aux 里的 VIRT 列代表进程申请的虚拟地址空间,很多服务启动时就预留了很大的 VIRT(比如 JVM 的堆配置),这不代表真的占用了那么多物理内存。RSS 更接近物理内存占用,但它包含共享库的内存,多个进程会重复计算。
要精确判断泄漏,更可靠的手段是连续采集 RSS。如果某个进程的 RSS 在业务量不变的情况下稳定增长,比如每小时涨 500MB,连续一天不回落,基本可以判断是泄漏。采集方法可以直接用 cron 把/proc/PID/status里的 VmRSS 记下来,也可以用ps -o pid,rss,vsz,cmd -p PID定时输出。
定位到进程之后,要区分是堆内存泄漏还是线程栈、本地内存缓存失控。Java 应用可以看堆内外的使用曲线,C/C++ 应用则可以用 valgrind 或 jemalloc 的 profiling 功能辅助。这个过程确实比较耗时,但比反复重启进程假装问题不存在,要专业得多。
3.4 缓存回收时机与 cgroup 内存兜底
有人喜欢用echo 3 > /proc/sys/vm/drop_caches清缓存来“释放”内存,我劝你不要把这个命令当成常规武器。它确实会清掉 page cache,但随后所有文件读取都要重新落盘,磁盘 IO 会短暂飙高,业务可能反而变慢。这个命令只在执行完大文件操作、想把内存腾给下一个大任务时偶尔使用。
正常情况下,Linux 自己的 writeback 机制会根据脏页比例和回收压力去处理缓存,根本不需要人工干预。你真正该做的,是为关键服务设置内存上限兜底。用 systemd 管理服务时,可以给单元文件添加MemoryMax=或通过systemctl set-property 服务名 MemoryMax=2G来限制。这样即使业务模块出现内存失控,也只是这个单元被限制,而不会把整台服务器拖进 OOM 危机。
4. 磁盘 I/O 与文件系统性能实践
4.1 iostat 关键指标与 %util 的局限
磁盘性能优化的第一步同样是读数。iostat -x 1输出里的 %util 被很多人当成磁盘忙不忙的唯一指标,但它的含义是设备在采样周期内有请求未完成的时间占比。对单块机械盘,%util 到 100% 基本就是满负荷;对 SSD 或做了阵列的存储,%util 很容易虚高,因为硬件支持并行处理多个请求,100% 的 %util 并不一定意味着达到性能极限。
更值得关注的是 await,它表示平均每次 IO 请求从提交到完成的时间。机械盘正常在 5-15ms,SSD 在 1ms 以内。如果 await 明显走高,svctm 却变化不大,说明请求在队列里排队,问题多半出在并发压力或调度上。另一个指标 r_await 和 w_await 最好分开看,因为很多场景下读延迟和写延迟差异巨大,混在一起会掩盖真实问题。
4.2 I/O 调度器和挂载参数真的需要改吗
I/O 调度器的选型,在传统机械盘和 NVMe 固态盘上结论不同。机械盘上,deadline 调度器能有效减少大文件的长时间饥饿,适合数据库负载;NVMe 固态盘本身随机 IO 能力强大,用 none 反而能减少调度层开销。不过现代内核已经默认使用多队列调度,除非明确知道自己的负载特征,否则没必要频繁切换。
挂载参数里性价比最高的优化是 noatime。默认的 atime 每次读取文件都要更新访问时间,等于每次读操作伴随一次写操作。对高并发读的服务器,改用 noatime 或 relatime 能立刻降掉一批写放大。现代文件系统多数发行版默认是 relatime,已经平衡了访问时间和性能;如果你的业务没有严格审计需求,可以进一步改为 noatime。
可能有人问,文件系统选 ext4 还是 xfs。这个问题没有标准答案,但我的习惯是:大文件、海量小文件混合的场景选 xfs,它处理大文件和高并发写入更稳;老牌兼容性更强的场景选 ext4,遇到问题资料多,运维也不慌。不要盲目追求“新版更好”,稳定性和运维熟悉度更重要。
4.3 iotop、lsof 定位热点文件的组合拳
定位具体热点进程,iotop 是最直观的。它能按实时 IO 读写量排序,一眼看到谁在大量读写。比如数据库高负载时,你经常会看到 mysqld 排在前面,问题往往不是它本身,而是某个慢查询或者备份任务在背后触发海量扫描。
想进一步知道进程在操作哪个文件,要结合打开的句柄目录来看。ls -l /proc/PID/fd会列出该进程打开的所有文件描述符,配合lsof -p PID能更清晰地看到文件路径。之前我排查一次日志磁盘写满,发现是某个服务的 debug 日志把整个盘打爆,原因就是日志框架级别配置错误,同一个日志文件被循环写出巨大体量。这种问题调度器救不了,只能从配置层面纠正。
4.4 备份对 IO 的冲击:一个案例复盘
处理过一个典型场景:数据库备份任务一跑,整台机器就卡到 SSH 都连不上。用 iostat 看,%util 在 95% 以上,await 从正常的 5ms 涨到 200ms,同时 iotop 显示有一个备份子进程在大量读数据。问题不在 CPU,也不在内存,就是备份脚本的并发度太高,把磁盘 IO 队列打满了。
解决办法并不复杂:用 ionice 把备份命令的 IO 优先级调低,同时把备份拆成按表分批执行。核心命令是ionice -c 3 /usr/local/bin/backup.sh,其中-c 3表示 idle 级别,只有当系统空闲时才会执行 IO。这一个改动做下去,备份照跑,业务恢复平稳。这提醒我,影响系统性能的往往不是某个工具本身,而是使用工具的时机和参数。
5. 日志审计与故障定位实战
5.1 journalctl 的组合过滤参数
系统日志是排查事故的最后一道防线。很多故障不会当场复现,但一定会在日志里留痕迹。journalctl 是 systemd 系发行版最常用的日志查询工具,它的价值不止是查看实时输出,更在于支持组合过滤。
我查问题的固定套路是:先用手表定位时间窗口。比如事故发生在 22:15 到 22:20,就用journalctl --since "22:15" --until "22:20" -p err把这条时间线里的错误日志全部拉出来。服务维度用-u指定单元名,比如journalctl -u 应用服务名 --since today -p warning。想看内核消息就用-k。熟练组合这些参数后,查日志的效率能提升一个量级。
5.2 从系统日志反推异常事件
日志里最容易忽略的线索是 OOM 记录。当内核内存不足时,会触发 OOM Killer 杀掉进程,对应的记录会出现在系统日志的内核消息里,提示哪类进程被杀了、内存占用多少。很多人看到服务进程突然消失,第一反应是查服务自身的错误日志,结果发现根本没有异常输出,白白浪费半天。这时候先查系统日志里的 oom 关键字,往往一眼就能定位。
另外,日志时间线的拼图也很重要。一个看似无关的定时任务,可能在某个时刻同时触发 CPU 密集、内存暴增和磁盘读写放大。用 journalctl 拉出同一时间窗内所有消息,按系统日志、内核日志、应用日志三个维度去对齐,经常能发现一根隐藏的因果链。这也是为什么我一直强调,事故处理不要只盯服务日志,要全局地看。
5.3 logrotate 的高频坑与配置要点
logrotate 配置里有两个高频坑。第一个是轮转周期和业务日志产生速度不匹配。有些服务一小时能写几百 MB 日志,按 daily 轮转根本顶不住,需要改成按 size 切。配置项很简单,比如size 100M,日志文件到 100MB 就轮转。
第二个坑是配置了 create 但服务还是收不到新日志。原因通常是服务进程一直持有旧文件句柄,轮转后没有重新打开日志文件。解决办法是配 postrotate 指令,通知进程重新加载配置或重开文件句柄。比如很多应用服务支持发送信号后重新打开日志,就可以在 postrotate 里写kill -HUP $(pidof 应用名)。还有一个容易忽略的点是延迟压缩,建议把 compress 改成 delaycompress,避免压缩大文件时把 IO 打满。
5.4 多机日志汇聚的最小方案
如果管理的机器数量比较多,本地 journalctl 显然不够用。我的经验是先别急着上重量级采集平台,自建一个最小方案就能解决大部分问题。用 rsyslog 把关键服务日志转发到一台集中日志机,按主机名和服务名分目录存储,配合简单的归档脚本就够日常排查了。
rsyslog 的配置不难:客户端在 /etc/rsyslog.d/ 下新建转发规则,把设施和日志级别转发到日志服务器的指定端口;服务器端开启接收模块,并按%HOSTNAME%/%programname%做目录模板。这套方案一天就能搭完,能解决 90% 的“日志散落在几十台机器上”的问题。等团队规模真正变大、检索需求变复杂,再评估更重的方案也不迟。
6. 常见误区与排查速查表
6.1 五个最常见的错误判断
这套监控方法用了这么多年,我见过太多同行在排查路上栽进同一个坑。第一个典型误区是:看见响应变慢就重启进程,却不先查是整体负载高还是单进程异常,重启之后问题复现,才发现什么都没解决。第二个是:看见 swap 有占用就急着加内存,却不先确认是不是空的 page cache 可以被回收。第三个:看到 sar 报告里 CPU 队列长,就调内核参数,却不先确认是不是应用层线程数配得太多。
第四个误区是:只看进程本身的“健康状态”,不看系统级资源水位。应用日志一切正常,不代表底层没有资源争抢。第五个误区是:把所有异常都归结为“流量突增”,不做数据对比。没有基线的支撑,流量突增只是句正确的废话。其实很多问题的根源只是配置不合理或者单应用算法问题,内核默认参数远比你想象的更可靠。
6.2 可复用的性能排查命令清单
最后把我常用的一套排查命令整理成速查表,平时记不得就翻一下:
| 目标 | 命令 | 关注点 |
|---|---|---|
| 整体负载 | uptime | load 1/5/15 分钟趋势 |
| 资源队列 | vmstat 1 5 | r、b、cs、si/so |
| 单核分布 | mpstat -P ALL 1 | 是否单个核被打满 |
| 进程切换 | pidstat -w 1 | cswch、nvcswch |
| 内存水位 | free -h | available,不是 free |
| 历史水位 | sar -r -d | 过去几天的内存和磁盘 |
| 磁盘 IO | iostat -x 1 | %util、await、w_await |
| 实时 IO 进程 | iotop | 按读写量排序 |
| 进程打开文件 | lsof -p PID | 定位热点文件 |
| 时间窗日志 | journalctl --since ... --until ... -p err | 按时间过滤错误 |
| 内核 OOM | `journalctl -k | grep -i oom` |
最后留一个个人习惯:每次处理完一起性能问题,我都把现象、用过的命令、最终根因写进一个本地笔记,一周后再回看。很多看似复杂的问题其实有相似的纹理,记录多了,下次再报警你会发现自己下意识已经开始分类。系统管理这门手艺,就是从无数个这样的复盘里长出来的。