Ubuntu 系统系列 | 第 11 天(重访·第二季):进程管理与监控——性能分析与调优

第 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 天回顾总结——第二季完结篇
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容