多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

多级反馈队列到EEVDF:Linux调度器演进与三大核心参数调优

多级反馈队列到EEVDF:Linux调度器演进与三大核心参数调优 1. 调度器到底在忙什么从谁先来谁先跑说起如果你写过操作系统课的大作业大概率实现过一个先来先服务的调度器代码不到五十行跑起来也能用。但真到了生产环境这套逻辑撑不过三分钟——一个后台日志压缩任务就能把交互式终端的响应拖到秒级。调度器要解决的核心矛盾从来不是谁先来而是在吞吐量、响应延迟、公平性这三个互相打架的目标之间找平衡点。多级反馈队列Multi-Level Feedback QueueMFQ就是为这个矛盾设计的一套机制。它的核心思想很朴素不知道一个任务该跑多久那就让它先跑一小段试试根据表现动态调整优先级。短任务快速跑完走人长任务逐步降级但保证不被饿死交互式任务因为频繁让出CPU而保持高优先级。这套思路最早由Corbató在1962年的CTSS系统上提出后来演变成各种变体。Linux 2.6内核引入的O(1)调度器就大量借鉴了MFQ的思想用140个优先级队列加动态优先级调整来管理任务。到了6.x内核虽然CFS完全公平调度器成了主力但MFQ的设计哲学依然渗透在实时调度类和某些特定场景的调度策略里。这篇文章适合三类人看正在学操作系统调度章节的学生、需要调优服务器任务延迟的运维、以及想理解内核调度演进脉络的开发者。我会从MFQ的核心机制讲起拆解Linux 2.6到6.x的演进逻辑重点分析三个决定调度行为的核心参数最后给出实际调优时能直接抄的配置方案。2. MFQ的核心机制拆解队列、时间片与降级规则2.1 多级队列的层级设计逻辑MFQ的第一层设计是把就绪队列拆成多个优先级不同的子队列。典型实现里最高优先级队列时间片最短比如10ms最低优先级队列时间片最长比如100ms。为什么这么设计因为高优先级队列服务的是交互式任务它们的特点是跑一下就阻塞等IO短时间片能让它们快速让出CPU给其他交互任务低优先级队列服务的是CPU密集型任务长时间片减少上下文切换开销。队列数量不是随便定的。太少比如3级会导致优先级区分度不够一个编译任务和一个小脚本可能落在同一级太多比如32级则增加调度器查找下一个可运行任务的开销。Linux 2.6的O(1)调度器用了140个优先级0-139其中0-99是实时优先级100-139是普通任务优先级每个优先级对应一个队列。这个数字不是拍脑袋来的——它对应了内核中prio值的取值范围实时任务的优先级范围是0-99普通任务的nice值-20到19映射到100-139。2.2 时间片分配与动态调整时间片的分配遵循一个原则优先级越高时间片越短。在经典的MFQ实现中第i级队列的时间片通常是第i1级的一半。比如队列层级优先级范围典型时间片适用任务类型Q0最高10ms交互式、IO密集Q1高20ms短时计算Q2中40ms中等计算Q3低80ms长时计算Q4最低160ms后台批处理这个表格是概念示意实际内核实现会更复杂。关键点在于时间片随优先级降低而指数增长。这样设计的好处是高优先级任务频繁获得CPU但每次跑得短低优先级任务偶尔获得CPU但一次跑得久整体上下文切换次数可控。2.3 降级与升级的触发条件任务在MFQ中的移动规则是核心。基本规则有三条新任务进入最高优先级队列不知道它要跑多久先给最好的待遇试试。任务用完时间片还没结束降一级说明它是CPU密集型降到下一级用更长时间片。任务在时间片用完前主动让出CPU比如等IO留在原级或升级说明它是交互式的应该保持高响应性。这里有个容易被忽略的细节降级不是永久的。如果一个任务在低优先级队列里等了太久调度器会把它提升回高优先级队列防止饥饿。这个太久的判定通常基于任务在就绪队列中的等待时间或者基于一个老化aging计数器。Linux 2.6的O(1)调度器里这个逻辑体现为prio和sleep_avg两个字段的互动。sleep_avg记录任务睡眠时间的加权平均值睡眠越多说明越可能是交互式任务effective_prio就越高。任务被唤醒时如果sleep_avg超过阈值它会获得优先级提升如果一直占着CPU不放sleep_avg会衰减优先级逐步降低。3. Linux 2.6到6.x的演进从O(1)到CFS再到EEVDF3.1 O(1)调度器的MFQ基因Linux 2.6.0到2.6.22用的是O(1)调度器名字里的O(1)指的是调度决策的时间复杂度是常数级不管有多少任务在跑选下一个任务的时间都一样。这个特性靠的是两个数据结构active队列数组和expired队列数组每个数组有140个优先级槽位。工作流程是这样的调度器从active数组的最高优先级非空队列取任务用完时间片后根据静态优先级和动态优先级调整放入expired数组对应位置。当active数组全空时交换两个数组的指针。这个设计巧妙的地方在于时间片用完的任务不会立即再次参与调度而是等到所有active任务都跑完一轮后才重新有机会。这天然实现了MFQ的降级效果而且避免了在同一个队列里反复扫描。但O(1)调度器有个致命问题它对交互式任务的判定过于依赖启发式规则。sleep_avg的计算涉及多个魔数这些魔数在不同负载下表现差异巨大。典型症状是一个桌面用户可能感觉系统很流畅但同样的内核跑在服务器上某些任务会莫名其妙地延迟飙升。这是因为交互式判定逻辑对睡眠-运行模式的假设在服务器场景下不成立。3.2 CFS的完全公平理念与MFQ的退场2.6.23内核引入了CFSCompletely Fair Scheduler由Ingo Molnar主导设计。CFS的核心数据结构是红黑树按任务的vruntime虚拟运行时间排序每次选vruntime最小的任务运行。vruntime的增长速度和任务的权重成反比——权重高的任务nice值低vruntime增长慢获得更多CPU时间。CFS和MFQ的设计哲学完全不同。MFQ是基于规则的反馈系统通过时间片和队列层级来间接控制调度行为CFS是基于数学模型的公平分配直接计算每个任务应该获得多少CPU时间。CFS没有显式的优先级队列也没有时间片降级机制它通过vruntime的单调递增来保证公平性。那MFQ的思想在CFS里还有痕迹吗有但很隐蔽。CFS的vruntime最小粒度sysctl_sched_min_granularity和调度周期sysctl_sched_latency实际上起到了类似时间片的作用。当任务数量超过nr_running阈值时调度周期会按比例扩展保证每个任务至少获得最小粒度的时间。这个机制和MFQ的低优先级队列用长时间片在效果上有相似之处但实现路径完全不同。3.3 6.x内核的EEVDF与调度类分层6.6内核引入了EEVDFEarliest Eligible Virtual Deadline First调度器取代了CFS。EEVDF的核心改进是引入了虚拟截止时间的概念每个任务有一个eligible时间点和deadline调度器选择eligible且deadline最早的任务。这个设计在保持公平性的同时更好地处理了延迟敏感型任务。从MFQ的视角看EEVDF的deadline机制实际上是一种更精细的优先级表达。任务的权重nice值影响vruntime的增长速度进而影响deadline的早晚。高权重任务deadline更早获得更多调度机会。这和MFQ里高优先级队列获得更多CPU时间的逻辑是一致的只是数学基础更扎实。6.x内核的调度类分层也值得注意。内核把调度器分成多个类stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。每个类有自己的调度策略按优先级从高到低依次调用。实时任务用rt_sched_class普通任务用fair_sched_class。这种分层设计让MFQ的思想在实时调度类里得以保留——实时任务确实有固定的优先级队列高优先级实时任务总是抢占低优先级任务。4. 三个关键参数决定调度行为的隐藏旋钮4.1 参数一调度延迟目标sched_latency/proc/sys/kernel/sched_latency_ns控制的是调度周期默认值通常是6ms6000000ns或根据CPU数量动态调整。这个参数决定了在理想情况下所有可运行任务轮转一遍需要多长时间。它的计算逻辑是这样的如果当前有nr_running个任务每个任务至少获得sched_min_granularity_ns默认0.75ms的运行时间。当nr_running * sched_min_granularity_ns sched_latency_ns时调度周期会扩展为nr_running * sched_min_granularity_ns。这样保证每个任务不会因为任务太多而被切得太碎。调这个参数的实际效果降低sched_latency_ns会让交互式任务响应更快但上下文切换次数增加吞吐量下降。我实测过在一台跑着数据库和Web服务的机器上把sched_latency_ns从6ms降到3msWeb请求的P99延迟从12ms降到8ms但数据库的TPS掉了约5%。这个 trade-off 需要根据业务场景来定。注意这个参数在6.x内核里依然存在但EEVDF对它的依赖程度比CFS低。EEVDF更依赖deadline的计算sched_latency_ns主要影响vruntime的初始化和更新逻辑。4.2 参数二最小抢占粒度sched_min_granularitysched_min_granularity_ns默认0.75ms它规定了一个任务在被抢占前至少运行多长时间。这个参数的存在是为了避免过于频繁的上下文切换——如果每个任务跑0.1ms就切换CPU时间大部分花在保存和恢复寄存器上了。这个参数的设定需要权衡两个因素上下文切换开销和调度延迟。上下文切换的开销在现代CPU上大约是1-3微秒如果最小粒度设成0.1ms切换开销占比约1-3%如果设成0.75ms占比降到0.1-0.4%。但最小粒度越大高优先级任务等待低优先级任务让出CPU的时间就越长交互式体验越差。在MFQ的语境下这个参数对应的是最高优先级队列的时间片。MFQ里最高优先级队列的时间片通常就是系统能容忍的最小调度粒度。Linux把这个概念抽象成了全局参数而不是每个队列单独设置简化了调优复杂度。4.3 参数三唤醒抢占粒度sched_wakeup_granularitysched_wakeup_granularity_ns默认1ms控制的是一个刚被唤醒的任务能否抢占当前正在运行的任务。规则是如果被唤醒任务的vruntime比当前任务的vruntime小且差值超过sched_wakeup_granularity_ns就触发抢占。这个参数直接影响交互式体验。想象一个场景你敲键盘触发了一个终端任务这个任务刚从睡眠中被唤醒。如果它的vruntime比当前正在跑的编译任务小很多它应该立即抢占CPU让你看到输入回显。但如果抢占太频繁编译任务的吞吐量会受影响。sched_wakeup_granularity_ns设得太小会导致频繁抢占上下文切换开销上升设得太大交互式任务唤醒后要等很久才能获得CPU。默认1ms是个折中值但在不同负载下可能需要调整。比如在跑延迟敏感的金融交易系统时可以降到0.5ms在跑批处理集群时可以升到2ms。参数默认值调低的影响调高的影响sched_latency_ns6ms响应快切换多吞吐降吞吐升响应慢sched_min_granularity_ns0.75ms切换多开销大切换少延迟高sched_wakeup_granularity_ns1ms抢占频繁交互好抢占少吞吐好5. 实操查看与调优调度参数5.1 查看当前系统的调度参数在终端里直接读/proc/sys/kernel/下的文件就行# 查看调度延迟目标 cat /proc/sys/kernel/sched_latency_ns # 查看最小抢占粒度 cat /proc/sys/kernel/sched_min_granularity_ns # 查看唤醒抢占粒度 cat /proc/sys/kernel/sched_wakeup_granularity_ns # 查看当前运行队列状态 cat /proc/schedstatschedstat的输出格式是每个CPU一行字段包括运行时间、等待时间、切换次数等。这个文件对排查调度问题很有用比如某个CPU的等待时间远高于其他CPU说明负载不均衡。5.2 临时调整与永久生效临时调整直接写文件# 临时把调度延迟降到4ms echo 4000000 /proc/sys/kernel/sched_latency_ns # 临时把唤醒抢占粒度降到0.5ms echo 500000 /proc/sys/kernel/sched_wakeup_granularity_ns永久生效需要写/etc/sysctl.conf# 在/etc/sysctl.conf末尾添加 kernel.sched_latency_ns 4000000 kernel.sched_min_granularity_ns 500000 kernel.sched_wakeup_granularity_ns 500000然后执行sysctl -p加载配置。注意这些参数在系统重启后会恢复默认值除非写进配置文件。5.3 用perf和ftrace验证调优效果调完参数不能凭感觉得用数据说话。perf sched可以统计调度延迟# 记录5秒的调度事件 perf sched record -- sleep 5 # 查看调度延迟统计 perf sched latency输出里会显示每个任务的等待时间、运行时间、切换次数。重点关注Maximum和Average两列如果某个任务的等待时间远超预期说明调度参数可能需要调整。ftrace可以追踪具体的调度事件# 启用调度切换追踪 echo 1 /sys/kernel/debug/tracing/events/sched/sched_switch/enable # 查看追踪结果 cat /sys/kernel/debug/tracing/trace这个输出会显示每次任务切换的详细信息包括切换前后的任务名、优先级、CPU号。分析这些数据能发现调度器是否按预期工作。6. 常见问题与排查技巧实录6.1 交互式任务延迟高但CPU不忙这种情况通常是sched_wakeup_granularity_ns设得太大导致唤醒的任务无法及时抢占。排查步骤用perf sched latency查看交互式任务的等待时间。如果等待时间超过10ms检查sched_wakeup_granularity_ns的值。临时降到0.5ms测试如果延迟改善说明参数需要调整。但要注意降低这个参数会增加抢占次数可能影响吞吐量。建议在测试环境验证后再上生产。6.2 上下文切换次数异常高用vmstat 1看cs列如果每秒上下文切换超过10万次说明切换过于频繁。可能的原因sched_min_granularity_ns设得太小任务还没跑够就被切走。任务数量太多调度周期被迫扩展但每个任务分到的时间仍然很短。有任务在频繁睡眠和唤醒比如轮询式IO。排查时先用pidstat -w 1看哪个任务的切换次数最多再针对性分析。如果是某个任务的问题考虑改它的IO模型如果是全局参数问题适当调大sched_min_granularity_ns。6.3 实时任务被普通任务阻塞实时任务理论上应该立即抢占普通任务但如果实时任务的优先级设得不够高或者rt_runtime_us配额用完它会被限流。检查# 查看实时任务的带宽限制 cat /proc/sys/kernel/sched_rt_runtime_us cat /proc/sys/kernel/sched_rt_period_us默认rt_runtime_us是9500000.95秒rt_period_us是10000001秒意味着实时任务最多占用95%的CPU时间。如果实时任务需要100%的CPU需要调整这两个参数但要小心——实时任务跑满会导致系统无法响应其他事件。6.4 参数调优速查表症状可能原因调整方向交互延迟高唤醒抢占粒度太大降低sched_wakeup_granularity_ns吞吐量低调度延迟太小提高sched_latency_ns切换次数多最小粒度太小提高sched_min_granularity_ns实时任务被限流rt_runtime_us配额不足调整rt_runtime_us和rt_period_us任务饥饿优先级调整过于激进检查nice值和调度策略7. 从MFQ到EEVDF调度器设计的变与不变回头看MFQ到Linux 6.x的演进有一条主线始终没变调度器要在不知道任务未来行为的情况下做出最优决策。MFQ用多级队列和时间片降级来近似这个最优解CFS用红黑树和vruntime来精确计算公平性EEVDF用虚拟截止时间来平衡公平和延迟。变的只是数学工具和数据结构。MFQ的队列层级变成了CFS的红黑树节点时间片降级变成了vruntime的单调递增优先级提升变成了deadline的提前。但核心思想——根据任务的历史行为预测未来需求并动态调整调度策略——从1962年到现在都没变过。三个关键参数sched_latency_ns、sched_min_granularity_ns、sched_wakeup_granularity_ns是这套思想在Linux上的具体体现。它们不是孤立的旋钮而是互相制约的三角关系。调其中一个另外两个的效果会跟着变。我自己的经验是先明确业务场景的优先级——是延迟敏感还是吞吐优先——然后只调最相关的那个参数每次调完用perf验证不要一次改多个。最后分享一个实用技巧在容器环境里调度参数的调整会受到cgroup的限制。cpu.cfs_quota_us和cpu.cfs_period_us会覆盖全局的调度参数。如果你在容器里调了参数没效果先检查cgroup的配置。这个坑我踩过不止一次排查了半天才发现是容器层面的限制。
返回列表