目錄[ 隱藏] |
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可以分析出系統當前的整體負載,各部分負載分布情況。
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); }
幾個宏定義如下:
#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定義如下:
#define CALC_LOAD(load,exp,n) \ load *= exp; \ load += n*(FIXED_1-exp); \ load >>= FSHIFT;
用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的采樣。
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的計算方法:
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
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。
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也已經更新至最新狀態,不會出現不更新的情況。如下圖所示:
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次。接着用例子對應用場景進行分析:
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里面都計算一遍是一種方法。接着用示例進行說明:
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、2、4在計算完自己的load之后,其他cpu計算完自己的load之前,進入了idle,由於默認情況下每個cpu都會去將idle計算入全局的load中,這部分進入idle造成的cpu load發生的變化會被計算到全局load中。依舊出現了之前10ticks的不公平問題。示例如下:
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):
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 |
再次回歸到公平性問題
然而這個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。