围绕 AI 平台配额管理里的“双写一致性”问题展开,重点包括:

  • 先把业务问题讲清楚:是否应该让 MySQL 中的配额账本K8s 中的真实资源占用 保持实时一致
  • FLPCAP两将军问题PACELC 这些经典理论出发,说明“跨两个独立分布式系统做实时强一致双写”为什么在理论上不可达
  • 再落到工程实践,分析提交链路、运行期和系统层故障会如何把最终一致方案推向状态机、幂等、补偿、对账和组合爆炸
  • 最后结合 Google Borg / Kubernetes / AWS 等业界做法,给出结论:放弃“实时库存式扣减”,采用 准入控制 + K8s 实时用量 的方案

AI 平台租户配额管理方案决策文档

结论先行:放弃"DB 实时库存式扣减"方案,采用业界主流的"准入控制 + K8s 实时用量"方案
前者在分布式系统理论上不可达,工程上代价极高;后者已被 Google、AWS、Microsoft 等所有头部厂商验证


分布式系统里,不存在"修好”,只存在"把不一致的窗口压到业务可接受”
承认这一点之后,工程问题就从"如何消除竞态 “变成"如何选择 SLA + 让残余竞态可观测、可恢复”


一、需求背景

1.1 业务场景

当前 AI 基础平台的资源管理模型如下:

  • 租户:每个租户拥有一份资源配额(例如租户 1 = 100 张 GPU 卡)
  • 空间:租户下可划分多个空间,配额从租户配额中切分(例如空间 a = 80 卡,空间 b = 20 卡)
  • 任务:用户在空间下提交 AI 任务,任务运行需要消耗 GPU
  • 执行层:任务真实运行在 K8s 集群(由 Volcano 调度)

1.2 当前提议(待评估)

业务侧提出的"实时库存式扣减"方案:

时机 动作
用户提交任务(10 卡) DB 中:空间a.used -= 10租户1.used -= 10,然后提交 K8s
任务运行结束 DB 中:空间a.used += 10租户1.used += 10

核心目标:让 MySQL 中的"已用配额"账本与 K8s 中真实占用的资源实时一致

1.3 待回答的问题

这个方案是否可行?是否值得做?业界是否有人这么做?是否有更好的替代方案?


二、方案不可行性证明:分布式系统理论限制

想做的"DB 配额账本与 K8s 真实占用实时一致”,本质上是一个跨两个独立分布式系统的实时强一致问题。这个问题在计算机科学中已被证明是不可达的。

2.1 FLP 不可能性定理(1985)

定理陈述:在异步分布式系统中,即使只有一个进程可能崩溃,不存在确定性算法能保证多方达成共识

来源:Fischer, Lynch, Paterson. Impossibility of Distributed Consensus with One Faulty Process. JACM, April 1985. 论文原文:https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf 该论文获得 2001 年 PODC Dijkstra 奖(分布式计算最具影响力论文奖)。

证明思路(双价配置论证 / Bivalence Argument)

  1. 假设存在一个确定性的共识算法
  2. 对于该算法,总能构造出一个"双价初始配置”——即从这个配置出发既可能决定 0,也可能决定 1
  3. 通过精心安排消息延迟和单一进程崩溃,可以让系统永远停留在"未决定"状态
  4. 因此原假设不成立 ∎

对当前场景的意义

  • 业务服务与 K8s API Server 之间通过异步网络通信
  • 任意一方都可能崩溃或响应延迟
  • “DB 完成扣减” 与 “K8s 完成调度” 这两个事件在两个独立系统中发生
  • ∴ 两边状态在有限时间内确定性达成一致——理论上不可达

2.2 CAP 定理(Brewer 2000;Gilbert & Lynch 2002 形式化证明)

定理陈述:在分布式数据存储中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可同时满足

来源:Gilbert, Lynch. Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. ACM SIGACT News, 2002. 论文原文:https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf

证明思路(反证法)

  1. 设系统包含两个节点 G1、G2,初始值 v0
  2. 客户端在 G1 写入 v1
  3. 此时 G1 与 G2 之间发生网络分区,所有消息丢失
  4. 客户端从 G2 读取
  5. 若保证一致性 → G2 必须返回 v1,但收不到更新,只能拒绝服务(违反可用性)
  6. 若保证可用性 → G2 必须立即响应,只能返回过期的 v0(违反一致性)
  7. ∴ 在分区场景下,C 与 A 不可兼得 ∎

