纯干货,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

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

一文解析C#中的StringSplitOptions枚举

《一文解析C#中的StringSplitOptions枚举》StringSplitOptions是C#中的一个枚举类型,用于控制string.Split()方法分割字符串时的行为,核心作用是处理分割后... 目录C#的StringSplitOptions枚举1.StringSplitOptions枚举的常用

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

MyBatis延迟加载与多级缓存全解析

《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓... 目录MyBATis延迟加载策略一对多示例一对多示例MyBatis框架的缓存一级缓存二级缓存MyBat

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse