理解 Linux CPU utilization 监控参数
2019-02-14 16:15:34 阿炯

工作中查看Zabbix中Linux监控项的时候对linux监控的cpu使用的各个参数没怎么明白,特意查看了下资料。Zabbix Linux模板下的CPU utilization是自带的监控Linux CPU各个参数的监控项,其实就是Linux下top命令显示的cpu信息。


Zabbix下的监控信息的各个值分别是什么意思呢?


官方解释,Cpu(s)表示的是cpu信息。
us: user cpu time (or) % CPU time spent in user space
sy: system cpu time (or) % CPU time spent in kernel space
ni: user nice cpu time (or) % CPU time spent on low priority processes
id: idle cpu time (or) % CPU time spent idle
wa: io wait cpu time (or) % CPU time spent in wait (on disk)
hi: hardware irq (or) % CPU time spent servicing/handling hardware interrupts
si: software irq (or) % CPU time spent servicing/handling software interrupts
st: steal time - - % CPU time in involuntary wait by virtual cpu while hypervisor is servicing another processor (or) % CPU time stolen from a virtual machine

翻译一下:
us:用户态使用的cpu时间比
sy:系统态使用的cpu时间比
ni:用做nice加权的进程分配的用户态cpu时间比
id:空闲的cpu时间比
wa:cpu等待磁盘写入完成时间
hi:硬中断消耗时间
si:软中断消耗时间
st:虚拟机偷取时间

上面解释过就好理解了,idle就是cpu的空闲时间,也就是说idle的空闲时间90%,cpu使用率就是10%。

而iowait实际测量的是cpu时间:%iowait = (cpu idle time)/(all cpu time)

知道怎么回事以后,那监控cpu使用率就好办了,cpu使用率超过百分之90就告警。

CPU user percent gt 90%

{Template OS Linux:system.cpu.util[,idle].avg(1m)}<10

下面是博客园一位网友对各个参数的理解,图文并茂,逻辑清晰。

首先这个百分比是怎么算出来的呢?

比如一秒内有100个cpu时间片,这个cpu时间片就是cpu工作的最小单位。那么这100个cpu时间片在不同的区域和目的进行操作使用,就代表这个区域所占用的cpu时间比;也就是这里得出的cpu时间百分比。

比如下面一个程序:

 
将文件从磁盘的src位置拷贝到磁盘的dst位置。文件会从src先读取进入到内核空间,然后再读取到用户空间,然后拷贝数据到用户空间的buf上,再通过用户空间,内核空间,数据才到磁盘的dst上。

所以从上面这个程序来看,cpu消耗在kernel space的时候就是sy(系统态使用的cpu百分比),cpu消耗在user space的时候就是us(用户态使用的cpu百分比)。

好了,下面说说hi和si。

如果程序都没什么问题,那么是没有hi和si的,但是实际上有个硬中断和软中断的概念。比如硬中断,cpu在执行程序的时候,突然外设硬件(比如硬盘出现问题了)机器需要立刻通知cpu进行现场保存工作。这个时候会cpu会出现上下文切换。就是cpu会有一部分时间会被硬中断占用了,这个时间就是hi。相类似,si是软中断的cpu占用时间,软中断是由软件的指令方式触发的。

相关软中断和硬中断的概念可以参考这里

ni

ni是nice的意思,nice是什么呢,每个linux进程都有个优先级,优先级高的进程有优先执行的权利,这个叫做pri。进程除了优先级外,还有个优先级的修正值。即比如你原先的优先级是20,然后修正值为-2,那么你最后的进程优先级为18。这个修正值就叫做进程的nice值。
 
那么nice是一个进程的优先级修正值,为什么会占用cpu时间呢?

ni是指用做nice加权的进程使用的用户态cpu时间比,我的理解就是一个进程的所谓修正值就意味着多分配一些cpu时间给这个进程的用户态,这个中间所多分配的cpu时间就是我们这里的ni。(这个理解没啥把握,如果有错误麻烦帮忙指出下)

wa

wa指的是CPU等待磁盘写入完成的时间,就是说前提是要进行IO操作,在进行IO操作的时候,CPU等待时间。比如上面那个程序,最后一步,从系统空间到dst硬盘空间的时候,如果程序是阻塞的,那么这个时候cpu就要等待数据写入磁盘才能完成写操作了。所以这个时候cpu等待的时间就是wa。所以如果一台机器看到wa特别高,那么一般说明是磁盘IO出现问题,可以使用iostat等命令继续进行详细分析。

