上一篇 《我们差点自己做了一套租户调度系统》 停在一个很关键的判断上:我们不想把主要精力花在另一套控制面上。可问题并没有因此消失。租户越来越多以后,采集链路到底怎么承载,还是得给出具体答案。

这篇往前走一步,只回答承载分工这件事:哪些租户先进共享池,哪些租户必须单独拉出来,MQ 和平台层该接哪一层,业务自己还得扛哪一层。

先把承载分工拆开,再谈调度还是 MQ

我后来越来越觉得,“做调度还是上 MQ”这个问法本身就容易把路带偏。

因为卡住的地方,通常不是少了一个更聪明的分配器,是下面这些事没有拆清:

  • 大多数普通租户到底该怎么被承接。
  • 头部租户什么时候要单独拉出来。
  • 哪些问题交给消息系统和平台层,哪些问题还得业务自己兜。

如果这几件事没拆开,就算再加一个调度器、再多几个 topic、再多一组 consumer 副本,最后也很容易只是补丁。

如果把这层分工先压成一张图,大概是这样:

flowchart LR
  longTail["长尾租户"]
  headTenant["头部租户<br/>特殊 SLA"]

  subgraph bearing["承载面"]
    sharedPool["共享池<br/>标准化承载"]
    dedicatedPool["专属池<br/>独立预算 / 独立阈值"]
  end

  subgraph standard["MQ 与平台层"]
    mq["MQ<br/>消息持久化 / 重新消费"]
    platform["平台层<br/>消费进度 / 订阅协作 / 常规扩缩容 / 自动接入"]
  end

  governance["业务治理<br/>拆池规则 / 限流熔断 / 重试预算 / 租户级可观测性"]
  downstream["下游系统<br/>平台写入"]

  longTail -->|默认进入| sharedPool
  headTenant -->|达到拆池条件| dedicatedPool
  mq -.->|标准化消息流| sharedPool
  mq -.->|独立消费组| dedicatedPool
  platform -.->|进度 / 副本 / 接入| sharedPool
  platform -.->|进度 / 副本 / 接入| dedicatedPool
  governance -.->|谁继续共享| sharedPool
  governance -.->|谁需要单独保护| dedicatedPool
  sharedPool -->|采集 / 写入| downstream
  dedicatedPool -->|采集 / 写入| downstream

共享承载面先接住长尾

对多租户采集来说,第一步通常不是把每个租户都做成独立承载面,而是先给大多数普通租户一个稳定的共享池。

原因很现实。长尾租户数量多、流量散,如果每个租户都长期占着独立实例,资源利用率和维护成本会一起变差。共享承载面的价值,在于它先把大部分问题压回常规动作里。

共享池至少能换来几样很实际的东西:

  • 租户接入不再默认跟着新实例走。
  • 长尾流量可以在同一套标准化承载面里消化。
  • 扩容动作更容易退化成常规加副本,而不是给某个租户单独做一轮迁移。
  • 日常管理开始从“按租户逐个处理”变成“先管好一类承载面”。

当然,代价也得承认。共享池的隔离天然不如专属池,某个租户抖得厉害时,别的租户可能会被带到。所以共享池从来不意味着把所有租户永久混在一起——它先接住长尾,再把确实扛不住的租户往外拆。

头部租户什么时候拆到专属池

共享池稳定以后,下一步要回答的,是少数头部租户什么时候必须单独拉出来。

不过这里不能只写一句“看 backlog 和流量”就算交代完。租户是不是头部,单看某个指标远远不够。更关键的是,它放在共享池里以后,会不会持续把别的租户一起带乱。

我们当时更在意的是几类连续信号,而不是某一次尖峰:

  • 持续高流量,而不是偶尔一次冲高。
  • 下游依赖特殊,失败模式和别人完全不是一个脾气。
  • SLA 明显更高,或者商业价值高到不能继续跟长尾共池。

落到判断时,我们看的更多是一段时间里的整体状态:这个租户是不是经常把消费堆积顶上去,是不是一遇到下游抖动就把重试和回压放大,是不是需要单独调限流、扩容和告警阈值。如果这些动作已经开始频繁按租户单独做了,那它其实就已经不适合再继续留在共享池里了。

