免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

RCU CPU Stall检测机制详解:从原理到排查实战

RCU CPU Stall检测机制详解:从原理到排查实战 搞Linux内核和后台服务的人迟早会遇到一次rcu_sched detected stalls on CPUs/tasks或者INFO: rcu_preempt detected stalls。第一次见到这类报错的时候大多数人第一反应是“内核是不是崩了”其实不是。这是RCURead-Copy Update机制在履行它的一个隐藏职责当某个CPU上的grace period迟迟推进不动的时候RCU会主动跳出来报一个CPU stall警告。这篇文章就把这个机制从原理到实操掰开揉碎讲清楚适合内核调试新人、运维排查人员以及想深入理解RCU同步原语的同学。理解RCU检测CPU stall的原理本质上要做好三件事先搞清楚RCU自己的运行节奏再弄明白触发stall检测的时机和数据结构最后才是学会看那串吓人的告警日志。本文会按这个顺序走把关键代码路径、内核参数、常见坑点都过一遍。1. 先把RCU的基本盘说清楚1.1 三个绕不开的概念读侧临界区、宽限期、静止状态RCU这个同步机制和普通的读写锁完全不一样。普通读写锁里读者和写者要互斥读者在读的时候写者必须等着。RCU的思路是读者不锁写者也不等读者而是通过延迟释放老数据来保证安全。这个思路里最重要的就是“宽限期”grace period这个时间窗口。一个宽限期要顺利结束必须有这样一个前提正在运行的每一个读者都已经离开了它的读侧临界区read-side critical section。注意这里说的是“已经离开”不是“同意离开”更不是“即将离开”。RCU会把读者离开临界区的那个时间点记为一次“静止状态”quiescent state简称QS。为了让这个概念更好懂我经常打一个比方假设有一列火车车上的乘客就是读者每个乘客到站下车的那一刻就是一次QS。只有当所有乘客都下车了列车员才能宣布这一站彻底清空然后才允许保洁员上车换掉旧座椅。这个“清空”的过程对应的就是一次grace period。1.2 RCU为什么需要主动监控“进度”这里出现了另一个关键点RCU并没有一个实打实的实体从头到尾盯着每个核。它靠的是一种“懒惰强制”的混合策略。绝大多数时候CPU都在忙自己的事不会专门向RCU汇报“我进临界区了”“我出临界区了”。RCU必须等到每个CPU都至少经历一次上下文切换、用户态执行或空闲状态才能推断出“这个CPU上的读者都走干净了”。问题就来了如果某个CPU长时间不切换上下文、长时间卡在内核态不动RCU怎么判断这个核上的读者离开了没有答案是判断不了。它只能等。但如果无限等下去写者永远拿不到旧数据的释放权内核里很多回收机制就会彻底停摆。所以RCU必须有一个“等待超时”的机制——这就是CPU stall检测存在的意义。1.3 一个超时的宽限期到底会造成什么后果你可能觉得一个宽限期完不成最多就是旧内存晚点释放好像没什么大不了的。但真实情况比这严重得多。现代内核里RCU的使用非常广泛从list_hlist遍历、文件系统路径查找到网络命名空间的销毁都在使用RCU保护。如果grace period停滞意味着调用synchronize_rcu()的内核线程会卡死在等待队列里这些线程又会阻塞上层业务。实践中有过案例一个RCU stall卡了几十秒直接把宿主机的容器创建、销毁操作全部堵死磁盘IO队列也跟着涨最终拖垮业务。所以RCU的CPU stall检测本质上不是一个“报告坏消息”的功能而是一个“防止故障扩散”的兜底。知道了这一点再看后面的机制就有方向感了。2. stall检测机制的设计骨架2.1 stall不是“CPU死了”而是“该有的QS超时了”很多资料把CPU stall直译成“CPU停摆”这个说法其实有误导。RCU报CPU stall不代表那个CPU上的代码真的停止了执行而是说“我期待这个CPU在限定时间内上报一次QS但时间到了我没等到”。这个CPU可能正在执行一个超长的内核代码路径、可能被中断风暴干扰、也可能干脆是硬件层面卡住了但在上报QS这件事上它“迟到”了。这个区分特别重要因为它决定了排查方向。如果你的业务进程在用户态忙等绝大概率不会引发RCU stall因为用户态运行本身就会被RCU当作一种QS。真正让RCU头疼的是内核态内的长时间自旋、延时、关闭抢占等行为。2.2 检测触发的两条主线RCU检测CPU stall的逻辑在kernel/rcu/tree_stall.h和kernel/rcu/tree.c这几个文件里。核心路径可以从两条主线来理解。第一条主线在grace period启动时。当一个CPU发起新宽限期的时候RCU内部会记录一个时间戳if (gp_init_done) { rdp-gp_start jiffies; rdp-gp_seq rsp-gp_seq; }这里gp_start记下的就是本宽限期的起始时间。随后RCU会设置一个“期望截止点”下次检查时如果发现当前时间已经超过了截止点而宽限期还没结束就开始走stall告警逻辑。第二条主线在强制推进机制force quiescent state简称fqs里。RCU有个内核线程叫rcu_gp_kthread它会定期扫描各个节点尝试识别哪些CPU已经上报过QS哪些还没有。这部分代码片段如下static int rcu_gp_fqs_check_wake(int *gprp) { ... if (READ_ONCE(rsp-gp_state) RCU_GP_DOING_FQS) return 0; /* 已经在FQS状态 */ ... }fqs循环里每次都会检查是否超时超时条件一旦满足就调check_cpu_stall(rsp, rdp)主动判断要不要打印告警。判断逻辑的核心就是一个时间差static void check_cpu_stall(struct rcu_state *rsp, struct rcu_data *rdp) { unsigned long js jiffies; unsigned long t jiffies - rdp-gp_start; ... if (t rcu_state.jiffies_stall) { if (rcu_cpu_stall_ftrace_dump) rcu_ftrace_dump(); ... if (rcu_cpu_stall_suppress) return; if (gp_state RCU_GP_DOING_FQS) print_other_cpu_stall(rsp, rdp); else print_cpu_stall(rsp, rdp); } }这里的jiffies_stall就是“宽限期的有效期”。如果jiffies已经超过了启动时间加上超时阈值说明宽限期已经逾期RCU就会判断是当前CPU自己的问题还是其他CPU的问题然后走不同的打印路径。2.3 数据结构里的“线索字段”RCU这套机制能定位到具体是哪个CPU卡住依赖的是分散在rcu_state、rcu_node、rcu_data里的若干字段。它们的关系是这样的rcu_state是全局状态记录DPU整体的宽限期序列号gp_seq、当前是否在做FQSgp_state、以及上一次记录stall的时间jiffies_stall。rcu_node是树形节点负责管理一组CPU的QS汇总情况每个节点上有个qsmask每一位对应一个子节点或者CPU哪位还是1说明那位还没上报QS。rcu_data是每个CPU一个的本地状态记录本CPU当前看到的尽头期序号、自身是否有待上报的QS等。当打印告警时内核会遍历这棵树利用qsmask来反向找出到底是哪位CPU卡住了。这个设计很精巧你不必挨个去问每个CPU“你怎么样了”只要看树上谁还没把自己的位清掉就知道嫌疑人了。3. 从stall报告看懂内核在说什么3.1 一份典型的RCU stall日志下面是一份真实场景里比较常见的RCU stall告警我稍微改了字段值帮你做拆解INFO: rcu_sched detected stalls on CPUs/tasks: 0-...!: (0 ticks this GP) idle1f6/1/0x4000000000000000 softirq245/246 fqs814 (detected by 7, t21009 jiffies, g160049, q2001)第一行说“rcu_sched检测到了CPU或任务上的stall”。注意这里的rcu_sched表示所属的RCU子类型它负责的是普通上下文里的读侧保护对应的还有rcu_bh和rcu_preempt。第二行开头那个0-...!先说CPU编号是0...系列符号表示这个CPU当前处于什么状态。ticks this GP说的是这个宽限期里这片CPU上经历的节拍数。后面idle...里的三个数字分别代表是否处于idle状态1f6是状态值、idle进入的层级、idle状态标志。softirq245/246说明软中断累计次数前后值fqs814表示强制静止状态被执行的次数。第三行detected by 7是本次检测者说明是CPU7发现CPU0不对劲的。t21009 jiffies表示已经等了这么多个jiffieg160049是宽限期序号q2001是当前队列里的东西数量。3.2 逐项解读日志里的关键信息我把日志里的核心字段整理成了一张速查表排查时直接对照看字段示例值含义与排查方向检测方detected by 7是哪个CPU触发的告警不代表故障CPU嫌疑CPU0-...!可能是问题来源的具体CPU编号ticks this GP(0 ticks)该CPU在当前宽限期经的节拍数若为0说明它几乎没走idle字段idle1f6/1/...CPU是否处于idle不是idle的话要重点关注t21009宽限期超时等待时间可初步判断卡了多久g160049宽限期序号多次报这个值变化很大则说明GP在推进q2001RCU队列里的回调数量数量激增是副产品而非原因在展开说怎么做之前0-...!里的状态符号也很关键。括号前面有一串点、斜杠、感叹号之类的字符它们一般表示的是这个CPU在RCU状态机里的位置。最简单直接的做法是去看内核源码里print_cpu_stall_info()函数它会打印每个bit位的含义。不过实践里我拿到日志后最先看的其实不是这些标志位而是后面CPU的指令指针值。3.3 CPU指令指针值为什么最重要如果stall报告附带了RIP:或者UIP:字段那基本等于内核在告诉你这个CPU当时正在执行哪段代码。这个是定位问题最直接的抓手。rcu: rcu_sched kthread starved for 21003 jiffies! g160049 f0x0 RCU_GP_WAIT_FQS(5) -state0x0 -cpu7有时候你看到的是这种“kthread starved”的告警。它说明RCU自己的内核线程rcu_sched被饿着了长时间得不到调度。这种情况多半不是某个CPU在死循环而是CPU被高优先级负载霸占普通线程一直排不上队。拿到RIP后用addr2line或者gdb把地址翻译成函数名是排查的第一步。比如addr2line -e /usr/lib/debug/lib/modules/$(uname -r)/vmlinux ffffffff810a2b3c当然很多线上环境没有完整的调试符号这时候你可以结合/proc/kallsyms查最近的内核符号或者直接把地址丢给crash工具去解析。4. 影响stall判定时长的关键参数4.1 超时阈值与抑制开关RCU的stall检测不是狠心一刀切它给了系统管理员很多可调旋钮。最常用的一个是rcupdate.rcu_cpu_stall_timeout这个参数控制的是“从宽限期开始到触发告警之间的秒数”。默认值通常是21秒但如果你在内核构建时设置了CONFIG_RCU_CPU_STALL_TIMEOUT这个默认值会随之改变。这个参数怎么调取决于你的业务容忍度。21秒内如果宽限期还没有结束内核就开始打印告警。如果系统负载本身很高虚拟机迁移、大页分配之类的操作会拖慢QS上报可以适当调到30到60秒。反过来如果你的业务对延迟极其敏感希望尽早发现问题可以调到5到10秒。单位是秒注意在启动参数里传的是整数。另一个很实用的参数是rcupdate.rcu_cpu_stall_suppress。把它设成1可以暂时屏蔽RCU stall告警的输出。这适合应急处理比如你已经知道有已知问题、暂时不想刷屏但绝不能长期开着否则等于蒙眼狂奔。建议这类参数只作为应急手段问题修复后立刻恢复正常配置。4.2 改动参数后的生效范围与副作用rcu_cpu_stall_timeout这类参数既可以通过内核启动命令行传入也可以在运行时通过/sys/module/rcupdate/parameters/下面对应的文件来调整。比如echo 30 /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout运行时调整的好处是无需重启实测下来对线上环境很友好。但要记住调大超时时间只会推迟内核报告stall的时间不会解决导致stall的根因。那个卡住的宽限期该卡还是卡只是报警迟到而已。调小超时时间会更容易暴露问题但也可能导致内核频繁刷告警影响dmesg里其他日志的可读性。rcu_cpu_stall_fail_text参数也值得一提。如果开启了相关配置一旦触发stall内核会尝试输出一个额外的失败报告文本。这个打印通常包含更多硬件层和底层状态信息适合做深层次排查时开启。开启方式同样是写对应sysfs节点。4.3 与lockup检测机制的联动RCU stall和softlockup、hardlockup检测器经常一起出现。softlockup通常是因为某个CPU在内核态跑了20秒以上导致软中断得不到调度而RCU stall可能是因为同样的原因导致QS没法上报。这两者不是同一个机制但往往共享同一个根因。诊断时有个技巧如果日志里RCU stall和softlockup同时出现优先排查共享的罪魁祸首比如长时间关抢占的代码路径、异常的时钟中断行为或者宿主机层的vCPU停止调度。如果只有RCU stall而没有softlockup那可能问题出在RCU自己的回调线程被饿死而不是CPU完全卡死。5. 实操从告警到定位问题的完整流程5.1 第一步判断是“谁在制造stall”拿到RCU stall告警后第一步不是去看代码而是先确认这到底是“某个CPU没有上报QS”还是“RCU线程本身被饿死”这两种情况的处理方向完全不同。如果是前者日志里通常会有明确的CPU编号和指针信息。如果是后者你会看到rcu_sched kthread starved这种表述。区分这两类能帮你把排查范围缩小一半。接着要看stall报告里的t值。如果t等于超时阈值附近比如默认21秒左右说明宽限期是一到时间就立刻报出来的这个CPU可能从宽限期开始就一直没交QS。如果t远大于阈值比如60秒甚至几百秒说明RCU经历了反复的fqs强制过程GP一直推不动这种情况多半不是简单死循环而是有持续的高优先级中断或软中断在作祟。5.2 第二步借助nmi_watchdog和硬锁检测交叉验证遇到RCU stall不要孤立地看这一条日志。我会同时打开/proc/sys/kernel/nmi_watchdog看看NMI看门狗是不是开着再关注dmesg里有没有hard LOCKUP或soft lockup的相邻输出。如果NMI watchdog同时触发说明那个CPU可能连定时器中断都进不去了这基本指向硬件故障、虚拟机vCPU抢占或者极端的高优先级自旋。如果只有RCU stall说明CPU还在响应局部中断只是作者没有机会走到用户态或idle导致RCU读侧临界区的退出事件迟迟没有发生。在虚拟化场景里要特别留意很多云主机上的RCU stall实际上是被宿主机的CPU调度影响。比如一台宿主机上vCPU数量超过物理核心数某个vCPU被赶下物理核之后长时间没有被调度回来guest里的时钟仍在走但实际执行的指令少得可怜这就会表现为RCU stall但看不出明显的内核代码问题。5.3 第三步用栈回溯和性能采样定位热点确认嫌疑CPU后如果日志附带了栈回溯直接看栈顶的几层函数。没有的话我通常组合使用两个手段/proc/pid/stack看可疑进程的内核栈以及用perf record对嫌疑CPU做短时间的采样。还有个笨但有效的办法如果问题可以复现就在触发前用ftrace把rcu_sched相关的函数调用全部记录下来。内核里有现成的tracepoint比如rcu_utilization可以打开看一下echo 0 /sys/kernel/debug/tracing/tracing_on echo 1 /sys/kernel/debug/tracing/events/rcu/rcu_utilization/enable echo 1 /sys/kernel/debug/tracing/tracing_on sleep 30 cat /sys/kernel/debug/tracing/trace | grep CPU:0 | tail -100这么抓出来的数据能清楚看到CPU0什么时候最后一次上报QS之后又发生了什么事件。有了这个时间线再去对代码路径做静态排查基本就能定位到具体函数。5.4 第四步常见的定位结论长什么样按我的经验RCU stall最常见的几类根因分别是驱动里的长临界区或长时间关闭抢占、实时线程设置优先级之后霸占CPU、虚拟化层的vCPU异常、以及ACPI/固件相关的深睡眠路径卡住。每类问题的特征都有区别驱动类的典型特征是栈上有某个驱动函数的长时间循环比如网卡驱动在某些异常情况下进入重传循环同时持有了一个spinlock。实时线程霸占CPU的特征是栈底是sched_class_rq之类的调度器路径而且nmi watchdog大概率同时报警。虚拟化类的特征则是guest里看不出明显热点且经常是某一台物理机上的多个guest同时出现stall。6. 常见问题与避坑指南6.1 排查RCU stall时常踩的坑第一个坑是看到rcu_sched就以为和“调度器”有关。这里的sched是历史命名问题指的是这个RCU变体在普通可抢占上下文中的行为和schedule()本身没有直接关系。把它当成调度器问题去查会白走很多弯路。第二个坑是忽略了stall报告里“detected by”字段。有些新手看到谁报的就查谁结果盯着一个无辜的CPU分析半天。记住detected by是发现者问题CPU在下面那行数字里。第三个坑是过于依赖q数值。RCU回调队列长度增长通常是宽限期滞后的结果不是原因。你把队列清掉、或者调大阈值都只是压住症状宽限期推不动的根因还在那里迟早还会再爆。第四个坑是修改超时参数后不等生效就判断结果。运行时通过sysfs改参数最好读回来确认一下有些发行版的内核启用了CONFIG_RCU_STALL_COMMON但某些参数只读你写进去是成功了但因为权限问题实际没改到。6.2 实操心得哪些方法最管用根据我个人大量排查stall问题的经验有几个方法非常管用。一是永远保留一份与生产环境相同版本内核的调试符号。没有符号RIP地址就是一堆数字有了符号问题定位时间能缩短一个数量级。具体做法是提前把kernel-debuginfo包或者自己编译时的vmlinux保存下来。二是学会用crash工具做离线分析。如果系统真的hang住走到kdump之后crash配合vmcore可以直接查看每个CPU的rcu_data状态搞清楚宽限期卡在了哪个节点上比看屏幕上的打印日志准确得多。三是在问题频发的系统上随手记录一下基线数据。你可以写一个简单的脚本定期采集/proc/pressure/cpu、每个CPU的软中断次数和/sys/kernel/debug/rcu/下的状态文件。有了基线再遇到stall就能快速判断是单点突变还是积累恶化。6.3 一个可以落地的快速排查脚本下面这个简单脚本可以在遇到stall告警时帮你一次性拉齐现场关键信息#!/bin/bash echo dmesg RCU lines dmesg | grep -i rcu.*stall | tail -20 echo per-cpu softirq counts cat /proc/softirqs | head -20 echo RCU state for f in /sys/kernel/debug/rcu/*; do echo --- $f --- cat $f 2/dev/null done echo load runqueue cat /proc/loadavg ps -eo pid,pri,pcpu,stat,comm --sort-pcpu | head -15把脚本放到出问题的那台机器上触发异常后立刻执行输出的内容几乎覆盖了判断RCU stall根因所需的全部现场数据。我在多个业务环境里都用这套方案效率很高。7. 最后再分享一个小技巧调rcu_cpu_stall_timeout的时候很多人会忽略它的最小值限制。内核源码里对这个参数有一个下限判断你设的值如果小于某个阈值实际生效时会自动被钳位。所以如果你设了5秒但告警还是21秒才出不要慌先读回sysfs节点看实际生效值不要凭想象判断参数是否写进去了。排查RCU stall这件事说到底比拼的是对内核运行节奏的理解。多抓几次现场、多看几份真实报告之后你会发现这类告警不仅不可怕反而是内核给你递过来的一条引线顺着它走下去往往能挖出埋得很深的问题。
返回列表