Linux系統load average異常值處理的trick

2020-08-08 08:33:04

週末再分享一個內核bug緊急熱處理case。

假如你發現某個Linux系統的load輸出如下:

#uptime
... 0 users,  load average: 32534565100.09, 31042979698.12, 21960303025.38

你會覺得你的系統已經不堪重負了嗎?

  1. uptime/top從/proc/loadavg取值。
  2. /proc/sched_debug可以觀察實時負載。

趕緊找出到底是哪些task把系統壓的如此不堪。

No!這明顯是內核的bug!

我們知道,Linux的load average統計的是running進程和uninterrupted睡眠進程的總和,因此我們不妨看看系統中到底有多少這些進程:

# running 進程數量
#awk '/\.nr_running/ {c += $3} END{print c}' /proc/sched_debug
34359738366

# uninterruptible 進程數量
#awk '/\.nr_uninterruptible/ {c += $3} END {print c}' /proc/sched_debug
0

然而系統中到底有多少進程呢?

#ls /proc/|awk '/^[0-9]{1,}$/ {c += 1} END {print c}'
983

到底要相信哪個數值?

走讀load avg的實現,發現它的值取自一個全域性陣列 avenrun ,內核中的該變數指向的就是load avg的值:

crash> rd avenrun
ffffffff81db56e0:  00003fb0ff266cce                    .l&..?..
crash> rd ffffffff81db56e8
ffffffff81db56e8:  00003f83b12997f2                    ..)..?..
crash> rd ffffffff81db56f0
ffffffff81db56f0:  00003f6da24095fc                    ..@.m?..

avenrun的值是通過另一個值 calc_load_tasks 移動指數平均計算出來的,該值表示的系統中的實時load,移動指數平均後就是load avg:

crash> rd calc_load_tasks
ffffffff81db5788:  00000007fffffffb                    ........
crash> rd calc_load_tasks
ffffffff81db5788:  00000007fffffffb                    ........
crash> rd calc_load_tasks
ffffffff81db5788:  00000007fffffff5                    ........
crash> rd calc_load_tasks
ffffffff81db5788:  00000007fffffff5                    ........
crash> runq -c XX ? # 唉,太麻煩,不如直接systemtap

這種天文數位肯定意味着哪裏算錯了,系統中怎麼可能會有如此數量的進程!

好,跟蹤一下實時的過程,函數calc_load_fold_active是實時計算calc_load_tasks的差值的:

probe kernel.function("calc_load_fold_active")
{
    if (@cast($this_rq, "struct rq")->nr_running > 1000) {
        printf("%d %x  %x  %x\n", cpu(), @cast($this_rq, "struct rq")->nr_running, @cast($this_rq, "struct rq")->nr_uninterruptible, @cast($this_rq, "struct rq")->calc_load_active);
    }
}

用stap執行它:

18 fffffffe  238cc  1000238ca
18 fffffffe  238cc  1000238ca
18 fffffffe  238cc  1000238cb
16 ffffffff  3244b  10003244a
10 fffffffe  6cafd  10006cafb
18 fffffffe  238cc  1000238ca
10 fffffffe  6cafd  10006cafb
10 fffffffe  6cafd  10006cafb
10 fffffffe  6cafd  10006cafb

nr_running異常均發生在10,16,18三個CPU上。nr_running的值也非常齊整。

排入執行佇列中的task一般不會太多,基本上就是總task數量除以CPU個數的量級,CPU會處理好負載均衡,出現0xffffffff這種值,一般是溢位導致。

Linux內核用unsigned int表示nr_running,因此基本上可以確定是nr_running–遞減操作在其值爲0的時候發生了向下溢位,這是一個錯誤的行爲,大概率是併發問題導致。

爲什麼nr_running爲0了還是遞減,這也許是一個fork時的同步問題,這裏有一個issue,參考一下:
https://github.com/torvalds/linux/commit/eeb61e53ea19be0c4015b00b2e8b3b2185436f2b

那麼如何在不打kpatch,不升級內核的情況下緩解問題?不太易!

系統中很多邏輯都依賴nr_running這個指標,如果改錯了,將會把瞎子治成啞巴,從而暴露其它異常。

但是,既然nr_running保持異常值如此之久系統依然沒有跑飛,說明load avg僅僅影響系統對負載的認知,那麼可以假設,將nr_running修改爲其它值也是可以的。

如果我們能確定nr_running的值只發生過一次溢位,那麼我們就可以將所有0xffffffff之類的值直接設定成0,如果發生2次溢位,那麼就把0xffffffff改成1,0xfffffffe改成0即可,發生過幾次溢位,就迴環回去即可。

然而,我們什麼也不知道!除了實際數runqueue上的task數量,基於該數量重置nr_running,沒有什麼好辦法。

這裏我就不數了,我假設的簡單些,我將所有異常值恢復成0,目睹一下load avg下降的過程:

probe kernel.function("calc_load_fold_active")
{
	if (@cast($this_rq, "struct rq")->nr_running > 0xfffffff0) {
		@cast($this_rq, "struct rq")->nr_running = 0;
	}
}
load average: 12595551963.26, 27998647231.01, 32041249020.48
load average: 9805506738.22, 26627205524.50, 31527727438.91
load average: 8297971422.09, 25750437865.30, 31189960211.67
load average: 7022210229.44, 24902539984.70, 30855811599.18
# 大約一個多小時後
load average: 5.27, 5.32, 5.04
# 至此,系統恢復平靜...
...

只是玩玩,真遇到這個問題還是建議升級內核,不必如此修復。


一點說明。

runqueue的nr_running作爲一個自然數計數位段,其下屆肯定是0,如果在dec_nr_running的時候加以判斷兜底,是不是能挽回一次溢位呢?

static inline void dec_nr_running(struct rq *rq)
{
	if (rq->nr_running > 0)
		rq->nr_running--;
}

當然了,這種見招拆招的方案無益於整體問題的解決,也許nr_running欄位是由於其它異常連帶的異常,最終還是要找出具體的觸發case纔是根本。而這些就是另外的話題了。


浙江溫州皮鞋溼,下雨進水不會胖。