linux loadavg詳解(top cpu load)

目錄

 [ 隱藏]

Loadavg分析

Loadavg淺述

cat /proc/loadavg能夠看到當前系統的load 
$ cat /proc/loadavg 
0.01 0.02 0.05 2/317 26207 
前面三個值分別對應系統當前1分鐘、5分鐘、15分鐘內的平均load。load用於反映當前系統的負載狀況,對於16核的系統,若是每一個核上cpu利用率爲30%,則在不存在uninterruptible進程的狀況下,系統load應該維持在4.8左右。對16核系統,若是load維持在16左右,在不存在uninterrptible進程的狀況下,意味着系統CPU幾乎不存在空閒狀態,利用率接近於100%。結合iowait、vmstat和loadavg能夠分析出系統當前的總體負載,各部分負載分佈狀況。php

Loadavg讀取

在內核中/proc/loadavg是經過load_read_proc來讀取相應數據,下面首先來看一下load_read_proc的實現:前端

fs/proc/proc_misc.c
static int loadavg_read_proc(char *page, char **start, off_t off, 
                                 int count, int *eof, void *data) 
{ 
        int a, b, c; 
        int len; 

        a = avenrun[0] + (FIXED_1/200); 
        b = avenrun[1] + (FIXED_1/200); 
        c = avenrun[2] + (FIXED_1/200); 
        len = sprintf(page,"%d.%02d %d.%02d %d.%02d %ld/%d %d\n", 
                LOAD_INT(a), LOAD_FRAC(a), 
                LOAD_INT(b), LOAD_FRAC(b), 
                LOAD_INT(c), LOAD_FRAC(c), 
                nr_running(), nr_threads, last_pid); 
        return proc_calc_metrics(page, start, off, count, eof, len); 
}

 

幾個宏定義以下:linux

#define FSHIFT          11              /* nr of bits of precision */ 
#define FIXED_1         (1<<FSHIFT)     /* 1.0 as fixed-point */ 
#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)帶入整數部分和小數部分計算可得:數組

LOAD_INT(a) = avenrun[0]/(2^11) + 1/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之間的關係如何,在第二階段進行分析。函數

Loadavg和進程之間的關係

內核將load的計算和load的查看進行了分離,avenrun就是用於鏈接load計算和load查看的橋樑。 
下面開始分析經過avenrun進一步分析系統load的計算。 
avenrun數組是在calc_load中進行更新測試

kernel/timer.c
/* 
* calc_load - given tick count, update the avenrun load estimates. 
* This is called while holding a write_lock on xtime_lock. 
*/ 
static inline void calc_load(unsigned long ticks) 
{ 
        unsigned long active_tasks; /* fixed-point */ 
        static int count = LOAD_FREQ;  
        count -= ticks; 
        if (count < 0) { 
                count += LOAD_FREQ; 
                active_tasks = count_active_tasks(); 
                CALC_LOAD(avenrun[0], EXP_1, active_tasks); 
                CALC_LOAD(avenrun[1], EXP_5, active_tasks); 
                CALC_LOAD(avenrun[2], EXP_15, active_tasks); 
        } 
}
static unsigned long count_active_tasks(void) 
{ 
        return nr_active() * FIXED_1; 
}
#define LOAD_FREQ       (5*HZ)          /* 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) */

 

calc_load在每一個tick都會執行一次,每一個LOAD_FREQ(5s)週期執行一次avenrun的更新。 
active_tasks爲系統中當前貢獻load的task數nr_active乘於FIXED_1,用於計算avenrun。宏CALC_LOAD定義以下:this

#define CALC_LOAD(load,exp,n) \ 
       load *= exp; \ 
       load += n*(FIXED_1-exp); \ 
       load >>= FSHIFT;

用avenrun(t-1)和avenrun(t)分別表示上一次計算的avenrun和本次計算的avenrun,則根據CALC_LOAD宏能夠獲得以下計算:spa

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的採樣。

Loadavg採樣

nr_active直接反映的是爲系統貢獻load的進程總數,這個總數在nr_active函數中計算:

