S3

CPU 调度入门

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

订单 API 的任务是尽快接收请求、检查库存、写入订单并返回结果。平时响应很快,促销开始后却偶尔停顿几百毫秒。机器没有报错,平均 CPU 使用率也不高。此时需要追问:请求对应的线程是在执行、等待执行,还是根本不能执行?本课沿用 S2 的就绪、运行、阻塞状态,从调度队列解释这些差别,并把线索接到 P3 已讨论的延迟与容量目标。

1 为什么调度决定「谁先跑」

CPU 调度解决的是竞争关系:某个逻辑 CPU 此刻可以执行指令,多个线程已经就绪,应该让谁运行、运行多久,以及何时换人。这里的逻辑 CPU 是操作系统看到的执行位置;有些来自独立物理核心,有些来自硬件多线程。同属某个物理核心的逻辑 CPU 会共享部分执行资源,因此不能把逻辑 CPU 数直接当作性能倍数。

订单服务、日志压缩、监控采样可能同时需要 CPU。它们都能继续工作,但执行位置有限。如果压缩任务连续占用 CPU,刚收到请求的订单线程即使只需很少计算,也可能先在队列里等待。请求最终耗时既包含真正执行指令的时间,也包含等待获得 CPU 的时间,还可能包含数据库、磁盘或网络等待。调度主要影响前面所说的 CPU 等待,不能替代对其他等待的分析。

把某次请求画成时间轴:业务计算累计 8 ms,等待 CPU 累计 22 ms,等待外部结果累计 70 ms,端到端耗时约 100 ms。这是为了理解而做的串行拆分,真实系统还可能存在并行调用、重叠等待和应用内部排队。优化前要先确定关键路径,否则把 CPU 计算减半,也未必能明显缩短响应。反过来,计算时间没变而就绪等待增加,也能让用户感到服务变慢。

调度还在平衡不同目标。吞吐量关注单位时间完成多少工作,响应性关注任务多久获得首次或再次执行的机会,公平性关注竞争者是否长期得不到合理份额。这些目标不能靠“让重要线程永远先跑”同时解决。订单 API 需要及时响应,日志压缩也必须在可接受时间内完成,否则积压会反过来消耗存储和运维资源。

本课讨论普通分时调度的直觉,不把算法名称当作诊断结论。普通任务的权重、唤醒行为和历史运行情况会影响实际选择;Linux 的具体机制还随版本、调度策略和分组配置变化。入门时先回答“任务能不能跑、在哪排队、何时获得 CPU”,比背诵某种算法更有用。深入的 CFS 与 vruntime 数学留到后续章节。

订单请求的延迟来自多个阶段
订单请求的延迟来自多个阶段

2 就绪、运行、阻塞:队列视角

沿用已有状态知识,把就绪理解为“条件已满足,只差 CPU”,运行理解为“正在某个 CPU 上执行”,阻塞理解为“还缺少事件或资源,暂时没有执行资格”。图中的队列是教学抽象,并不表示内核只维护 1 个全局队列。多核环境通常存在与各 CPU 相关的运行队列,调度器还需要处理任务迁移和负载平衡。

订单线程收到请求后进入就绪状态,被选择后开始运行。运行期间可能主动等待数据库结果而阻塞,也可能因为调度决策被抢占,回到就绪状态。抢占不是请求失败,也不意味着进度丢失;系统保存必要执行上下文,之后可以继续。需要注意,“运行队列”这个名称在不同观测口径中可能包含正在运行的任务,读指标时必须确认定义。

状态变化与 CPU 执行资格
状态变化与 CPU 执行资格

观察动画时,先只看 1 个 CPU 和 3 个普通线程。订单线程 A 运行,订单线程 B 与压缩线程 C 等待;发生抢占后,A 回到就绪集合,B 获得执行机会。箭头表示状态变化,不表示实际系统一定按固定顺序轮流执行。真正要学会判断的是:A 被抢占后仍然有工作可做,因此等待属于 CPU 排队,而不是 I/O 阻塞。

