暗色模式

Linux PSI 资源压力指标:从 /proc/pressure 到 cgroup v2 压力定位

技术教程
2026-09-29
4
0
本文要点
  • PSI 回答的是「有多少时间在等」,而不是「有多忙」。它把 CPU、内存、IO 三个资源的停顿时间做成百分比,直接可读、可直接告警
  • 每个资源的文件里有两行:some 是「至少有一个任务在等」,full 是「所有非空闲任务都在等」。判断「整机是不是卡住了」看 full,判断「有没有人受影响」看 some
  • ⚠️ CPU 的 full 几乎恒为 0。实测 1 核上跑 4 个忙循环:some 的累计停顿从 44478637 涨到 49553195(+5.07 秒),而同一窗口里 full 只从 6127945 涨到 6128995(+1.05 毫秒),相差约 4800 倍——因为只要还有任何一个任务在跑,full 就不计
  • total 的单位是微秒,自开机起单调累加。两个时刻相减,就是这段窗口里「一共停顿了多少秒」
  • ⚠️ 三列 avg 是指数滑动平均,不是窗口内的精确平均。实测一台已经空闲十几分钟的机器,memory 的 avg10=0.00、avg60=0.04,而 avg300 还挂着 10.25——那是之前那轮实验的尾巴。看「现在有没有压力」用 avg10,看「趋势」才用 avg60/avg300
  • cgroup v2 每个组都有自己的 memory.pressure / cpu.pressure / io.pressure。根文件只告诉你「有压力」,逐层往下读才能定位「是哪个组在制造压力」
  • 内存压力演示用的是 memory.high(节流)而不是 memory.max(超限即 OOM 击杀):实测限 200M 后 memory.current 停在 213696512 字节(≈203.8 MiB),进程没有被杀,只是被按在那儿慢慢磨
  • /proc/pressure/* 的权限是 rw-rw-rw-,普通用户也能读——这是刻意设计的,监控不该需要 root

Linux PSI 资源压力指标:从 /proc/pressure 到 cgroup v2 压力定位

「这台机器忙不忙」这个问题,传统上靠 load average 回答。但 load average 有个根本性的毛病:它统计的是任务数,不是等待时间。一个进程被 IO 卡住 10 秒,和一个进程老老实实算满 10 秒 CPU,对 load 的贡献是一样的——可这两件事对用户的影响完全相反。

PSI(Pressure Stall Information,压力停顿信息)就是为填这个坑来的。它不问「有多少任务」,只问一件事:在过去这段时间里,有多少比例的时间有任务因为等不到 CPU、内存或磁盘而干等着。答案直接是个百分比,不需要任何换算就能拿去告警。

本文在一台 Ubuntu 22.04(内核 5.15.0-30-generic、1 核、1.9G 内存、无 swap)的小机上,把三种资源分别压出压力,实测每个字段的含义和几个容易读错的地方。

三个文件、两行、三列

PSI 的接口是三个只读文件,一个资源一个:

echo "CPU 核数: $(nproc)"
cat /proc/loadavg
for f in cpu memory io; do
  echo "--- /proc/pressure/$f ---"
  cat /proc/pressure/$f
done

/proc/pressure 下 cpu、memory、io 三个文件的原始内容,每行 some/full 各带 avg10、avg60、avg300 与 total

格式拆开看是这样的:

位置含义
some 行至少有 1 个任务因为等这个资源而停摆的时间占比
full 行所有非空闲任务都在停摆的时间占比
avg10 / avg60 / avg300最近 10 / 60 / 300 秒窗口的百分比(指数滑动平均)
total自开机以来累计停摆的微秒数,只增不减

几个立刻能用的读法:

  • total 是最实的数据。它没有平滑、没有窗口,就是硬邦邦的累计值。记下两个时刻的值相减,除以 1000000,就是这段时间里「一共停摆了几秒」——本文后面所有结论都靠这个算。
  • full 才是「卡住」。some 高只能说明有人在等,机器整体可能还很流畅;full 高说明所有能干活的都被挡住了,这才是用户会打电话来投诉的状态。
  • 权限是 rw-rw-rw-。/proc/pressure/* 对所有人可读,普通用户、容器里的进程都能直接 cat,不需要 root。这是个刻意的设计取舍:监控代理不该为了读一个百分比就要特权。

顺带看一眼这台机器的 loadavg:0.00 0.17 0.17 1/121 7637,整机基本闲着。

实测:把三种压力分别造出来

光看格式没用,得知道它动起来是什么样。下面这段脚本分两个阶段:先让 4 个忙循环去抢 1 个核,再用两路 O_DIRECT 直写把磁盘占住,每个阶段都打印对应的 pressure 文件。

echo "### 阶段 1:4 个忙循环抢 1 个核 ###"
for i in 1 2 3 4; do timeout 9 bash -c 'while :; do :; done' & done
sleep 5
echo "loadavg = $(cut -d' ' -f1-3 /proc/loadavg)"
grep -E 'some|full' /proc/pressure/cpu
wait
echo "### 阶段 2:2 路 O_DIRECT 直写 ###"
for i in 1 2; do ( for n in $(seq 1 60); do dd if=/dev/zero of=/tmp/psi_$i.bin bs=1M count=4 oflag=direct status=none; done ) & done
sleep 5
grep -E 'some|full' /proc/pressure/io
wait
rm -f /tmp/psi_1.bin /tmp/psi_2.bin

4 路忙循环下 cpu some 涨到 38.87% 而 cpu full 仍是 0.00%;2 路 O_DIRECT 直写下 io some 8.15%、io full 4.59%

CPU:some 涨了 4800 倍,full 纹丝不动

把这张图和上一张的 CPU 段对着读,两个 total 相减:

  • some 的 total:44478637 → 49553195,多了 5074558 微秒,也就是 5.07 秒
  • full 的 total:6127945 → 6128995,只多了 1050 微秒,也就是 1.05 毫秒

两个数字差了大约 4800 倍。这不是测量误差,而是 full 的定义决定的:它要求所有非空闲任务同时都在等 CPU。1 核上跑着 4 个忙循环,CPU 永远有任务在跑,所以「大家都停下来」这一刻几乎不存在——那 1.05 毫秒只是 SSH 会话建立那一瞬间蹭出来的。

结论很直接:在 CPU 这个维度上,full 基本可以不用看,some 才是有效信号。 这一点和内存、IO 正好相反(那两个资源的 full 非常有意义),照搬同一个阈值去配 CPU 告警会永远不触发。

同一时刻,load average 还在打瞌睡

注意截图里的 loadavg = 0.32 0.23 0.19。4 个进程已经把唯一的核占满了 5 秒,而 1 分钟口径的 load 才爬到 0.32——因为它是个 60 秒的指数平均,5 秒的突发在它眼里只是个小水花。

同一时刻 PSI 的 cpu some avg10 已经是 38.87%。

这就是两者最实际的分工:load average 适合看长期趋势,PSI 适合做短窗口告警。如果你把告警阈值挂在 load 上,一个 5 秒的 CPU 风暴要等到几十秒后 load 才爬过阈值,等告警发出来,事情可能已经过去了。

IO:这里的 full 才有意义

阶段 2 是两路 dd ... oflag=direct 直写(绕过页缓存,每一次写都实打实等磁盘),5 秒后读到的是:

  • io some avg10=8.15(total 11451988)
  • io full avg10=4.59(total 7407259)

some 和 full 都有值,而且 full ≈ some × 0.56。原因不难推:这台机器上真正在干活的只有那 2 个 dd 进程,所以「至少一个在等」和「两个都在等」之间只差一个条件——实测下大约一半的停顿时间里,两个写进程是同时被卡住的。

这也说明 full 的正确读法:它衡量的是「整机/整组是否集体停摆」,进程数越少,some 和 full 越接近。

用 cgroup v2 把「谁在制造压力」挖出来

根目录那三个文件有个绕不过去的局限:它只告诉你有压力,不告诉你谁造成的。一台跑了二十个服务的机器上,memory some avg10 突然涨到 40%,你只能一个一个猜。

cgroup v2 解决了这件事——每个组都有自己的三个 pressure 文件。下面这个演示用 systemd-run 起一个临时服务,给它设 200M 的 memory.high,然后让它在里面分配 300MB:

systemd-run --unit=psi-demo -p MemoryHigh=200M python3 -c "b=[bytearray(1048576) for _ in range(300)]" 2>&1
sleep 6
CG=/sys/fs/cgroup$(systemctl show -p ControlGroup --value psi-demo.service)
echo "cgroup = $CG"
echo "--- 该 cgroup 自己的 memory.pressure ---"
cat $CG/memory.pressure
echo "--- memory.current / memory.high ---"
cat $CG/memory.current
cat $CG/memory.high
echo "--- 整机 /proc/pressure/memory ---"
cat /proc/pressure/memory
systemctl stop psi-demo

临时服务 cgroup 内的 memory.pressure(some 与 full 都是 43.06,且 total 完全相同)与整机 pressure 的对照,memory.current 停在 213696512 字节

三个可以立刻带走的观察:

① 该组的 some 和 full 完全相等(都是 43.06),连 total 都一模一样。 这不是巧合——这个 cgroup 里只有一个任务,「至少一个任务在等」和「所有任务都在等」对它来说是同一件事。推论:看到 some == full,先怀疑这个组里只有一个能干活的东西,别急着下「整机要炸」的结论。

② 进程没有被杀。 memory.current 停在 213696512 字节(约 203.8 MiB),比 memory.high 的 209715200 字节(200 MiB)略高一点点,然后就不动了,进程一直在跑。这是 memory.high 和 memory.max 的关键区别:

memory.highmemory.max
超限后节流:强制回收 + 把分配内存的任务按住等待硬限:回收不回来就触发 cgroup OOM 击杀
进程结局活着,但变慢被杀
适合「可以慢,不能死」的服务硬隔离,防止单个服务拖垮整机

想让演示里的内存压力持续存在、又不想把进程打死,memory.high 就是那个旋钮——本文这段压力就是这么稳稳造出来的。反过来,如果你在 memory.pressure 里看到某个组的压力长期偏高、同时那个组的进程又频繁重启,就该去查它的 memory.max 和 dmesg 里的 OOM 记录了(这块可以配合本站的 Linux 内存排查:从 free 到 /proc/meminfo 与 OOM Killer 定位 一起看)。

③ 逐层下钻就是定位过程。 根文件 some avg10=41.52 告诉你「内存有压力」,而拿 ControlGroup 路径去读具体组的文件,得到的是同一个压力在归因上的落点。真实排查的套路就是:根 pressure 报警 → 沿 cgroup 树逐层 cat */memory.pressure → 找到那个数值最高的分支 → 它就是压力源。比 top 按内存排序靠谱得多,因为 top 只显示当前瞬时占用,而 PSI 显示的是这段时间里谁在真的等。

