上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-26 09:41 阅读量:661


我坐在将军澳的 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 用起来。记得:先量后动,逐步推进,可观测、可回滚。当内核为你站队时,实时业务的脾气就会好很多。祝顺利!

目录结构
全文