阻塞线程不应靠不断获得 CPU 来检查结果。数据库响应到达并满足等待条件后,系统可以将对应线程唤醒,使它重新具备运行资格。但“已唤醒”不等于“已经运行”:如果此时 CPU 正忙,它仍可能在就绪队列等待。订单请求的尾部延迟有时正出现在这个间隙,即外部结果已经返回,应用却过了一段时间才继续处理。

还有 1 层队列容易混淆:请求可能先在应用工作队列里等待空闲工作线程。这时并不一定存在与该请求对应的就绪线程。例如线程池全部等待数据库,后来请求停在应用队列中,调度器无法直接为这些请求分配 CPU。排查必须区分应用排队、内核就绪排队和外部事件等待,它们需要不同证据,也对应不同调整位置。

3 时间片与响应:交互进程为什么不能饿死

时间片可以先理解为连续占用 CPU 的时间预算。预算用完或出现其他调度条件时,当前任务可能让出执行位置;等待 I/O 的任务也可能在预算耗尽前主动阻塞。现代普通调度的实际连续运行时长会随策略和负载变化,不能把教学中的固定时间片当成所有 Linux 系统都能统一设置的旋钮,更不能承诺线程每隔固定毫秒必然运行。

考虑 1 个 CPU 上有 4 个始终就绪的线程,在理想轮转模型中,每个线程连续运行 10 ms。某线程刚错过机会,可能需要等待其他 3 个线程各运行 10 ms,才轮到自己。把片长缩到 2 ms,会让机会更密集,但切换次数增加。这个模型忽略了唤醒、优先级与内核开销,只用来展示等待机会与连续执行长度的关系,不用于预测线上百分位延迟。

切换需要保存和恢复执行状态,也可能让后续执行重新适应工作数据,不能假设完全免费。不过,“切换多”本身也不是故障:大量短请求和 I/O 唤醒天然可能产生较多切换。只有把切换速率、完成请求数、CPU 消耗和响应时间放在同一负载条件下比较,才有依据判断开销是否异常。只追求减少切换,可能让交互请求等待更久。

滑块采用明确的理想模型:4 个始终就绪线程,固定片长,每次切换假设消耗 0.05 ms。观察片长缩短后,最坏的轮转等待估计下降,而用于业务执行的时间比例也略有下降。这里的“吞吐”只显示理想有效执行比例,不代表每秒订单数。真实订单包含 I/O 和不同计算长度,必须通过实际负载测量吞吐量与 p95、p99 延迟。

交互任务通常呈现“短计算、等事件、再短计算”的节奏。如果每次被唤醒都长期排在大量计算工作后面,用户会觉得输入或请求没有反应。饥饿则更严重:任务持续就绪,却长期得不到足够执行机会。普通调度希望在响应和公平之间保持平衡,但这不是硬实时保证;策略差异、资源配额和长期过载仍可能造成明显等待。

因此,面对订单 API 毛刺,优先看竞争来源与负载形态,而不是先寻找调小时间片的命令。减少请求路径中的多余计算、限制后台计算并发、让无事可做的线程真正等待,往往比修改底层调度参数更容易验证。若负载持续超过可用 CPU,调整轮换方式不能凭空增加算力,还需要容量、限流或削减工作量。

运行队列中的竞争与抢占
运行队列中的竞争与抢占

4 优先级与 nice:谁可以调、谁不该调

优先级表达竞争时的相对待遇,但首先要区分调度策略。Linux 普通分时任务的 nice 值通常在 -20 到 19 之间;数值较小通常意味着更高的相对 CPU 权重,数值较大意味着更低权重。它不是延迟承诺,也不是“分到固定百分比 CPU”的配置。只有发生竞争,且结合调度分组、资源限制等条件,权重差异才会体现为实际份额差异。

例如订单服务与离线报表同时争用 CPU,可以考虑让报表使用较大的 nice 值,以降低它对前台服务的竞争。CPU 充足时,报表仍可能充分运行;这通常符合后台任务“有空就做”的定位。但如果它持有前台请求需要的锁,或本身是订单关键依赖,简单降低其待遇也可能延长前台等待。因此要先画清依赖,再决定谁适合退让。

