一、为什么需要感知资源压力?

在传统 Linux 运维中,系统 "变慢" 往往要等到用户投诉或故障发生后才被察觉。CPU/内存/IO 的饱和度可以通过 top、iostat 等工具观察,但这些工具只反映 瞬时状态,无法揭示 等待排队导致的延迟恶化。

Linux 4.20 引入的 PSI (Pressure Stall Information) 正是为了解决这个盲点:它能衡量 "多少任务因为等待某种资源而被阻塞了多久",提供了一个可量化、可感知(pollable)、可触发(triggerable)的资源压力指标。

二、PSI 核心概念

2.1 压力 = 时间 × 阻塞任务数

PSI 的核心思想是计算资源争夺的时间开销:

压力 = (总时间 - 实际工作时间) / 总时间

# 举例: 若 10 个任务在 10 秒内等待了 3 秒资源
# 压力 = (10 任务 × 10 秒 - 所有任务实际运行时间) / (10 × 10)
#      = 3 / 10 = 30%

输出的值范围是 0-100%,表示 "过去 X 秒内,至少有一个任务因该资源停滞的时间占比"。

2.2 三类资源指标

资源度量对象含义
CPU任务在运行队列中等待调度的时间CPU 饱和/过载
Memory任务因内存分配失败/回收阻塞的时间(包括 direct reclaim、swap)内存紧张/受限
IO任务因同步 IO 阻塞的时间存储带宽/延迟瓶颈

2.3 "some" vs "full" 压力

PSI 为每类资源提供两个维度:

  • some:至少有一个任务被阻塞的时间占比——反映 "是否有人受影响"
  • full:几乎所有 任务同时被阻塞的时间占比——反映 "系统全面停滞"
例:4 核 CPU,10 个 CPU 密集任务
  some avg10=80% → 过去 10 秒内平均有任务排队
  full avg10=20% → 过去 10 秒内平均 20% 时间全部 4 核心都在排队等待

CPU 压力: some=80%, full=20%
说明: 系统有排队但未全面受阻 → 还能运转,但延迟变高了

2.4 三个时间窗口

每类压力都提供 10秒、60秒、300秒 三个时间窗口:

  • avg10:10 秒均值——用于触发即时告警
  • avg60:1 分钟均值——用于趋势判断
  • avg300:5 分钟均值——用于容量规划

三、PSI 数据解读指南

压力值含义建议动作
< 5%基本无压力无需干预
5-20%轻度压力密切观察
20-50%中度压力考虑扩容或优化
> 50%重度压力必须立即处理

3.1 典型压力模式

# 模式 1:CPU 饱和
CPU:  some=85%, full=30%    → CPU 扩容或优化热路径
Mem:  some=2%,  full=0%     → 内存充足
IO:   some=5%,  full=0%     → IO 良好

# 模式 2:内存瓶颈
CPU:  some=30%, full=10%    → CPU 尚可
Mem:  some=75%, full=45%    → 内存严重不足!
IO:   some=20%, full=5%     → IO 因 swap 上升

# 模式 3:IO 瓶颈
CPU:  some=15%, full=5%     → CPU 空闲
Mem:  some=10%, full=3%     → 内存尚可
IO:   some=80%, full=60%    → IO 严重受限!升级存储或调整 IO 模式

四、PSI 内核实现原理

4.1 内核数据结构

// kernel/sched/psi.c
struct psi_group {
    /* 全局统计器——所有 CPU 共享 */
    u64 avg_total[NR_PSI_STATES];      // 原子累计
    u64 avg_last_update;               // 上次更新时间
    u64 avg_next_update;               // 下次更新时间

    /* 每 CPU 贡献器 */
    struct psi_group_cpu *pcpus;       // 每 CPU 本地累计
};

enum psi_states {
    PSI_IO_SOME = 0,
    PSI_IO_FULL,
    PSI_MEM_SOME,
    PSI_MEM_FULL,
    PSI_CPU_SOME,
    // 注意:CPU 没有 FULL(多核总有空闲)
    NR_PSI_STATES
};

4.2 采样与更新流程

PSI 采用 定时器 + 每 CPU 对比 的更新策略:

1. 每个 CPU 在内核态关键路径记录:
   - 从 "开始等待资源" → "获得资源" 的时间戳差值
   - 该时间差计入对应资源的 "停滞时间"

2. 定时器每 2 秒触发一次(定时窗口):
   - 收集所有 CPU 的停滞时间
   - 计算增量 delta = 本次采样 - 上次采样
   - 更新三个时间窗口的指数移动平均(EMA)

3. EMA 公式:
   avg = avg * exp(-Δt/period) + delta * (1 - exp(-Δt/period))
   
   其中 period = {10s, 60s, 300s}

4.3 任务状态追踪

PSI 通过 task_struct.flags 中的标志位追踪任务状态:

// include/linux/psi_types.h
#define TSK_IOWAIT     (1 << 0)    // 任务在等待 IO
#define TSK_MEM_SCHED  (1 << 1)    // 任务在内核内存分配路径中等待
#define TSK_RUNNING    (0)          // 任务正在运行

// 内核在调度点判断:
// - 任务进入等待 → 记录开始时间
// - 任务恢复运行 → 累计停滞时间到 per-CPU 计数器

五、PSI 用户态接口

5.1 procfs 全局接口

# 读取全局 PSI 压力
cat /proc/pressure/cpu
# 输出:
# some avg10=0.00 avg60=0.00 avg300=0.00 total=1234567
# full avg10=0.00 avg60=0.00 avg300=0.00 total=2345678

cat /proc/pressure/memory
# 输出:
# some avg10=5.23 avg60=12.34 avg300=8.56 total=9876543210
# full avg10=2.10 avg60=6.78 avg300=4.32 total=4567890123

cat /proc/pressure/io
# some avg10=1.00 avg60=3.00 avg300=5.00 total=1111111111
# full avg10=0.50 avg60=1.50 avg300=2.50 total=2222222222

5.2 cgroup v2 PSI 接口

# cgroup v2 中的 PSI 文件
cat /sys/fs/cgroup/mycontainer/cpu.pressure
cat /sys/fs/cgroup/mycontainer/memory.pressure
cat /sys/fs/cgroup/mycontainer/io.pressure

# —— cgroup v2 专属:可注册触发器 -- #
# 当压力超过阈值时唤醒等待进程
echo "some avg10=50" > /sys/fs/cgroup/mycontainer/cpu.pressure

5.3 Polling 感知接口(重点!)

cgroup v2 的 *.pressure 文件支持 poll(),可实现低延迟的压力感知:

// 示例:监听内存压力超过 50% 超过 10 秒
#include <poll.h>

int fd = open("/sys/fs/cgroup/myapp/memory.pressure", O_RDWR);

// 写入触发器:some 窗口,10s 内平均 ≥ 50%
write(fd, "some avg10=50", strlen("some avg10=50"));

// 等待触发
struct pollfd pfd = { .fd = fd, .events = POLLPRI | POLLERR };
poll(&pfd, 1, -1);  // 阻塞直到压力超阈值

// 触发后的动作:暂停任务、缩减缓存、触发扩容...

5.4 systemd 集成

systemd 2.45+ 自动生成 service 单元的 PSI 度量:

# 查看 service 压力
systemctl show nginx.service --property=MemoryPressureUSec,CPUPressureUSec,IOPressureUSec

# 在 unit 文件中设置压力限制:
# /etc/systemd/system/myapp.service.d/limits.conf
[Service]
MemoryMax=4G
IOReadBandwidthMax=/dev/sda 100M
# 压力限制——超过即触发 OOM-kill 或 CPU 节流
MemoryPressureThresholdSec=10s
CPUPressureThresholdSec=30s

六、生产级实践:监控集成

6.1 Prometheus 监控方案

# Node Exporter 已支持 PSI 指标
# 指标名:node_pressure_cpu_waiting_seconds_total
#         node_pressure_memory_stalled_seconds_total
#         node_pressure_io_stalled_seconds_total

# 压力百分比指标(更直观)
node_pressure_cpu_some_ratio    # 0-1 范围
node_pressure_memory_some_ratio
node_pressure_io_some_ratio

6.2 告警规则推荐

# Prometheus AlertManager 规则
groups:
  - name: psi_alerts
    rules:
      - alert: HighMemoryPressure
        expr: node_pressure_memory_some_ratio > 0.6
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "节点 {{ $labels.instance }} 内存压力超过 60% 持续 5 分钟"

      - alert: HighCPUPressure
        expr: node_pressure_cpu_some_ratio > 0.8
        for: 10m
        labels:
          severity: warning

      - alert: HighIOPressure
        expr: node_pressure_io_full_ratio > 0.5
        for: 3m
        labels:
          severity: critical

6.3 容器化场景最佳实践

在 Kubernetes/Docker 中,PSI 提供了全新的资源调优视角:

# 1. Limit 调优
# 症状:容器 PSI Mem some=60%,OOMKill 未触发但延迟激增
# 方案:提升 memory.limit_in_bytes 或优化应用内存使用

