第 11/30 天(重访·第二季)
引言
在第一季的第 11 天,我们已经学会了用 ps 查看进程快照、用 top/htop 实时观察 CPU 和内存、用 journalctl 检索服务日志。但真实的生产环境远比「看看谁在吃资源」复杂得多——服务器负载高但 CPU 却空闲,问题出在哪?哪个进程、哪一行代码吃掉了 CPU?内存被 OOM Killer 误杀如何定位?故障发生前,如何提前收到告警?
本篇文章是第二季的重访,我们将从「会用监控命令」跃升到「读懂系统性能模型」:深入理解 load average 的真实含义、掌握 pidstat/perf/strace 等性能剖析工具链、透过 cgroup v2 理解 systemd 的资源控制、配置生产级 journald 日志、解读 OOM Killer 的裁决逻辑,最后搭建一套从指标采集到告警触达的生产级监控体系。如果你已经会看进程列表,本文会让你真正「听懂」系统的呼吸。
一、系统负载模型:load average 背后的真相
1.1 负载不等于 CPU 使用率
很多新手把 load average 当成 CPU 占用率,这是最常见的误解。负载(load)是「处于可运行状态(R)+ 不可中断睡眠状态(D)的进程平均数」,它衡量的是任务队列的拥挤程度,而非 CPU 忙不忙。
$ uptime
14:22:31 up 87 days, 4:12, 2 users, load average: 3.15, 2.80, 2.41
$ nproc # 查看 CPU 核心数
4
$ cat /proc/loadavg # 原始数据:1/5/15 分钟 + 当前可运行/总进程 + 最近 PID
3.15 2.80 2.41 4/823 451209
解读原则:
| 场景 | 含义 |
|---|---|
| load < 核心数 | 任务队列有空闲,系统健康 |
| load ≈ 核心数 | 队列饱和,资源刚好用完 |
| load > 核心数 × 0.7 | 需要关注,任务开始排队 |
| load >> 核心数 | 系统过载,响应会明显变慢 |
关键:负载高 ≠ CPU 忙。当大量进程处于 D 状态(不可中断睡眠,通常是等待磁盘/网络 IO) 时,负载会飙升但 CPU 利用率很低——这是典型的「IO 瓶颈」信号。
1.2 用 vmstat 定位负载来源
# vmstat 每秒采样 5 次,重点关注 r、b、wa 三列
$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 2 0 128456 102344 4192832 0 0 1245 3402 1284 3512 12 8 48 32 0
4 3 0 127890 102400 4193001 0 0 9867 1204 1321 3567 6 4 41 49 0
- r:可运行队列长度(R 状态进程数)——负载高的主因之一
- b:阻塞在 IO 上的进程数(D 状态)——b > 0 且 wa 高,说明磁盘是瓶颈
- wa:CPU 等待 IO 完成的时间占比——
wa > 30%基本可以断定 IO 吃紧
# 找出所有处于 D 状态的进程(IO 卡死元凶)
$ ps -eo state,pid,ppid,comm,wchan:24 | awk '$1 == "D"'
D 1234 1 mysqld io_schedule
D 2456 1234 kworker/u8:1 -
# 结合 iostat 确认是哪块磁盘在忙
$ iostat -x 1 2 | grep -E 'Device|vda|sda'
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await ...
vda 0.00 124.50 0.00 58624.00 0.00 12.50 0.00 10.04 0.00 99.44 ...
定位口诀:r 高看 CPU 占用,b/wa 高查磁盘 IO,si/so 持续非零则内存不足在疯狂换页。负载问题的根源,90% 是这三个之一。
二、性能剖析工具箱:从 pidstat 到 perf
top 告诉我们「谁最忙」,但想知道「为什么忙」,需要 sysstat 家族和内核级剖析工具。
2.1 sysstat:历史回溯的利器
# 安装 sysstat(提供 pidstat、mpstat、iostat、sar)
sudo apt install sysstat -y
sudo systemctl enable --now sysstat # 开启 sadc 后台采集,保留历史性能数据
pidstat——按进程维度的精确统计:
# 每秒输出 PID 1234 的 CPU 使用率,共 10 次
$ pidstat -u -p 1234 1 10
Linux 6.8.0-31-generic (web01) 2026-08-30 _x86_64_ (4 CPU)
14:30:05 UID PID %usr %system %guest %wait %CPU CPU Command
14:30:06 1000 1234 0.00 0.00 0.00 98.20 0.00 2 mysqld
注意 %wait 列——99% 的等待时间意味着该进程根本没抢到 CPU,是调度延迟问题而不是进程自身忙。再配合内存与磁盘维度:
pidstat -r -p 1234 1 5 # 内存:minflt/s(缺页)、majflt/s(主缺页)、VSZ、RSS
pidstat -d -p 1234 1 5 # 磁盘:每秒读写 KB、IO 等待时间
pidstat -T CHILD -p 1234 1 # 连同子进程一起统计(排查 fork 沉重的守护进程)
sar——回顾「刚才发生了什么」: 出故障时没人恰好盯着终端,好在 sysstat 每 10 分钟自动落盘一次:
sar -u -f /var/log/sysstat/sa30 # 查看今天每个采样点的 CPU
sar -r # 内存与交换分区历史
sar -q # 负载与队列长度历史
sar -n DEV 1 3 # 实时网卡流量
2.2 perf:内核级性能剖析
当 pidstat 显示某个进程 CPU 占用 100%,就该用 perf 看它到底在执行什么函数:
# 安装并授权(perf 需要内核性能事件权限)
sudo apt install linux-tools-common linux-tools-generic -y
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid # 或写入 /etc/sysctl.d/
# 实时火焰图式观察:哪个函数占用最高
sudo perf top -p 1234
# 采样 30 秒并生成报告
sudo perf record -g -p 1234 -- sleep 30
sudo perf report # 交互式查看调用链
sudo perf annotate # 逐行注释汇编,定位热点代码行
真实场景:某 Java 应用 CPU 飙到 800%,perf top 显示热点在 __do_page_fault + memcpy_erms——结合 pidstat -r 看到的 majflt/s 暴涨,定位到是 GC 后在反复拷贝大对象导致大量缺页,最终通过调整堆大小解决。**perf 的价值是把「猜」变成「看」。
2.3 strace:追踪系统调用
进程「卡住不动」时,用 strace 看它阻塞在哪个系统调用:
# 跟踪 PID 1234 的所有系统调用(-f 含子线程,-T 显示耗时)
sudo strace -f -T -p 1234 -o /tmp/trace.log
# 只看网络相关调用
sudo strace -f -e trace=network -p 1234
# 统计各系统调用次数与耗时(-c 汇总模式,无需 attach 即可统计运行 5 秒)
sudo strace -c -f timeout 5 ./my-slow-app
- 卡在
read()→ 等待数据/IO - 卡在
futex()→ 等待锁,排查并发问题 - 卡在
connect()→ 网络对端不响应 - 高频
epoll_wait→ 事件循环正常空闲,可放心
注意:strace 会显著拖慢被跟踪进程(10 倍以上),生产环境 attach 不要超过几秒;高并发进程优先用
perf而非 strace。
三、systemd 视角的进程管理:cgroup v2 与资源控制
Ubuntu 22.04+ 全面采用 cgroup v2 统一层级,所有用户态进程都活在 systemd 的 slice/scope 树里。理解这棵树,等于拿到了资源管理的上帝视角。
3.1 用 systemd-cgtop 看资源账单
$ systemd-cgtop
Control Group Tasks %CPU Memory Input/s Output/s
user.slice - 0.2 1.2G - -
system.slice - 1.8 3.4G - -
├─ mysqld.service - 0.1 802.4M - -
└─ nginx.service - 0.3 24.1M - -
比 top 更强的是:它按 systemd 资源账本 统计——即使进程 fork 出几十个子进程,内存、CPU 都汇聚到同一个 cgroup,不会漏算。
3.2 直接读取 cgroup v2 控制文件
# 找到服务的 cgroup 路径
$ systemctl show mysqld -p ControlGroup
ControlGroup=/system.slice/mysqld.service
# 查看内存与 CPU 使用量(cgroup v2 按字节计,CPU 以纳秒计)
$ cat /sys/fs/cgroup/system.slice/mysqld.service/memory.current
841318400
$ cat /sys/fs/cgroup/system.slice/mysqld.service/cpu.stat
usage_usec 15230188391
user_usec 13790044621
system_usec 1440143770
3.3 用 systemd 指令限制资源(不需要写 cgroup 文件)
# /etc/systemd/system/mysqld.service.d/limit.conf
[Service]
# CPU 配额:最多使用 1.5 个核心(150%)
CPUQuota=150%
# 内存硬上限:超过即触发 OOM kill 该服务(保护整机)
MemoryMax=2G
# 内存软上限:超过后内核积极回收该 cgroup 的页缓存
MemoryHigh=1.5G
# 限制可打开文件数与进程数
LimitNOFILE=65535
TasksMax=512
sudo systemctl daemon-reload && sudo systemctl restart mysqld
# 验证限制已生效
systemctl show mysqld -p MemoryMax -p CPUQuota
关键:MemoryMax 触发的是该 cgroup 内进程被 kill,而不是系统级 OOM Killer 随机杀进程——用资源限制把「未知的灾难」变成「可预期的降级」。配合
Restart=on-failure,被限制杀掉后服务还能自动拉起。
四、日志分析升级:journald 生产配置
第一季我们学会了 journalctl -u nginx 的基本检索。生产环境里,journald 的配置和高级检索同样重要。
4.1 开启持久化存储
默认 journald 把日志存在内存 tmpfs 中,重启即丢!
# /etc/systemd/journald.conf
[Journal]
Storage=persistent # 落盘到 /var/log/journal
Compress=yes # 压缩历史日志
SystemMaxUse=2G # 日志总大小上限,防止撑爆磁盘(默认可达磁盘 10%!)
SystemMaxFileSize=128M
MaxRetentionSec=30day # 最多保留 30 天
sudo systemctl restart systemd-journald
journalctl --disk-usage # 查看当前日志占用,应为 ~0 或很小
sudo journalctl --vacuum-size=500M # 立即压缩瘦身到 500M
踩坑提醒:日志目录被撑爆是生产事故高发区。曾有客户
/var独立分区 20G 被 journald 默认策略写满,导致数据库无法落盘。设置 SystemMaxUse 是部署新机器第一步。
4.2 高级检索技巧
# 只看某个服务的错误级日志(最近 1 小时)
journalctl -u nginx --since "1 hour ago" -p err
# 按可执行文件路径过滤
journalctl _COMM=mysqld --since today
# 按 PID 过滤(排障时锁定单次故障现场)
journalctl _PID=1234
# JSON 输出,便于脚本解析
journalctl -u nginx -o json --since "10 min ago"
# 跟踪模式 + 高亮关键词
journalctl -f -u mysqld | grep -i --color error
# 查看某服务的启动时间线
journalctl -u nginx -o short-precise --no-pager
配合 journalctl -p err --since today | grep -c . 可以写进监控脚本:错误量骤增即告警,比单纯看 CPU/内存更能反映服务真实健康度。
五、进程异常与 OOM:内核如何裁决内存
服务器内存被耗尽时,内核会启动 OOM Killer 挑选「最该杀」的进程。理解它的裁决逻辑,才能避免数据库被误杀。
5.1 裁决依据:oom_score
每个进程都有一个 oom_score(0-1000),分数越高越容易被杀,主要由三要素决定:内存占用(RSS)、进程存活时间(越老越不容易被杀)、oom_score_adj 调权值。
# 查看各进程 OOM 分数,按从高到低排列
$ for pid in $(ls /proc | grep -E '^[0-9]+$'); do
[ -r /proc/$pid/oom_score ] && echo "$(cat /proc/$pid/oom_score) $pid $(cat /proc/$pid/comm)"
done | sort -rn | head -10
998 2345 mysqld
5.2 保护关键进程
[Service]
# systemd 内置调权:-1000 表示永不被 OOM Killer 选中(等价 oom_score_adj=-1000)
OOMScoreAdjust=-1000
# 命令行方式(对非 systemd 管理的进程)
echo -1000 | sudo tee /proc/2345/oom_score_adj
5.3 解读 OOM 现场
# 内存耗尽瞬间,内核会在 dmesg 留下完整现场
$ sudo dmesg -T | grep -i -A5 "out of memory"
[Fri Aug 30 03:12:44 2026] mysqld invoked oom-killer: gfp_mask=0x14000c0, order=0, oom_score_adj=0
[Fri Aug 30 03:12:44 2026] [ pid ] uid tgid total_vm rss pgtables_bytes oom_score_adj name
[Fri Aug 30 03:12:44 2026] [ 2345] 112 2345 621148 210240 1257472 -1000 mysqld
[Fri Aug 30 03:12:44 2026] [ 5123] 1000 5123 420088 332108 1330572 0 java
[Fri Aug 30 03:12:44 2026] Out of memory: Killed process 5123 (java) total-vm:420088kB, anon-rss:332108kB
# systemd 视角:被 OOM 杀死的服务会记录失败原因
$ systemctl status mysqld | grep -i "oom|killed"
从现场可知:oom_score_adj=-1000 的 mysqld 被保护,而 rss 高达 332MB 的 java 进程被选中。排查结论通常是:某个应用的缓存/连接池无上限膨胀,彻底修复需要给该服务加 MemoryMax 限额,而不是只调 OOM 分数。
六、生产级监控告警:从指标采集到告警触达
第二季第 20 天会讲 Prometheus + Grafana 的完整部署,这里我们聚焦「进程与性能」维度的告警设计——这些规则在任何监控体系下都通用。
6.1 轻量方案:30 行脚本实现第一道防线
#!/bin/bash
# /usr/local/bin/host-health-check.sh —— 每 5 分钟跑一次(systemd timer)
# 失败 3 次后通知钉钉/企业微信/邮件(示例为钉钉 webhook)
send_alert() {
# 钉钉机器人 Webhook:在钉钉群中添加自定义机器人后获取完整地址,
# 将其中的 token 部分写入环境变量 DINGDING_TOKEN(示例:export DINGDING_TOKEN='xxxx')
local hook_url="https://oapi.dingtalk.com/robot/send?access_token=${DINGDING_TOKEN}"
[ -z "$DINGDING_TOKEN" ] && { echo "未设置 DINGDING_TOKEN,跳过告警"; return; }
curl -s -H 'Content-Type: application/json'
-d "{"msgtype":"text","text":{"content":"[ALERT] $1"}}" "$hook_url"
}
load=$(awk '{print $1}' /proc/loadavg)
cores=$(nproc)
# 负载超核心数 3 倍持续触发
if awk "BEGIN{exit !($load > $cores * 3)}"; then
send_alert "负载异常: $load / $cores 核 (时间: $(date '+%F %T'))"
fi
# 磁盘用满预警
disk=$(df / | awk 'NR==2{print $5}' | tr -d '%')
[ "$disk" -gt 90 ] && send_alert "根分区使用率 ${disk}%"
# D 状态进程堆积(IO 卡死信号)
d_count=$(ps -eo state | grep -c '^D')
[ "$d_count" -gt 5 ] && send_alert "D 状态进程堆积: $d_count 个"
6.2 标准方案:Node Exporter + Prometheus 告警规则
进程与性能维度最值得盯的四条 Prometheus 规则:
groups:
- name: host-basics
rules:
# 负载/核心数 > 3,持续 10 分钟
- alert: HighLoadAverage
expr: node_load1 / count(node_cpu_seconds_total{mode="idle"}) > 3
for: 10m
annotations: { summary: "负载过高", description: "{{ $labels.instance }} load1 = {{ $value }}" }
# 内存可用不足 10%
- alert: HostOutOfMemory
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9
for: 5m
# 磁盘即将写满
- alert: DiskAlmostFull
expr: node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} < 0.1
for: 15m
# 服务进程消失(如 mysqld 挂掉)
- alert: ProcessDown
expr: node_systemd_unit_state{name="mysqld.service",state="active"} != 1
for: 2m
# 用 systemd unit 状态直接做进程告警,比 pid 探针可靠
curl -s http://localhost:9100/metrics | grep systemd_unit_state | grep mysql
# node_systemd_unit_state{name="mysqld.service",state="active"} 1
告警设计三原则:① 只告警需要人介入的问题(负载波动不要打扰人);② 每条告警必须有明确的排查入口(附上 dmesg 命令、日志路径);③ 告警要分级——负载 >3 是 warning,>5 配合 D 状态堆积才是 critical。
总结与下期预告
进程管理是系统运维的「听诊器」:本文从读懂 load average 的真实含义出发,让你明白负载高不等于 CPU 忙,r/b/wa 三个指标能快速定位瓶颈维度;随后掌握 pidstat/perf/strace 逐层下钻的工具链,把「哪个进程忙」升级为「哪里为什么忙」;透过 cgroup v2 和 systemd 资源限制,把失控风险关进笼子;journald 持久化与高级检索让日志真正可用;最后从 OOM 裁决逻辑到生产告警规则,构建「故障发生前就被发现」的防线。记住三个立即可用的要点:部署新机器先设 SystemMaxUse、%wait 高是调度问题不是进程问题、关键服务 OOMScoreAdjust=-1000 并加 MemoryMax 兜底。
下期预告:第 12 天(重访·第二季)将带来《计划任务与自动化——进阶自动化》,深入 cron 与 systemd timer 的进阶用法、Ansible 自动化运维实战与 CI/CD 定时流水线。敬请期待!
系列目录
| 天数 | 主题 | 状态 |
|---|---|---|
| 第 1 天 | Ubuntu 简介与版本选择 | ✅ |
| 第 2 天 | 手把手安装 Ubuntu | ✅ |
| 第 3 天 | Ubuntu 桌面环境初探 | ✅ |
| 第 4 天 | Ubuntu 终端基础 | ✅ |
| 第 5 天 | 用户与权限管理 | ✅ |
| 第 6 天 | 软件包管理 | ✅ |
| 第 7 天 | 文件与文本操作 | ✅ |
| 第 8 天 | 系统服务管理 | ✅ |
| 第 9 天 | 磁盘与文件系统管理 | ✅ |
| 第 10 天 | 网络配置与管理 | ✅ |
| 第 11 天 | 进程管理与监控 | ✅ |
| 第 12 天 | 计划任务与自动化 | ✅ |
| 第 13 天 | 备份与恢复策略 | ✅ |
| 第 14 天 | 系统更新与升级管理 | ✅ |
| 第 15 天 | SSH 远程管理与安全加固 | ✅ |
| 第 16 天 | Web 服务器搭建 | ✅ |
| 第 17 天 | 数据库服务器部署 | ✅ |
| 第 18 天 | Docker 容器化管理 | ✅ |
| 第 19 天 | 文件共享服务 | ✅ |
| 第 20 天 | 监控与告警系统 | ✅ |
| 第 21 天 | 邮件服务器基础 | ✅ |
| 第 22 天 | 系统安全加固 | ✅ |
| 第 23 天 | 性能调优与内核参数 | ✅ |
| 第 24 天 | 虚拟化技术 | ✅ |
| 第 25 天 | 容器编排入门 | ✅ |
| 第 26 天 | 高可用与负载均衡 | ✅ |
| 第 27 天 | Ubuntu 自动部署 | ✅ |
| 第 28 天 | 故障排查实战 | ✅ |
| 第 29 天 | Ubuntu 社区与文档 | ✅ |
| 第 30 天 | 30 天回顾总结 | ✅ |
| 第 23 天(重访) | 性能调优与内核参数——生产环境基准测试与踩坑实战 | ✅ |
| 第 24 天(重访) | KVM 虚拟化高级实战——生产级配置与性能调优 | ✅ |
| 第 25 天(重访) | 容器编排进阶——Docker Compose 生产实践与 Kubernetes 单节点集群实战 | ✅ |
| 第 26 天(重访) | 高可用与负载均衡生产实战——Keepalived + HAProxy 深度调优与故障排查 | ✅ |
| 第 27 天(重访) | 自动部署生产实践——PXE + Preseed + Cloud-init 深度进阶与企业级流水线 | ✅ |
| 第 28 天(重访) | 故障排查实战——深度内核调试、文件系统救援与生产级排障 | ✅ |
| 第 29 天(重访) | Ubuntu 社区与文档——从使用者到贡献者深度实践 | ✅ |
| 第 30 天(重访) | 30 天系列完结篇——学习路线图与成长行动指南 | ✅ |
| 第 1 天(重访·第二季) | Ubuntu 新篇章——2026 年生态全景与版本选择 | ✅ |
| 第 2 天(重访·第二季) | 手把手安装 Ubuntu——2026 年全新安装体验与自动化部署 | ✅ |
| 第 3 天(重访·第二季) | Ubuntu 桌面环境深度探索——GNOME 46 新特性与个性化定制 | ✅ |
| 第 4 天(重访·第二季) | Ubuntu 终端与 Shell 高级技巧——现代 CLI 工具链 | ✅ |
| 第 5 天(重访·第二季) | 用户与权限管理——从 ACL 到 PAM 企业级权限体系 | ✅ |
| 第 6 天(重访·第二季) | 软件包管理——apt、dpkg、snap、flatpak 深入对比 | ✅ |
| 第 7 天(重访·第二季) | 文件与文本操作——现代化 CLI 工具链 | ✅ |
| 第 8 天(重访·第二季) | 系统服务管理——systemd 深度剖析与企业级服务治理 | ✅ |
| 第 9 天(重访·第二季) | 磁盘与文件系统管理——LVM 与存储进阶 | ✅ |
| 第 10 天(重访·第二季) | 网络配置与管理——netplan 高级网络、策略路由与生产级防火墙 | ✅ |
| 第 11 天(重访·第二季) | 进程管理与监控——性能分析与调优 | 🟢 今日 |
| 第 12 天(重访·第二季) | 计划任务与自动化——进阶自动化 | ⏳ |
| 第 13 天(重访·第二季) | 备份与恢复策略——企业级备份方案 | ⏳ |
| 第 14 天(重访·第二季) | 系统更新与升级管理——生产环境升级策略 | ⏳ |
| 第 15 天(重访·第二季) | SSH 远程管理——安全加固进阶 | ⏳ |
| 第 16 天(重访·第二季) | Web 服务器搭建——高性能 Nginx 调优 | ⏳ |
| 第 17 天(重访·第二季) | 数据库服务器部署——生产级数据库运维 | ⏳ |
| 第 18 天(重访·第二季) | Docker 容器化——生产级容器管理 | ⏳ |
| 第 19 天(重访·第二季) | 文件共享服务——高性能存储方案 | ⏳ |
| 第 20 天(重访·第二季) | 监控与告警系统——可观测性平台 | ⏳ |
| 第 21 天(重访·第二季) | 邮件服务器基础——企业邮件方案 | ⏳ |
| 第 22 天(重访·第二季) | 系统安全加固——深度安全策略 | ⏳ |
| 第 23 天(重访·第二季) | 性能调优与内核参数——生产环境优化 | ⏳ |
| 第 24 天(重访·第二季) | 虚拟化技术——KVM 高级虚拟化 | ⏳ |
| 第 25 天(重访·第二季) | 容器编排入门——Docker Compose 与 K8s | ⏳ |
| 第 26 天(重访·第二季) | 高可用与负载均衡——集群方案 | ⏳ |
| 第 27 天(重访·第二季) | Ubuntu 自动部署——自动化运维 | ⏳ |
| 第 28 天(重访·第二季) | 故障排查实战——深度排障 | ⏳ |
| 第 29 天(重访·第二季) | Ubuntu 社区与文档——开源贡献 | ⏳ |
| 第 30 天(重访·第二季) | 30 天回顾总结——第二季完结篇 | ⏳ |















暂无评论内容