PSI 到底在测什么

/proc/pressure/memory 在终端里的输出 mockup——内核格式示意

Pressure Stall Information(PSI)在 Linux 4.20 合入(提交 e970d0aa0bb98a9d4b9a2e3887da44b79f7b46c8——Johannes Weiner 的 psi-tracking 系列)。它不是 load average,不是吞吐量,也不是「CPU 多忙」。PSI 只回答一个问题:

在这个作用域(system 或 cgroup)里,这段时间里有多少墙上时钟时间花在「至少有任务在等资源」上?

单位是「测量窗口内的毫秒数」——等得越多,数字越大。Linux 暴露三种 PSI 资源:cpusome/iosome/memoryfull/(仅 cgroup v2)。本文聊的是后面这俩——内存压力那一对。

somedata/memory(/proc/pressure/memory)统计「集合内任意任务因任意内存原因阻塞」的时间总和。”some” 意思是「集合里有任务得等」。full/memory(只 cgroup v2)意思是「同一瞬间集合里所有任务都在等」。你的工作负载有 32 个 worker,31 个在跑、1 个陷在回收里——some 会亮、full 仍是 0。32 个一起卡,俩都亮。

时序图看一眼就懂

想象一个 1 秒窗口。内核在每个 100ms 切片里累加「因内存原因被调度器阻塞的任务数」。最终输出:

Bash
some avg10=2.34 avg60=1.85 avg300=0.92 total=9182345
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

avg10/60/300 是 α=2/(window+1) 的指数移动平均,所以 avg10 对尖峰反应最快。total 是自开机以来累计的阻塞时间(微秒)。这是规范格式,工具链应该只消费这个。

内核在哪里算的

示意图:60 秒窗口里 some/memory 对比 full/memory

所有 PSI 会计都集中在 kernel/sched/psi.c。机制是每个 cgroup 各持两组 PSI 状态机:一组「任务在本 CPU 上追踪压力」、一组「按 cgroup 递归聚合」。每次上下文切换、调度 tick、内存回收回调,内核都会调 psi_task_change(),或对应内存场景的 psi_memstall_{enter,leave} 包装。

mm/ 里主要的调用点:

调用点 原因 PSI 更新
shrink_inactive_list() (mm/vmscan.c) 直接回收:从 inactive LRU 抽页 入口 ENTER、出口 LEAVE
balance_pgdat() kswapd 醒来扫描 node nr_writeback + nr_inqueue > watermark 时 ENTER
page_alloc() 慢路径 __GFP_KSWAPD_RECLAIM / __GFP_DIRECT_RECLAIM 触发 psi_memstall_enter
try_to_free_pages() thin 分配失败路径 ENTER
compaction_alloc() compaction 引起的阻塞 竞争时 ENTER
cgroup memory.pressure 写入 阈值突破时通知用户态 cgroup-v1/pressure.c 中写入回调

精妙之处:就算回收 5 微秒后就成功了,阻塞信号照样记账。PSI 不测系统是否丢工,测的是「有没有任务被迫等」。这一区分对容量规划很关键——256KB 请求里一个 page 的 stall 也是 stall。

轮询(Polling) vs 触发(Trigger)

PSI 给两种接口:

轮询——读一次,自维护状态

Bash
cat /proc/pressure/memory
# some avg10=0.05 avg60=0.01 avg300=0.00 total=12345
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

/proc/pressure/memory 是全系统资源。full/不会在这里出现——full 需要 cgroup 上下文,因为 system 级别「一个任务 100% 卡住」是平凡成立的、没有信息量。

触发——让内核只在「有必要」时叫醒你

Bash
cat > /proc/pressure/memory <<EOF
some 200000 1000000  # 任意 1s 窗口内阻塞 200ms
EOF

这就在 some 行上装了一个 trigger。内核安排一个 pollqueue;一旦 some 在 1s 窗口内累计超过 200ms,pollwake() 就唤醒你的 fd,你的 poll()/select() 拿到 POLLPRI,读一条后状态自动复位。必须持续读——错过唤醒不会自动重发。

那个双阈值语法(stall_us window_us)就是去抖机制。PSI trigger 只有在「累计 stall 超 stall_us而且「这个暴露持续了整整 window_us」才触发。每隔 1ms 闪一下的 50ms 尖峰叫不醒它;持续半秒的 20% 内存压力能。

生产踩坑: cgroup v2 leaf 上装 trigger 需要 cgroup.event_control(legacy v1)或 memory.pressure(v2)的写权限。非特权用户不能装,但随便读。

轮询 vs 触发——怎么选

