纯干货,PSI 原理解析与应用

2024-03-31 07:18
文章标签 应用 原理 解析 干货 psi

本文主要是介绍纯干货,PSI 原理解析与应用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

https://blog.csdn.net/feelabclihu/article/details/105534140

 

一、什么是 PSI

Pressure Stall Information 提供了一种评估系统资源压力的方法。系统有三个基础资源:CPU、Memory 和 IO,无论这些资源配置如何增加,似乎永远无法满足软件的需求。一旦产生资源竞争,就有可能带来延迟增大,使用户体验到卡顿。

如果没有一种相对准确的方法检测系统的资源压力程度,有两种后果。一种是资源使用者过度克制,没有充分使用系统资源;另一种是经常产生资源竞争,过度使用资源导致等待延迟过大。准确的检测方法可以帮忙资源使用者确定合适的工作量,同时也可以帮助系统制定高效的资源调度策略,最大化利用系统资源,最大化改善用户体验。

Facebook 在 2018 年开源了一套解决重要计算集群管理问题的 Linux 内核组件和相关工具,PSI 是其中重要的资源度量工具,它提供了一种实时检测系统资源竞争程度的方法,以竞争等待时间的方式呈现,简单而准确地供用户以及资源调度者进行决策。

二、为什么出现 PSI

在此之前,Linux 也有一些资源压力的评估方法,最具代表性的是 load average 和 vmpressure。

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 的输出,例如:

$ uptime

16:08 up 13 days, 5:06, 1 user, load averages: 0.15 0.41 0.26

Load averages 的三个值分别代表最近 1/5/15 分钟的平均系统负载。在多核系统中,这些值有可能经常大于1,比如四核系统的 100% 负载为 4,八核系统的 100% 负载为 8。

Loadavg 有它固有的一些缺陷:

  • uninterruptible的进程,无法区分它是在等待 CPU 还是 IO。无法精确评估单个资源的竞争程度;

  • 最短的时间粒度是 1 分钟,以 5 秒间隔采样。很难精细化管理资源竞争毛刺和短期过度使用;

  • 结果以进程数量呈现,还要结合 cpu 数量运算,很难直观判断当前系统资源是否紧张,是否影响任务吞吐量。

2、Vmpressure

Vmpressure 的计算在每次系统尝试做do_try_to_free_pages 回收内存时进行。其计算方法非常简单:

(1 - reclaimed/scanned)*100,也就是说回收失败的内存页越多,内存压力越大。

同时 vmpressure 提供了通知机制,用户态或内核态程序都可以注册事件通知,应对不同等级的压力。

默认定义了三级压力:low/medium/critical。low 代表正常回收;medium 代表中等压力,可能存在页交换或回写,默认值是 65%;critical 代表内存压力很大,即将 OOM,建议应用即可采取行动,默认值是 90%。

vmpressure 也有一些缺陷:

  • 结果仅体现内存回收压力,不能反映系统在申请内存上的资源等待时间;

  • 计算周期比较粗;

  • 粗略的几个等级通知,无法精细化管理。

三、PSI 软件架构

1、蓝图 

PSI 相关的软件结构图如下所示:

对上,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 的压力值的时候会上报近期一段时间值的滑动平均值。

2、PSI 用户接口定义

每类资源的压力信息都通过 proc 文件系统的独立文件来提供,路径为 /proc/pressure/ -- cpu, memory, and io.

其中 CPU 压力信息格式如下:

some avg10=2.98 avg60=2.81 avg300=1.41 total=268109926

memory 和 io 格式如下:

some avg10=0.30 avg60=0.12 avg300=0.02 total=4170757

full avg10=0.12 avg60=0.05 avg300=0.01 total=1856503

avg10、avg60、avg300 分别代表 10s、60s、300s 的时间周期内的阻塞时间百分比。total 是总累计时间,以毫秒为单位。

some 这一行,代表至少有一个任务在某个资源上阻塞的时间占比,full 这一行,代表所有的非idle任务同时被阻塞的时间占比,这期间 cpu 被完全浪费,会带来严重的性能问题。我们以 IO 的 some 和 full 来举例说明,假设在 60 秒的时间段内,系统有两个 task,在 60 秒的周期内的运行情况如下图所示:

红色阴影部分表示任务由于等待 IO 资源而进入阻塞状态。Task A 和 Task B 同时阻塞的部分为 full,占比 16.66%;至少有一个任务阻塞(仅 Task B 阻塞的部分也计算入内)的部分为 some,占比 50%。

some 和 full 都是在某一时间段内阻塞时间占比的总和,阻塞时间不一定连续,如下图所示:

IO 和 memory 都有 some 和 full 两个维度,那是因为的确有可能系统中的所有任务都阻塞在 IO 或者 memory 资源,同时 CPU 进入 idle 状态。

