深入理解 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

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