RCU stall与CPU隔离:isolcpus和nohz_full引发的宽限期超时排查指南
发布时间:2026/10/18 21:06:17 锦皓数字建站

前一阵子排查一台跑数据采集服务的机器系统负载明明不高业务线程也很稳可dmesg里每隔几十秒就蹦出一行rcu_sched self-detected stall on CPU而且每次都是同一个核。一开始我怀疑是驱动占了自旋锁查完中断、查完IO最后定位到启动参数里的isolcpus—— 我把几个核单独隔离出来跑实时采集任务却没想到这个看似“给业务让路”的操作把RCU机制推进了死角。今天就把这个问题拆开讲清楚为什么隔离的核会导致RCU超时以及碰到这种告警时该怎么定位、怎么修。这篇文章适合正在做CPU隔离、实时业务调优的人也适合那些见到RCU stall就心慌但不知道从哪里下手的系统工程师。1. 一次“诡异”的RCU stall告警1.1 现场情况还原机器是双路服务器总共16个逻辑核内核版本5.10。业务是数据采集要求低延迟于是启动参数里加了isolcpus2-7 nohz_full2-7 rcu_nocbs2-7隔离核2到核7专门跑采集线程。启动之后业务确实稳了延迟也下去了可过了不到半小时系统开始出现间歇性卡顿——不是业务线程卡而是整个系统响应变慢比如ssh登录偶尔要等好几秒ps命令也像被什么东西拖住一样。再看dmesg全是这类信息rcu_sched self-detected stall on CPU (t60005 jiffies g-785 q...)最扎眼的是stall on CPU后面跟的编号正是我隔离出来的核2。当时第一反应是“不可能”核2明明只跑一个用户态采集线程它不碰内核锁、不访问磁盘怎么可能把RCU拖住1.2 先给问题定性RCU stall的字面意思是RCU机制检测到某个宽限期grace period太长时间没有结束。RCU是内核里负责“延迟回收”的一套同步机制它需要所有CPU定期向它报告“我已经到了一个安全点”。一旦某个CPU长时间不报告宽限期就悬在那里依赖RCU释放的内核对象全部堆积起来——这就解释了为什么系统会变慢不是CPU忙而是内核里某些回收动作被卡住了。这里要强调一点RCU stall不等于死锁。死锁是大家都在等资源RCU stall是RCU在等一个特定的CPU“表态”。所以排查的方向不是找锁而是找到“谁没表态、为什么没表态”。在我这个案例里答案很快就指向了隔离配置本身。2. RCU的底层逻辑它为什么非要所有核“表态”2.1 RCU到底解决了什么问题要理解隔离核为什么引发RCU超时得先明白RCU存在的意义。内核里有很多数据结构比如文件系统inode、网络连接、内存映射会被多个CPU并发读取。传统的做法是用读写锁读的人拿读锁写的人拿写锁大家互相等待。但读写锁有个性能问题——读多写少时写者会被读者活活饿死读者之间也要互相竞争缓存行。RCU的思路很不一样。它允许读者完全不加锁地访问数据写者要更新数据时不直接修改原来的对象而是复制一份、改好、然后把指针原子地换成新版本。旧版本的数据还在内存里可能有读者正在使用不能马上释放。那什么时候能释放必须等所有读者都离开旧数据。这个“离开”的动作就是静止状态quiescent state简称QS。读者什么时候算“离开”内核里定义得很宽包括CPU进入idle状态进程发生上下文切换从内核态返回到用户态线程在内核态被阻塞、让出CPURCU要求每个CPU周期性地经过这些状态之一并把这个信息记录下来。只要所有CPU都经过了QS就意味着旧数据不会再被任何人引用可以安全回收了。从写者发布新指针到所有CPU都报告QS之间的时间称为一个宽限期。2.2 静止状态、宽限期、回调三件套把上面这段翻译成更直白的比喻RCU像是一个图书馆管理系统写者要把书架上的旧书撤下来换新书。撤旧书之前要确认所有在馆读者都已经离开了旧书所在的位置。每个读者走出那个位置时门口的感应器会响一下管理员听到所有位置都响过了才开始动手回收旧书。这里“每个读者走出位置”就是QS从管理员开始等待到所有感应器都响完的时间就是宽限期管理员撤旧书的动作就是回调callback。如果有人一直坐在位置上不动感应器永远不响管理员就只能一直等。等太久就会报警——这就是RCU stall。2.3 宽限期是怎么被推进的RCU内部有个内核线程常见的名字是rcu_gp负责跟踪宽限期的状态。但这个线程不会主动去问每个CPU而是靠CPU自己“汇报”。汇报的途径主要有这么几条调度器tick中断内核周期性地在每个CPU上触发时钟中断在中断处理里检查当前CPU是否经过QS上下文切换进程切换时天然是一个QSidle状态CPU进入空闲循环时也会报告系统调用、异常返回路径从内核态回到用户态时顺手报告一次只要一个CPU还在正常接受时钟中断RCU就能定期看到它的状态。宽限期即便稍长也总能在几十毫秒内结束。问题就出在“隔离核”恰恰把这些汇报途径给堵上了。3. 隔离的核到底让RCU“看不见”了什么3.1 isolcpus不等于CPU在RCU面前消失isolcpus这个内核参数最容易被误解。它的作用是把指定CPU从通用调度器中拿出去普通用户进程默认不会调度到这些核上但它并没有让CPU从系统中消失——RCU照样要把这个核统计到宽限期等待列表里。这就是第一个矛盾点你想要的是“这个核只跑我的采集线程其他一切别来打扰”但RCU要的是“这个核每隔一段时间告诉我它还活着”。两者天然存在冲突。如果只配置isolcpus而不碰nohz_full其实很少触发RCU stall。因为隔离核仍然在接收调度器tick中断tick一到RCU就能确认这个核经过了一次QS。隔离的效果确实打了折扣因为CPU还是会周期性被打断但RCU不会报警。真正引爆问题的是把isolcpus和nohz_full组合起来。3.2 nohz_full让隔离核“静音”了nohz_full2-7的意思是这些核进入自适应tick模式有任务在跑的时候尽可能关闭周期性tick没有任务的时候也关闭tick。目的很纯粹——减少对实时任务的打扰。隔离核2上跑着我的采集线程它进入用户态死循环后几乎没有任何中断、没有系统调用、没有调度切换那么这个核就再也没有机会主动向RCU报告QS了。你可能会问这个线程一直跑不代表它一直抱着旧数据不撒手吧问题是RCU不知道啊。RCU只能通过CPU报告QS来判断读者是否离开如果CPU根本不来报告宽限期就只能无限期等下去。等超过阈值就是RCU stall。这就像管理员要求每个位置的人每隔一分钟按一次按钮报平安结果某个位置的人把手机关机了、人也睡了管理员等半小时收不到信号只能判定“这个位置失联”。3.3 FQS机制也唤不醒“深睡”的隔离核RCU当然有预案。宽限期拖得太久时RCU会启动强迫静止状态检测Force Quiescent StateFQS。它会周期性地向那些没有报告QS的CPU发送中断强制它们回到内核态去检查一下。这相当于管理员拿大喇叭喊“你还活着吗报一下状态”在大多数隔离配置下这个机制是有效的。因为即使你隔离了CPU中断还是能到达CPU被打断后会在中断返回路径上报告QS。真正的麻烦出在更极端的隔离场景把中断也严格绑定了所有外设中断都路由到非隔离核业务线程长时间关闭本地中断使用某些深度隔离方案连IPI都被刻意屏蔽如果隔离核连中断都不进FQS也拿它没办法。宽限期就会无休止地悬停RCU回调全部堆积最终整个内核的回收机制被拖死。这就是“重度隔离引发RCU卡死”的最典型路径。3.4 从30秒到120秒宽限期如何从紧张走向崩溃RCU stall告警里有一个时间阈值内核默认通常是21秒或60秒很多发行版调成120秒。在隔离核不报告QS的情况下时间线大概是这样的0秒写者发布新指针宽限期开始0~60秒RCU等待CPU2报告QS没有等到60秒超过阈值内核触发self-detected stall on CPU告警开始打印CPU状态告警后系统不会自动恢复因为写者还在等宽限期还在继续后续每过一个阈值周期就再刷一次告警我见过有些机器硬撑了半个小时后内核直接抛出blocked for more than 120 seconds的hung task告警紧接着某个业务进程被标记为不可中断整个服务像被点了定身术。所以RCU stall不是“警告一下就算了”它是系统级的慢性病越拖越严重。4. 日志与系统指标如何确认是隔离核引起的RCU超时4.1 从dmesg里读出关键信息RCU stall告警的信息量其实挺大关键是别一慌就跳过。看这行日志rcu_sched self-detected stall on CPU2 (t60005 jiffies g-785 q12345)CPU2被判定为失联的CPU编号先跟你的隔离核列表对一下t60005 jiffies宽限期已经持续了多长时间jiffies换算很简单250Hz内核里1秒等于250个jiffies60005大约就是240秒g-785宽限期编号负数通常是内核日志格式里grace period计数器的有符号表示不用太纠结具体值q12345待处理回调数量如果这个数持续上涨说明宽限期推不动回调只进不出诊断时先看两件事第一CPU字段是不是落在isolcpus/nohz_full的列表里第二q后面数字是否只增不减。两个条件都满足基本可以锁定是隔离配置引发的问题。4.2 用 /proc 和 debugfs 验证隔离核状态光看dmesg还不够最好再确认一下当前系统的真实状态。几个我常用的入口cat /sys/devices/system/cpu/isolated这个文件直接列出当前isolcpus隔离的CPU和命令行参数对一下确认哪些核处于隔离状态。cat /proc/rcu/rcu_preempt看rcu_preempt这一节的qlen和gpnum如果回调数量一直在涨而宽限期号没怎么变说明宽限期确实卡住了。ls /sys/kernel/debug/rcu/debugfs下的RCU节点更细每个CPU的回调列表都能看到。重点观察隔离核上是否有回调堆积以及回调是否被offload到了别的CPU。如果隔离核上根本没有回调堆积说明问题不在回调执行而在QS报告——这个区别对后续定位很重要。4.3 区分隔离引发和负载引发不是所有RCU stall都跟隔离有关。高负载、中断风暴、驱动长时间关抢占同样能拖住宽限期。我给你一个简单的区分方法隔离引发告警的CPU编号正好在nohz_full/isolcpus列表里系统整体负载不高但宽限期就是不走。业务线程长时间在用户态跑没有系统调用没有tick。负载引发告警CPU在普通核上q数量巨大软中断占用高或者某个驱动在内核态长时间自旋。这时重点看top里si和hi比例以及/proc/interrupts里中断分布。我遇到过一个案例某网卡驱动在极端流量下长时间关中断导致rcu_schedstall。那个日志的CPU编号是个普通核跟isolcpus毫无关系排查方向完全不同。所以看到stall别急着改内核参数先确认失联CPU是不是隔离核。5. 配置层面的正确解法隔离业务但别隔离RCU5.1 标准组合nohz_full rcu_nocbs housekeeping既然问题出在隔离核不再上报QS那解决方案就不是“别隔离了”而是让隔离核上不积压RCU回调同时给RCU留一条能确认“它还活着”的通道。最常用的参数组合是isolcpus2-7 nohz_full2-7 rcu_nocbs2-7关键在rcu_nocbs。它把隔离核上的RCU回调offload到专门的内核线程名字通常是rcuop/rcuog上执行这些线程运行在housekeeping CPU上一般是没被隔离的CPU0、CPU1。这样隔离核就不用承担RCU回调的软中断处理业务受干扰更少回调也不会堆在隔离核上。但这里必须说清楚rcu_nocbs只解决“回调放在哪里执行”的问题不解决“QS由谁来报告”的问题。RCU依然需要隔离核本身报告QS。所以nohz_full模式下还得保证隔离核至少有一个能触发QS的途径。5.2 真正把QS问题也解决掉的手段保留tick最简单粗暴的办法是不用nohz_full只保留isolcpus。隔离核仍然会周期性接收tickRCU能正常看到QSstall自然消失。代价是实时任务每个tick还是会被打断一次隔离效果打折。让FQS能生效如果确实想保留nohz_full那么隔离核至少要能接收中断。只要本地中断没有被屏蔽RCU的FQS机制就能通过IPI把隔离核唤醒强制它报告QS。也就是说不要在这个核上把中断完全禁掉也不要把业务线程放在关本地中断的死循环里。有些深度隔离方案会调整中断亲和性把所有外设中断都移到housekeeping核。这通常没问题只要保留CPU间中断IPI的投递路径。要留意的是别把irqaffinity设得太极端导致隔离核连IPI都收不到。增加 rcu_nocb_poll如果业务对延迟极敏感但对功耗不太敏感可以加一个参数rcu_nocb_poll它让rcuop/rcuog线程用轮询方式去检查隔离核的回调队列而不是等待事件唤醒。好处是回调延迟更可控坏处是这些线程会占用更多housekeeping CPU时间功耗和CPU占用会上升。实测下来它对RCU stall的缓解效果有限不能指望靠它解决QS缺失问题。5.3 编译期配置也要注意rcu_nocbs参数需要内核打开CONFIG_RCU_NOCB_CPU编译选项否则传了参数也不生效。检查方式grep RCU_NOCB /boot/config-$(uname -r)如果输出是CONFIG_RCU_NOCB_CPUy说明支持。如果是is not set就得换内核或者重新编译。很多人配了rcu_nocbs没效果第一反应是参数没生效其实是内核根本没编译这个功能。另外nohz_full需要CONFIG_NO_HZ_FULLy检查方法类似。这两个选项在大多数发行版内核里默认是开启的但某些精简内核会把它关掉。5.4 别忘了housekeeping核的承载能力rcu_nocbs把隔离核的回调转移到housekeeping核这意味着CPU0和CPU1的负担会变重。如果housekeeping核本身已经忙得不可开交RCU的内核线程也会被饿死同样会出现另一种形态的告警rcu_preempt kthread starved for 60 seconds这种告警的CPU字段可能不在隔离列表里但根子还是隔离配置导致housekeeping核过载。排查时要把CPU0、CPU1的负载一起看必要时把隔离区间缩小一点给housekeeping留出足够的算力。6. 一次完整排查过程的还原6.1 步骤一锁定失联CPU并核对其隔离配置我在处理开头那台数据采集服务器时第一步不是改参数而是把所有RCU相关告警抓下来统计失联CPU的分布dmesg -T | grep -i rcu.*stall结果所有告警都指向CPU2。接着看启动参数cat /proc/cmdline确认CPU2确实在isolcpus2-7 nohz_full2-7 rcu_nocbs2-7的列表里。这时已经能初步判断问题出在隔离配置和RCU机制之间的冲突而不是单纯的负载问题。6.2 步骤二确认隔离核上的业务行为下一步是弄清CPU2上到底在跑什么。用taskset和perf看一下taskset -pc pid perf stat -e irq_vectors:reschedule_* -C 2 sleep 10确认采集线程牢牢占着CPU2而且CPU2几乎不接收任何中断也没有调度切换。这就是典型的“沉默的核”——它在RCU眼里就像人间蒸发了一样。6.3 步骤三验证回调堆积是否在隔离核上再看debugfs里的回调分布grep -r . /sys/kernel/debug/rcu/ 2/dev/null | grep CPU2如果CPU2上的回调数量持续增长而CPU0/CPU1上的回调不断被处理说明rcu_nocbs的offload其实在工作卡住的只是QS报告。这个线索很重要因为它把问题从“回调没人处理”重定向到“QS没人报告”。6.4 步骤四选择修复路径并验证针对这个案例我当时的修复方案是保留隔离需求但给隔离核留出必要的RCU可见性把nohz_full从隔离核列表中去掉隔离核恢复tick保留isolcpus和rcu_nocbs业务依然不会被普通进程抢占RCU回调也依然被offload中断保持原有路由确保IPI能到达隔离核修改启动参数后重启连续跑48小时dmesg里不再出现任何RCU stall。业务延迟数据也回到了隔离前的水平几乎没受影响。6.5 为什么不能简单去掉isolcpus有人可能会问既然保留tick就能解决RCU问题那把isolcpus去掉不就完了不行。isolcpus解决的问题是通用调度器不会把任意进程跑到隔离核上这个是tick替代不了的。如果只保留nohz_full而不隔离CPU系统其他进程可能随机跑上核2实时业务的确定性就被破坏了。所以正确的心法是业务隔离要保留但RCU的“视线”不能断。让隔离核继续周期性上报QS同时把耗时的回调处理转移到housekeeping核这才是兼顾延迟和稳定性的做法。7. 常见问题速查与我的实操体会7.1 现象、根因、处理速查表场景典型现象根因首选处理isolcpusnohz_full组合dmesg刷rcu_sched self-detected stall on CPUxx在隔离列表里隔离核关闭tick长时间不报告QS去掉nohz_full或保留FQS通路仅配置isolcpus基本不出现RCU stalltick仍在QS能正常上报无需处理rcu_nocbs未生效隔离核回调堆积stall持续内核没开CONFIG_RCU_NOCB_CPU确认编译选项换内核housekeeping核过载rcu_preempt kthread starvedrcuop线程被饿死缩小隔离范围给CPU0/CPU1减负隔离核屏蔽所有中断任意RCU告警都无法唤醒该核FQS中断无法到达中断路由保留IPI别彻底屏蔽高负载驱动导致stall普通核上出现stall中断占用高驱动长时间关抢占检查驱动如KVM网络后端virtio等7.2 我踩过几次坑之后的一些体会第一别在线上环境临时用echo 0 /sys/devices/system/cpu/cpuX/online来“解决”隔离核问题。把隔离核直接下线确实能让RCU不再等它但代价是该核上的业务线程直接被冻结采集任务当场丢数据这在生产环境比RCU stall本身更致命。第二rcu_cpu_stall_timeout这个参数可以调整告警阈值但它只是延后告警不解决问题。我见过有人把阈值调到600秒眼不见心不烦结果系统卡死的时间反而更长最后只能强制重启。告警是帮你定位问题的不是用来压制的。第三排查RCU stall时优先怀疑配置但别只怀疑配置。有一次我花了两个小时对比内核参数最后发现是某个后门驱动加载后把系统的时钟频率改了导致tick异常。看日志时要结合dmesg里驱动加载的顺序别陷入参数对比的惯性里。第四也是我最想说的RCU和CPU隔离本身并不对立它们只是各自对“CPU应该做什么”有不同预期。RCU希望CPU定期表态隔离希望CPU安静干活。理解了这一点再回头看待rcu_nocbs、nohz_full这些参数每一个都在调节这对矛盾的不同侧面。遇到问题先想清楚“RCU到底在等什么”往往比急着改配置更能直达根因。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。