场景 推荐
横向扩缩容器决定要不要多起一个 pod trigger。99 秒没事就不该每秒轮询。
长期容量仪表盘 / Grafana 抓取 polling。5s–60s 滑动平均把抖动压平。
在线排查一个慢请求 watch -n 1 cat 看 30 秒。别装 trigger 然后忘掉。
Kubernetes 内存压力驱逐(kubelet --eviction-pressure-transition-period) trigger,间接触发 memory.available

cgroup v2 怎么接

cgroup v2 树状结构示意——展示 PSI 自叶子节点向上聚合

内存压力沿 cgroup 树向上传播。cgroup B/A/my-pod 卡了,所有开启了 PSI 的祖先(cgroup.subtree_control +memory 之后默认开)都会同步记账。memory.pressure 文件放在每个开启 PSI 的非叶子节点上。

读 leaf 的完整压力:

Bash
mkdir -p /sys/fs/cgroup/bench
cd /sys/fs/cgroup/bench
cat memory.pressure
# some avg10=12.34 avg60=8.10 avg300=4.55 total=9182345
# full avg10=2.10 avg60=1.04 avg300=0.50 total=4451234

这条数据就是 kubelet 暴露给 Kubernetes、容器运行时再吐给 OOM-free 可观测性栈的口径。

祖先聚合的口径

对 root 上的 full精确描述:leaf 上的压力事件会累加到每一个开启 PSI 的祖先。父节点的总和不是「取孩子的 max」,而是「带重叠的并集」。两个 sibling 同时卡的时候,父亲节点的 full一次,不是两次。

容量规划时:别重复算

容器运行时里怎么读 memory.pressure

实操流水线:cgroup v2 enabled + CONFIG_PSI=y(5.x 之后大多数发行版默认开)。

先确认内核真的有:

Bash
zgrep PSI /proc/config.gz
# CONFIG_PSI=y

在容器里读——只要把 cgroup v2 层级挂进来就行:

Bash
# 特权调试容器内
cat $(mount -t cgroup2 | awk '{print $3}')/memory.pressure

Go:

Go
// gopsutil 的 memtype.VirtualMemory() 不返回 PSI。用这个:
import "github.com/prometheus/procfs"

psi, err := procfs.NewPSI("/proc/pressure/memory")
if err != nil { return err }
some, _ := psi.SomePSI()
fmt.Println(some.Avg10, some.Avg60)

Python,直接解格式:

Python
def read_psi(path):
    out = {}
    with open(path) as f:
        for line in f:
            label, rest = line.split(" ", 1)
            fields = dict(t.split("=") for t in rest.split())
            out[label] = {k: float(v) for k, v in fields.items()}
            out[label]["total"] = int(out[label]["total"])
    return out

在带 namespace 感知的容器内,直接读容器内 mount 点(/sys/fs/cgroup/...)下的 memory.pressure

为什么 PSI 抓得到 loadavg 抓不到的东西

top 显示 wa %。/proc/loadavg 显示 load average。这俩都是「系统忙不忙」的 popularity 投票——告诉你系统忙,但不告诉你对那份真正重要的活进展是不是慢。

PSI 是 Linux 内核第一个导出的以工作为锚的指标。some/memory 跑 10% 持续 60 秒,意思是「这 60 秒里有 6 秒至少有一个任务在睡觉等内存」。这告诉你:

  • 系统跑后台扫描还有富余、不卡前台
  • 或者,工作负载单进程的 working set 刚超 RAM、开始换页——loadavg 看不到这个

经典例子:make -j$(nproc) 加核不会变快。变成不卡 reclaimed page 才会变快。PSI 直接跟踪后者;loadavg 不。

常见生产模式

模式 1 — 按 PSI 触发自动扩缩容

爬虫消费 RSS 工作单元:

Python
import time, subprocess
def pressure_pct():
    out = subprocess.check_output(
        ["/bin/cat", "/sys/fs/cgroup/bench/memory.pressure"]
    ).decode()
    for line in out.splitlines():
        if line.startswith("some"):
            return float(line.split()[1].split("=")[1])
    return 0.0

while True:
    p = pressure_pct()
    if p > 30.0:           # 10s 内有 30% 在内存 stall
        print("scale-out: spawning extra worker")
        spawn_extra_worker()
    time.sleep(10)

模式 2 — 触发式驱逐提示

Bash
# 5s 窗口内有 500ms 内存 stall → 驱逐
echo "some 500000 5000000" > /sys/fs/cgroup/canary/memory.pressure
# 阈值跨过去内核就会 pollwake 我们

这本质就是 Kubernetes --eviction-pressure-transition-period 的机制,但非 K8s 工作负载也能自己跑。

模式 3 — 尾延迟调试

