深入理解 Linux Load Average

一直不解,为什么io占用较高时,系统负载也会变高,偶遇此文,终解吾惑。

#1 load average介绍

##1.1 load average 指标介绍

uptime和top等命令都可以看到load average指标,从左至右三个数字分别表示1分钟、5分钟、15分钟的load average:


uptime

16:04:43 up 20 days, 6:08, 2 users, load average: 0.01, 0.00, 0.00

Load average的概念源自UNIX系统,虽然各家的公式不尽相同,但都是用于衡量正在使用CPU的进程数量和正在等待CPU的进程数量,一句话就是runnable processes的数量。所以load average可以作为CPU瓶颈的参考指标,如果大于CPU的数量,说明CPU可能不够用了。

但是, Linux上不是这样的!

Linux上的load average除了包括正在使用CPU的进程数量和正在等待CPU的进程数量之外,还包括uninterruptible sleep的进程数量。通常等待IO设备、等待网络的时候,进程会处于uninterruptible sleep状态。Linux设计者的逻辑是,uninterruptible sleep应该都是很短暂的,很快就会恢复运行,所以被等同于runnable。然而uninterruptible sleep即使再短暂也是sleep,何况现实世界中uninterruptible sleep未必很短暂,大量的、或长时间的uninterruptible sleep通常意味着IO设备遇到了瓶颈。众所周知,sleep状态的进程是不需要CPU的,即使所有的CPU都空闲,正在sleep的进程也是运行不了的,所以sleep进程的数量绝对不适合用作衡量CPU负载的指标,Linux把uninterruptible sleep进程算进load average的做法直接颠覆了load average的本来意义。所以在Linux系统上,load average这个指标基本失去了作用,因为你不知道它代表什么意思,当看到load average很高的时候,你不知道是runnable进程太多还是uninterruptible sleep进程太多,也就无法判断是CPU不够用还是IO设备有瓶颈。

参考资料:https://en.wikipedia.org/wiki/Load_(computing)

“Most UNIX systems count only processes in the running (on CPU) or runnable (waiting for CPU) states. However, Linux also includes processes in uninterruptible sleep states (usually waiting for disk activity), which can lead to markedly different results if many processes remain blocked in I/O due to a busy or stalled I/O system.“

##1.2 load_average 的含义

###1.2.1 如何衡量通车大桥的负荷

判断系统负荷是否过重,必须理解load average的真正含义。下面,我根据 “Understanding Linux CPU Load” 这篇文章,尝试用最通俗的语言,解释这个问题。

首先,假设最简单的情况,你的电脑只有一个CPU,所有的运算都必须由这个CPU来完成。

那么,我们不妨把这个CPU想象成一座大桥,桥上只有一根车道,所有车辆都必须从这根车道上通过。(很显然,这座桥只能单向通行。)

系统负荷为0,意味着大桥上一辆车也没有。

图片1

系统负荷为0.5,意味着大桥一半的路段有车。

图片 2

系统负荷为1.0,意味着大桥的所有路段都有车,也就是说大桥已经"满"了。但是必须注意的是,直到此时大桥还是能顺畅通行的。

图片 3

桥上在通车的时候, 不光桥上的车影响通车的效率, 后面排队等着还没有上桥的也对桥的拥挤程度有贡献, 因此我们有必要考虑这点, 如果把等待的那些车也算到负载中去, 那么负荷就会 > 1.0.

系统负荷为1.7,意味着车辆太多了,大桥已经被占满了(100%),后面等着上桥的车辆为桥面车辆的70%。以此类推,系统负荷2.0,意味着等待上桥的车辆与桥面的车辆一样多;系统负荷3.0,意味着等待上桥的车辆是桥面车辆的2倍。总之,当系统负荷大于1,后面的车辆就必须等待了;系统负荷越大,过桥就必须等得越久。

图片 4

###1.2.2 类比CPU的系统负荷

CPU的系统负荷,基本上等同于上面的类比。大桥的通行能力,就是CPU的最大工作量;桥梁上的车辆,就是一个个等待CPU处理的进程(process)。