别把 avg 当精确值读

回到第一张截图,有一处需要专门解释:memory 那两行是

some avg10=0.00 avg60=0.04 avg300=10.25 total=204421134

avg10 是 0.00、avg60 只有 0.04,可 avg300 却是 10.25。这台机器当时已经空闲了十几分钟,没有任何内存压力——那 10.25 是更早那轮实验留下的指数滑动平均的尾巴。

PSI 的 avg10/avg60/avg300 用的是时间常数为 10/60/300 秒的 EWMA,按 exp(-t/τ) 衰减。τ=300 秒意味着一个尖峰要经过十几分钟才会衰减到接近 0。所以:

  • 问「现在有没有压力」→ 只看 avg10
  • 问「最近是不是一直不太平」→ 看 avg60 和 avg300
  • 要精确数字 → 用 total 相减,它没有平滑

这一点在实际配告警时很关键:拿 avg300 当触发条件,会因为衰减太慢而告警迟迟不消;拿 avg10 当触发条件,又容易被瞬时毛刺反复触发。常见做法是 avg10 触发、avg60 确认。

把它用起来

这台机器上验证过的几个参数,可以当作起步参考(都是 some 口径的 avg10):

  • CPU:看 some,full 忽略。持续 > 5% 说明有任务在排队等 CPU。
  • 内存:some 和 full 都要看。full 一旦持续非 0,说明有任务在等内存回收,通常紧接着就是 OOM。
  • IO:full 最有价值。它是「整机因为磁盘而集体卡住」的直接证据,也是 iostat 的 %util 给不出来的信息——%util 100% 的盘可能一点都不卡(比如全在缓存里),而 PSI 的 full 不会骗人。

