深蓝黑背景封面:右侧大标题「共享一套系统,互不拖累」,副标题「配额、限流、优先级调度与隔离的四层框架」;左侧原创示意图——三个租户色块共享一个资源池,中间立着一堵隔离墙,池子上方一只调节阀表示配额与限流;左上标注「知识专栏 · 资源隔离」,右上标注「Day 10 · 10-28」 图 1:本文封面,共享一套系统,互不拖累。

之前解析「存储系统的 IO 优先级与公平性」这道真题时,问答聚焦在单个存储系统内部;这篇把视野拉到整套多租户系统——只要多个租户共享同一套资源,「会不会互相拖累」就是必须体系化回答的问题,而不只是存储层的事。

先把结论立住:多租户资源治理是一套四层叠加防线——隔离管故障域、配额管总量、限流管速率、优先级调度管顺序。四层各回答一个问题,合起来才构成 QoS 保障;只做其中一层,总有机动车能从缺口冲过去。

**自下而上逐层看。**最底层是隔离,回答「故障会不会蔓延」:用独立进程、容器或独立线程池把租户的资源域分开,一个租户崩溃或泄漏不拖垮别人。它最硬也最贵,内核级的实现是 cgroup——Linux 官方文档对它的定义就是「把进程按层级组织起来,受控、可配置地沿层级分配系统资源」,cpu.max 限 CPU 带宽、memory.max 限内存上限、io.max 限磁盘 BPS 与 IOPS,越靠近根的约束越严格,子层级无法推翻。

**第二层配额,回答「最多能用多少」。**给每个租户划定资源上限,超出即拒绝,防止长期霸占。配额管的是「长期平均」,管不了瞬时尖峰—— tenant 可以在配额内一秒打出一整天的量。

**第三层限流,回答「瞬间能打多猛」。**令牌桶、滑动窗口这类算法削平突发,保护系统在尖峰下不超载。配额与限流的分工:一个管积分,一个管速度。

**第四层优先级调度,回答「挤的时候谁先」。**资源充足时怎么都行,争抢发生时才见高下:按租户等级与任务紧急度排序,让高优先级在争抢中先拿到资源。cgroup v2 的 cpu.weight 就是按比例给活跃子级分 CPU 的同款思路。

分层架构图:自下而上四层,每层回答一个问题——最底层「隔离」回答「故障会不会蔓延」:用独立进程、容器或线程池把租户的资源域分开,一个租户崩溃不拖垮别人,最硬也最贵;其上「配额」回答「最多能用多少」:给每个租户划定资源上限,超出即拒绝,防止长期霸占;再上「限流」回答「瞬间能打多猛」:用令牌桶等算法削平突发流量,保护系统在尖峰下不超载;最上层「优先级调度」回答「挤的时候谁先」:按租户等级与任务紧急度排序调度,让高优先级在争抢中先得到资源。每层右侧标注失控后果:无隔离则一崩全崩,无配额则被长期挤占,无限流则尖峰打穿,无调度则关键任务排队挨饿。底部提示:四层不是互斥选型而是叠加防线——隔离管故障域、配额管总量、限流管速率、调度管顺序,合起来才构成 QoS 保障 图 2:四层防线,每层各管一件事。

没有治理时「互拖累」有多惨、四层治理又能救回多少?用 Node.js 22.15.0 跑一个最小仿真:一台共享服务每时间片处理 1 个任务,在线租户 A 每片约 0.2 个任务,批量租户 B 每片固定灌 2 个(node qos.js):

'use strict';

// 可复现随机数
function mulberry32(seed) {
  return function () {
    seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// 场景:一台共享服务,每个时间片只能处理 1 个任务。
// 租户 A(在线交互)流量小但要求快;租户 B(批量回填)流量大且随时打满。
const SLICES = 20000;

function simulate(mode) {
  const rand = mulberry32(20261028);
  const queueA = []; // [到达时刻]
  const queueB = [];
  const latencyA = [];
  let servedB = 0, droppedB = 0;
  // 限流参数:B 的令牌桶,容量 50,每片补 0.6 个令牌(配额:长期最多用六成)
  let tokens = 50;
  for (let t = 0; t < SLICES; t++) {
    if (rand() < 0.2) queueA.push(t);
    if (mode === 'fifo') {
      queueB.push(t, t);
    } else {
      // 治理模式:限流挡尖峰 + 配额压均值 + 优先级调度
      tokens = Math.min(50, tokens + 0.6);
      for (let k = 0; k < 2; k++) {
        if (tokens >= 1) { tokens -= 1; queueB.push(t); }
        else droppedB++;
      }
    }
    // 服务一个任务:fifo 按到达顺序;治理模式按优先级(A 永远先走)
    if (mode === 'fifo') {
      if (queueA.length && (queueB.length === 0 || queueA[0] <= queueB[0])) {
        latencyA.push(t - queueA.shift());
      } else if (queueB.length) { queueB.shift(); servedB++; }
    } else {
      if (queueA.length) latencyA.push(t - queueA.shift());
      else if (queueB.length) { queueB.shift(); servedB++; }
    }
  }
  latencyA.sort((a, b) => a - b);
  const p50 = latencyA[Math.floor(latencyA.length * 0.5)];
  const p99 = latencyA[Math.floor(latencyA.length * 0.99)];
  return { p50, p99, servedB, droppedB, backlogB: queueB.length, unfinishedA: queueA.length };
}

const fifo = simulate('fifo');
const qos = simulate('qos');
console.log('租户 A(在线)每片约 0.2 个任务,租户 B(批量)每片 2 个任务,系统容量 1 个/片');
console.log(`无治理 FIFO:A 排队中位 ${fifo.p50} 片,P99 ${fifo.p99} 片;B 队列末尾还压着 ${fifo.backlogB} 个`);
console.log(`四层治理后:A 排队中位 ${qos.p50} 片,P99 ${qos.p99} 片;B 被限流拒绝 ${qos.droppedB} 个、完成 ${qos.servedB} 个、残压 ${qos.backlogB} 个`);
console.log(`A 是否有请求没跑完:fifo=${fifo.unfinishedA > 0},qos=${qos.unfinishedA > 0}`);

实测输出:

租户 A(在线)每片约 0.2 个任务,租户 B(批量)每片 2 个任务,系统容量 1 个/片
无治理 FIFO:A 排队中位 5308 片,P99 10781 片;B 队列末尾还压着 21831 个
四层治理后:A 排队中位 0 片,P99 0 片;B 被限流拒绝 27951 个、完成 12049 个、残压 0 个
A 是否有请求没跑完:fifo=true,qos=false

无治理时 B 的持续超载把队列无限拉长,A 的请求排在两万个批量任务后面,中位等待五千多个时间片,仿真结束时还有请求没轮到。治理后 B 被令牌桶压到长期六成容量以内,A 借优先级随到随走。这不是「优先级高所以快」的玄学,是限流先把队伍长度封了顶,优先级再决定谁先。

**常见误区。**一是只做限流不做隔离,一个租户的 bug(死循环、内存泄漏)照样拖垮全局;二是把配额当限流用,月配额挡不住秒级尖峰;三是优先级不设上限,低优先级任务饿死,需要配 aging 或保底份额;四是上了容器就以为有了隔离,共享内核、共享网卡队列的邻居噪声仍在,cgroup 的各项 controller 要逐项配置才生效。

落到行动上:评估一个多租户方案时,按四层各问一句——故障域怎么切、总量怎么封、尖峰怎么削、争抢谁优先。答不上来的那一层,就是下一次事故的入口。你维护的系统里四层各用了什么实现?欢迎分享。