PELT 与 cgroup v2 压力反馈:Linux 内核资源竞争监测的工程实践
引言
在大型生产环境中,"系统变慢"往往是最令人头疼的故障表现。CPU 被争抢?内存回收跟不上了?还是磁盘 IO 成为了瓶颈?Linux 内核提供了多种底层机制来量化资源竞争,其中 PELT(Per-Entity Load Tracking) 和 cgroup v2 压力反馈(Pressure Stall Information, PSI) 是最核心的两个子系统。它们不仅能回答"到底慢在哪",还能让应用层主动感知资源压力,做出动态降级、弹性伸缩等智能决策。
本文将深入 PELT 和 PSI 的内核实现原理,分析其设计取舍与数值含义,并展示如何在实际工程中利用它们构建可观测的弹性系统。
一、PELT:从全局负载到单实体负载追踪
1.1 为什么需要 Per-Entity 负载追踪
传统的 CPU 负载指标(loadavg)只有一个全局数值,无法区分是哪个进程或 cgroup 导致的负载。在容器化和多租户场景下,这种粗粒度指标几乎无用。PELT 最早由 Linux 3.8 引入,旨在为每个调度实体(sched entity)—— 即单个进程、进程组(task_group)或 cgroup —— 独立计算其负载贡献。
1.2 指数移动加权平均:衰减与累积的平衡
PELT 的核心是一个Exponential Moving Average(EMA)滤波器,用Y值近似代表1分钟、5分钟、15分钟负载。每个实体的负载贡献按如下递归公式更新:
load_avg = load_avg × decay + runnable × (1 - decay)
util_avg = util_avg × decay + running × (1 - decay)
其中 decay 因子由被称为"__accumulate_pelt_deltas()"的例程通过位移操作实现。内核预存了32个衰减周期(每1ms为一个周期)对应的乘数表 decay/load_avg,避免每次除法运算。每个实体的负载跟踪结构体(sched_avg)包含以下关键字段:
| 字段 | 含义 |
|---|---|
| last_update_time | 上次更新的时钟周期(clock_pelt) |
| load_sum | 可运行状态的累积时间 × NICE_0_LOAD |
| runnable_sum | 就绪或运行状态的总累积时间 |
| util_sum | 实际运行时的累积 CPU 时间 × Capacity Scale (SCHED_CAPACITY_SCALE) |
| load_avg | 归一化后的负载平均值 |
| util_avg | 归一化后的利用率平均值(0..1024) |
1.3 层级传播与频率缩放
PELT 的负载值在 cgroup 层级中自下而上传播。一个 cgroup 的 load_avg 等于其子实体(子进程或子 cgroup)的 load_avg 之和乘以一个衰减因子。这一设计让上层 cgroup 能看到整体负载,但随之带来了"层级衰减问题"——深层嵌套的 cgroup 负载会被反复衰减,导致上层看到的值偏低。
此外,PELT 的 util_avg 会乘以 cpu_capacity_orig / arch_scale_cpu_capacity() 来适配动态调频(DVFS)场景,确保一个在 1.6GHz 小核上跑满的线程不会比在 3.2GHz 大核上跑一半的线程看起来更忙。
二、PSI:压力停滞信息的内核实现
2.1 从"统计"到"停滞"
PELT 告诉你"实体有多忙",PSI 则更直接地告诉你"实体被资源阻塞了多久"。PSI 建立在 PELT 基础之上,但增加了一个关键视角:每个实体在资源争抢上的停滞时间(stall time)。
2.2 三种压力类型
PSI 监控三种资源维度:
- some:至少有一个任务在某个资源上停滞
- full:所有非空闲任务都在该资源上停滞
- avg:平均停滞程度,给出 10s/60s/300s 三个时间窗口
"some" 适合发现毛刺,"full" 适合发现严重瓶颈。例如 full avg10=25.6 表示过去 10 秒内,CPU 上所有任务平均有 25.6% 的时间在排队等待。
2.3 内核实现:中断窗口与触发器
PSI 的数据流动可以概括为:
psi_group 对象
├── tasks[NR_PSI_TASKS] // 跟踪的任务计数
├── avg[NR_PSI_AGGREGATORS] // 累积量,每 WINDOW 周期更新
├── total[PSI_POLL][...] // 单调累计,供用户态读取
└── polling // 是否由用户态轮询驱动
更新路径有两种触发方式:
- 时钟驱动:每 tick,内核通过
psi_task_tick()调用psi_avgs_work()周期性计算平均值。 - 事件驱动:任务进入/离开等待状态时调用
psi_task_change()实时更新 tasks 计数。
每个 CPU 上独立维护一个 cpu=.../avgX=.../total=... 快照,最终按 CPU 维度聚合,确保多核并发下的准确性。
2.4 等待队列与阻塞读取
PSI 的另一强大功能是为压力事件提供等待接口。内核的 psi_trigger_poll() 允许进程在压力超过阈值的资源上阻塞,直到压力回落。cgroup v2 memory /io pressure 文件支持 echo "some 80 2s" > memory.pressure 让内核在"some"压力超过 80%(2秒窗口内)时触发通知。
三、cgroup v2 压力反馈的实时链路
3.1 cgroup 与 PSI 的协作
cgroup v2 的 cpu.pressure、memory.pressure、io.pressure 三个接口直接映射到 PSI 的 psi_group 追踪。当一个 cgroup 的内存被限制到 memory.max 时,分配路径进入直接回收,任务因此被阻塞,PSI 立即累积 MEM_FULL 停滞时间。
3.2 活动通知与 epoll
cgroup v2 通过 cgroup.pressure 文件与 eventfd 结合实现事件驱动的压力通知:
// 监控 cgroup 的内存压力
fd = open("memory.pressure", O_WRONLY);
write(fd, "some 50 1s\0", 11); // some 50% 1秒窗口
efd = eventfd(0, EFD_CLOEXEC);
ctrl_fd = open("cgroup.pressure", O_WRONLY);
write(ctrl_fd, to_string(efd));
// 现在 epoll_wait(efd) 会在压力超过阈值时唤醒
这条路径完全在内核态完成,无需用户态轮询,响应时间可达微秒级。
四、实际工程:构建弹性伸缩的容器平台
4.1 基于 PSI 的自适应降级
以 Nginx 为例,当系统 CPU 达到 FULL avg10 > 60 时,后端服务应自动降低工作线程数以减少上下文开销。实现方式:
// 伪代码:基于 CPU full 压力的 worker 调控
const FULL_THRESHOLD = 60.0;
const READ_INTERVAL_MS = 5000;
setInterval(async () => {
const psi = await readCPUPressure(); // 读 /proc/pressure/cpu
const full10 = psi.cpu.some.avg10; // some avg10 对应 CPU 排队程度
if (full10 > FULL_THRESHOLD) {
workers.downscale();
} else if (full10 < FULL_THRESHOLD * 0.5) {
workers.upscale();
}
}, READ_INTERVAL_MS);
4.2 基于内存压力的 OOM 预防
传统 OOM Killer 是"事后补救",PSI 提供了"事前预警"。Kubernetes 的 Vertical Pod Autoscaler (VPA) 可以结合 PSI 的 memory full avg10 在 OOM 发生前主动重启容器或扩容。
// 容器级别的 OOM 预防
func monitorMemoryPressure(cgroupPath string, threshold float64) {
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
f, _ := os.Open(filepath.Join(cgroupPath, "memory.pressure"))
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "some") {
parts := strings.Fields(line)
avg10, _ := strconv.ParseFloat(parts[2][4:], 64)
if avg10 > threshold {
triggerEviction(cgroupPath) // 清理缓存、回收内存
}
}
}
f.Close()
}
}
4.3 IO 压力驱动的 QoS
在混合部署(OLTP + OLAP)中,OLAP 扫描操作可能导致 IO 争抢,拖慢 OLTP。通过监控 /sys/fs/cgroup/.../io.pressure 的 full avg60,OLAP 控制器可以在检测到存储争抢时主动降低 IO 并发。
// Doris 扫描节点 IO 自适应限流
func adaptiveIOScan(scanner *Scanner, ctrlGroup string) {
for {
psi := readPSI(ctrlGroup + "/io.pressure")
if psi.full.avg60 > 40.0 {
scanner.SetConcurrency(scanner.Concurrency() / 2)
} else if psi.full.avg60 < 10.0 {
scanner.SetConcurrency(min(scanner.Concurrency()*2, MaxConcurrency))
}
time.Sleep(5 * time.Second)
}
}
五、性能考量与调优
5.1 PELT 层级衰减的应对
如前所述,PELT 的负载值在 cgroup 层级传播中会自然衰减。对于深度嵌套的 cgroup 容器平台,建议以下措施:
- 限制 cgroup 层级深度不超过 5 层
- 利用
cgroup.type的 threaded 模式让子 cgroup 共享父级 PELT 时钟 - 结合
sched.stat的实体级负载作为补充
5.2 PSI 采样的开销
启用 PSI 后,每次调度器 tick 都需要检查任务状态变化。基准测试表明,PSI 带来的额外开销通常小于 0.3% CPU,对于大多数生产负载可以忽略不计。但需要注意:
- 开启
psi=1内核参数(开启后不可关闭,需重启) - 对于存在数万个 cgroup 的场景,PSI 的内存占用为每 cgroup ~64 字节
5.3 窗口选择的工程经验
| 场景 | 推荐窗口 | 用途 |
|---|---|---|
| 网络延迟敏感服务(L7 网关) | avg10 | 秒级毛刺检测 |
| OLTP 数据库 | avg60 | 渐进式磁盘 IO 降级 |
| 批处理任务队列 | avg300 | 稳定负载趋势观察 |
| OOM 应急响应 | avg10 (full) | 快速识别内存压力 |
六、与其他监控体系的集成
6.1 Prometheus + node_exporter
Linux 4.20+ 的 node_exporter 已内置 PSI 采集器。关键指标包括:
node_pressure_cpu_waiting_seconds_totalnode_pressure_memory_stalled_seconds_totalnode_pressure_io_stalled_seconds_total
6.2 BPF 增强观测
通过 eBPF 可以直接读取 psi_group->avg 字段,实现内核级的 PSI 监控,无需遍历 /sys 文件系统:
// BPF 程序:跟踪特定 cgroup 的压力变化
SEC("tp_btf/psi_task_change")
int handle_psi_change(struct bpf_raw_trap_args *ctx) {
struct task_struct *task = (struct task_struct *)ctx->args[0];
u32 cpu = bpf_get_smp_processor_id();
u64 *val = bpf_map_lookup_elem(&pressure_map, &cpu);
if (val) {
__sync_fetch_and_add(val, 1);
}
return 0;
}
6.3 内核源码中的 PELT/PSI 路径
关键文件一览:
kernel/sched/pelt.c:PELT 核心算法,包含衰减表计算与负载更新kernel/sched/pelt.h:内联函数与快速路径定义kernel/sched/stats.h:调度器向 PELT 数据的填充逻辑kernel/psi/psi.c:PSI 核心实现,包括 trigger 机制include/linux/psi_types.h:PSI 数据结构定义
七、前沿发展
7.1 Core Scheduling 与 PSI
Linux 5.14 引入的 Core Scheduling 机制让同一物理核上的 SMT 线程共享安全上下文。PSI 正在与 Core Scheduling 深度协同,未来可能出现"安全停滞"(因安全策略而非资源争抢导致的停滞)的独立统计维度。
7.2 MQ-Deadline 与 PSI 联动
blk-mq 调度器 MQ-Deadline 已在实验性支持中利用 PSI 的 io full 压力来动态调整队列深度,在"所有任务都阻塞"的场景下主动降低写入倍率。
7.3 容器编排的标准化
Kubernetes 社区正在推动 PSI 作为标准容器资源指标暴露,未来的 metrics.k8s.io API 可能包含 pressure 维度。
总结
PELT 和 PSI 代表了 Linux 内核在可观测性领域的两个重要演进方向:前者提供实体级的精确负载追踪,后者提供压力时间维度的阻塞量化。在云原生时代,这两个子系统已经从"调试工具"升级为"弹性基础设施"的基石。
对于系统工程师来说,深入理解 PELT 的衰减模型能让调度器决策更精准;理解 PSI 的事件驱动机制能让容器平台更敏感地响应资源争抢。将两者结合使用,构建自动降级、智能弹性、可靠自愈的现代 Linux 系统。

发表评论 取消回复