某次接手排查一个核心低延迟网关的间歇性“假死”问题。现象极为惨烈:物理节点突然与控制面失联,监控指标白屏,SSH 无法建立连接(TCP 握手超时),但基础的 ICMP Ping 依然能通。最终强制重启并在终端外接主板收集到了崩溃现场。
结论先行:业务开发为了追求所谓的“极致低延迟”,绕过 SRE 压测体系,在 systemd service 中私自将网关进程的 CPU 调度策略设置为 SCHED_FIFO(实时调度)且优先级拉满到 99,甚至顺手把内核保护机制 kernel.sched_rt_runtime_us 改成了 -1。这种鲁莽的操作导致用户态死循环轮询(Busy-Polling)线程霸占了 CPU,直接饿死内核 RCU (Read-Copy Update) 宽限期线程和其他系统守护进程,最终触发内核 Watchdog 机制,导致节点引发 Hard Lockup 彻底瘫痪。
正确的低延迟调优绝不是靠暴力抢占调度权,而是通过 isolcpus、NO_HZ_FULL 以及网卡中断亲和性绑定来实现 CPU 的“纯净隔离”。
事故现场:SSH 进不去的诡异假死
排查过程中,通过带外管理(IPMI)查看崩溃前的系统终端,满屏飘着内核报错日志。提取出的核心堆栈如下:
watchdog: BUG: soft lockup - CPU#4 stuck for 22s! [gateway-worker:14322]
...
rcu: INFO: rcu_sched self-detected stall on CPU
rcu: 4-....c1e.. dps: 125199 GPs: 43232121
rcu: (t=60000 jiffies g=123321 q=123)
Task dump for CPU 4:
task:gateway-worker state:R running task stack:0 pid:14322 ppid:1
Call Trace:
<IRQ>
rcu_dump_cpu_stacks+0xdf/0x110
rcu_check_callbacks+0x7b5/0x8e0
update_process_times+0x2c/0x50
tick_sched_timer+0x4d/0x90
__hrtimer_run_queues+0x10b/0x290
hrtimer_interrupt+0xf4/0x210
smp_apic_timer_interrupt+0x5e/0x120
...
从日志看,CPU 4 陷入了长达 22 秒的 Soft Lockup,随后 RCU 机制检测到了 CPU 停滞(Stall)。当前在这个 CPU 上运行的进程正是业务的核心网关程序 gateway-worker。
为什么 SSH 连不上但 Ping 能通? 因为 ICMP 包的响应通常在网卡软中断(SoftIRQ)上下文中处理,而 SSHD 是运行在完全公平调度器(CFS)下的普通用户态进程。如果 CPU 被比 CFS 优先级更高的任务死死咬住,普通进程根本分不到时间片,连 shell 提示符都弹不出来。
扒开配置的底裤:夺命的优先级
挂载崩溃节点的系统盘后,我直接翻看了业务侧提交的 systemd 配置文件,发现了令人窒息的代码片段:
[Service]
ExecStart=/opt/gateway/bin/gateway-worker
# 业务开发自行添加的“性能优化”配置
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=99
不仅如此,他们在服务的 pre-start 脚本中,还加入了这样一行 sysctl 注入:
sysctl -w kernel.sched_rt_runtime_us=-1
这套组合拳的逻辑漏洞在于,他们完全没有理解 Linux 内核调度类的层次结构。
Linux 调度器分为不同的调度类,优先级从高到低依次为:
Stop > Deadline > Real-Time (RT) > Completely Fair Scheduler (CFS) > Idle。
业务代码使用的 SCHED_FIFO 属于 RT 调度类。在 SCHED_FIFO 策略下,一旦线程拿到 CPU,除非它主动让出(阻塞于 I/O、调用 sched_yield),或者被更高优先级的 RT 线程抢占,否则它将永远运行下去。
而该网关程序为了降低网络处理延迟,采用了类似于 DPDK 的 Busy-Polling 模式(死循环空跑轮询队列)。
正常情况下,Linux 为了防止这种流氓进程锁死系统,内核有一个保护参数 /proc/sys/kernel/sched_rt_runtime_us,默认值为 950000(即每秒钟最多允许 RT 进程运行 950ms,必须留 50ms 给非 RT 进程如 CFS 调度类)。
但这位开发者不知从哪搜来的“黑魔法”,把这个值改成了 -1(完全关闭限制)。
结果是灾难性的:
优先级 99 的死循环线程独占了 CPU。内核的 RCU 宽限期线程(rcu_sched)、迁移线程(migration)、甚至是负责清理内存的内核工作队列,全部被饿死。RCU 无法推进,系统内部锁积压,最终 Watchdog 狗咬死,节点硬重启。
真正的防御性调优:如何正确榨干 CPU 性能?
追求低延迟,不应该在调度器上玩火,而是要通过CPU 隔离与亲和性绑定,把 CFS 的干扰降到最低。
正确的改造方案如下:
1. 隔离 CPU,避免系统进程干扰 在 Grub 内核启动参数中,将部分核心(如物理核 4-15)完全隔离开来,不参与普通进程的 CFS 调度,并开启无滴答模式(减少时钟中断):
GRUB_CMDLINE_LINUX="... isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15"
这样,这几个核心将被彻底“净化”,系统默认进程不会跑在上面。
2. 使用 cset 或 taskset 绑定业务进程
将业务进程锁定在这些被隔离的核心上。在 systemd 中,使用 CPUAffinity 替代愚蠢的 CPUSchedulingPolicy:
[Service]
ExecStart=/opt/gateway/bin/gateway-worker
CPUAffinity=4-15
# 确保不要使用 fifo 策略,保持默认的 other (CFS) 即可
3. 剥离网卡中断
默认情况下,irqbalance 服务会在所有 CPU 上随机分配网卡硬中断。为了防止网卡中断打断我们的轮询线程,需要修改 irqbalance 配置,或者手动设置网卡的 SMP affinity,将中断绑定到非隔离的 CPU(如 0-3)上。
通过这套“隔离+绑定”的方案,业务进程在独占的 CPU 核心上运行,既能享受近似 100% 的时间片(无上下文切换抖动),又不会影响内核在其他核心上处理基础系统任务,这才是真正企业级高可用的落地做法。
排查清单与同类问题速查
-
RCU Stall / Soft Lockup 初判:
dmesg如果频繁出现rcu_sched self-detected stall或BUG: soft lockup,首查占用该 CPU 的进程是否进入了死循环,其次查是否滥用了实时调度优先级。 -
实时调度策略审查:使用
chrt -m查看系统支持的优先级范围,使用ps -eo pid,ni,rtprio,psr,comm,policy | grep FIFO审查生产环境是否存在未经审批的SCHED_FIFO或SCHED_RR进程。 -
RT 防御机制检查:绝不要在生产环境将
kernel.sched_rt_runtime_us设为 -1。如果确需微调,请保留至少 50000(50ms)的余量给系统进程。 -
低延迟优化正规军:对 CPU 密集型或轮询型低延迟业务,标准答案是
isolcpus隔离 +taskset亲和性绑定 +nohz_full关闭时钟滴答,切勿试图通过篡改全局调度策略来插队。 -
假死现象倒推:如果 SSH 无法登录但 Ping 延迟极低且稳定,说明网络中断层正常但用户态调度已瘫痪,重点排查 CPU 抢占和 OOM 导致的 fork 拒绝。