对当前场景的意义

  • MySQL 集群与 K8s(etcd)集群之间必然存在网络分区可能性
  • 想要"DB 扣减"和"K8s 提交"在任意时刻强一致 → 在分区时必须牺牲可用性
  • 即整个平台在网络抖动期间将无法接受任何任务提交

2.3 两将军问题(Two Generals’ Problem,1975)

定理陈述:通过不可靠信道通信的双方,无法通过有限次消息交换达成对某个动作的共同知识

证明思路(归纳法)

  1. 假设存在 N 步消息协议能达成共识
  2. 考虑最后一条消息:若它丢失,发送方无法确认接收方是否收到 → 发送方不能依赖它 → 它实际上是冗余的
  3. 因此 N-1 步就够了
  4. 对 N-1 步重复同样论证,归纳到底:0 步就能达成共识 → 矛盾 ∎

对当前场景的意义

  • 业务服务调用 K8s API 后,永远无法 100% 确认 K8s 是否真正处理了请求
  • 这就是为什么需要 idempotency key、retry、对账——因为绝对的"双方共同知道"在数学上不可能
  • 任何"提交后的状态推断"都建立在概率而非确定性之上

2.4 PACELC 定理(Abadi, 2010)

定理陈述:CAP 的扩展——

若发生分区(P),需在可用性(A)和一致性(C)间取舍; 否则(E),需在延迟(L)和一致性(C)间取舍。

对当前场景的意义: 即使在没有故障的"晴天"场景下,想做 DB 与 K8s 的强一致同步,也必须接受高延迟。任务提交 RT 会显著上升,吞吐能力下降。

2.5 理论结论组合推导

前提 1:目标是 DB 配额账本与 K8s 真实占用"实时一致"
前提 2:DB 与 K8s 是两个独立的分布式系统,通过异步网络通信
前提 3:网络可能分区、消息可能丢失(两将军问题)
前提 4:节点可能故障、消息延迟无上界(FLP 模型)

由 两将军问题 → 双方无法在有限步内确认彼此状态
由 FLP        → 异步+故障下不存在确定性共识算法
由 CAP        → 分区时强一致与可用性不可兼得
由 PACELC     → 即使无分区,强一致也要付出延迟代价

∴ "实时强一致的 DB-K8s 双写"在理论上不可达
∴ 退而求其次只能做"最终一致"
∴ 最终一致需要:状态机 + 幂等 + 对账 + 补偿
∴ 这些机制的复杂度与故障空间组合爆炸

⚠️ 诚实的边界:理论证明的是"实时强一致不可达”,并未证明"最终一致不可达”。 但下一节会展示:即便退到"最终一致”,工程代价也远超收益。


三、工程不可行性:异常场景的组合爆炸

即使退而求其次选择"最终一致"路线(业务服务 + Outbox + Informer + 对账系统),仍会面临大量难以根除的异常场景。下面列举生产环境会真实遇到的边界问题:

3.1 提交链路异常(共 8 类)

# 场景 后果
1 DB 扣减成功,调用 K8s 时网络超时 DB 不知道 K8s 是否收到,重试可能双扣
2 DB 扣减成功,K8s 提交失败(参数错误等不可重试错误) 必须触发回滚/退款流程
3 K8s 提交成功,但响应在网络中丢失 DB 状态停留在中间态,依赖重试 + 幂等键兜底
4 DB 扣减失败(主从切换、连接池打满) 配额状态被回滚,但可能已经发起了 K8s 调用
5 业务服务在"DB 扣完"和"调 K8s"之间崩溃 任务不存在,但配额已扣 → 配额泄漏
6 业务服务在"调 K8s 成功"和"持久化任务记录"之间崩溃 任务在跑,但 DB 没记录 → 孤儿任务
7 同一用户并发提交两个相同任务(重复点击) 必须设计业务幂等键,否则配额被重复扣
8 MySQL 主从切换丢失最后几秒数据 已扣的配额被回退,造成实际超卖

3.2 运行期异常(共 7 类)

