Linux 基础体系 · 第 39/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux PSI 资源压力:CPU、内存、I/O Stall、告警和容量判断

Linux 的资源监控通常从“使用了多少资源”开始:CPU 使用率、内存使用率、磁盘吞吐量、I/O 延迟。但这些指标不能直接回答一个更接近用户体验的问题:

有多少时间,任务因为资源不足而无法继续执行?

Linux Pressure Stall Information(PSI,压力停顿信息)就是围绕这个问题设计的内核观测机制。它统计任务因为等待 CPU、内存或 I/O 而无法前进的时间,并将结果暴露为时间比例、累计停顿时间以及可触发的告警事件。

PSI 适合回答以下问题:

  • CPU 很忙时,是否真的有任务排队等待 CPU?
  • 内存还没有耗尽时,是否已经出现回收、换入或直接内存分配停顿?
  • 磁盘吞吐量不高时,是否有大量任务被 I/O 阻塞?
  • 某个 cgroup 是否比整台机器更早出现资源压力?
  • 当前资源压力是短暂尖峰,还是持续性的容量不足?
  • 应该扩容、限流、调整 cgroup 配额,还是先修复单个 I/O 或内存异常任务?

PSI 不是资源利用率,也不是延迟直方图,更不是容量规划的单一答案。它提供的是“资源争用导致任务停顿”的证据。


一、理解 PSI 之前:使用率和压力不是同一个概念

设一台机器有 8 个逻辑 CPU。

如果只有一个线程持续运行,它可能占用一个 CPU:

  • CPU 利用率约为 12.5%;
  • 没有其他任务因为 CPU 排队;
  • CPU PSI 可能接近 0。

如果有 32 个 CPU 密集型线程:

  • 8 个线程同时运行;
  • 其余线程处于可运行但未运行状态;
  • CPU 利用率接近 100%;
  • CPU PSI 会反映任务等待 CPU 的时间。

因此:

CPU utilizationCPU pressure\text{CPU utilization} \neq \text{CPU pressure}

前者描述 CPU 执行时间占比,后者描述任务因为无法获得 CPU 而停顿的时间占比。

反过来也成立。CPU 利用率可能不高,但任务仍然有明显的 CPU PSI,例如:

  • 任务被 cgroup CPU 配额限制;
  • 大量线程频繁唤醒,产生调度竞争;
  • CPU 被中断、软中断或不可抢占内核代码占用;
  • 任务的可运行时间集中在短时间窗口内;
  • 应用只需要一个 CPU,但该 CPU 被其他任务竞争。

同样,内存使用率达到 80% 不一定表示内存压力很高;如果剩余内存足够,分配和访问都没有停顿,memory PSI 可以接近 0。相反,内存使用率还没有达到 100%,但频繁回收、页面抖动或直接回收已经让应用停顿,memory PSI 会先上升。


二、PSI 的核心定义:任务因资源停顿了多久

2.1 Stall 的含义

PSI 中的 stall 可以译为“停顿”或“受阻”。

它不是简单统计资源是否忙,而是统计:

一个任务本来可以继续推进,但因为目标资源条件不满足,暂时无法推进。

不同资源的“无法推进”状态不同:

资源 典型 stall 状态
CPU 任务处于可运行状态,但暂时没有获得 CPU
内存 任务因内存回收、换页、内存分配等原因无法继续
I/O 任务等待块设备或相关 I/O 操作完成

PSI 记录的是任务停顿时间的聚合结果,而不是每个系统调用的延迟明细。因此,它不能直接替代:

  • perf sched 的调度延迟分析;
  • iostat 的块设备延迟;
  • vmstat 的换页统计;
  • eBPF 记录的具体请求生命周期;
  • 应用自身的请求延迟直方图。

PSI 的价值在于先回答“是否已经影响到任务推进”,然后再选择更细的工具寻找原因。

2.2 somefull

PSI 对内存和 I/O 通常提供两类状态:

  • some:至少有一个非空闲任务正在因为该资源停顿;
  • full:所有非空闲任务都正在因为该资源停顿。

这里的“非空闲任务”不是“系统中所有进程”,而是当前参与该压力统计的、具有执行需求的任务。睡眠等待定时器、等待网络事件或正常阻塞的任务不应被简单视为资源压力。

设某个时间点有 4 个活跃任务:

时间区间 停顿任务 some full
0~2 ms 1 个
2~5 ms 2 个
5~7 ms 4 个
7~10 ms 0 个

那么:

  • some 停顿时间为 2+3+2=72+3+2=7 ms;
  • full 停顿时间为 22 ms。

full 的意义不是“资源使用率达到 100%”,而是:

在那段时间内,所有有执行需求的任务都被该资源阻塞,系统没有可推进的工作。

这通常比 some 更接近系统级抖动、吞吐下降和整体停顿。

