返回文章列表

AiVedio:高并发调度与资源隔离

从准入控制、分层队列、WRR/DRR、公平调度、资源池隔离、多级并发限制、Provider Semaphore、Worker Slot、资源预留、原子认领、Fencing Token、大客户容量、成本估算、背压和降级出发,设计 AI 视频平台的高并发调度与多租户资源隔离。

第 18 章:高并发调度与资源隔离

18.1 本章要解决的业务问题

AI 视频平台的高并发,与普通 Web 系统的高 QPS 不完全相同。

一次生成请求可能只需要几十毫秒完成任务创建,但任务会在系统中持续数十秒甚至数分钟。在此期间,它会持续占用一种或多种稀缺资源:

  • 第三方供应商并发槽位;
  • 模型级 RPM、并发数或账户额度;
  • 平台分钟级成本预算;
  • GPU、CPU、内存和临时磁盘;
  • 回调处理、轮询和媒体回源能力;
  • 转码、代理视频和最终渲染 Worker。

因此,系统真正需要控制的是:

在途任务量
排队任务量
单位时间调度成本
不同租户的资源份额
不同资源池的占用率
任务最大等待时间

如果只把所有任务放进一个全局 FIFO 队列,会出现以下问题:

  1. 某个大客户瞬间提交数万条任务,占满全部队列和供应商并发。
  2. 普通用户的短任务被企业客户的长任务长期阻塞。
  3. 4K、长视频等高成本任务与普通任务共用 Worker,导致普通任务延迟激增。
  4. 高优先级任务持续进入后,低优先级任务永久饥饿。
  5. 调度器无限消费 RocketMQ 消息,却没有可用供应商槽位,最终造成大量本地堆积和重试风暴。
  6. 第三方调用已经成功但本地超时,系统提前释放槽位并再次提交,造成并发超限和重复计费。
  7. Redis 计数丢失后,多个调度器同时认为还有容量,向供应商发送远超配额的请求。

本章要解决的核心问题是:

在大量、长耗时、成本不同且通常不可抢占的任务下,如何同时保证平台稳定、租户公平、付费等级差异、资源隔离和成本可控。


18.2 核心设计原则

原则一:先准入,再排队,最后执行

生成请求通过参数校验,不代表系统必须无条件接收。

系统需要依次判断:

请求是否允许进入系统
→ 是否允许进入某个队列
→ 当前是否允许被调度
→ 是否存在可以执行它的资源

准入控制、排队和执行是三个不同阶段。

原则二:高并发控制必须分层

至少需要以下几层限制:

层级典型限制
API 层用户请求速率、租户请求速率
业务层每日额度、分钟成本、最大排队量
调度层每租户运行任务数、服务等级并发
供应商层Provider、模型、地区、账户并发
Worker 层CPU、GPU、显存、磁盘、进程槽位
平台层全局成本、数据库连接、MQ 消费速度

任何单一限流器都无法覆盖全部风险。

原则三:按资源消耗单位调度,而不是只按任务数调度

下面两个任务不能简单视为同样的“一条任务”:

任务 A:5 秒、720p、普通文生视频
任务 B:60 秒、4K、参考视频驱动、带音频

系统需要为任务估算:

fairness_units       公平调度单位
provider_slot_units  供应商槽位单位
gpu_units            GPU 或显存单位
media_units          转码资源单位
estimated_cost       预计真实成本

调度公平可以使用一个归一化单位,但实际资源准入仍应按各自维度分别判断。

原则四:采用分层公平调度

推荐的调度层次是:

第一层:选择服务等级或资源池
第二层:选择租户
第三层:选择租户内部任务
第四层:选择供应商或 Worker

不能把企业客户、付费个人用户、免费用户和高成本任务全部放在一个全局优先级队列中。

原则五:PostgreSQL 管事实,Redis 管速度

  • PostgreSQL 保存任务状态、调度尝试、资源预留和策略版本。
  • Redis 保存限流计数、活跃租户索引、队列加速索引和容量镜像。
  • RocketMQ 负责异步传递、削峰、延迟调度和重试。
  • 对象存储负责视频、图片和中间媒体文件。

Redis 中的数据必须能够从 PostgreSQL 重建。

原则六:先完成资源预留和任务认领,再产生外部副作用

调用供应商前,系统必须先确定:

  • 当前任务仍处于可调度状态;
  • 当前租户仍有运行配额;
  • 当前供应商仍有容量;
  • 当前成本预算仍允许调用;
  • 本次 attempt 已经持久化。

不能先调用供应商,成功后再尝试认领任务。

原则七:外部视频生成任务通常按不可抢占任务处理

很多供应商即使支持取消,也不保证:

  • 取消立即生效;
  • 已消耗的计算不会收费;
  • 已提交任务能够释放并发;
  • 取消与完成之间没有竞态。

因此,平台不能依赖强制抢占来保障高级用户,而应通过预留容量、独立资源池和准入控制实现隔离。


18.3 总体架构和组件职责

                         ┌──────────────────────┐
Client / Open API ──────▶│ API Gateway          │
                         │ Auth / Rate Limit     │
                         └──────────┬───────────┘


                         ┌──────────────────────┐
                         │ Admission Controller │
                         │ 配额、队列、成本准入    │
                         └──────────┬───────────┘

                           PostgreSQL Transaction

                    ┌───────────────┴────────────────┐
                    ▼                                ▼
          GenerationTask / Ledger               OutboxEvent
                    │                                │
                    │                                ▼
                    │                            RocketMQ
                    │                                │
                    ▼                                ▼
          Queue Index Rebuilder  ───────────────▶ Redis Index


                                            Scheduler Replicas
                                         ┌───────────┴──────────┐
                                         │ 公平调度与资源选择      │
                                         └───────────┬──────────┘

                                          PostgreSQL 原子预留


                                                Dispatch MQ
                                          ┌──────────┴──────────┐
                                          ▼                     ▼
                                  Provider Dispatcher     Media Worker
                                          │                     │
                                          ▼                     ▼
                                  第三方 AI Provider       FFmpeg / GPU
                                          │                     │
                                          └──────────┬──────────┘

                                              Completion Event


                                               Slot Release

18.3.1 Admission Controller

负责判断请求能否进入平台。

主要检查:

  • 用户和租户请求速率;
  • 最大排队任务数;
  • 最大排队资源单位;
  • 当前最老任务等待时间;
  • 预计排队时间;
  • 用户剩余额度;
  • 租户分钟成本预算;
  • 当前服务等级是否已关闭准入;
  • 高成本任务池是否还有空间。