普通用户通常可以降低自己任务的调度待遇,也就是增大 nice 值;提高待遇、减小 nice 值通常需要相应权限,具体还受资源限制与系统策略影响。系统管理员能做的事也不等于应用应当默认去做的事。修改前应明确目标进程、涉及线程、当前策略和回退方式,避免把同机其他服务或关键后台任务挤出必要的执行机会。

可以用 nice -n 10 command 启动较低权重的普通后台任务;renice -n 10 -p PID 可用于调整指定现有任务,但必须先确认 PID 与权限。在 Linux 上 nice 与线程相关,多线程程序不能仅检查主线程后就假定所有线程已采用相同设置。这里的命令只展示语义,不要求在生产订单服务上直接执行;正式变更应遵循服务自己的发布与回退流程。

不要把普通任务的 nice 与实时调度策略混为一谈。实时策略具有不同规则和权限要求,错误使用可能严重压缩其他任务获得 CPU 的机会。本课不提供把订单服务提升为实时任务的操作配方。对于共享机器或容器,CPU 配额、权重和允许运行的 CPU 范围也会共同影响结果;单看 nice 无法推断服务实际可用容量,更无法判断是否出现配额节流。

5 多核与亲和性入门(不进 Cache 一致性)

在多核机器上,“CPU 还空闲”要继续追问是哪些 CPU 空闲。若订单服务的繁忙线程只允许在 1 个逻辑 CPU 上运行,其他 CPU 即使空闲,也不能直接替它分担。亲和性描述任务允许在哪些 CPU 上运行,系统会在这个允许集合内安排执行。它限制选择范围,并不保证独占,也不保证每个允许 CPU 都同时有该任务运行。

再看并发上限:某段订单计算只能由当前 1 个线程顺序执行,增加空闲 CPU 不会自动把这段代码拆开。允许使用 8 个 CPU、实际只有 1 个可运行线程,与存在 8 个可运行线程却被限制在 1 个 CPU,是不同问题。前者需要分析应用并行度,后者需要分析允许集合与部署配置。把它们都称为“CPU 不够”会误导容量决策。

调度器可能迁移任务,让不同 CPU 的工作更均衡;任务留在熟悉的 CPU 上也可能有局部性收益。本课只保留这个直觉,不展开 Cache 一致性。手工固定线程到特定 CPU,有时能减少迁移或隔离干扰,有时却会制造热点、缩小可用容量。没有稳定负载与对照证据,不应把绑定 CPU 当成普遍加速技巧。

Linux 上可用 taskset -pc PID 查看指定任务的 CPU 亲和性列表,也可在适当权限下查看各线程。它反映指定对象的情况,不能只查询主线程便概括整个多线程服务。容器的有效允许运行范围还可能受到 cpuset 等配置约束。CPU 配额则限制一定周期内可用的 CPU 时间,和“能在哪些 CPU 上运行”是不同维度,排查时要分别核对。

主机空闲与服务可用 CPU 的差别
主机空闲与服务可用 CPU 的差别

对订单 API 而言,最有用的检查顺序是先确认线程是否可运行,再看允许集合中是否有竞争,随后检查资源限额与应用并行度。只有确认热点来自不合理绑定,解除或调整绑定才是有因果依据的动作。任何调整都应同时观察订单尾延迟、后台任务进度和错误率,避免通过牺牲依赖任务来换取短暂的表面改善。

6 观测:top/ps 看什么字段

观测要从业务时间窗口出发。先记录订单延迟异常发生的时间、持续多久、影响哪些实例,再对照同一时段的 CPU 与线程数据。每隔较长时间手工看 1 次终端,可能刚好错过短毛刺;长期平均值也可能把局部高峰抹平。可复现环境中应保留连续采样,线上则优先使用已有监控与获准的诊断工具,控制额外采样开销。

