PSI 到底在测什么

Pressure Stall Information(PSI)在 Linux 4.20 合入(提交 e970d0aa0bb98a9d4b9a2e3887da44b79f7b46c8——Johannes Weiner 的 psi-tracking 系列)。它不是 load average,不是吞吐量,也不是「CPU 多忙」。PSI 只回答一个问题:
在这个作用域(system 或 cgroup)里,这段时间里有多少墙上时钟时间花在「至少有任务在等资源」上?
单位是「测量窗口内的毫秒数」——等得越多,数字越大。Linux 暴露三种 PSI 资源:cpu、some/io、some/memory、full/(仅 cgroup v2)。本文聊的是后面这俩——内存压力那一对。
somedata/memory(/proc/pressure/memory)统计「集合内任意任务因任意内存原因阻塞」的时间总和。”some” 意思是「集合里有任务得等」。full/memory(只 cgroup v2)意思是「同一瞬间集合里所有任务都在等」。你的工作负载有 32 个 worker,31 个在跑、1 个陷在回收里——some 会亮、full 仍是 0。32 个一起卡,俩都亮。
时序图看一眼就懂
想象一个 1 秒窗口。内核在每个 100ms 切片里累加「因内存原因被调度器阻塞的任务数」。最终输出:
some avg10=2.34 avg60=1.85 avg300=0.92 total=9182345
full avg10=0.00 avg60=0.00 avg300=0.00 total=0avg10/60/300 是 α=2/(window+1) 的指数移动平均,所以 avg10 对尖峰反应最快。total 是自开机以来累计的阻塞时间(微秒)。这是规范格式,工具链应该只消费这个。
内核在哪里算的

所有 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 给两种接口:
轮询——读一次,自维护状态
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% 卡住」是平凡成立的、没有信息量。
触发——让内核只在「有必要」时叫醒你
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 树向上传播。cgroup B/A/my-pod 卡了,所有开启了 PSI 的祖先(cgroup.subtree_control +memory 之后默认开)都会同步记账。memory.pressure 文件放在每个开启 PSI 的非叶子节点上。
读 leaf 的完整压力:
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 之后大多数发行版默认开)。
先确认内核真的有:
zgrep PSI /proc/config.gz
# CONFIG_PSI=y在容器里读——只要把 cgroup v2 层级挂进来就行:
# 特权调试容器内
cat $(mount -t cgroup2 | awk '{print $3}')/memory.pressureGo:
// 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,直接解格式:
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 工作单元:
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 — 触发式驱逐提示
# 5s 窗口内有 500ms 内存 stall → 驱逐
echo "some 500000 5000000" > /sys/fs/cgroup/canary/memory.pressure
# 阈值跨过去内核就会 pollwake 我们这本质就是 Kubernetes --eviction-pressure-transition-period 的机制,但非 K8s 工作负载也能自己跑。
模式 3 — 尾延迟调试
p99 飙升时,在飙升窗口内采样 /proc/pressure/memory。some 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 强制的硬限额。要拆开看:
# 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.maxmemory.current > memory.high 且 PSI 高,说明 cgroup 在软上限回收——内核在求工作负载还内存、但还不杀它。memory.current >= memory.max 且 PSI 还在涨,你离 OOM-kill 不远了。看 memory.events 的 low/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,不是POLLIN。select()/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 要
cgroup2mount。你在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.c和memcontrol.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 上正在发生的「这个算法该不该留」之争。
评论