CPU PSI 通常只提供 some。原因是系统级 CPU pressure 主要描述可运行任务等待 CPU,而系统总会有至少一个任务获得 CPU 执行;内核文档和具体内核版本的接口应作为最终依据。不要假设所有资源文件都必然同时有 somefull


三、PSI 的数学含义:平均值是时间比例,不是资源占用率

典型 PSI 文件包含三类窗口平均值:

some avg10=0.00 avg60=0.00 avg300=0.00 total=123456
full avg10=0.00 avg60=0.00 avg300=0.00 total=123456

各字段含义如下:

  • avg10:最近约 10 秒内的压力比例;
  • avg60:最近约 60 秒内的压力比例;
  • avg300:最近约 300 秒内的压力比例;
  • total:从统计开始以来累计的压力时间,单位为微秒。

如果最近 10 秒内,some 状态累计持续 1.2 秒,则:

avg10=1.210×100%=12%\text{avg10} = \frac{1.2}{10} \times 100\% = 12\%

这表示最近 10 秒内,有 12% 的墙上时钟时间至少存在一个任务因该资源停顿。它不表示:

  • 该资源被使用了 12%;
  • 所有任务平均被阻塞了 12%;
  • 某个请求必然增加了 12% 延迟;
  • 设备吞吐量下降了 12%。

3.1 some 中的重叠时间不会按任务数重复计算

假设两个任务同时因 I/O 停顿 100 ms:

  • 任务 A 停顿 100 ms;
  • 任务 B 停顿 100 ms;
  • some 不是 200 ms,而是这段时间的并集,即 100 ms;
  • 如果这两个任务是该压力域中全部活跃任务,full 也是 100 ms。

因此 PSI 的 some 反映“受影响时间段”,而不是“受影响任务数乘以停顿时长”。

这也是 PSI 与逐任务阻塞时间的一个重要区别:PSI 更适合观察系统或 cgroup 是否出现资源推进能力下降,而不是计算每个任务总共损失了多少 CPU 时间。

3.2 用累计值计算任意时间区间的压力

窗口平均值适合实时观察,但容量分析通常需要用累计值计算指定时间段。

设两次读取间隔为 Δt\Delta t 秒:

  • 第一次读取 total_1
  • 第二次读取 total_2
  • 两次读取间隔为 Δt\Delta t
  • total 单位为微秒。

则该区间的压力比例为:

P=total2total1Δt×106P = \frac{total_2-total_1}{\Delta t \times 10^6}

例如,某 memory PSI:

第一次:some total=8000000
第二次:some total=15800000

两次读取间隔 10 秒,则:

P=15,800,0008,000,00010×1,000,000=0.78=78%P=\frac{15{,}800{,}000-8{,}000{,}000}{10\times1{,}000{,}000} =0.78 =78\%

这说明在这 10 秒的时间线上,至少有一个任务处于 memory stall 的时间占比达到 78%。这是严重的推进能力下降,即使 MemAvailable 还没有归零,也应立即调查。

注意 total 是累计计数器,不应直接当作当前压力百分比。系统启动、cgroup 创建或 PSI 统计生命周期的具体起点,应结合文件和内核版本验证;监控系统应使用相邻采样点的差值,而不是保存一个固定的“启动时基线”并假定其永不重置。


四、PSI 暴露在哪里

4.1 系统级接口

现代 Linux 通常通过以下文件提供 PSI:

/proc/pressure/cpu
/proc/pressure/memory
/proc/pressure/io

查看示例:

$ cat /proc/pressure/cpu
some avg10=0.00 avg60=0.00 avg300=0.00 total=1234567

$ cat /proc/pressure/memory
some avg10=0.02 avg60=0.01 avg300=0.00 total=234567
full avg10=0.00 avg60=0.00 avg300=0.00 total=12345

$ cat /proc/pressure/io
some avg10=0.15 avg60=0.08 avg300=0.03 total=4567890
full avg10=0.04 avg60=0.02 avg300=0.01 total=1234567

输出格式的具体字段和可用资源类型受内核版本、配置和发行版影响。首先检查文件是否存在:

for f in /proc/pressure/cpu \
         /proc/pressure/memory \
         /proc/pressure/io; do
    if [ -r "$f" ]; then
        echo "== $f =="
        cat "$f"
    else
        echo "unavailable: $f"
    fi
done

如果这些文件不存在,可能原因包括:

  1. 内核未启用 CONFIG_PSI
  2. 使用了较老的内核;
  3. 内核通过启动参数关闭了 PSI;
  4. 容器的 /proc 视图或安全策略没有提供该接口;
  5. 当前环境并非完整宿主机。

可以检查内核配置:

grep -E 'CONFIG_PSI|CONFIG_PSI_DEFAULT_DISABLED' \
    /boot/config-"$(uname -r)" 2>/dev/null