它不负责真正调用供应商。

18.3.2 Queue Indexer

任务事实保存在 PostgreSQL,但每次调度都全表扫描会增加数据库压力,因此可以在 Redis 中维护可重建的排队索引:

活跃服务等级
服务等级中的活跃租户
租户内部的优先级队列
租户的 DRR deficit

Queue Indexer 可以消费 Outbox 事件,将任务加入 Redis 索引。索引更新失败时,由定期扫描器补偿。

18.3.3 Fair Scheduler

负责:

  1. 选择服务等级;
  2. 选择租户;
  3. 选择租户内的具体任务;
  4. 匹配供应商或 Worker;
  5. 发起数据库资源预留事务。

Scheduler 不直接把 Redis 中弹出的任务当作已认领任务。

18.3.4 Resource Manager

资源池不应只有一个全局并发数,而应至少按以下维度拆分:

provider
provider_account
region
model
resource_class
worker_pool
GPU 类型

例如:

provider:runway:model-x:region-us
provider:vendor-b:model-y:region-ap
worker:proxy-transcode:cpu
worker:render-4k:gpu-a100

18.3.5 Provider Dispatcher

只处理已经完成资源预留和 attempt 创建的任务。

负责:

  • 构造供应商请求;
  • 携带供应商幂等键;
  • 设置超时;
  • 记录原始响应;
  • 区分明确失败和结果未知;
  • 更新 provider_job_id
  • 启动回调或轮询跟踪。

18.3.6 Reconciler

负责修复分布式系统中的不一致:

  • 过期但未释放的资源预留;
  • 长时间处于 DISPATCHING 的任务;
  • 供应商已经存在但本地没有记录的任务;
  • Redis 队列索引与 PostgreSQL 状态不一致;
  • 资源计数与有效预留不一致;
  • MQ 发送失败的 Outbox;
  • 长时间没有调度机会的饥饿任务。

18.4 服务等级与资源池设计

推荐至少划分以下逻辑队列:

队列典型用户调度策略
ENTERPRISE_RESERVED有合同 SLA 的企业客户预留容量和硬等待时间目标
PAID_SHARED普通付费用户权重公平、允许弹性扩容
STANDARD普通用户标准公平调度
BEST_EFFORT免费或活动用户仅使用剩余容量
EXPENSIVE4K、长时长、高成本模型独立并发和成本预算
RECOVERY对账、补偿、未知任务恢复小容量但不可完全阻塞

资源隔离不一定意味着每个队列都部署一整套独立集群。

可以采用三种程度:

逻辑隔离

共用服务实例,但使用独立的:

  • 队列;
  • 配额;
  • semaphore;
  • 成本预算;
  • 调度权重。

进程隔离

不同队列使用不同的:

  • RocketMQ Consumer Group;
  • Worker Deployment;
  • Go worker pool;
  • HTTP 连接池;
  • 数据库连接池配额。

计算节点隔离

对于高成本或高风险任务,进一步使用:

  • 独立 Kubernetes Namespace;
  • 独立 Node Pool;
  • GPU 节点标签和 taint;
  • CPU、内存和临时磁盘配额;
  • 独立网络出口。

Kubernetes 的 ResourceQuota 可以限制 Namespace 聚合资源消耗,PriorityClass 可以表达 Pod 相对重要性及是否允许抢占低优先级 Pod;但视频生成任务是否能被安全抢占,仍需由业务状态机决定,不能直接等同于 Pod 调度优先级。(Kubernetes)


18.5 多级并发限制

18.5.1 推荐限制矩阵

限制项示例
单用户提交速率5 次/分钟
单租户提交速率100 次/分钟
单用户排队任务20
单租户排队任务5,000
单用户运行任务2
单租户运行任务100
模型全局运行任务800
供应商账户并发200
高成本任务并发20
代理转码槽位400 units
4K 渲染槽位32 GPU units
平台分钟成本10,000 cost units
单租户分钟成本1,000 cost units

具体值必须通过压测、供应商合同和线上指标确定。

18.5.2 任务数限制与资源单位限制应同时存在

仅限制任务数会被高成本任务绕过。

例如:

普通任务:1 unit
1080p 长视频:3 units
4K 视频:8 units
复杂视频到视频任务:12 units

租户可能允许:

max_running_tasks = 100
max_running_units = 240

这样即使任务数未达到 100,高成本任务也无法无限进入。

18.5.3 Provider Semaphore

供应商 semaphore 应按真实配额边界划分:

provider + account + region + model

不能把所有供应商模型共用一个 semaphore,也不能只在单个 Go 进程中使用本地 channel 控制。

本地 semaphore 只能限制当前 Pod,无法限制所有调度器实例的总并发。

18.5.4 Worker Slot

Worker slot 应表达资源消耗,而不是简单表达进程数。

例如:

720p 代理视频:1 CPU unit
1080p 代理视频:2 CPU units
4K 转码:6 CPU units
普通 GPU 渲染:1 GPU unit
高显存渲染:2 GPU units

Worker 启动任务前需要确认:

  • 节点 CPU;
  • 内存;
  • GPU 和显存;
  • 临时磁盘;
  • 输出存储空间;
  • 单任务超时;
  • 最大 FFmpeg 进程数。

18.6 WRR、DRR、优先级与 Aging

18.6.1 Weighted Round Robin

Weighted Round Robin 按配置权重轮询租户。

例如:

Tenant A:weight = 4
Tenant B:weight = 2
Tenant C:weight = 1

理想情况下,一个轮次可以近似调度:

A、A、A、A、B、B、C

WRR 适用于任务成本比较接近的场景。

如果 A 的任务平均耗时 30 秒,而 B 的任务平均耗时 5 分钟,仅按任务数轮询并不公平。

18.6.2 Deficit Round Robin

DRR 为每个租户维护一个 deficit

每轮增加:

quantum_i = base_quantum × tenant_weight_i
deficit_i = min(deficit_i + quantum_i, deficit_cap_i)

只有当任务成本不大于当前 deficit 时才能执行:

task.cost_units <= tenant.deficit

成功调度后:

tenant.deficit -= task.cost_units

DRR 最初用于不同报文大小下的公平排队,其关键价值是把“服务数量”转换为“服务成本”,并让未用完的配额能够跨轮次累积。

18.6.3 AI 视频任务中的 DRR 示例

假设:

base_quantum = 4

Tenant A:
weight = 2
quantum = 8

Tenant B:
weight = 1
quantum = 4

