Linux系统load average异常值处理的trick

周末再分享一个内核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才是根本。而这些就是另外的话题了。


浙江温州皮鞋湿,下雨进水不会胖。

你可能感兴趣的:(loadavg)