在生产环境中,我们经常面临一个棘手的困境:系统负载看起来一切正常,CPU 使用率不过 50%,内存还有余量,但应用的响应时间却在飙升,用户体验急剧恶化。传统的 load average、CPU 使用率、内存占用率等指标,本质上都是"某个时间窗口内的平均忙碌程度",它们无法回答一个更关键的问题:有多少工作因为资源不足而被迫等待?
Linux 4.20 引入的 PSI (Pressure Stall Information) 正是为了解决这一盲区而设计的。它不是又一个"平均负载"变体,而是直接测量任务因等待 CPU、内存或 IO 而停滞的时间比例。本文将深入剖析 PSI 的内核实现机制,并展示如何将其应用于生产级可观测性体系。
一、从 Load Average 到 PSI:测量范式的转变
传统 Linux 运维依赖 load average 判断系统压力,但 load average 存在根本性缺陷。它统计的是处于 RUNNING 或 UNINTERRUPTIBLE 状态的任务数,混合了 CPU 等待和 IO 等待,且使用指数衰减平均 (Exponential Decay Average),无法区分"10 个任务因 CPU 满载等待"与"1 个任务因磁盘 IO 阻塞等待"这两种截然不同的场景。
PSI 的突破性在于直接量化"停滞时间"——即任务因资源不足而无法执行的时间占比。例如,PSI 报告内存压力 40% 意味着:在过去 60 秒窗口内,平均有 40% 的任务在等待内存分配完成。这种表述方式让压力指标具备了可直接操作的语义。
二、PSI 的双重维度:Some 与 Partial
PSI 为每个资源维度(CPU、内存、IO)提供两组指标:
- some:至少有一个任务在观测窗口内经历过停滞。例如
memory some 30%表示窗口内某个时刻至少有一个任务因内存压力停滞了 30% 的时间。 - partial:所有任务停滞时间的加权平均。例如
memory partial 5%表示所有任务平均有 5% 的时间因内存停滞。
这两者结合能有效区分两类场景:当 some 高而 partial 低,说明只有少数任务被严重阻塞,可能是特定进程的内存泄漏;当 some 和 partial 都高,则说明整个系统普遍遭受内存压力。
PSI 报告的时间窗口为 10 秒、60 秒和 300 秒(5 分钟),分别对应瞬时、短期和中长期趋势。
三、内核实现机制深度解析
3.1 数据结构:per-CPU 跟踪区域
PSI 的核心数据结构定义在 include/linux/psi_types.h 中:
struct psi_group {
/* 锁保护 */
struct mutex stat_lock;
/* 每个 CPU 的跟踪数据 */
struct psi_group_cpu *pcpu;
/* 累积统计 */
u64 total[PSI_NONIDLE][NR_PSI_STATES];
/* 更新时间戳 */
u64 avg_last_update;
u64 avg_total[NR_PSI_STATES];
};
PSI 为每个 cgroup 组、每个 CPU 维护独立的跟踪区域,避免跨 CPU 锁竞争。任务状态变化时通过 psi_task_change() 宏注入跟踪点:
static inline void psi_task_change(struct task_struct *task, int clear, int set)
{
if (!static_branch_unlikely(&psi_disabled))
__psi_task_change(task, clear, set);
}
当任务从 RUNNING 变为非 RUNNING 状态(或被唤醒),内核会自动更新对应 PSI 组的停滞时间统计。
3.2 滑动窗口与指数移动平均
PSI 使用滑动窗口结合 EMA (Exponential Moving Average) 计算压力值。窗口大小由内核配置定义:
#define PSI_FREQ_MAX_US (1 * USEC_PER_SEC)
#define WINDOW_10_SEC_US (10 * USEC_PER_SEC)
#define WINDOW_60_SEC_US (60 * USEC_PER_SEC)
#define WINDOW_300_SEC_US (300 * USEC_PER_SEC)
每次触发状态变化或定时器到期时,psi_avgs_work 后台工作队列执行聚合计算:
static void psi_avgs_work(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct psi_group *group = container_of(dwork, struct psi_group, avgs_work);
u64 now;
now = sched_clock();
group->avg_total + = now - group->avg_last_update;
/* 对每个状态计算各窗口的 EMA */
for (...) {
/* EMA = prev * (1 - ratio) + current * ratio */
u64 avg = calc_avgs(group, window_size);
}
group->avg_last_update = now;
}
3.3 CPU 压力的特殊处理
CPU 压力与其他维度不同:它追踪的是"可运行但因 CPU 满载无法获得时间片的任务"。内核通过调度器钩子 psi_sched_switch() 在上下文切换时采样:
static void psi_sched_switch(struct task_struct *prev, struct task_struct *next, bool sleep)
{
if (sleep) {
/* 任务自愿睡眠(IO 等待)不计算 CPU 压力 */
record_stat(prev, PSI_IO_SOME, 0);
record_stat(prev, PSI_MEM_SOME, 0);
}
/* CPU 压力的判定:任务处于 TASK_RUNNING 但无法被调度 */
}
这意味着 CPU PSI 指标排除了自愿 IO 等待,纯粹反映"工作想做但 CPU 不分配时间"的情况。
四、/proc/pressure 接口与 cgroup v2 集成
4.1 procfs 接口格式
读取 /proc/pressure/memory 输出:
some avg10=0.00 avg60=0.12 avg300=0.05 total=1234567
partial avg10=0.00 avg60=0.08 avg300=0.03 total=987654
其中:
avg10/60/300:过去 10/60/300 秒窗口的压力百分比total:自启动以来的累计停滞时间(微秒)
4.2 cgroup v2 的 PSI 挂载点
cgroup v2 在 /sys/fs/cgroup/ 下自动提供 cpu.pressure、memory.pressure、io.pressure 三个接口:
/sys/fs/cgroup/system.slice/memory.pressure
/sys/fs/cgroup/system.slice/io.pressure
通过 cgroup PSI,可以精确测量单个容器的资源压力,这是容器化环境可观测性的关键能力。例如,Kubernetes 结合 PSI 可以实现:
- 基于内存压力的驱逐决策(而非被动等待 OOM killer)
- 更精确的 HPA(水平自动伸缩)触发条件
- 容器 QoS 级别验证
五、生产环境应用实战
5.1 OOM 预测与内存压力分级响应
传统 OOM killer 是"最后一刻"的防御机制,此时系统往往已经严重 thrashing。PSI 可实现分级响应:
#!/bin/bash
# 内存压力分级响应脚本
read_some_avg60() {
awk '/some/ {for(i=1;i<=NF;i++) if($i ~ /avg60/) print $i}' /proc/pressure/memory | cut -d= -f2
}
while true; do
mem_pressure=$(read_some_avg60)
if (( $(echo "$mem_pressure > 80" | bc -l) )); then
# 紧急:触发核心服务缓存清理
sync && echo 3 > /proc/sys/vm/drop_caches
alert "CRITICAL: memory some avg60=${mem_pressure}%"
elif (( $(echo "$mem_pressure > 50" | bc -l) )); then
# 告警:暂停低优先级批处理任务
systemctl stop batch-low-priority
alert "WARNING: memory some avg60=${mem_pressure}%"
elif (( $(echo "$mem_pressure > 20" | bc -l) )); then
# 预警:记录并观察
log "MEMORY_PRESSURE: some avg60=${mem_pressure}%"
fi
sleep 10
done
5.2 IO 瓶颈的实时定位
当应用延迟增加时,PSI 可以快判断是 CPU 瓶颈还是 IO 瓶颈:
#!/usr/bin/env python3
"""PSI 诊断工具:快速定位性能瓶颈类型"""
import time
def parse_pressure(path: str) -> dict:
"""解析 /proc/pressure/ 文件"""
result = {}
with open(path) as f:
for line in f:
parts = line.strip().split()
result[parts[0]] = {
kv.split('=')[0]: float(kv.split('=')[1])
for kv in parts[1:]
}
return result
def diagnose():
cpu = parse_pressure('/proc/pressure/cpu')
mem = parse_pressure('/proc/pressure/memory')
io = parse_pressure('/proc/pressure/io')
cpu_some = cpu['some']['avg60']
mem_some = mem['some']['avg60']
io_some = io['some']['avg60']
print(f"压力诊断 (avg60):")
print(f" CPU some: {cpu_some:5.1f}%")
print(f" MEM some: {mem_some:5.1f}%")
print(f" IO some: {io_some:5.1f}%")
max_pressure = max(cpu_some, mem_some, io_some)
if max_pressure < 5:
print("结论: 系统无显著压力")
elif max_pressure == cpu_some:
print("结论: CPU 瓶颈 — 考虑扩容或优化计算密集型任务")
elif max_pressure == mem_some:
if io_some > 30:
print("结论: 内存压力导致 Swap thrashing — 检查内存泄漏或扩容内存")
else:
print("结论: 纯内存压力 — 检查大内存分配或泄漏")
else:
print("结论: IO 瓶颈 — 检查磁盘/网络 IO 使用率或慢设备")
5.3 自适应服务质量控制
PSI 最大的生产价值在于将"被动监控"转变为"主动控制"。以下是一个自适应 Nginx 流量控制的示例:
/* 基于内存压力的 Nginx 自适应模块 */
ngx_int_t adaptive_rate_control(ngx_connection_t *c) {
float mem_pressure = read_psi_avg60("/proc/pressure/memory", "some");
if (mem_pressure > 60.0) {
/* 高压:拒绝低优先级请求,返回 503 */
if (c->priority < PRIORITY_HIGH) {
return NGX_HTTP_SERVICE_UNAVAILABLE;
}
c->client_body_timeout = 5000; /* 缩短超时 */
} else if (mem_pressure > 30.0) {
/* 中压:降级响应(缩短 keepalive) */
c->keepalive_timeout = 15;
}
/* 低压:正常服务 */
return NGX_OK;
}
5.4 容器调度优化
Kubernetes 中结合 PSI 可实现"压力感知调度":
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-policy
data:
policy.cfg: |
{
"kind": "Policy",
"apiVersion": "v1",
"extenders": [{
"urlPrefix": "http://psi-scheduler.default.svc:8080",
"filterVerb": "filter",
"prioritizeVerb": "prioritize"
}]
}
调度器扩展服务读取各节点 PSI 指标,将 Pod 调度到压力最低的节点:
def prioritize(pod, nodes):
scores = []
for node in nodes:
psi = get_node_psi(node.name) # 通过 node-exporter 获取
cpu_score = 100 - psi.cpu_some_avg60
mem_score = 100 - psi.mem_some_avg60
io_score = 100 - psi.io_some_avg60
scores.append((node, 0.5*cpu_score + 0.3*mem_score + 0.2*io_score))
return sorted(scores, key=lambda x: x[1], reverse=True)
六、PSI 与 eBPF 深度集成
BPF 可以直接读取 PSI 指标并触发实时响应,无需用户态轮询。
6.1 BPF 程序读取 PSI 状态
/* psi_monitor.bpf.c */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
/* 跟踪内存压力变化 */
SEC("kprobe/psi_avgs_work")
int BPF_PROBE(on_psi_update, struct psi_group *group) {
u64 mem_some_avg60 = 0;
u64 now = bpf_ktime_get_ns();
/* 读取 pressure some avg60 */
bpf_probe_read(&mem_some_avg60, sizeof(u64),
&group->avg[PSI_MEM_SOME][1]);
if (mem_some_avg60 * 100 / 100000 > 50) {
/* 内存压力超过 50%,触发事件 */
struct event e = {
.timestamp = now,
.type = EVENT_HIGH_MEM_PRESSURE,
.value = mem_some_avg60,
};
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
}
return 0;
}
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
6.2 BPF 与用户态联动
用户态通过 perf buffer 接收 BPF 事件并执行自动化响应:
/* 用户态处理 */
void handle_event(void *ctx, int cpu, void *data, __u32 size) {
struct event *e = data;
switch (e->type) {
case EVENT_HIGH_MEM_PRESSURE:
/* 自动收缩 Redis 缓存 */
http_post("http://localhost:8080/control/redis",
"{\"action\":\"trim\",\"percent\":20}");
break;
/* ... */
}
}
七、监控与告警最佳实践
7.1 Prometheus Exporter 配置
# psi_exporter 配置
collectors:
cpu:
windows: [10s, 60s, 300s]
metrics: [some, partial]
memory:
windows: [10s, 60s, 300s]
metrics: [some, partial]
io:
windows: [10s, 60s, 300s]
metrics: [some, partial]
cgroup_paths:
- /sys/fs/cgroup/system.slice/*.service
- /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice
7.2 关键告警规则
groups:
- name: psi_alerts
rules:
# 节点内存压力持续 5 分钟超过 50%
- alert: NodeHighMemoryPressure
expr: node_psi_memory_some_avg60 > 50
for: 2m
labels:
severity: warning
annotations:
summary: "节点 {{ $labels.instance }} 内存压力过高"
# IO 压力:区分纯 IO 压和 swap thrashing
- alert: HighIOSwapThrashing
expr: |
node_psi_io_some_avg60 > 60
and
node_psi_memory_some_avg60 > 40
for: 1m
labels:
severity: critical
annotations:
summary: "节点 {{ $labels.instance }} 可能出现 swap thrashing"
7.3 Grafana 面板核心面板
| 面板名称 | 指标 | 用途 |
|---|---|---|
| PSI Overview | some avg60 三资源对比 |
系统压力总览 |
| Memory Pressure Detail | some/partial avg10/60/300 |
内存压力趋势 |
| IO vs CPU Correlation | io_some vs cpu_some |
瓶颈类型判断 |
| Per-Container PSI | cgroup_memory_pressure |
容器级压力 |
八、PSI 的局限与注意事项
PSI 并非万能,需要注意以下局限:
1. 采样开销:PSI 在每次任务状态变化时执行 if (in_task()) 检查,对极高频率上下文切换(如数万 QPS 的 epoll 服务器)可带来 0.5%-1.5% 的额外开销。
2. 窗口大小刚性:当前内核只提供 10 秒/60 秒/300 秒三个窗口,无法自定义。对于需要秒级响应的场景(如高频交易系统),PSI 的粒度可能不够。
3. 累计值溢出:total 字段在长时间运行(数年)的系统中可能发生 u64 溢出,虽然不影响 avg 指标,但监控系统需要处理这一边界情况。
4. 虚拟化环境:PSI 在容器中读取的是宿主机层面的统计,无法准确反映 cgroup 内的真实压力。需要配合 cgroup v2 PSI 接口使用。
九、总结与展望
PSI 代表了 Linux 可观测性度量从"资源利用率"到"工作受影响程度"的重要范式转变。在云原生、容器化和微服务架构下,PSI 与 cgroup v2 的集成为容器级压力诊断提供了标准接口,与 eBPF 的结合使得实时响应成为可能。
PSI 的发展方向包括:更灵活的窗口配置(已在讨论中)、对 DPU/SmartNIC 卸载 IO 的感知、与 CXL 内存分层压力的集成。在超大规模数据中心中,基于 PSILevel 的全局调度决策系统正在成为研究热点。
关键参考:
- Linux 内核源码:
kernel/sched/psi.c - 文档:
Documentation/accounting/psi.rst - cgroup-v2 PSI:
Documentation/cgroup-v2.rst - 相关论文:"PSI: Pressure Stall Information for the Linux Kernel", Linux Plumbers Conference 2019

发表评论 取消回复