当前任务:

A1.cost = 8
A2.cost = 2
B1.cost = 4
B2.cost = 4

第一轮:

A deficit += 8,调度 A1,剩余 0
B deficit += 4,调度 B1,剩余 0

第二轮:

A deficit += 8,可调度多个小任务或一个大任务
B deficit += 4,调度 B2

长期来看,A 获得约两倍于 B 的资源份额,但不会简单按照任务条数计算。

18.6.4 防止 deficit 积累造成突发占用

当一个租户长期没有任务时,不能让 deficit 无限积累。

推荐:

deficit_cap = quantum × burst_rounds

队列从空变为非空时,也可以将 deficit 重置为:

min(previous_deficit, initial_burst_credit)

否则沉默一天的租户可能在重新提交任务后瞬间抢占大量资源。

18.6.5 优先级不能替代公平调度

推荐先选择租户,再选择租户内部任务。

服务等级选择
→ 租户公平选择
→ 租户内部优先级选择

如果直接使用全局优先级队列,持续进入的企业任务可能让普通用户永久无法运行。

18.6.6 Aging

任务的有效优先级可以表示为:

effective_priority =
    base_priority
  + min(wait_seconds / aging_step, aging_cap)

但不建议每秒更新 Redis 中所有任务的 score。

更合理的实现方式是:

  1. 每个租户维护多个优先级桶;
  2. 桶内按 enqueue_at 排序;
  3. 定时将等待时间过长的任务提升到更高优先级桶;
  4. 对提升次数设置上限。

例如:

P0:紧急恢复任务
P1:企业高优先级
P2:普通付费任务
P3:标准任务
P4:Best Effort

每等待 5 分钟,可以提升一级,最多提升到 P1。

18.6.7 防止大任务队头阻塞

某租户队头任务需要 12 units,但当前 deficit 只有 8 units。

此时不能无限跳过它、只执行后面的小任务,否则大任务可能永久饥饿。

可以采用:

  • 高成本任务进入独立资源队列;
  • 允许有限次数的 fit scan;
  • 达到最大跳过次数后停止跳过;
  • 通过 aging 提高大任务优先级;
  • 让 deficit 跨轮次积累到足够大小。

18.7 任务成本估算

18.7.1 公平调度成本

可以定义一个归一化公平单位:

fairness_units = max(
    expected_provider_seconds / provider_second_base,
    expected_provider_cost / cost_base,
    expected_gpu_seconds / gpu_second_base
)

使用 max 而不是简单相加,可以近似表达任务的主要瓶颈资源。

例如:

provider_second_base = 30 秒
cost_base = 0.1 成本单位
gpu_second_base = 30 GPU 秒

18.7.2 实际资源仍应分别限制

fairness_units 只用于租户之间的公平选择。

真正执行时仍需要分别检查:

provider_slot_units
GPU units
CPU units
temporary_disk_units
cost_budget

一个标量不能完全表达多维资源约束。

18.7.3 估算误差修正

任务完成后记录:

estimated_units
actual_units
estimation_error = actual_units - estimated_units

如果长期低估某类任务,应更新模型参数。

还可以将误差转为租户后续调度债务:

tenant.fairness_debt += actual_units - estimated_units

下一轮增加 deficit 时,先抵扣 fairness debt。

为了防止单个异常任务让租户长期无法运行,应对单次修正设置上限。


18.8 文字版正常时序图

1. 用户调用创建生成任务接口,并携带 Idempotency-Key。

2. API Gateway 完成认证、请求速率限制和请求大小限制。

3. Admission Controller 获取租户策略,估算任务的:
   - fairness_units
   - provider_slot_units
   - estimated_cost
   - resource_class

4. Admission Controller 检查:
   - 用户和租户排队上限
   - 最大预计等待时间
   - 租户预算
   - 服务等级是否开放
   - 高成本任务池是否已满

5. PostgreSQL 事务中:
   - 创建 GenerationTask,状态为 QUEUED
   - 预占用户额度
   - 写入 OutboxEvent
   - 记录 policy_version 和 estimate_version

6. Outbox Relay 将 TaskQueued 事件发送到 RocketMQ。

7. Queue Indexer 消费事件,将 task_id 加入 Redis 的租户队列索引,
   并将 tenant_id 加入相应服务等级的活跃租户集合。

8. Scheduler 选择一个服务等级。

9. Scheduler 使用 DRR 选择一个租户。

10. Scheduler 在租户内部按 priority、aging 和 enqueue_at 选择任务。

11. Provider Router 根据能力矩阵、健康度、成本和可用槽位,
    选择 provider、account、region 和 model。

12. PostgreSQL 原子事务中:
    - CAS 将任务从 QUEUED 更新为 DISPATCHING
    - 增加租户 running_units
    - 增加资源池 used_units
    - 创建 resource_reservation
    - 创建 task_attempt
    - 生成 fencing_token
    - 写入 TaskDispatchRequested Outbox

13. Dispatch Worker 消费消息,调用第三方供应商。

14. 供应商明确受理后:
    - 保存 provider_job_id
    - 将任务更新为 RUNNING
    - 启动回调或轮询跟踪

15. 供应商完成后,回调或轮询服务使用 task_id、attempt_id、
    fencing_token 和 provider_job_id 更新任务。

16. PostgreSQL 事务中:
    - 将任务更新为 PROVIDER_SUCCEEDED
    - 幂等释放资源预留
    - 更新租户和资源池计数
    - 写入媒体回源事件

17. 媒体 Worker 完成回源、探测、转码和代理视频生成。

18. 任务进入 SUCCEEDED,并进行最终计费结算。

18.9 关键数据结构

18.9.1 GenerationTask

CREATE TABLE generation_task (
    task_id                 UUID PRIMARY KEY,
    tenant_id               UUID NOT NULL,
    user_id                 UUID NOT NULL,

    queue_class             TEXT NOT NULL,
    priority_band           SMALLINT NOT NULL,
    resource_class          TEXT NOT NULL,

    state                   TEXT NOT NULL,
    version                 BIGINT NOT NULL DEFAULT 0,

    model_code              TEXT NOT NULL,
    selected_provider       TEXT,
    selected_account        TEXT,

    fairness_units          BIGINT NOT NULL,
    provider_slot_units     BIGINT NOT NULL,
    estimated_cost_units    BIGINT NOT NULL,

    enqueue_at              TIMESTAMPTZ NOT NULL,
    next_eligible_at        TIMESTAMPTZ NOT NULL,
    deadline_at             TIMESTAMPTZ,

    current_attempt_id      UUID,
    policy_version          BIGINT NOT NULL,
    estimate_version        BIGINT NOT NULL,

    idempotency_key         TEXT NOT NULL,
    created_at              TIMESTAMPTZ NOT NULL,
    updated_at              TIMESTAMPTZ NOT NULL,

    UNIQUE (tenant_id, idempotency_key)
);

