一、引言:为什么需要 PSI 压力监控

在 Linux 内核的调度子系统中,CFS(Completely Fair Scheduler)作为默认的进程调度器,负责管理绝大多数非实时任务的 CPU 时间分配。然而,在多租户服务器、容器化部署和实时业务混合部署的场景下,仅仅知道 CPU 利用率是远远不够的。

PSI(Pressure Stall Information,压力停滞信息) 是 Linux 4.20 版本引入的一项关键内核特性,它提供了比传统负载平均值(loadavg)更细粒度的资源压力度量。与 loadavg 只能告诉你"系统有多忙"不同,PSI 能够精确回答"任务因为等待资源而停滞了多长时间"这一核心问题。

在实际生产环境中,我见过太多因缺乏压力感知导致的故障:电商大促时数据库响应延迟飙升却找不到根因,容器集群中某个服务突然卡顿但 CPU 使用率并不高,或者 IO 密集型任务拖垮整个节点。PSI 正是解决这类问题的利器——它直接测量任务因 CPU、内存或 IO 资源不足而处于不可运行状态的累积时间。

对于内核开发者、系统运维工程师和性能调优专家而言,理解 PSI 在 CFS 中的实现机制,特别是 psi_enqueuepsi_dequeue 这两个关键函数,是掌握 Linux 调度子系统的必经之路。本文将从源码层面深入剖析,结合实战案例,帮助读者建立完整的 PSI 知识体系。

二、核心概念解析

2.1 PSI 的三类压力指标

PSI 框架监控三类资源压力,每类又分为"部分压力"(some)和"全部压力"(full)两个维度:

资源类型 指标名称 含义说明
CPU cpu.some 至少一个任务因 CPU 不足而等待的时间比例
CPU cpu.full 所有非空闲任务因 CPU 不足而等待的时间比例(通常为0,因为 CPU 可抢占)
Memory memory.some 至少一个任务因内存不足而等待的时间比例(如 thrashing、refaulting)
Memory memory.full 所有任务因内存不足而等待的时间比例(如 OOM、直接回收)
IO io.some 至少一个任务因 IO 不足而等待的时间比例
IO io.full 所有任务因 IO 不足而等待的时间比例

2.2 CFS 中的任务状态流转

在 CFS 调度器中,任务(task_struct)在运行队列(rq)中的状态变化遵循严格的生命周期:

TASK_RUNNING (可运行状态)
    ↓
enqueue_task_fair() → psi_enqueue() [加入就绪队列]
    ↓
pick_next_task_fair() → 实际获得 CPU
    ↓
dequeue_task_fair() → psi_dequeue() [离开就绪队列]
    ↓
TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE (睡眠状态)

PSI 的核心逻辑就嵌入在 enqueuedequeue 这两个钩子函数中,通过跟踪任务在就绪队列中的等待时间来计算压力值。

2.3 关键数据结构

// include/linux/psi_types.h
struct psi_group {
    // 聚合的压力事件计数器
    struct psi_group_cpu __percpu *pcpu;
    
    // 每个状态的累积时间(ns)
    u64 times[NR_PSI_STATES];
    
    // 当前正在等待的任务数
    unsigned int nr_tasks[NR_PSI_STATES];
    
    // 上一次更新时间
    u64 last_update;
};

// 任务级别的 PSI 状态
enum psi_task_status {
    PSI_NONE = 0,      // 无压力
    PSI_IO_WAIT,       // 等待 IO
    PSI_MEM_WAIT,      // 等待内存
    PSI_NONIDLE,       // 非空闲运行
};

三、环境准备

3.1 硬件与软件要求

最低配置:

  • x86_64 或 ARM64 架构的物理机/虚拟机

  • Linux 内核版本 ≥ 4.20(推荐 5.4+ 以获得完整 cgroup v2 支持)

  • 内存 ≥ 2GB(用于内存压力测试)

