内核优化之PSI篇:快速发现性能瓶颈,提高资源利用率
欢迎关注【字节跳动 SYS Tech】公众号。字节跳动 SYS Tech 聚焦系统技术领域,与大家分享前沿技术动态、技术创新与实践、行业技术热点分析等内容。
背景介绍
了解操作系统原理的同学应该知道,业务进程的运行性能取决于多种系统资源的分配。比如进程需要等待某些 IO 的返回,需要从伙伴系统分配内存,可能会由于 memory cgroup 的限制或者系统内存的水线配置而进行内存回收,进程运行需要调度器分配 CPU 时间,可能由于 CPU cgroup 的 quota 配置或者系统负载较大而等待调度器的调度。
所以一个进程的运行过程实际上是一个不断等待不断执行的过程,过多的等待会对进程的吞吐和延时造成负面影响,是否有一种内核机制能够量化这些等待时间呢?
PSI 通过 hook CPU 调度器和 hook 一些 IO 和 memory 的关键点,来得到一个线程开始等待某种资源和结束等待某种资源的时间点以及程序运行的时间,然后用移动平均算法算出一个表示 pressure stall information 的百分比来量化。
用途
PSI 监控机制给我们提供了一种实时量化指标,反映业务进程的吞吐和延时,是否有某种系统资源上的瓶颈。这可以帮助我们了解特定业务进程的资源需求,协助业务的部署密度。并且可以根据 PSI 指标,动态调节业务的部署和资源的分配,来保证特定业务的性能要求和系统的健康程度。
使用接口
监控接口
PSI 通过 /proc 文件系统导出了三个接口文件,用于反映实时的系统级别的 CPU,memory 和 IO 压力。
# cat /proc/pressure/cpu some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0 # cat /proc/pressure/memory some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0 # cat /proc/pressure/io some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0
其中的 some 表示至少有一个线程有资源瓶颈的时间比例,full 表示所有线程都有资源瓶颈的时间比例。
full 状态相当于整个系统由于资源瓶颈没有执行任何 productive 的代码,白白浪费了 CPU 资源。而 some 则是某些线程有资源瓶颈,另外的线程还是在利用 CPU 资源执行 productive 的代码。
avg10,avg60,avg300 则是在 10s,60s,300s 的时间窗口计算的移动平均百分比,表示资源瓶颈状态的时间比例,可以给我们短期、中期和长期的量化了解。total 则是资源瓶颈状态的绝对时间积累,我们也可以对 total 的变化进行监控来发现延时抖动的情况。
trigger 接口
除了读取这些接口文件获取实时指标外,我们还可以写入这些接口文件向内核注册 trigger,通过select()
,poll()
或 epoll()
等待 trigger 事件的发生。
# 150ms threshold for partial memory stall in 1sec time window echo "some 150000 1000000" > /proc/pressure/memory # 50ms threshold for full io stall measured within 1sec time window echo "full 50000 1000000" > /proc/pressure/io
trigger 示例代码
#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <poll.h> #include <string.h> #include <unistd.h> /* * 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; }
cgroup接口
使用cgroup-v2 时 PSI 还提供了 per-cgroup 的 pressure stall information 接口,用于监控和跟踪特定 cgroup 的资源瓶颈状态。
cgroupfs 的 mount 点的每个子目录都有三个文件接口:cpu.pressure,memory.pressure,io.pressure。读取文件内容的形式和 /proc/pressure 一样,同样也可以进行写入注册 trigger 事件。
性能优化
PSI 机制为我们提供了一种实时量化系统级别或 cgroup 级别是否存在资源瓶颈的指标,对于调度部署业务负载和资源分配有很好的指导作用,而且可以明确业务进程的吞吐和延时方面存在的资源瓶颈。
但是 PSI 机制不是没有开销的,它 hook 了调度器和 IO,memory 等热点路径,统计等待资源的时间并计算比例,这些都是有开销的。为了在生产环境常态化开启 PSI 特性,我们需要解决 PSI 的性能开销问题。
问题场景
我们在线上场景遇到的一起 PSI 性能问题的 perf top 热点,其中 psi_task_change()
热点比较高,对当时业务进程的吞吐和延时都有较大的负面影响。
代码分析
psi_task_change()
的热点开销问题可能有两个方面的原因:一个是它本身执行比较耗时,另一个是它被调用的频率太高。
psi_task_change()
函数的作用通过名字可知,在 PSI 的所有 hook 点都会发生 task 状态的变化,比如开始等待 memory,结束等待 memory,开始等待 CPU,结束等待 CPU 等,因此该函数调用频率较高。
另外psi_task_change()
内部实现不仅需要改变该 task 的状态,还要改变 task 所在每个 cgroup 的状态,改变前需要统计上个状态的时间。
详情参见以下代码片段:
void psi_task_change(struct task_struct *task, int clear, int set) { int cpu = task_cpu(task); struct psi_group *group; bool wake_clock = true; void *iter = NULL; if (!task->pid) return; psi_flags_change(task, clear, set); /* * Periodic aggregation shuts off if there is a period of no * task changes, so we wake it back up if necessary. However, * don't do this if the task change is the aggregation worker * itself going to sleep, or we'll ping-pong forever. */ if (unlikely((clear & TSK_RUNNING) && (task->flags & PF_WQ_WORKER) && wq_worker_last_func(task) == psi_avgs_work)) wake_clock = false; while ((group = iterate_groups(task, &iter))) psi_group_change(group, cpu, clear, set, wake_clock); }
通过以上代码分析,我们发现有两个可以优化的方向:尽量减少psi_task_change()
的调用,以及尽量减少psi_group_change()
的调用。
代码优化
1. 利用共同的cgroup
我们需要知道的一个捷径是当task A切换到task B时,如果A和B存在共同的cgroup时,其实cgroup的状态是没有变化的,只是task A和task B的状态变化了。
所以根据这个事实我们可以优化发生频率比较高的task_switch hook,不再遍历改变task A的所有cgroup,然后再次遍历task B的所有cgroup,而是进行整合:只改变task A和task B的不同cgroup分支,直到相同的cgroup停止,减少psi_task_change()
的调用开销。
2. 减少sleep导致的状态切换
sleep before: psi_dequeue() while ((group = iterate_groups(prev))) # all ancestors psi_group_change(prev, .clear=TSK_RUNNING|TSK_ONCPU) psi_task_switch() while ((group = iterate_groups(next))) # all ancestors psi_group_change(next, .set=TSK_ONCPU) sleep after: psi_dequeue() nop psi_task_switch() while ((group = iterate_groups(next))) # until (prev & next) psi_group_change(next, .set=TSK_ONCPU) while ((group = iterate_groups(prev))) # all ancestors psi_group_change(prev, .clear=common?TSK_RUNNING:TSK_RUNNING|TSK_ONCPU)
通过以上对比可知:优化掉了 psi_dequeue 触发的 psi_group_change()
调用,将状态的改变整合放到了psi_task_switch()
,减少了 cgroup 的状态切换开销。
总结
PSI 的优化成果已经合入上游的主线内核,也合入了 veLinux 开源的 5.4 内核版本,现在部署的 5.4 内核都已经常态化开启了 PSI 机制,配合 cgroup-v2 的使用会更好地帮助我们发现业务性能的资源瓶颈,以及动态的自适应运维和资源分配。
社区也已经存在基于 PSI 机制的 OOMD(out of memory daemon) service,可以更好地应对系统 OOM 的场景,同时在字节内部也积极开发和应用基于 PSI 机制的动态资源管控程序,以在保证业务性能的同时,提高资源利用率。
veLinux 内核 GitHub 地址:https://github.com/bytedance/kernel

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
得物染色环境落地实践
1. 背景 测试环境治理一直是各大公司非常重要的一个课题,测试环境稳定性很大程度影响迭代开发&测试效率。 综合来看,测试环境不稳定的原因主要有以下几点: 测试环境的变更非终态变更,经常会有代码发布/配置发布导致服务无法启动或者链路有问题的情况。 变更频繁,开发需要联调、测试需要迭代测试,代码需要变更,配置也需要变更,权限控制就比较难做,增加了测试环境不稳定性。 并行需求,同一时间单个应用需要多个分支同时支持多个需求的测试,测试环境资源的抢占和冲突比较明显。 得物测试环境稳定性治理也经历了几个阶段: 2020~2021:多套物理环境隔离方案(基于ECS) T0、T1、T2三套测试环境,每套环境物理隔离,无资源冲突和共享。 规划T1用于迭代测试、T0用于集成回归、T2用于独立项目分配使用,但在实际使用过程中,业务测试并行太多,冲突比较明显,环境就开始乱用了,谁有需求就随便占用一套环境使用了。结果就是没有一套稳定的环境,测试有效性无法保障,并行项目环境冲突也无法解决。 2021~2022:MF全链路容器环境方案(基于容器) 随着业务增长,3套测试环境已明显不能满足业务需求,因此去年得...
- 下一篇
mybatisplus-plus 1.7.2 支持多主键与原生单主键兼容
mybatisplus-plus1.7.2 现已发布,具体更新内容包括: 支持mpp的多主键@MppMultiId可以和mp的单主键@TableId兼容,同时修饰同一个entity的field mybatisplus-plus1.7.2支持继承多主键entity 对mybatisplus-plus在逻辑saveorupdate时的效果,对ds多数据源等做了测试,与原生mybatisplus表现一致。
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2全家桶,快速入门学习开发网站教程
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启