常见结果可能是:

CONFIG_PSI=y
# CONFIG_PSI_DEFAULT_DISABLED is not set

不同发行版对内核配置和启动参数的处理不同,不能仅凭某一台机器的结果推断所有发行版都支持同样的 PSI 行为。

4.2 cgroup v2 接口

在 cgroup v2 中,某个 cgroup 目录通常可以看到:

cpu.pressure
memory.pressure
io.pressure

例如:

mountpoint -q /sys/fs/cgroup && \
cat /sys/fs/cgroup/my-service/memory.pressure

cgroup PSI 用来观察该 cgroup 压力域中的任务,而不是只看整台机器。它对于多租户主机尤其重要:

  • 整机 CPU PSI 很低,但某个容器受到 CPU 配额限制;
  • 整机内存尚可,但某个服务的 memory.max 太小;
  • 整机磁盘压力一般,但某个服务的 I/O 请求集中在慢设备或受限层;
  • 多个服务共享宿主机,只有一个 cgroup 出现 full 压力。

cgroup PSI 与 cgroup 控制器不是一回事:

  • 不启用 cpu 控制器,也可能观察 CPU pressure;
  • 不启用 memory 控制器,也可能观察 memory pressure;
  • PSI 是观测机制,不是限额机制;
  • memory.maxcpu.maxio.max 决定控制行为,pressure 文件描述任务是否因此停顿。

cgroup v2 的层级、委派和权限会影响谁能读取或写入哪些文件。将 /sys/fs/cgroup 直接写入容器、让不可信租户拥有创建子 cgroup 或配置监控触发器的权限,都可能造成资源管理和拒绝服务风险。


五、CPU PSI:可运行任务为什么没有立即执行

5.1 CPU pressure 的状态链路

CPU PSI 关注的是调度等待:

flowchart LR
    A[任务需要继续执行] --> B{任务是否可运行}
    B -- 否 --> C[正常睡眠/等待事件]
    B -- 是 --> D{是否立即获得CPU}
    D -- 是 --> E[运行]
    D -- 否 --> F[进入CPU some stall]
    F --> D

CPU some 上升意味着在观测窗口中,至少有一个任务处于:

  • 可运行;
  • 尚未获得 CPU;
  • 等待时间被 PSI 计入。

这与 /proc/stat 中的 idle、user、system 等 CPU 时间分类不同。一个 CPU 可以接近 100% 利用率,却没有明显的 CPU pressure,例如只有一个线程独占一个 CPU;也可以在平均利用率不高时出现明显 pressure,例如任务集中在少量 CPU 上排队。

5.2 一个具体算例

假设只有 2 个 CPU,有 4 个 CPU 密集型任务:

时间 正在运行 等待 CPU CPU some
0~10 ms 2 2
10~20 ms 2 2
20~30 ms 2 2

在这 30 ms 中:

  • CPU 利用率可能接近 100%;
  • 至少一个任务等待 CPU 的时间也是约 30 ms;
  • 因此这段时间对 CPU some 的贡献约为 30 ms;
  • 但不能据此说“CPU pressure 是 200%”,因为 PSI 按时间并集统计,不按等待任务数相加。

如果只有 1 个任务运行在 8 CPU 主机上:

  • CPU 使用率约为 12.5%;
  • 没有任务排队;
  • CPU some 可以接近 0。

这说明“CPU 空闲很多”并不能排除单线程任务遇到局部 CPU 争用,例如 CPU 亲和性、cpuset、实时任务或虚拟化调度造成的局部瓶颈。

5.3 CPU PSI 的边界

CPU PSI 上升可能来自:

  • CPU 核数不足;
  • 线程数过多;
  • CPU affinity 或 cpuset 将任务限制在少数 CPU;
  • cgroup CPU 配额或权重造成的竞争;
  • 虚拟机中的 vCPU 被宿主机调度延迟;
  • 高优先级任务、软中断或中断占用执行机会;
  • 应用线程模型导致大量可运行任务争用。

CPU PSI 不能独自区分这些原因。应结合以下证据:

pidstat -u -w 1
vmstat 1
mpstat -P ALL 1
ps -eo pid,psr,stat,pri,ni,comm --sort=psr

进一步可以使用:

perf sched record -a -- sleep 10
perf sched latency

或使用 eBPF 工具观察调度延迟、运行队列、cgroup throttling 和 CPU 迁移。

一个常见误判是把 CPU PSI 当作 CPU 利用率的另一种写法。正确的因果链应是:

任务产生执行需求
    -> 任务可运行
    -> 暂时没有获得执行机会
    -> CPU PSI some 增加

而不是:

CPU 使用率高
    -> CPU PSI 必然同样高

六、内存 PSI:内存还没用完,为什么任务已经卡住

6.1 memory stall 的典型来源

内存压力主要来自任务为了获得或访问内存而触发的内核工作,例如:

  • 直接内存回收;
  • 页面换入;
  • 内存压缩或整理;
  • 文件页回收后重新读取;
  • cgroup 内存限制导致的回收;
  • 内存分配路径上等待可用页;
  • 内存回收进一步引发 I/O 等待。

memory PSI 的关键不是“已分配内存占总内存多少”,而是:

内存管理路径是否已经让任务不能顺畅地继续执行。

因此,下列现象可能同时发生:

MemAvailable 尚未耗尽
memory PSI 已经升高
应用延迟已经恶化

原因是 Linux 会尽量使用空闲内存之外的 page cache,且可回收缓存不等于真正不可用的内存。另一方面,某个 cgroup 的 memory.max 可能很小,导致它先于宿主机进入回收和分配停顿。

6.2 somefull 的区别

假设某个服务有 10 个活跃工作线程:

  • 只有 1 个线程因内存回收暂停:memory some 增加;
  • 5 个线程同时暂停:memory some 继续增加,但 full 仍可能为 0;
  • 10 个线程全部因内存问题暂停:memory full 增加。

memory full 长时间持续,通常说明该压力域几乎没有可推进的工作,常见于:

  • 内存抖动;
  • 大量页面换入换出;
  • 极小的 cgroup 内存上限;
  • 工作集明显超过可用内存;
  • 多个线程同步等待同一批内存回收或 I/O。

6.3 内存 PSI 与 swap 的关系

swap 活跃不等于必然发生 memory pressure。

例如:

  • 后台线程主动换出冷页面;
  • 内存仍有较大余量;
  • 前台任务没有因为换入而明显停顿。

此时 swap I/O 可能存在,但 memory PSI 不一定高。

反之,如果工作集持续大于可用内存:

  1. 任务访问已换出的页面;
  2. 触发页面换入;
  3. 任务等待 I/O;
  4. 多个任务重复触发换入;
  5. memory somefull 可能上升;
  6. 应用请求延迟和吞吐同时恶化。

所以诊断内存压力至少应同时查看:

free -h
vmstat 1
cat /proc/pressure/memory
cat /sys/fs/cgroup/<group>/memory.current
cat /sys/fs/cgroup/<group>/memory.events

memory.events 中的 highmaxoomoom_kill 等事件可以帮助判断 cgroup 限制是否触发。PSI 说明“任务被内存系统拖慢”,而 memory.events 更接近“哪些内存控制边界被碰到了”。

6.4 反例:内存使用率高但没有 memory pressure

一台机器长期缓存大量文件:

  • free 显示 used 很高;
  • MemAvailable 仍然充足;
  • 应用访问命中 page cache;
  • 没有显著回收和换入;
  • memory PSI 接近 0。

此时直接根据“内存使用率 90%”触发扩容,可能是误判。

另一个反例是:

  • MemAvailable 看起来还不少;
  • 一个容器设置了较低的 memory.max
  • 容器内部频繁回收页面;
  • 容器的 memory PSI 明显高于宿主机 PSI。

这时应先检查 cgroup 限额和工作集,而不是只看整机内存。


七、I/O Stall:磁盘不一定忙,但任务可能一直在等

7.1 I/O PSI 的含义

I/O PSI 统计任务因为 I/O 路径而无法继续执行的时间。典型情况包括:

  • 等待块设备读写完成;
  • 等待文件系统提交或回写;
  • 等待换页 I/O;
  • 存储设备队列拥塞;
  • 网络文件系统或其他文件系统路径上的等待。

I/O PSI 不是磁盘利用率,也不是吞吐量:

  • 设备利用率高,但请求短、队列管理良好,任务不一定长时间停顿;
  • 设备利用率不高,但单个请求延迟很长,任务仍可能出现 I/O stall;
  • 吞吐量较低,但大量同步读请求集中在关键路径,I/O PSI 可能很高。

“磁盘没有跑满”不能推出“没有 I/O 压力”。

7.2 I/O somefull 算例

假设某 cgroup 有 3 个活跃任务:

时间区间 因 I/O 停顿的任务 io some io full
0~100 ms A 100 ms 0
100~180 ms A、B 80 ms 80 ms
180~230 ms B、C 50 ms 50 ms
230~300 ms 0 0

总计:

  • io some100+80+50=230100+80+50=230 ms;
  • io full80+50=13080+50=130 ms。

在 300 ms 观察区间内:

some=230/30076.7%\text{some}=230/300\approx76.7\%

full=130/30043.3%\text{full}=130/300\approx43.3\%

这表示该 cgroup 在大部分时间里至少有一个任务被 I/O 阻塞,并且有相当长时间所有活跃任务都无法推进。即使设备总吞吐量不高,也足以造成服务级别的明显延迟。

7.3 I/O PSI 不能直接告诉你是哪块盘