再补两个实用细节:

  • PSI 需要内核 CONFIG_PSI=y(4.20 起主线自带)。cgroup v1 时代还得在启动参数里加 psi=1 才能拿到 per-cgroup 的数据;cgroup v2 默认就有,本文这台机器是 cgroup2fs,直接可用。
  • 只想看某个具体进程造成的压力,把它塞进 systemd-run --scope(或者写个带 MemoryHigh/CPUWeight 的单元)再读那个组的 pressure 文件就行,和本文的演示完全一样。

小结

  • PSI 给出的是停顿时间占比,不是任务数、不是利用率。这让它天然适合做告警阈值。
  • some = 有人受影响;full = 集体停摆。CPU 的 full 恒为 0,别拿它配告警;内存和 IO 的 full 才是有信息量的那个。
  • total 是微秒累计值,两个时刻相减得到「这段时间停摆了几秒」——本文靠它算出 4 路忙循环期间 some 涨了 5.07 秒、full 只涨了 1.05 毫秒。
  • 三列 avg 是 EWMA 不是精确平均,avg10 看当下、avg60/avg300 看趋势、total 要精确。
  • cgroup v2 让每个组都有自己的 pressure 文件,这是从「有压力」走到「谁制造的压力」的那一步。
  • 造持续的内存压力又不想杀进程,用 memory.high(节流)而不是 memory.max(击杀)。

想继续往资源控制这块挖,可以接着看本站的 Linux cgroup 资源控制:从 CPU 与内存限制到 systemd 运行单元(控制器与限额的完整用法)和 Linux 系统实时监控:从 htop 到 iostat、mpstat 与 ss 的性能观测工具(瞬时指标的观测手段,和本文的时间维度互补)。

参考资料:

发表评论

暂无评论,快来抢沙发吧!