# 2. CPU 分配调优
# 症状:CPU some=85%,full=25%
# 方案:考虑增加 CPU request 或优化锁争用

# 3. IO 调度调优
# 症状:IO some=70%,full=5%
# 方案:检查是否 swap 导致间接 IO 压力,升级存储介质

# 4. 混合指标决策树
if mem_full > 40%:
    → 内存瓶颈 → 扩容内存 / 优化缓存 / 限流
elif cpu_full > 30%:
    → CPU 瓶颈 → 扩容核心 / 异步化 / 锁优化
elif io_full > 30%:
    → IO 瓶颈 → 升级SSD / 调整 IO 模式 / 限流
else:
    → 系统健康 → 继续观察

七、PSI vs 传统指标对比

指标PSI传统工具 (top/iostat)
度量维任务等待时间的时间占比资源使用率/饱和度
时间窗口10s/60s/300s 三窗口瞬时快照
预测价值avg10 高 → avg60 将更高(趋势)无法预测
可触发支持 poll() 阻塞等待需轮询
粒度可为每个 cgroup 度量通常只能全局
开销极低(每 CPU 计数器 + 定时器)根据采集频率而定

7.1 PSI 的局限

  • CPU 没有 full 指标(多核总有空闲核)
  • 压力高不一定是坏事(NUMA 本地争抢可能不影响总体吞吐)
  • 短时突发可能被 avg60 "稀释"
  • 需结合其他指标(内存用量、CPU 利用率)综合判断

八、PSI 内核调优参数

8.1 采样频率

# 默认每 2 秒采样一次(无需调整)
# 通过 /etc/sysctl.conf 不可配置
# 编译时可调整 PSI_FREQ (kernel/sched/psi.c)

8.2 根 cgroup PSI(旧系统)

# 旧内核可能需要在根 cgroup 启用 PSI
# 新内核默认启用

8.3 Trip 点计算

PSI 的 "触发器阈值" 是相对于时间窗口计算的:

例: "some avg10=50"
  → 过去 10 秒中至少有一个任务停滞的时间 ≥ 5 秒时触发
  
用例优先级:
  1. 即时告警:some avg10 > 70%  → 1 秒内响应
  2. 自动扩容:full avg60  > 40%  → 1 分钟内扩容
  3. 容量回顾:some avg300 > 20% → 需要中长期规划

九、实战调优案例

9.1 案例:Redis 内存压力感知

# 问题:Redis 在内存接近 limit 时响应延迟急剧上升
# 现象:PSI Mem some=65%,但系统内存用量仅 70%

# 原因分析:
# Redis 频繁触发 direct reclaim(分配时回收)
# → 每次分配都等待一小段时间 → 累计起来 avg10=65%
# 但整体内存尚未耗尽 → 传统指标未告警

# 解决方案:
# 1. 配置 vm.overcommit_memory = 2(禁止超额承诺)
# 2. 设置容器 memory limit = Redis 工作的 1.25 倍
# 3. 启用 THP(透明大页)减少 PTE 碎片
# 4. 通过 PSI 触发器在 mem_full > 30% 时主动淘汰缓存

9.2 案例:Kafka IO 压力诊断

# 消息吞吐下降但 CPU/内存正常
# PSI 数据:IO some=78%, full=42%
# iostat:%util=60%

# iostat 显示磁盘使用率 "仅60%"
# 但 PSI IO full=42% 说明几乎全部 IO 请求都在等待
# → 这是 IO 调度器不公平或设备队列深度不足导致的等待

# 调整方案:
# 1. echo deadline > /dev/sda/queue/scheduler
# 2. echo 256 > /dev/sda/queue/nr_requests
# 3. 若使用 NVMe,echo none > /dev/nvme0n1/queue/scheduler

十、总结

维度PSI 带来的变革
从被动到主动avg10 捕获早期信号 → 在宕机前行动
从整体到细粒度cgroup 级别 PSI → 精准定位مشكلة容器
从轮询到事件驱动pressure poll → 毫秒级响应,无空转
从使用率到体验感困惑于 "CPU 仅 70%" → 直接看到 "任务等了 80%"
从人工到自动化systemd + K8s + PSI → 自动伸缩与恢复

PSI 不只是一个内核特性,它是一种全新的资源健康管理范式。从 Kubernetes 到嵌入式系统,从数据库到实时应用,提供了一种统一的、低开销的、可编排的资源压力度量手段。在容器化和云原生时代,掌握 PSI = 掌握系统可观测性的下一程。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部