在生产环境中,我们经常面临一个棘手的困境:系统负载看起来一切正常,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
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部