kernel/sched.c
unsigned long nr_active(void) 
{ 
        unsigned long i, running = 0, uninterruptible = 0; 

        for_each_online_cpu(i) { 
                running += cpu_rq(i)->nr_running; 
                uninterruptible += cpu_rq(i)->nr_uninterruptible; 
        } 

        if (unlikely((long)uninterruptible < 0)) 
                uninterruptible = 0; 

        return running + uninterruptible; 
}

#define TASK_RUNNING            0 
#define TASK_INTERRUPTIBLE      1 
#define TASK_UNINTERRUPTIBLE    2 
#define TASK_STOPPED            4 
#define TASK_TRACED             8 
/* in tsk->exit_state */ 
#define EXIT_ZOMBIE             16 
#define EXIT_DEAD               32 
/* in tsk->state again */ 
#define TASK_NONINTERACTIVE     64

 

該函數反映,爲系統貢獻load的進程主要包括兩類,一類是TASK_RUNNING,一類是TASK_UNINTERRUPTIBLE。
當5s採樣週期到達時,對各個online-cpu的運行隊列進行遍歷,取得當前時刻該隊列上running和uninterruptible的
進程數做爲當前cpu的load,各個cpu load的和即爲本次採樣獲得的nr_active。

下面的示例說明了在2.6.18內核狀況下loadavg的計算方法:

18內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load
0HZ+10 1 1 1 0 0 0 0 0 0
5HZ 0 0 0 0 1 1 1 1 4
5HZ+1 0 1 1 1 0 0 0 0 0
5HZ+9 0 0 0 0 0 1 1 1 0
5HZ+11 1 1 1 0 0 0 0 0 0

 

 

18內核計算loadavg存在的問題

xtime_lock解析

內核在5s週期執行一次全局load的更新,這些都是在calc_load函數中執行。追尋calc_load的調用:

kernel/timer.c
static inline void update_times(void) 
{  
        unsigned long ticks; 

        ticks = jiffies - wall_jiffies; 
        wall_jiffies += ticks; 
        update_wall_time(); 
        calc_load(ticks); 
}

 

update_times中更新系統wall time,而後執行全局load的更新。

kernel/timer.c
void do_timer(struct pt_regs *regs) 
{  
        jiffies_64++; 
        /* prevent loading jiffies before storing new jiffies_64 value. */ 
        barrier(); 
        update_times(); 
}

 

do_timer中首先執行全局時鐘jiffies的更新,而後是update_times。

void main_timer_handler(struct pt_regs *regs) 
{ 
...
        write_seqlock(&xtime_lock);
...
        do_timer(regs); 
#ifndef CONFIG_SMP 
        update_process_times(user_mode(regs)); 
#endif 
...
        write_sequnlock(&xtime_lock); 
}

 

對wall_time和全局jiffies的更新都是在加串行鎖(sequence lock)xtime_lock以後執行的。

include/linux/seqlock.h
static inline void write_seqlock(seqlock_t *sl) 
{ 
        spin_lock(&sl->lock);
        ++sl->sequence; 
        smp_wmb(); 
} 

static inline void write_sequnlock(seqlock_t *sl) 
{ 
        smp_wmb(); 
        sl->sequence++; 
        spin_unlock(&sl->lock); 
} 
 
typedef struct { 
        unsigned sequence; 
        spinlock_t lock; 
} seqlock_t;

 

sequence lock內部保護一個用於計數的sequence。Sequence lock的寫鎖是經過spin_lock實現的, 
在spin_lock後對sequence計數器執行一次自增操做,而後在鎖解除以前再次執行sequence的自增操做。 
sequence初始化時爲0。這樣,當鎖內部的sequence爲奇數時,說明當前該sequence lock的寫鎖正被拿, 
讀和寫可能不安全。若是在寫的過程當中,讀是不安全的,那麼就須要在讀的時候等待寫鎖完成。對應讀鎖使用以下:

#if (BITS_PER_LONG < 64) 
u64 get_jiffies_64(void) 
{ 
        unsigned long seq; 
        u64 ret;  

        do { 
                seq = read_seqbegin(&xtime_lock); 
                ret = jiffies_64; 
        } while (read_seqretry(&xtime_lock, seq)); 
        return ret; 
} 

