watchdog: BUG: soft lockup - CPU#3 stuck for 22s! 完整排查:从寄存器 dump 到自旋锁争用根因
你的服务器 dmesg 里反复冒出这一行:
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/3:1:1234]
机器要么卡住几十秒、SSH 敲不进去,要么直接重启。你去搜中文答案,十篇有八篇叫你 sysctl kernel.watchdog_thresh=30 把阈值调大。调完报错确实少了,但下一次它不打招呼,直接变成 Hard LOCKUP panic。
这篇给出能一路定位到具体哪把自旋锁的排查路径:先讲清 22s 这个数字怎么来的、方括号里的进程名为什么常常不是元凶,再给四个回合的取证命令、内核看门狗的源码机制、5 个厂商 KB 的真实根因,最后一份自检脚本。核心结论先给:调大阈值只是把检测线往后挪,不解决任何根因。
TL;DR
- 软锁(soft lockup)的官方定义是:内核态循环超过 20 秒没有让出 CPU 给其他任务跑。定时器中断还在,所以能上报;连中断都不响应了叫硬锁(hard lockup)。
watchdog_thresh默认 10 秒,软锁阈值 = 2 × 10 = 20 秒;看门狗 hrtimer 周期是2*thresh/5= 4 秒。所以日志里常见 22s、23s、24s,而不是整数 20s。- 方括号里的
[kworker/3:1:1234]是当时占住该 CPU 的任务 ,不一定是元凶;真正的线索是紧跟其后的RIP:那一行。 RIP落在native_queued_spin_lock_slowpath或_raw_spin_lock*上,基本可判定为自旋锁争用,这是生产环境里占比最高的一类根因。- 排查顺序:先
sysrq-l抓所有 CPU 栈,再perf top -a -g找热点函数,最后区分是自旋锁、中断风暴还是内存回收路径。
一、先分清:soft lockup 和 hard lockup 不是一回事
分不清这两个,后面全白忙。它们是两套检测机制、两种日志、两种危险程度。
| 项目 | soft lockup(软锁) | hard lockup(硬锁) |
|---|---|---|
| 官方定义 | 内核态循环超过 20 秒,不让其他任务跑 | 内核态循环数秒,连中断都没机会跑 |
| 还能响应什么 | 定时器中断仍工作,能打印日志 | 中断不响应 |
| 检测手段 | 看门狗 kthread 时间戳(hrtimer 回调) | NMI / PMU 溢出回调 |
| 默认阈值 | 20 秒(2 × watchdog_thresh) | 10 秒(1 × watchdog_thresh) |
| 日志样子 | watchdog: BUG: soft lockup - CPU#x stuck for 22s! |
watchdog: BUG: hard lockup - CPU#x / Hard LOCKUP panic |
| 危险程度 | 通常能自愈,但可能是硬锁前兆 | 基本必 panic,需 kdump 取证 |
关键判断:看到 soft lockup,先别急着庆祝「只是软的」 。多个真实案例里,软锁和 RCU stall 并发出现,最终收敛成 Kernel panic - not syncing: Hard LOCKUP。软锁是警报,不是结论。
二、先看懂日志字段:22s 是怎么算出来的、谁才是元凶
一条完整的软锁日志信息量很大,逐字段拆开看:
watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [kworker/3:1:1234]
RIP: 0010:native_queued_spin_lock_slowpath+0x122/0x200
Call Trace:
_raw_spin_lock
cfs_percpt_lock [libcfs]
LNetMDUnlink [lnet]
Modules linked in: ...drbd(OE)...
| 字段 | 含义 | 怎么用 |
|---|---|---|
watchdog: |
固定前缀,来自 pr_fmt 宏(kernel/watchdog.c 第 13 行) |
用它过滤日志 |
CPU#3 |
发生软锁的 CPU 编号 | 与后面 CPU: 3 寄存器行一致 |
stuck for 22s |
距上次成功调度的秒数,约等于 2 × watchdog_thresh | 数值大小反映卡了多久,不是根因 |
[kworker/3:1:1234] |
comm:pid,当时占住该 CPU 的任务 |
只是受害者,不一定是元凶 |
RIP: |
CPU 卡住时执行的指令地址 | 最关键 ,native_queued_spin_lock_slowpath 即自旋锁争用 |
Call Trace: |
内核栈 | 从顶往下持续不变的那一层最可能是元凶 |
Modules linked in: |
已加载模块,(OE) 表示 out-of-tree |
快速锁定第三方模块 |
关于「谁才是元凶」,内核 RCU 文档给了一条非常实用的经验:多次 stall 之间,内核栈里从栈顶往下保持同一层不变的那个函数 ,最接近真正的卡点。单个 [comm:pid] 会骗你,跨多次抓取的稳定栈层不会。
三、排查回合制:四个回合从报警到定位
第一回合:确认检测器状态和阈值(但别停在这里)
cat /proc/sys/kernel/watchdog # 1 = 检测开启
cat /proc/sys/kernel/watchdog_thresh # 默认 10,软锁阈值 = 2 倍
cat /proc/sys/kernel/soft_watchdog # 软锁检测开关
cat /proc/sys/kernel/nmi_watchdog # 硬锁(NMI)检测开关
这一步只能回答「检测是不是被人关过、阈值是不是被人调大过」。它回答不了根因,很多教程却把它当成了唯一答案。
第二回合:卡住的那一刻抓现场
这是最值钱的一步,必须在卡住时立刻执行,事后补不回来:
echo l > /proc/sysrq-trigger # 打印所有活动 CPU 的内核栈,一次拿全
echo w > /proc/sysrq-trigger # 打印所有 D 状态(不可中断睡眠)任务
echo d > /proc/sysrq-trigger # 打印所有被持有的锁,定位锁争用
cat /proc/<pid>/stack # 单个目标内核线程的栈(注意:running 任务可能为空)
sysrq-l 拿到的多份栈,横向对比就能看出「大家都堵在同一个函数上」,这是自旋锁争用最典型的特征。
第三回合:找热点函数
perf top -a -g # 实时看哪个内核函数在烧 CPU
perf record -a -g -F 999 -- sleep 30
perf report
软锁场景下,perf top 常年排第一的就是 native_queued_spin_lock_slowpath。一旦命中它,方向就从「谁在跑」转成「大家在抢哪把锁」。
第四回合:区分自旋锁 / 中断风暴 / 内存回收
cat /proc/interrupts # 看是否有某个 IRQ 计数疯涨
dmesg -T | grep -iE "soft lockup|rcu.*stall|hung_task|watchdog"
cat /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress
如果 soft lockup、rcu_sched detected stalls、task blocked for more than 120 seconds 三者同时出现,基本可以判定是同一条链路的多个探针在报警,而不是三个独立故障。这一点中文资料里很少讲清。
四、根因:内核看门狗到底是怎么检测的(源码级)
看门狗实现在 kernel/watchdog.c,核心几行如下:
// kernel/watchdog.c
#define pr_fmt(fmt) "watchdog: " fmt // 第 13 行,日志前缀来源
int __read_mostly watchdog_thresh = 10; // 第 50 行,默认阈值
工作机制分三层:
- 每个 CPU 起一个看门狗 kthread(实时 FIFO 优先级),配上 hrtimer,周期是
2*watchdog_thresh/5,默认 4 秒。 - 每次 hrtimer 回调
watchdog_timer_fn()更新该 CPU 的时间戳,同时给硬锁检测踢心跳。 - 如果某个 CPU 的时间戳超过
2*watchdog_thresh没被更新,softlockup_fn()就打印stuck for %us!。任务在途中的喂狗动作走touch_softlockup_watchdog()。
这解释了 22s 这个数字:阈值线是 20 秒,检测周期 4 秒,所以上报点落在 20 到 24 秒之间,日志里写成 22s、23s 都很正常。
硬锁检测的位置这几年变过,引用时要注意版本:老内核和多数生产内核(RHEL/CentOS 系)在 kernel/watchdog_hld.c,走 Perf/NMI 溢出回调 watchdog_overflow_callback();上游新版本已把核心合并回 kernel/watchdog.c,并拆出 kernel/watchdog_buddy.c。写工单或搜文档时,按你的内核版本对号入座,别拿新树的文件名去套老内核。
常见触发路径,按生产环境出现频率排:
| 触发路径 | 特征 | 典型场景 |
|---|---|---|
| 自旋锁持有过长 / 争用 | RIP 在 native_queued_spin_lock_slowpath,多 CPU 同栈 |
文件系统、网络、DRBD/Ceph 等模块 |
| 关抢占下的长循环 | 内核态大循环没 cond_resched() |
第三方 out-of-tree 模块 |
| 中断风暴 / 软中断过长 | /proc/interrupts 某 IRQ 疯涨,[ksoftirqd/x] 上榜 |
网卡、NVMe、驱动异常 |
| 内存回收路径 | 栈里有 lru_gen_*、shrink_many、khugepaged |
大内存 + THP + MGLRU |
| 虚拟化超分 | 单 vCPU 小机器,低负载也报 | VPS、超卖云主机 |
五、五个真实根因(来自厂商 KB 与社区)
下面五个案例都有原始日志和内核版本,可作为对照排查的模板。
| 案例 | 内核版本 | 日志特征(RIP / 栈) | 根因 | 出处 |
|---|---|---|---|---|
| Lustre/LNet 自旋锁 | RHEL 7.6(3.10.0) | native_queued_spin_lock_slowpath 栈含 cfs_percpt_lock[libcfs] |
多 CPU 争同一把 libcfs 自旋锁,软锁与 RCU stall 并发,最终 Hard LOCKUP | Red Hat 5618071 |
| DRBD + 内核回归 | RHEL 8.7(4.18.0) | dequeue_work_batch[drbd],rcu_sched kthread starved for 59998 jiffies |
升级到该内核版本后回归叠加 DRBD 自旋锁争用,RCU 宽限线程被饿死 | Red Hat 7010399 |
| lru_gen 回收 + THP | RHEL 9.5(5.14.0) | shrink_many -> lru_gen_shrink_node,khugepaged stuck 26s |
Multi-Gen LRU 回收自旋锁争用,THP 分配放大压力 | Red Hat 7077348 |
| Ceph delayed cap | RHEL 8.3(4.18.0) | _raw_spin_lock,栈含 ceph_check_delayed_caps |
Ceph delayed cap 检查中 igrab 锁争用 |
Red Hat 6967188 |
| 单 vCPU VPS | Rocky 8.9(4.18.0) | new_slab,栈含 tcp_make_synack -> __alloc_skb |
未确认内核 bug,倾向 CPU 饥饿/超分(悬案,恰好说明不是每次都能定性) | Rocky 论坛 |
把五个案例横向排开,能看到一条规律:软锁几乎从不孤立发生。四个有定论的案例里,自旋锁争用是共同内核,RCU stall 与 hung task 是同场的伴生报警。
六、解决方案:分清「止血」和「治本」
| 参数 | 作用 | 属于哪一类 | 怎么用 |
|---|---|---|---|
kernel.softlockup_panic=1 |
软锁即 panic,触发 kdump | 取证 | 生产建议开,把现场留在 vmcore 里 |
kernel.hardlockup_panic=1 |
硬锁即 panic | 取证 | 同上 |
kernel.watchdog_thresh=N |
调检测阈值(0 到 60,设 0 直接关闭检测) | 只为掩盖 | 不建议当解法 |
kernel.watchdog_cpumask |
限定监控哪些 CPU | 调优 | nohz_full 隔离核场景 |
nosoftlockup / nowatchdog |
启动参数,关日志 / 关看门狗 | 只为掩盖 | 只在压测环境短暂使用 |
rcu_cpu_stall_suppress |
静音 RCU stall 报警 | 只为掩盖 | 同上,不解决 stall |
为什么调大 watchdog_thresh 是掩盖而不是修复:它只挪动检测线,不改变「某段内核代码长时间不让出 CPU」这个事实。调大之后有两笔代价:一是那条唯一能定位问题的栈会迟到甚至错过,你失去了取证窗口;二是如果循环是无界的,它终究会 hang 或 panic,只是从「可见的软锁」拖成「不可见的硬锁」。
对照一下现成的反面教材:Autodesk 的官方 KB 直接把 kernel.watchdog_thresh=30 写成解决方案;Proxmox 社区给的是加 nosoftlockup 启动参数;SUSE KB 建议延长看门狗周期。这些都是止血式 workaround。而 Red Hat 在虚机多 CPU 同时软锁的案例里,给的治本建议是「降低 hypervisor 负载、消除 CPU 超分」。
止血和治本要分清标注 :先把 softlockup_panic 打开保住现场,再去动根因;不要用调阈值把报警按住,然后当它没发生过。
七、如何自检你是否中招
一条命令扫出所有相关报警,并统计它们是否成组出现:
dmesg -T | grep -icE "soft lockup" # 软锁次数
dmesg -T | grep -icE "rcu.*stall" # RCU stall 次数
dmesg -T | grep -icE "blocked for more than" # hung task 次数
dmesg -T | grep -A 25 "soft lockup" | tail -30 # 打印命中行及其后 25 行
这里特意没用「遇到空行就停」的 awk 写法:dmesg -T 每一行都带时间戳前缀,内核日志里根本不存在真正的空行,那种写法会一路打印到日志末尾,把现场淹掉。用 grep -A 固定打印命中行及其后 25 行,才拿得到干净、可直接贴进工单的现场。
判断标准:只要三项计数同时大于 0,就不是偶发抖动,按本文第三节四个回合完整走一遍。如果只有软锁、RIP 落在 native_queued_spin_lock_slowpath,优先怀疑自旋锁争用,并去比对 Modules linked in 里的第三方模块。
八、四个常见误区
排查软锁时,下面四个坑几乎人人踩过一遍。列出来对照,能省下几小时。
| 误区 | 为什么错 | 正确做法 |
|---|---|---|
把 [comm:pid] 当元凶 |
它只是当时恰好占住该 CPU 的受害者,真正卡住的可能是别的 CPU 在抢锁 | 看 RIP:,并对比多次抓取里保持不变的栈层 |
| 拿「调大 watchdog_thresh」当修复 | 只把检测线往后挪,根因仍在,最终会从软锁演变成 Hard LOCKUP | 先开 softlockup_panic 保住 vmcore,再动根因 |
| 只看单次日志就下结论 | 单次栈拿到的可能正是受害者 | 用 sysrq-l 一次抓全部 CPU 栈,横向比对公共层 |
| 把 RCU stall 当成独立故障另开工单 | 它和软锁常常同根并发,分开查等于把一条链路拆成三份 | 三者同现时按一条链路查,重点找公共的自旋锁 |
判断自己有没有踩第一个坑,方法很简单:看那条 [comm:pid] 换不换 。同一个 [ksoftirqd/0] 反复出现,说明卡点确实在软中断;每次都是不同进程,却都堵在同一个 RIP,那元凶是那把锁,不是进程。
九、启示
- 日志里
[comm:pid]是被卡住的受害者,RIP:才是指认元凶的证据。 拿前者定位,十次有八次找错人。 - 调
watchdog_thresh是关警报,不是修故障。 判断标准很简单:改完之后「内核还会不会在同一个函数上长时间不让出 CPU」,会,就是掩盖。 - 软锁、RCU stall、hung task 同场出现时,按一条链路查,不要开三个工单。 它们大概率是同一把锁的不同探针。
- out-of-tree 模块要单独怀疑。
Modules linked in:里的(OE)标记是强信号,升级内核后新出现的软锁尤其要往这里查。 - 单 vCPU 小机器上反复软锁,先怀疑超分而不是内核 bug。 案例五就是反面教材:同样的日志,根因在宿主机。
原始出处
- 内核文档:Lockup detection、RCU stall 警告、SysRq 键
- 源码:
kernel/watchdog.c - 厂商 KB:Red Hat 5618071、7010399、7077348、6967188、1503333
- 社区案例:Rocky Linux 论坛、ServerFault
- 增强检测实践:阿里云 Alinux soft lockup 检测
本文首发于 CSDN 专栏《运维漏洞指南》。