I/O PSI 聚合的是任务侧压力,不会直接给出:

  • 哪个块设备;
  • 哪个进程发起的请求;
  • 哪个文件;
  • 哪种 I/O 类型;
  • 读还是写;
  • 请求在队列中等待了多久。

因此 I/O PSI 上升后,应继续建立证据链:

iostat -xz 1
pidstat -d 1
vmstat 1
cat /proc/pressure/io

如果使用 eBPF,可以进一步关联:

进程/容器
  -> 文件或块设备请求
  -> request 发出时间
  -> request 完成时间
  -> 任务阻塞时间
  -> PSI 时间线

这样才能区分:

  • 设备本身延迟高;
  • I/O 队列过深;
  • 文件系统回写导致阻塞;
  • swap I/O 引发内存抖动;
  • 某个服务执行大量同步随机读;
  • 网络文件系统或远端存储延迟。

7.4 反例:I/O PSI 高不一定是本地磁盘坏了

以下情况都可能导致 I/O PSI 上升:

  • 数据库同步提交等待持久化;
  • 日志系统频繁 fsync
  • 容器使用共享存储;
  • 文件系统发生回写;
  • 内存压力触发 swap;
  • 网络文件系统服务端变慢;
  • 存储设备延迟增加但吞吐量没有达到峰值。

因此,直接因为 io.full 上升就替换磁盘或扩大磁盘带宽,可能把应用同步写、内存不足或文件系统配置问题误判为设备容量问题。


八、从读取到告警:PSI 的事件触发机制

窗口平均值适合轮询,但轮询有一个问题:短暂尖峰可能被采样间隔错过。Linux PSI 还提供基于文件描述符的触发机制。

常见触发器格式为:

some 100000 1000000

它表示:

  • 监控 some
  • 在约 1 秒窗口内;
  • 如果累计 stall 时间达到约 100000 微秒,即 100 ms;
  • 触发一次事件。

换算为比例:

100000/1000000=10%100000 / 1000000 = 10\%

full 也可以使用类似格式,但前提是对应资源和内核版本提供 full 统计。

8.1 一个最小的 Python 触发器示例

下面的程序展示 Linux PSI 的基本事件生命周期:

#!/usr/bin/env python3
import os
import select
import time

PATH = "/proc/pressure/memory"
TRIGGER = "some 100000 1000000\n"

fd = os.open(PATH, os.O_RDWR | os.O_NONBLOCK)

try:
    os.write(fd, TRIGGER.encode())

    poller = select.poll()
    poller.register(fd, select.POLLPRI)

    print(f"watching {PATH}: {TRIGGER.strip()}")

    while True:
        events = poller.poll(5000)
        if events:
            now = time.strftime("%Y-%m-%d %H:%M:%S")
            print(now, "memory PSI trigger fired")
        else:
            print("no trigger in the last 5 seconds")
finally:
    os.close(fd)

运行前需要满足:

  • 内核支持 PSI;
  • 文件存在;
  • 当前用户拥有打开和写入该接口所需权限;
  • Python 能够访问 /proc/pressure/memory
  • 当前内核支持对该文件注册触发器。

启动:

python3 psi-watch.py

可能输出:

watching /proc/pressure/memory: some 100000 1000000
no trigger in the last 5 seconds
2025-01-01 12:00:03 memory PSI trigger fired

8.2 触发器的生命周期

触发器的操作顺序是:

  1. 打开 PSI 文件;
  2. 向文件描述符写入触发器规则;
  3. 将文件描述符注册到 pollselectepoll
  4. 等待 POLLPRI 等事件;
  5. 触发后读取或记录当前 PSI;
  6. 文件描述符关闭后,触发器失效。

触发器是绑定到这个文件描述符的,不是永久写入内核配置。监控进程退出、文件描述符关闭或容器被重启后,规则需要重新注册。

触发器不是计数队列。它适合通知“窗口内发生了达到阈值的压力”,不适合精确记录每一个任务的每一次阻塞。若监控程序处理事件过慢,应配合读取累计 total,用累计差值检查是否丢失了多个事件。

具体可用的事件标志、权限要求和触发器行为会随内核版本变化。生产部署时应在目标内核上验证:

uname -r
test -r /proc/pressure/memory
test -w /proc/pressure/memory

不能因为某台发行版允许普通用户注册触发器,就推断其他发行版也允许。


九、告警设计:不要只对一个 avg10 数值报警

一个压力告警至少要解决三个问题:

  1. 压力是否真实存在;
  2. 压力是否持续;
  3. 压力是否已经影响关键工作负载。

9.1 只看 avg10 的失败表现

假设每 15 秒采集一次:

12:00:00 avg10=0.00
12:00:15 avg10=0.00
12:00:30 avg10=18.00
12:00:45 avg10=0.00