EXPORT_SYMBOL(get_jiffies_64); 
#endif 

 

讀鎖實現以下:

static __always_inline unsigned read_seqbegin(const seqlock_t *sl) 
{ 
        unsigned ret = sl->sequence; 
        smp_rmb(); 
        return ret; 
} 

static __always_inline int read_seqretry(const seqlock_t *sl, unsigned iv) 
{ 
        smp_rmb(); 
        /*iv爲讀以前的鎖計數器
        * 當iv爲基數時,說明讀的過程當中寫鎖被拿,可能讀到錯誤值
        * 當iv爲偶數,可是讀完以後鎖的計數值和讀以前不一致,則說明讀的過程當中寫鎖被拿,
        * 也可能讀到錯誤值。
        */
        return (iv & 1) | (sl->sequence ^ iv);  
}

 

至此xtime_lock的實現解析完畢,因爲對應寫鎖基於spin_lock實現,多個程序競爭寫鎖時等待者會一直循環等待, 
當鎖裏面處理時間過長,會致使整個系統的延時增加。另外,若是系統存在不少xtime_lock的讀鎖,在某個程 
序獲取該寫鎖後,讀鎖就會進入相似spin_lock的循環查詢狀態,直到保證能夠讀取到正確值。所以須要儘量 
短的減小在xtime_lock寫鎖之間執行的處理流程。

全局load讀寫分離解xtime_lock問題

在計算全局load函數calc_load中,每5s須要遍歷一次全部cpu的運行隊列,獲取對應cpu上的load。1)因爲cpu個數是不固 
定的,形成calc_load的執行時間不固定,在覈數特別多的狀況下會形成xtime_lock獲取的時間過長。2)calc_load是 
每5s一次的採樣程序,自己並不可以精度特別高,對全局avenrun的讀和寫之間也不須要專門的鎖保護,能夠將全局load的 
更新和讀進行分離。 
Dimitri Sivanich提出在他們的large SMP系統上,因爲calc_load須要遍歷全部online CPU,形成系統延遲較大。 
基於上述緣由Thomas Gleixnert提交了下述patch對該bug進行修復:

[Patch 1/2] sched, timers: move calc_load() to scheduler
[Patch 2/2] sched, timers: cleanup avenrun users

文件:rw isolate.jpg

Thomas的兩個patch,主要思想如上圖所示。首先將全局load的計算分離到per-cpu上,各個cpu上計算load時不加xtime_lock 
的鎖,計算的load更新到全局calc_load_tasks中,全部cpu上load計算完後calc_load_tasks即爲總體的load。在5s定 
時器到達時執行calc_global_load,讀取全局cacl_load_tasks,更新avenrun。因爲只是簡單的讀取calc_load_tasks, 
執行時間和cpu個數沒有關係。

幾個關鍵點:

不加xtime_lock的per cpu load計算

在不加xtime_lock的狀況下,如何保證每次更新avenrun時候讀取的calc_load_tasks爲全部cpu已經更新以後的load?

Thomas的解決方案

Thomas的作法是將定時器放到sched_tick中,每一個cpu都設置一個LOAD_FREQ定時器。 
定時週期到達時執行當前處理器上load的計算。sched_tick在每一個tick到達時執行 
一次,tick到達是由硬件進行控制的,客觀上不受系統運行情況的影響。

sched_tick的時機

將per-cpu load的計算放至sched_tick中執行,第一反應這不是又回到了時間處理中斷之間,是否依舊 
存在xtime_lock問題? 下面對sched_tick進行分析(如下分析基於linux-2.6.32-220.17.1.el5源碼)

static void update_cpu_load_active(struct rq *this_rq) 
{ 
        update_cpu_load(this_rq); 

        calc_load_account_active(this_rq); 
}
 
void scheduler_tick(void) 
{ 
        int cpu = smp_processor_id(); 
        struct rq *rq = cpu_rq(cpu); 
...
        spin_lock(&rq->lock); 
...
        update_cpu_load_active(rq); 
...
        spin_unlock(&rq->lock); 

...
} 
 
