一 问题背景
在CPU数量众多、多层级且cgroup数量也众多的环境中,偶发CFS带宽时钟中断处理过程中出现hardlockup。
[exception RIP: tg_unthrottle_up+25] 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
2 原因分析
2.1 CFS带宽的原理
CFS带宽功能提供了cgroup级别的CPU资源限制功能;这个功能的核心原理就是为一个CPU cgroup提供两个要素:period和quota,即在period周期内这个CPU cgroup能够使用的CPU资源。为此内核为各个CPU cgroup安装了一个高精度定时器,其周期为period;当这个定时器period到期后,就会去检查这个CPU cgroup的消耗的时间。
由于一个CPU cgroup内有多个cfs_rq队列,CPU cgroup的带宽quota也由这些cfs_rq所共享;同时这些cfs_rq就绪队列上的任务对CPU时间的需求量不一致,为了保证各个cfs_rq上的CPU时间分配的相对合理(即不浪费,又不太频繁的申请),各个CPU上的cfs_rq每次只从带宽的quota中申请“一小片”时间片。这个“时间片”的大小可以通过/proc/sys/kernel/sched_cfs_bandwidth_slice_us(默认值为5ms)接口来调整。
当CFS带宽的周期时钟到期后,在时钟中断里面会检查该CPU cgroup中的所有时间片用完的cfs_rq,(这些cfs_rq的时间片用完与否的检查是在其他点做的。比如调度实体入队时enqueue_entity(),如果超过了时间片就会通过cfs_rq->throttled_list把cfs_rq挂到cfs_b->throttled_cfs_rq链表)。如果这个周期内CPU cgroup还有quota剩余,则从剩余的quota中为各个cfs_rq补充时间片。补充完时间片后还会遍历cfs_rq及其所有的parent将它们从throttled状态改变为unthrottled,这个过程就叫做unthrottle_cfs_rq(cfs_rq)
2.2 hardlockup原因分析
考虑一种情况在一个CPU核数很多的机器上创建有多层级、大量的cgroup;如果在某个周期这些cgroup中各个CPU上cfs_rq队列的时间片都用完被挂到了cfs_b->throttled_cfs_rq链表,这时候就需要去遍历、并去unthrottle_cfs_rq(cfs_rq),这个时间维度就是nr_cpus*nr_cgroups级别;假设nr_cpus==256,nr_cgroups=1000,这里将会有25W次的轮巡,同时这个轮巡过程是在时钟中断上下文,处于关中断状态,一次轮巡平均10us就会导致高达10秒的延迟,进而触发hardlockup。
3 修复说明
这个问题在Linux社区也有提出,社区对该问题也进行了修复,将原来一个CPU上的unthrottle_cfs_rq()改为异步方式来实现。
其思路就是将原来一个CPU上执行的nr_cpus*nr_cgroups次的unthrottle_cfs_rq()改造为只对this_cpu的cfs_rq进行直接unthrottle_cfs_rq();而对于其他CPU的cfs_rq,将他们挂到一个对应CPU的rq->cfsb_csd_list链表,然后通过smp_call_function_single_async()的IPI方式来通知到其他CPU去做unthrottle_cfs_rq();这样原本一个CPU上nr_cpus*nr_cgroups次的unthrottle_cfs_rq()就变成了1*nr_cgroups,减少了CPU数量太多的影响,时间复杂度变得可控。