推荐环境:

  • Ubuntu 22.04 LTS 或 RHEL 8/9

  • 内核版本 5.15 或 6.x 系列

  • 开启 cgroup v2(systemd.unified_cgroup_hierarchy=1

3.2 内核配置检查

# 检查 PSI 是否编译进内核
$ grep CONFIG_PSI /boot/config-$(uname -r)
CONFIG_PSI=y

# 检查 cgroup v2 挂载情况
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

# 检查 PSI 接口是否存在
$ ls /proc/pressure/
cpu  io  memory

3.3 开发工具安装

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y linux-headers-$(uname -r) \
    build-essential git vim bpfcc-tools \
    trace-cmd kernelshark sysstat

# 安装 bpftool 用于 eBPF 分析
sudo apt-get install -y linux-tools-common linux-tools-generic

# 验证安装
$ bpftool --version
bpftool v5.15.0

3.4 内核源码获取(用于源码分析)

# 下载与当前内核匹配的源码
VERSION=$(uname -r | cut -d'-' -f1)
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${VERSION}.tar.xz
tar -xf linux-${VERSION}.tar.xz
cd linux-${VERSION}

# 或者直接克隆主线
git clone --depth 1 --branch v6.1 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

四、应用场景:云原生环境下的资源隔离与 SLO 保障

在 Kubernetes 集群中,我们曾遇到一个典型的生产问题:某在线推理服务(延迟敏感)与离线数据分析任务(批处理)混部在同一节点。尽管我们为推理服务设置了 CPU limit,但在离线任务启动后,推理服务的 P99 延迟从 50ms 飙升至 800ms。

通过传统监控(top、vmstat)只能看到 CPU 使用率 80%,无法解释延迟抖动。引入 PSI 后,我们发现 cpu.some 在离线任务启动时达到 95%,意味着推理任务 95% 的时间都在等待 CPU。基于这一数据,我们实施了动态资源调整:当 cpu.some > 30% 且持续 5 秒时,自动将离线任务迁移到负载较低的节点。

这一方案的核心正是依赖 CFS 中的 PSI 采集机制——每个容器 cgroup 的 cpu.pressure 文件实时反映其任务在 CFS 就绪队列中的等待压力。通过理解 psi_enqueue/dequeue 的实现,我们能够编写更精准的监控脚本,设计基于压力触发的自动扩缩容策略,甚至在自定义调度器插件中集成 PSI 数据,实现真正的负载感知调度。

五、源码剖析与实战案例

5.1 psi_enqueue 的实现机制

当任务被加入 CFS 就绪队列时,psi_enqueue 负责记录其进入等待状态的时间戳,并更新 PSI 组的计数器。

源码位置: kernel/sched/psi.c

void psi_enqueue(struct task_struct *p, bool wakeup)
{
    struct psi_group *group;
    u64 now = sched_clock();
    
    // 获取任务所属的 PSI 组(通常是 task_group 或 cgroup)
    group = psi_task_group(p);
    if (!group)
        return;
    
    // 加锁保护 PSI 状态更新
    psi_group_lock(group);
    
    // 记录任务进入就绪队列的时间
    p->psi_flags |= TSK_ON_CPU;
    p->sched_psi_wake = now;
    
    // 更新该组的非空闲任务计数
    if (p->psi_flags & TSK_NONIDLE)
        group->nr_tasks[PSI_NONIDLE]++;
    
    // 如果这是第一个等待的任务,开始记录压力时间窗口
    if (group->nr_tasks[PSI_NONIDLE] == 1)
        group->poll_states |= PSI_NONIDLE;
    
    psi_group_unlock(group);
    
    // 更新全局统计(每 CPU 聚合)
    psi_update_stats(group, now);
}

关键点解析:

  1. sched_clock() 获取高精度纳秒级时间戳,确保压力计算的准确性

  2. TSK_ON_CPU 标记任务已进入就绪状态,但尚未实际运行

  3. poll_states 位图用于跟踪当前有哪些压力状态处于活跃状态

5.2 psi_dequeue 的实现机制

当任务离开就绪队列(无论是被调度执行还是进入睡眠),psi_dequeue 计算其在队列中的等待时间,并累加到对应的压力计数器。

void psi_dequeue(struct task_struct *p, bool sleep)
{
    struct psi_group *group;
    u64 now = sched_clock();
    u64 delta;
    
    group = psi_task_group(p);
    if (!group)
        return;
    
    psi_group_lock(group);
    
    // 清除就绪状态标记
    p->psi_flags &= ~TSK_ON_CPU;
    
    // 计算在就绪队列中的等待时间
    delta = now - p->sched_psi_wake;
    
    // 如果任务实际获得了 CPU 时间,减去运行时间
    if (!sleep && p->sched_psi_run)
        delta -= now - p->sched_psi_run;
    
    // 根据任务状态累加到对应的压力类型
    if (p->psi_flags & TSK_NONIDLE) {
        group->times[PSI_NONIDLE] += delta;
        group->nr_tasks[PSI_NONIDLE]--;
    }
    
    // 如果该状态的任务数归零,清除活跃标记
    if (group->nr_tasks[PSI_NONIDLE] == 0)
        group->poll_states &= ~PSI_NONIDLE;
    
    psi_group_unlock(group);
    
    // 触发可能的窗口刷新
    psi_update_stats(group, now);
}

5.3 实战案例一:监控脚本编写

基于 /proc/pressure 接口,我们可以编写实时监控脚本:

#!/bin/bash
# psi_monitor.sh - 实时监控 CPU/Memory/IO 压力
# 用法: ./psi_monitor.sh [采样间隔秒数]

INTERVAL=${1:-2}
LOG_FILE="/var/log/psi_monitor.log"

echo "=== PSI Pressure Monitor Started at $(date) ===" | tee -a $LOG_FILE
echo "采样间隔: ${INTERVAL}秒" | tee -a $LOG_FILE
echo "格式: some% full% (10秒/60秒/300秒窗口)" | tee -a $LOG_FILE
echo "" | tee -a $LOG_FILE

# 检查 PSI 支持
if [[ ! -f /proc/pressure/cpu ]]; then
    echo "错误: 内核不支持 PSI,需要 Linux 4.20+" >&2
    exit 1
fi

while true; do
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    
    # 读取 CPU 压力
    CPU_PSI=$(cat /proc/pressure/cpu)
    
    # 读取 Memory 压力
    MEM_PSI=$(cat /proc/pressure/memory)
    
    # 读取 IO 压力
    IO_PSI=$(cat /proc/pressure/io)
    
    # 解析并格式化输出
    CPU_SOME=$(echo $CPU_PSI | grep -oP 'some avg10=\K[0-9.]+')
    CPU_FULL=$(echo $CPU_PSI | grep -oP 'full avg10=\K[0-9.]+')
    
    MEM_SOME=$(echo $MEM_PSI | grep -oP 'some avg10=\K[0-9.]+')
    MEM_FULL=$(echo $MEM_PSI | grep -oP 'full avg10=\K[0-9.]+')
    
    IO_SOME=$(echo $IO_PSI | grep -oP 'some avg10=\K[0-9.]+')
    IO_FULL=$(echo $IO_PSI | grep -oP 'full avg10=\K[0-9.]+')
    
    # 彩色输出:压力高时显示红色
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    NC='\033[0m' # No Color
    
    CPU_COLOR=$GREEN
    if (( $(echo "$CPU_SOME > 30" | bc -l) )); then
        CPU_COLOR=$RED
    fi
    
    MEM_COLOR=$GREEN
    if (( $(echo "$MEM_SOME > 20" | bc -l) )); then
        MEM_COLOR=$RED
    fi
    
    IO_COLOR=$GREEN
    if (( $(echo "$IO_SOME > 40" | bc -l) )); then
        IO_COLOR=$RED
    fi
    
    printf "[%s] CPU: ${CPU_COLOR}%5.1f%%${NC} / %5.1f%% | MEM: ${MEM_COLOR}%5.1f%%${NC} / %5.1f%% | IO: ${IO_COLOR}%5.1f%%${NC} / %5.1f%%\n" \
        "$TIMESTAMP" "$CPU_SOME" "$CPU_FULL" "$MEM_SOME" "$MEM_FULL" "$IO_SOME" "$IO_FULL" | tee -a $LOG_FILE
    
    sleep $INTERVAL
done

运行效果:

=== PSI Pressure Monitor Started at Mon Apr 14 10:30:00 CST 2025 ===
采样间隔: 2秒
格式: some% full% (10秒/60秒/300秒窗口)

[2025-04-14 10:30:02] CPU:   5.2% /  0.0% | MEM:   0.1% /  0.0% | IO:  12.3% /  0.5%
[2025-04-14 10:30:04] CPU:   8.7% /  0.0% | MEM:   0.2% /  0.0% | IO:  45.2% /  2.1%
[2025-04-14 10:30:06] CPU:  35.4% /  0.1% | MEM:   1.5% /  0.0% | IO:  67.8% /  5.4%

5.4 实战案例二:使用 cgroup v2 监控容器压力

在现代容器环境中,我们更关注每个 cgroup 的压力而非全局压力:

#!/bin/bash
# container_psi_monitor.sh - 监控特定容器的 PSI

CONTAINER_NAME=${1:-"stress-test"}
CGROUP_PATH="/sys/fs/cgroup/system.slice/docker-${CONTAINER_NAME}.scope"

if [[ ! -d $CGROUP_PATH ]]; then
    # 尝试查找 docker cgroup 路径
    CGROUP_PATH=$(find /sys/fs/cgroup -name "*${CONTAINER_NAME}*" -type d | head -1)
fi

if [[ -z $CGROUP_PATH ]] || [[ ! -f "$CGROUP_PATH/cpu.pressure" ]]; then
    echo "错误: 找不到容器的 cgroup PSI 接口"
    echo "尝试路径: $CGROUP_PATH"
    exit 1
fi

echo "监控容器: $CONTAINER_NAME"
echo "Cgroup 路径: $CGROUP_PATH"
echo ""

while true; do
    echo "=== $(date) ==="
    
    # CPU 压力
    if [[ -f "$CGROUP_PATH/cpu.pressure" ]]; then
        echo "CPU Pressure:"
        cat "$CGROUP_PATH/cpu.pressure" | sed 's/^/  /'
    fi
    
    # Memory 压力
    if [[ -f "$CGROUP_PATH/memory.pressure" ]]; then
        echo "Memory Pressure:"
        cat "$CGROUP_PATH/memory.pressure" | sed 's/^/  /'
    fi
    
    # IO 压力
    if [[ -f "$CGROUP_PATH/io.pressure" ]]; then
        echo "IO Pressure:"
        cat "$CGROUP_PATH/io.pressure" | sed 's/^/  /'
    fi
    
    echo ""
    sleep 3
done

5.5 实战案例三:基于 eBPF 的 PSI 内核追踪

为了深入理解 psi_enqueue/dequeue 的调用时机,我们可以使用 eBPF 进行动态追踪:

#!/usr/bin/env python3
# psi_trace.py - 使用 eBPF 追踪 PSI 更新事件

from bcc import BPF
from time import strftime

# eBPF 程序代码
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/psi_types.h>

// 定义数据结构
struct psi_event {
    u64 timestamp;
    u32 pid;
    u32 tgid;
    char comm[16];
    u8 event_type;  // 0: enqueue, 1: dequeue
    u64 delta_ns;
};

BPF_PERF_OUTPUT(events);

// 追踪 psi_enqueue
int trace_psi_enqueue(struct pt_regs *ctx, struct task_struct *p, bool wakeup) {
    struct psi_event event = {};
    
    event.timestamp = bpf_ktime_get_ns();
    event.pid = p->pid;
    event.tgid = p->tgid;
    bpf_probe_read_kernel_str(&event.comm, sizeof(event.comm), p->comm);
    event.event_type = 0;
    event.delta_ns = 0;
    
    events.perf_submit(ctx, &event, sizeof(event));
    return 0;
}

// 追踪 psi_dequeue
int trace_psi_dequeue(struct pt_regs *ctx, struct task_struct *p, bool sleep) {
    struct psi_event event = {};
    
    event.timestamp = bpf_ktime_get_ns();
    event.pid = p->pid;
    event.tgid = p->tgid;
    bpf_probe_read_kernel_str(&event.comm, sizeof(event.comm), p->comm);
    event.event_type = 1;
    // delta 计算在 dequeue 中完成,这里简化处理
    event.delta_ns = 0;
    
    events.perf_submit(ctx, &event, sizeof(event));
    return 0;
}
"""

# 加载 BPF 程序
b = BPF(text=bpf_text)

# 附加 kprobe
b.attach_kprobe(event="psi_enqueue", fn_name="trace_psi_enqueue")
b.attach_kprobe(event="psi_dequeue", fn_name="trace_psi_dequeue")

print("开始追踪 PSI enqueue/dequeue 事件... (按 Ctrl+C 停止)")
print(f"{'TIME':<12} {'PID':<8} {'TGID':<8} {'EVENT':<10} {'COMM':<16}")
print("-" * 60)

# 处理事件
def print_event(cpu, data, size):
    event = b["events"].event(data)
    time_str = strftime("%H:%M:%S")
    event_str = "ENQUEUE" if event.event_type == 0 else "DEQUEUE"
    
    print(f"{time_str:<12} {event.pid:<8} {event.tgid:<8} "
          f"{event_str:<10} {event.comm.decode('utf-8', 'replace'):<16}")

b["events"].open_perf_buffer(print_event)

while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        print("\n停止追踪")
        break

运行前需要确保:

# 安装依赖
sudo apt-get install -y python3-bpfcc

# 或者使用 pip
pip3 install bcc

# 运行需要 root 权限
sudo python3 psi_trace.py

5.6 实战案例四:生成压力并验证 PSI 准确性

为了验证 PSI 数据的准确性,我们编写一个可控的压力生成器:

// psi_stress_test.c - 生成 CPU/Memory/IO 压力以验证 PSI
// 编译: gcc -o psi_stress_test psi_stress_test.c -O2 -lpthread

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>

#define NUM_THREADS 8
#define WORK_SIZE (1024 * 1024 * 100)  // 100MB 内存操作
#define IO_SIZE (1024 * 1024)          // 1MB IO 块

// 全局标志
volatile int stop_flag = 0;

// 获取当前时间(纳秒)
static inline unsigned long long get_ns() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}

// CPU 压力线程:密集计算
void* cpu_stress_thread(void* arg) {
    int id = *(int*)arg;
    unsigned long long start = get_ns();
    volatile double result = 0.0;
    
    printf("[Thread %d] CPU 压力线程启动\n", id);
    
    while (!stop_flag) {
        // 执行浮点运算消耗 CPU
        for (int i = 0; i < 1000000; i++) {
            result += i * 3.14159;
            if (result > 1000000000.0) result = 0.0;
        }
    }
    
    unsigned long long duration = get_ns() - start;
    printf("[Thread %d] CPU 压力结束,运行 %.2f 秒\n", 
           id, duration / 1000000000.0);
    return NULL;
}

// Memory 压力线程:频繁分配/释放导致 thrashing
void* mem_stress_thread(void* arg) {
    int id = *(int*)arg;
    printf("[Thread %d] Memory 压力线程启动\n", id);
    
    while (!stop_flag) {
        // 分配大内存并随机访问
        char* buffer = malloc(WORK_SIZE);
        if (!buffer) {
            perror("malloc failed");
            break;
        }
        
        // 随机写入,触发 page fault
        for (int i = 0; i < WORK_SIZE; i += 4096) {
            buffer[i] = (char)(i % 256);
        }
        
        // 立即释放,制造内存压力波动
        free(buffer);
        
        // 短暂休眠,避免 OOM killer
        usleep(10000);
    }
    
    printf("[Thread %d] Memory 压力结束\n", id);
    return NULL;
}

// IO 压力线程:同步随机 IO
void* io_stress_thread(void* arg) {
    int id = *(int*)arg;
    char filename[64];
    snprintf(filename, sizeof(filename), "/tmp/io_stress_%d.tmp", id);
    
    printf("[Thread %d] IO 压力线程启动 (文件: %s)\n", id, filename);
    
    // 创建测试文件
    int fd = open(filename, O_CREAT | O_RDWR | O_DIRECT, 0644);
    if (fd < 0) {
        perror("open failed");
        return NULL;
    }
    
    // 分配对齐的缓冲区
    char* buffer;
    posix_memalign((void**)&buffer, 4096, IO_SIZE);
    
    // 预分配文件空间
    fallocate(fd, 0, 0, IO_SIZE * 1000);
    
    while (!stop_flag) {
        // 随机位置同步写入
        off_t offset = (random() % 1000) * IO_SIZE;
        lseek(fd, offset, SEEK_SET);
        
        // 填充数据
        for (int i = 0; i < IO_SIZE; i += 4096) {
            memset(buffer + i, (char)(i % 256), 4096);
        }
        
        // 同步写入(确保实际 IO 操作)
        ssize_t written = write(fd, buffer, IO_SIZE);
        if (written < 0) {
            perror("write failed");
            break;
        }
        fsync(fd);
    }
    
    free(buffer);
    close(fd);
    unlink(filename);
    
    printf("[Thread %d] IO 压力结束\n", id);
    return NULL;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("用法: %s [cpu|mem|io|mixed] [持续时间秒数]\n", argv[0]);
        printf("示例: %s cpu 30  # 生成 CPU 压力 30 秒\n", argv[0]);
        return 1;
    }
    
    const char* mode = argv[1];
    int duration = (argc > 2) ? atoi(argv[2]) : 30;
    
    printf("=== PSI 压力测试 ===\n");
    printf("模式: %s, 持续时间: %d 秒\n", mode, duration);
    printf("线程数: %d\n", NUM_THREADS);
    printf("请在另一个终端运行: watch -n 1 cat /proc/pressure/*\n");
    printf("开始测试...\n\n");
    
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    
    // 根据模式创建线程
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        
        if (strcmp(mode, "cpu") == 0) {
            pthread_create(&threads[i], NULL, cpu_stress_thread, &thread_ids[i]);
        } else if (strcmp(mode, "mem") == 0) {
            pthread_create(&threads[i], NULL, mem_stress_thread, &thread_ids[i]);
        } else if (strcmp(mode, "io") == 0) {
            pthread_create(&threads[i], NULL, io_stress_thread, &thread_ids[i]);
        } else if (strcmp(mode, "mixed") == 0) {
            // 混合模式:部分 CPU,部分 IO
            if (i % 2 == 0) {
                pthread_create(&threads[i], NULL, cpu_stress_thread, &thread_ids[i]);
            } else {
                pthread_create(&threads[i], NULL, io_stress_thread, &thread_ids[i]);
            }
        } else {
            printf("未知模式: %s\n", mode);
            return 1;
        }
    }
    
    // 主线程等待
    sleep(duration);
    stop_flag = 1;
    
    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    
    printf("\n测试完成\n");
    return 0;
}

编译与运行:

# 编译
gcc -o psi_stress_test psi_stress_test.c -O2 -lpthread

# 终端 1:监控 PSI
watch -n 1 'cat /proc/pressure/cpu && echo "---" && cat /proc/pressure/memory && echo "---" && cat /proc/pressure/io'

# 终端 2:生成 CPU 压力(持续 60 秒)
sudo ./psi_stress_test cpu 60

# 终端 2:生成 Memory 压力
sudo ./psi_stress_test mem 60

# 终端 2:生成 IO 压力
sudo ./psi_stress_test io 60

六、常见问题与解答

Q1: 为什么我的系统 /proc/pressure 目录不存在?

A: 这通常意味着内核未启用 PSI 支持。检查方法:

# 检查内核配置
zcat /proc/config.gz | grep CONFIG_PSI 2>/dev/null || \
    grep CONFIG_PSI /boot/config-$(uname -r)

# 如果显示 CONFIG_PSI=n,需要重新编译内核并启用:
# General setup -> CPU/Task time and stats accounting -> Pressure stall information tracking

Q2: PSI 的 avg10/avg60/avg300 是如何计算的?

A: 这是指数加权移动平均(EWMA),类似于 CPU loadavg 的计算方式,但基于实际的压力时间比例。内核代码中通过 psi_update_avgs() 函数每 2 秒更新一次,使用以下公式:

// 简化的 EWMA 计算逻辑
avg = old_avg * decay_factor + new_sample * (1 - decay_factor);

其中 decay 因子分别为:

  • avg10: exp(-2/10) ≈ 0.8187

  • avg60: exp(-2/60) ≈ 0.9672

  • avg300: exp(-2/300) ≈ 0.9934

Q3: 为什么 cpu.full 几乎总是 0?

A: 这是符合预期的。CPU 资源是可抢占的,高优先级任务可以立即抢占低优先级任务,因此极少出现"所有任务同时等待 CPU"的情况。相比之下,memory.fullio.full 在资源耗尽时可能出现非零值。

Q4: 容器内的 PSI 数据与宿主机不一致?

A: 确保使用 cgroup v2 而非 v1。cgroup v1 的 PSI 支持不完整。检查并迁移:

# 检查当前 cgroup 版本
stat -fc %T /sys/fs/cgroup

# 如果是 tmpfs,说明是 v1;如果是 cgroup2fs,说明是 v2

# 临时切换到 v2(需要重启)
sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"

Q5: 如何调试 PSI 数据异常偏高?

A: 使用 ftrace 跟踪 PSI 更新:

# 启用 ftrace
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo > trace

# 设置过滤条件
echo psi_* > set_ftrace_filter
echo function > current_tracer

# 开始追踪
echo 1 > tracing_on

# 运行测试程序...
sleep 10

# 查看结果
echo 0 > tracing_on
cat trace | head -100

七、实践建议与最佳实践

7.1 监控阈值设置建议

基于生产环境经验,推荐以下告警阈值:

指标 警告阈值 严重阈值 处理建议
cpu.some 30% 70% 考虑扩容或优化任务优先级
memory.some 20% 50% 检查内存泄漏或增加内存
memory.full 1% 10% 立即处理,可能触发 OOM
io.some 40% 80% 优化 IO 模式或升级存储

7.2 与 systemd 集成实现自动干预

创建 systemd 服务,当压力过高时自动触发资源调整:

# /usr/local/bin/psi_auto_rescue.sh
#!/bin/bash

THRESHOLD_CPU=70
THRESHOLD_MEM=50

while true; do
    CPU_SOME=$(cat /proc/pressure/cpu | grep -oP 'some avg10=\K[0-9.]+')
    MEM_SOME=$(cat /proc/pressure/memory | grep -oP 'some avg10=\K[0-9.]+')
    
    if (( $(echo "$CPU_SOME > $THRESHOLD_CPU" | bc -l) )); then
        logger -t psi-monitor "CPU pressure $CPU_SOME% exceeds threshold, triggering rescue"
        # 降低非关键服务优先级
        systemctl kill --signal=SIGSTOP non-critical.service
        sleep 30
        systemctl kill --signal=SIGCONT non-critical.service
    fi
    
    if (( $(echo "$MEM_SOME > $THRESHOLD_MEM" | bc -l) )); then
        logger -t psi-monitor "Memory pressure $MEM_SOME% exceeds threshold, triggering OOM prevention"
        # 触发内存回收
        echo 3 > /proc/sys/vm/drop_caches
    fi
    
    sleep 10
done

7.3 调试技巧:确认 psi_enqueue 调用路径

在分析调度问题时,确认 PSI 钩子是否被调用至关重要:

# 使用 perf 查看 psi_enqueue 调用频率
sudo perf stat -e 'kprobes:psi_enqueue' -a sleep 10

# 或者查看内核日志中是否有 PSI 相关警告
dmesg | grep -i psi

7.4 性能优化:减少 PSI 开销

PSI 本身有一定的性能开销,在极端高并发场景(每秒百万级任务切换)可能显现。优化建议:

  1. 编译选项:确保内核使用 CONFIG_PSI_DEFAULT_DISABLED=n,避免动态开关开销

  2. 监控频率:用户态读取 /proc/pressure 的频率不宜过高(建议 ≥ 1秒)

  3. 选择性监控:对延迟不敏感的批处理任务,可考虑放入单独的 cgroup 并降低监控粒度

八、总结

通过本文的深入剖析,我们完整理解了 PSI 机制在 CFS 调度器中的实现原理。psi_enqueuepsi_dequeue 作为连接任务调度与压力监控的关键桥梁,通过精确跟踪任务在就绪队列中的等待时间,为系统提供了前所未有的资源压力可见性。

从源码角度看,PSI 的实现体现了 Linux 内核设计的精妙之处:通过 per-cpu 数据结构和精细的锁策略,在保证数据准确性的同时最小化性能开销;通过 cgroup 集成,实现了从系统级到容器级的多级压力监控。

在实战层面,PSI 数据已成为现代云原生运维的核心指标。无论是构建自动扩缩容策略、实现资源隔离保障 SLO,还是进行根因分析和容量规划,PSI 都提供了比传统指标更可靠的决策依据。

对于内核开发者而言,理解 PSI 的实现机制有助于在自定义调度器或资源控制器中正确集成压力反馈;对于应用开发者,基于 PSI 的应用程序可以实现自适应的资源使用策略;对于运维工程师,PSI 是保障服务稳定性的利器。

建议读者在实际环境中部署本文提供的监控脚本和压力测试工具,通过亲手实验加深理解。同时,关注内核邮件列表中 PSI 相关的讨论(如 PSI 与 BPF 的结合、新的压力预测算法等),保持对这一活跃领域的跟踪。

掌握 PSI,意味着掌握了 Linux 系统资源管理的"脉搏",这是每一位资深 Linux 工程师必备的技能。


参考资源:

  • Linux 内核源码:kernel/sched/psi.c, include/linux/psi_types.h

  • 内核文档:Documentation/accounting/psi.rst

  • LWN 文章:"Pressure stall information for CPU, memory, and IO" (2018)

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