void update_process_times(int user_tick) 
{ 
...
        scheduler_tick(); 
...
}
 
static void tick_periodic(int cpu) 
{ 
        if (tick_do_timer_cpu == cpu) { 
                write_seqlock(&xtime_lock); 

                /* Keep track of the next tick event */ 
                tick_next_period = ktime_add(tick_next_period, tick_period); 
           
                do_timer(1);  // calc_global_load在do_timer中被調用
                write_sequnlock(&xtime_lock); 
        } 
 
        update_process_times(user_mode(get_irq_regs())); 
...
}
 
void tick_handle_periodic(struct clock_event_device *dev) 
{ 
        int cpu = smp_processor_id(); 
...
        tick_periodic(cpu); 
...
}

 

交錯的時間差

將per-cpu load的計算放到sched_tick中後,還存在一個問題就是什麼時候執行per-cpu上的load計算,如何保證更新全 
局avenrun時讀取的全局load爲全部cpu都計算以後的? 當前的方法是給全部cpu設定一樣的步進時間LOAD_FREQ, 
過了這個週期點當有tick到達則執行該cpu上load的計算,更新至全局的calc_load_tasks。calc_global_load 
的執行點爲LOAD_FREQ+10,即在全部cpu load計算執行完10 ticks以後,讀取全局的calc_load_tasks更新avenrun。

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+10 0 0 0 0 0 0 0 0 0
5HZ 1 1 1 1 1 1 1 1 0
5HZ+1 0 1 1 1 0 0 0 0 0
    +1 +1 +1         1+1+1=3
5HZ+11 0 1 1 1 0 0 0 0 3
calc_global_load <-- -- -- -- -- -- -- -- 3

 

經過將calc_global_load和per-cpu load計算的時間進行交錯,能夠避免calc_global_load在各個cpu load計算之間執行, 
致使load採樣不許確問題。

32內核Load計數nohz問題

一個問題的解決,每每伴隨着無數其餘問題的誕生!Per-cpu load的計算可以很好的分離全局load的更新和讀取,避免大型系統中cpu 
核數過多致使的xtime_lock問題。可是也同時帶來了不少其餘須要解決的問題。這其中最主要的問題就是nohz問題。

爲避免cpu空閒狀態時大量無心義的時鐘中斷,引入了nohz技術。在這種技術下,cpu進入空閒狀態以後會關閉該cpu對應的時鐘中斷,等 
到下一個定時器到達,或者該cpu須要執行從新調度時再從新開啓時鐘中斷。

cpu進入nohz狀態後該cpu上的時鐘tick中止,致使sched_tick並不是每一個tick都會執行一次。這使得將per-cpu的load計算放在 
sched_tick中並不能保證每一個LOAD_FREQ都執行一次。若是在執行per-cpu load計算時,當前cpu處於nohz狀態,那麼當 
前cpu上的sched_tick就會錯過,進而錯過此次load的更新,最終全局的load計算不許確。 
基於Thomas第一個patch的思想,能夠在cpu調度idle時對nohz狀況進行處理。採用的方式是在當前cpu進入idle前進行一次該cpu 
上load的更新,這樣即使進入了nohz狀態,該cpu上的load也已經更新至最新狀態,不會出現不更新的狀況。以下圖所示:

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+11 1 1 1 0 0 0 0 0 3
5HZ 0 0 0 0 3 2 1 3 0
  -1 -1 -1           3-3=0
5HZ+1 0 1 1 1 1 1 1 1 1
    +1 +1 +1 +1 +1 +1 +1 0+1+...+1=7
5HZ+11 0 1 1 1 1 1 1 1 7
calc_global_load <-- -- -- -- -- -- -- -- 7

 

理論上,該方案很好的解決了nohz狀態致使全局load計數可能不許確的問題,事實上這倒是一個苦果的開始。大量線上應用反饋 
最新內核的load計數存在問題,在16核機器cpu利用率平均爲20%~30%的狀況下,總體load卻始終低於1。

解決方案