如果CPU每分钟最多处理100个进程,那么系统负荷0.2,意味着CPU在这1分钟里只处理20个进程;系统负荷1.0,意味着CPU在这1分钟里正好处理100个进程;系统负荷1.7,意味着除了CPU正在处理的100个进程以外,还有70个进程正排队等着CPU处理。

为了电脑顺畅运行,系统负荷最好不要超过1.0,这样就没有进程需要等待了,所有进程都能第一时间得到处理。很显然,1.0是一个关键值,超过这个值,系统就不在最佳状态了,你要动手干预了。

###1.2.3 系统负荷的经验法则

1.0是系统负荷的理想值吗?

不一定,系统管理员往往会留一点余地,当这个值达到0.7,就应当引起注意了。经验法则是这样的:

当系统负荷持续大于0.7,你必须开始调查了,问题出在哪里,防止情况恶化。

当系统负荷持续大于1.0,你必须动手寻找解决办法,把这个值降下来。

当系统负荷达到5.0,就表明你的系统有很严重的问题,长时间没有响应,或者接近死机了。你不应该让系统达到这个值。

###1.2.4 多处理器的情形

上面,我们假设你的电脑只有1个CPU。如果你的电脑装了2个CPU,会发生什么情况呢?

2个CPU,意味着电脑的处理能力翻了一倍,能够同时处理的进程数量也翻了一倍。

还是用大桥来类比,两个CPU就意味着大桥有两根车道了,通车能力翻倍了。

图片 6

所以,2个CPU表明系统负荷可以达到2.0,此时每个CPU都达到100%的工作量。推广开来,n个CPU的电脑,可接受的系统负荷最大为n.0。

###1.2.5 多核处理器的情形

芯片厂商往往在一个CPU内部,包含多个CPU核心,这被称为多核CPU。

在系统负荷方面,多核CPU与多CPU效果类似,所以考虑系统负荷的时候,必须考虑这台电脑有几个CPU、每个CPU有几个核心。然后,把系统负荷除以总的核心数,只要每个核心的负荷不超过1.0,就表明电脑正常运行。

怎么知道电脑有多少个CPU核心呢?

"cat /proc/cpuinfo"命令,可以查看CPU信息。"grep -c ‘model name’ /proc/cpuinfo"命令,直接返回CPU的总核心数。

-###1.2.6 最佳观察时长

最后一个问题,"load average"一共返回三个平均值----1分钟系统负荷、5分钟系统负荷,15分钟系统负荷,----应该参考哪个值?

如果只有1分钟的系统负荷大于1.0,其他两个时间段都小于1.0,这表明只是暂时现象,问题不大。

如果15分钟内,平均系统负荷大于1.0(调整CPU核心数之后),表明问题持续存在,不是暂时现象。所以,你应该主要观察"15分钟系统负荷",将它作为电脑正常运行的指标。

#2 Loadavg分析

##2.1 读取 loadavg 的接口 /proc/loadavg

在内核中 /proc/loadavg 是通过 loadavg_proc_show 来读取相应数据,下面首先来看一下load_read_proc的实现:

# https://elixir.bootlin.com/linux/v5.2.13/source/kernel/sched/loadavg.c#L64

/**
 * get_avenrun - get the load average array
 * @loads: pointer to dest load array
 * @offset: offset to add
 * @shift: shift count to shift the result left
 *
 * These values are estimates at best, so no need for locking.
 */
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;
}

# http://elixir.bootlin.com/linux/v5.2.13/source/fs/proc/loadavg.c#13
static int loadavg_proc_show(struct seq_file *m, void *v)
{
    unsigned long avnrun[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,
        idr_get_cursor(&task_active_pid_ns(current)->idr) - 1);
    return 0;
}

几个宏的定义如下 :


# https://elixir.bootlin.com/linux/v5.2.13/source/include/linux/sched/loadavg.h#L43



#define FSHIFT 11 /* nr of bits of precision */

#define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */

#define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */

#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) */



#define LOAD_INT(x) ((x) >> FSHIFT)

#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)

根据输出格式,LOAD_INT对应计算的是load的整数部分,LOAD_FRAC计算的是load的小数部分。