关键索引:

CREATE INDEX idx_task_schedulable
ON generation_task (
    queue_class,
    tenant_id,
    priority_band,
    next_eligible_at,
    enqueue_at
)
WHERE state = 'QUEUED';

18.9.2 TenantSchedulingPolicy

CREATE TABLE tenant_scheduling_policy (
    tenant_id                UUID PRIMARY KEY,
    service_class            TEXT NOT NULL,

    weight                   INTEGER NOT NULL,
    max_queued_tasks         INTEGER NOT NULL,
    max_queued_units         BIGINT NOT NULL,
    max_running_tasks        INTEGER NOT NULL,
    max_running_units        BIGINT NOT NULL,

    max_cost_per_minute      BIGINT NOT NULL,
    max_cost_per_day         BIGINT NOT NULL,

    reserved_units           BIGINT NOT NULL,
    burst_units              BIGINT NOT NULL,
    max_wait_seconds         INTEGER NOT NULL,

    policy_version           BIGINT NOT NULL,
    updated_at               TIMESTAMPTZ NOT NULL
);

18.9.3 ResourcePoolShard

资源计数如果集中在一行,可能形成数据库热点,因此可以拆成多个 shard。

CREATE TABLE resource_pool_shard (
    resource_key             TEXT NOT NULL,
    shard_no                 INTEGER NOT NULL,

    capacity_units           BIGINT NOT NULL,
    used_units               BIGINT NOT NULL,

    version                  BIGINT NOT NULL,
    updated_at               TIMESTAMPTZ NOT NULL,

    PRIMARY KEY (resource_key, shard_no)
);

资源预留使用条件更新:

UPDATE resource_pool_shard
SET used_units = used_units + $demand,
    version = version + 1,
    updated_at = now()
WHERE resource_key = $resource_key
  AND shard_no = $shard_no
  AND used_units + $demand <= capacity_units
RETURNING used_units, version;

没有返回行表示当前 shard 容量不足。

18.9.4 ResourceReservation

CREATE TABLE resource_reservation (
    reservation_id           UUID PRIMARY KEY,
    task_id                  UUID NOT NULL,
    attempt_id               UUID NOT NULL,

    resource_key             TEXT NOT NULL,
    shard_no                 INTEGER NOT NULL,
    reserved_units           BIGINT NOT NULL,

    state                    TEXT NOT NULL,
    lease_until              TIMESTAMPTZ NOT NULL,
    fencing_token            BIGINT NOT NULL,

    created_at               TIMESTAMPTZ NOT NULL,
    released_at              TIMESTAMPTZ
);

可增加部分唯一索引,防止同一 attempt 重复创建同类有效预留。

18.9.5 TaskAttempt

CREATE TABLE task_attempt (
    attempt_id               UUID PRIMARY KEY,
    task_id                  UUID NOT NULL,
    attempt_no               INTEGER NOT NULL,

    provider                 TEXT NOT NULL,
    provider_account         TEXT NOT NULL,
    provider_request_key     TEXT NOT NULL,
    provider_job_id          TEXT,

    state                    TEXT NOT NULL,
    fencing_token            BIGINT NOT NULL,

    submit_started_at        TIMESTAMPTZ,
    submit_finished_at       TIMESTAMPTZ,
    next_check_at            TIMESTAMPTZ,

    error_class              TEXT,
    error_code               TEXT,

    created_at               TIMESTAMPTZ NOT NULL,
    updated_at               TIMESTAMPTZ NOT NULL,

    UNIQUE (task_id, attempt_no),
    UNIQUE (provider, provider_request_key)
);

18.9.6 Redis Key

Key作用
rate:tenant:{id}:submit租户提交令牌桶
queue:active_tenants:{lane}当前有排队任务的租户
queue:tenant:{tenant}:{band}租户优先级桶中的任务索引
sched:deficit:{lane}各租户 DRR deficit
capacity:mirror:{resource}PostgreSQL 容量的快速镜像
queue:oldest:{lane}最老任务时间
budget:minute:{tenant}分钟成本快速计数

Redis Lua 可以原子执行多个读取和写入,但脚本执行期间会阻塞其他服务器活动,因此只能用于短小、边界明确的操作,不能在 Lua 中扫描大型队列。Redis Sorted Set 适合维护按 score 排序的可重建索引。(Redis)

18.9.7 Dispatch Message

{
  "event_id": "uuid",
  "event_type": "TaskDispatchRequested",
  "task_id": "uuid",
  "attempt_id": "uuid",
  "tenant_id": "uuid",
  "queue_class": "PAID_SHARED",
  "provider": "provider-a",
  "provider_account": "account-01",
  "model_code": "video-model-x",
  "resource_class": "video-standard",
  "fencing_token": 18423,
  "policy_version": 37,
  "aggregate_version": 12,
  "trace_id": "trace-id",
  "created_at": "2026-06-24T08:00:00Z"
}

消息中可以携带资源预留 ID,但资源预留的事实仍以 PostgreSQL 为准。


18.10 原子认领与资源预留

一次调度应在单个 PostgreSQL 事务中完成:

锁定并校验任务
→ 校验租户运行额度
→ 校验资源池容量
→ 创建资源预留
→ 创建 attempt
→ 更新任务状态
→ 写入 Outbox

建议按照固定顺序锁定资源:

tenant runtime
→ service lane
→ provider pool
→ cost budget
→ task

或者先锁任务,再按排序后的资源键锁资源。整个系统必须采用统一顺序,降低死锁概率。

简化伪代码:

func ReserveAndClaim(ctx context.Context, taskID uuid.UUID) error {
    return db.WithTx(ctx, func(tx *sql.Tx) error {
        task, err := lockQueuedTask(ctx, tx, taskID)
        if err != nil {
            return err
        }

        if err := reserveTenantCapacity(ctx, tx, task); err != nil {
            return err
        }

        if err := reserveProviderCapacity(ctx, tx, task); err != nil {
            return err
        }

        attempt, err := createAttempt(ctx, tx, task)
        if err != nil {
            return err
        }

        if err := markDispatching(ctx, tx, task, attempt); err != nil {
            return err
        }

        return insertDispatchOutbox(ctx, tx, task, attempt)
    })
}