top 适合观察短间隔内的变化。进程行的 %CPU 帮助定位谁消耗了执行时间,PR、NI 分别提供调度优先级显示与 nice 线索,S 显示采样时的状态。交互界面中可按 H 查看线程、按 1 展开每个逻辑 CPU。按键与显示受版本影响,应结合本机帮助;多线程任务的 CPU 百分比也可能超过 100%,需要确认显示模式和归一化口径。

CPU 汇总行里的 us、sy、id、wa 分别提供用户态、内核态、空闲及 I/O 等待记账线索。wa 不能直接等同于订单请求的 I/O 等待时间,也不是所有外部等待都会使它明显升高;网络调用慢时尤其不能只依赖它。虚拟化环境中的 st 可提示虚拟 CPU 被宿主环境占用的时间,但仍需结合平台指标解释,不能直接归因给某个应用线程。

需要保留清晰快照时,可用以下只读示例;1234 必须替换为实际订单服务 PID。它展示线程标识、最近运行 CPU、状态、优先级、nice、CPU 比例与命令名。部分字段依赖 Linux procps 实现,可先查本机 ps 帮助。

ps -L -p 1234 -o pid,tid,psr,stat,pri,ni,pcpu,comm

TID 帮助把热点追到具体线程;PSR 表示最近执行所在 CPU,不是亲和性列表。STAT 的 R 通常包含运行或可运行,单张快照无法判断线程是在 CPU 上还是排队。S 常见于可中断睡眠,可能等待网络、锁或定时事件;D 常见于不可中断等待,往往涉及内核资源或 I/O,但不能只凭字母断定是哪块磁盘。ps 的 %CPU 通常是任务生命周期内的平均比例,不能与 top 采样区间数值直接等同。

观测字段能回答的问题与边界
观测字段能回答的问题与边界

优先级显示也要谨慎:top 的 PR 与 ps 的 PRI 可能采用不同显示约定,不能机械比较数值大小。nice 提供普通任务的权重线索,策略仍需另行确认。系统 load average 也不等于 CPU 使用率;Linux 的负载统计还包含某些不可中断等待任务。看到负载高而 CPU 不忙,应继续找等待来源,不能直接断言运行队列拥堵。

这些字段负责缩小范围,不直接给出根因。确认就绪等待通常需要连续样本,并在必要时使用环境支持的调度等待统计或现有性能平台,与请求时间线对齐。不要根据 1 次 R、1 个高 %CPU 或 1 次高负载就修改优先级。可靠结论应能解释异常何时发生、哪些线程受影响,以及为什么某次针对性变更能使现象变化。

7 常见坑:忙等、过度提高优先级、忽略 I/O 等待

忙等是指条件尚未满足时,线程反复执行检查,例如循环询问队列是否有新订单。它看起来没有做有效业务,却一直保持可运行并消耗 CPU,与真正的请求线程竞争。若等待通常较长,应使用适合该场景的阻塞等待或事件通知,让线程在没有工作时退出竞争。极短等待的自旋有特定适用条件,但不能把它作为普通业务代码的默认策略。

另一个变体是“把循环睡眠改短就会更快”。定时检查间隔过长可能增加发现事件的延迟,过短又会产生频繁唤醒与额外工作。如果等待来源已经提供事件通知,优先采用通知机制,而不是用密集轮询模拟通知。改动后要测量有效请求处理量与空转消耗,不能只看到某次请求更快,就忽略整体 CPU 和尾部延迟的变化。

过度提高优先级会重新分配竞争结果,不会消除竞争本身。假如把订单工作线程提高待遇,却让处理依赖响应、日志落盘或健康检查的线程长期得不到机会,系统可能更不稳定。若多方都不断“加急”,优先级最终失去业务含义,还留下难以维护的特殊配置。合理做法是先限定后台并发,再以服务目标验证是否需要相对权重调整。

忽略 I/O 等待则会走向另一端:看到 CPU 使用率低,就认定应用资源足够。订单线程可能都在等数据库连接、远端响应或锁;CPU 再空闲,也不能替外部结果到达。应先用请求分段耗时与线程等待线索定位缺失条件。不要无依据扩大线程池,更多线程可能把压力转移给数据库、加长队列,还增加内存与调度开销。