# 场景 后果
9 Informer 监听到 Pod 完成事件,但归还配额时 DB 失败 配额没归还,依赖对账修复
10 Informer 重启期间漏接事件 任务结束了配额没回收
11 用户绕过平台直接 kubectl delete 删除任务 平台不知道,配额永远占着
12 Pod 被 Node Pressure 驱逐 是失败还是会重调度?配额怎么算?
13 节点宕机,Pod 进入 Unknown 状态 配额是占着还是释放?两难选择
14 GPU 卡硬件故障,Pod 还在但卡不可用 物理资源 ≠ 逻辑资源
15 Volcano gang scheduling 部分成功(8 卡任务实际只起了 6 卡) 配额按 8 算还是 6 算

3.3 系统层异常(共 5 类)

# 场景 后果
16 K8s 集群短暂 API 不可用 所有提交/释放堆积在消息队列中
17 跨可用区网络分区 DB 在 A 区可写,K8s 在 B 区,谁是真相?
18 时钟漂移导致事件顺序错乱 “释放"事件可能先于"扣减"到达
19 服务重启时正好有任务在中间态(SUBMITTING/REFUNDING) 状态丢失,需要完整 replay 逻辑
20 配额扩容/缩容时正好有任务进出 账本基线在变,扣减计算依据混乱

3.4 这些异常带来的工程负担

每一个场景都需要

  1. 专门的代码路径
  2. 单独的单测/集成测试用例
  3. 独立的监控告警
  4. 对账脚本兜底
  5. 长期的运维 runbook

真实的技术债形态

“线上某个租户配额对不上了,差了 3 张卡,时间不确定,复现不了。”

这种 bug 通常依赖特定的事件交错顺序 + 时间窗口 + 网络状态,无法稳定复现,无法通过测试覆盖,只能靠生产环境的对账脚本去发现,再人肉去修。这种债持续吃工程师时间,没有尽头。

注意!以上只是列出了能想到的场景,可能会存在更多未知的异常场景!

3.5 对账系统本身又是一个新的复杂系统

很多人以为"做个对账兜底就行了”。但对账本身要解决:

  • 对账的快照怎么取(K8s 和 DB 不能同时冻结)?
  • 对账期间新进的任务算谁的?
  • 对账发现差异,自动修复还是人工介入?自动修复的边界在哪?
  • 对账脚本本身挂了谁来对账?

对账系统是"治理治理者"的递归问题,复杂度只会上升不会下降。


四、业界头部公司的真实做法

调研了业界主流的资源调度/配额管理系统。没有任何一家头部公司采用"DB 实时库存式扣减"方案。它们的共同选择是:配额作为准入控制策略,使用量从底层调度器实时获取

4.1 Google Borg(Kubernetes 的前身)

论文:Verma et al. Large-scale cluster management at Google with Borg. EuroSys 2015. 链接:https://research.google/pubs/large-scale-cluster-management-at-google-with-borg/

Borg 论文明确指出:

“Quota is used to decide whether to admit a job (admission control). … Quota-checking is part of admission control, not scheduling: jobs with insufficient quota are immediately rejected upon submission."