接到咱們線上報告load計數偏低的問題以後,進行了研究。最初懷疑對全局load計數更新存在競爭。對16核的系統,若是都沒有進入 
nohz狀態,那麼這16個核都將在LOAD_FREQ週期到達的那個tick內執行per-cpu load的計算,並更新到全局的load中,這 
之間若是存在競爭,總體計算的load就會出錯。當前每一個cpu對應rq都維護着該cpu上一次計算的load值,若是發現本次計算load 
和上一次維護的load值之間差值爲0,則不用更新全局load,不然將差值更新到全局load中。正是因爲這個機制,全局load若是被 
篡改,那麼在各個cpu維護着本身load的狀況下,全局load最終將可能出現負值。而負值經過各類觀察,並無在線上出現,最終競 
爭條件被排除。

經過/proc/sched_debug對線上調度信息進行分析,發現每一個時刻在cpu上運行的進程基本維持在2~3個,每一個時刻運行有進程的cpu都 
不同。進一步分析,每一個cpu上平均每秒出現sched_goidle的狀況大概爲1000次左右。所以獲得線上每次進入idle的間隔爲1ms/次。 
結合1HZ=1s=1000ticks,能夠獲得1tick =1ms。因此能夠獲得線上應用基本每個tick就會進入一次idle!!! 這個發現就比如 
原來一直用肉眼看一滴水,看着那麼完美那麼純淨,忽然間給你眼前架了一個放大鏡,一下出現各類凌亂的雜碎物。 在原有的世界裏, 
10ticks是那麼的短暫,一個進程均可能沒有運行完成,現在發現10ticks內調度idle的次數就會有近10次。接着用例子對應用場景進行分析:

 

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+11 1 1 1 0 0 0 0 0 3
5HZ 0 0 0 1 1 1 0 0  
  -1 -1 -1           3-3=0
5HZ+1 1 0 0 0 0 0 1 1  
  +1           +1 +1 0+1+1+1=3
5HZ+3 0 1 1 1 0 0 0 0 3
  -1           -1 -1 3-1-1-1=0
5HZ+5 0 0 0 0 1 1 1 0 0
5HZ+11 1 0 0 0 0 0 1 1 0
calc_global_load <-- -- -- -- -- -- -- -- 0

(說明:可能你注意到了在5HZ+5到5HZ+11過程當中也有CPU從非idle進入了idle,可是爲何沒有-1,這裏是因爲每一個cpu都保留 
了一份該CPU上一次計算時的load,若是load沒有變化則不進行計算,這幾個cpu上一次計算load爲0,並無變化)

Orz!load爲3的狀況直接算成了0,難怪系統總體load會偏低。這裏面的一個關鍵點是:對已經計算過load的cpu,咱們對idle進 
行了計算,卻從未考慮過這給從idle進入非idle的狀況帶來的不公平性。這個是當前線上2.6.32系統存在的問題。在定位到問題 
以後,跟進到upstream中發現Peter Z針對該load計數問題前後提交了三個patch,最新的一個patch是在4月份提交。這三個 
patch以下:

[Patch] sched: Cure load average vs NO_HZ woes
[Patch] sched: Cure more NO_HZ load average woes
[Patch] sched: Fix nohz load accounting – again!

這是目前咱們backport的patch,基本思想是將進入idle形成的load變化暫時記錄起來,不是每次進入idle都致使全局load的更新。 
這裏面的難點是何時將idle更新至全局的load中?在最開始計算per-cpu load的時候須要將以前全部的idle都計算進來, 
因爲目前各個CPU執行load計算的前後順序暫時沒有定,因此將這個計算放在每一個cpu裏面都計算一遍是一種方法。接着用示例進行說明:

 

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks tasks_idle
0HZ+11 1 1 1 0 0 0 0 0 3 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3
5HZ+1 1 0 0 0 0 0 1 1 3
  +1           +1 +1 3-3+1+1+1=3 0
5HZ+3 0 1 1 1 0 0 0 0 3
5HZ+3 -1           -1 -1 3 -1-1-1=-3
5HZ+5 0 0 0 0 1 1 1 0 3
5HZ+11 1 0 0 0 0 0 1 1 3
calc_global_load <-- -- -- -- -- -- -- -- 3 -3