将a=avenrun[0] + (FIXED_1/200)带入整数部分和小数部分计算可得:

$$avnrun_i = (avnrun_i + \frac{FIXED_1}{200}) <<11

= (avnrun_i+ 1>>11/200) <<11$$

= \frac{avnrun_i + \frac{2{11}}{200}}{2{11}}$$


LOAD_INT(a) = avnrun_i + \frac{2^{11}}{200}

LOAD_FRAC(a) = ((avenrun[0]%(2^11) + 2^11/200) * 100) / (2^11)
             = (((avenrun[0]%(2^11)) * 100 + 2^10) / (2^11)
             = ((avenrun[0]%(2^11) * 100) / (2^11) + ½

由上述计算结果可以看出,FIXED_1/200在这里是用于小数部分第三位的四舍五入,由于小数部分只取前两位,第三位如果大于5,则进一位,否则直接舍去。

临时变量a/b/c的低11位存放的为load的小数部分值,第11位开始的高位存放的为load整数部分。

因此可以得到a=load(1min) * 2^11

因此有: load(1min) * 2^11 = avenrun[0] + 2^11 / 200

进而推导出: load(1min)=avenrun[0]/(2^11) + 1/200

忽略用于小数部分第3位四舍五入的1/200,可以得到load(1min)=avenrun[0] / 2^11,即:

avenrun[0] = load(1min) * 2^11

avenrun是个陌生的量,这个变量是如何计算的,和系统运行进程、cpu之间的关系如何,在第二阶段进行分析。

##2.2 avenrun 如何表示CPU 负载

内核将 load的计算和load的查看进行了分离,avenrun就是用于连接load计算和load查看的桥梁。
下面开始分析通过avenrun进一步分析系统load的计算。

###2.2.1 avenrun 的更新

avenrun 数组是在 calc_global_load 中进行更新, 在系统更新了 calc_load_update 过了 10 个 jiffies 之后, 会在 do_timer 更新 jiffies 之后, 直接调用 calc_global_load 更新 avenrun 数组


do_timer

    -=> calc_global_load

# https://elixir.bootlin.com/linux/v5.2.13/source/kernel/sched/loadavg.c#L337
/*

 * calc_load - update the avenrun load estimates 10 ticks after the

 * CPUs have updated calc_load_tasks.

 *

 * Called from the global timer code.

 */

void calc_global_load(unsigned long ticks)

{

    unsigned long sample_window;

    long active, delta;



    sample_window = READ_ONCE(calc_load_update);

    if (time_before(jiffies, sample_window + 10))

        return;



    /*

     * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.

     */

    delta = calc_load_nohz_fold();

    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);



    WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);



    /*

     * In case we went to NO_HZ for multiple LOAD_FREQ intervals

     * catch up in bulk.

     */

    calc_global_nohz();

}

calc_load_tasks 可以理解为当前系统中 RUNNING(R状态)进程和 uninterruptible(D状态)进程的总数目.

active_tasks为系统中当前贡献load的task数nr_active乘于FIXED_1,用于计算avenrun

avenrun 的计算方法 calc_load 如下所示:


# https://elixir.bootlin.com/linux/v5.2.13/source/include/linux/sched/loadavg.h#L19

/*

 * a1 = a0 * e + a * (1 - e)

 */

static inline unsigned long

calc_load(unsigned long load, unsigned long exp, unsigned long active)

{

    unsigned long newload;



    newload = load * exp + active * (FIXED_1 - exp);

    if (active >= load)

        newload += FIXED_1-1;



    return newload / FIXED_1;

}

用avenrun(t-1)和avenrun(t)分别表示上一次计算的avenrun和本次计算的avenrun,则根据CALC_LOAD宏可以得到如下计算:

avenrun(t)=(avenrun(t-1) * EXP_N + nr_active * FIXED_1*(FIXED_1 – EXP_N)) / FIXED_1

      = avenrun(t-1) + (nr_active*FIXED_1 – avenrun(t-1)) * (FIXED_1 -EXP_N) / FIXED_1

推导出:

avenrun(t) – avenrun(t-1) = (nr_active*FIXED_1 – avenrun(t-1)) * (FIXED_1 – EXP_N) / FIXED_1

