接到业务兄弟报障,出现多例crash,堆栈大多数如下:

KERNEL: /usr/lib/debug/lib/modules/3.10.0-957.27.2.el7.x86_64/vmlinux
DUMPFILE: vmcore [PARTIAL DUMP]
CPUS: 48
DATE: Wed Dec 11 23:25:55 2019
UPTIME: 80 days, 12:06:06---------------------运行时间
LOAD AVERAGE: 67.57, 65.21, 53.49-----------死之前load 很高
TASKS: 3169
NODENAME: cdh-bjht-3814
RELEASE: 3.10.0-957.27.2.el7.x86_64
VERSION: #1 SMP Mon Jul 29 17:46:05 UTC 2019
MACHINE: x86_64 (2300 Mhz)
MEMORY: 382.5 GB
PANIC: "Kernel panic - not syncing: Hard LOCKUP"
PID: 0
COMMAND: "swapper/34"
TASK: ffff9f1a36998000 (1 of 48) [THREAD_INFO: ffff9f1a36994000]
CPU: 34
STATE: TASK_RUNNING (PANIC)

crash> bt
PID: 0 TASK: ffff9f1a36998000 CPU: 34 COMMAND: "swapper/34"
#0 [ffff9f487e0489f0] machine_kexec at ffffffffae063b34
#1 [ffff9f487e048a50] __crash_kexec at ffffffffae11e242
#2 [ffff9f487e048b20] panic at ffffffffae75d85b
#3 [ffff9f487e048ba0] nmi_panic at ffffffffae09859f
#4 [ffff9f487e048bb0] watchdog_overflow_callback at ffffffffae14a881
#5 [ffff9f487e048bc8] __perf_event_overflow at ffffffffae1a26b7
#6 [ffff9f487e048c00] perf_event_overflow at ffffffffae1abd24
#7 [ffff9f487e048c10] intel_pmu_handle_irq at ffffffffae00a850
#8 [ffff9f487e048e38] perf_event_nmi_handler at ffffffffae76d031
#9 [ffff9f487e048e58] nmi_handle at ffffffffae76e91c
#10 [ffff9f487e048eb0] do_nmi at ffffffffae76eb3d
#11 [ffff9f487e048ef0] end_repeat_nmi at ffffffffae76dd89
[exception RIP: tg_unthrottle_up+24]
RIP: ffffffffae0dc4d8 RSP: ffff9f487e043e08 RFLAGS: 00000046
RAX: ffff9f4a6a078400 RBX: ffff9f787be9ab80 RCX: ffff9f4a03ea9530
RDX: 0000000000000005 RSI: ffff9f787be9ab80 RDI: ffff9f4a03ea9400------task_group
RBP: ffff9f487e043e08 R8: ffff9f782c78a100 R9: 0000000000000001
R10: 0000000000004dcd R11: 0000000000000005 R12: ffff9f78790edc00
R13: ffffffffae0dc4c0 R14: 0000000000000000 R15: ffff9f4a03ea9400
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0000
--- <NMI exception stack> ---
#12 [ffff9f487e043e08] tg_unthrottle_up at ffffffffae0dc4d8
#13 [ffff9f487e043e10] walk_tg_tree_from at ffffffffae0d3b20
#14 [ffff9f487e043e60] unthrottle_cfs_rq at ffffffffae0e46e7
#15 [ffff9f487e043e98] distribute_cfs_runtime at ffffffffae0e496a
#16 [ffff9f487e043ee8] sched_cfs_period_timer at ffffffffae0e4b67
#17 [ffff9f487e043f20] __hrtimer_run_queues at ffffffffae0c71e3
#18 [ffff9f487e043f78] hrtimer_interrupt at ffffffffae0c776f
#19 [ffff9f487e043fc0] local_apic_timer_interrupt at ffffffffae05a61b
#20 [ffff9f487e043fd8] smp_apic_timer_interrupt at ffffffffae77b6e3
#21 [ffff9f487e043ff0] apic_timer_interrupt at ffffffffae777df2
--- <IRQ stack> ---
#22 [ffff9f1a36997db8] apic_timer_interrupt at ffffffffae777df2
[exception RIP: cpuidle_enter_state+87]
RIP: ffffffffae5b06c7 RSP: ffff9f1a36997e60 RFLAGS: 00000202
RAX: 0018b604313aebda RBX: ffff9f1a36997e38 RCX: 0000000000000018
RDX: 0000000225c17d03 RSI: ffff9f1a36997fd8 RDI: 0018b604313aebda
RBP: ffff9f1a36997e88 R8: 0000000000005a0e R9: 0000000000000018
R10: 0000000000004dcd R11: 0000000000000005 R12: ffffffffae7699bc
R13: ffffffffae7699c8 R14: ffffffffae7699bc R15: ffffffffae7699c8
ORIG_RAX: ffffffffffffff10 CS: 0010 SS: 0000
#23 [ffff9f1a36997e90] cpuidle_idle_call at ffffffffae5b081e
#24 [ffff9f1a36997ed0] arch_cpu_idle at ffffffffae0366de
#25 [ffff9f1a36997ee0] cpu_startup_entry at ffffffffae0fd7ba
#26 [ffff9f1a36997f28] start_secondary at ffffffffae0580d7
#27 [ffff9f1a36997f50] start_cpu at ffffffffae0000d5

