Linux PSI 指标
源码基于:Linux 5.4
0. 前言
当CPU、内存或IO设备被竞争时,工作负载将经历延迟峰值、吞吐量损失,并运行OOM终止的风险。如果没有对这种争用的精确度量,用户就被迫要么安全行事,要么不充分利用其硬件资源,要么孤注一掷,经常遭受由于过度承诺而导致的中断。
PSI(Pressure Stall Information)特性识别并量化这种资源紧张所造成的中断,以及它对复杂工作负载甚至整个系统的时间影响。对资源稀缺造成的生产力损失有一个准确的度量,可以帮助处于根据硬件调整负载或是根据工作负载需求提供硬件的用户。
当PSI 实时聚合上述这些信息,系统可以通过一些技术进行动态管理,例如减负荷、将作业迁移到其他系统或数据中心、战略性暂停或终止低优先级或可重启的批处理作业。
Facebook 在 2018 年开源了一套解决重要计算集群管理问题的 Linux 内核组件和相关工具,PSI 是其中重要的资源度量工具,它提供了一种实时检测系统资源竞争程度的方法,以竞争等待时间的方式呈现,简单而准确地供用户以及资源调度者进行决策。
1. PSI 之前的历史
在PSI 之前,Linux 也有一些资源压力的评估方法,最具代表性的是load average 和 vmpressure。
1.1 Load average
系统平均负载是指在特定时间间隔内运行队列中(在 CPU 上运行或者等待运行)的平均进程数。Linux 进程中 running 和 uninterruptible 状态进程数量加起来的占比就是当前系统 load。其具体算法为:
for_each_possible_cpu(cpu)
nr_active += cpu_of(cpu)->nr_running + cpu_of(cpu)->nr_uninterruptible;
avenrun[n] = avenrun[0] * exp_n + nr_active * (1 - exp_n)
Linux 命令 uptime、top 等都可以获得 load average 的输出,例如:
shift:/proc/pressure # uptime
06:57:42 up 49 min, 0 users, load average: 1.70, 1.71, 1.73
Load averages 的三个值分别代表最近 1/5/15 分钟的平均系统负载。在多核系统中,这些值有可能经常大于1,比如四核系统的 100% 负载为 4,八核系统的 100% 负载为 8。
Loadavg 有它固有的一些缺陷:
- uninterruptible的进程,无法区分它是在等待 CPU 还是 IO。无法精确评估单个资源的竞争程度;
- 最短的时间粒度是 1 分钟,以 5 秒间隔采样。很难精细化管理资源竞争毛刺和短期过度使用;
- 结果以进程数量呈现,还要结合 cpu 数量运算,很难直观判断当前系统资源是否紧张,是否影响任务吞吐量。
Load average 与PSI 比较可以参考:psi: pressure stall information for CPU, memory, and IO v2 [LWN.net]
1.2 vmpressure
Vmpressure 的计算在每次系统尝试做do_try_to_free_pages 回收内存时进行。其计算方法非常简单:
(1 - reclaimed/scanned)*100,也就是说回收失败的内存页越多,内存压力越大。
同时 vmpressure 提供了通知机制,用户态或内核态程序都可以注册事件通知,应对不同等级的压力。
默认定义了三级压力:low/medium/critical。
- low 代表正常回收;
- medium 代表中等压力,可能存在页交换或回写,默认值是 65%;
- critical 代表内存压力很大,即将 OOM,建议应用即可采取行动,默认值是 90%。
vmpressure 也有一些缺陷:
-
结果仅体现内存回收压力,不能反映系统在申请内存上的资源等待时间;
-
计算周期比较粗;
-
粗略的几个等级通知,无法精细化管理。
2. PSI
2.1 PSI 的准备工作
PSI 是kernel 4.2 添加进来的,4.2 及以后版本才有PSI 功能。
如果需要编译PSI 功能,需要确保 CONFIG_PSI=y
虽然打开了PSI 编译,有可能默认情况下是disable的,通过CONFIG_PSI_DEFAULT_DISABLED,可以在boot 是kernel cmdline 中添加psi=1 使能。
2.2 确定pressure 指标
目录/proc/pressure/ 下面有三个资源指标:cpu、io、memory,可以通过cat /proc/pressure/* 方式查看压力统计信息。
cpu 输出:
some avg10=0.26 avg60=0.20 avg300=0.20 total=45776130
io 输出:
some avg10=0.00 avg60=0.00 avg300=0.00 total=8370054
full avg10=0.00 avg60=0.00 avg300=0.00 total=4706725
memory 输出:
some avg10=0.00 avg60=0.00 avg300=0.00 total=934250
full avg10=0.00 avg60=0.00 avg300=0.00 total=683355
cpu 只有some 行;io 和memroy 有some 行和full 行;分别显示最近10s、60s和300s 的运行平均百分比。total 是总累计时间,以毫秒为单位。
some 这一行,代表至少有一个任务在某个资源上阻塞的时间占比,full 这一行,代表所有的非idle任务同时被阻塞的时间占比,这期间 cpu 被完全浪费,会带来严重的性能问题。
我们以memory 的 some 和 full 来举例说明,假设在 60 秒的时间段内,系统有两个 task,在 60 秒的周期内的运行情况如下图所示:
红色阴影部分表示任务由于缺少memory 资源而进入阻塞状态。Task A 没有阻塞,而Task B 必须要等30s 时间,所以,导致了some 只为50%。
下面这个情况,Task B 等待memory 30s ,而在这期间的10s时间,Task A 页处于等待状态:

Task A 和 Task B 同时阻塞的部分为 full,占比 16.66%;至少有一个任务阻塞(仅 Task B 阻塞的部分也计算入内)的部分为 some,占比 50%。
full 如果值很高, 表示总吞吐量的缺失,由于缺乏资源,所做的总工作量减少了。
some 和 full 都是在某一时间段内阻塞时间占比的总和,阻塞时间不一定连续,如下图所示:

IO 和 memory 都有 some 和 full 两个维度,那是因为的确有可能系统中的所有任务都阻塞在 IO 或者 memory 资源,同时 CPU 进入 idle 状态。
但是 CPU 资源不可能出现这个情况:不可能全部的 runnable 的任务都等待 CPU 资源,至少有一个 runnable 任务会被调度器选中占有 CPU 资源,因此 CPU 资源没有 full 维度的 PSI 信息呈现。
通过这些阻塞占比数据,我们可以看到短期以及中长期一段时间内各种资源的压力情况,可以较精确的确定时延抖动原因,并制定对应的负载管理策略。
读取压力度量值的成本很低,并且可以对最近的延迟进行采样,或者在某些任务操作之前和之后进行采样,以确定它们的资源相关延迟。
2.3 配置PSI 阈值
用户可以注册触发器,并通过poll() 在资源压力超过一定阈值时进行唤醒。
触发器描述在一个特定的时间窗口内的最大累积失速时间,例如在任何500ms窗口内总失速时间的100ms来生成一个唤醒事件。
要注册一个触发器,用户必须打开/proc/pressure/下的psi接口文件,该文件表示要监视的资源,并写入所需的阈值和时间窗口。打开的文件描述符应该用于使用select()、poll()或epoll()等待触发事件。使用以下格式:
例如,在/proc/pressure/memory中写入“some 150000 1000000”会增加150ms的阈值,在1秒的时间窗口中测量部分内存失速。在/proc/pressure/io中写入“full 50000 1000000”将增加在1秒时间窗口内测量到的完全io停止50ms阈值。
触发器可以设置在一个以上的 PSI 度量上,并且可以为相同的 PSI 度量指定多个触发器。然而,对于每个触发器,都需要一个单独的文件描述符来分别轮询它,因此,对于每个触发器,即使在打开同一个psi接口文件时,也应该进行一个单独的open()系统调用。
监视器激活只有当系统进入失速状态监测的PSI 度量和关闭退出失速状态。当系统处于失速状态时,PSI 信号增长以每跟踪窗口10倍的速度被监测。
内核接受从500ms到10s的窗口大小,因此最小监视更新间隔为50ms,最大更新间隔为1s。设置最小限制以防止过于频繁的轮询。最大极限被选择为一个足够高的数字,之后监视器最有可能不需要和PSI 平均可以代替。
2.4 软件框架

对上,PSI 模块通过文件系统节点向用户空间开放两种形态的接口。一种是系统级别的接口,即输出整个系统级别的资源压力信息。另外一种是结合 control group,进行更精细化的分组。
对下,PSI 模块通过在内存管理模块以及调度器模块中插桩,我们可以跟踪每一个任务由于 memory、io 以及 CPU 资源而进入等待状态的信息。例如系统中处于 iowait 状态的 task 数目、由于等待 memory 资源而处于阻塞状态的任务数目。
基于 task 维度的信息,PSI 模块会将其汇聚成 PSI group 上的 per cpu 维度的时间信息。例如该cpu上部分任务由于等待 IO 操作而阻塞的时间长度(CPU 并没有浪费,还有其他任务在执行)。PSI group 还会设定一个固定的周期去计算该采样周期内核的当前 psi 值(基于该 group 的 per cpu 时间统计信息)。
为了避免 PSI 值的抖动,实际上上层应用通过系统调用获取某个 PSI group 的压力值的时候会上报近期一段时间值的滑动平均值。
3. 源码分析
3.1 初始化psi 文件
/kernel/sched/psi.c
static int __init psi_proc_init(void)
{proc_mkdir("pressure", NULL);proc_create("pressure/io", 0, NULL, &psi_io_fops);proc_create("pressure/memory", 0, NULL, &psi_memory_fops);proc_create("pressure/cpu", 0, NULL, &psi_cpu_fops);return 0;
}
module_init(psi_proc_init);
模块在加载的时候,会创建几个文件并注册在 proc 文件系统(详见 Linux 中proc 文件系统简介)中。分别是:
- pressure 目录;
- pressure/io 文件,并指定file_operations;
- pressure/memory 文件,并指定file_operations;
- pressure/cpu 文件,并指定file_operations;
接着来看下 psi 文件的file_operations:
static const struct file_operations psi_io_fops = {.open = psi_io_open,.read = seq_read,.llseek = seq_lseek,.write = psi_io_write,.poll = psi_fop_poll,.release = psi_fop_release,
};static const struct file_operations psi_memory_fops = {.open = psi_memory_open,.read = seq_read,.llseek = seq_lseek,.write = psi_memory_write,.poll = psi_fop_poll,.release = psi_fop_release,
};static const struct file_operations psi_cpu_fops = {.open = psi_cpu_open,.read = seq_read,.llseek = seq_lseek,.write = psi_cpu_write,.poll = psi_fop_poll,.release = psi_fop_release,
};
除了提供正常的open、read、write、llseek,还提供了poll 功能,这也是PSI 文件与用户通信的关键函数(详细见第 5 节,用户可以通过epoll、poll等对PSI 文件进行监听,得益于poll 函数指针,详细见 Linux epoll原理及使用)
3.2 psi_init
/kernel/sched/psi.c
void __init psi_init(void)
{if (!psi_enable) {static_branch_enable(&psi_disabled);return;}psi_period = jiffies_to_nsecs(PSI_FREQ);group_init(&psi_system);
}
主要两件事:
- 指定 psi_period,即PSI 任务统计的周期,这是PSI 中的一个全局变量,这里指定 2s;
- pis_system group 的初始化;
/* System-level pressure and stall tracking */
static DEFINE_PER_CPU(struct psi_group_cpu, system_group_pcpu);
struct psi_group psi_system = {.pcpu = &system_group_pcpu,
};
这里为每个CPU 定义了一个私有数据system_group_pcpu。
struct psi_group 用来定义 PSI 统计管理数据,其中包括各 cpu 状态、周期性更新函数、更新时间戳、以及各 PSI 状态的时间记录。PSI 状态一共有六种:
include/linux/psi_types.henum psi_states {PSI_IO_SOME,PSI_IO_FULL,PSI_MEM_SOME,PSI_MEM_FULL,PSI_CPU_SOME,/* Only per-CPU, to weigh the CPU in the global average: */PSI_NONIDLE,NR_PSI_STATES = 6,
};
其他5种,上面介绍过了。PSI_NONIDLE 是指 CPU 非空闲状态,最终的时间占比是以 CPU 非空闲时间来计算的。
3.3 PSI 状态整理
在task_struct 结构中有个成员:
struct task_struct {...#ifdef CONFIG_PSI/* Pressure stall state */unsigned int psi_flags;
#endif...
};
状态定义有以下几种:
/* Task state bitmasks */
#define TSK_IOWAIT (1 << NR_IOWAIT)
#define TSK_MEMSTALL (1 << NR_MEMSTALL)
#define TSK_RUNNING (1 << NR_RUNNING)
状态的标记主要通过函数 psi_task_change,这个函数在任务每次进出调度队列时,都会被调用,从而准确标注任务的状态,例如:
psi_enqueue
psi_dequeue
psi_ttwu_dequeue
psi_memstall_enter
psi_memstall_leave
3.4 周期性统计
主要是统计在进入psi_task_change 状态中,各状态在10s、60s、300s 三个间隔的时间占比。
从底层看,一个 psi group 的 PSI 值是基于任务数目统计的,当一个任务状态发生变化的时候,首先需要遍历该任务所属的 PSI group(如果不支持 cgroup,那么系统只有一个全局的 PSI group),更新 PSI group 的 task counter。
| PSI 状态 | 描述 |
| PSI_IO_SOME | 该cpu 上的task 中至少有一个task 处于io wait 状态 |
| PSI_IO_FULL | 该cpu 上的task 中至少有一个task 处于io wait 状态,并且该CPU 没有可执行的程序,进入了IDLE 状态 |
| PSI_MEM_SOME | 该cpu 上的task 中国至少有一个 task 处于memory stall 状态 |
| PSI_MEM_FULL | 该cpu 上的task 中至少有一个task 处于memory stall 状态,并且该 cpu 没有可执行的程序,进入了idle 状态 |
| PSI_CPU_SOME | 该cpu 的run queue 中至少有一个task 在等待调度 |
| PSI_CPU_NONIDLE | 并不是说cpu 进入了idle 就是真的idle 了,实际上有些task 在等待io,要不是io 资源卡住了任务执行,cpu 是不会idle 的。 因此我们可以定义cpu idle 如下: 1. 该cpu 上等待io 的任务为0 2. 该cpu 上等待memory 的任务为0 3. 该cpu runqueue 上没有任务等待,当前cpu 上也没有任务执行。 满足以上三个条件说明cpu idle了,PSI_NONIDLE 统计的就是cpu 没有任务执行,并且也没有任务因为资源而阻塞的时间。 |
4. PSI 应用
有了 PSI 对系统资源压力的准确评估,可以做很多有意义的功能来最大化系统资源的利用。比如 facebook 开发的 cgroup2 和 oomd。oomd 是一个用户态的 out of memory 监控管理服务。
Android 早期在 kernel 新增了一个功能叫 lmk(low memory killer),在有了 PSI 之后,android 将默认的 LMK 替换成了用户态的 LMKD。其代码存放于 android/system/memory/lmkd/。
其核心思想是给 /proc/pressure/memory 的 SOME 和 FULL 设定阈值,当延时超过阈值时,触发 lmkd daemon 进程选择进程杀死。同时,还可以结合 meminfo 的剩余内存大小来判断需要清理的程度和所选进程的优先级。
5. 实例
#include
#include
#include
#include
#include
#include /** Monitor memory partial stall with 1s tracking window size* and 150ms threshold.*/
int main() {const char trig[] = "some 150000 1000000";struct pollfd fds;int n;fds.fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);if (fds.fd < 0) {printf("/proc/pressure/memory open error: %s\n",strerror(errno));return 1;}fds.events = POLLPRI;if (write(fds.fd, trig, strlen(trig) + 1) < 0) {printf("/proc/pressure/memory write error: %s\n",strerror(errno));return 1;}printf("waiting for events...\n");while (1) {n = poll(&fds, 1, -1);if (n < 0) {printf("poll error: %s\n", strerror(errno));return 1;}if (fds.revents & POLLERR) {printf("got POLLERR, event source is gone\n");return 0;}if (fds.revents & POLLPRI) {printf("event triggered!\n");} else {printf("unknown event received: 0x%x\n", fds.revents);return 1;}}return 0;
}
参考:
kernel/Documentation/accounting/psi.rst
https://facebookmicrosites.github.io/psi/docs/overview#pressure-metric-definitions
https://lwn.net/Articles/759658/
https://blog.csdn.net/feelabclihu/article/details/105534140
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