st

st的名字很生动,偷取。。是专门对虚拟机来说的,一台物理是可以虚拟化出几台虚拟机的。在其中一台虚拟机上用top查看发现st不为0,就说明本来有这么多个cpu时间是安排给我这个虚拟机的,但是由于某种虚拟技术,把这个cpu时间分配给了其他的虚拟机了。这就叫做偷取。

id

剩下的id就是除了上面那么多cpu处理上下文以外的cpu时间片。当然在这些时间片上,cpu是空闲的。

top的所有这些cpu时间应该是相加为100%的。

上文转自:Zabbix CPU utilization监控参数


CPU性能指标可以从两方面来看:静态、动态

静态指标主要包括:CPU的型号、主频、核数、cache等
动态指标主要包括:CPU的平均负载状况、CPU的使用率、最耗CPU的进程有哪些

查看静态信息

在服务器运行过程中,一般我们比较少关注CPU的静态信息,但刚开始拿到一台服务器时,就会很关心静态信息了。可根据这些信息判断分配给这台服务器多大的压力等,通过 /proc/cpuinfo文件来查看具体的信息。

查看动态信息

服务器变慢时,通常会先查看下CPU的负载是否过高,如果高了,再看下是哪些进程最耗费CPU,CPU使用率也是重要指标,让我们知道CPU消耗在哪些部分

1)、CPU负载状况
通过负载信息能够直观的了解到CPU的压力状况,linux会给出最近1分钟、5分钟、15分钟的平均负载值:
可以通过 top 命令查看
uptime 命令更加简洁直观

查看到负载值后,怎么判断CPU的负载是否过高呢?

有一个经验型的标准:CPU负载上限值 = CPU的核数 * 4

例如是4核CPU,那么CPU的负载最好不要超过16,否则CPU的压力就很大了。

2)、耗费CPU的主要进程
发现CPU负载过高后,肯定想知道是谁把CPU搞的这么忙:
通过 top 命令查看
用 ps 命令根据CPU使用量对进程排序
# ps -aux --sort -pcpu | less

3)、CPU使用率
通过 top 命令查看,还可以查看每个核的使用率状况,执行 top 后按数字1键,就可以列出每个cpu的使用率。其中有几个主要信息项(上文也有说明):
id - CPU的空闲度
us - 用户进程对CPU的使用率
sy - 系统进程对CPU的使用率
wa - IO等待情况
st - 如果系统中运行了虚拟机,此项显示虚机使用CPU的情况


Linux 中 CPU 利用率是如何计算出来的

本节转自张彦飞空间,感谢原作者。观察线上服务运行状态的时候,绝大多数人都是喜欢先用 top 命令看看当前系统的整体 cpu 利用率。例如,随手拿来的一台机器,top 命令显示的利用率信息如下:


这个输出结果说简单也简单,说复杂也不是那么容易就能全部搞明白的。例如:
问题 1:top 输出的利用率信息是如何计算出来的,它精确吗?
问题 2:ni 这一列是 nice,它输出的是 cpu 在处理啥时的开销?
问题 3:wa 代表的是 io wait,那么这段时间中 cpu 到底是忙碌还是空闲?

这里对 cpu 利用率统计进行深入的学习。通过学习不但能了解 cpu 利用率统计实现细节,还能 nice、io wait 等指标有更深入的理解。

一、先思考一下
抛开 Linux 的实现先不谈,如果有如下需求,有一个四核服务器,上面跑了四个进程。


来设计计算整个系统 cpu 利用率的这个需求,支持像 top 命令这样的输出,满足以下要求:
cpu 使用率要尽可能地准确
要能地体现秒级瞬时 cpu 状态

这个看起来很简单的需求,实际还是有点小复杂的。其中一个思路是把所有进程的执行时间都加起来,然后再除以系统执行总时间*4。


这个思路是没问题的,用这种方法统计很长一段时间内的 cpu 利用率是可以的,统计也足够的准确。

但只要用过 top 你就知道 top 输出的 cpu 利用率并不是长时间不变的,而是默认 3 秒为单位会动态更新一下(这个时间间隔可以使用 -d 设置)。这个方案体现总利用率可以,体现这种瞬时的状态就难办了。可能会想到那我也 3 秒算一次不就行了?但这个 3 秒的时间从哪个点开始呢。粒度很不好控制。

