本文來自微公眾號:開內功修煉 (ID:kfngxl),作者:張彥飛 allen大家好,我是哥!負載是看 Linux 服務器運行狀態(tài)時很用的一個性指標。在觀線上服務器行狀況的時,我們也是常把負載找來看一看。線上請求壓過大的時候經(jīng)常是也伴著負載的飆。但是負載原理你真的解了嗎?我列舉幾個問,看看你對載的理解是足夠的深刻負載是如何算出來的?負載高低和 CPU 消耗正相關嗎?內是如何暴露載數(shù)據(jù)給應層的?如果對以上問題理解還拿捏是很準,那飛哥今天就你來深入地解一下 Linux 中的負載!一、解負載查看程我們經(jīng)常 top 命令查看 Linux 系統(tǒng)的負載情況一個典型的 top 命令輸出的負載下所示。#?topLoad?Avg:?1.25,?1.30,?1.95??...........輸出中的 Load Avg 就是我們常說的負載也叫系統(tǒng)平負載。因為純某一個瞬的負載值并有太大意義所以 Linux 是計算了過去一段間內的平均,這三個數(shù)別代表的是去 1 分鐘、過去 5 分鐘和過去 15 分鐘的平均負載值那么 top 命令展示的數(shù)據(jù)數(shù)是如來的呢?事上,top 命令里的負值是從 /proc/ loadavg 這個偽文件里來的。通 strace 命令跟蹤 top 命令的系統(tǒng)調可以看的到個過程。#?strace?topopenat(AT_FDCWD,?"/proc/loadavg",?O_RDONLY)?=?7內核中定義了 loadavg 這個偽文件 open 函數(shù)。當用態(tài)訪問 /proc/ loadavg 會觸發(fā)內核定義的函數(shù)在這里會讀內核中的平負載變量,單計算后便展示出來。體流程如下所示。我們據(jù)上述流程再展開了看。偽文件 /proc/ loadavg 在 kernel 中定義是在 /fs/ proc / loadavg.c 中。在該文件中會建 /proc/ loadavg,并為其指定操方法 loadavg_proc_fops。//file:?fs/proc/loadavg.cstatic?int?__init?proc_loadavg_init(void){?proc_create("loadavg",?0,?NULL,?&loadavg_proc_fops);?return?0;}在 loadavg_proc_fops 中包含了打開該件時對應的作方法。//file:?fs/proc/loadavg.cstatic?const?struct?file_operations?loadavg_proc_fops?=?{?.open??=?loadavg_proc_open,?};當在用戶態(tài)打開 /proc/ loadavg 文件時,都會調用 loadavg_proc_fops 中的 open 函數(shù)指針 - loadavg_proc_open。loadavg_proc_open 接下來會調用 loadavg_proc_show 進行處理,核心的算是在這里成的。//file:?fs/proc/loadavg.cstatic?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,??task_active_pid_ns(current)-last_pid);?return?0;}在 loadavg_proc_show 函數(shù)中做了兩件事。用 get_avenrun 讀取當前負載值將平負載值按照定的格式打輸出在上面源碼中,大看到了 FIXED_1/200、LOAD_INT、LOAD_FRAC 等奇奇怪怪的義,代碼寫這么猥瑣是為內核中并有 float、double 等浮點數(shù)類型,而用整數(shù)來模的。這些代都是為了在數(shù)和小數(shù)之轉化使的。道這個背景行了,不用度展開剖析這樣用戶通訪問 /proc/ loadavg 文件就可以取到內核計的負載數(shù)據(jù)。其中獲取 get_avenrun 只是在訪問 avenrun 這個全局數(shù)組而已。//file:kernel/sched/core.cvoid?get_avenrun(unsigned?long?*loads,?unsigned?long?offset,?int?shift){?loads[0]?=?(avenrun[0]?+?offset)? update_process_times => scheduler_tick。最終在 scheduler_tick 中會刷新當前 CPU 上的負載值到 calc_load_tasks 上。因為每 CPU 都在定時刷,以 calc_load_tasks 上記錄的就整個系統(tǒng)的時負載值。們來看下負刷新的 scheduler_tick 這個核心函數(shù)://file:kernel/sched/core.cvoid?scheduler_tick(void){?int?cpu?=?smp_processor_id();?struct?rq?*rq?=?cpu_rq(cpu);?update_cpu_load_active(rq);?}在這個函數(shù)中獲取當前 cpu 以及其對應的運行列 rq(run queue),調用 update_cpu_load_active 刷新當前 CPU 的負載數(shù)據(jù)到全局組中。//file:kernel/sched/core.cstatic?void?update_cpu_load_active(struct?rq?*this_rq){??calc_load_account_active(this_rq);}//file:kernel/sched/core.cstatic?void?calc_load_account_active(struct?rq?*this_rq){?//獲取當前運行隊列負載相對?delta??=?calc_load_fold_active(this_rq);?if?(delta)??//添加到全局瞬時負載??atomic_long_add(delta,?&calc_load_tasks);?}在 calc_load_account_active 中看到,通過 calc_load_fold_active 獲取當前運行隊的負載相對,并把它加全局瞬時負值 calc_load_tasks 上。至此,calc_load_tasks 上就有了當前系統(tǒng)前時間下的體瞬時負載數(shù)了。我們展開看看是何根據(jù)運行列計算負載的://file:kernel/sched/core.cstatic?long?calc_load_fold_active(struct?rq?*this_rq){?long?nr_active,?delta?=?0;?//?R?和?D?狀態(tài)的用戶?task?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;}哦,原來是同時計算 nr_running 和 nr_uninterruptible 兩種狀態(tài)的進程的量。對應于戶空間中的 R 和 D 兩種狀態(tài)的 task 數(shù)(進程 OR 線程)。由于 calc_load_tasks 是一個長期在的數(shù)據(jù)。以在刷新 rq 里的進程數(shù)到其上的候,只需要變化的量就,不用全部算。因此上函數(shù)返回的一個 delta。2.2 定時計算系統(tǒng)平均負載一小節(jié)中我找到了系統(tǒng)前瞬時負載 calc_load_tasks 變量的更新過程現(xiàn)在我們還一個計算過 1 分鐘、過去 5 分鐘、過去 15 分鐘平均負載的機制傳統(tǒng)意義上我們在計算均數(shù)的時候取的方法都把過去一段間的數(shù)字都起來然后平一下。把過 N 個時間點的所有瞬負載都加起取一個平均不完事了。其實是我們統(tǒng)意義上理的平均數(shù),如有 n 個數(shù)字,分別 x1, x2, ..., xn。那么這個數(shù)據(jù)合的平均數(shù)是 (x1 + x2 + ... + xn) / N。但是如果用這種簡的算法來計平均負載的,存在以下個問題:1.需要存儲過每一個采樣期的數(shù)據(jù)假我們每 10 毫秒都采集一次,那么需要使用一比較大的數(shù)將每一次采的數(shù)據(jù)全部存起來,那統(tǒng)計過去 15 分鐘的平均數(shù)就得存 1500 個數(shù)據(jù) (15 分鐘 * 每分鐘 100 次) 。而且每出現(xiàn)個新的觀察,就要從移平均中減去個最早的觀值,再加上個最新的觀值,內存數(shù)會頻繁地修和更新。2.計算過程較復雜計算的候再把整個組全加起來再除以樣本數(shù)。雖然加很簡單,但成百上千個字的累加仍很是繁瑣。3.不能準確表示當前變化勢傳統(tǒng)的平數(shù)計算過程,所有數(shù)字權重是一樣。但對于平負載這種實應用來說,實越靠近當時刻的數(shù)值重應該越要一些才好。為這樣能更反應近期變的趨勢。所,在 Linux 里使用的并不是我所以為的傳的平均數(shù)的算方法,而采用的一種數(shù)加權移動均(Exponential Weighted Moving Average,EMWA)的平均數(shù)算法。這種數(shù)加權移動均數(shù)計算法深度學習中很廣泛的應。另外股票場里的 EMA 均線也是使用的是類的方法求均的方法。該法的數(shù)學表式是:a1 = a0 * factor + a * (1 - factor)。這個算法想理解起有點小復雜感興趣的同可以 Google 自行搜索。我們需要知道這方法在實際算的時候只要上一個時的平均數(shù)即,不需要保所有瞬時負值。另外就越靠近現(xiàn)在時間點權重高,能夠很地表示近期化趨勢。這實也是在時子系統(tǒng)中定完成的,通一種叫做指加權移動平計算的方法計算這三個均數(shù)。我們詳細看下上中的執(zhí)行過。時間子系將在時鐘中中會注冊時中斷的處理數(shù)為 timer_interrupt 。//file:arch/ia64/kernel/time.cvoid?__inittime_init?(void){?register_percpu_irq(IA64_TIMER_VECTOR,?&timer_irqaction);?ia64_init_itm();}static?struct?irqaction?timer_irqaction?=?{?.handler?=?timer_interrupt,?.flags?=?IRQF_DISABLED?|?IRQF_IRQPOLL,?.name?=??"timer"};當每次時鐘節(jié)拍到來時調用到 timer_interrupt,依次會調用到 do_timer 函數(shù)。//file:kernel/time/timekeeping.cvoid?do_timer(unsigned?long?ticks){???calc_global_load(ticks);}其中 calc_global_load 是平均負載計算核心。它會取系統(tǒng)當前時負載值 calc_load_tasks,然后來計算過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均載,并保存 avenrun 中,供用戶進程讀。//file:kernel/sched/core.cvoid?calc_global_load(unsigned?long?ticks){??//?1獲取當前瞬時負值?active?=?atomic_long_read(&calc_load_tasks);?//?2平均負載的計算?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);?}獲取瞬時負載比簡單,就是取一個內存量而已。在 calc_load 中就是采用了我前面說的指加權移動平法來計算過 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載的。體實現(xiàn)的代如下://file:kernel/sched/core.c/*?*?a1?=?a0?*?e?+?a?*?(1?-?e)?*/static?unsigned?longcalc_load(unsigned?long?load,?unsigned?long?exp,?unsigned?long?active){?load?*=?exp;?load?+=?active?*?(FIXED_1?-?exp);?load?+=?1UL?<(FSHIFT?-?1);?return?load?>>?FSHIFT;}雖然這個算法理解起來復雜,但是碼看起來確要簡單不少計算量看起很少。而且不懂也沒有系,只需要道內核并不采用的原始平均數(shù)計算法,而是采了一種計算,且能更好達變化趨勢算法就行。此,我們開提到的“負是如何計算來的?”這個問題也有結了。Linux 定時將每個 CPU 上的運行隊中 running 和 uninterruptible 的狀態(tài)的進程數(shù)匯總到一個局系統(tǒng)瞬時載值中,然再定時使用數(shù)加權移動均法來統(tǒng)計去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載。、平均負載 CPU 消耗的關系現(xiàn)很多同學都平均負載和 CPU 給聯(lián)系到了一起認為負載高CPU 消耗就會高,負低,CPU 消耗就會低在很老的 Linux 的版本里,統(tǒng)負載的時候實是只計算 runnable 的任務數(shù)量,這進程只對 CPU 有需求。在那個年里,負載和 CPU 消耗量確實是正關的。負載高就表示正 CPU 上運行,或等 CPU 執(zhí)行的進程越,CPU 消耗量也會越。但是前面們看到了,文使用的 3.10 版本的 Linux 負載平均數(shù)不僅跟蹤 runnable 的任務,而且還跟處于 uninterruptible sleep 狀態(tài)的任務。而 uninterruptible 狀態(tài)的進程其實是不占 CPU 的。所以說,負高并一定是 CPU 處理不過來,也可能會是因磁盤等其他源調度不過而使得進程入 uninterruptible 狀態(tài)的進程致的!為什要這么修改我從網(wǎng)上搜了遠在 1993 年的一封郵件里找了原因,以是郵件原文From:?Matthias?Urlichs?Subject:?Load?average?broken??Date:?Fri,?29?Oct?1993?11:37:23?+0200??The?kernel?only?counts?"runnable"?processes?when?computing?the?load?average.I?don't?like?that;?the?problem?is?that?processes?which?are?swing?orwaiting?on?"fast",?i.e.?noninterruptible,?I/O,?also?consume?resources.?It?seems?somewhat?nonintuitive?that?the?load?average?goes?down?when?youreplace?your?fast?swap?disk?with?a?slow?swap?disk...?Anyway,?the?following?patch?seems?to?make?the?load?average?much?moreconsistent?WRT?the?subjective?speed?of?the?system.?And,?most?important,?theload?is?still?zero?when?nobody?is?doing?anything.?;-)---?kernel/sched.c.orig?Fri?Oct?29?10:31:11?1993+++?kernel/sched.c??Fri?Oct?29?10:32:51?1993@@?-414,7?+414,9?@@????unsigned?long?nr?=?0;?????for(p?=?&LAST_TASK;?p?>?&FIRST_TASK;?--p)-???????if?(*p?&&?(*p)->state?==?TASK_RUNNING)+???????if?(*p?&&?((*p)->state?==?TASK_RUNNING)?||+????????????????(*p)->state?==?TASK_UNINTERRUPTIBLE)?||+????????????????(*p)->state?==?TASK_SWING))???????????nr?+=?FIXED_1;????return?nr;?}可見這個修改在 1993 年就引入了。在這封靈恝所示的 Linux 源碼變化中可以到,負載正把 TASK_UNINTERRUPTIBLE 和 TASK_SWAPPING 狀態(tài)(交換狀態(tài)后從 Linux 中刪除)的進程也給加了進來。這封郵件中正文中,作也清楚地表了為什么要 TASK_UNINTERRUPTIBLE 狀態(tài)的進程添加來的原因。把他的說明譯一下,如:“內核在算平均負載只計算“可行”進程。不喜歡那樣問題是正在快速”交換等待的進程即不可中斷 I / O,也會消耗源。當您用速交換磁盤換快速交換盤時,平均載下降似乎點不直觀...... 無論如何,下的補丁似乎負載平均值加一致 WRT 系統(tǒng)的主觀速度。而,最重要的,當沒有人任何事情時負載仍然為。;-)”這一補丁提交的主要思想平均負載應表現(xiàn)對系統(tǒng)有資源的需情況,而不該只表現(xiàn)對 CPU 資源的需求。假某個 TASK_UNINTERRUPTIBLE 狀態(tài)的進程為等待磁盤 IO 而排隊的話,此時并不消耗 CPU,但是正在等磁盤等件資源。那它是應該體在平均負載計算里的。以作者把 TASK_UNINTERRUPTIBLE 狀態(tài)的進程都表現(xiàn)歷山均負載里了所以,負載低表明的是前系統(tǒng)上對統(tǒng)資源整體求更情況。果負載變高可能是 CPU 資源不夠了,也可能磁盤 IO 資源不夠了所以還需要合其它觀測令具體分情分析。四、結今天我?guī)?家深入地學了一下 Linux 中的負載。我們據(jù)一幅圖來結一下今天到的內容。把負載工作理分成了如三步。1.內核定時匯總 CPU 負載到系統(tǒng)瞬負載2.內核使用指數(shù)加移動平均快計算過去 1、5、15 分鐘的平均3.用戶進程通過打開 loadavg 讀取內核中的平均負載們再回頭來結一下開篇到的幾個問。1.負載是如何計算出的?是定時將每個 CPU 上的運行隊列中 running 和 uninterruptible 的狀態(tài)的進程量匯總到一全局系統(tǒng)瞬負載值中,后再定時使指數(shù)加權移平均法來統(tǒng)過去 1 分鐘、過去 5 分鐘、過去 15 分鐘的平均負載2.負載高低和 CPU 消耗正相關?負載高低明的是當前統(tǒng)上對系統(tǒng)源整體需求情況。如果載變高,可是 CPU 資源不夠了也可能是磁 IO 資源不夠了。所不能說看著載變高,就得是 CPU 資源不夠用了。3.內核是如何暴露載數(shù)據(jù)給應層的?內核義了一個偽件 /proc/ loadavg,每當用戶打開個文件的時,內核中的 loadavg_proc_show 函數(shù)就會被用到,該函中訪問 avenrun 全局數(shù)組變,并將平均載從整數(shù)轉為小數(shù),然打印出來?