内核发生了hard LOCKUP然后panic了,代码版本是linux-3.10.0-514.16.1.el7.x86_64
[4474426.249700] NMI watchdog: Watchdog detected hard LOCKUP on cpu 50
crash下的bt信息如下:
[exception RIP: tg_unthrottle_up+24]
RIP: ffffffff810c9658 RSP: ffff882f7fc83dc8 RFLAGS: 00000046
RAX: ffff885d4767d800 RBX: ffff885f7e4d6c40 RCX: ffff8830767f2930
RDX: 000000000000005b RSI: ffff885f7e4d6c40 RDI: ffff8830767f2800
RBP: ffff882f7fc83dc8 R8: ffff885f697c7900 R9: 0000000000000001
R10: 0000000000000000 R11: 0000000000000000 R12: ffff8830764e5400
R13: ffffffff810c9640 R14: 0000000000000000 R15: ffff8830767f2800
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
--- <NMI exception stack> ---
#12 [ffff882f7fc83dc8] tg_unthrottle_up at ffffffff810c9658
#13 [ffff882f7fc83dd0] walk_tg_tree_from at ffffffff810c17db
#14 [ffff882f7fc83e20] unthrottle_cfs_rq at ffffffff810d1675
#15 [ffff882f7fc83e58] distribute_cfs_runtime at ffffffff810d18e2
#16 [ffff882f7fc83ea0] sched_cfs_period_timer at ffffffff810d1a7f
#17 [ffff882f7fc83ed8] __hrtimer_run_queues at ffffffff810b4d72
#18 [ffff882f7fc83f30] hrtimer_interrupt at ffffffff810b5310
#19 [ffff882f7fc83f80] local_apic_timer_interrupt at ffffffff81050fd7
#20 [ffff882f7fc83f98] smp_apic_timer_interrupt at ffffffff8169978f
#21 [ffff882f7fc83fb0] apic_timer_interrupt at ffffffff81697cdd
--- <IRQ stack> ---
hard LOCKUP原理比较简单,就是在关中断的情况下栈里面的函数执行时间过长,时间和是否panic由以下开关决定:

从栈上可以看出,整个函数栈都是cfs_bandwidth的东西,cfs_bandwidth就是控制cfs调度带宽的,先详细看一下cfs_bandwidth

先说一下大致上的运行逻辑
  1. cfs_rq有自己的运行时间,当运行时间为负数时,cfs_rq被标记为throttled,cfs_rq上运行的当前进程就被标记为重新调度。
  2. cfs_rq的运行时间都是从对应的cfs_bandwidth中获取的,当cfs_bandwidth时间总和是quota,会逐渐消耗完,cfs_rq得不到运行时间就被标记为throttled
  3. cfs_bandwidth是每隔period时间补充一次quota。
看第一点,cfs_rq运行时间被限制是throttle_cfs_rq完成的,它的调用关系是:

在throttle_cfs_rq和check_enqueue_throttle中可以看到,当cfs_rq->runtime_remaining不大于0时,cfs_rq就会被限制。
在throttle_cfs_rq中,干了如下一些事情,注意cfs_rq将自己添加到cfs_b->throttled_cfs_rq。

再看第二步:scheduler_tick负责更新调度时间,具体调用路径如下:
scheduler_tick -> task_tick_fair -> entity_tick -> update_curr -> account_cfs_rq_runtime -> __account_cfs_rq_runtime
在__account_cfs_rq_runtime下,

assign_cfs_rq_runtime负责cfs_rq从cfs_bandwith里面借时间,sched_cfs_bandwidth_slice的值默认是5ms

sched_cfs_bandwidth_slice值是白送的5ms,因为计算时间不够时需要一个负值的判断。
再看第三步, init_cfs_bandwidth搞了两个定时器,sched_cfs_period_timer和sched_cfs_slack_timer

do_sched_cfs_period_timer具体内容如下:

回来接着看问题,问题就出在do_sched_cfs_period_timer函数下,来自zhoujian的结论,内核patch如下:
看代码就可以分析出来,内核并没有真正的发送死锁问题,只是单纯的在关中断环境下代码执行时间太长,关键是执行时间长在哪里?
do_sched_cfs_period_timer下有一个while循环 :while (throttled && runtime > 0){},在distribute_cfs_runtime下,第16行,唤醒cfs_rq时cfs_rq只被给了多余的1ns,不能再吝啬了,而在外层的do_sched_cfs_period_timer,runtime则是一个从全局赋值的本地变量,并发代码对runtime的消耗便体现不出来了,导致while循环次数非常多。

而在另外一个循环中,distribute_cfs_runtime下有一个for循环: list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq, throttled_list) {},原理比较简单,即throttled_cfs_rq上处理的cfs_rq没有进栈的速度快。
bsegall的patch实际上两个方面都考虑了,那么哪个才可能性大呢,第一个循环需要苛刻的偶然性,而第二个则不需要,最重要的是通过稳定的重现方法,证明了第二个概率大。
最终结论:
在CentOS内核3.10.0-585.el7版本之后该问题解决:
[kernel] sched: Fix potential near-infinite distribute_cfs_runtime() loop (Lauro Ramos Venancio) [1399391]
kpatch热补丁:不要用原版patch,做出的热补丁不解决问题,需要魔改。

cfs_bandwidth下的distribute_cfs_runtime hard lockup来自于OenHan

链接为:http://oenhan.com/cfs-bandwidth

发表评论