多个调度器做数据库兜底扫描时,可以使用 FOR UPDATE SKIP LOCKED 跳过已被其他事务锁定的任务。它适合队列式认领,但返回的是有意不完整的视图,不能仅凭它实现租户公平。(PostgreSQL)


18.11 准入控制、队列深度与最大等待时间

18.11.1 准入决策

Admission Controller 可以返回:

ACCEPTED
ACCEPTED_BEST_EFFORT
REJECT_USER_RATE_LIMIT
REJECT_TENANT_QUEUE_FULL
REJECT_EXPECTED_WAIT_TOO_LONG
REJECT_COST_BUDGET
REJECT_RESOURCE_CLASS_DISABLED
REJECT_PLATFORM_OVERLOADED

对允许排队的任务,响应中可以包含:

{
  "task_id": "uuid",
  "state": "QUEUED",
  "queue_class": "PAID_SHARED",
  "estimated_start_at": "2026-06-24T08:05:00Z",
  "queue_position_level": "MEDIUM"
}

精确队列位置在公平调度系统中通常不稳定,建议返回等级或预计时间范围。

18.11.2 队列上限应以资源单位为主

同时限制:

max_queued_tasks
max_queued_units

原因是 1,000 个普通任务和 1,000 个 4K 长视频任务的资源压力完全不同。

18.11.3 最大等待时间

设某服务等级的安全服务能力为:

μ_units = 每秒可处理的公平资源单位

前方排队量为:

Q_ahead_units

则可粗略估算:

estimated_wait ≈ Q_ahead_units / μ_units

如果:

estimated_wait > tenant.max_wait_seconds

应采取以下一种策略:

  • 拒绝新任务;
  • 降级到 Best Effort;
  • 降低分辨率或模型档位,但必须取得用户同意;
  • 提示用户稍后重试;
  • 企业合同用户进入独立预留池。

18.11.4 队列最大深度

可用以下方式初步估算:

max_queue_units ≈ safe_service_units_per_second × max_wait_seconds

这只是初始容量值,线上还需要根据以下指标动态修正:

  • P95 实际生成时长;
  • 供应商失败率;
  • 不同任务成本分布;
  • 调度权重;
  • 资源池利用率;
  • 回调和轮询延迟;
  • 不可取消任务比例。

18.12 大客户与普通用户的资源隔离

18.12.1 预留容量

例如平台有 2,000 个供应商并发单位,可以划分为:

企业合同预留:500
付费共享池:1,200
Best Effort:200
恢复和运维:100

预留容量不能简单永久闲置。

18.12.2 可借用但不可立即抢占

企业池空闲时,普通付费任务可以借用一部分容量:

borrowable_enterprise_units = 200

借用任务需要标记:

capacity_mode = BORROWED

当企业流量重新上升时:

  • 停止新的借用;
  • 等待已有借用任务自然结束;
  • 不直接取消已经提交到供应商的任务;
  • 始终保留一部分未借出的企业 headroom。

这种策略能够兼顾利用率和企业 SLA。

18.12.3 独立大客户

对于有明确合同容量的大客户,可以进一步采用:

  • 独立服务等级;
  • 独立 Provider Account;
  • 独立 Consumer Group;
  • 独立 Worker Deployment;
  • 独立 Kubernetes Namespace 或 Node Pool;
  • 独立成本预算和告警。

但不建议为每个普通租户都创建独立 Topic、独立数据库表或独立 Kubernetes Namespace,否则运维对象数量会快速膨胀。


18.13 高成本任务隔离

高成本任务包括:

  • 4K;
  • 长时长;
  • 视频到视频;
  • 多参考人物;
  • 高帧率;
  • 带音频生成;
  • 复杂最终渲染;
  • 大量 GPU 显存需求。

这些任务应至少具备:

独立 queue_class
独立 max_running_units
独立 cost budget
独立 worker pool
独立超时
独立最大重试次数

普通任务池资源耗尽时,不应自动借用所有高成本任务资源;反过来也一样。

可以允许有限借用,但必须配置:

borrow_limit
borrow_timeout
minimum_reserved_headroom

高成本任务还应采用更严格的准入:

  • 先完成额度预占;
  • 检查租户合同权限;
  • 检查媒体时长、分辨率和文件大小;
  • 检查供应商实际成本;
  • 对不支持幂等的供应商减少自动重试。

18.14 异常流程与竞态条件

场景风险处理方式
Scheduler 认领后崩溃槽位长期泄漏lease、过期扫描、资源重建
MQ 重复投递重复调用供应商attempt_id、状态 CAS、供应商幂等键
供应商已受理但本地超时重复生成、重复计费进入 SUBMIT_UNKNOWN,先查询后重试
Provider 返回 429重试风暴释放明确未受理的槽位,延迟重新调度
Redis 队列索引丢失任务不再被调度PostgreSQL 扫描重建
Redis 容量镜像偏大预检查通过但真实容量不足PostgreSQL 条件更新最终拒绝
任务取消与调度同时发生已取消任务仍被提交state + version CAS
完成回调与取消回调同时到达终态覆盖状态机限定合法迁移
lease 过期但旧 Worker 仍执行旧 Worker 覆盖新 attemptfencing_token
大任务长期在队首队头阻塞或任务饥饿DRR 累积、独立资源类、有限 fit scan
调度策略在线修改同一任务前后规则不一致固化 policy_version
释放消息重复消费计数被重复减小reservation 状态 CAS,幂等释放

RocketMQ 的消费重试适合处理偶发业务处理失败,不应被当作限速机制;消费者扩缩容和重新平衡也可能导致少量消息重复处理,因此消费逻辑必须具备业务幂等。(RocketMQ)


18.15 “供应商已成功,但本地超时”的处理

这是整章最重要的异常场景之一。

错误做法

调用供应商超时
→ 释放 provider slot
→ 立即重新投递
→ 再次调用供应商

供应商可能已经创建了第一个任务。

结果是:

  • 实际并发超过平台记录;
  • 两次供应商费用;
  • 两个视频结果;
  • 用户只支付一次;
  • 后续无法准确释放资源。

正确状态

DISPATCHING
→ SUBMIT_UNKNOWN