至此這三個patch可以很好的處理咱們的以前碰到的進入idle的問題。 
將上述三個patch整理完後,在淘客前端線上機器中進行測試,測試結果代表load獲得了明顯改善。

更細粒度的時間問題

將上述三個patch整理完後,彷佛一切都完美了,idle進行了很好的處理,全局load的讀寫分離也很好實現。然而在業務線上的測試結果卻出乎意料,雖然添加patch以後load計數較以前有明顯改善,可是依舊偏低。下面是一個抓取的trace數據(粗體爲pick_next_idle):

<...>-9195 [000] 11994.232382: calc_global_load: calc_load_task = 0
<...>-9198 [000] 11999.213365: calc_load_account_active: cpu 0 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 1
<...>-9199 [001] 11999.213379: calc_load_account_active: cpu 1 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 2
<...>-9194 [002] 11999.213394: calc_load_account_active: cpu 2 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 3 
<...>-9198 [000] 11999.213406: calc_load_account_active: cpu 0 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 2
<...>-9201 [003] 11999.213409: calc_load_account_active: cpu 3 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 3
<...>-9190 [004] 11999.213424: calc_load_account_active: cpu 4 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 4
<...>-9197 [005] 11999.213440: calc_load_account_active: cpu 5 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 5
<...>-9194 [002] 11999.213448: calc_load_account_active: cpu 2 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 4
<...>-9203 [006] 11999.213455: calc_load_account_active: cpu 6 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 5
<...>-9202 [007] 11999.213471: calc_load_account_active: cpu 7 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 6
<...>-9195 [008] 11999.213487: calc_load_account_active: cpu 8 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 7
<...>-9204 [009] 11999.213502: calc_load_account_active: cpu 9 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 8
<...>-9190 [004] 11999.213517: calc_load_account_active: cpu 4 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 7
<...>-9192 [010] 11999.213519: calc_load_account_active: cpu 10 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 8
<...>-9200 [011] 11999.213533: calc_load_account_active: cpu 11 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 9
<...>-9189 [012] 11999.213548: calc_load_account_active: cpu 12 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 10
<...>-9196 [013] 11999.213564: calc_load_account_active: cpu 13 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 11
<...>-9193 [014] 11999.213580: calc_load_account_active: cpu 14 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 12
<...>-9191 [015] 11999.213596: calc_load_account_active: cpu 15 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 13
<...>-9204 [009] 11999.213610: calc_load_account_active: cpu 9 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 12<...>-9195 [008] 11999.213645: calc_load_account_active: cpu 8 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 11<...>-9203 [006] 11999.213782: calc_load_account_active: cpu 6 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 10<...>-9197 [005] 11999.213809: calc_load_account_active: cpu 5 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 9<...>-9196 [013] 11999.213930: calc_load_account_active: cpu 13 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 8<...>-9193 [014] 11999.213971: calc_load_account_active: cpu 14 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 7<...>-9189 [012] 11999.214004: calc_load_account_active: cpu 12 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 6<...>-9199 [001] 11999.214032: calc_load_account_active: cpu 1 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 5<...>-9191 [015] 11999.214164: calc_load_account_active: cpu 15 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 4<...>-9202 [007] 11999.214201: calc_load_account_active: cpu 7 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 3<...>-9201 [003] 11999.214353: calc_load_account_active: cpu 3 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 2<...>-9192 [010] 11999.214998: calc_load_account_active: cpu 10 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 1<...>-9200 [011] 11999.215115: calc_load_account_active: cpu 11 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 0
<...>-9198 [000] 11999.223342: calc_global_load: calc_load_task = 0

雖然這個是未加三個patch以前的trace數據,可是咱們依舊可以發現一些問題:原來的10tick對咱們來講從一個微不足道的小時間片被提高爲一個大時間片,相對此低了一個數量級的1 tick卻一直未真正被咱們所重視。trace數據中,cpu0、二、4在計算完本身的load以後,其餘cpu計算完本身的load以前,進入了idle,因爲默認狀況下每一個cpu都會去將idle計算入全局的load中,這部分進入idle形成的cpu load發生的變化會被計算到全局load中。依舊出現了以前10ticks的不公平問題。示例以下:

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks tasks_idle
0HZ+11 1 1 1 0 0 0 0 0 3 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3
5HZ+1.3 1 0 0 0 0 0 1 1  
  +1               3-3+1=1 0
