当前位置: 首页 > 科技观察

Linux中的负载水平和CPU开销并不完全对应

时间:2023-03-16 00:58:51 科技观察

嗨,我是飞哥!在查看Linux服务器的运行状况时,负载是一个非常常见的性能指标。在观察线上服务器的运行状态时,我们经常会找出负载看看。当线上请求压力过大时,往往伴随着负载的飙升。但是你真的了解load的原理吗?我列几个问题,看看大家对负载的理解是否足够深入。负载是如何计算的?负载与CPU消耗呈正相关吗?内核如何将负载数据暴露给应用层?如果你对以上问题的理解还不是很准确,那么飞哥今天就带你深入了解一下Linux中的负载!1.了解负载查看过程我们经常使用top命令来查看Linux系统的负载。典型的top命令输出负载如下所示。#topLoadAvg:1.25,1.30,1.95.......输出中的LoadAvg就是我们常说的负载,也叫系统的平均负载。因为单一的瞬时负载值没有多大意义。所以Linux计算过去一段时间的平均值,这三个数字分别代表过去1分钟、过去5分钟、过去15分钟的平均负载值。那么top命令显示的数据条数是怎么来的呢?实际上,top命令中的加载值来自/proc/loadavg伪文件。通过strace命令跟踪top命令的系统调用可以看到这个过程。#stracetop...openat(AT_FDCWD,"/proc/loadavg",O_RDONLY)=7内核中定义了伪文件loadavg的open函数。当用户态访问/proc/loadavg时,会触发内核定义的函数,在这里读取内核中的averageload变量,简单计算后即可显示出来。整体流程如下图所示。我们来看看上面的流程图。伪文件/proc/loadavg在/fs/proc/loadavg.c的内核中定义。在此文件/proc/loadavg中创建并分配了操作方法loadavg_proc_fops。//文件:fs/proc/loadavg.cstaticint__initproc_loadavg_init(void){proc_create("loadavg",0,NULL,&loadavg_proc_fops);return0;}loadavg_proc_fops包含打开文件时对应的操作方法。//文件:fs/proc/loadavg.cstaticconststructfile_operationsloadavg_proc_fops={.open=loadavg_proc_open,......};在用户态打开/proc/loadavg文件时,会调用loadavg_proc_fops中的open函数指针——loadavg_proc_open。loadavg_proc_open随后会调用loadavg_proc_show进行处理,核心计算就在这里完成。//文件:fs/proc/loadavg.cstaticintloadavg_proc_show(structseq_file*m,void*v){unsignedlongavnrun[3];//获取平均负载值get_avenrun(avnrun,FIXED_1/200,0);//打印平均负载seq_printf(m,"%lu.%02lu%lu.%02lu%lu.%02lu%ld/%d%d\n",LOAD_INT(avnrun[0]),LOAD_FRAC(avnrun[0]),LOAD_INT(avnrun[1]),LOAD_FRAC(avnrun[1]),LOAD_INT(avnrun[2]),LOAD_FRAC(avnrun[2]),nr_running(),nr_threads,task_active_pid_ns(current)->last_pid);return0;}在loadavg_proc_show函数中完成了两件事。上面源码中调用get_avenrun读取当前负载值,并以一定格式打印出平均负载值。你见过FIXED_1/200、LOAD_INT、LOAD_FRAC等奇怪的定义,代码写得这么猥琐是因为内核没有float、double等浮点数类型,而是用整数来模拟。这些代码都是用于整数和小数之间的转换。知道背景就够了,没必要过分分析。这样用户就可以通过访问/proc/loadavg文件来读取内核计算出的负载数据。其中获取get_avenrun只是访问avenrun的全局数组。//file:kernel/sched/core.cvoidget_avenrun(unsignedlong*loads,unsignedlongoffset,intshift){loads[0]=(avenrun[0]+offset)<sched_timer,CLOCK_MONOTONIC,HRTIMER_MODE_ABS);//设置定时器的过期函数为tick_sched_timerts->sched_timer.function=tick_sched_timer;...}在初始化高分辨率时,将过期函数设置为tick_sched_timer。通过这个函数,每个CPU都会周期性的执行一些任务。其中,刷新当前系统负载就是在这个时候进行的。这里需要注意的一点是每个CPU都有自己独立的运行队列。我们根据tick_sched_timer的源码进行追踪,依次调用tick_sched_handle=>update_process_times=>scheduler_tick。最后在scheduler_tick中,会将当前CPU上的负载值刷新到calc_load_tasks中。因为每个CPU都是定时刷新的,所以calc_load_tasks上记录的是整个系统的瞬时负载值。我们来看看负责刷新的核心函数scheduler_tick://file:kernel/sched/core.cvoidscheduler_tick(void){intcpu=smp_processor_id();结构rq*rq=cpu_rq(cpu);update_cpu_load_active(rq);...}在这个函数中,获取当前cpu及其对应的运行队列rq(runqueue),调用update_cpu_load_active将当前cpu的负载数据刷新到全局数组中。//file:kernel/sched/core.cstaticvoidupdate_cpu_load_active(structrq*this_rq){...calc_load_account_active(this_rq);}//file:kernel/sched/core.cstaticvoidcalc_load_account_active(structrq*this_rq){//获取当前运行队列的负载相对值delta=calc_load_fold_active(this_rq);if(delta)//添加到全局瞬时负载值atomic_long_add(delta,&calc_load_tasks);...}在calc_load_account_active中看到,通过calc_load_fold_active获取当前运行的队列负载相对值,并添加到全局瞬时负载值calc_load_tasks中。至此,calc_load_tasks拥有了当前系统在当前时刻的总瞬时负载。下面展开看看如何根据运行队列计算负载值://file:kernel/sched/core.cstaticlongcalc_load_fold_active(structrq*this_rq){longnr_active,delta=0;//R和D状态下的用户tasknr_active=this_rq->nr_running;nr_active+=(长)this_rq->nr_uninterruptible;//只返回变化量if(nr_active!=this_rq->calc_load_active){delta=nr_active-this_rq->calc_load_active;this_rq->calc_load_active=nr_active;}returndelta;}哦,原来nr_running和nr_uninterruptible状态的进程数是同时计算的。用户空间中R和D两种状态对应的任务(进程OR线程)数量。由于calc_load_tasks是长期存在的数据。所以当刷新rq中的进程数到它上面时,只需要刷新变化量即可,不需要全部重新计算。所以上面的函数返回的是一个delta。2.2系统平均负载的定时计算在上一节中,我们找到了系统当前瞬时负载的calc_load_tasks变量的更新过程。现在我们缺少一种机制来计算最后1分钟、最后5分钟、最后15分钟的平均负载。传统上,我们计算平均值的方法是将一段时间内的数字相加,然后取平均值。将过去N个时间点的所有瞬时负载相加求平均值是不够的。这其实就是我们传统意义上理解的平均数。如果有n个数字,则它们是x1,x2,...,xn。那么这个数据集的均值就是(x1+x2+...+xn)/N。但是如果用这种简单的算法来计算平均负载,有几个问题:1.需要存储过去每个采样周期的数据。假设我们每10毫秒采集一次,那么我们需要用一个比较大的数组来存储每次采样的所有数据,所以必须存储1500条数据(15分钟*每分钟100次)来计算过去15次的平均值分钟。而且,每出现一个新的观测值,移动平均都要减去最早的观测值,再加上最新的观测值。内存阵列会经常修改和更新。2.当计算过程比较复杂时,将整个数组加起来除以样本总数。加法虽然简单,但是几百上千个数的累加还是很繁琐的。3、不能准确反映当前的变化趋势。在传统的平均计算过程中,所有数字都具有相同的权重。但对于平均负载等实时应用,其实越接近当前时刻,该值的权重应该越大。因为它更能反映近期变化的趋势。因此,Linux中使用的并不是我们认为的传统平均计算方法,而是一种指数加权移动平均(ExponentialWeightedMovingAverage,EMWA)平均计算方法。这种指数加权移动平均计算方法在深度学习中有着广泛的应用。另外,股市中的EMA移动平均线也是采用类似的求均值的方法。该算法的数学表达式为:a1=a0*factor+a*(1-factor)。这个算法理解起来有点复杂,有兴趣的同学可以谷歌搜索一下。我们只需要知道,这种方法在实际计算中只需要前一次的平均值,不需要保存所有的瞬时负载值。另外,距离当前时间点越近,权重越高,可以很好的代表近期的变化趋势。这实际上是在时间子系统中定期进行的,这三个平均值是通过一种称为指数加权移动平均计算的方法计算出来的。我们仔细看看上图中的执行过程。时间子系统会将时钟中断处理函数注册为时钟中断中的timer_interrupt。//文件:arch/ia64/kernel/time.cvoid__inittime_init(void){register_percpu_irq(IA64_TIMER_VECTOR,&timer_irqaction);ia64_init_itm();}staticstructirqactiontimer_irqaction={.handler=timer_interrupt,.flags=IRQF_LED="timer"};当每个时钟节拍到来时,会调用timer_interrupt,依次调用do_timer函数。//file:kernel/time/timekeeping.cvoiddo_timer(unsignedlongticks){...calc_global_load(ticks);}其中calc_global_load是平均负载计算的核心。它会获取系统calc_load_tasks当前的瞬时负载值,然后计算出过去1分钟、过去5分钟、过去15分钟的平均负载,保存在avenrun中供用户进程读取。//file:kernel/sched/core.cvoidcalc_global_load(unsignedlongticks){...//1.获取当前瞬时负载值active=atomic_long_read(&calc_load_tasks);//2.计算平均负载avenrun[0]=calc_load(avenrun[0],EXP_1,active);avenrun[1]=calc_load(avenrun[1],EXP_5,活动);avenrun[2]=calc_load(avenrun[2],EXP_15,活动);...}获取瞬时负载比较简单,读取一个内存变量即可。在calc_load中,使用我们前面提到的指数加权移动平均法来计算过去1分钟、过去5分钟、过去15分钟的平均负载。具体实现代码如下://file:kernel/sched/core.c/**a1=a0*e+a*(1-e)*/staticunsignedlongcalc_load(unsignedlongload,unsignedlongexp,unsigned长活动){加载*=exp;加载+=活动*(FIXED_1-exp);加载+=1UL<<(FSHIFT-1);returnload>>FSHIFT;}虽然这个算法理解起来挺复杂的,但是代码看起来真的是简单多了,而且计算量好像也很少。而且你看不懂也没关系,你只要知道kernel并不是原来的平均计算方式,而是一种计算速度快,更能表达变化趋势的算法。至此,“负载是如何计算的?”这个问题我们开头提到的也有了结论。Linux定期将每个CPU上运行队列中处于运行状态和不可中断状态的进程数汇总成一个全局系统瞬时负载值,然后用指数加权移动平均法统计过去1分钟、过去5分钟、过去15分钟的平均负载。3、平均负载与CPU消耗的关系现在很多同学都会将平均负载与CPU联系起来。认为高负载意味着高CPU消耗,低负载意味着低CPU消耗。在很老的Linux版本中,统计负载时只计算可运行任务的数量,这些进程只有对CPU的需求。那时候负载和CPU消耗确实是正相关的。负载越高,CPU上运行或者等待CPU执行的进程越多,CPU消耗就会越高。但正如我们前面看到的,本文使用的3.10版本的Linuxloadaverage不仅可以跟踪可运行的任务,还可以跟踪处于不间断睡眠状态的任务。处于不可中断状态的进程实际上并不占用CPU。所以,负载高并不一定代表CPU处理不了,也有可能是因为磁盘等资源无法调度导致进程进入不间断状态!为什么要这样修改。上网查了一下,在一封邮件中找到了原因,最早可以追溯到1993年。以下是邮件原文。发件人:MatthiasUrlichs主题:平均负载损坏?日期:1993年10月29日星期五11:37:23+0200内核在计算平均负载时只计算“可运行”进程。我不不喜欢那样;问题是正在“快速”交换或等待的进程,即不可中断的I/O,也会消耗资源。当您用慢速交换磁盘替换快速交换磁盘时,平均负载下降似乎有点不直观……无论如何,以下补丁似乎使平均负载更加一致WRT系统的主观速度。而且,最重要的是,当没有人做任何事情时,负载仍然为零。;-)---kernel/sched.c.origFriOct2910:31:111993+++kernel/sched.cFriOct2910:32:511993@@-414,7+414,9@@无符号长nr=0;for(p=&LAST_TASK;p>&FIRST_TASK;--p)-if(*p&&(*p)->state==TASK_RUNNING)+if(*p&&((*p)->state==TASK_RUNNING)||+(*p)->state==TASK_UNINTERRUPTIBLE)||+(*p)->state==TASK_SWAPPING))nr+=FIXED_1;返回编号;}可以看出这个修改是在1993年引入的从这封邮件显示的Linux源码变化可以看出,payload官方添加了TASK_UNINTERRUPTIBLE和TASK_SWAPPING状态的进程(swap状态后来从Linux移除)。在这封邮件的正文中,作者也明确表达了为什么要添加处于TASK_UNINTERRUPTIBLE状态的进程的原因。我将他的说明翻译如下:“内核在计算平均负载时只计算“可运行”进程。我不喜欢这样;问题是正在“快速”交换或等待的进程,即不间断的I/O,也消耗资源。当你用慢速交换磁盘替换快速交换磁盘时,平均负载下降似乎有点不直观......无论如何,下面的补丁似乎使平均负载更一致WRT系统的主观速度。和,最重要的是,当没有人做任何事情时,负载仍然为零。;-)”这个补丁的提交者的主要想法是平均负载应该代表对系统所有资源的需求,而不仅仅是对CPU资源的需求。假设一个处于TASK_UNINTERRUPTIBLE状态的进程因为等待磁盘IO而进入队列,此时它并不消耗CPU,而是在等待磁盘等硬件资源。那么它应该反映在平均负载的计算中。所以作者把所有处于TASK_UNINTERRUPTIBLE状态的进程都放入平均负载。因此,负载水平表示当前系统对系统资源的总体需求。如果负载变高,可能是CPU资源不足,也可能是磁盘IO资源不足,需要配合其他观察命令具体情况具体分析。4、小结今天带大家深入了解Linux中的负载。我们用一张图来总结一下今天所学的内容。我把负载工作原理分为以下三个步骤。1、内核定时汇总各个CPU的负载到系统的瞬时负载。2.内核使用指数加权移动平均快速计算过去1、5、15分钟的平均值。3、用户进程通过打开loadavg读取内核中的平均负载。回过头来总结一下开头提到的问题。1、负载是怎么计算的?就是定期将每个CPU上运行队列中处于运行状态和不可中断状态的进程数汇总成一个全局的系统瞬时负载值,然后用指数加权移动平均的方法每隔一定时间统计过去的负载平均值为1分钟,最后5分钟,最后15分钟。2.负载水平和CPU消耗之间是否存在正相关关系?负载水平表示当前系统对系统资源的总体需求。如果负载变高,可能是CPU资源不足,也可能是磁盘IO资源不足。所以不能说看到负载增加就感觉CPU资源不够用了。3.内核如何将payload数据暴露给应用层?内核定义了一个伪文件/proc/loadavg。每当用户打开这个文件时,内核中的loadavg_proc_show函数就会被调用。此函数访问avenrun全局数组变量并将平均负载从整数转换为小数。然后打印出来。