但是 CPU 资源不可能出现这个情况:不可能全部的 runnable 的任务都等待 CPU 资源,至少有一个 runnable 任务会被调度器选中占有 CPU 资源,因此 CPU 资源没有 full 维度的 PSI 信息呈现。

通过这些阻塞占比数据,我们可以看到短期以及中长期一段时间内各种资源的压力情况,可以较精确的确定时延抖动原因,并制定对应的负载管理策略。

四、源码解析

PSI 相关源代码比较简单,核心功能都在  kernel/sched/psi.c 文件中实现。

1、初始化

第一步,在 psi_proc_init 函数中完成 PSI 接口文件节点的创建。首先建立proc/pressure目录,然后 3 个 proc_create 函数创建了 io、memory 和 cpu 三个 proc 属性文件:

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);

第二步,在 psi_init 函数中初始化统计管理结构和更新任务的周期:

psi_period = jiffies_to_nsecs(PSI_FREQ);

group_init(&psi_system);

我们把相关的任务组成一个 group,然后针对这个任务组计算其 PSI 值。如果不支持 control group,那么实际上系统中只有一个 PSI group:

static DEFINE_PER_CPU(struct psi_group_cpu, system_group_pcpu);

static struct psi_group psi_system = {

.pcpu = &system_group_pcpu,

};

如果支持 cgroup(需要 mount cgroup2 文件系统),那么系统中会有多个 PSI group,形成层级结构。我们可以在挂载的 cgroup 文件系统下面获取 per-group 的 PSI 信息。

我们也可以从 proc 文件系统下面获取整个系统级别的 PSI 信息。Cgroup 中各个分组的 PSI 信息跟踪是类似的,后续我们的文章主要基于系统级别的 PSI 信息跟踪来描述代码逻辑流程。

struct psi_group 用来定义 PSI 统计管理数据,其中包括各 cpu 状态、周期性更新函数、更新时间戳、以及各 PSI 状态的时间记录。PSI 状态一共有六种:

enum 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,

};

前 5 种状态的定义在本文上一节已经介绍,PSI_NONIDLE 是指 CPU 非空闲状态,最终的时间占比是以 CPU 非空闲时间来计算的。

2、状态埋点

整个 PSI 技术的核心难点其实在于如何准确捕捉到任务状态的变化,并统计状态持续时间。我们首先看看 task 维度的埋点信息。

PSI 作者在 task_struct 结构中加入了一个成员:PSI_flags,用于标注任务所处状态,状态定义有以下几种:

#define TSK_IOWAIT(1 << NR_IOWAIT)

#define TSK_MEMSTALL(1 << NR_MEMSTALL)

#define TSK_RUNNING(1 << NR_RUNNING)

状态的标记主要通过函数 psi_task_change,这个函数在任务每次进出调度队列时,都会被调用,从而准确标注任务状态。

其中 psi_memstall_tick 并没有任务状态的转换,只是在每个调度 tick 及时更新各状态的积累时间。

3、周期性统计

周期性的更新任务 psi_update_work 函数非常简单,更新统计数据,然后设定下一次任务唤醒的时间。周期间隔为 PSI_FREQ,2s。

更新统计数据的函数 update_stats,主要有两步:

第一步 get_recent_times,对每个 cpu 更新各状态的时间并统计各状态系统总时间;

第二步 calc_avgs,更新每个状态的 10s、60s、300s 三个间隔的时间占比。

计算一个 PSI group 的 PS I值的过程示意图如下所示:

从底层看,一个 psi group 的 PSI 值是基于任务数目统计的,当一个任务状态发生变化的时候,首先需要遍历该任务所属的 PSI group(如果不支持 cgroup,那么系统只有一个全局的 PSI group),更新 PSI group 的 task counter。

一旦 task counter 发生了变化,那么我们需要进一步更新对应 CPU 上的时间统计信息。例如 iowait task count 从 0 变成 1,那么 SOME 维度的 io wait time 需要更新。具体的 per-CPU PSI 状态时间统计信息如下:

完成了上面 6 种状态的时间统计之后,在系统的每个 cpu 上就建立了 6 条 time line,而上层的 PSI group 会以固定周期来采样 time line 的数组。采样点之间相减就可以得到该周期内各种状态的时间长度值。通过下面的公式我们可以计算单个 CPU 上的 PSI 值:

%SOME = time(SOME) / period

%FULL = time(FULL) / period

在多 CPU 场景下,我们要综合考虑 CPU 个数和 non idle task 的个数,计算公式如下:

tNONIDLE = sum(tNONIDLE[i])

tSOME = sum(tSOME[i] * tNONIDLE[i]) / tNONIDLE

tFULL = sum(tFULL[i] * tNONIDLE[i]) / tNONIDLE