还有 1 个常见错误是只看整机。主机平均值无法表达单核饱和、容器节流与不同实例之间的不均衡。要保持观测范围一致:实例的延迟对应实例的线程、允许 CPU 和配额;主机的 CPU 汇总只提供背景。把不同范围的数据混在一起,很容易出现“监控显示空闲,应用却一直排队”的表面矛盾。

8 案例:订单 API 偶发毛刺与调度线索

某订单 API 平时 p99 为 90 ms,每隔一段时间上升到 260 ms,错误率基本不变。促销流量并未明显增加,数据库分段耗时也没有同步上升。团队先保留异常前后相同粒度的请求耗时和线程数据,发现毛刺经常与后台报表计算重叠。此时只能形成“CPU 竞争可能有关”的假设,时间重合尚不足以证明因果。

进一步检查发现,服务与报表因部署遗留配置共享较小的允许 CPU 集合;主机其他 CPU 仍有空闲。异常期间,报表线程持续消耗 CPU,订单工作线程多次出现在可运行状态。已有调度等待统计也在同一时间窗口上升。数据库耗时稳定、就绪等待增加与允许集合受限,组成了比“整机平均 CPU 升高”更具体的证据链。

团队在测试环境复现相似请求速率与报表负载,先只限制报表计算并发,保留其余配置。订单尾延迟降低,报表完成时间略有增加,符合资源竞争假设。随后评估允许 CPU 范围是否确有业务约束,再决定是否修改部署设置。把两个动作分开,便于知道改善来自何处,也便于出现回归时恢复原配置。

结果仍需在多个负载窗口验证。对照要包含请求速率、请求类型、订单成功量、p95 与 p99、报表完成时限,而不只是 1 张 CPU 图。低峰时 p99 回落不能单独证明修改有效;如果报表被无限拖延,也不算完整修复。最终记录应说明异常触发条件、证据、变更范围与回退条件,让后续容量评估能够重用。

订单毛刺的证据与验证路径
订单毛刺的证据与验证路径

这个案例没有要求提高订单服务优先级,也没有要求更换调度算法。它展示的是把“谁在竞争、可以在哪里运行、等待发生在哪段时间”连成因果假设。若后续发现就绪等待并未增加,便应撤回该假设,转向应用队列或外部依赖;不能因为已经学了调度,就把所有毛刺都解释成调度问题。

9 排查案例:CPU 空闲却延迟高

另一个订单实例出现持续高延迟,整机 CPU 大部分时间空闲。先看线程数据,多数工作线程处于睡眠状态,CPU 热点不明显;单核视图也没有发现明显饱和。请求分段计时显示,进入业务处理前的等待增加。这个组合提示应用可能缺少可用工作线程,但仍需查清工作线程正在等待什么,不能从睡眠状态直接推断数据库故障。

线程等待线索与连接池指标表明,工作线程在等待数据库连接;已取得连接的请求又因慢查询长时间不释放。后来请求停在应用队列,调度器面对的可运行工作并不多。此时提高 nice 待遇无法让连接更快释放,增加 CPU 也不能直接缩短慢查询。团队应沿连接占用时间、查询耗时与并发来源继续调查,并先按既定方案控制进入系统的工作量。

与此不同,如果大批线程持续可运行,却只允许在 1 个繁忙 CPU 上执行,主机同样可能显示总体空闲。这是允许集合限制下的局部竞争;如果服务因 CPU 配额用尽而被节流,则还要核对节流计数与配额周期。两者都可能让整机空闲与服务等待同时存在,修复位置却不同,因此“CPU 空闲”必须连同观测范围一起陈述。

可以按可执行资格组织判断:请求尚在应用队列,就看工作线程为何不能接单;线程在阻塞,就看它等待的事件与依赖;线程已就绪但没有及时运行,就看允许集合、竞争者和资源限制。每个分支都先找证据,再选动作。如果观测分辨率不足以区分,记录不确定性并补充针对性采样,比凭平均 CPU 猜测更有效。

