盲目将业务进程设置为 SCHED_FIFO(实时调度)并绑核,若未保留内核线程调度余量,会直接饿死同核的 RCU 回调与 kworker,触发 rcu_sched stall 导致节点假死。解法:恢复 sched_rt_runtime_us 节流,高并发组件应优先使用 CFS 结合 cpuset 绑核,严禁滥用 RT 调度。
故障现场:P99 飙升与节点失联
某次排查高频交易网关的性能问题时,业务侧反馈部分节点的 P99 延迟会突然从 2ms 飙升至 3000ms 以上,甚至出现短暂的连接超时。 登录问题节点(Kernel 5.10.112-11)发现,Load Average 异常飙升至所在 CPU 核数以上,SSH 交互出现明显卡顿,但整体内存和磁盘 IO 并无压力。
查看系统内核日志,直接抓到核心报错:
$ dmesg -T | tail -n 20
[Tue Oct 24 14:12:33] INFO: rcu_sched self-detected stall on CPU
[Tue Oct 24 14:12:33] 12-....c.: (59999 ticks this GP) idle=31a/1/4611686018427387906 softirq=1028321/1028321 fqs=14995
[Tue Oct 24 14:12:33] (t=60000 jiffies g=2431213 q=101345)
[Tue Oct 24 14:12:33] NMI backtrace for cpu 12
...
[Tue Oct 24 14:12:33] RIP: 0010:gateway_poll_loop+0x45/0x120 [gateway_bin]
...
日志非常明确:CPU 12 上发生了 rcu_sched stall(RCU 宽限期超时)。内核检测到 CPU 12 上的 RCU grace period 已经停滞了 60000 个 jiffies(通常是 60 秒),并且抓到的现场 RIP 寄存器停留在业务进程 gateway_bin 的轮询函数 gateway_poll_loop 中。
剥丝抽茧:谁霸占了 CPU?
既然业务进程在 CPU 12 上死循环或高负载,第一步是检查该进程的调度策略和 CPU 亲和性。
抓取目标进程的 PID,查看其调度属性:
$ chrt -p 48291
pid 48291's current scheduling policy: SCHED_FIFO
pid 48291's current scheduling priority: 99
$ taskset -cp 48291
pid 48291's current affinity list: 12
结果一目了然:开发为了追求极致延迟,通过 sched_setscheduler 将该网关的轮询线程设置成了 SCHED_FIFO(实时调度),优先级拉到了最高的 99,并且通过 sched_setaffinity 强行绑定在了 CPU 12 上。
顺手检查一下系统的 RT 调度全局限制:
$ sysctl kernel.sched_rt_runtime_us
kernel.sched_rt_runtime_us = -1
发现 kernel.sched_rt_runtime_us 被改为了 -1(无限制)。这就是彻头彻尾的“自杀式”调优。
为什么 SCHED_FIFO 结合绑核会引发系统假死?
在 Linux 内核的调度子系统中,调度类(Scheduling Class)是有严格层级关系的:Stop > Deadline > Real-Time (RT) > Fair (CFS) > Idle。
SCHED_FIFO 属于 RT 调度类,其优先级碾压普通的 CFS 调度任务。当一个 SCHED_FIFO 线程进入 Runnable 状态时,它会无条件抢占当前 CPU 上的 CFS 线程,并且只有在它主动让出 CPU(如 sleep、IO 阻塞)或被更高优先级的 RT 任务抢占时,才会交出执行权。
在本案中,网关线程是一个 while(1) 的密集轮询(Poll)循环,不包含任何阻塞系统调用。这就导致了以下连锁反应:
-
绝对霸占:该线程在 CPU 12 上以
SCHED_FIFO优先级 99 运行,永远不会主动 yield。 -
内核线程饿死:Linux 内核依赖每 CPU 的
ksoftirqd(处理软中断,如网络包处理)、rcuc(RCU 回调处理)等内核线程来维持系统运转。这些内核线程大部分默认是 CFS 调度。 -
RCU 宽限期停滞:RCU(Read-Copy-Update)机制依赖各 CPU 周期性地报告 quiescent state(静止状态)来结束 Grace Period,进而回收内存。CPU 12 上的
rcuc线程被彻底饿死,无法上报静止状态。 -
全局雪崩:其他 CPU 上的写操作在等待 RCU Grace Period 结束,由于 CPU 12 一直不上报,整个系统的 RCU 回收被阻塞。随着时间推移,系统内存无法回收,其他依赖 RCU 同步的内核路径全部卡死,最终触发
rcu_sched stall,甚至引发 Watchdog 触发 NMI 宕机(如果开启了kernel.unknown_nmi_panic)。
内核原本有一个保护机制:kernel.sched_rt_period_us(默认 1000000,即 1s)和 kernel.sched_rt_runtime_us(默认 950000,即 0.95s)。这意味着在每 1 秒内,RT 任务最多只能运行 0.95 秒,强制留出 50ms 给普通的 CFS 任务和内核线程运行。
但排查过程中发现,有人为了“避免这 50ms 的毛刺”,将 sched_rt_runtime_us 设置成了 -1,彻底关闭了 RT 节流保护,直接把系统推向了深渊。
破局与架构优化:防御性调度
针对高频网关/DPDK/Redis等对延迟极度敏感的场景,正确的调优姿势绝不是盲目开 SCHED_FIFO,而是通过“隔离与独占”在 CFS 下实现类似 RT 的效果。
1. 紧急止血:恢复 RT 节流
立即将 sched_rt_runtime_us 恢复为默认值,给内核线程留出活路。
sysctl -w kernel.sched_rt_runtime_us=950000
注:即使业务出现少量 P99 毛刺,也比整个 Node 假死要好。
2. 长期重构:isolcpus + NOHZ_FULL + CFS
废弃代码中的 SCHED_FIFO 设置,改回默认的 SCHED_OTHER (CFS)。
利用内核参数将特定 CPU 从调度器和中断中隔离出来,让业务线程在干净的 CPU 上运行。
在 Grub 内核启动参数中添加:
isolcpus=12 nohz_full=12 rcu_nocbs=12
-
isolcpus=12:将 CPU 12 从内核调度器的普通负载均衡域中剥离,CFS 不会主动将其他进程调度到该核。 -
nohz_full=12:当 CPU 12 上只有一个 Runnable 进程时,关闭 Tick 时钟中断,消除 1000Hz/250Hz 的调度时钟抖动。 -
rcu_nocbs=12:将 CPU 12 的 RCU 回调处理卸载到其他 CPU(如 CPU 0)上执行,防止 RCU 干扰业务线程。
随后,通过 taskset 或 cgroup cpuset 将业务进程绑定到 CPU 12。此时,业务进程虽然是 CFS 调度,但在 CPU 12 上没有竞争者,既能实现接近 100% 的独占运行,又不会引发 RCU Stall。
常见问题
Q: 如何快速排查系统中是否存在滥用 SCHED_FIFO 的进程?
使用 ps 命令可以快速拉取调度策略和优先级。FF 代表 FIFO,RR 代表 Round Robin。
ps -eo pid,tid,class,rtprio,ni,pri,psr,pcpu,stat,comm --sort=-rtprio | grep -v TS
Q: SCHED_RR (Round Robin) 能否解决这个饿死问题?
不能。SCHED_RR 同样属于 RT 调度类,优先级依然高于 CFS。它只是在同等优先级的多个 RT 任务之间采用时间片轮转。如果同核上只有一个跑满 CPU 的 SCHED_RR 任务,它依然会饿死普通的 CFS 内核线程。
Q: 容器环境 (K8s) 中如何实现类似的安全绑核?
在 K8s 中,通过配置 kubelet 的 --cpu-manager-policy=static,并为 Pod 申请整数型的 CPU 资源(如 requests.cpu: 2, limits.cpu: 2),K8s 会自动为其分配独占的物理 CPU(借助 cpuset cgroup)。不要在容器内自行调用 sched_setscheduler 修改为 RT 调度,容易被 cgroup 限制策略拦截或引发宿主机雪崩。
Q: 为什么有时候设置了 SCHED_FIFO 并没有引发机器死机?
因为业务逻辑中存在 epoll_wait、sleep、网络 IO 等阻塞调用。当 RT 进程阻塞等待时,它会主动让出 CPU,此时 ksoftirqd 等内核线程就能趁机运行。只有在纯 CPU 密集型的 while(1) 死循环或高频极密轮询下,RT 任务才会真正引发系统级灾难。