Jamal的博客

Linux load_average解析

以下是工作中学习到的一些技巧和原理性的介绍,主要来自于各个同事写的文章知识总结,对同事们表示感谢

另外微信公众号上也有一篇相关的文章:文章地址

Linux load的准确含义

日常运维中我们经常会碰到Linux系统load过高的问题,但是什么是load,load是怎么得到的,以及在load高的时候应该是怎样的排查思路,都没有一个很好的说明文档,在这里,尝试全面阐述一下Linux load的计算原理和相关的排查方法。
日常我们在排查问题的时候经常会使用到的就是top uptime等命令,实际上这些命令都是通过/proc/loadavg获取的,我们可以使用如下的命令进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@iZhp357raozla15gapy0h0Z hexo]# rpm -ql procps-ng | grep bin
/usr/bin/free
/usr/bin/pgrep
/usr/bin/pkill
/usr/bin/pmap
/usr/bin/ps
/usr/bin/pwdx
/usr/bin/skill
/usr/bin/slabtop
/usr/bin/snice
/usr/bin/tload
/usr/bin/top
/usr/bin/uptime
/usr/bin/vmstat
/usr/bin/w
/usr/bin/watch
/usr/sbin/sysctl

实际上都是来自procps-ng这个rpm包中。
/proc/目录中mount的是Linux的伪文件系统,主要是被用作和内核数据结构的接口,我们查看其中loadavg的含义:

1
2
3
4
5
6
7
/proc/loadavg
The first three fields in this file are load average figures giving the number of jobs in the run queue (state R) or waiting for
disk I/O (state D) averaged over 1, 5, and 15 minutes. They are the same as the load average numbers given by uptime(1) and other
programs. The fourth field consists of two numbers separated by a slash (/). The first of these is the number of currently exe-
cuting kernel scheduling entities (processes, threads); this will be less than or equal to the number of CPUs. The value after the
slash is the number of kernel scheduling entities that currently exist on the system. The fifth field is the PID of the process
that was most recently created on the system.

注意centos7之后的系统(未验证过)已经取消了这个的man信息
从这段话中我们可以总结以下信息:

  • /proc/loadavg前三个值表示的是load1 load5 load15的值
  • load值代表的是对应时间内jobs的平均数量,比如load1表示过去1min内jobs数量的平均值,这里的jobs其实应该是内核中的tasks或者用户态的threads概念
  • load计算的值只包括状态为R和D的两种jobs,其他不包含在内,其中R表示运行队列,D表示在等待磁盘(具体的进程的状态转移请参考另一篇文章)

load分析工具

在这里我们先抛开其他因素,先看一下日常会用到的脚本分析工具:

1
2
#!/bin/sh
ps -e -L h o state,ucmd | awk '{if($1=="R"||$1=="D"){print $0}}' | sort | uniq -c | sort -k 1nr

简单介绍一下几个参数:

  • -e表示显示当前系统中的所有进程
  • -L表示对每一个进程都展示所包含的所有线程,每个线程一行
  • h表示隐藏ps命令第一行的header标题信息
  • o state,ucmd,这里他们是组合在一起生效的,只输出state和ucmd这两列信息,state表示线程状态,ucmd表示线程的名称
    找一台比较繁忙的机器运行下脚本,我这边起了一个打cpu的进程,代码就是微信公众号文章的那个代码,输出结果:
1
2
3
4
5
6
7
# ./loadprocess.sh
101 R cpuburn
1 R AliYunDun
1 R ps
1 R rcu_sched
# uptime
15:58:35 up 4 min, 2 users, load average: 83.50, 29.94, 10.80

这里发现脚本输出的第一列相加在104,和uptime load1输出的差不多。此时如果想要将load1值拆解到具体的线程级别上,就可以看到影响这个load输出结果的最重要因素是这个cpuburn的,状态是R,数量上贡献了101个。

内核里load是怎么计算的

注:这一段是看其他同学做的分析的结果,我自己本身并未做相关的分析实验,此处只是拿其他同学的分析结果。
首先来看下/proc/loadavg伪文件对应的内核代码:

1
2
3
4
5
6
7
8
9
10
11
12
......
unsigned long avnrun[3], nr_runnable = 0;
......
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_runnable, nr_threads,
task_active_pid_ns(current)->last_pid);
......

预编译之后如下:

1
2
3
4
5
6
7
8
9
10
......
get_avenrun(avnrun, (1<<11)/200, 0);
......
seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %ld/%d %d\n",
((avnrun[0]) >> 11), ((((avnrun[0]) & ((1<<11)-1)) * 100) >> 11),
((avnrun[1]) >> 11), ((((avnrun[1]) & ((1<<11)-1)) * 100) >> 11),
((avnrun[2]) >> 11), ((((avnrun[2]) & ((1<<11)-1)) * 100) >> 11),
nr_runnable, nr_threads,
task_active_pid_ns(get_current())->last_pid);
......

LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0])预编译之后是((avnrun[0]) >> 11), ((((avnrun[0]) & ((1<<11)-1)) *="" 100)="">> 11)。由于内核没有小数计算,这段代码本质上是实现了avnrun[0]除以2048且取2位小数的浮点运算
get_avenrun函数定义在kernel/sched/core.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static atomic_long_t calc_load_tasks;
static unsigned long calc_load_update;
unsigned long avenrun[3];
void get_avenrun(unsigned long *loads, unsigned long offset, int shift)
{
loads[0] = (avenrun[0] + offset) << shift;
loads[1] = (avenrun[1] + offset) << shift;
loads[2] = (avenrun[2] + offset) << shift;
}
static long calc_load_fold_active(struct rq *this_rq)
{
long nr_active, delta = 0;
nr_active = this_rq->nr_running;
nr_active += (long) 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;
}
return delta;
}
unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
load *= exp;
load += active * (FIXED_1 - exp);
load += 1UL << (FSHIFT - 1);
return load >> FSHIFT;
}
void calc_global_load(unsigned long ticks)
{
long active, delta;
if (time_before(jiffies, calc_load_update + 10))
return;
delta = calc_load_fold_idle();
if (delta)
atomic_long_add(delta, &calc_load_tasks);
active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;
avenrun[0] = calc_load(avenrun[0], EXP_1, active);
avenrun[1] = calc_load(avenrun[1], EXP_5, active);
avenrun[2] = calc_load(avenrun[2], EXP_15, active);
#ifdef CONFIG_CGROUP_CPUACCT
cpuacct_cgroup_walk_tree(NULL);
#endif
calc_load_update += LOAD_FREQ;
calc_global_nohz();
}
static void calc_load_account_active(struct rq *this_rq)
{
long delta;
if (time_before(jiffies, this_rq->calc_load_update))
return;
delta = calc_load_fold_active(this_rq);
if (delta)
atomic_long_add(delta, &calc_load_tasks);
this_rq->calc_load_update += LOAD_FREQ;
}

预编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
void get_avenrun(unsigned long *loads, unsigned long offset, int shift)
{
loads[0] = (avenrun[0] + offset) << shift;
loads[1] = (avenrun[1] + offset) << shift;
loads[2] = (avenrun[2] + offset) << shift;
}
static long calc_load_fold_active(struct rq *this_rq)
{
long nr_active, delta = 0;
nr_active = this_rq->nr_running;
nr_active += (long) 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;
}
return delta;
}
unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
load *= exp;
load += active * ((1<<11) - exp);
load += 1UL << (11 - 1);
return load >> 11;
}
void calc_global_load(unsigned long ticks)
{
long active, delta;
if ((({ unsigned long __dummy; typeof(calc_load_update + 10) __dummy2; (void)(&__dummy == &__dummy2); 1; }) && ({ unsigned long __dummy; typeo
f(jiffies) __dummy2; (void)(&__dummy == &__dummy2); 1; }) && ((long)(jiffies) - (long)(calc_load_update + 10) < 0)))
return;
delta = calc_load_fold_idle();
if (delta)
atomic_long_add(delta, &calc_load_tasks);
active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * (1<<11) : 0;
avenrun[0] = calc_load(avenrun[0], 1884, active);
avenrun[1] = calc_load(avenrun[1], 2014, active);
avenrun[2] = calc_load(avenrun[2], 2037, active);
cpuacct_cgroup_walk_tree(((void *)0));
calc_load_update += (5*1000 +1);
calc_global_nohz();
}
static void calc_load_account_active(struct rq *this_rq)
{
long delta;
if ((({ unsigned long __dummy; typeof(this_rq->calc_load_update) __dummy2; (void)(&__dummy == &__dummy2); 1; }) && ({ unsigned long __dummy; t
ypeof(jiffies) __dummy2; (void)(&__dummy == &__dummy2); 1; }) && ((long)(jiffies) - (long)(this_rq->calc_load_update) < 0)))
return;
delta = calc_load_fold_active(this_rq);
if (delta)
atomic_long_add(delta, &calc_load_tasks);
this_rq->calc_load_update += (5*1000 +1);
}