上一个思路问题核心就是如何解决瞬时问题。提到瞬时状态,可能就又来思路了。就用瞬时采样去看,看看当前有几个核在忙。四个核中如果有两个核在忙,那利用率就是 50%。这个思路思考的方向也是正确的,但是问题有两个:
算出的数字都是 25% 的整数倍
这个瞬时值会导致 cpu 使用率显示的剧烈震荡。比如下图:


在 t1 的瞬时状态看来,系统的 cpu 利用率毫无疑问就是 100%,但在 t2 时间看来,使用率又变成 0% 了。思路方向是对的,但显然这种粗暴的计算无法像 top 命令一样优雅地工作。

再改进一下它,把上面两个思路结合起来,可能就能解决我们的问题了。在采样上把周期定的细一些,但在计算上把周期定的粗一些。引入采用周期的概念,定时比如每 1 毫秒采样一次。如果采样的瞬时,cpu 在运行,就将这 1 ms 记录为使用。这时会得出一个瞬时的 cpu 使用率,把它都存起来。


在统计 3 秒内的 cpu 使用率的时候,比如上图中的 t1 和 t2 这段时间范围。那就把这段时间内的所有瞬时值全加一下,取个平均值。这样就能解决上面的问题了,统计相对准确,避免了瞬时值剧烈震荡且粒度过粗(只能以 25 %为单位变化)的问题了。可能有同学会问了,假如 cpu 在两次采样中间发生变化了呢,如下图这种情况。


在当前采样点到来的时候,进程 A 其实刚执行完,有一点点时间没有既没被上一个采样点统计到,本次也统计不到。对于进程 B,其实只开始了一小段时间,把 1 ms 全记上似乎有点多记了。

确实会存在这个问题,但因为采样是 1 ms 一次,而实际查看使用的时候最少也有是秒级别地用,会包括有成千上万个采样点的信息,所以这种误差并不会影响对全局的把握。事实上,Linux 也就是这样来统计系统 cpu 利用率的。虽然可能会有误差,但作为一项统计数据使用已经是足够了的。在实现上,Linux 是将所有的瞬时值都累加到某一个数据上的,而不是真的存了很多份的瞬时数据。接下来进入 Linux 来查看它对系统 cpu 利用率统计的具体实现。

二、top 命令使用数据在哪儿
上一节说的 Linux 在实现上是将瞬时值都累加到某一个数据上的,这个值是内核通过 /proc/stat 伪文件来对用户态暴露。Linux 在计算系统 cpu 利用率的时候用的就是它。整体上看,top 命令工作的内部细节如下图所示。


top 命令访问 /proc/stat 获取各项 cpu 利用率使用值
内核调用 stat_open 函数来处理对 /proc/stat 的访问
内核访问的数据来源于 kernel_cpustat 数组,并汇总
打印输出给用户态

接下来我们把每一步都展开来详细看看。通过使用 strace 跟踪 top 命令的各种系统调用,可以看的到它对该文件的调用。

# strace top
...
openat(AT_FDCWD, "/proc/stat", O_RDONLY) = 4
openat(AT_FDCWD, "/proc/2351514/stat", O_RDONLY) = 8
openat(AT_FDCWD, "/proc/2393539/stat", O_RDONLY) = 8
...
除了 /proc/stat 外,还有各个进程细分的 /proc/{pid}/stat,是用来计算各个进程的 cpu 利用率时使用的。

内核为各个伪文件都定义了处理函数,/proc/stat 文件的处理方法是 proc_stat_operations。

//file:fs/proc/stat.c
static int __init proc_stat_init(void){
 proc_create("stat", 0, NULL, &proc_stat_operations);
 return 0;
}

static const struct file_operations proc_stat_operations = {
 .open  = stat_open,
 ...
};

proc_stat_operations 中包含了该文件时对应的操作方法。当打开 /proc/stat 文件的时候,stat_open 就会被调用到。stat_open 依次调用 single_open_size,show_stat 来输出数据内容。来看看它的代码:

//file:fs/proc/stat.c
static int show_stat(struct seq_file *p, void *v){
 u64 user, nice, system, idle, iowait, irq, softirq, steal;

 for_each_possible_cpu(i) {
  struct kernel_cpustat *kcs = &kcpustat_cpu(i);

  user += kcs->cpustat[CPUTIME_USER];
  nice += kcs->cpustat[CPUTIME_NICE];
  system += kcs->cpustat[CPUTIME_SYSTEM];
  idle += get_idle_time(kcs, i);
  iowait += get_iowait_time(kcs, i);
  irq += kcs->cpustat[CPUTIME_IRQ];
  softirq += kcs->cpustat[CPUTIME_SOFTIRQ];
  ...
 }

 //转换成节拍数并打印出来
 seq_put_decimal_ull(p, "cpu  ", nsec_to_clock_t(user));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(nice));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(system));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(idle));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(iowait));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(irq));
 seq_put_decimal_ull(p, " ", nsec_to_clock_t(softirq));
 ...
}