将第一阶段推导的结果代入上式,可得:

(load(t) – load(t-1)) * FIXED_1 = (nr_active – load(t-1)) * (FIXED_1 – EXP_N)

进一步得到nr_active变化和load变化之间的关系式:

load(t) – load(t-1) = (nr_active – load(t-1)) * (FIXED_1 – EXP_N) / FIXED_1

这个式子可以反映的内容包含如下两点:

1)当nr_active为常数时,load会不断的趋近于nr_active,趋近速率由快逐渐变缓

2)nr_active的变化反映在load的变化上是被降级了的,系统突然间增加10个进程,

1分钟load的变化每次只能够有不到1的增加(这个也就是权重的的分配)。

另外也可以通过将式子简化为:

load(t)= load(t-1) * EXP_N / FIXED_1 + nr_active * (1 - EXP_N/FIXED_1)

这样可以更加直观的看出nr_active和历史load在当前load中的权重关系 (多谢任震宇大师的指出)


#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分钟对应的EXP_N值如上,随着EXP_N的增大,(FIXED_1 – EXP_N)/FIXED_1值就越小,

这样nr_active的变化对整体load带来的影响就越小。对于一个nr_active波动较小的系统,load会

不断的趋近于nr_active,最开始趋近比较快,随着相差值变小,趋近慢慢变缓,越接近时越缓慢,并最

终达到nr_active。如下图所示:

文件:load 1515.jpg(无图)

也因此得到一个结论,load直接反应的是系统中的nr_active。 那么nr_active又包含哪些? 如何去计算

当前系统中的nr_active? 这些就涉及到了nr_active的采样。

###2.2.2 calc_load_tasks 的更新


calc_load_tasks 常规情况下在 tick 中进行更新.

this_rq->calc_load_active 记录了当前 RQ 上 RUNNING(R状态)线程和 uninterruptible(D状态)线程的总数.

delta 为上次更新到现在 this_rq 上 R+D 状态进程的增量情况.

calc_load_tasks 则保存了当前系统中进程 R+D 状态进程的总数目.

LOAD_FREQ 被定义成 5HZ+1(5S 之后), 是更新 this_rq->calc_load_active 和 全局的 calc_load_tasks 的时间间隔, 每 LOAD_FREQ 才会更新一次.

this_rq->calc_load_update 总表示当前RQ 上下次执行 calc_global_load_tick 时可以进行更新的时间

在 calc_global_load_tick 中

  1. 先检查 calc_load_update 到期没,

  2. 到期后,

先更新了 this_rq->calc_load_active

接着更新了全局的 calc_load_tasks.

最后设置 this_rq->calc_load_update 为 LOAD_FREQ(5S) 之后.

因此每隔 LOAD_FREQ的时间, 系统在calc_global_load_tick 中基于 this_rq->calc_load_active, 更新 全局的calc_load_tasks.


# https://elixir.bootlin.com/linux/v5.2.13/source/kernel/sched/loadavg.c#L79

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;

}



# https://elixir.bootlin.com/linux/v5.2.13/source/kernel/sched/loadavg.c#L369

/*

 * Called from scheduler_tick() to periodically update this CPU's

 * active count.

 */

void calc_global_load_tick(struct rq *this_rq)

{

    long delta;



    if (time_before(jiffies, this_rq->calc_load_update))

        return;



    delta = calc_load_fold_active(this_rq, 0);

    if (delta)

        atomic_long_add(delta, &calc_load_tasks);



    this_rq->calc_load_update += LOAD_FREQ;

}

而 avenrun 则在 calc_load_update 更新 10 ticks 之后通过 calc_global_load 更新.

#3 参考资料

https://www.cnblogs.com/qqmomery/p/6267429.html

http://linuxperf.com/?p=176

https://scoutapm.com/blog/understanding-load-averages

http://www.ruanyifeng.com/blog/2011/07/linux_load_average_explained.html

http://www.blogjava.net/cenwenchu/archive/2008/06/30/211712.html

https://www.cnblogs.com/qqmomery/p/6267429.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章