Skip to content

Falcon resource manager 设计文档 ​

背景 ​

团队当前在多个 GCP 项目下维护着多个 GKE 集群,用于运行 TPU 相关的工作负载(profiling、benchmark、精度对齐、模型训练等)。这些集群分布在不同的 project 和 region 中,各自拥有不同的 TPU 类型和预留资源。

上层业务组件需要向这些边缘集群下发任务,并在任务完成后获取其状态和产物(GCS 路径),以支持后续的产物索引、报告生成等流程。

目前缺少一个统一的中心化任务下发和状态回收机制,各集群的任务管理依赖手动操作或分散的脚本,无法规模化。

动机 ​

  1. 跨项目任务下发: 边缘集群分布在不同 GCP 项目下,需要一个安全的、基于 IAM 的跨项目访问方案,而非静态 kubeconfig 或手动操作
  2. 统一的任务生命周期管理: 上层组件写入任务、controller 下发执行、状态回写数据库,形成闭环
  3. 产物与日志: 产物和日志路径由上层组件约定,Job 通过 GCS FUSE 挂载自行写入 GCS,falcon-resource-manager 不参与数据面
  4. 规模化: 随着 TPU 资源(v6e、v7x 等)和集群数量增长(当前 2-5 个),需要多集群支持从第一天就内建于架构中