在上面的代码中,for_each_possible_cpu 是在遍历存储着 cpu 使用率数据的 kcpustat_cpu 变量。该变量是一个 percpu 变量,它为每一个逻辑核都准备了一个数组元素。里面存储着当前核所对应各种事件,包括 user、nice、system、idel、iowait、irq、softirq 等。在这个循环中,将每一个核的每种使用率都加起来。最后通过 seq_put_decimal_ull 将这些数据输出出来。


注意,在内核中实际每个时间记录的是纳秒数,但是在输出的时候统一都转化成了节拍单位。至于节拍单位多长,下一节介绍。总之 /proc/stat 的输出是从 kernel_cpustat 这个 percpu 变量中读取出来的。接着再看看这个变量中的数据是何时加进来的。

三、统计数据怎么来的
前面提到内核是以采样的方式来统计 cpu 使用率的。这个采样周期依赖的是 Linux 时间子系统中的定时器。Linux 内核每隔固定周期会发出 timer interrupt (IRQ 0),这有点像乐谱中的节拍的概念。每隔一段时间,就打出一个拍子,Linux 就响应之并处理一些事情。


一个节拍的长度是多长时间,是通过 CONFIG_HZ 来定义的。它定义的方式是每一秒有几次 timer interrupts。不同的系统中这个节拍的大小可能不同,通常在 1 ms 到 10 ms 之间。可以在自己的 Linux config 文件中找到它的配置。

# grep ^CONFIG_HZ /boot/config-5.4.56.bsk.10-amd64
CONFIG_HZ=1000

从上述结果中可以看出,机器的每秒要打出 1000 次节拍。也就是每 1 ms 一次。每次当时间中断到来的时候,都会调用 update_process_times 来更新系统时间。更新后的时间都存储在我们前面提到的 percpu 变量 kcpustat_cpu 中。


来详细看下汇总过程 update_process_times 的源码,它位于 kernel/time/timer.c 文件中。
//file:kernel/time/timer.c
void update_process_times(int user_tick){
 struct task_struct *p = current;
 //进行时间累积处理
 account_process_tick(p, user_tick);
 ...
}

这个函数的参数 user_tick 值得是采样的瞬间是处于内核态还是用户态。接下来调用 account_process_tick。

//file:kernel/sched/cputime.c
void account_process_tick(struct task_struct *p, int user_tick){
 cputime = TICK_NSEC;
 ...

 if (user_tick)
  //3.1 统计用户态时间
  account_user_time(p, cputime);
 else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
  //3.2 统计内核态时间
  account_system_time(p, HARDIRQ_OFFSET, cputime);
 else
  //3.3 统计空闲时间
  account_idle_time(cputime);
}

在这个函数中,首先设置 cputime = TICK_NSEC, 一个 TICK_NSEC 的定义是一个节拍所占的纳秒数。接下来根据判断结果分别执行 account_user_time、account_system_time 和 account_idle_time 来统计用户态、内核态和空闲时间。

3.1 用户态时间统计
//file:kernel/sched/cputime.c
void account_user_time(struct task_struct *p, u64 cputime){
 //分两种种情况统计用户态 CPU 的使用情况
 int index;
 index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER;

 //将时间累积到 /proc/stat 中
 task_group_account_field(p, index, cputime);
 ......
}

account_user_time 函数主要分两种情况统计:
如果进程的 nice 值大于 0,那么将会增加到 CPU 统计结构的 nice 字段中。
如果进程的 nice 值小于等于 0,那么增加到 CPU 统计结构的 user 字段中。

至此开篇的问题 2 就有答案了,其实用户态的时间不只是 user 字段,nice 也是。之所以要把 nice 分出来,是为了让 Linux 用户更一目了然地看到调过 nice 的进程所占的 cpu 周期有多少。平时如果想要观察系统的用户态消耗的时间的话,应该是将 top 中输出的 user 和 nice 加起来一并考虑,而不是只看 user!接着调用 task_group_account_field 来把时间加到前面我们用到的 kernel_cpustat 内核变量中。

