Linux Kernel PSI 深度解析:从内核压力信号到 AI 推理 QoS 与自动弹性伸缩
在生产环境中运行 AI 推理服务时,我们常面临一个困境:传统的 CPU 利用率、内存占用率等指标往往是"事后诸葛亮"——当指标恶化时,服务延迟已经飙升。Linux 内核 4.20 引入的 PSI(Pressure Stall Information)提供了一种全新的视角,它直接量化了任务因资源不足而被"卡住"的时间和程度,让我们能在延迟恶化之前就发现压力,做出主动的调度决策。
本文将深入 PSI 的内核实现机制,探讨如何利用 cgroup2 的 per-cgroup 压力视图,编写 BPF 程序实时跟踪压力信号,并构建一套基于 PSI 的 AI 推理自动弹性伸缩系统。
一、PSI 的设计哲学:为什么传统的指标不够
1.1 "利用率陷阱"
对于 AI 推理这类尾延迟敏感的服务,CPU 利用率 70% 并不意味着一切正常。GPU 推理请求的预处理阶段(Tokenize、张量布局变换)依赖 CPU,当大量推理请求同时涌入时,CPU 调度延迟可能在几毫秒内从亚微秒级暴增至数十毫秒。传统指标看到的是"CPU 还很闲",但 PSI 看到的是"有 40% 的任务在等待 CPU"。
传统指标:
CPU usage: 65% ← 看起来一切正常
Memory used: 8GB ← 还远没打满
PSI 指标:
cpu.some avg10=45% ← 45%的任务在等待CPU至少10秒
memory.full avg60=30% ← 30%的任务同时因内存等待
PSI 的核心洞察是:压力不是"用了多少",而是"等了多久"。它统计的是至少有一个任务因该资源不足而停滞的时间占比。
1.2 PSI 的三类资源
PSI 监控三类资源压力:
| 资源 | 含义 | 典型场景:AI 推理 |
|---|---|---|
| cpu | CPU 调度等待 | 预处理线程争抢、GPU 调度回调延迟 |
| memory | 内存回收/分配等待 | KV Cache 分配、OOM 前兆、模型加载 |
| io | 块设备 I/O 等待 | 模型权重加载、KV Cache Swap |
每类压力又分为两个维度:
- some:至少一个任务因该资源停滞的时间占比
- full:几乎所有可运行任务同时停滞的时间占比(full 是 some 的超集)
二、PSI 内核实现机制
2.1 核心数据结构
PSI 的实现位于 kernel/sched/psi.c,核心是 psi_group 结构:
struct psi_group {
/* 任务统计 */
struct task_group_stats *tasks; /* [PSI_NONISR] 各状态任务计数 */
/* 时间累积器 */
u64 avg_total[NR_PSI_STATES]; /* 总停滞时间 */
u64 avg_last_update; /* 上次采样时间戳 */
u64 avg[NR_PSI_STATES]; /* 输出给用户的滑动平均 */
/* 锁与定时器 */
struct mutex avgs_lock;
struct timer_list avg_timer; /* 周期性定时器,触发平均计算 */
/* ... */
};
每当任务状态变迁(try_to_wake_up、schedule 等路径),内核会调用 psi_task_change() 和 psi_task_switch() 更新各资源域的任务计数。
2.2 停滞时间计算
PSI 不逐任务计时,而是采样一群任务的全局状态:
/* psi.c 中的核心计算逻辑 */
void psi_account_irqtime(struct task_struct *task, u64 irqtime) { ... }
static u64 calc_timer_delay(enum psi_states state, ...) {
u64 now = cpu_clock(cpu);
/* 计算所有资源不足的任务的总停滞时间 */
u64 *total = group->tasks[state];
/* 更新滑动窗口平均值 */
for_each_possible_cpu(cpu)
aggregate(group->cpus[cpu], ...);
}
核心思想是:在内核采样时刻(psi_clock()),登记此刻因 CPU/内存/IO 不足而停滞的任务数,乘以采样间隔即为停滞时间增量。
2.3 滑动平均算法
PSI 使用指数加权移动平均(EWMA)算法来计算 avg10、avg60、avg300 三个窗口:
新平均 = 旧平均 × e^(-Δt/τ) + 当期停滞比例 × (1 - e^(-Δt/τ))
其中 τ 分别对应 10 秒、60 秒、300 秒。这意味着 avg10 对突发压力高度敏感(适合触发告警),而 avg300 更适合趋势决策(适合容量规划)。
内核在 psi_avgs_work() 中通过高精度定时器每 2 秒执行一次 EWMA 计算:
static void psi_avgs_work(struct work_struct *work) {
struct psi_group *group =
container_of(work, struct psi_group, avgs_work);
u64 now = sched_clock();
mutex_lock(&group->avgs_lock);
group->avg_last_update = now;
calculate_averages(group, now);
mutex_unlock(&group->avgs_lock);
}
三、用户态接口
3.1 procfs 接口
PSI 通过 /proc/pressure/ 暴露系统级压力:
$ cat /proc/pressure/cpu
some avg10=2.04 avg60=4.64 avg300=1.65 total=123456789
$ cat /proc/pressure/memory
some avg10=0.50 avg60=1.20 avg300=0.30 total=98765432
full avg10=0.30 avg60=0.80 avg300=0.10 total=54321000
$ cat /proc/pressure/io
some avg10=0.00 avg60=0.10 avg300=0.05 total=12345678
每行格式为:部分聚合 窗口平均=百分比 总累积微秒。total 字段表示系统启动以来该资源总停滞时间,适合做差值计算。
3.2 轮询与事件通知
PSI 文件支持 poll() 和 select(),可以在压力超过阈值时触发事件,实现"事件驱动"的压力监控,避免频繁轮询:
int fd = open("/proc/pressure/memory", O_RDONLY);
struct pollfd pfd = { .fd = fd, .events = POLLPRI | POLLERR };
// 设置压力阈值(通过 write 写入)
// 格式:<资源> <窗口秒> <阈值微秒>
dprintf(fd, "some 60 1000000"); // 60秒窗口停滞超过1秒时通知
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret > 0 && (pfd.revents & POLLPRI)) {
// 压力超阈值!
trigger_auto_scaling();
}
}
3.3 cgroup2 per-cgroup 压力视图
对于容器化 AI 服务,更精确的方式是 per-cgroup PSI。cgroup2 在组内 memory.pressure、cpu.pressure、io.pressure 中暴露了各容器的压力值:
# 查看某命名空间的内存压力
$ cat /sys/fs/cgroup/system.slice/ai-inference/memory.pressure
some avg10=2.04 avg60=4.64 avg300=1.65 total=123456789
full avg10=0.30 avg60=0.80 avg300=0.10 total=54321000
这意味着你可以看到"推理服务 A 在内存压力下挣扎,而训练任务 B 一清二白",精准判断谁是噪音邻居(noisy neighbor)。
四、BPF 程序跟踪 PSI 事件
4.1 BPF tracepoints
内核为 PSI 暴露了 tracepoints,BPF 程序可以直接钩取压力更新事件:
tracepoint/psi/psi_memstall_enter // 进入内存停滞
tracepoint/psi/psi_memstall_leave // 退出内存停滞
tracepoint/psi/psi_ioenter // 进入 IO 停滞
tracepoint/psi/psi_ioleave // 退出 IO 停滞
tracepoint/psi/psi_irqtime // IRQ 时间统计
4.2 BPF 实时压力监控程序
编写 BPF 程序追踪推理进程的内存停滞事件:
# psi_monitor.py (BCC 风格伪代码)
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct stall_event {
u64 delta_us;
u32 pid;
char comm[TASK_COMM_LEN];
u8 resource; // 0=cpu, 1=memory, 2=io
};
BPF_HASH(start_info, u32, u64);
BPF_PERF_OUTPUT(stall_events);
TRACEPOINT_PROBE(psi, memstall_enter) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 只跟踪推理服务进程
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
if (comm[0] != 'i' || comm[1] != 'n' || comm[2] != 'f')
return 0; // 过滤
u64 ts = bpf_ktime_get_ns();
start_info.update(&pid, &ts);
return 0;
}
TRACEPOINT_PROBE(psi, memstall_leave) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *tsp = start_info.lookup(&pid);
if (!tsp) return 0;
u64 delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
// 只报告超过 100 微秒的停滞
if (delta_us < 100) return 0;
struct stall_event e = {};
e.delta_us = delta_us;
e.pid = pid;
bpf_get_current_comm(&e.comm, sizeof(e.comm));
e.resource = 1;
stall_events.perf_submit(args, &e, sizeof(e));
start_info.delete(&pid);
return 0;
};
"""
b = BPF(text=bpf_text)
print("追踪 AI 推理服务的内存停滞事件...")
def print_event(cpu, data, size):
event = b["stall_events"].event(data)
print(f"PID={event.pid:6d} comm={event.comm.decode():16s} "
f"memstall={event.delta_us:8d} us")
b["stall_events"].open_perf_buffer(print_event)
while True:
b.perf_buffer_poll()
4.3 实际采样输出
在高负载推理场景下运行该 BPF 程序,可以看到:
PID=18452 comm=inference-serv memstall= 2840 us
PID=18453 comm=inference-serv memstall= 5210 us
PID=18452 comm=inference-serv memstall= 12300 us ← 接近超时阈值
PID=18454 comm=inference-serv memstall= 960 us
这些微秒级的停滞正是 GPU 推理请求等待 CPU 侧预处理完成时产生的。通过 delta_us 分布直方图(BPF_HISTOGRAM),你可以确定 P99 延迟峰值与内存停滞的强相关性。
五、基于 PSI 的 AI 推理自动弹性伸缩
5.1 伸缩决策框架
传统方案依赖请求队列深度或 QPS,但这些是结果的体现。PSI 是压力信号的早期指标,能提前 30-60 秒预警:
请求数 ↑ → CPU 停滞增加(10s 后 avg10 上升)→ 最终队列积压、超时
↑
在这里主动扩容!
5.2 决策矩阵
| 压力信号 | 持续时间 | 建议动作 |
|---|---|---|
| cpu.some avg10 > 30% | > 30s | 扩容 +1 实例 |
| cpu.full avg10 > 10% | > 15s | 紧急扩容 +2 实例 |
| memory.some avg60 > 5% | > 60s | 检查 KV Cache 配置 |
| memory.full avg10 > 10% | > 10s | OOM 预警,立刻扩容 + 降负载压力 |
| io.some avg10 > 20% | > 30s | 模型加载慢,预热后重启 |
5.3 伸缩器原型实现
# psi_autoscaler.py - 基于 PSI 的推理服务弹性伸缩器
import time
import os
import subprocess
class PSIAutoScaler:
def __init__(self,
cgroup_path: str,
thresholds: dict,
scale_up_step: int = 1,
cooldown: int = 60):
self.cgroup = cgroup_path
self.thresholds = thresholds # {'cpu_some_10': 30.0, ...}
self.step = scale_up_step
self.cooldown = cooldown
self.last_scale = 0
self.consecutive_triggers = 0
def read_psi(self, resource: str = 'cpu') -> dict:
"""读取 cgroup PSI 数据"""
path = os.path.join(self.cgroup, f'{resource}.pressure')
with open(path) as f:
line = f.readline() # some 行
parts = line.split()
result = {}
for p in parts[1:]:
k, v = p.split('=')
result[k] = float(v)
return result
def should_scale_up(self) -> bool:
"""判断是否应扩容"""
cpu = self.read_psi('cpu')
mem = self.read_psi('memory')
# CPU 调度停滞:avg10 > 30% 持续 3 个周期
cpu_trigger = cpu.get('avg10', 0) > self.thresholds.get('cpu_some_10', 30)
# 内存停滞:avg60 > 5%
mem_trigger = mem.get('avg60', 0) > self.thresholds.get('mem_some_60', 5)
return cpu_trigger or mem_trigger
def scale_up(self, target_name: str = 'ai-inference'):
"""执行扩容"""
now = time.time()
if now - self.last_scale < self.cooldown:
print(f"[冷却中] 距上次扩容 {int(now - self.last_scale)}s")
return
self.consecutive_triggers += 1
if self.consecutive_triggers < 3:
print(f"[触发计数 {self.consecutive_triggers}/3] 压力上升中,等待确认...")
return
replicas = self._get_current_replicas(target_name) + self.step
print(f"[执行扩容] {target_name}: {replicas - self.step} → {replicas}")
subprocess.run(['kubectl', 'scale', f'deployment/{target_name}',
f'--replicas={replicas}'], check=True)
self.last_scale = now
self.consecutive_triggers = 0
def run(self, interval: int = 10):
"""主循环"""
print(f"PSI Autoscaler 开始监控 {self.cgroup}")
while True:
try:
if self.should_scale_up():
self.scale_up()
else:
self.consecutive_triggers = 0
except Exception as e:
print(f"[错误] {e}")
time.sleep(interval)
# 实际启动
if __name__ == '__main__':
scaler = PSIAutoScaler(
cgroup_path='/sys/fs/cgroup/system.slice/ai-inference',
thresholds={
'cpu_some_10': 30.0,
'cpu_full_10': 10.0,
'mem_some_60': 5.0,
},
cooldown=60
)
scaler.run(interval=10)
5.4 反抖动策略
仅凭平均值决策很容易引起抖动,关键的三层保护:
- 连续触发确认:连续 N 个周期(如 3 次)压力超标才执行
- 冷却期:扩容后 60 秒内不再扩容(让新实例稳定)
- 平局滤波:
avg10> 阈值且avg60也 > 阈值时才行动 - 使用
nostat+ 关掉psi=1(内核启动参数,关闭 PSI 统计) - 对延迟极度敏感的服务,可关闭 PSI:
psi=0 - 大多数场景下 PSI 开销 < 0.1%,无需关闭
- 纯 GPU 计算瓶颈:推理延迟由 GPU kernel 运行时间主导,CPU/IO 停滞很少
- 网络带宽饱和:PSI 不监控网络带宽压力
- 冷启动延迟:PSI 反映的是持续压力,不覆盖冷启动拉镜像等一次性事件
- 全量开启 PSI,通过 Grafana 建立压力仪表盘
- 为推理服务单独设置 cgroup,独立观察 PSI
- 使用 avg10 + avg60 双窗口确认策略避免误扩
- 实现 BPF 探针收集停滞时间 P99 分布,作为延迟根因分析的佐证
- 与现有 Kubernetes HPA 互补:HPA 处理 QPS 维度,PSI Autoscaler 处理内核资源压力维度
avg10 > 30% AND avg60 > 15% → 确认扩容
avg10 > 30% BUT avg60 < 15% → 观望(可能是突发尖刺)
六、生产部署实战
6.1 容器内 PSI 可见性
要让容器能看到自己的压力信号,确保:
# cgroup v2 + cgroup.subtree_control 包含 cpu memory io
securityContext:
# 不要做无意义的 seccomp 过滤阻断 PSI 读取
# 在容器内验证
cat /proc/pressure/cpu # 宿主机全局压力
cat /sys/fs/cgroup/cpu.pressure # 本容器 cgroup 的压力
注意:容器默认只看到其所在 cgroup 的压力(per-cgroup PSI),看不到全局压力。这也是优势——你看到的是自身"切蛋糕"大小的后果。
6.2 与 systemd 集成
systemd 自 245 起原生支持 PSI 阈值:
# /etc/systemd/system/ai-inference.service
[Service]
MemoryPressureWatch=on
MemoryPressureThresholdSec=5s # 内存压力超5秒触发通知
CPUPressureWatch=on
CPUPressureThresholdSec=10s
systemd 通过 sd-bus 发出 org.freedesktop.systemd1.Service.PressureWatch 事件,可以配合 systemctl show --property=MemoryPressureUSec 查询。
6.3 记录历史数据(Prometheus + node_exporter)
node_exporter 自 1.5 起暴露 PSI 指标:
# prometheus 监控规则
- alert: AIInferenceHighMemoryPressure
expr: node_psi_memory_seconds{job="ai-inference"} > 5
for: 1m
labels:
severity: warning
annotations:
summary: "推理实例 {{ $labels.instance }} 内存压力异常"
- alert: AIInferenceCPUPressure
expr: rate(node_psi_cpu_seconds{job="ai-inference"}[5m]) > 0.3
for: 3m
labels:
severity: critical
6.4 eBPF 热路径优化
对于超高 QPS 推理服务,频繁的 PSI 统计本身可能引入微妙开销。建议:
七、与传统方案的对比与取舍
| 维度 | PSI 方案 | 传统方案(QPS/CPU) |
|---|---|---|
| 预警提前量 | 30-60 秒 | 几乎为 0 |
| 噪音邻居感知 | 直接量化 | 间接推断 |
| 尾延迟相关性 | 强(直接测量停滞) | 弱(仅看均值) |
| 部署复杂度 | 中(需内核 4.20+) | 低 |
| 可解释性 | 需要团队学习 | 全员熟悉 |
| 适用场景 | QoS 敏感、弹性伸缩 | 简单阈值告警 |
7.1 不依赖 PSI 的场景
PSI 不是万能的。以下场景它帮助不大:
八、总结与展望
PSI 为 AI 推理服务提供了一种全新的压力感知范式:从"看结果"转向"看原因"。通过 per-cgroup 压力视图,你可以精准定位噪音邻居;通过 BPF 追踪,你可以关联微秒级停滞与尾延迟毛刺;通过事件驱动的伸缩器,你可以提前扩容而不是被动救火。
生产建议总结:
随着 CXL 内存分层和 CCF(云原生基础设施)的成熟,PSI 将在 AI 推理的边缘计算、Serverless 场景中发挥更大价值——毕竟在那些场景下,"资源紧张导致尾延迟飙升"正是最大的敌人。

发表评论 取消回复