如果压力只持续了几秒,应用可能已经发生一次严重请求超时,但轮询数据几乎看不出问题。

反过来,单次 avg10=20% 也可能只是一次部署、备份或批处理尖峰,不应立即触发扩容。

9.2 使用多时间窗口区分尖峰和持续压力

可以把窗口解释为:

  • avg10:近期是否正在恶化;
  • avg60:一分钟内是否有持续影响;
  • avg300:是否已经形成容量趋势。

例如:

cpu.pressure:
some avg10=35.00 avg60=12.00 avg300=2.00

memory.pressure:
some avg10=8.00 avg60=5.00 avg300=1.00
full avg10=1.00 avg60=0.40 avg300=0.05

io.pressure:
some avg10=25.00 avg60=18.00 avg300=10.00
full avg10=8.00 avg60=4.00 avg300=1.00

合理的解释是:

  • CPU 近期突然拥塞,但长期平均尚不严重;
  • 内存有稳定的回收压力,近期还出现过所有任务同时停顿;
  • I/O 压力已经持续数分钟,并且存在全体活跃任务受阻的时段。

这里不能脱离业务 SLO 直接说“25% 一定严重”。对在线 API、离线转码、消息消费和数据库写入而言,同一 PSI 数值的含义不同。

9.3 告警应绑定服务对象

整机 PSI 告警无法自动说明哪个服务受影响。更有价值的监控维度包括:

主机 CPU PSI
主机 memory PSI
主机 io PSI

服务 cgroup CPU PSI
服务 cgroup memory PSI
服务 cgroup io PSI

服务请求延迟
服务错误率
服务吞吐量
cgroup 限额事件

例如,一个容器的 memory full 持续升高,同时:

memory.events: high 计数持续增加
请求 p99 升高
吞吐量下降

这比“整机内存使用率 90%”更能支持“该容器内存上限过低或工作集过大”的判断。


十、用 PSI 做容量判断:从压力到扩容的推导

PSI 最适合做容量判断的方式不是设定一个通用阈值,而是将资源压力与业务产出关联起来。

10.1 先定义资源容量是否足够

设某个时间窗口为 WW,资源的 some 压力比例为:

Psome=窗口内至少一个任务停顿的时间WP_{\text{some}} = \frac{\text{窗口内至少一个任务停顿的时间}} {W}

设业务吞吐量为 QQ,业务延迟为 LL

如果在逐步增加负载的实验中得到:

并发 吞吐量 p99 延迟 CPU some Memory some I/O some
100 1000 req/s 20 ms 0.5% 0% 1%
200 1980 req/s 24 ms 2% 0.1% 3%
300 2700 req/s 60 ms 12% 1% 15%
400 2750 req/s 180 ms 35% 8% 40%

从 300 到 400 并发:

  • 吞吐量几乎不再增加;
  • 延迟显著恶化;
  • 多种 PSI 同时上升。

这说明系统接近或超过有效容量。这里的“有效容量”不是 CPU 使用率达到某个固定百分比,而是:

在满足业务延迟和错误率目标的条件下,系统还能承载的最大稳定负载。

10.2 根据 PSI 区分容量瓶颈

CPU 瓶颈

典型证据:

CPU some 持续升高
CPU 利用率较高
run queue 增长
应用 CPU time 增加
I/O PSI 和 memory PSI 不明显

可能措施:

  • 优化热点代码;
  • 减少无效线程和频繁唤醒;
  • 调整 CPU affinity;
  • 增加 CPU 配额或核数;
  • 将批处理与在线服务隔离。

内存瓶颈

典型证据:

memory some 持续升高
memory full 也出现
major fault、swap-in 或 reclaim 增加
memory.events 中 high/max 增加
应用延迟变长

可能措施:

  • 减少工作集;
  • 修复缓存无界增长;
  • 调整 cgroup 内存上限;
  • 增加物理内存;
  • 限制并发,避免同时触发大量分配;
  • 检查是否错误地将 page cache、匿名内存和 swap 混为一谈。

I/O 瓶颈

典型证据:

io some 持续升高
io full 出现
iostat 显示 await 或队列等待上升
请求路径包含同步读写或 fsync

可能措施:

  • 减少同步 I/O;
  • 合并小请求;
  • 调整日志和刷盘策略;
  • 优化索引与访问模式;
  • 将不同工作负载放到不同设备;
  • 增加存储 IOPS 或降低设备延迟。

PSI 只能说明“任务被 I/O 拖住”,不能直接证明“需要更大带宽”。如果瓶颈是单次同步写的尾延迟,增加顺序带宽可能没有效果。


十一、cgroups v2 场景:主机压力和服务压力可能完全不同

cgroup v2 的资源控制和 PSI 结合后,可以观察“谁正在承受压力”。

假设宿主机有足够内存:

主机 memory some avg60=0.5
服务 A memory some avg60=12
服务 B memory some avg60=0