从堆栈中可以看出,我们在关中断运行的时间过长,导致了hardlock检测,hardlock检测的原理在此不多说,网上成熟的资料很多。

/*
 * High resolution timer interrupt
 * Called with interrupts disabled
 */
void hrtimer_interrupt(struct clock_event_device *dev)

既然是时间长,就要看一下这个阈值是多少:

crash> p watchdog_thresh
watchdog_thresh = $1 = 60

默认是10s,说明这个环境上有人因为hardlock的问题加大过这个阈值。

反汇编对应的代码,分析如下:

crash> task_group ffff9f4a03ea9400
struct task_group {
css = {
cgroup = 0xffff9f4a57564000,

crash> cgroup.sibling 0xffff9f4a57564000
sibling = {
next = 0xffff9f4a84df3c10,
prev = 0xffff9f1b5b435a10
}
crash> list 0xffff9f4a84df3c10 |wc -l
4460

说明该task_group有4459个兄弟,也就是同一等级的task_group 有这么多,4459比 4460少一个是因为要减去

父cgroup的作为链表串接头的计数。

我们知道,cfs在支持组调度之后,每个task_group创建的两个定时器,一个是周期性定时器,也就是堆栈中的sched_cfs_period_timer ,

另外一个是 slack_timer,用来归还时间给总池子的。

因为hrtimer是关中断运行的,所以需要解决堆栈中这个定时器为什么会运行这么长时间。

经过分析代码,有两种可能,一种如下:

static enum hrtimer_restart sched_cfs_period_timer(struct hrtimer *timer)
{
    struct cfs_bandwidth *cfs_b =
        container_of(timer, struct cfs_bandwidth, period_timer);
    ktime_t now;
    int overrun;
    int idle = 0;

raw_spin_lock(&cfs_b->lock);
    for (;;) {-----------一直循环
        now = hrtimer_cb_get_time(timer);//其实就是ktime_get,也就是当前时间
        overrun = hrtimer_forward(timer, now, cfs_b->period);

        if (!overrun)-------假设overrun一直非0则不退出
            break;

        idle = do_sched_cfs_period_timer(cfs_b, overrun);
    }
    raw_spin_unlock(&cfs_b->lock);

    return idle ? HRTIMER_NORESTART : HRTIMER_RESTART;
}

这是一种可能,还有一种可能如下:

static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b, int overrun)

{。。。

while (throttled && cfs_b->runtime > 0 && !cfs_b->distribute_running) {////当前还是被throttled并且还有时间没有分发完毕,并且没有并发分发

runtime = cfs_b->runtime;//剩余的时间继续分发

cfs_b->distribute_running = 1;//设置正在分发的标志
        raw_spin_unlock(&cfs_b->lock);
        /* we can't nest cfs_b->lock while distributing bandwidth */
        runtime = distribute_cfs_runtime(cfs_b, runtime,
                         runtime_expires);
        raw_spin_lock(&cfs_b->lock);

cfs_b->distribute_running = 0;
        throttled = !list_empty(&cfs_b->throttled_cfs_rq);//判断当前是否被throttled

        cfs_b->runtime -= min(runtime, cfs_b->runtime);//防止变成负数
    }

}

因为第二个循环受限于一种条件,也就是当前被限流的task_group不为空,同时还有时间没有分发完毕,同时没有在并发分发,否则则会break,

我们先来分析第二种可能,先看看当前被限流的task_group链表是否为空,这个是串接在父task_group中的。

crash> task_group.parent ffff9f4a03ea9400
parent = 0xffff9f78790edc00

crash> task_group.cfs_bandwidth.throttled_cfs_rq 0xffff9f78790edc00
cfs_bandwidth.throttled_cfs_rq = {
next = 0xffff9f782c78c900,
prev = 0xffff9f48362bd900
},
crash> list 0xffff9f782c78c900 |wc -l
46

说明还有46个task_group被限流,满足一个条件,那runtime呢?

crash> task_group.cfs_bandwidth.runtime 0xffff9f78790edc00
cfs_bandwidth.runtime = 0,

初步看起来好像不符合循环的条件,那有没有可能是前面一直循环,但是到crash的时候,这个值被修改为了0呢?

那就需要查看一下调用 distribute_cfs_runtime 的时候的 runtime是多少,runtime是第二个参数,也就是rsi,我们需要获取栈中的数据,

crash> dis -l distribute_cfs_runtime
/usr/src/debug/kernel-3.10.0-957.27.2.el7/linux-3.10.0-957.27.2.el7.x86_64/kernel/sched/fair.c: 3487
0xffffffffae0e4860 <distribute_cfs_runtime>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffae0e4865 <distribute_cfs_runtime+5>: push %rbp
0xffffffffae0e4866 <distribute_cfs_runtime+6>: mov %rsp,%rbp
0xffffffffae0e4869 <distribute_cfs_runtime+9>: push %r15
0xffffffffae0e486b <distribute_cfs_runtime+11>: push %r14
0xffffffffae0e486d <distribute_cfs_runtime+13>: push %r13
0xffffffffae0e486f <distribute_cfs_runtime+15>: push %r12
0xffffffffae0e4871 <distribute_cfs_runtime+17>: push %rbx
0xffffffffae0e4872 <distribute_cfs_runtime+18>: sub $0x18,%rsp
0xffffffffae0e4876 <distribute_cfs_runtime+22>: mov %rsi,-0x40(%rbp)----将初始剩余时间压栈,也就是栈中的000000003b9aca00,也就是十进制的 1000000000

#15 [ffff9f487e043e98] distribute_cfs_runtime at ffffffffae0e496a
ffff9f487e043ea0: 000000003b9aca00 ffff9f782c78a100 ----我们的rsi被压栈在此
ffff9f487e043eb0: 51afefcdce629c68 ffff9f78790edd80
ffff9f487e043ec0: ffff9f78790edd48 ffff9f78790ede40
ffff9f487e043ed0: 0018a72ed3fa2963 0000000000000001
ffff9f487e043ee0: ffff9f487e043f18 ffffffffae0e4b67
#16 [ffff9f487e043ee8] sched_cfs_period_timer at ffffffffae0e4b67
ffff9f487e043ef0: ffff9f78790edd80 ffff9f487e055960
ffff9f487e043f00: ffff9f487e0559a0 ffffffffae0e4a90
ffff9f487e043f10: ffff9f487e055a98 ffff9f487e043f70
ffff9f487e043f20: ffffffffae0c71e3

crash> pd 0x000000003b9aca00
$2 = 1000000000

而带宽设置的时候,参数如下:

crash> cfs_bandwidth.distribute_running,runtime,period,quota ffff9f78790edd48
distribute_running = true
runtime = 0
period = {
tv64 = 23148000
}
quota = 1000000000

所以说,能确定进入 distribute_cfs_runtime的时候,是第一次循环,因为rsi入参的值就是quota配置的值。也就是说,我们推断的第二种可能性被推翻了,

不是 do_sched_cfs_period_timer 的while 循环导致了hardlock的检测,当然我们也不能排除一次while循环耗时很长的情况。

而 do_sched_cfs_period_timer 的执行时间长短取决于 throttled_cfs_rq 链表的长短,目前crash的时候是有 45 个task_group 被限流,

有没有一种可能,很多个限流的task_group,导致处理很长时间,直到crash的时刻还剩下45个未处理完毕了?通过走读代码,这种概率是存在的,

但是我认为非常小,因为我们 当前对应的内核版本3.10.0.957内核,已经合入了 id=c06f04c70489b9deea3212af8375e2f0c2f0b184 这个补丁的。

哪怕最极端的情况,当出现 4459个 task_group被限流,需要 distribute_cfs_runtime 来解除限流,对应的时间消耗也达不到60s之多。

所以我们需要回到 第一种可能性去,就是 sched_cfs_period_timer 出现了多次循环。

第一种可能需要出现循环的条件是:

hrtimer_forward 返回非0值,

overrun就是当前时间减去timer->node.expires  为 cfs_b->period 的整数倍的次数,也就是相当于本来应该在 timer->node.expires  时刻回调的定时器没有及时执行

period_timer = {
node = {
node = {
__rb_parent_color = 18446637938508750208,
rb_right = 0x0,
rb_left = 0x0
},
expires = {
tv64 = 6955566201684000
}
},

那么需要制造这样一种情况的条件就是:cfs_b→period比较小,另外待解除限流的进程足够多就行,好的,下面我们就模仿一下这种情况:

第一步,创建一个耗cpu的进程

# cat caq.c

#include <stdio.h>

int main(int argc,char* argv[])
{
int i=0;
while(1)
i++;

}

然后gcc -g -o caq.o caq.c

这个caq.o下面会用到。

#!/bin/bash

mkdir -p /sys/fs/cgroup/cpu/user.slice/1

echo 1000> /sys/fs/cgroup/cpu/user.slice/1/cpu.cfs_period_us

#因为我们有60个核,就设置54个吧,

echo 54000> /sys/fs/cgroup/cpu/user.slice/1/cpu.cfs_quota_us

for i in {1..2000}
do
mkdir -p /sys/fs/cgroup/cpu/user.slice/1/$i
temp="_caq_$i"
echo $temp
./caq.o "$temp" &
pid=$(ps -ef |grep -i $temp|grep -v grep |awk '{print $2}')
echo $pid >/sys/fs/cgroup/cpu/user.slice/1/$i/cgroup.procs
done

在一个目录下创建2000个cg,嗯,很快,我们就触发了crash

如果你在线上执行了上述操作,嗯,你可以更新自己的简历了。。。。

那么怎么解决这个问题呢?commitid=2e8e19226,看似把问题解决了,如下:

	for (;;) {
@@ -4899,6 +4902,28 @@ static enum hrtimer_restart sched_cfs_period_timer(struct hrtimer *timer)
if (!overrun)
break; + if (++count > 3) {
+ u64 new, old = ktime_to_ns(cfs_b->period);
+
+ new = (old * 147) / 128; /* ~115% */
+ new = min(new, max_cfs_quota_period);
+
+ cfs_b->period = ns_to_ktime(new);
+
+ /* since max is 1s, this is limited to 1e9^2, which fits in u64 */
+ cfs_b->quota *= new;
+ cfs_b->quota /= old;
+
+ pr_warn_ratelimited(
+ "cfs_period_timer[cpu%d]: period too short, scaling up (new cfs_period_us %lld, cfs_quota_us = %lld)\n",
+ smp_processor_id(),
+ new/NSEC_PER_USEC,
+ cfs_b->quota/NSEC_PER_USEC);
+
+ /* reset count so we don't come right back in here */
+ count = 0;
+ }
+

但是如果仔细分析的话,也不一定能解决,比如你变态地创建更多cpu消耗型的cg,period升级也可能会触发crash。
从业务的角度说,尽量不要在一个目录下创建那么多耗cpu的cg,同时,将period尽量放大一些比较好。

centos7.6内核之cfs_bandwidth下的distribute_cfs_runtime hard lockup的更多相关文章

  1. (转)centos7优化内核参数详解

    centos7优化内核参数详解 原文:http://blog.csdn.net/xiegh2014/article/details/52132863 cat /etc/sysctl.conf #CTC ...

  2. CentOS-7 在windows server 2012下的虚拟机安装教程

    CentOS-7 在windows server 2012下的虚拟机安装教程 一.下载 CentOS-7-x86_64-DVD-1611.iso https://mirrors.aliyun.com/ ...

  3. CentOS7.0 内核(3.10.0-123.el7.x86_64)bug导致KVM物理机重启

    一.问题描述 服务器硬件:DELL R720 系统版本:CentOS7.0 内核版本:3.10.0-123.el7.x86_64 故障现象:偶尔会重启 二.问题原因 经查看dmesg日志发现是kern ...

  4. CentOS7修改内核启动顺序

    CentOS7修改内核启动顺序: 1.首先查看当前系统有几个内核 cat /boot/grub2/grub.cfg |grep menuentry   2.查看当前默认内核 grub2-editenv ...

  5. Centos7多内核情况下修改默认启动内核方法

    1.1  进入grub.cfg配置文件存放目录/boot/grub2/并备份grub.cfg配置文件 [root@linux-node1 ~]# cd /boot/grub2/ [root@linux ...

  6. Centos7 kernel 内核升级 GPU显卡驱动程序编译安装

    1.NVIDIA官网下载相关显卡驱动 #在服务器上查看网卡型号 lspci -mm | grep NVIDIA   #在NVIDIA官网下载相应型号驱动程序 https://www.geforce.c ...

  7. Centos7在单用户模式下重置root密码

    1.启动Centos7 ,按空格让其停留在如下界面: 鼠标上下可以选择启动内核,默认选择第一个内核开机 2.按e键进入编辑模式 e 按下e键后我们可能无法看到我们需要编辑的区域,这是因为在较新版本的C ...

  8. 基于centos7的真实机环境下安装 vmware workstastion

    通常我们在在虚拟机里面搭建大数据集群,如果我们换在真实机里面搭建大数据集群的话, 我们的每一台电脑就是centos系统了,这个时候如果我们需要按vmware 软件的话我们就需要下载不同的版本了 废话不 ...

  9. CentOS7升级内核kernel5.0

    升级过程: 原系统:CentOS7.3 [root@my-e450 ~]# uname -r3.10.0-514.el7.x86_64 安装必需的软件包: # yum update# yum inst ...

随机推荐

  1. KNN算法推理与实现

    Overview K近邻值算法 KNN (K - Nearest Neighbors) 是一种机器学习中的分类算法:K-NN是一种非参数的惰性学习算法.非参数意味着没有对基础数据分布的假设,即模型结构 ...

  2. 【NOIP2017 提高组正式赛】列队 题解

    题目大意 有一个 \(n\times m\) 的方阵,每次有 \((x,y)\) 离开,离开后有两个命令 向左看齐.这时第一列保持不动,所有学生向左填补空缺.这条指令之后,空位在第 \(x\) 行第 ...

  3. SQL的语法

    SQL的语法 SQL通用语法 SQL语句可以单行或多行书写,以分号(";")结尾. SQL语句可以使用空格或缩进增强可读性. MySQL数据库的SQL语句不区分大小写(建议关键字大 ...

  4. JavaScript 语言入门

    目录 JavaScript 介绍 JavaScript 和 和 html 代码的结合方式 第一种方式 第二种方式 4.变量 关系(比较)运算 逻辑运算 数组(重点) 函数(重点) 函数的二种定义方式 ...

  5. VueX的热更替你知道多少?

    前言 我们在使用Vuex的时候,会时不时的更改Vuex内的数据,但是页面不会随之更新,如果数据量大,一个数据依赖另一个数据的话,这样我们要是再刷新页面的话会把以前依赖的数据清空,效率特别低.所以,今天 ...

  6. 学习笔记-JDBC连接数据库操作的步骤

    前言 这里我就以JDBC连接数据库操作查询的步骤作以演示,有不到之处敬请批评指正! 一.jdbc连接简要步骤 1.加载驱动器. 2.创建connection对象. 3.创建Statement对象. 4 ...

  7. 【Azure Developer】完成算法第4版书中,第一节基础编码中的数组函数 histogrm()

    问题描述 算法 Algorithms (第四版)书中,第1章:基础编程模型第15题: 结果: 编写一个静态方法 histogram(), 接受一个整型数组a[] 和一个整数M为参数,并返回一个大小为M ...

  8. 聊聊Netty那些事儿之从内核角度看IO模型

    从今天开始我们来聊聊Netty的那些事儿,我们都知道Netty是一个高性能异步事件驱动的网络框架. 它的设计异常优雅简洁,扩展性高,稳定性强.拥有非常详细完整的用户文档. 同时内置了很多非常有用的模块 ...

  9. XXXX系统测试计划

    XXXX系统测试计划 目录 XXXX系统测试计划 目标 概述 项目背景 适用范围 组织形式 组织架构图 角色及职责 测试工作分工 团队协作 测试对象 应测试特性 不被测试特性 测试任务安排 系统测试任 ...

  10. Elasticsearch深度应用(上)

    索引文档写入和近实时搜索原理 基本概念 Segments in Lucene 众所周知,Elasticsearch存储的基本单元是shard,ES种一个index可能分为多个shard,事实上每个sh ...