进入 SUBMIT_UNKNOWN 后:

  1. 暂时保留供应商资源预留;
  2. 记录唯一 provider_request_key
  3. 查询供应商是否支持按幂等键或客户端请求 ID 查询;
  4. 如果查到任务,保存 provider_job_id 并进入 RUNNING
  5. 如果供应商明确表示不存在,再释放旧预留并重新提交;
  6. 如果供应商既不能查询也不支持幂等,需要等待安全窗口或进入人工补偿;
  7. 不得把普通 MQ 重试直接等同于再次调用供应商。

如果必须重试供应商调用,应优先使用同一个供应商幂等键。


18.16 幂等、一致性、重试和补偿

18.16.1 幂等键层次

层次幂等键
API 创建任务tenant_id + idempotency_key
调度 attempttask_id + attempt_no
资源预留attempt_id + resource_key
MQ 事件event_id
供应商提交provider_request_key
输出资产task_id + output_version
计费结算task_id + settlement_type

18.16.2 状态 CAS

更新任务时使用:

UPDATE generation_task
SET state = $new_state,
    version = version + 1,
    updated_at = now()
WHERE task_id = $task_id
  AND state = $expected_state
  AND version = $expected_version;

受影响行数为 0,说明状态已经被其他流程修改。

18.16.3 Fencing Token

每次 attempt 生成单调递增的 fencing token。

Worker 回写时必须满足:

callback.fencing_token == task.current_fencing_token

旧 Worker 即使仍在运行,也不能覆盖新 attempt 的结果。

但应注意:

fencing token 只能阻止本地旧结果写入,不能撤销旧 Worker 已经对第三方产生的外部副作用。

因此仍需要对供应商任务进行孤儿任务查询和成本对账。

18.16.4 资源释放

资源释放应在 PostgreSQL 事务中执行:

ACTIVE reservation
→ RELEASED reservation
→ resource_pool.used_units 减少
→ tenant running_units 减少

只有成功将 reservation 从 ACTIVE 更新为 RELEASED 的事务,才允许减少计数。

18.16.5 重试分类

操作是否可直接重试
PostgreSQL 事务死锁可以,短退避并限制次数
Redis 预检查失败可以回退数据库或稍后重试
RocketMQ 消费失败可以,但消费逻辑必须幂等
Provider 明确未受理可以延迟重试
Provider 返回 429可以延迟重试并降低并发
Provider 请求超时不能直接重新提交
FFmpeg 在写临时文件阶段失败可以清理工作目录后重试
计费结算超时使用唯一业务键重试
回调处理失败可以重放,但状态更新必须 CAS

18.16.6 补偿任务

需要定期执行:

ExpiredReservationScanner
StuckDispatchingScanner
UnknownSubmissionReconciler
QueueIndexRebuilder
ResourceCounterRebuilder
StarvationDetector
ProviderCostReconciler

资源计数重建时,以有效 resource_reservation 聚合结果为事实,而不是以 Redis 当前值为事实。


18.17 性能瓶颈与容量估算

18.17.1 在途任务估算

假设:

稳态到达速率:8 tasks/s
峰值到达速率:12 tasks/s
平均生成时间:150 s

稳态在途任务:

L = λ × W
  = 8 × 150
  = 1,200 tasks

这意味着即使任务创建接口只有 8 QPS,平台也可能长期维护 1,200 个供应商在途任务。

18.17.2 供应商服务能力

假设多个供应商总共提供:

原始并发槽位:2,000
目标安全利用率:75%
平均任务时间:150 秒

安全运行槽位:

safe_slots = 2,000 × 0.75
           = 1,500

安全吞吐量:

μ_safe = 1,500 / 150
       = 10 tasks/s

稳态到达 8 tasks/s 时,有约 2 tasks/s 的恢复余量。

18.17.3 峰值积压

峰值持续 5 分钟:

峰值流入:12 tasks/s
安全处理:10 tasks/s
持续时间:300 s

新增积压:

backlog = (12 - 10) × 300
        = 600 tasks

峰值结束后回到 8 tasks/s:

净清理速度 = 10 - 8
           = 2 tasks/s

清理时间:

600 / 2 = 300 秒

约 5 分钟恢复。

如果稳态到达速率已经等于安全处理能力,则峰值积压不会自动消失,系统必须扩容或拒绝部分请求。

18.17.4 队列上限

若业务允许最大排队 10 分钟,安全服务率为 10 tasks/s:

理论队列上限 ≈ 10 × 600
             = 6,000 个平均任务

实际应使用资源单位,并为以下因素保留余量:

  • 供应商故障;
  • 任务成本长尾;
  • 回调丢失;
  • 高成本任务;
  • 重试和未知提交;
  • 企业预留容量。

例如将实际全局准入上限设置为:

4,000 个平均任务单位

同时再设置每租户上限。

18.17.5 媒体 Worker 容量

假设生成成功任务进入媒体处理的速率为:

8 tasks/s

平均媒体处理时间:

45 s

需要的并发单位:

8 × 45 = 360 units

增加 30% 余量:

360 × 1.3 ≈ 468 units

这说明生成供应商容量和媒体 Worker 容量必须一起规划。只扩容供应商而不扩容回源和转码,会把瓶颈从生成阶段转移到媒体阶段。

18.17.6 轮询容量

假设:

  • 1,500 个在途任务;
  • 70% 通过回调;
  • 30% 需要轮询;
  • 轮询间隔 15 秒。

平均轮询 QPS:

1,500 × 30% / 15
= 30 QPS

如果所有任务每秒轮询一次,则会变成 1,500 QPS,因此必须使用分阶段轮询和抖动:

刚提交:10 秒后首次查询
运行中:15~30 秒
接近超时:适当加密
连续异常:指数退避

18.17.7 调度系统常见瓶颈

瓶颈优化方式
单个资源池计数行过热资源计数分 shard
单个 Redis 活跃租户 key 过热按 lane 和 hash 分片
每次调度扫描全部租户维护活跃租户环
无资源时持续空转next_eligible_at、事件唤醒、退避
一次只调度一条任务小批量 claim,但限制事务大小
MQ Consumer 拉取过快降低消费并发和本地缓存
数据库连接池耗尽为 API、Scheduler、Reconciler 分配预算
Provider 429自适应降低 semaphore
大任务估算严重偏差使用历史 P95 和实际消耗校准
Worker 临时盘耗尽磁盘 slot、工作目录配额和清理器

18.18 高可用与降级方式

