我干了件看起来有点“蠢”的事:把Linux内核里那个被奉为经典的完全公平调度器(CFS),核心逻辑改成随机调度——哪个进程下一个运行,全凭运气。
所有人都觉得会慢成狗。结果在数据库高并发场景下,吞吐量反而提升了十几个百分点。这个反直觉的结果,让我重新审视了调度器“公平性”背后的隐藏成本。
一、我为什么质疑“完美调度”
CFS 的设计哲学很迷人:给每个进程分配“虚拟运行时间”,总是选择累计运行时间最少的进程,从而实现绝对的公平。红黑树、ns级精度、近乎完美的 O(logN) 复杂度——这是教科书级的优雅。
但我一直有个怀疑:在多核、大缓存、NUMA 架构普及的今天,过度公平会不会反而破坏数据的局部性?进程切换过于频繁,L1/L2 缓存频繁失效,TLB 被冲刷,大量 CPU 周期浪费在“搬家”而非“计算”上。
公平调度的隐性代价:每次切换都可能带走前一个进程温暖的缓存,让新进程面对冰冷的存储墙。带着这个怀疑,我决定做个极端实验:把 CFS 的“公平选择”替换为“完全随机”。
二、随机调度:20行代码的疯狂手术
修改思路很简单,入口就在 kernel/sched/fair.c 中的 pick_next_task_fair() 函数。原本的逻辑是从红黑树中取 vruntime 最小的进程,我改成从当前可运行队列中随机抽取一个进程。
static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf){ // 原有红黑树逻辑全部注释 // 改为:获取当前就绪队列的进程数,随机取一个下标,返回对应 task_struct return random_task_from_rq(rq);}——核心改动不到20行,还顺手删了 vruntime 的维护逻辑。
当然,这是个“学术破坏性”实验,完全不考虑实时性、优先级和交互响应。编译内核、重启、进入这个“摇奖模式”的系统。
三、数据库测试下的“反常胜利”
测试环境:双路 Xeon Gold 32核 / 128GB RAM / NVMe SSD,运行 PostgreSQL 15(oltp_read_write 混合负载)。预期中随机调度会带来剧烈性能崩溃,但结果让我盯着屏幕看了很久:
- TPS(每秒事务数)提升约 11% ~ 14%
- CPU 利用率反而略微降低(因为 cache miss 导致流水线停顿减少?)
一开始我以为是测量误差。反复跑了几轮,同样的结果:在某些多进程/多线程数据库OLTP场景下,随机调度居然比 CFS 更快。
这不科学?但数据不会撒谎。
四、分析:公平调度是如何“污染”缓存的
冷静下来分析,原因逐渐清晰:CFS 为了尽量公平,会高频地切换不同进程,尤其是当多个进程的 vruntime 很接近时。在高并发数据库中,几十个 worker 进程反复读写共享的热点数据页,CFS 的策略是让大家“雨露均沾”,结果就是:
- 进程A刚刚把某个 B+树索引页加载进 L3 缓存,还没用完就被切出;
- 换进程B上来,发现缓存里没有它的数据,重新去内存加载(慢几十倍);
- 所有人都没享受到缓存红利,内存带宽被频繁读取消耗殆尽。
而随机调度看似无序,反而减少了“强制平均主义”带来的颠簸。某些幸运的进程可能连续运行几个时间片,将热数据深度缓存;另一些进程虽然暂时被跳过,但整体吞吐量却上升了。这就像交通调度:红绿灯过于公平地轮转每个车道,反而不如让某条主路一次多走几辆车效率更高。
五、这不是推翻 CFS,而是一场提醒
随机调度不可能用于真实生产——交互响应会崩,优先级形同虚设,实时任务直接挂掉。但这个实验揭示了一个重要事实:“完美公平”是有代价的,而且这个代价随着缓存层级加深、核数变多而放大。
这也解释了为什么很多数据库、键值存储(如 Redis)会主动做 CPU 亲和性和绑核,减少调度器介入;为什么 DPDK、SPDK 这类框架干脆让进程独占核,自己轮询。
没有普适的最优调度器,只有针对特定负载的权衡艺术。有时候,适当的“不公平”反而是一种全局效率。如果你的业务饱受上下文切换开销之苦,也许该审视一下:是不是过于公平的调度器,正在悄悄吃掉你的缓存红利。
——
本文为「比特原点」原创内容,基于公开资料与技术原理独立撰写。
文中不包含任何品牌/产品的商业推广信息。
如无特殊说明,所有技术观点仅代表个人理解,不构成专业建议。
内容可转载,请后台联系获取授权。