说白了,头部租户的识别标准不是“绝对多大才算大”,而是“它是否已经把共享承载面逼成了半专属运维”。一旦到了这一步,就别再硬留。把它迁到专属承载面,给独立消费组、独立资源预算、独立告警阈值,很多问题会立刻清楚不少。

这步一走,压力一下子小很多。

我后来不太认同“全量专属化”这条路,也是因为它会把系统重新拖回“实例数跟租户数一起长”。真正重的租户当然应该拆出来,但大多数普通租户还是应该留在标准化承载面里。

而且我们这里还有一个很现实的前提:最开始本来就是一租户一 Pod。所以很多判断都是沿着原来的单租户运行状态和后来的共享池表现,一步步把确实扛不住的租户筛出来。

MQ 和平台层更适合接住标准化承载

承载面一旦拆开,MQ 才开始发挥作用。原因很简单:消息分发、消费进度和副本协作这些事,本来就更适合放在消息系统和平台层,而不是再自己做一层。

在我们的链路里,MQ 和平台层更适合接住这些职责:

  • 消息持久化。
  • 消费进度管理。
  • 订阅关系和副本协作。
  • 消费者挂掉后的重新消费。
  • 常规副本扩缩容。
  • 新承载面出来后的自动接入。

租户治理这层还得业务自己扛

但也别把 MQ 想得太万能。它擅长接住标准化消息流,不会自动替你做租户治理。

业务侧自己还得把这些事扛住:

  • 哪些租户该拆池,哪些租户继续共享。
  • 租户级并发上限和资源预算。
  • 限流、熔断和降级策略。
  • 重试预算和死信处理。
  • 写入端的幂等、背压和回放。
  • 租户级可观测性。

这条路在我们这里为什么落得下去

这条路能推,不等于它没有代价。

你不开一套强动态分配器,就得接受更静态、或者半静态的承载切法。它没那么聪明,不过行为通常更可预测。这就够了。代价是灵活性会降下来,很多事不会再靠一层控制面临场调度,而要提前把规则定清楚。

而且命名、订阅和拆池规则会变成长期治理动作。少了一层强调度控制面,不代表日常维护就消失了,只是维护对象换了:哪些租户进共享池,哪些租户单独拆,承载面怎么命名,哪些阈值触发迁移,这些都要长期有人盯。

头部租户也还是得单独处理。再好的共享池,也不会自动吞掉极端不均衡负载。极端的尖峰租户、特殊 SLA 租户,最后还是要拆出来。

这条路在我们这里能落下去,还有一个经常会被忽略的现实原因:历史包袱刚好帮了忙。我们一开始并没有做高密度共享,先经历的是一租户一 Pod 的阶段,所以租户级部署、迁移、单独扩缩容的基础能力都已经在。等到要做承载分层时,要处理的就是“哪些租户继续专属,哪些租户开始共享”,不是重新发明一套迁移能力。

这会让迁移问题一下子小很多。因为当时租户总量并不算夸张,一百多个租户里,需要长期盯着的头部租户只有二十来个。于是运维模型也就变成了两层:上面是一小批需要单独看图、单独调阈值、单独做资源预算的头部租户,下面是一批可以按统一规则承载的共享租户。这个分层一旦成立,后面的治理动作就不再是对一百多个租户逐个操作,而是长期维护二十来个重点对象,加一套共享池规则。

这套拆法也有明确边界。如果你的产品本身就是调度平台、作业平台或资源编排平台,那控制面本来就是主系统,不该强行往“共享池 + MQ”上收。那种场景里,消息系统只是一部分部件,不是主轴。

最后留下的分工

回头看,采集承载要先想清的是租户怎么分层、谁来接、出了问题怎么拆开处理。这个问题比“要不要再做一套更聪明的系统”更靠前。

共享池先接住长尾,专属池专门兜头部租户,MQ 和平台层接住标准化的分发与进度问题,业务侧自己负责租户治理。先把这几层分工理顺,后面的设计才不容易一上来又长成另一套调度平台。