故障降级行为
单个 Scheduler 崩溃其他实例继续;过期 lease 由 Reconciler 接管
Redis 不可用切换 PostgreSQL 保守调度,降低并发,暂停 Best Effort
RocketMQ 不可用Outbox 堆积;超过阈值后停止新任务准入
PostgreSQL 不可用停止新付费任务和新调度,避免无记录外部成本
单个 Provider 故障熔断并切换兼容供应商,已受理任务不直接迁移
全部 Provider 故障保留排队任务,停止继续消耗额度
Media Worker 满载暂停媒体消费,生成结果保留待回源
策略服务不可用使用最近一次已签名策略快照
容量计数异常进入保守容量模式并触发全量重建
回调网关故障使用后端轮询补偿
大客户流量突增使用预留池,限制共享池借用

Redis 降级

Redis 不可用时:

  1. 不再依赖 Redis deficit 和队列索引;
  2. 从 PostgreSQL 按服务等级扫描候选任务;
  3. 降低调度批量;
  4. 使用数据库资源预留作为最终限制;
  5. 暂停免费或高成本任务;
  6. 恢复后重建 Redis 索引。

RocketMQ 降级

不能简单绕过 MQ 直接调用供应商,否则会产生两套提交路径。

可以:

  • 继续把事件写入 Outbox;
  • 让 Outbox Relay 重试;
  • Outbox 积压超过阈值后关闭新准入;
  • 保留运维控制的紧急直连流程,但必须使用同一 attempt、幂等键和状态机。

Provider 降级

切换供应商前必须确认:

  • 模型能力兼容;
  • 用户是否允许模型变化;
  • 内容审核标准是否兼容;
  • 输出分辨率和时长一致;
  • 成本是否超出租户预算;
  • 原供应商是否明确未受理。

18.19 安全风险

18.19.1 用户不能自行指定调度权重

以下字段必须由服务端根据合同和策略生成:

queue_class
tenant_weight
base_priority
resource_class
reserved_capacity

不能直接信任客户端提交的 priority=9999

18.19.2 防止低报资源需求

客户端可能伪造:

  • 视频时长;
  • 分辨率;
  • 文件大小;
  • 编码格式;
  • 模型档位。

平台必须通过服务端媒体探测和模型能力矩阵重新计算资源需求。

18.19.3 队列洪泛

攻击者可能:

  • 不断创建不同幂等键;
  • 创建后立即取消;
  • 提交大量超大参考视频;
  • 使用高成本参数制造估算压力。

需要:

  • 请求速率限制;
  • 排队单位限制;
  • 上传大小和时长限制;
  • 取消频率限制;
  • 创建任务前额度预占;
  • 异常租户熔断。

18.19.4 管理面安全

调整以下配置的接口必须具有严格 RBAC 和审计:

租户权重
预留容量
供应商并发
成本预算
资源池容量
优先级
准入开关

配置变更应记录:

operator
old_value
new_value
reason
policy_version
timestamp

18.19.5 多租户隔离

所有调度查询、任务查询和资源统计都必须带 tenant_id 边界。

不能只依赖前端隐藏任务 ID。


18.20 常见错误设计及其后果

错误设计后果
所有任务共用一个 FIFO大客户造成 noisy neighbor
只按任务数公平长任务和高成本任务占用过多资源
全局高优先级队列低优先级任务永久饥饿
Redis 计数作为唯一事实故障后槽位丢失或超发
每个 Pod 使用本地 semaphore多实例总并发失控
先调用 Provider,再记录 attempt超时和崩溃后产生孤儿任务
Provider 超时后立即释放槽位对方已受理时发生超卖
利用 MQ 消费失败做限流重试风暴和 DLQ 污染
无界队列高峰期间耗尽内存、数据库和预算
高成本任务与普通任务共池普通任务延迟被长任务拖垮
企业预留资源永久禁止借用利用率过低
企业资源全部允许借用企业流量恢复时无法满足 SLA
每秒重写所有任务 aging scoreRedis CPU 和网络开销过高
任务完成时直接 used--重复回调导致计数变负
只监控队列长度无法发现高成本单位积压和饥饿

18.21 面试官可能追问的 10 个问题及资深回答

问题一:为什么不用 RocketMQ 的多个队列直接实现租户公平?

回答:

RocketMQ 更适合可靠传递和削峰,不适合作为全部业务调度策略的事实源。

消息队列可以按 Topic、Tag 或 MessageQueue 做物理分流,但租户数量可能非常大,为每个租户创建 Topic 或 Queue 会增加运维复杂度。更重要的是,租户权重、任务成本、Provider 容量和成本预算是动态业务条件,不能只依赖 MQ 的原生消费顺序。

我会使用少量按服务等级或资源类型划分的 Topic,由调度器在业务层做租户 DRR 和资源预留。RocketMQ 负责传递“任务已排队”和“任务可派发”事件,不负责最终公平决策。


问题二:WRR 和 DRR 应该怎样选择?

回答:

如果任务成本高度接近,例如都是相同时长和相同模型,WRR 简单、易实现。

AI 视频任务通常在时长、分辨率、模型和成本上差异明显。只按任务条数做 WRR,会让提交大任务的租户获得更多实际资源。因此更适合使用 DRR,把预计成本转换为 units。

实际系统可以先用 WRR 上线,再根据历史耗时和费用引入 DRR。但从设计上要预留 cost_unitsdeficit,避免后期大改数据模型。


问题三:任务实际成本在完成前不知道,DRR 怎么保证公平?

回答:

调度时使用历史数据估算:

模型
分辨率
时长
输入视频大小
供应商
地区
质量档位

任务完成后记录实际时长和实际成本,计算估算误差。

对于长期系统性误差,更新估算模型;对于单次低估,可以把差额计入租户后续的 fairness debt。公平调度不需要每个任务估算绝对精确,但估算不能长期被某类请求系统性利用。


问题四:多个 Scheduler Pod 怎样保证不会超过 Provider 并发?

回答:

本地 Go semaphore 只能作为 Pod 内保护,不能作为全局限制。

全局流程是:

  1. Redis 容量镜像做快速预检查;
  2. PostgreSQL 事务对资源池 shard 做条件更新;
  3. 更新成功后创建资源预留和 attempt;
  4. 事务提交后才发送派发事件。

这样即使多个 Scheduler 同时选择到相同 Provider,只有成功完成数据库预留的任务可以调用供应商。


问题五:为什么不直接用 Redis Lua 原子扣减所有槽位?

回答:

Redis Lua 很适合做高性能准入和快速预留,但 Redis 不应成为核心任务和资源事实的唯一来源。

如果 Redis 故障、切换或数据被清理,系统必须知道哪些任务实际占用了 Provider 和 Worker。这个事实需要与 task attempt 一起持久化在 PostgreSQL。