目标(Goals) ​

  • G1: 实现一个运行在中心 GKE 集群上的 Go controller,能够从数据库读取待执行任务,根据 TPU 类型自动选择目标集群,向边缘集群提交 K8s Job
  • G2: 支持多集群——通过集群注册表(clusters 表)管理边缘集群,新增集群只需 IAM 授权 + 数据库插入一行记录
  • G3: Node pool 按需创建与延迟删除——controller 在提交 Job 前创建所需的 TPU node pool(或复用已有的同拓扑 node pool),Job 终态后检查是否有后续同拓扑任务可复用,无则删除
  • G4: 任务状态闭环——controller 状态机与上层 Job 模型对齐(Status / Reason / Message),支持 abort(Aborting → Aborted);Reason 同时承载正常阶段标识和失败原因,Message 提供人类可读的进度与排队信息
  • G5: Job 日志收集——Job 的 Pod 通过 GCS FUSE 挂载将 stdout/stderr 重定向写入 GCS,日志路径按模板可推导(gs://<bucket>/logs/<job_id>/<pod_name>.log),controller 不参与日志收集
  • G6: 基于 GCP Workload Identity 实现零静态凭证的跨项目认证,已通过 POC 验证(2026-04-15)

非目标(Non-Goals) ​

  • NG1: falcon-resource-manager 不决定"运行什么"——任务的创建由上层组件负责,falcon-resource-manager 负责集群选择和任务下发
  • NG2: 不管理产物——产物由 Job 自行写入 GCS(路径由上层组件约定),falcon-resource-manager 不记录、不读取、不转移产物
  • NG3: 不替代现有 CI/CD 流水线(GitHub Actions + ARC runners),Falcon 面向的是 CI 之外的按需任务下发场景
  • NG4: 不提供 Web UI——MVP 阶段通过数据库记录 + 日志完成可观测性

成功指标(Success Metrics) ​

  1. 任务写入数据库后,GKE node pool 应成功创建,K8s Job 应成功生成
  2. 除非集群无可用资源,Job 对应的 Pod 应成功启动
  3. Job 无论成功或失败,job 表状态应被更新至终态
  4. Job 运行期间,Pod 日志应通过 GCS FUSE 实时写入 GCS

影响范围 ​

组件影响说明
中心 GKE 集群 (tpu-service-473302/tpu-service)部署 falcon-resource-manager,新增 falcon namespace 和 Workload Identity 绑定
边缘 GKE 集群 (poc-tpu-partner 等)接收 controller 提交的 Job,需在对应 project 配置 IAM 授权
GCP IAM中心 project 创建 SA (cluster-controller),边缘 project 授予 container.admin + container.clusterViewer。MVP 使用 container.admin 是因为 node pool 的创建/删除需要较高权限;后续应收敛为自定义角色,仅包含 container.nodePools.*、container.jobs.*、container.pods.get/list 等必要权限
数据库 (MySQL)job 表由上层组件定义和创建,Falcon 读写其中的调度相关字段;clusters 表由 Falcon 管理
上层业务组件拥有 job 表的 schema 定义权;负责写入 job 记录、读取状态;产物路径由上层自行约定并写入 GCS
GCS各边缘集群的 Job 将产物写入各自的 GCS bucket,bucket 信息在 clusters 表中注册

高层架构 ​

text
                         ┌──────────┐
                         │ 上层组件  │
                         └────┬─────┘
                              │ 写入任务
                         ┌────▼─────┐      ┌────────┐
                         │API Server│─────►│ MySQL  │
                         └────┬─────┘      └────────┘
                              │
                    拉取任务 / 回写状态
                              │
┌─────────────────────────────┼──────────────────────────────────┐
│  中心集群 (tpu-service-473302/tpu-service)                      │
│  ┌──────────────────────────┼────────────────────────────────┐ │
│  │  falcon-resource-manager ▼                                │ │
│  │  ns: falcon, sa: falcon-controller (Workload Identity)    │ │
│  │                                                           │ │
│  │  ┌─────────────┐                                          │ │
│  │  │ Task Poller │ ── 轮询 API Server 拾取 pending 任务      │ │
│  │  └──────┬──────┘                                          │ │
│  │         ▼                                                 │ │
│  │  ┌──────────────┐                                         │ │
│  │  │NodePool Mgr  │ ── GKE API 创建/删除 node pool          │ │
│  │  └──────┬───────┘                                         │ │
│  │         ▼                                                 │ │
│  │  ┌──────────────┐                                         │ │
│  │  │Job Submitter │ ── K8s API 提交 Job                     │ │
│  │  └──────┬───────┘                                         │ │
│  │         ▼                                                 │ │
│  │  ┌──────────────┐                                         │ │
│  │  │Status Reconc.│ ── K8s Informer 监听 Job 状态            │ │
│  │  └──────────────┘                                         │ │
│  └───────────────────────────┬───────────────────────────────┘ │
└──────────────────────────────┼─────────────────────────────────┘
                               │
                      Workload Identity
                               │
          ┌────────────────────┼──────────────────────┐
          │                    ▼                      │
          │  ┌──────────────────────────────────────┐ │
          │  │ 边缘集群 1                            │ │
          │  │ (poc-tpu-partner/tpuv7x-64-node)     │ │
          │  │ Jobs → GCS Bucket A                  │ │
          │  └──────────────────────────────────────┘ │
          │  ┌──────────────────────────────────────┐ │
          │  │ 边缘集群 2                            │ │
          │  │ (project-b/cluster-y)                │ │
          │  │ Jobs → GCS Bucket B                  │ │
          │  └──────────────────────────────────────┘ │
          │            ... (2-5 个集群)               │
          └───────────────────────────────────────────┘

关键设计决策与权衡 ​

1. 认证方案:Workload Identity + GKE API ​

选择: Workload Identity 自动凭证 + GKE API 动态发现集群端点

权衡:

  • (+) 零静态凭证,token 自动刷新,安全审计友好
  • (+) 跨项目只需 IAM binding,无需网络特殊配置
  • (-) 依赖边缘集群 API server 为 public endpoint(private cluster 需额外配置 VPN / Private Service Connect)
  • (-) token 有效期 1 小时,长连接(Informer)需要 transport 层自动刷新,增加了 client 构造复杂度

已通过 POC 验证全链路:credentials 获取 → 集群 endpoint 发现 → Job 提交 → Job 执行完成

2. 多集群支持:注册表模式 ​

选择: 数据库 clusters 表管理边缘集群

clusters 表 Schema(由 Falcon 管理):

字段类型说明
idUUID PK集群 ID
nameVARCHAR集群显示名(如 "tpuv7x-64-node")
projectVARCHARGCP 项目 ID
locationVARCHARGKE location(如 "us-central1")
tpu_typeVARCHAR集群提供的 TPU 类型(如 "v7x"、"v6e"),用于任务匹配
gcs_bucketVARCHAR该集群产物的默认 GCS bucket
default_reservationVARCHARTPU 预留名称(如 "ghostfish-kankgdm2b4m7f")。为空时默认使用 flex-start;有值时默认使用 reservation。用户可在任务级 supply_mode 显式覆盖
statusENUMactive / maintenance / offline
created_atTIMESTAMPTZ创建时间

权衡:

  • (+) 新增集群只需 IAM 授权 + 插入一行记录,运维成本极低
  • (+) 集群可设置状态(active / maintenance / offline),controller 自动跳过非活跃集群
  • (-) 引入了数据库作为集群注册的 source of truth,集群元数据变更需要同步更新数据库
  • (-) 无法利用 K8s 原生的多集群方案(如 KubeFed),但避免了引入额外的 K8s 组件

3. 集群选择策略 ​

任务不指定具体集群,而是声明所需的 TPU 类型(如 "v7x")。Controller 根据 TPU 类型匹配所有 active 状态的候选集群,按以下优先级选择:

  1. 优先复用 node pool: 候选集群中存在空闲的、拓扑匹配的 node pool(由延迟删除机制保留),直接复用,省去 5-10 分钟创建时间
  2. 队列深度: 无可复用 node pool 时,选择当前排队任务最少的集群,均衡负载

权衡:

  • (+) 上层组件无需感知集群拓扑细节,只需声明 TPU 类型
  • (+) node pool 复用与集群选择形成闭环——有空闲 node pool 的集群自然被优先选中
  • (-) 引入了调度决策,增加了 controller 复杂度
  • (-) 队列深度信息需要 controller 维护,增加了状态管理

4. 排队与调度模型 ​

队列粒度: per (cluster, topology)。每个队列同时只允许一个任务处于资源获取阶段(provisioning / dispatching),已 Running 的任务不占队列位。

  • 资源获取并行度 = 集群数量 x 每个集群的拓扑种类数(每个队列同时一个任务在获取资源)
  • 执行并行度取决于实际 TPU 资源,不受队列限制
  • 不同拓扑之间不阻塞(64chip 等资源不影响 4chip)
  • 不同集群之间不阻塞

放行条件: 当前资源获取中的任务满足以下任一条件后,放行下一个:

  • Pod 确认 Running(资源到手)
  • 任务到达终态(Failed / Aborted)

Pick Job 规则: 队列槽位空出时,从 waiting 任务中选择下一个执行:

  1. 用户内 FIFO — 同一个 actor 的任务按提交顺序执行
  2. 用户间 Round-Robin — 上一个获得槽位的 actor 让位给其他 actor

选择流程:收集 waiting 任务 → 按 actor 分组,每组取最早提交的 → 如果候选 actor > 1,排除上次获得槽位的 actor → 剩余中取最早提交的

超时兜底: 每个任务有 timeout_seconds,用于 workload 已提交后的执行超时。node pool 控制面已创建但尚无 Ready node 时,Resource Manager 会记录 NodeRegistration/WaitingForNode 并继续重试;若没有不同拓扑任务等待,单租户场景会保留该 pool,让 GKE/MIG 继续内部重试,直到 node Ready 或用户 abort。

权衡:

  • (+) FIFO 保证排队公平,无论 reservation 还是 flex-start
  • (+) Round-robin 防止单一 actor 垄断资源
  • (+) per (cluster, topology) 粒度避免不同拓扑互相阻塞
  • (-) 串行化资源获取阶段降低了瞬时吞吐(同队列不能同时创建多个 node pool)
  • (-) Controller 需要维护每个队列的状态和 actor 轮转记录

5. Node pool 按需创建与延迟删除 ​

选择: Job 独享 node pool 执行,Job 结束后延迟删除——若有同拓扑的 pending 任务,保留 node pool 供下一个 Job 复用;若无,立即删除

供给模式选择: TaskSpec.supply_mode 可以由用户显式指定。支持值为 reservation、on-demand、flex-start;为空时使用集群默认策略。

  1. supply_mode=flex-start → 跳过 reservation lookup,创建 GKE flex-start node pool
  2. supply_mode=on-demand → 跳过 reservation lookup,创建 on-demand node pool
  3. supply_mode=reservation → 要求目标集群配置 default_reservation,并通过 GCP Compute API 查询 reservation 剩余容量(count - inUseCount);容量不足则保持 pending/重试,不自动切到 flex-start
  4. supply_mode 为空且集群配置了 default_reservation → 按 reservation 路径创建;容量不足时保持 pending/重试
  5. supply_mode 为空且集群未配置 default_reservation → 创建 flex-start node pool

flex-start 的 GKE create RPC 返回后,node pool 可能短时间内还不能被 NodePools.Get 读到。Resource Manager 对新建 Active lease 有 create-visibility grace:在 grace 内看到 transient 404 时保留 lease 并延迟复查,避免把刚创建的 flex-start pool 当作孤儿清掉。

Autoscaler 空闲超时: 根据实际使用的供给模式(而非集群配置)设置不同的 scaleDownUnneededTime:

实际供给模式scaleDownUnneededTime原因
reservation短(默认 10min)资源预留,释放后仍可重新获取
flex-start长(如 6h)资源稀缺,释放后难以重新获取;node pool RUNNING 后仍可能继续等待 GKE 实际分配 node

权衡:

  • (+) TPU 预留资源按需使用,无任务时不持有 node pool
  • (+) 执行期间仍为单 Job 独享,保持资源隔离
  • (+) 同拓扑任务连续到达时,省去 node pool 创建的 5-10 分钟延迟
  • (-) 延迟删除期间 node pool 短暂空闲,占用少量 quota(但时间窗口很短——仅为检查 pending 队列的耗时)
  • (-) 需要在 deprovisioning 阶段查询 pending 队列,增加了 NodePool Manager 与调度逻辑的耦合

6. 任务与 Job 的关系 ​

一个 task 对应一个 K8s Job。task 记录中的 spec 字段包含 Job 渲染所需的参数(镜像、命令、TPU 拓扑等),controller 负责将其渲染为完整的 K8s Job manifest。

7. 日志收集 ​

选择: Pod 内通过 GCS FUSE 挂载将 stdout/stderr 重定向写入 GCS,controller 不参与日志数据面

日志存储格式: 每个 Pod 一个日志文件,路径按模板可推导,默认 gs://<bucket>/logs/<job_id>/<pod_name>.log。多 Pod 场景(如多节点训练)下各 Pod 日志独立存储。

Controller 在渲染 Job manifest 时注入 GCS FUSE volume 和重定向配置,Pod 启动后日志实时写入 GCS。

权衡:

  • (+) 控制面与数据面解耦——controller 崩溃不影响日志收集
  • (+) 无需在边缘集群部署额外的日志收集组件
  • (+) 日志实时写入,不依赖 Pod 存续状态,无需管理 Job 删除时机
  • (+) 日志路径按模板可推导,无需额外存储路径信息
  • (-) 需要 Job 容器支持 stdout/stderr 重定向(controller 在 Job manifest 中注入)
  • (-) 依赖 GCS FUSE 可用性

8. 任务 Abort 流程 ​

触发: 上层组件通过 API Server 将任务状态设为 Aborting,并设置 aborting_at 时间戳。

端到端流程: 上层设置 Job 为 Aborting → ResourceManager Controller 检测到 Aborting Job → 删除 K8s Job → 更新 Job 为 Aborted → 上层组件监听到 Job Aborted 后自行处理关联状态

Controller 处理: Resource Manager 检测到 Aborting 状态后,根据任务当前所处阶段执行不同的中止操作:

当前阶段中止动作
waiting从 (cluster, topology) 队列中移除,直接设为 Aborted
provisioning取消或等待 node pool 创建完成,走 deprovisioning 复用逻辑(检查同拓扑 pending 任务,有则保留,无则删除),设为 Aborted
dispatching取消 Job 提交,走 deprovisioning 复用逻辑,设为 Aborted
running删除 K8s Job,走 deprovisioning 复用逻辑,设为 Aborted

Node pool 处理: abort 场景下的 node pool 清理与正常终态一致——复用 deprovisioning 流程,检查是否有同拓扑的 pending 任务可复用 node pool,有则保留,无则删除。不因 abort 而跳过复用检查。

最终状态: Controller 将任务状态更新为 Aborted,设置 aborted_at 时间戳。

权衡:

  • (+) Abort 复用现有的 deprovisioning 流程进行 node pool 清理,不因 abort 浪费可复用的 node pool
  • (+) 通过状态机驱动(Aborting → Aborted),与现有生命周期管理一致
  • (-) provisioning 阶段的 abort 可能需要等待 node pool 创建完成后再删除,无法立即释放资源
  • (-) abort 检测依赖轮询频率,从用户发起到实际中止存在短暂延迟

9. 状态机设计:与上层 Job 模型对齐 ​

选择: falcon-resource-manager 直接复用上层 Job 的 Status / Reason / Message 三字段模型,不引入独立的 phase、error_phase、error_message 字段。

上层 Job 状态机:

text
Pending → Running (active == replica) → Succeeded (succeeded == replica)
                                      → Failed (failed > 0 or terminating > 0)

falcon-resource-manager 状态机:

text
Pending ──────────────► Running ──► Succeeded
 │ (Reason: Waiting →     │         │
 │  Provisioning →        │         │
 │  Dispatching)          ▼         │
 │                      Failed ◄────┘
 │                        ▲
 └────────────────────────┘
         (任意阶段可失败)

(任意非终态) ──► Aborting ──► Aborted

Status / Reason / Message 对照表:

StatusReasonMessage 示例
PendingWaiting"队列位置 #3,前面有 2 个任务 (user-a: profiling-v7x, user-b: benchmark-v7x)"
PendingProvisioning"正在创建 node pool (supply: reservation)..."
PendingClusterBusy"reservation_remaining_slices=1 reservation_needed_slices=2,等待 reservation 容量释放..."
PendingWaitingForNode"node pool falcon-job-xxxx control-plane RUNNING but 0/1 expected node(s) registered (Ready=0)..."
PendingDispatching"Node pool 就绪,正在提交 K8s Job..."
Running—"Pod 运行中 (1/1 active)"
Succeeded—""
FailedNodePoolTimeout"node pool 创建超时 (10m): GKE API timeout"
FailedNodePoolFailed"node pool 创建失败: quota 'TPUS_PER_PROJECT' exceeded"
FailedJobSubmitFailed"K8s Job 提交失败: admission webhook denied"
FailedJobFailed"Pod terminated: OOMKilled (exit code 137)"
FailedTimeout"任务超时 (3600s),已取消 Job"
Aborting—"正在中止,等待 K8s Job 删除和 node pool 清理..."
Aborted—"用户中止"

Reason 设计原则:

  • CamelCase 程序化标识,上层可用于条件判断和分类统计
  • 仅在 Pending 和 Failed 状态下设值,其他状态留空(Status 本身已足够表达语义)
  • Pending 时表达"当前在做什么"(Waiting / Provisioning / ClusterBusy / WaitingForNode / Dispatching)
  • Failed 时表达"为什么失败"(NodePoolTimeout / NodePoolFailed / JobSubmitFailed / JobFailed / Timeout)

Message 设计原则:

  • 人类可读的动态信息,随任务进展实时更新
  • Waiting 阶段承载排队信息:队列位置、前序任务列表、预计等待原因
  • Provisioning/Dispatching 阶段承载资源获取进度
  • 终态时承载错误详情或完成信息
  • 上层可直接展示给用户,无需额外格式化

权衡:

  • (+) 与上层 Job 完全对齐,Status / Reason / Message 三字段即可表达全部状态信息
  • (+) 消除了 phase、error_phase、error_message 三个独立字段,减少字段冗余
  • (+) Message 承载排队信息,用户可直观看到"前面还有谁、排第几"
  • (-) Reason 需要维护一组约定的枚举值,falcon 和上层需要对齐
  • (-) Message 为自由文本,格式化一致性需要在 controller 侧约束

任务全生命周期编排 ​

一个任务从创建到结束的完整流程,包括各组件的协作关系、状态流转和异常路径:

text
╔══════════════════════════════════════════════════════════════╗
║  Phase 1: 拾取任务                         [Task Poller]     ║
╠══════════════════════════════════════════════════════════════╣
║                                                              ║
║  上层组件 ── API Server ──► DB (status=Pending)              ║
║                              │                               ║
║                   Task Poller 轮询 API Server 拾取            ║
║                              │                               ║
║                      校验字段、匹配 TPU 类型                   ║
║                      选择目标集群(优先有空闲 node pool)       ║
║                              │                               ║
║                      Reason → Waiting                        ║
║                      Message → "队列位置 #N, ..."            ║
║                      进入 (cluster, topology) 队列            ║
║                              ▼                               ║
╠══════════════════════════════════════════════════════════════╣
║  Phase 2: waiting → provisioning           [Scheduler]       ║
╠══════════════════════════════════════════════════════════════╣
║                                                              ║
║        检查队列槽位(同队列是否有任务正在 provisioning/        ║
║        dispatching)                                         ║
║        ┌──── 占满 ────┐       ┌──── 空闲 ────┐               ║
║        ▼              │       ▼              │               ║
║   继续等待            │  Pick Job 规则选中:                    ║
║   (等待放行条件:       │  1. 同 actor FIFO                    ║
║    Pod Running 或      │  2. 跨 actor Round-Robin             ║
║    任务终态)           │       │                              ║
║        │              │       │                              ║
║        └──────────────┘  Reason → Provisioning               ║
║                              ▼                               ║
╠══════════════════════════════════════════════════════════════╣
║  Phase 3: provisioning → dispatching  [NodePool Manager]    ║
╠══════════════════════════════════════════════════════════════╣
║                              │                               ║
║               检查目标集群是否有同拓扑空闲 node pool           ║
║               ┌──── 有 ────┐       ┌──── 无 ────┐            ║
║               ▼            │       ▼            │            ║
║          复用已有          │  查询 reservation 剩余容量       ║
║               │            │  (GCP Compute API)              ║
║               │            │  ┌─ 充足 ─┐  ┌─ 不足/无 ─┐     ║
║               │            │  ▼        │  ▼           │     ║
║               │            │  reservation  flex-start        ║
║               │            │  └────────┴──────────────┘     ║
║               │            │  GKE API: CreateNodePool()      ║
║               │            │       │                         ║
║               │            │  轮询等待 node pool RUNNING     ║
║               │            │  ╳ 超时/失败 →                  ║
║               │            │    Reason=NodePoolTimeout/       ║
║               │            │          NodePoolFailed          ║
║               │            │    status=Failed, 结束          ║
║               └────────────┴───────┘                         ║
║                              │                               ║
║                      记录 node_pool_name                     ║
║                      Reason → Dispatching                    ║
║                              ▼                               ║
╠══════════════════════════════════════════════════════════════╣
║  Phase 4: dispatching → Running            [Job Submitter]   ║
╠══════════════════════════════════════════════════════════════╣
║                              │                               ║
║               从 spec 渲染 K8s Job manifest                  ║
║               注入 nodeSelector, resources, volumes          ║
║               注入 GCS FUSE volume + 日志重定向              ║
║                              │                               ║
║               提交 Job 到边缘集群                             ║
║               ╳ 失败 → Reason=JobSubmitFailed                ║
║                 Message=K8s API 错误详情                      ║
║                 status=Failed, 触发删除 node pool             ║
║                              │                               ║
║                      记录 job_uid                            ║
║                      status → Running                        ║
║                              ▼                               ║
╠══════════════════════════════════════════════════════════════╣
║  Phase 5: Running → (终态)         [Status Reconciler]       ║
╠══════════════════════════════════════════════════════════════╣
║                              │                               ║
║               Informer 监听边缘集群 Job 状态                  ║
║               (label: managed-by=falcon)                     ║
║                              │                               ║
║                    ┌─────────┴─────────┐                     ║
║                    ▼                   ▼                      ║
║              Job Complete        Job Failed / 超时            ║
║                    │                   │                      ║
║                    └─────────┬─────────┘                     ║
║                              ▼                               ║
╠══════════════════════════════════════════════════════════════╣
║  Phase 6: deprovisioning → 终态       [NodePool Manager]    ║
╠══════════════════════════════════════════════════════════════╣
║                              │                               ║
║               Controller 删除 Job                            ║
║               检查是否有 pending 任务需要同拓扑 node pool     ║
║               ┌──── 有 ────┐       ┌──── 无 ────┐            ║
║               ▼            │       ▼            │            ║
║          保留 node pool    │  GKE API: DeleteNodePool()      ║
║          供下一个 Job 复用  │  等待删除完成                    ║
║               │            │  ╳ 失败 → 重试, 最终 Message     ║
║               │            │    追加错误信息                   ║
║               └────────────┴───────┘                         ║
║                              │                               ║
║                      status → Succeeded / Failed             ║
║                          (取决于 Job 原始结果;                ║
║                           超时归入 Failed;                    ║
║                           abort 场景见 Abort Handler)        ║
║                              ▼                               ║
║                            结束                              ║
╚══════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════╗
║  Abort: (任意活跃阶段) → aborted        [Abort Handler]     ║
╠══════════════════════════════════════════════════════════════╣
║                                                              ║
║  上层组件 ── API Server ──► DB (status=Aborting,             ║
║                              aborting_at=now())              ║
║                              │                               ║
║               Controller 检测到 Aborting 状态                 ║
║               根据当前阶段执行中止:                            ║
║                              │                               ║
║     ┌────── waiting ──────┐  │  ┌── provisioning ──┐         ║
║     ▼                     │  │  ▼                  │         ║
║  从队列移除               │  │  取消/等待创建完成    │         ║
║     │                     │  │  │                  │         ║
║     │  ┌── dispatching ──┐│  │  │                  │         ║
║     │  ▼                 ││  │  │                  │         ║
║     │  取消 Job 提交      ││  │  │                  │         ║
║     │  │                 ││  │  │                  │         ║
║     │  │  ┌── running ──┐││  │  │                  │         ║
║     │  │  ▼             │││  │  │                  │         ║
║     │  │  删除 K8s Job  │││  │  │                  │         ║
║     │  │  │             │││  │  │                  │         ║
║     │  └──┴─────────────┴┴┘  │  │                  │         ║
║     │        │               │  │                  │         ║
║     │        └── 有 node pool ──┘                  │         ║
║     │            │                                 │         ║
║     │     deprovisioning (复用逻辑):                │         ║
║     │     检查同拓扑 pending 任务                    │         ║
║     │     ┌── 有 ──┐    ┌── 无 ──┐                 │         ║
║     │     ▼        │    ▼        │                 │         ║
║     │  保留 node pool  删除 node pool              │         ║
║     │     └────────┴────┘        │                 │         ║
║     └────────────────────────────┘                 │         ║
║                              │                               ║
║                      status → Aborted                        ║
║                      设置 aborted_at                         ║
║                              ▼                               ║
║                            结束                              ║
╚══════════════════════════════════════════════════════════════╝

异常路径汇总 ​

异常场景ReasonMessage 来源处理方式
node pool 创建超时NodePoolTimeoutGKE API 超时信息status→Failed,无需清理 node pool
node pool 创建失败NodePoolFailedGKE API 错误响应(quota 不足、reservation 无效等)status→Failed
Job 提交失败JobSubmitFailedK8s API 错误响应(资源冲突、权限不足等)status→Failed,触发 node pool 删除
Job 执行失败JobFailedJob condition message + Pod 事件(OOM、镜像拉取失败等)Controller 删除 Job→检查复用→无则删除 node pool→status→Failed
Job 超时Timeout超时信息(dispatched_at + timeout_seconds)取消 Job→Controller 删除 Job→检查复用→无则删除 node pool→status→Failed
node pool 删除失败—GKE API 错误响应重试,最终失败则追加 Message,status 仍按 Job 原始结果设置
孤儿 node pool——扫描 falcon-* 前缀的 node pool,无对应非终态 job 则清理
Abort (waiting)Aborted上层组件设置 Aborting从队列移除→status→Aborted,设置 aborted_at
Abort (provisioning)Aborted上层组件设置 Aborting取消/等待 node pool 创建完成→检查复用→无则删除 node pool→status→Aborted
Abort (dispatching)Aborted上层组件设置 Aborting取消 Job 提交→检查复用→无则删除 node pool→status→Aborted
Abort (running)Aborted上层组件设置 Aborting删除 K8s Job→检查复用→无则删除 node pool→status→Aborted

实施阶段 ​

Phase 1: 核心循环(MVP) ​

  • Controller 框架:Task Poller + Job Submitter + Status Reconciler
  • 多集群支持(clusters 注册表)
  • 集群选择(TPU 类型匹配 + node pool 复用优先 + 队列深度均衡)
  • Node pool 按需创建与延迟删除复用
  • 数据库 schema(clusters 表由 Falcon 管理,job 表由上层定义)
  • 基础 Job 渲染
  • 状态回写
  • 日志通过 GCS FUSE Pod 内重定向写入 GCS
  • 使用 poc-tpu-partner/tpuv7x-64-node 作为首个边缘集群验证

Phase 2: 健壮性 ​

  • 重试逻辑(可配置退避策略)
  • 超时强制执行
  • 优雅关闭(drain in-flight reconciliation)
  • 健康检查 / readiness probe

Phase 3: 可观测性 ​

任务事件上报: controller 在任务状态流转时更新 Reason / Message,供上层组件查询任务进度,减少手动 describe job / 集群操作。Message 示例:

  • "队列位置 #3,前面有 2 个任务 (user-a: profiling-v7x, user-b: benchmark-v7x)"
  • "正在创建 node pool (supply: reservation)..."
  • "reservation_remaining_slices=1 reservation_needed_slices=2,等待 reservation 容量释放..."
  • "node pool falcon-job-xxxx control-plane RUNNING but 0/1 expected node(s) registered (Ready=0)..."
  • "Node pool 就绪,正在提交 K8s Job..."
  • "Pod 运行中 (1/1 active)"

集群与资源观测: controller 暴露各集群维度的资源信息:

  • Reservation 剩余容量(GCP Compute API count - inUseCount)
  • 各集群活跃 node pool 数量及拓扑
  • 各 (cluster, topology) 队列深度和 running job 数

暴露方式: TBD——可能通过 HTTP/gRPC 接口供上层 API Server 采集,也可能直接作为 Prometheus metrics 暴露

其他:

  • 结构化日志(任务生命周期事件)
  • Prometheus 指标(下发延迟、任务耗时、失败率)
  • 审计日志
  • Grafana 面板

Phase 4: 高级特性 ​

  • 优先级调度(跨集群任务优先级)