关键设计

  • 配额是一个资源向量 + 优先级 + 时间段的策略配置(如"X 团队在 Y 集群可使用 20 TiB RAM 至 7 月底”)
  • 配额检查在任务提交时一次性执行,不通过、立即拒绝
  • 配额本身不参与扣减/归还
  • 真实资源占用由 Borgmaster(调度器)维护,不是配额系统的职责

4.2 Kubernetes ResourceQuota(Borg 的开源继任者)

官方文档:https://kubernetes.io/docs/concepts/policy/resource-quotas/ 设计文档:https://github.com/kubernetes/kubernetes/blob/release-1.2/docs/design/admission_control_resource_quota.md 控制器源码:https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/resourcequota/resource_quota_controller.go

K8s 原生的 ResourceQuota 同样采用准入控制 + 实时计算用量的模型:

“Users create resources (pods, services, etc.) in the namespace, and the quota system tracks usage to ensure it does not exceed hard resource limits defined in a ResourceQuota."

关键实现

  • ResourceQuota 对象只存储 hard(上限值)
  • ResourceQuotaController 通过 listwatch Pod 等资源对象,实时计算 used
  • 创建新资源时,准入控制器检查 used + 新请求 ≤ hard,否则返回 HTTP 403 Forbidden
  • 没有任何"扣减后归还"的事务逻辑

4.3 AWS Service Quotas

官方文档:https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html CloudWatch 集成:https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Quotas-Visualize-Alarms.html

AWS 的全局配额服务设计:

  • 配额是配置值(如某账号最多能开 100 个 EC2 实例)
  • 资源创建时,实时查询当前已用数量(从对应资源服务获取),与配额对比
  • 不存在"创建 EC2 时扣减配额表,删除时加回来"的事务
  • 用量统计走 CloudWatch,是异步聚合的指标

4.4 Meta / Microsoft

Meta Twine:https://research.facebook.com/publications/twine-a-unified-cluster-management-system-for-shared-infrastructure/

Meta 的 Twine 与 Microsoft 的 Singularity 等内部 AI/资源平台均采用类似设计:

  • 配额是 entitlement(权益),不是 inventory(库存)
  • 资源争抢由调度器的 fair-share / priority 机制解决
  • 没有"扣库存"的概念

4.5 业界共识

厂商 系统 配额模型 是否做"DB 扣减+归还”?
Google Borg Admission Control ❌ 不做
Google/CNCF Kubernetes ResourceQuota Admission Control ❌ 不做
AWS Service Quotas Policy + Real-time Usage ❌ 不做
Meta Twine Entitlement ❌ 不做
Microsoft Singularity Entitlement + Fair-share ❌ 不做

业界统一设计哲学

“Quota is policy, not inventory."(配额是策略,不是库存)


五、推荐方案:准入控制 + K8s 实时用量

5.1 核心思想

重新定义配额

  • ❌ 旧模型:配额是一个需要扣减/归还的库存数字
  • ✅ 新模型:配额是一个"上限策略”,使用量从 K8s 实时计算
用户看到的"剩余配额" = 配额上限(DB 中的策略) − 实时使用量(K8s 中的真实数据)

5.2 数据模型

配额策略表(仅存储上限,不存储已用)

1
2
3
4
5
6
7
CREATE TABLE quota_policy (
    owner_type   VARCHAR(16),    -- 'tenant' / 'space'
    owner_id     VARCHAR(64),
    resource     VARCHAR(32),    -- 'gpu' / 'cpu' / 'memory'
    limit_value  BIGINT,         -- 上限
    PRIMARY KEY (owner_type, owner_id, resource)
);

注意:表中没有 used 列,没有 available

5.3 提交任务流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def submit_task(space_id, request_gpus):
    # 1. 从 DB 读策略上限
    space_limit  = db.get_quota_policy(space_id, 'gpu')
    tenant_limit = db.get_quota_policy(space_id.tenant, 'gpu')

    # 2. 从 K8s/Volcano 实时查当前用量(informer 缓存,毫秒级)
    space_used  = k8s_informer.sum_gpu_usage(label=f"space={space_id}")
    tenant_used = k8s_informer.sum_gpu_usage(label=f"tenant={space_id.tenant}")

    # 3. 准入判断
    if space_used + request_gpus > space_limit:
        return reject("空间配额不足")
    if tenant_used + request_gpus > tenant_limit:
        return reject("租户配额不足")

    # 4. 直接提交 K8s,不做任何 DB 扣减
    return k8s.submit(task)

5.4 任务结束流程

1
2
# 什么都不做。
# 因为从来就没扣过,Pod 消失后 informer 缓存自然变小。

5.5 前端展示

1
2
3
4
5
6
def get_quota_status(space_id):
    return {
        "limit":     db.get_quota_policy(space_id, 'gpu'),
        "used":      k8s_informer.sum_gpu_usage(label=f"space={space_id}"),
        "available": limit - used,
    }

用户看到的画面与"库存式"完全一致已使用 10 / 80 卡,剩余 70 卡

5.6 硬限制兜底

为了防止"软校验通过但实际超卖"的极端情况,建议在 K8s 层加第二道闸:

  • 把租户/空间映射到 Volcano QueueK8s Namespace + ResourceQuota
  • K8s 自身的准入控制器作为最终硬限制
  • 即使业务层判断有偏差,K8s 也会拒绝超额任务

5.7 计费/统计

  • 不依赖实时账本
  • 通过定时任务(每小时/每天)聚合 Pod 用量数据,写入数仓
  • 报表、计费、月度封顶等业务通过离线 ETL 实现

5.8 方案对比

维度 库存式(DB 扣减/归还) 准入控制式(推荐)
一致性问题 跨系统双写,理论上不可达 单一事实源(K8s),无一致性问题
任务结束时归还逻辑 必须有,且要处理大量异常 不需要
Outbox/状态机 必须 不需要
对账系统 必须 不需要
用户绕过平台直接操作的处理 必须专门处理 自动反映(K8s 是真相)
服务重启中间态恢复 复杂 replay 逻辑 无中间态
配额扩容 处理基线变化 改一个 limit 即可
用户体验 “已用 X / 总 Y” “已用 X / 总 Y”(完全相同)
初始开发成本 2~3 人月 1~2 周
长尾维护成本 持续 6~12 个月 bug 几乎为零
业界采用度 几乎无大厂使用 业界通用方案

六、最终结论

6.1 一句话总结

配额一致性这件事,不是"做不到”,而是"做了之后会让系统持续地、不可见地变脆弱”。 K8s 已经是资源的真相,再造一个 MySQL 账本去和它实时对账,相当于花 80% 的成本去解决一个 5% 用户能感知到的问题。 把硬限制交给 K8s,把展示和统计放在这一层,这是 Google、AWS、Microsoft、Meta 等所有头部厂商的统一选择,也是更可持续的工程方案。

6.2 核心结论

  1. 理论层面:FLP(1985)+ CAP(2002)+ 两将军问题(1975)联合证明,DB 与 K8s 之间的"实时强一致"在分布式系统理论上不可达

  2. 工程层面:即便退到"最终一致”,需要处理 20+ 种异常场景(可能还有很多未想到的各种异常场景),引入状态机、Outbox、Informer、对账系统等多个组件,长期维护成本极高,且 bug 难以根除。

  3. 业界层面:从 Google Borg 到 Kubernetes 原生 ResourceQuota,从 AWS Service Quotas 到 Meta Twine —— 没有任何头部厂商采用"DB 实时库存式扣减”。它们的共同选择是准入控制 + 实时用量计算

  4. 替代方案:采用"配额策略 + K8s 实时用量"模型。用户体验完全一致,工程复杂度降低一个数量级,硬限制由 K8s 自身机制兜底。

6.3 行动建议

  • ✅ 停止"DB 实时库存式扣减"方案的开发
  • ✅ 改为"配额策略表 + K8s Informer 实时计算"模型
  • ✅ 在 K8s 层使用 ResourceQuota / Volcano Queue 作为硬限制兜底
  • ✅ 计费、报表、月度封顶等业务通过离线聚合实现

附录:参考文献

分布式系统理论

  1. Fischer, Lynch, Paterson. Impossibility of Distributed Consensus with One Faulty Process. JACM, 1985. https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf

  2. Gilbert, Lynch. Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. ACM SIGACT News, 2002. https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf

  3. Gilbert, Lynch. Perspectives on the CAP Theorem. IEEE Computer, 2012. https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf

  4. Abadi. Consistency Tradeoffs in Modern Distributed Database System Design. IEEE Computer, 2012.(PACELC 定理)

业界系统论文与文档

  1. Verma et al. Large-scale cluster management at Google with Borg. EuroSys 2015. https://research.google/pubs/large-scale-cluster-management-at-google-with-borg/

  2. Kubernetes ResourceQuota Documentation. https://kubernetes.io/docs/concepts/policy/resource-quotas/

  3. Kubernetes ResourceQuota Admission Control Design Doc. https://github.com/kubernetes/kubernetes/blob/release-1.2/docs/design/admission_control_resource_quota.md

  4. Kubernetes ResourceQuota Controller Source. https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/resourcequota/resource_quota_controller.go

  5. AWS Service Quotas User Guide. https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html

  6. AWS Service Quotas - CloudWatch Integration. https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Quotas-Visualize-Alarms.html

  7. Meta. Twine: A Unified Cluster Management System for Shared Infrastructure. OSDI 2020. https://research.facebook.com/publications/twine-a-unified-cluster-management-system-for-shared-infrastructure/