香港服务器运行CentOS 8时,如何通过优化内核调度类(SCHED_FIFO/SCHED_RR)提升实时应用性能?

我坐在将军澳的 IDC 里,对着一台刚上架两周的 1U 机器发呆。白天还跑得有模有样的实时风控服务,晚高峰却偶发 8~15 ms 的抖动,把下游撮合链路吓出一身冷汗。网络、磁盘都看过了,只有 CPU 调度还没“下手”。于是我决定把这台 CentOS 8 的内核调度拽回到我能完全掌控的 SCHED_FIFO / SCHED_RR 上——那一夜的记录,就是这篇文章。
我的环境 & 目标
硬件与系统
| 项 | 规格 |
|---|---|
| 机型 | Dell PowerEdge R650 (1U) |
| CPU | 2 x Intel Xeon Gold 6330(28C/56T,2.0–3.1GHz,AVX-512),SMT 开启 |
| 内存 | 256 GB DDR4-3200(NUMA 2 节点) |
| 磁盘 | 2 x 1.92TB NVMe(RAID1,OS 盘) |
| 网卡 | 2 x Intel X710 10GbE(启用 RSS / 多队列) |
| OS | CentOS 8.5(kernel 4.18),systemd v239,cgroup v1(保守选择) |
| 时钟 | chrony 同步(局域网 PTP 源,<50 μs 偏差) |
应用画像 & 目标
实时风控服务(C++),p99 处理延迟目标 < 1 ms,抖动(p99.9)< 3 ms
高并发但单请求短计算链,GC 不敏感(无 JVM),瓶颈在 CPU 抢占和中断干扰
为什么是 SCHED_FIFO / SCHED_RR?
Linux 实时调度类优先于普通 CFS(完全公平调度器):
SCHED_FIFO:固定优先级,不轮转,除非主动让出(sched_yield)或被更高优先级任务抢占。
SCHED_RR:固定优先级,时间片轮转(默认 100ms 左右,可调 sched_rr_timeslice_ms)。
优先级范围 1–99,数字越大优先级越高;同级由 FIFO 或 RR 决定次序。
CentOS 8 默认 RT 节流:kernel.sched_rt_runtime_us=950000、kernel.sched_rt_period_us=1000000,防止 RT 任务饿死系统。对我们这种“短平快”的 RT 计算来说,适当放宽有帮助,但要谨慎。
基线测量(先量再动)
在动刀之前,我用 cyclictest 和应用自带的探针先做一轮“地形测绘”。
# 关闭节能影响,先上 tuned 的低延迟策略
tuned-adm profile latency-performance
# 基线延迟(NUMA node0,绑定3个核观测)
taskset -c 2-4 cyclictest -m -Sp99 -i 100 -l 200000 -q
基线(未调度优化)结果摘要
| 指标 | 值 |
|---|---|
| cyclictest max | 8,743 μs |
| cyclictest p99 | 2,910 μs |
| 应用 p99 | 2.4 ms |
| 应用 p99.9 | 6.8 ms |
结论:波峰在几毫秒级,CPU 抢占 + IRQ 干扰嫌疑最大。
总体思路(我那晚的作战图)
- 隔离关键核:预留一组 CPU 专供实时线程,最小化 CFS 干扰与软中断。
- IRQ 定向:把 NIC 等中断定向到“杂务核”,不要碰到 RT 核。
- RT 调度上线:用 systemd、chrt 或应用内 pthread_setschedparam 设置 SCHED_FIFO / SCHED_RR。
- 放宽但不失控:适调 RT 节流、RLIMIT,确保可用性。
- 可观测:持续采集 ps, perf, ftrace、应用探针,确认收益与副作用。
步骤 1:隔离 CPU(内核引导 & cgroup 双保险)
1)GRUB 隔离内核参数
我选择 node0 的 6 个核做 RT 专用(示例:CPU 2–7),并把 RCU 回调也迁出:
编辑 /etc/default/grub 的 GRUB_CMDLINE_LINUX:
GRUB_CMDLINE_LINUX="... isolcpus=managed,2-7 nohz_full=2-7 rcu_nocbs=2-7 intel_pstate=disable idle=poll"
说明
- isolcpus=managed 允许 systemd/cgroup 再分配,利于动态管理。
- nohz_full 降低时钟中断干扰。
- rcu_nocbs 将 RCU 回调迁出这些核。
- idle=poll 牺牲功耗换低延迟(可按需取消);intel_pstate=disable + tuned 会把 governor 拉到 performance。
更新 GRUB(BIOS/UEFI 按需二选一):
grub2-mkconfig -o /boot/grub2/grub.cfg
# 或
grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
reboot
2)systemd 限制“非 RT 服务”不要用这些核
避免后台服务“误入 RT 核”。我对默认 slice 限制:
# 限制 system.slice 只在非 RT 核(例如 0-1,8-55)运行
systemctl set-property system.slice AllowedCPUs=0-1,8-55
systemctl set-property user.slice AllowedCPUs=0-1,8-55
之后专门给实时服务的 unit 再放开到 2–7(见下)。
步骤 2:中断与软中断“分流”
1)关闭 irqbalance(或定向例外)
我那晚直接停了它,手动钉扎更可控:
systemctl stop irqbalance && systemctl disable irqbalance
2)把 NIC 中断绑到“杂务核”
查看队列与 IRQ 号:
cat /proc/interrupts | egrep -i "x710|ixgbe|i40e|ena|mlx"
为每个 IRQ 设置 smp_affinity_list(示例绑到 40–55):
echo 40-55 | tee /proc/irq/<IRQ>/smp_affinity_list
3)软中断核
把 ksoftirqd/* 保持在非 RT 核即可(通常随 IRQ 走)。确认:
ps -eLo pid,cls,rtprio,cmd,psr | egrep "ksoftirqd|irq/"
步骤 3:用 systemd 让服务跑在 SCHED_FIFO / SCHED_RR
我给实时服务写了一个独立 unit(以 FIFO 为例):
/etc/systemd/system/risk-rt.service
[Unit]
Description=Risk RT Engine
After=network.target
[Service]
Type=simple
ExecStart=/opt/risk/bin/engine --config /etc/risk/config.yaml
# CPU 亲和与 NUMA
CPUAffinity=2 3 4 5 6 7
NUMAPolicy=preferred
NUMAMask=0
# 实时调度
CPUSchedulingPolicy=fifo # 或 rr
CPUSchedulingPriority=90 # 1-99,越大越高
LimitRTPRIO=95 # 进程可申请的最大 RT prio
LimitRTTIME=infinity # 或 950000 (微秒),与内核节流配合
# 内存/锁
LimitMEMLOCK=infinity
MemoryDenyWriteExecute=no
# 其他
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
启动并校验:
systemctl daemon-reload
systemctl enable --now risk-rt
ps -eLo pid,cls,rtprio,cmd,psr | grep risk
# CLS 列应为 FF(FIFO)或 RR,RTPRIO=90
优先级规划建议(我现场用的是这套)
| 角色 | 策略 | 优先级 |
|---|---|---|
| 硬实时工作线程(关键计算) | SCHED_FIFO |
90–95 |
| IO 提交/回调 | SCHED_RR |
70–80 |
| 日志/监控处理 | CFS | N/A |
| 运维守护/探针 | CFS | N/A |
步骤 4:应用内显式设置(C++ 示例)
如果你想更细粒度地按线程设置:
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <stdexcept>
void pin_cpu(int cpu) {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (pthread_setaffinity_np(pthread_self(), sizeof(set), &set) != 0)
throw std::runtime_error("affinity fail");
}
void set_rt_fifo(int prio) {
sched_param sp{};
sp.sched_priority = prio; // 1-99
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0)
throw std::runtime_error("setsched fail (need CAP_SYS_NICE)");
}
int main() {
pin_cpu(3);
set_rt_fifo(92);
// ... do hard-realtime work ...
for(;;) { /* 你的主循环 */ }
}
非 root 进程要能升 RT 需要能力位或 PAM 限额:
# 赋予二进制 CAP_SYS_NICE
setcap 'cap_sys_nice+ep' /opt/risk/bin/engine
# 或者:/etc/security/limits.d/90-rt.conf
# riskuser - rtprio 95
# riskuser - memlock unlimited
步骤 5:RT 节流(小心使用)
内核默认不让 RT 任务霸占 CPU:每周期(默认 1s)里,RT 最多跑 95% 时间。
我把它放宽到 不节流(配合预留核,且有守护监控):
# 临时
sysctl -w kernel.sched_rt_runtime_us=-1
# 永久:/etc/sysctl.d/90-rt.conf
kernel.sched_rt_period_us=1000000
kernel.sched_rt_runtime_us=-1
警告:只有在隔离核且实时线程可控的前提下这么做,否则可能把系统“吸死”。
步骤 6:RR 还是 FIFO?我的选型经验
核心计算:SCHED_FIFO 更稳,避免同优先级线程间轮转带来的额外调度点。
需要公平共享的 RT 辅助线程:SCHED_RR,并把 sched_rr_timeslice_ms 调小(例如 10ms)。
修改 RR 时间片(全局):
echo 10 > /proc/sys/kernel/sched_rr_timeslice_ms
步骤 7:验证与可观测(别只看一眼)
1)进程/线程状态
ps -eLo pid,tid,psr,cls,rtprio,pri,nice,cmd | egrep "risk|FF|RR" | column -t
chrt -p <PID>
2)性能/中断画像
# 查看上下文切换/软中断
perf stat -a -- sleep 10
# ftrace 看调度延迟热点
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on; sleep 5; echo 0 > .../tracing_on
cat /sys/kernel/debug/tracing/trace | head -200
3)再跑 cyclictest(绑到 RT 核)
taskset -c 2-4 cyclictest -m -Sp99 -i 100 -l 300000 -q
优化后结果(实测)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| cyclictest max | 8,743 μs | 1,092 μs |
| cyclictest p99 | 2,910 μs | 480 μs |
| 应用 p99 | 2.4 ms | 0.86 ms |
| 应用 p99.9 | 6.8 ms | 1.9 ms |
注:数值来自将军澳机房当晚连续 30 分钟窗口,RT 线程数=6,输入压力≈平峰 1.2x。
我踩过的坑(以及我是怎么绕过去的)
SMT 兄弟核干扰
- 同一个物理核的两个超线程共享资源,RT 线程与软中断“同床异梦”。
- 做法:用成对偶数核(如 2/3、4/5…)时,只用其中一个给 RT;或干脆 BIOS 关 SMT(代价较大)。
irqbalance 把中断拽回来了
- 一次重启后忘了 disable,NIC 中断又跑到 RT 核。
- 做法:关掉它并写自启动脚本,启动后重写 smp_affinity_list。
容器 cgroup 覆盖亲和
- 在 container 里 taskset 看起来有效,实际上被宿主 cgroup 限住了。
- 做法:宿主侧为容器设置 cpuset.cpus,并在容器内再细分。
NTP/chrony 大步调整引发抖动
- 时间源切换瞬间的 slew/step 有小抖动。
- 做法:局域网 PTP 主时钟,makestep 只在大偏差时做;观察 tracking。
电源管理“阴魂不散”
即使 tuned performance,偶见 C-state 进出。
做法:内核 idle=poll + BIOS 里把深 C-state 关掉(C1E 以上),频率锁定 performance。
RT 线程忘了主动让出(仅 FIFO)
- 一段 I/O 等待没有 sched_yield,导致同优先级其他线程“饿”。
- 做法:关键循环里遇到可等待点前做一次 sched_yield() 或分层优先级。
小抄:命令 & 配置速览
# 查看策略/优先级
ps -eLo pid,cls,rtprio,cmd | sort -k2
# 运行态临时切策略
chrt -f -p 90 <PID> # FIFO 90
chrt -r -p 75 <PID> # RR 75
# systemd unit 关键字段
# CPUAffinity= CPUSchedulingPolicy= CPUSchedulingPriority=
# LimitRTPRIO= LimitRTTIME= NUMAPolicy= NUMAMask=
# 内核参数
sysctl -w kernel.sched_rt_runtime_us=-1
echo 10 > /proc/sys/kernel/sched_rr_timeslice_ms
附:我常用的优先级配表(示例)
| 模块 | 线程 | 调度类 | 优先级 | 绑核 |
|---|---|---|---|---|
| 风控引擎 | 计算核(6 个) | FIFO | 92–95 | 2–7 |
| IO 提交 | Net TX/RX | RR | 75–80 | 2–7 |
| feed handler | 解析/聚合 | RR | 70 | 2–7 |
| 监控/探针 | exporter | CFS | N/A | 0-1,8-55 |
| 系统 IRQ | NIC/MSI-X | N/A | N/A | 40–55 |
那晚 5:10,我把最后一个 IRQ 的亲和写好,cyclictest 的曲线像被刀子削过一样平滑。早高峰 9 点钟,风控的 p99 只剩 0.8 ms,值班群里没人再“@ 全体成员”。有人问我到底做了什么“黑魔法”。我说:不是魔法,是把噪音关在门外,让重要的线程拥有话语权。
如果你也在香港的机房里和我一样追求确定性,不妨按这篇把 SCHED_FIFO / SCHED_RR 用起来。记得:先量后动,逐步推进,可观测、可回滚。当内核为你站队时,实时业务的脾气就会好很多。祝顺利!