我会让 Redis 负责减少无效数据库尝试,而让 PostgreSQL 的资源预留事务承担最终正确性。Redis 恢复后,可以从有效 reservation 重建。


问题六:Provider 调用超时后为什么不能直接换供应商?

回答:

因为超时只说明本地没有收到响应,不说明对方没有受理。

如果原供应商已经创建任务,再切换到新供应商,会出现双生成和双计费。

正确流程是进入 SUBMIT_UNKNOWN

  • 保留原资源预留;
  • 使用客户端请求 ID 查询;
  • 优先重试同一个幂等键;
  • 只有明确确认原任务不存在,才允许创建新 attempt;
  • 无法确认时进入延迟对账或人工补偿。

问题七:Priority 加 Aging 能否替代 DRR?

回答:

不能。

Priority 和 Aging 解决的是任务紧急程度和饥饿问题,DRR 解决的是不同租户和不同任务成本之间的长期资源公平。

合理顺序是:

先通过 DRR 选择租户
再通过 priority 和 aging 选择租户内任务

只有少量真正紧急的恢复任务,才进入独立的高优先级恢复池。


问题八:企业预留容量空闲时怎样避免浪费?

回答:

允许共享池借用一部分企业容量,但借用必须有上限并标记为 borrowed。

企业流量恢复时:

  • 停止新借用;
  • 保留固定 headroom;
  • 等待借用任务自然完成;
  • 不对已提交供应商的任务做强制抢占。

如果企业 SLA 很严格,应降低可借比例,而不是假设所有运行任务都可以随时取消。


问题九:Scheduler 崩溃后怎样恢复资源计数?

回答:

每次资源占用都有持久化的 resource_reservation

Reconciler 定期:

  1. 找出 lease 过期的 reservation;
  2. 检查对应 attempt 是否仍可能在 Provider 运行;
  3. 对明确终止的 reservation 幂等释放;
  4. 对状态未知的任务进入查询流程;
  5. 聚合所有有效 reservation,与资源池计数对比;
  6. 修正 PostgreSQL 计数并重建 Redis 镜像。

不能只依据 lease 到期就释放,因为供应商任务可能已经成功提交。


问题十:队列最大长度应该怎样确定?

回答:

不能只拍一个任务数。

先计算安全服务能力:

safe_service_rate =
    安全并发槽位 / 平均服务时间

再根据业务允许的最大等待时间:

max_queue_units ≈
    safe_service_units_per_second × max_wait_seconds

然后根据任务成本长尾、供应商故障、重试和预留容量打折。

最终同时设置:

  • 全局任务数上限;
  • 全局资源单位上限;
  • 每服务等级上限;
  • 每租户上限;
  • 最大预计等待时间;
  • 最老任务等待时间告警。

18.22 三分钟口述稿

这个系统的高并发重点不是 HTTP QPS,而是分钟级 AI 任务带来的大量在途任务和稀缺资源占用。假设每秒进入 8 个任务,平均生成 150 秒,根据 Little’s Law,平台长期会有大约 1,200 个在途任务。因此我不会通过无限增加 goroutine 来解决,而是设计准入、排队、公平调度、资源预留和背压。

整个调度分四层。第一层选择企业预留、付费共享、普通或高成本资源池;第二层在资源池内使用 DRR 选择租户;第三层在租户内部根据优先级、等待时间和任务成本选择任务;第四层匹配具体 Provider、模型、账户和 Worker。

因为不同视频任务的时长、分辨率和费用差异很大,我不会只按任务条数公平,而会给任务估算 fairness units。租户每轮获得与权重成比例的 quantum,任务只有在成本不超过 deficit 时才能运行。这样大客户可以获得更高份额,但不能占满所有资源;普通用户等待过久后,也可以通过 aging 提升优先级。

资源控制是多级的,包括用户并发、租户并发、服务等级容量、Provider 并发、模型并发、GPU 槽位和分钟成本预算。Redis 保存令牌桶、活跃租户和容量镜像,用于快速筛选;PostgreSQL 保存任务、attempt 和 resource reservation,是最终事实源。调度器必须先在 PostgreSQL 事务中完成任务认领、租户配额扣减、Provider 槽位预留和 Outbox 写入,提交后才能调用第三方。

最关键的异常是供应商已经受理但本地超时。此时不能直接释放槽位并重试,而要进入 SUBMIT_UNKNOWN,保留资源预留,通过供应商幂等键或客户端请求 ID 查询。只有明确确认原请求不存在,才能重新提交,否则可能发生双生成和双计费。

高成本任务会进入独立资源池,企业预留容量可以有限借用,但已提交的生成任务通常按不可抢占处理。Scheduler、MQ Consumer 和 Worker 都是有界并发;队列达到最大资源单位或预计等待时间后,系统会拒绝或降级新请求。

最终目标不是让所有用户完全相同,而是在合同权重、平台成本和资源容量边界内,实现可解释、可恢复、可观测的公平调度。


18.23 十分钟深入讲解提纲

时间内容
0:00—1:00说明 AI 视频高并发的本质:长任务、在途量、成本和不可抢占
1:00—2:00展示总体架构:Admission、Queue Index、Scheduler、Resource Manager、Dispatcher
2:00—3:30讲解分层调度:服务等级、租户、任务、Provider
3:30—5:00对比 FIFO、WRR 和 DRR,解释 cost units、deficit 和 aging
5:00—6:00讲解多级并发:租户、Provider、模型、Worker、成本预算
6:00—7:00讲解 PostgreSQL 原子认领、resource reservation、Outbox 和 fencing token
7:00—8:00重点推演“供应商已受理但本地超时”及重复消费
8:00—9:00使用 Little’s Law 完成在途量、供应商吞吐和队列深度计算
9:00—9:40讲解 Redis、MQ、Provider 和 Scheduler 故障时的降级
9:40—10:00总结:有界准入、分层公平、持久化预留、不可盲目重试

18.24 本章总结

本章可以浓缩为五句话:

  1. AI 视频系统的高并发核心是大量长任务在途,而不是单纯的 API QPS。
  2. 公平调度应先选服务等级和租户,再选租户内部任务,不能只使用全局 FIFO。
  3. 任务成本差异较大时,DRR 通常比按任务条数计算的 WRR 更合理。
  4. Redis 可以加速准入和队列选择,但任务认领与资源预留必须有持久化事实源。
  5. 供应商调用超时不等于调用失败,任何重试都必须先分析重复生成和重复计费风险。