//file:kernel/sched/cputime.c
static inline void task_group_account_field(struct task_struct *p, int index,u64 tmp){
 __this_cpu_add(kernel_cpustat.cpustat[index], tmp);
 ...
}

3.2 内核态时间统计
我们再来看内核态时间是如何统计的,找到 account_system_time 的代码。

//file:kernel/sched/cputime.c
void account_system_time(struct task_struct *p, int hardirq_offset, u64 cputime){
 if (hardirq_count() - hardirq_offset)
  index = CPUTIME_IRQ;
 else if (in_serving_softirq())
  index = CPUTIME_SOFTIRQ;
 else
  index = CPUTIME_SYSTEM;

 account_system_index_time(p, cputime, index);
}

内核态的时间主要分 3 种情况进行统计。

如果当前处于硬中断执行上下文, 那么统计到 irq 字段中
如果当前处于软中断执行上下文, 那么统计到 softirq 字段中
否则统计到 system 字段中

判断好要加到哪个统计项中后,依次调用 account_system_index_time、task_group_account_field 来将这段时间加到内核变量 kernel_cpustat 中。

//file:kernel/sched/cputime.c
static inline void task_group_account_field(struct task_struct *p, int index,u64 tmp){
 __this_cpu_add(kernel_cpustat.cpustat[index], tmp);
}

3.3 空闲时间的累积
没错,在内核变量 kernel_cpustat 中不仅仅是统计了各种用户态、内核态的使用统计,空闲也一并统计起来了。如果在采样的瞬间,cpu 既不在内核态也不在用户态的话,就将当前节拍的时间都累加到 idle 中。

//file:kernel/sched/cputime.c
void account_idle_time(u64 cputime){
 u64 *cpustat = kcpustat_this_cpu->cpustat;
 struct rq *rq = this_rq();

 if (atomic_read(&rq->nr_iowait) > 0)
  cpustat[CPUTIME_IOWAIT] += cputime;
 else
  cpustat[CPUTIME_IDLE] += cputime;
}

在 cpu 空闲的情况下,进一步判断当前是不是在等待 IO(例如磁盘 IO),如果是的话这段空闲时间会加到 iowait 中,否则就加到 idle 中。从这里,我们可以看到 iowait 其实是 cpu 的空闲时间,只不过是在等待 IO 完成而已。看到这里,开篇问题 3 也有非常明确的答案了,io wait 其实是 cpu 在空闲状态的一项统计,只不过这种状态和 idle 的区别是 cpu 是因为等待 io 而空闲。

四、总结

本节深入分析了 Linux 统计系统 CPU 利用率的内部原理,全文的内容可以用如下一张图来汇总:


Linux 中的定时器会以某个固定节拍,比如 1 ms 一次采样各个 cpu 核的使用情况,然后将当前节拍的所有时间都累加到 user/nice/system/irq/softirq/io_wait/idle 中的某一项上。top 命令是读取的 /proc/stat 中输出的 cpu 各项利用率数据,而这个数据在内核中的是根据 kernel_cpustat 来汇总并输出的。

回到开篇问题 1,top 输出的利用率信息是如何计算出来的,它精确吗?

/proc/stat 文件输出的是某个时间点的各个指标所占用的节拍数。如果想像 top 那样输出一个百分比,计算过程是分两个时间点 t1, t2 分别获取一下 stat 文件中的相关输出,然后经过个简单的算术运算便可以算出当前的 cpu 利用率。提供了一个简单的 Shell 脚本,可以用它来实际查看一下你服务器的 cpu 利用率。

再说是否精确。这个统计方法是采样的,只要是采样,肯定就不是百分之百精确。但由于查看 cpu 使用率的时候往往都是计算 1 秒甚至更长一段时间的使用情况,这其中会包含很多采样点,所以查看整体情况是问题不大的。另外也学到了 top 中输出的 cpu 时间项目其实大致可以分为三类:
第一类:用户态消耗时间,包括 user 和 nice。如果想看用户态的消耗,要将 user 和 nice 加起来看才对。
第二类:内核态消耗时间,包括 irq、softirq 和 system。
第三类:空闲时间,包括 io_wait 和 idle。其中 io_wait 也是 cpu 的空闲状态,只不过是在等 io 完成而已。如果只是想看 cpu 到底有多闲,应该把 io_wait 和 idle 加起来才对。