我们可以看到get_avenrun函数通过avenrun全局数组变量,返回上面的avnrun数组变量。avenrun全局数组变量在calc_global_load函数中每隔5001毫秒,由calc_load函数将active值添加到原有的load值中,进而产生新的load值。其中load1、load5和load15的区别只是第二个exp参数传入给calc_load函数不同的值,依次为1884、2014和2037。这里active值从calc_load_tasks全局结构体变量获取。calc_load_tasks全局结构体变量在calc_load_account_active函数中设置,而整个值的最初来源是calc_load_fold_active函数。
  在函数calc_load_fold_active中,我们可以看到最终获取的是rq(run queue)队列中的this_rq->nr_running和this_rq->nr_uninterruptible两种状态的task数。熟悉cpu调度算法的同学知道,这里实际上是把每个cpu队列里的nr_running和nr_uninterruptible值都汇总到一起。而nr_running和nr_uninterruptible正好对应于用户空间中的R和D两种状态的线程的数量。
  回头再看前面man page说明中的jobs概念显然是不对的,正确的应该是tasks概念(用户空间对应threads概念)。
  load2process输出结果中第一列之和、load5s和load1值之间的关系也一目了然。load2process第一列之和是运行这个脚本的瞬时nr_running和nr_uninterruptible状态线程数之和。load5s值是每5001毫秒对nr_running和nr_uninterruptible状态线程数之和的一个采样。load1是对之前历史所有load5s采样值按某种算法的平均值。   

如何分析load高的问题

上面我们已经基本搞清楚了load的原理,也有了load分析的工具,接下来我们利用上述的工具来具体看看怎么分析load的问题。
前面我们说到,load的计算过程中,取得是R和D两种状态的数据进行计算,其中R状态代表的是running状态的线程,D状态代表的是uninterruptible状态的线程,具体的进程状态请参考Linux进程状态
而事实上,R和D状态对系统的影响度是完全不同的,一般来说:

  • R状态导致的load高,主要和CPU核数有关,大于CPU核数2倍以上,就会出现严重问题,出现CPU争抢问题
  • D状态的数据,则在load高的时候,系统有可能还是能正常服务,只是在读写操作上需要等待
    因此我们需要分开监控R和D状态,kernel为我们提供了这样的数据:
1
2
# cat /proc/loadavg
101.00 100.62 84.29 103/199 1699

在内核中,还提供了另外的几个参数:103代表此时正在运行的R状态的线程数。
另外,我们在日常运维的过程中,经常会在load过高的情况下去top看一下CPU占用,但其实只有在R状态的时候才耗费CPU,如果load高是因为D状态导致的,那么这时候去看top是无效的,即使是由R状态导致的,如果在运行top的时候R状态已经停止了,那么也是看不到相关信息的,默认情况下,top是3s刷新一次,显示3s内的CPU信息。
我们总结上述的几种规律:

  • 单线程R状态过高导致load高,这种一般都是我们自己代码写的问题
  • 多线程R状态高导致load高,这种不多出现
  • 单线程D状态过高导致load高,可以查看当前这个程序对应进程的waiting channel信息:/proc//wchan,表示当前线程在这个函数位置,D状态导致load高的原因有很多种,需要具体分析
  • 多个线程D状态高导致load高,需要具体分析,可能是系统或者磁盘损坏了。
    在这里,也分享几个查看load高的时候排查的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
系统load高时排查处于运行队列的进程
#!/bin/bash
LANG=C
PATH=/sbin:/usr/sbin:/bin:/usr/bin
interval=1
length=86400
for i in $(seq 1 $(expr ${length} / ${interval}))
do
date
LANG=C ps -eTo stat,pid,tid,ppid,comm --no-header | sed -e 's/^ \*//' | perl -nE 'chomp;say if (m!^\S*[RD]+\S*!)'
date
cat /proc/loadavg
echo -e "\n"
sleep ${interval}
done
查CPU使用率比较高的线程小脚本
#!/bin/bash
LANG=C
PATH=/sbin:/usr/sbin:/bin:/usr/bin
interval=1
length=86400
for i in $(seq 1 $(expr ${length} / ${interval}))
do
date
LANG=C ps -eT -o%cpu,pid,tid,ppid,comm | grep -v CPU | sort -n -r | head -20
date
LANG=C cat /proc/loadavg
{ LANG=C ps -eT -o%cpu,pid,tid,ppid,comm | sed -e 's/^ *//' | tr -s ' ' | grep -v CPU | sort -n -r | cut -d ' ' -f 1 | xargs -I{} echo -n "{} + " && echo ' 0'; } | bc -l
sleep ${interval}
done
fuser -k $0