可能原因是服务 A 的 cgroup 受到了:

memory.max
memory.high

限制。服务 A 内部频繁回收,而其他服务没有受到影响。

同样,某服务的 CPU PSI 高可能不是整机 CPU 不足,而是:

cpu.max 设置过低
cpuset.cpus 只允许使用少数 CPU
cpu.weight 很低

服务自身的 pressure 文件能够揭示这种局部容量不足。

一个典型排查流程:

CG=/sys/fs/cgroup/my-service

cat "$CG/cpu.pressure"
cat "$CG/memory.pressure"
cat "$CG/io.pressure"

cat "$CG/cpu.max" 2>/dev/null
cat "$CG/memory.max" 2>/dev/null
cat "$CG/memory.current" 2>/dev/null
cat "$CG/memory.events" 2>/dev/null
cat "$CG/io.max" 2>/dev/null

如果目录不属于当前用户、文件不可读或不存在,应先确认:

stat -fc %T /sys/fs/cgroup
mount | grep cgroup
findmnt -t cgroup2

不要在生产环境随意修改 cpu.maxmemory.maxio.max 来验证假设。修改资源上限可能立即导致:

  • 任务节流;
  • 内存回收加剧;
  • OOM kill;
  • I/O 延迟扩大;
  • 服务连锁超时。

验证应优先通过只读观察、压测环境或可回滚的配置变更完成。


十二、PSI 与其他工具的证据链

PSI 提供“结果性证据”,但故障定位需要把它与原因性证据连接起来。

12.1 CPU

cat /proc/pressure/cpu
mpstat -P ALL 1
pidstat -u -w 1
vmstat 1

重点观察:

  • 是否只有某些 CPU 忙;
  • 运行队列是否增长;
  • 上下文切换是否异常;
  • 某个进程是否长期可运行但得不到调度;
  • cgroup 是否发生 CPU quota throttling。

12.2 内存

cat /proc/pressure/memory
free -h
vmstat 1
sar -B 1
cat /proc/vmstat

重点观察:

  • reclaim、pgscan、pgsteal;
  • major fault;
  • swap in/out;
  • direct reclaim;
  • cgroup memory.events
  • 应用工作集是否超过限制。

12.3 I/O

cat /proc/pressure/io
iostat -xz 1
pidstat -d 1
vmstat 1

重点观察:

  • await
  • 队列深度;
  • 设备 %util
  • 读写模式;
  • 哪些进程产生 I/O;
  • 是否存在 swap 或回写诱发的 I/O。

12.4 eBPF 的位置

当 PSI 已经证明任务受压,但常规统计无法定位原因时,可以使用 eBPF 建立更细的关联:

PSI 上升
  -> 找到受影响的 cgroup
  -> 找到任务阻塞时间
  -> 关联调度、内存回收或块 I/O 事件
  -> 关联文件、设备和请求延迟
  -> 回到具体服务和请求类型

eBPF 工具的名称、探针和字段高度依赖内核版本、BTF、发行版和工具包版本,不能把某个版本的脚本当作所有主流发行版都支持的稳定 API。生产环境应先验证工具与当前内核的兼容性,并评估采样开销。


十三、常见误解和失败诊断

13.1 “CPU 使用率 100%,所以 CPU PSI 一定是 100%”

错误原因:CPU 使用率按执行时间统计,CPU PSI 按等待执行的任务时间统计。

  • 单线程占满一个 CPU:利用率可以很高,pressure 很低;
  • 多线程在 8 个 CPU 上排队:利用率接近 100%,pressure 也可能很高;
  • 多个 cgroup 受配额限制:整机利用率不高,某个 cgroup pressure 很高。

13.2 “内存使用率超过 90%,所以一定有内存压力”

错误原因:Linux 会使用 page cache,使用率不等于不可回收内存,也不等于任务停顿。

应同时看:

MemAvailable
memory PSI
swap 活动
回收统计
memory.events
应用延迟

13.3 “I/O PSI 高,就是磁盘吞吐不够”

错误原因:PSI 表示任务等待 I/O 的时间,不说明瓶颈是带宽、IOPS、延迟、队列、文件系统还是远端存储。

必须结合 iostat、进程 I/O、文件系统和请求级追踪。

13.4 “full 越高,说明机器用了越多资源”

错误原因:full 表示所有相关活跃任务都处于该资源 stall,不是资源利用率。

一个任务组只有一个活跃任务时,该任务一旦等待 I/O,somefull 都可能同时增加;这并不代表整台机器所有任务都被 I/O 阻塞。

13.5 “PSI 数值超过 10% 的机器都应该扩容”

错误原因:没有业务上下文的固定阈值通常不可靠。