5HZ+1.5 0 1 1 1 0 0 0 0 1 0
  -1     +1         1+1-1=1 0
5HZ+1.7 0 0 0 0 1 1 1 0 0 0
        -1     +1   1-1+1=3 0
5HZ+3 0 1 1 1 0 0 1 0    
              -1   1 -1
5HZ+5 0 0 0 0 1 1 1 0  
5HZ+11 1 1 0 0 0 0 1 -1  
calc_global_load <-- -- -- -- -- -- -- -- 1 -1

線上業務平均每一個任務運行時間爲0.3ms,任務運行週期爲0.5ms,所以每一個週期idle執行時間爲0.2ms。在1個tick內,cpu執行完本身load的計算以後,很大的機率會在其餘cpu執行本身load計算以前進入idle,導致總體load計算對idle和非idle不公平,load計數不許確。 針對該問題,一個簡單的方案是檢測第一個開始執行load計算的CPU,只在該CPU上將以前全部進入idle計算的load更新至全局的load,以後的CPU不在將idle更新至全局的load中。這個方案中檢測第一個開始執行load計算的CPU是難點。另一個解決方案是將LOAD_FREQ週期點和全局load更新至avenren的LOAD_FREQ+10時間點做爲分界點。對上一次LOAD_FREQ+10到本次週期點之間的idle load,能夠在本次CPU執行load計算時更新至全局的load;對週期點以後到LOAD_FREQ+10時間點之間的idle load能夠在全局load更新至avenrun以後更新至全局load。 
Peter Z採用的是上述第二個解決,使用idx翻轉的技術實現。經過LOAD_FREQ和LOAD_FREQ+10兩個時間點,能夠將idle致使的load分爲兩部分,一部分爲LOAD_FREQ至LOAD_FREQ+10這部分,這部分load因爲在各個cpu計算load以後到全局avenrun更新之間,不該該直接更新至全局load中;另外一部分爲LOAD_FREQ+10至下一個週期點LOAD_FREQ,這部分idle致使的load能夠隨時更新至全局的load中。實現中使用了一個含2個元素的數組,用於對這兩部分load進行存儲,但這兩部分並非分別存儲在數組的不一樣元素中,而是每一個LOAD_FREQ週期存儲一個元素。以下圖所示,在0~5週期中,這兩部分idle都存儲在數組下標爲1的元素中。5~10週期內,這兩個部分都存儲在數組下標爲0的元素中。在5~10週期中,各個cpu計算load時讀取的idle爲0~5週期存儲的;在計算完avenrun以後,更新idle至全局load時讀取的爲5~10週期中前10個ticks的idle致使的load。這樣在10~15週期中,各個cpu計算load時讀取的idle即爲更新avenrun以後產生的idle load。具體實現方案以下:

 

      0             5             10            15          --->HZ
        +10           +10           +10           +10       ---> ticks
      |-|-----------|-|-----------|-|-----------|-|
idx:0   1     1       0     0       1      1      0   
  w:0 1 1         1 0 0         0 1 1         1 0 0
  r:0 0 1         1 1 0         0 0 1         1 1 0
說明:1)0 5 10 15表明的爲0HZ、5HZ、10HZ、15HZ,這個就是各個cpu執行load計算的週期點
     2)+10表示週期點以後10ticks(即爲計算avenrun的時間點)
     3)idx表示當前的idx值(每次只取最後一位的值,所以變化範圍爲0~1)
     4)w後面3列值,第一列表示週期點以前idle計算值寫入的數組idx;第二列表示週期點到+10之間idle致使的load變化寫入的數
       組idx;第三列表示計算萬avenrun以後到下一個週期點之間idle寫入的數組idx;

用以下示例進行說明(假定0HZ+11以後idx爲0):

32內核loadavg計算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks idle[0] idle[1] idx
0HZ+11 1 1 1 0 0 0 0 0 3 0 0 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3 0 0
5HZ+1.3 1 0 0 0 0 0 1 1  
  +1               3-3+1=1 0 0 0