p99 飙升时,在飙升窗口内采样 /proc/pressure/memorysome avg10 > 1.0(10s 内阻塞 100ms+)就把内存压力加进嫌疑列表。配 cat /proc/vmstat | grep -E 'pgfault|pgmajfault' 区分 minor fault(文件缓存 miss,正常)和 major fault(真的去读盘,要钱)。

full/ vs some/ 的取舍 — full 更刁

full 触发条件是集合里所有可运行任务都 stall。系统级别很少见——你几乎总有几个内核线程在跑。放在 cgroup 级别才有意义:整个工作负载(比如你那 4 个 Redis-tier 副本)在某次卡顿里一起 memory-blocked 了。

你管的是同构集群(全部副本跑同一份二进制),full/memory 就是个领先的”所有人一起受罪“信号——和「我的某个 pod 行为不端、自己卡了」是不同的事

实操建议:some avg60 > 5.0 当一级报警页面,full avg10 > 0.5 当”我们已过不可逆点”页面。

跟 memory cgroup 限额怎么互动

some/memory 不区分原因是全局回收、NUMA locality migration、还是 cgroup 强制的硬限额。要拆开看:

Bash
# memory.current —— 这个 cgroup 实际用了多少
cat /sys/fs/cgroup/bench/memory.current
# memory.high —— 软上限(回收目标)
cat /sys/fs/cgroup/bench/memory.high
# memory.max —— 硬上限(杀进程目标)
cat /sys/fs/cgroup/bench/memory.max

memory.current > memory.high 且 PSI 高,说明 cgroup 在软上限回收——内核在求工作负载还内存、但还不杀它。memory.current >= memory.max 且 PSI 还在涨,你离 OOM-kill 不远了。看 memory.eventslow/high/max/oom 计数器——那些是 PSI 实时报告 stall 的结果

cgroup v2 内存模型的细节放同伴文章:Linux memory cgroup v2:内核真的执行这个预算

局限和尖刺

  • 4.20 以下内核不支持。你在 RHEL 7(3.10)的话,CentOS Stream 的 backport 不一定干净。用 vmstat + 自定义 instrumentation。
  • trigger 窗口不是免费的。装了不读,内核给每个 cgroup 写入维持轮询。别装一堆不读。
  • trigger 触发的是 POLLPRI,不是 POLLINselect()/epoll_wait() 用户常常忘。旧版 Node.js binding 默认 POLLIN,你得显式写 eventType: 'PRI'
  • total 单调递增; 32 位编译下 ~71 分钟溢出。决策只用 avg*,total 当「yes/no,PSI 触发过没」。
  • work-conserving 调度会遮蔽 stall。你的工作负载 100 个线程跑在 32 核机上,那 100 个内部的 stall 能看见,但内核让 CPU 满跑,所以 top 看着忙。PSI 才是「每个任务进展多慢」的指标。
  • 容器 memory.pressure 要 cgroup2 mount。你在 cgroup v1(老 systemd),PSI 只有系统级别——每 cgroup 的 memory.pressure 是 v2 独家。

读源码

要排查 bug 或者想知道真相:

  • kernel/sched/psi.c —— 整个会计状态机
  • include/linux/psi.h —— 回收路径用的 psi_memstall_{enter,leave}
  • mm/vmscan.c —— 大部分 ENTER/LEAVE 调用都在这里
  • mm/page_alloc.c —— 慢路径的 __GFP_* 处理
  • kernel/cgroup/memcg-v1.cmemcontrol.c —— 每 cgroup 聚合
  • Documentation/admin-guide/psi.rst —— 官方文档,稀疏但准确
  • Documentation/accounting/psi.rst —— 老格式,还有用

面积看着小,但回收重的负载上 PSI 会计会加几个百分点调度开销,所以它由 CONFIG_PSI=y 控制、启动参数 psi=0 可以关掉(性能关键路径用)。

## 相关文章

这是 tux.fan os-kernel 标签下三篇 Linux 内核内存管理文章之一。

– [Linux memory cgroup v2:内核真的执行这个预算](https://tux.fan/zh/2026/07/21/memory-cgroup-v2-zh/) —— 配套篇。PSI 的每 cgroup `memory.pressure` 必须在理解三档限额(`low` / `high` / `max`)和 per-memcg LRU 会计模型之后才有意义。
– [Linux 多代 LRU:mglru 在 2026 年的真实状态](https://tux.fan/zh/2026/07/21/mglru-page-reclaim-zh/) —— PSI 拿到回收回调的那个地方。这篇聊代际算法本身、跟 active/inactive 的对比,以及 2026 LSFMM+BPF 上正在发生的「这个算法该不该留」之争。

最后修改: 2026年7月21日

作者

评论

发表评论

您的邮箱地址不会被公开。