%SOME = tSOME / period

%FULL = tFULL / period

tNONIDLE[i]、tSOME[i] 和 tFULL[i] 已经在 per-CPU 状态统计中获取了,通过上面的公式即可以计算该 psi group 在当前周期内的 PSI 值。

在计算三种间隔的时间占比时,有人可能会有疑问,周期是 2s,如何做到每次都更新三种数据呢?这个问题其实在上面讲到的老技术 load average 计算时已经解决,采用公式:a1 = a0 * e + a * (1 - e);

于是得到:newload = load * exp + active * (FIXED_1 - exp)

其中 active 是当前更新周期的 load average,load 是上个周期得到的 load average,exp 的定义如下:

#define EXP_10s 1677 /* 1/exp(2s/10s) as fixed-point */

#define EXP_60s 1981 /* 1/exp(2s/60s) */

#define EXP_300s   2034 /* 1/exp(2s/300s) */

五、PSI 的应用

有了 PSI 对系统资源压力的准确评估,可以做很多有意义的功能来最大化系统资源的利用。比如 facebook 开发的 cgroup2 和 oomd。oomd 是一个用户态的 out of  memory 监控管理服务。

Android 早期在 kernel 新增了一个功能叫 lmk(low memory killer),在有了 PSI 之后,android 将默认的 LMK 替换成了用户态的 LMKD。其代码存放于 android/system/core/lmkd/。

其核心思想是给 /proc/pressure/memory 的 SOME 和 FULL 设定阈值,当延时超过阈值时,触发 lmkd daemon 进程选择进程杀死。同时,还可以结合 meminfo 的剩余内存大小来判断需要清理的程度和所选进程的优先级。

参考文献:

[1]Getting Started with PSI, https://facebookmicrosites.github.io/psi/docs/overview#pressure-metric-definitions

[2]psi: pressure stall information for CPU, memory, and IO v2, https://lwn.net/Articles/759658/

这篇关于纯干货,PSI 原理解析与应用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/863735

相关文章

5分钟获取deepseek api并搭建简易问答应用

《5分钟获取deepseekapi并搭建简易问答应用》本文主要介绍了5分钟获取deepseekapi并搭建简易问答应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需... 目录1、获取api2、获取base_url和chat_model3、配置模型参数方法一:终端中临时将加

JavaScript中的isTrusted属性及其应用场景详解

《JavaScript中的isTrusted属性及其应用场景详解》在现代Web开发中,JavaScript是构建交互式应用的核心语言,随着前端技术的不断发展,开发者需要处理越来越多的复杂场景,例如事件... 目录引言一、问题背景二、isTrusted 属性的来源与作用1. isTrusted 的定义2. 为

Python调用另一个py文件并传递参数常见的方法及其应用场景

《Python调用另一个py文件并传递参数常见的方法及其应用场景》:本文主要介绍在Python中调用另一个py文件并传递参数的几种常见方法,包括使用import语句、exec函数、subproce... 目录前言1. 使用import语句1.1 基本用法1.2 导入特定函数1.3 处理文件路径2. 使用ex

Linux中shell解析脚本的通配符、元字符、转义符说明

《Linux中shell解析脚本的通配符、元字符、转义符说明》:本文主要介绍shell通配符、元字符、转义符以及shell解析脚本的过程,通配符用于路径扩展,元字符用于多命令分割,转义符用于将特殊... 目录一、linux shell通配符(wildcard)二、shell元字符(特殊字符 Meta)三、s

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

将Python应用部署到生产环境的小技巧分享

《将Python应用部署到生产环境的小技巧分享》文章主要讲述了在将Python应用程序部署到生产环境之前,需要进行的准备工作和最佳实践,包括心态调整、代码审查、测试覆盖率提升、配置文件优化、日志记录完... 目录部署前夜:从开发到生产的心理准备与检查清单环境搭建:打造稳固的应用运行平台自动化流水线:让部署像

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

使用Python实现批量访问URL并解析XML响应功能

《使用Python实现批量访问URL并解析XML响应功能》在现代Web开发和数据抓取中,批量访问URL并解析响应内容是一个常见的需求,本文将详细介绍如何使用Python实现批量访问URL并解析XML响... 目录引言1. 背景与需求2. 工具方法实现2.1 单URL访问与解析代码实现代码说明2.2 示例调用

SSID究竟是什么? WiFi网络名称及工作方式解析

《SSID究竟是什么?WiFi网络名称及工作方式解析》SID可以看作是无线网络的名称,类似于有线网络中的网络名称或者路由器的名称,在无线网络中,设备通过SSID来识别和连接到特定的无线网络... 当提到 Wi-Fi 网络时,就避不开「SSID」这个术语。简单来说,SSID 就是 Wi-Fi 网络的名称。比如

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R