对批处理任务,短时高 pressure 可能可以接受;对低延迟服务,即使 1% 的 full 也可能对应明显的尾延迟问题。阈值应来自:

  • 服务 SLO;
  • 压测曲线;
  • 历史基线;
  • 资源隔离策略;
  • 负载类型。

十四、生产使用时的权限、版本和风险

14.1 内核与发行版差异

PSI 的可用性依赖:

  • 内核版本;
  • CONFIG_PSI
  • 是否默认启用;
  • cgroup v2 是否挂载;
  • 容器是否能看到对应 /proc 或 cgroup 文件;
  • 文件权限和安全策略。

先在目标机器确认,而不要直接把文章中的文件路径当作部署保证:

uname -a
test -e /proc/pressure/cpu
test -e /proc/pressure/memory
test -e /proc/pressure/io
findmnt -t cgroup2

14.2 权限

读取系统级 PSI 通常只需要读取相应文件,但写入触发器、访问某个 cgroup 或委派 cgroup 子树可能受到权限限制。

在容器中还要注意:

  • 容器看到的 /proc 可能是受限视图;
  • 容器可能只能读取自己的 cgroup;
  • systemd、容器运行时或编排系统可能管理 cgroup 层级;
  • 直接修改运行时创建的 cgroup 文件可能被覆盖。

14.3 监控系统的实现风险

监控 PSI 时应避免:

  • total 当作瞬时值;
  • 用固定采样间隔假定窗口平均值精确代表整个区间;
  • 只采集主机 PSI,不采集服务 cgroup PSI;
  • 只设置 avg10 告警;
  • 忽略容器迁移、cgroup 重建导致的计数器生命周期变化;
  • 频繁创建大量触发器;
  • 在高压状态下执行会进一步增加 I/O 或内存负担的诊断动作。

更稳妥的采集方式是:

  1. 读取并解析 avg10/60/300
  2. 保存 total 和采样时间;
  3. 对相邻 total 做差;
  4. 检测 cgroup 或主机重启、计数器回退;
  5. 将 PSI 与业务延迟和资源控制事件关联;
  6. 对告警设置恢复条件和抑制策略。

十五、一个可操作的诊断顺序

当服务延迟升高时,可以按以下因果顺序检查:

第一步:确认是否存在任务级资源压力

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

如果所有 PSI 都接近 0,问题可能不在这三类资源的系统级争用上,还应检查:

  • 网络;
  • 锁竞争;
  • 应用线程池;
  • GC;
  • 上游依赖;
  • 数据库连接池;
  • 业务限流。

第二步:区分短时尖峰和持续容量不足

比较:

avg10
avg60
avg300
  • 只有 avg10 高:近期尖峰或突发故障;
  • avg10avg60 都高:当前正在持续;
  • avg300 也高:容量或长期配置问题的可能性增加。

第三步:定位资源类型

  • CPU some 高:检查运行队列、亲和性、配额和调度;
  • memory some/full 高:检查回收、swap、cgroup 上限和工作集;
  • io some/full 高:检查设备延迟、队列、同步 I/O 和回写。

第四步:从主机缩小到 cgroup

比较:

主机 PSI
服务 cgroup PSI
同机其他服务 cgroup PSI

如果只有一个 cgroup 高,优先检查该服务的配额和访问模式;如果多个 cgroup 同时高,则更像主机级容量或共享设备问题。

第五步:验证业务影响

将压力时间与以下指标对齐:

请求 p50/p95/p99
超时率
错误率
吞吐量
队列长度
消费延迟
数据库提交延迟

只有当“资源压力”和“业务退化”在时间上、对象上都能对应,才能较有把握地把 PSI 作为根因链的一环。


十六、总结:PSI 的正确定位

Linux PSI 的核心不是告诉你“CPU、内存或磁盘用了多少”,而是告诉你:

任务有多少时间因为资源争用或资源管理路径而无法继续推进。

需要记住几个关键关系:

pressure=任务因资源停顿的时间观察墙上时间\text{pressure} = \frac{\text{任务因资源停顿的时间}} {\text{观察墙上时间}}

  • some:至少一个相关活跃任务停顿;
  • full:所有相关活跃任务都停顿;
  • avg10/60/300:不同时间窗口的压力比例;
  • total:累计停顿时间,应通过差值计算任意区间压力;
  • CPU PSI 通常关注 some,不能假定所有资源都有 full
  • cgroup PSI 用于发现局部资源压力,不能被主机平均值替代;
  • PSI 是结果性指标,需要与调度、内存回收、块 I/O、cgroup 事件和业务 SLO 组成证据链;
  • 容量判断应依据“负载增加后吞吐、延迟和 PSI 的变化”,而不是采用脱离业务的固定阈值。

当 CPU 利用率、内存使用率或磁盘吞吐量无法解释服务延迟时,PSI 往往能提供一个更接近实际用户影响的答案:系统是否已经因为资源不足,让任务停止前进。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。