本文主要根据Brendan Gregg大神的一篇博客改编而来,原文链接如下。
http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html
导读
本文主要是对平均负载(load average)的含义进行剖析,并对linux上的实现做了一些考证工作。load avg的wikipedia解释为:系统需要进行处理的工作量,具象则可以表述为系统上正在运行(running)和已经ready且等待调度(runnable)的进程数之和。有经验的读者肯定发现,该描述并不符合linux的实现,linux还包含了处于不可中断睡眠( uninterruptible sleep,即我们平常所说的D状态)进程。那么,Linux的load avg为何包含不可中断睡眠进程?这样统计会带来哪些影响?本文尝试来解答这个问题。
正文
load avg是业界常用的一个关键指标,其用途广泛,但我们真的完全理解它(尤其是在Linux系统中)么?linux load avg是system load avg(可暂时理解为loag avg的一种特化子类型,具体原因后文会解释),它表示系统上正在以及等待运行的线程对资源的需求。统计平均负载的工具一般都会有三个值,分别表征过去1/5/15分钟内的平均负载:
$ uptime 16:48:24 up 4:11, 1 user, load average: 25.25, 23.40, 23.46 top - 16:48:42 up 4:12, 1 user, load average: 25.25, 23.14, 23.37 $ cat /proc/loadavg 25.72 23.19 23.35 42/3411 43603
一些简单的使用规则:
- 如果load avg == 0,则系统空闲;
- 如果过去1分钟内的平均负载高于过去5/15分钟的平均负载,则说明负载在增加;
- 如果过去1分钟内的平均负载低于过去5/15分钟的平均负载,则说明负载在减少;
- 如果load avg > cpu数目,则可能存在性能问题(视具体情况而论)。
通过上述规则,我们可以评估一个系统的负载是在增减或者减少。但有时候仅凭负载值是无法得到具体的结论,比如我们说一个系统的负载为23-25,在不确定CPU数目时,我们无法判断系统的负载是否合理。
历史
load avg最早定义为对cpu资源的需求:系统上正在运行或等待运行的进程数。RDC546脚注里有如下一段话:“TENEX平均负载是对CPU需求的度量,是在给定时间段内系统上可运行的进程数平均值。举个例子,单cpu系统,某一小时的平均负载为10,则表示在该小时内的任意时刻,我们将期望看到有1个进程在运行,而另外有9个进程在等待运行(没有被I/O阻塞)”。下面的代码是load avg的实现,源码摘至SCHED.MAC文件:
NRJAVS==3 ;NUMBER OF LOAD AVERAGES WE MAINTAIN GS RJAV,NRJAVS ;EXPONENTIAL AVERAGES OF NUMBER OF ACTIVE PROCESSES [...] ;UPDATE RUNNABLE JOB AVERAGES DORJAV: MOVEI 2,^D5000 MOVEM 2,RJATIM ;SET TIME OF NEXT UPDATE MOVE 4,RJTSUM ;CURRENT INTEGRAL OF NBPROC+NGPROC SUBM 4,RJAVS1 ;DIFFERENCE FROM LAST UPDATE EXCH 4,RJAVS1 FSC 4,233 ;FLOAT IT FDVR 4,[5000.0] ;AVERAGE OVER LAST 5000 MS [...] ;TABLE OF EXP(-T/C) FOR T = 5 SEC. EXPFF: EXP 0.920043902 ;C = 1 MIN EXP 0.983471344 ;C = 5 MIN EXP 0.994459811 ;C = 15 MIN
load avg在linux实现参考
#define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */
可以看到,都是硬编码为1/5/15分钟的统计。在一些较老的其他系统中,我们也能看到类似的实现,比如Multics。
三个数字
三个数字是指1,5,15分钟的平均负载,这不是我们常用的算术平均,也不仅仅是指最近的1,5,15分钟时间段内的统计。上面的源码是每5秒分别更新一次1,5,15分钟的指数衰减滑动平均。由于是指数衰减,所以统计值对应的时间段显然不止最近的1,5,15分钟。假设存在一个初始时完全空闲的系统,然后开始运行一个单线程的CPU-bound型的工作负载,那么在60秒后,1分钟的平均负载应该是多少呢?假设采用算术平均,显然平均负载等于1,而指数衰减滑动平均则如下图所示:
可以看到,1分钟平均负载在程序持续运行1分钟后仅为0.62。更多的细节可参考Dr. Neil Gunther的” How It Works “以及linux源码kernel/sched/loadavg.c中的注释。
Linux不可中断任务
与其他系统一样,平均负载一开始在linux系统上实现时,表征的也是CPU资源的度量。但后来,linux上的统计算法发生了变化,不仅仅包含可运行的任务,还包含不可中断状态的进程(TASK_UNINTERRUPTIBLE或 nr_uninterruptible)。进程进入不可中断睡眠状态后,不会被信号中断,例如任务被I/O或者锁(比如内核信号量)阻塞。读者可能已经遇见过这类任务:ps或者 top命令输出的D状态进程。ps的man手册称其为“不可中断睡眠(一般是因为I/O)”。
平均负载包含不可中断睡眠的进程,意味着linux的load avg可能因为磁盘(或NFS文件系统)I/O负载而增加。读者如果熟悉其他系统的cpu load avg统计方式,再与linux来对比,可能会感到疑惑:为何包含不可中断睡眠?目前已有非常多的文章来解释load avg,其中大部分也能指出Linux包含了不可中断睡眠,但是无人给出解释甚至猜测:为什么会包含它呢?brendangregg认为,linux的统计不仅仅是cpu维度的度量,而是多维度的统计,下面来详细分析。
Patch追溯
【译者注:本段文字主要记录了brendangregg的探索历程,如果有了解linux代码历史记录的需求,可以参考该方法,如果只对结果感兴趣,请直接跳至下一段文字。】
brendangregg同学开始刨根究底,一开始期望通过linux的git commit历史,看能否发现什么蛛丝马迹。首先检查kernel/sched/loadavg.c的提交历史,发现uninterruptible state比loadavg.c文件还要早出现,所以肯定是先在其他文件中出现。接着他又检查了另外一个文件,一无所获,线索又指向了更老的文件。
brendangregg不得已采取了暴力破解法,通过对linux整个repo使用git log -p命令,输出了4G字节的文本文件,然后开始慢慢回溯…但是很不幸,这还是走向了死胡同。linux repo最早的记录是2005年,版本是2.6.12-rc2,而该版本已包含不可中断睡眠的统计。brendangregg没有放弃,转而使用更老repo记录,但仍然没有得到答案。
brendangregg又寄希望于找到何时引入了该修改,通过直接对比LINUX v0.99上的tar包,发现在在0.99.15中已有该项修改,而在0.99.13中还未出现,但是…0.99.14的tar包没能在kernel.org没能找到。幸好,他在其他地方找到了这个tar包,可以确认的是该项修改于1993年11月,在0.99的patch level 14中引入。
遗憾的是,0.99.14的发布描述里,没有任何与此项修改相关的解释,又一个死胡同:“Changes to the last official release (p13) are too numerous to mention (or even to remember)…” – Linus。Linus在发布描述里只提到了主要修改,对load avg这类小修改一概而过。通过版本的发布日期,brendangregg在内核邮件列表档案里找到了那个patch,但是这里最老的邮件是1995年6月份开始,系统管理员在里面写了一句话:”为了使这些邮件档案更加高效地工作,我不小心摧毁了现有的档案(哎呀)”。brendangregg感到了这个世界深深的恶意,然而他还是没有放弃。他在一些备份服务器中发现了一部分更老的内核邮件列表tar包。他搜查了六千多份摘要,共包含九万八千多封电子邮件,其中三万份是从一九九三年起的。似乎,linux load avg为何包含不可中断睡眠,将成为永远的谜…
原始Patch
皇天不负有心人,brendangregg在oldlinux.org上找到原始patch,如下:
From: Matthias Urlichs <urlichs@smurf.sub.org> Subject: Load average broken ? Date: Fri, 29 Oct 1993 11:37:23 +0200 The kernel only counts "runnable" processes when computing the load average. I don't like that; the problem is that processes which are swapping or waiting on "fast", i.e. noninterruptible, I/O, also consume resources. It seems somewhat nonintuitive that the load average goes down when you replace your fast swap disk with a slow swap disk... Anyway, the following patch seems to make the load average much more consistent WRT the subjective speed of the system. And, most important, the load is still zero when nobody is doing anything. ;-) --- kernel/sched.c.orig Fri Oct 29 10:31:11 1993 +++ kernel/sched.c Fri Oct 29 10:32:51 1993 @@ -414,7 +414,9 @@ unsigned long 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; return nr; } -- Matthias Urlichs \ XLink-POP N|rnberg | EMail: urlichs@smurf.sub.org Schleiermacherstra_e 12 \ Unix+Linux+Mac | Phone: ...please use email. 90491 N|rnberg (Germany) \ Consulting+Networking+Programming+etc'ing 42
由此,我们可以确认linux load avg的修改是为了反映系统资源的需求量,而不仅仅是CPU资源。打上这个patch后,linux从“cpu load avg”统计变为“system load avg”统计。提交记录中提到了一个低性能的swap磁盘案例:由于性能下降,导致对系统资源的需求增加(运行+排队等待的进程),没有打上这个patch时,系统的平均负载统计将会变低,Matthias认为这样不符合直观判断,所以修正了统计。
如今的Uninterruptible
Linux的平均负载有时变得非常高,并非完全与磁盘I/O压力正相关,似乎还有未知因素。brendangregg猜测这是因为在1993年改动后,新增了许多可进入不可中断睡眠的代码路径。在linux 0.99.14中,仅存在13条代码路径会直接进入TASK_UNINTERRUPTIBLE或TASK_SWAPPING(该状态后来被删除)状态。如今的Linux 4.12版本,已经有超过400条代码路径会进入TASK_UNINTERRUPTIBLE,这其中包含一些锁原语。也许,这里面的某些代码路径不应该被统计入负载。brendangregg给Matthias发邮件咨询其对load avg含义变化的看法,回复如下:“平均负载是从人的视角来衡量量系统繁忙程度的数值。
TASK_UNINTERRUPTIBLE意味着(或曾经意味着)被阻塞(比如读磁盘)的进程对系统负载的贡献。比如一个disk-bound型的系统,其TASK_RUNNING的平均值也许是0.1,该值毫无实际意义。”所以,Matthias认为linux上的修正是有道理的,从邮件回复中也可以看出TASK_UNINTERRUPTIBLE曾经是指进程被磁盘I/O阻塞。但如今TASK_UNITERRUPTIBLE的含义丰富了许多。平均负载是否应只考虑CPU或者磁盘资源的需求?调度器的maintainer Peter Zijstra提到了一个想法:系统负载不统计TASK_UNINTERRUPTIBLE,而是统计task_struct->in_iowait。但这是我们真正想要的么?也许我们会想统计线程对系统资源的需求,而不仅仅是物理资源?如果是基于线程统计,那么那些进入不可中断睡眠的线程也消耗了系统资源,因为他们并非idle。所以,也许目前的linux load avg统计也许才是我们真正想要的。接下来brendangregg给出了几个例子。
不可中断任务的测量分析
下图是一台生产环境服务器的Off-Cpu火焰图,统计持续60秒,仅显示TASK_UNINTERRUPTIBLE状态的内核栈。通过统计图可以得到不可中断睡眠的代码路径:
x轴代表off-CPU的时间长短,从左至右没有任何排序,色彩饱和度随机没有任何含义。图是通过bcc工具(依赖于kernel4.8+上的eBPF特性)以及火焰图脚本:
# ./bcc/tools/offcputime.py -K --state 2 -f 60 > out.stacks # awk '{ print $1, $2 / 1000 }' out.stacks | ./FlameGraph/flamegraph.pl --color=io --countname=ms > out.offcpu.svgb>
brendangregg使用awk将时间精度从us调整为ms,’–state 2’表示TASK_UNINTERRUPTIBLE(参考sched.h)。offcputime.py支持用户态与内核态栈,这里为了说明仅显示用户态栈。上图60秒内有926ms进入了不可中断睡眠,所以仅影响平均负载0.015(926ms/60s)。该服务程序并没有太多的磁盘I/O,主要是cgroup路径相关。
下图是另一案例,仅统计了10秒
图片右侧高耸的平坦区域是systemd-journal调用函数proc_pid_cmdline_read()(读取/proc/PID/cmdline),对平均负载贡献了0.07。在左侧的更为宽阔的平坦区域是阻塞在rwsem_down_read_failed()函数(对平均负载贡献了0.23)。下面的一段代码摘自rwsem_down_read_failed()函数:
/* wait to be given the lock */ while (true) { set_task_state(tsk, TASK_UNINTERRUPTIBLE); if (!waiter.task) break; schedule(); }
可以看到,lock路径使用了TASK_UNINTERRUPTIBLE。linux的锁原语一般支持可中断与不可中断两类API(如互斥锁mutex_lock() vs mutex_lock_interruptible(), 信号量 down() vs down_interruptible())。可中断版本可被signal打断并被唤醒执行。一般而言,因为lock失败而进入不可中断睡眠不会对平均负载造成很大的影响,但本案例则影响了0.30。如果该值更高,则需要分析锁竞争是否合理以及能否优化。
上面俩案例中的不可中断睡眠路径是否应该被统计入平均负载呢?brendangregg认为应该被统计,理由如下:线程在处理任务的过程中,获取锁失败被阻塞,并非进入idle,这仍然会消耗系统资源(主要指软件资源,而不是硬件资源)。
linux平均负载剖析
brendangregg尝试对linux平均负载按类型进行细分统计。他在一个8核空闲CPU的系统上使用tar打包未被page cache缓存的文件,被磁盘读阻塞了几分钟。下面是一些统计输出:
terma$ pidstat -p `pgrep -x tar` 60 Linux 4.9.0-rc5-virtual (bgregg-xenial-bpf-i-0b7296777a2585be1) 08/01/2017 _x86_64_ (8 CPU) 10:15:51 PM UID PID %usr %system %guest %CPU CPU Command 10:16:51 PM 0 18468 2.85 29.77 0.00 32.62 3 tar termb$ iostat -x 60 [...] avg-cpu: %user %nice %system %iowait %steal %idle 0.54 0.00 4.03 8.24 0.09 87.10 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util xvdap1 0.00 0.05 30.83 0.18 638.33 0.93 41.22 0.06 1.84 1.83 3.64 0.39 1.21 xvdb 958.18 1333.83 2045.30 499.38 60965.27 63721.67 98.00 3.97 1.56 0.31 6.67 0.24 60.47 xvdc 957.63 1333.78 2054.55 499.38 61018.87 63722.13 97.69 4.21 1.65 0.33 7.08 0.24 61.65 md0 0.00 0.00 4383.73 1991.63 121984.13 127443.80 78.25 0.00 0.00 0.00 0.00 0.00 0.00 termc$ uptime 22:15:50 up 154 days, 23:20, 5 users, load average: 1.25, 1.19, 1.05 [...] termc$ uptime 22:17:14 up 154 days, 23:21, 5 users, load average: 1.19, 1.17, 1.06
Off-CPU火焰图
一分钟load avg=1.19,拆分如下:
- 0.33来自于CPU(pidstat)
- 0.67来自于磁盘阻塞读
- 0.04来自于其他的CPU消耗(iostat user + system 减去pidstat的CPU)
- 0.11来自于内核工作线程的不可中断的磁盘I/O以及刷磁盘cache。
上述之和等于1.15,距实际统计值1.19相差0.04。相差可能是统计误差引入,但更可能是因为load avg是指数衰减滑动平均,而上面计算用到的统计都是常规的算术平均。 1.19的前一分钟统计值为1.25,结合前文提到的1分钟指数衰减统计,可如此计算:0.62 x 1.15 + 0.38 x 1.25 = 1.18,与实际统计值1.19非常接近。如果仅考虑cpu load avg,结合mpstat的输出可推断出0.37。
重新解释linux load avg
brendangregg认为平均负载应做如下解释:
- 在linux系统里,平均负载 =“system load avg”,包含正在工作以及等待工作(CPU/磁盘/不可中断锁)的线程。另一种等价说法:平均负载统计所有非idle的线程。优点:统计了不同的资源需求。
- 在非linux系统里,平均负载 = “cpu load avg”,包含正在运行以及等待运行的线程。优点:仅统计CPU资源需求,容易理解。
也许以后还会有“物理资源平均负载”,包含CPU以及磁盘资源,甚至“CPU平均负载”, “磁盘平均负载”, “网络平均负载”等等。
如何评估负载是否合理
一般的经验是当系统负载超过一个特定值X,则认为将影响业务延迟,但事实上并非如此。仅考虑CPU负载,load/CPU数量如果大于1.0则可能存在性能问题。实际上这种判断方法存在缺陷,因为负载是一个平均值,并不能反映瞬时状态。比如load/CPU等于1.5时,有些系统能正常工作,而有些则存在性能问题。brendangregg举了个例子,两台邮件服务器的CPU平均负载为11-16(load/CPU比值为5.5-8),负载较高,但用户完全能接受。所以他给出了一个结论:大部分系统在load/CPU小于2时,都能够正常运行。
Linux的system load avg更复杂,因为它不仅仅包含CPU资源,所以不能直接除以CPU数量。但我们可以用它来做纵向对比:比如系统在负载为20时能正常工作,突然变成了40,这时可结合其他手段来分析具体原因。
其他量化手段
当linux系统负载增加,意味着对系统的资源消耗增加,但并没有指出是哪一种具体的资源(比如CPU,磁盘,锁等)。可以通过其他的手段来辅助分析确认,比如CPU资源:
- 每CPU利用率: mpstat -P ALL 1
- 每进程的CPU利用率:top, pidstat 1
- 每线程的run queue调度延迟:/proc/PID/schedstats, delaystats, perf sched
- CPU run queue延迟:/proc/schedstat, perf sched, runqlat, bcc工具
- CPU run queue队列长度:vmstat 1的’r’列,runqlen bcc工具
前两个是利用率指标,可用来识别工作负载的特征;后三个是饱和度指标,可用来进行性能分析。除了CPU量化,还可以对磁盘I/O进行量化。需要说明的是,本节提到的各种量化手段与平均负载统计并不冲突。
结语
1993年,一位linux工程师发现负载统计存在不符合直观之处,提了三行patch将“CPU load avg”变成了“system load avg”,旨在反映系统对CPU与磁盘资源的需求,具体实现则是增加了对uninterruptible的统计。随着linux的发展,进入uninterruptible的路径越来越多,比如lock。本文中,brendangregg剖析时使用bcc/eBPF采集数据并制作出Off-CPU火焰图,还提到了各种工具,这些我们都可以在实际分析问题时加以利用。最后,本文以linux kernel/sched/loadavg.c中的注释来结束:
* This file contains the magic bits required to compute the global loadavg * figure. Its a silly number but people think its important. We go through * great pains to make it work on big machines and tickless kernels.
—— 完 ——
加入龙蜥社群
加入微信群:添加社区助理-龙蜥社区小龙(微信:openanolis_assis),备注【龙蜥】拉你入群;加入钉钉群:扫描下方钉钉群二维码。欢迎开发者/用户加入龙蜥社区(OpenAnolis)交流,共同推进龙蜥社区的发展,一起打造一个活跃的、健康的开源操作系统生态!
龙蜥社区钉钉交流群 龙蜥社区-小龙
关于龙蜥社区
龙蜥社区(OpenAnolis)是由企事业单位、高等院校、科研单位、非营利性组织、个人等按照自愿、平等、开源、协作的基础上组成的非盈利性开源社区。龙蜥社区成立于2020年9月,旨在构建一个开源、中立、开放的Linux上游发行版社区及创新平台。
短期目标是开发龙蜥操作系统(Anolis OS)作为CentOS替代版,重新构建一个兼容国际Linux主流厂商发行版。中长期目标是探索打造一个面向未来的操作系统,建立统一的开源操作系统生态,孵化创新开源项目,繁荣开源生态。
龙蜥OS 8.4已发布,支持x86_64和ARM64架构,完善适配Intel、飞腾、海光、兆芯、鲲鹏芯片。
欢迎下载:
https://openanolis.cn/download
加入我们,一起打造面向未来的开源操作系统!