5HZ+1.5 0 1 1 1 0 0 0 0 1 0
  -1     +1         1+1=2 0 -1 0
5HZ+1.7 0 0 0 0 1 1 1 0 0 0
        -1     +1   2+1=3 0 -2 0
5HZ+3 0 1 1 1 0 0 1 0 0
5HZ+3                 3 0 -2 0
5HZ+5 0 0 0 0 1 1 1 0 0
5HZ+11 1 1 0 0 0 0 1 1  
calc_global_load <-- -- -- -- -- -- -- -- 3 0 -2 0
                  3-2=1 0 0 1
5HZ+15 1 1 0 0 0 0 0 1    
              -1   1 0 -1 1

再次迴歸到公平性問題

通過對細粒度idle調度問題進行解決,在線上業務總體load獲得了很好的改善。原來平均運行進程數在16的狀況下,load一直徘徊在1左右,改善以後load回升到了15左右。 
然而這個patch發佈到社區,通過相關報告load計數有問題的社區人員進行測試以後,發現系統的load總體偏高,並且不少時候都是趨近於系統總運行進程數。爲了驗證這個patch的效果,升級了一臺添加該patch的機器,進行觀察,確實發現升級以後機器的load比原有18還高出1左右。 
又是一次深度的思考,是否當前這個patch中存在BUG? 是否從第一個CPU到最後一個CPU之間的idle就應該直接計算在總體load中? 對於高頻度調度idle的狀況,這部分idle是不該該加入到全局load中,不然不管系統運行多少進程,最終load都會始終徘徊在0左右。所以這部分idle必須不可以加入到全局load中。經過trace數據進行分析,也證實了patch運行的行爲符合預期,並不存在異常。 
若是假設以前全部的patch都沒有問題,是否存在其餘狀況會致使系統load偏高?致使load偏高,一個極可能的緣由就是在該計算爲idle時,計算爲非idle狀況。爲此前後提出了負載均衡的假設、計算load時有進程wakeup到當前運行隊列的假設,最終都被一一排除。 
進一步觀察trace數據,發現幾乎每次都是在作完該CPU上load計算以後,該CPU當即就進入idle。16個CPU,每一個CPU都是在非idle的時候執行load計算,執行完load計算以後又都是當即進入idle。並且這種狀況是在每一次作load計算時都是如此,並不是偶然。按照採樣邏輯,因爲採樣時間點不受系統運行情況影響,對於頻繁進出idle的狀況,採樣時idle和非idle都應該會出現。現在只有非idle狀況,意味着採樣時間點選取存在問題。 
進一步分析,若是採樣點處於idle內部,因爲nohz致使進入idle以後並不會週期執行sched_tick,也就沒法執行load計算,看起來彷佛會致使idle load計算丟失。事實並非,以前計算idle load就是爲了不進入nohz致使load計算丟失的問題,在進入idle調度前會將當前cpu上的load計算入idle load中,這樣其餘cpu執行load計算時會將這部分load一同計算入內。 
可是基於上述邏輯,也能夠獲得一個結論:若是採樣點在idle內部,默認應該是將進入idle時的load做爲該cpu上採樣load。事實是否如此?繼續分析,該CPU若是從nohz從新進入調度,這個時候因爲採樣時間點還存在,並且間隔上一次採樣已經超過一個LOAD_FREQ週期,會再次執行load計算。再次執行load計算會覆蓋原有進入idle時計算的load,這直接的一個結果是,該CPU上的採樣點從idle內部變成了非idle! 問題已經變得清晰,對採樣點在idle內部的狀況,實際計算load應該爲進入idle時該cpu上的load,然而因爲該cpu上採樣時間點沒有更新,致使退出nohz狀態以後會再次執行load計算,最終將退出nohz狀態以後的load做爲採樣的load。 
問題已經清楚,解決方案也比較簡單:在退出nohz狀態時檢測採樣時間點在當前時間點以前,若是是,則意味着此次採樣時間點在idle內部,這 個週期內不須要再次計算該CPU上的load。
 
 
相關文章
相關標籤/搜索