深蓝黑背景品牌封面:左侧大标题「优先级与公平性」,副标题「共享存储的 IO 调度怎么设计」;右侧原创装饰——三条租户通道汇入一台调度器,通道粗细不同寓意权重不同,调度器出口连接存储圆柱;左上标注「历史题目复盘 · 系统设计」,右上标注「Day 05 · 10-03」 图 1:本文封面,点明场景「共享存储的 IO 调度」。

先说明属性:这是一篇历史题目复盘,题目出自本地历史面试资料(云计算大数据场景:每天成千上万的任务读写存储,多个作业或用户共享系统时,如何保证优先级和公平性),与任何公司的当前招聘无关。这是一道开放式设计题,没有标准答案,考察的是你把一对矛盾目标拆成机制组合的能力。

先把矛盾摆清楚。「优先级」要求区别对待:在线业务的 IO 要先于离线备份完成。「公平性」要求不搞特殊:每个租户都该拿到自己那份吞吐。偏心任何一边,另一边就出事——绝对公平会把在线业务拖垮,绝对优先会把低优任务饿死。

**核心结论:用权重把优先级量化,用份额把公平兜底,用限速把突发削平,用监控把承诺闭环。**四件套缺一不可。

第一层,给任务打标签,把优先级变成权重。

调度器不认「很重要」这种描述,只认数字。按业务把 IO 分级:在线业务高优、分析任务普通、备份清理类后台,然后给每级配权重,比如 5:3:2。权重的含义是:设备过载时,空闲带宽按这个比例分配。优先级由此从口号变成了可计算的份额。

第二层,加权队列,而不是一个队列排到底。

所有请求进同一个 FIFO,优先级就失效了;按优先级严格抢占,低优就永远轮不到。工程上的主流解法是加权公平调度:每个租户一个队列,调度器按权重轮询取请求下发——平滑加权轮询(SWRR)是其中最简单的一种,每轮给各队列的计数器加上权重,取当前值最大者服务,再扣掉总权重。它保证长期运行下,各租户拿到的服务比例收敛于权重比。

第三层,令牌桶限速,把突发摊平到时间维度。

权重解决的是「长期怎么分」,不解决「瞬间冲垮」。某个租户一分钟内灌进一万个 IO,哪怕它权重低,瞬时压力照样打满设备。在每个队列前挂一个令牌桶:按约定速率发令牌,没令牌的请求排队等。突发被削成匀速,调度器看到的永远是平滑流量。

第四层,保底份额加监控回路,让公平可观测。

「公平」的正确含义是按权重分配,不是绝对平均——但低优租户必须有保底份额,绝不允许被饿死。光配置还不够,要持续统计每个租户实际达成的 IO 份额,和承诺份额比对,偏差过大就告警并动态纠偏。调度策略不可观测,等于没有策略。

实测:权重承诺能不能兑现?

下面用平滑加权轮询模拟三个租户(权重 5:3:2)在设备过载时的分配结果,Node.js 22.15.0 直接运行(node io-scheduler.js):

'use strict';

// 平滑加权轮询(SWRR)模拟多租户共享存储的 IO 调度
const tenants = [
  { name: '在线业务', weight: 5, current: 0, served: 0 },
  { name: '分析任务', weight: 3, current: 0, served: 0 },
  { name: '备份清理', weight: 2, current: 0, served: 0 },
];
const totalWeight = tenants.reduce((sum, t) => sum + t.weight, 0);

// 设备每轮只能处理 1 个 IO,三个租户队列里始终有积压(过载场景)
const ROUNDS = 100000;
for (let i = 0; i < ROUNDS; i++) {
  for (const t of tenants) t.current += t.weight; // 每轮先加权重
  const picked = tenants.reduce((a, b) => (b.current > a.current ? b : a)); // 取当前值最大者
  picked.current -= totalWeight; // 被选中后扣掉总权重
  picked.served += 1;
}

console.log('过载 100000 个 IO 时隙的分配结果:');
for (const t of tenants) {
  const promised = ((t.weight / totalWeight) * 100).toFixed(1);
  const achieved = ((t.served / ROUNDS) * 100).toFixed(1);
  console.log(`${t.name}:承诺份额 ${promised}%,实际达成 ${achieved}%`);
}

// 对比:绝对平均(权重 1:1:1)时高优租户拿不到优先
const flat = tenants.map((t) => ({ ...t, weight: 1, current: 0, served: 0 }));
for (let i = 0; i < ROUNDS; i++) {
  for (const t of flat) t.current += t.weight;
  const picked = flat.reduce((a, b) => (b.current > a.current ? b : a));
  picked.current -= 3;
  picked.served += 1;
}
console.log('若绝对平均调度,在线业务实际份额:', ((flat[0].served / ROUNDS) * 100).toFixed(1) + '%');

实测输出:

过载 100000 个 IO 时隙的分配结果:
在线业务:承诺份额 50.0%,实际达成 50.0%
分析任务:承诺份额 30.0%,实际达成 30.0%
备份清理:承诺份额 20.0%,实际达成 20.0%
若绝对平均调度,在线业务实际份额: 33.3%

两个数字把问题讲透了:加权调度下实际份额与承诺分毫不差,高优拿到一半带宽;而绝对平均时,在线业务只能拿到三分之一——这就是「没有优先级的公平」对在线业务的实际伤害。

架构与设计要点图:上半部分是调度链路——多个租户的 IO 请求先按业务打优先级标签(在线业务高优、分析任务普通、备份清理类后台),进入各自的加权队列,权重如 5:3:2 决定空闲带宽的分配比例;每个队列前还有令牌桶限速器,防止单租户突发流量冲垮设备;统一 IO 调度器按权重轮询取请求下发到存储设备,并用监控回路统计各租户实际达成的 IO 份额,动态纠偏。下半部分是四条设计要点:高优可以抢占空闲容量,但不能饿死低优——每个租户有保底份额;公平的含义是按权重分配而非绝对平均;令牌桶削峰,让突发摊平到时间维度;调度策略必须可观测,用「实际份额 vs 承诺份额」的偏差做告警与反馈调节 图 2:标签分级、加权队列、令牌桶限速、监控闭环的调度链路。

常见误区。

一,把公平理解成绝对平均——它抹掉优先级,在线业务第一个受害。二,让高优无限抢占,低优长期零吞吐,等于把「优先级」做成了「拒绝服务」。三,只管配置不管达成:权重写在配置里,实际份额从不统计,调度失衡到出事才被发现。四,忽视突发:长期比例再完美,瞬时洪峰照样能把设备打挂。

这套机制的迁移面很广:GPU 推理集群的排队调度与此同构——在线推理要高优先、离线训练要保底公平,权重队列加限速的思路完全可以照搬。回到面试现场,建议的答题顺序是:先点破优先级与公平的矛盾本质,再分层给出权重、队列、限速、监控四件套,最后主动补一句「公平是按权重分配,不是绝对平均」——这句话往往是区分度所在。

你设计或维护过带优先级的调度系统吗?实际份额和承诺份额对不上的坑,你踩过吗?欢迎聊聊。