CPU 空闲但延迟高的分支判断
CPU 空闲但延迟高的分支判断

最终,这个实例通过处理慢查询来源并控制连接占用恢复,验证时保持请求负载可比,同时观察连接池等待、订单尾延迟和错误率。调度排查在这里的价值,是确认 CPU 暂时没有足够可执行工作,并把问题交给正确的等待链分析。排除某条路径同样是进展,但前提是证据足够覆盖异常时间窗口,而不是仅凭故障后的平静快照。

补一句可操作的判断:若订单进程在连续样本里 %CPU 不高、却仍报超时,不要立刻调高 nice。先对齐线程状态(可运行排队 vs 睡眠)、该时段允许使用的 CPU、配额/限制,以及请求分段耗时,判断等待更像调度排队、I/O、锁,还是外部依赖;把「调度看起来很闲」写成可复核证据,再决定是否扩容线程或优化等待路径。

放进方案里

在订单 API 的技术方案中,把调度写成可以检查的资源假设。先注明目标请求速率、典型与高峰请求组合,以及 p95、p99 延迟目标;再说明服务预期有多少可运行工作、允许使用哪些 CPU、是否受 CPU 配额限制、后台计算是否共享资源。区分主机容量、服务可用容量和应用并行度,避免用“机器还有空闲”代替可用性证明。

观测部分约定业务与系统数据采用可对齐的时间窗口。业务侧保留请求排队、关键计算和外部依赖的分段耗时;系统侧保留进程与线程 CPU、状态、每核负载以及适用的配额节流指标。出现尾延迟毛刺时,先判断工作停在应用队列、阻塞等待还是就绪等待,再决定补采哪类证据。字段应注明统计范围和采样口径,防止跨实例或跨时间比较。

资源策略优先写清后台任务的并发边界与完成时限。若调整 nice、亲和性或配额,应记录原值、目标对象、调整理由与回退条件,并确认订单服务依赖的线程不会被连带压制。普通服务默认沿用经过验证的调度策略;任何更特殊的配置,都应由可复现的问题和对照结果支持。

验收使用相同负载比较变更前后的订单完成量、错误率、尾延迟和后台任务进度,同时覆盖持续高峰与短时突发。只有订单变快而后台工作无限积压,不能算目标达成。方案还要说明容量不足时如何限流、何时扩容,以及谁负责处置资源竞争。这样,调度检查就接到了 P3 的非功能需求:每项资源判断都有业务目标、观测依据和可执行的响应动作。

10 小结:走向 I/O 与中断(H13)

调度决定就绪任务何时获得 CPU;阻塞任务首先需要等待条件满足。唤醒不保证立即执行,平均 CPU 空闲也不保证某个服务拥有足够可用算力。时间片帮助理解响应与切换开销的权衡,nice 表达相对权重,亲和性限制运行位置,它们都不是端到端延迟的保证。

把本课收束为 5 项检查:请求停在哪个队列;线程是否具备执行资格;哪些任务在相同 CPU 范围内竞争;观测字段的时间与范围是否一致;变更是否同时满足订单延迟与后台工作目标。先回答这些问题,再讨论扩容、并发与调度参数。

自测时尝试解释 3 个现象:线程已被唤醒为什么仍可能等待;主机有空闲 CPU 为什么服务仍可能排队;所有订单线程都在等连接时为什么提高优先级通常无效。能把答案接到请求时间线与证据来源,就可以继续把调度清单用于容量设计和非功能需求验收。

下一课转向 H13 I/O 与中断。

把本章的调度验收带走:就绪队列证据、时间片与交互延迟的关系、nice/亲和性的权限边界,以及「CPU 看起来很闲但请求仍慢」时要核对的线程状态与等待类别。下一课解释中断与 I/O 如何把进程推进或卡住,并检查调度空闲是否其实在等磁盘、网卡或锁。

作业:保留本课 1 组 top/ps 连续样本与 1 条毛刺时间线,作为 H13 对照「中断/I/O 等待」时的基线;不要只用单次快照下结论。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务