2026-06-25 06:00:00

多个团队共用一个 Kubernetes 集群,A 团队提交了一批训练任务,几十张 GPU 很快就被占满;B 团队新提交的 Job 只能一直 Pending。 因为,而是 Kubernetes 原生采用"先到先得"的调度方式,没有 Job 队列,也没有多租户配额管理。
Kueue 正是 Kubernetes 官方为此提供的解决方案。它不替换 kube-scheduler,只负责 Job 的排队和准入,在此基础上实现资源配额管理和公平调度。
Kueue 是 Kubernetes SIGs 维护的官方 Job 级队列管理系统,负责决定 Job 何时准入(Admit)、何时被驱逐(Preempt),核心目标是管理资源配额和多租户公平调度。
和 Volcano 最大的区别:Kueue 不替换 kube-scheduler,它只管"排队和准入",调度还是交给原生调度器。
|
|
多租户配额管理:通过 ClusterQueue 为不同团队划分资源配额
公平调度:基于 Dominant Resource Shares(DRS)的公平共享算法,防止资源被单一团队长期独占
队列排队:Job 提交后按优先级排队,配额不足时挂起等待
Cohort 借调:同一 Cohort 内的 ClusterQueue 可以借用彼此空闲配额,用完归还
弹性配额:支持 nominalQuota(保底)+ borrowingLimit(借用上限)+ lendingLimit(出借上限)三层额度控制
标准兼容:原生支持 K8s Job / JobSet / PyTorchJob / TFJob / RayJob 等,无需改 Job YAML
或者说 Kueue 解决了什么原生 K8s 解决不了的问题?
原生 Kubernetes 的资源管理是"先到先得",没有队列的概念:
| 场景 | 原生 Kubernetes | Kueue | 优势 |
|---|---|---|---|
| 配额管理 | ResourceQuota 粗粒度控制 | ClusterQueue 细粒度配额(按 GPU 型号/节点池划分) | 精确到资源类型和 Flavor |
| 多租户公平 | 先到先得,无法保障公平 | Fair Sharing 基于 DRS 的公平分配 | 防止资源独占 |
| Job 排队 | Pending 后只能等 | 支持优先级队列、FIFO 策略 | 按优先级有序调度 |
| 跨团队借调 | 不支持 | Cohort 机制,空闲配额自动借出、按需归还 | 提高集群利用率 |
举一个典型场景:公司有两个 AI 团队共享一个 GPU 集群,各有 50% 的 A100 配额。团队 A 没任务时,团队 B 可以借用全部 GPU;团队 A 提交任务后,Kueue 会通过 preemption 让团队 B 释放借用的资源,回到公平状态。
原生 K8s 做不到这一点。
| 维度 | Kueue | Volcano |
|---|---|---|
| 定位 | 队列 + 配额管理(轻量) | 批处理调度平台(重量) |
| 架构 | 不替换调度器,旁路管理 | 自定义调度器,替换 kube-scheduler |
| 归属 | Kubernetes SIGs 官方 | CNCF(华为发起) |
| 部署复杂度 | 一个 Controller | Scheduler + Controller + Admission 三个组件 |
| Gang Scheduling | 通过 All-or-Nothing with Ready Pods(超时机制) | 原生 Gang Scheduling |
| Job API | 标准 K8s Job(自动创建 Workload) | 自定义 VolcanoJob(vcjob) |
更多参考:Kueue 官方文档、Volcano 系列文章
在部署之前,先理解 Kueue 的 5 个核心对象:
|
|
ResourceFlavor:Flavor,代表不同类型的资源(如 A100 vs T4、Spot vs On-Demand),可以绑定 nodeLabels / taints
ClusterQueue:集群级队列,定义资源配额(nominalQuota / borrowingLimit / lendingLimit),是配额管理的核心
LocalQueue:命名空间级队列,用户直接和它打交道,指向一个 ClusterQueue
Cohort:ClusterQueue 的组,同组内可以互相借调空闲配额
Workload:Kueue 的调度单元,Kueue 会为每个 Job 自动创建对应的 Workload,用户一般不需要手动创建
一个 Job 从提交到运行,经过 Kueue 的完整流程:
|
|
关键点:Kueue 只负责"准入"决策,Pod 真正调度到哪个节点还是 kube-scheduler 决定。
Kueue 部署非常轻量,只需要一个 Controller。
Kueue 要求 Kubernetes 1.29+,本文使用 Kubernetes v1.36.1 部署最新的 Kueue v0.18.1。
官方提供了两种安装方式,推荐 Helm:
|
|
|
|
无法访问
registry.k8s.io可以从 GitHub 下载 chart 包安装:
1 2 3 4helm install kueue https://github.com/kubernetes-sigs/kueue/releases/download/v0.18.1/kueue-0.18.1.tgz \ --namespace kueue-system \ --create-namespace \ --wait --timeout 300s
|
|
|
|
|
|
一个 Pod 就搞定了,这就是 Kueue 轻量的地方。
接下来我们跑一个最小化 Demo:创建 ResourceFlavor → ClusterQueue → LocalQueue → 提交 Job,完整走一遍 Kueue 的工作流程。
Kueue 使用三种资源来管理作业排队:
|
|
|
|
|
|
|
|
队列创建成功,当前没有 Workload。
注意看,这里用的就是标准的 K8s Job,只需要加一个 label 就能接入 Kueue:
|
|
|
|
|
|
|
|
可以看到 Kueue 自动为 Job 创建了对应的 Workload,Workload 状态为 Admitted(已准入),3 个 Pod 正常运行。
|
|
|
|
Job 完成后,Workload 也会被标记为 Finished:
|
|
|
|
到这里,我们完整走了一遍 Kueue 的工作流程:创建队列 → 提交 Job → Kueue 自动准入 → Pod 运行 → 任务完成。
整个过程中,我们用的是标准的 K8s Job,唯一的变化就是加了一个 kueue.x-k8s.io/queue-name 标签。这就是 Kueue “不替换调度器、旁路管理"设计哲学的体现。
Kueue 是 Kubernetes SIGs 官方出品的 Job 队列与配额管理系统,它的核心设计哲学是”只管排队和准入,不动调度器",这让它非常轻量且对现有集群侵入性极小。
本文介绍了 Kueue 的背景、核心概念和部署方式,并通过一个最小化 Demo 走完了"创建队列 → 提交 Job → 自动准入 → 任务完成"的完整流程。
下一篇我们将深入解析 Kueue 的五大核心对象(ResourceFlavor / ClusterQueue / LocalQueue / Cohort / Workload),搞清楚它们各自的职责和配置细节。
2026-06-17 06:00:00

OCI 规范建立的目的就是将容器镜像格式标准化,正如其名,OCI 镜像在之前一直用来跑容器,但现在它还能干更多事。
Kubernetes v1.36 里 ImageVolume 特性正式 GA 了,该特性允许我们把 OCI 镜像直接作为 Volume 挂载到 Pod 里。现在 OCI 镜像在 K8s 里不再只能跑容器了,模型权重、配置文件、安全签名、CI/CD 工件,只要是只读数据,都可以打包成 OCI 镜像供 Pod 挂载使用。
这个特性从 v1.31 以 Alpha 引入,经过 v1.33、v1.35 的 Beta 阶段,终于在 v1.36 正式 GA。GA 之后不用再手动开 Feature Gate,containerd 也原生支持了,开箱即用。
OCI(Open Container Initiative)是 2015 年在 Linux 基金会支持下成立的开放项目,Docker、CoreOS(后来被 Red Hat 收购)和容器行业的主要厂商一起,围绕容器格式和运行时制定了开放标准。Docker 捐出了自己的镜像格式作为基础,社区在此基础上逐步形成了 Runtime、Image 和 Distribution 三大规范。
2017 年 image-spec v1.0 发布,容器镜像的格式算是定下来了。在 OCI 标准下,运行一个容器的过程就是:从 Registry 下载 OCI 镜像 → 解压到 OCI Bundle → OCI Runtime 运行这个 Bundle。整个流程标准化之后,不同 Runtime、不同 Registry 之间可以互操作,不用强行绑定 Docker。
Docker 也把 libcontainer 的实现移动到 runC 捐给了 OCI,社区有了第一个 OCI Runtime 的参考实现。
虽然 OCI 标准最初完全是围绕容器设计的,镜像格式里的 mediaType、config 结构都是为「跑容器」量身定做的。
但是如果我不想跑容器,只想把一堆文件打包分发呢?人们很快发现,OCI Registry 的分层存储和分发机制天然适合分发更多东西。
OCI 镜像的本质是什么?就是一堆只读的层(layer),加上一个 manifest 描述这些层的组织方式,再通过 Registry 的 API 完成分发。同时 OCI Registry 提供了一整套“可寻址、可校验、可去重、可控访问”的分发原语,然后这个模型并不绑定「容器」。
所以社区很自然地想到:能不能把 OCI Registry 当成一个通用的内容分发平台来用?
实际上社区很早就开始在 OCI Registry 里存非镜像内容了,但早期的做法都是 hack——把非镜像内容伪装成容器镜像塞进去,Registry 其实并不知道这些东西不是用来跑的:
OCI Artifacts 就是这么来的,把各种产物存进 OCI Registry、当成通用内容仓库来分发。
这些用法推动了 OCI 规范本身的演进。2024 年,image-spec v1.1.0 正式加入了 artifactType 字段,允许 Manifest 声明「我不是容器镜像,我是一个签名 / 一个 Helm Chart / 一个模型权重」。OCI 对非镜像内容的支持从社区 hack 变成了规范的一部分,OCI Registry 正式成为了一个通用的内容仓库。

现在 OCI Registry 已经变成了一个通用的内容仓库,但问题来了:Helm、Cosign、ORAS 这些工具都在往里塞东西,但到了 Kubernetes 这边,OCI 镜像还是只能拿来跑容器,缺少一个原生的消费方式。
ImageVolume 就是来补上这一块的。它允许在 Pod 中将 OCI 镜像直接作为 Volume 挂载,让 OCI Artifacts 在 K8s 里也能被原生消费,不再只是跑容器。就像这样:
|
|

不过有一点要注意,ImageVolume 挂载是只读的,如果需要在运行时修改挂载的文件,还是得用 PVC。后面会详细讨论这个限制。
ImageVolume 让 OCI Artifacts 在 K8s 里有了第一个原生的消费方式。不过这个能力并不是一步到位的,从 Alpha 到 GA 走了近两年。
ImageVolume 这个特性来源于 KEP-4639,由 SIG Node 和 SIG Storage 共同推动。从 v1.31 Alpha 到 v1.36 GA 走了近两年,具体时间线如下:
| 阶段 | K8s 版本 | 发布时间 | Feature Gate 默认值 | 说明 |
|---|---|---|---|---|
| Alpha | v1.31 | 2024-08 | false |
需要手动开启 Feature Gate |
| Beta(默认关) | v1.33 | 2025-04 | false |
Beta 代码合入但仍默认关闭 |
| Beta(默认关) | v1.34 | 2025-08 | false |
移除 noexec 限制,仍默认关闭 |
| Beta(默认开) | v1.35 | 2025-12 | true |
首次默认启用 |
| GA | v1.36 | 2026-04 |
true(锁定) |
Feature Gate 锁定,v1.39 移除 |
Containerd v2.1.0 才正式支持 ImageVolume,而且没有回移到 v2.0.x 分支,所以用 containerd 的话必须升级到 v2.1.0+。

接下来过一遍每个阶段的变化。
Alpha 阶段 ImageVolume 不支持 subPath,也就是说你只能挂载镜像的整个文件系统,没法只挂载其中的某个子目录。
Beta 阶段(v1.33)解除了这个限制,subPath 和 subPathExpr 都可以用了。对应的 CRI API 也新增了 image_sub_path 字段来支持这个功能。
现在你可以这样用:
|
|
很多时候一个镜像里会放多个目录,有了 subPath 就不用把整个镜像都挂进来了。
Alpha 阶段 ImageVolume 挂载时强制加了 noexec 选项,挂载进来的文件不能被执行。
这个限制在 Beta 阶段(2025-06,PR #5354)被移除了。社区讨论后觉得 noexec 限制过于严格,ImageVolume 的主要用途是分发只读数据,强制 noexec 没有必要,反而限制了某些合理的使用场景,比如挂载包含可执行工具的镜像。
不过 ImageVolume 仍然是只读挂载(ro),读写支持还得等后续的 KEP。
Beta 阶段新增了 3 个 Kubelet 指标,方便监控 ImageVolume 的使用情况。
kubelet_image_volume_requested_total — 请求的 ImageVolume 数量kubelet_image_volume_mounted_succeed_total — ImageVolume 挂载成功的数量kubelet_image_volume_mounted_errors_total — ImageVolume 挂载失败的数量GA 阶段这些指标提升到了 BETA 稳定性级别,可以在 Prometheus 里配告警了。
v1.36 GA 后,ImageVolume Feature Gate 被锁定为默认开启,没法关了。按照 K8s 的惯例,Feature Gate 会在 GA 后 3 个版本移除,也就是 v1.39 会彻底删掉这个 Gate。
所以现在的状态就是:
+featureGate=ImageVolume 注解也被移除了这个虽然不是 K8s 代码的变化,但可能是对使用者影响最大的变化。
Alpha 阶段,containerd 不支持 ImageVolume,想玩的话只能自己动手。参考我之前的文章 K8s V1.31 新特性:ImageVolume,
我当时手动 checkout 了 containerd 的 PR #10579,编译替换二进制文件。编译倒是不难,但是那个 PR 还有个 bug,kubelet 没有把 readOnly 参数透传到 CRI mounts 配置中,导致 containerd 校验 readOnly 失败一直报错。。。没办法,还得手动注释掉校验逻辑,整个流程折腾下来真的挺崩溃的。
现在 containerd v2.1.0 已经原生支持 ImageVolume,直接用就行,不需要任何 hack。
CRI-O 的话从 v1.31 就支持了,v1.34 还增加了 subPath 支持,一直走在前面。
整理一下:
| 变化项 | Alpha (v1.31) | Beta (v1.33-v1.35) | GA (v1.36) |
|---|---|---|---|
| subPath | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| noexec 限制 | 强制 noexec | 移除 | 无限制 |
| 监控指标 | 无 | Alpha 级别 | BETA 级别 |
| Feature Gate | 默认关,需手动开 | v1.33/34 默认关,v1.35 默认开 | 锁定开启 |
| containerd | 需手动编译 PR | v2.1.0 原生支持 | v2.1.0+ |
| CRI-O | v1.31 支持 | v1.34 支持 subPath | 同左 |
| 挂载模式 | 只读 | 只读 | 只读 |
变化聊完了,实际用起来是什么感觉呢。GA 之后用起来比 Alpha 阶段简单太多了,不用再折腾 Feature Gate 和手动编译 containerd 了。
就这么简单,不需要额外配置任何 Feature Gate。
本次验证环境是使用 KubeClipper 安装的 K8s 集群,版本如下:
使用方式和 Alpha 阶段基本一致。先构建一个包含模型文件的 OCI 镜像,用 FROM scratch 就行,不需要任何基础镜像。
为了后面演示 subPath,这里在镜像里放两个模型目录,再放一个配置文件:
|
|
目录结构如下:
|
|
Dockerfile 如下:
|
|
构建并推送到镜像仓库:
|
|
创建 Pod 挂载这个镜像:
|
|
应用到集群,等 Pod Running 后查看挂载内容:
|
|
镜像里的文件都挂载进来了,跟预期一致。
上面那个镜像里放了两个模型目录,如果 Pod 只需要 Qwen2-0.5B,不需要把整个镜像都挂进来,用 subPath 就行:
|
|
验证一下,挂载目录里只有 Qwen2-0.5B 的内容:
|
|
只挂载了 Qwen2-0.5B 目录,Llama2-7B 和 app.conf 都不在。如果 subPath 指定的路径在镜像中不存在,容器创建会直接报错:
|
|
ImageVolume 挂载是只读的,尝试写入会报 Read-only file system:
|
|
最后提几个实际使用中的注意事项。
ImageVolume 挂载是只读的,如果需要运行时修改文件还是得用 PVC,目前没有读写支持的 KEP。Pod 重建时 ImageVolume 会重新解析远端镜像,所以生产环境建议用 digest 而不是 tag 引用镜像,避免 Pod 重建后 tag 被覆盖导致拿到非预期版本。
镜像层共享能省磁盘,两个 ImageVolume 引用的镜像有相同层的话 containerd 只存一份,但大模型镜像多了也要注意节点磁盘压力。
OCI 从 2017 年 image-spec v1.0 发布到今天,走了一条挺清晰的路:先是容器镜像格式标准化,然后 OCI Registry 被社区 hack 成通用内容仓库,接着 image-spec v1.1.0 和 distribution-spec v1.1.0 把这种用法正式写入规范(artifactType + referrers API)。
OCI Artifacts 从社区变通变成了标准的一部分。ImageVolume GA 等于 Kubernetes 也跟上了,OCI Artifacts 在 K8s 里有了第一个原生的消费方式。
往后看,读写支持可能是下一个方向,目前只读挂载还是限制了些场景,社区已经有相关讨论。另外 ImageVolumeWithDigest 子特性在 v1.36 也 GA 了,Pod Status 里能直接看到挂载镜像的 digest,版本追溯方便不少。
2026-06-12 04:00:00

不知道你们有没有发现,进入 AI 时代之后,传统微服务那套 HPA 好像突然不好使了。
CPU 20%,内存 30%,监控面板一片岁月静好,但你的 AI 推理服务已经在排队了。HPA 看了一眼指标,嗯,一切正常,不用扩。
这不是 HPA 的 bug,是它背后那套「资源使用率等于负载压力」的逻辑,在 AI 推理场景下从根上就不成立。问题不在调参,在于观测信号本身就是失真的。
今天就来聊聊这件事的来龙去脉,以及目前我们认为最合理的解法,KEDA。
先说清楚 HPA 的底层逻辑。
HPA(Horizontal Pod Autoscaler)的设计假设是,资源使用率跟负载压力成正比。CPU 高了说明忙,内存高了说明扛不住,扩就完事了。对传统 Web 服务来说,这个假设基本成立,请求来了 CPU 就涨,请求走了 CPU 就降,很线性。
但 AI 推理服务把这个假设的每一个环节都打破了。以图片生成服务为例。

图片生成服务启动的时候,第一件事就是把模型权重加载到 GPU 显存里。比如 Stable Diffusion SDXL 这种级别的模型,光权重就要好几个 GB,加载完之后,这些显存就一直占着,不管你有没有请求,它都是满的。
所以你如果用 GPU 显存使用率做 HPA 指标,服务刚启动就已经「满」了。HPA 会觉得你一直需要扩容,即使压根没有任何请求在处理。
生成一张图片,一般要迭代几十上百步,整个过程可能就要几秒到几十秒。在这个过程中,GPU 使用率直接飙到 90% 甚至 100%。
问题在于,一个请求就能把 GPU 跑满。1 个请求和 10 个请求,在 GPU 使用率上看起来差不多,都是满载。 HPA 那套「采样窗口取平均值再做判断」的逻辑,放到这里完全没意义,因为不管采多少秒,看到的都是满载。
更离谱的是,你按 GPU 使用率做 HPA,一个请求过来就触发扩容,扩出来的 Pod 加载模型要 30 秒到 2 分钟,等它准备好,原来那个请求早就处理完了。
你可能会想,那 CPU 呢?内存呢?
推理服务的 CPU 主要做些预处理和调度,内存主要是模型权重的 CPU 侧缓存。0 个请求和 10 个请求的 CPU/内存使用,差别可能就是几个百分点。
所以,三个问题,三条路,全堵死了。HPA 最依赖的两个指标,在 AI 推理场景下,全部失真。
| 指标 | 传统 Web 服务 | AI 推理服务 |
|---|---|---|
| GPU 显存 | 随负载动态变化 | 模型加载后常驻满载 |
| GPU 计算使用率 | 与请求量正相关 | 一个请求就能拉满,无法反映积压程度 |
| CPU 使用率 | 与请求量正相关 | 仅做预处理,变化极小 |
| 内存使用率 | 与请求量正相关 | 主要是模型缓存,基本不变 |
| HPA 扩容效果 | 准确 | 失真 |
这就是为什么 HPA 在 AI 场景下不是扩错了,就是扩晚了,要么干脆就不扩。它的设计假设被 AI 服务或者 GPU 的工作模式彻底打碎了。
那怎么办?
HPA 之所以在 AI 场景下失灵,是因为它在看「机器忙不忙」。但 AI 推理的真正压力不在机器指标上,在「有多少活等着干」。
那「活」在哪?
形式不同本质一样,都是「有多少事还没干」。
对 AI 推理来说,最常见的载体是消息队列。图片生成、视频生成这些请求动辄几秒、几十秒甚至几分钟的,一般都会走异步处理。用户请求先进 MQ 排队,Worker 从队列里消费。队列里积压了多少消息,就是有多少待处理的推理请求。
这就是最真实的扩容信号。不根据指标看 GPU 忙不忙,直接看系统里有多少活没干完。

KEDA 干的就是这件事。
KEDA 全称 Kubernetes Event-driven Autoscaling,核心思路特别简单,根据外部事件源的状态来决定扩缩容,而不是看 Pod 的资源使用率。消息队列里有积压就扩,没消息就缩,就是这么直接。
用一张表对比一下 HPA 和 KEDA 的核心差异。
| 对比维度 | HPA | KEDA |
|---|---|---|
| 扩容信号来源 | Pod 资源使用率(CPU/内存/自定义指标) | 外部事件源(队列深度、消费延迟等) |
| 信号本质 | 机器侧,看「忙不忙」 | 需求侧,看「活多不多」 |
| Scale to Zero | 不支持,minReplicaCount 最低为 1 | 原生支持 |
| 外部指标接入复杂度 | 需要 Prometheus Adapter + ServiceMonitor + 自定义指标 + HPA 规则 | 一个 ScaledObject CRD 搞定 |
| 适合场景 | 传统 Web 服务,资源使用率与负载线性相关 | 异步处理,负载压力体现在队列积压 |
KEDA 支持 60 多种 Scaler,包括 Kafka、RabbitMQ、Redis、Prometheus、Cron 等,每个 Scaler 对接一种外部事件源,直接读取事件源的指标来决定扩缩容。
其中几个关键差异值得说一下。
可以缩到零。 没有请求的时候直接把 Pod 数量缩到 0,有请求来了再拉起来。对 GPU 这种昂贵资源来说,不用的时候就不占是最优的。
扩容信号直接来自需求侧。 不再绕一圈去猜 GPU 忙不忙,直接看队列深度、消费延迟这些业务强相关指标。
配置更简单。 用 HPA 接外部指标,你得部署 Prometheus Adapter、写 ServiceMonitor、配自定义指标、再写 HPA 规则,一整套流水线。KEDA 就一个 ScaledObject CRD,声明一下触发器类型和参数就搞定了。
下面用一个 demo 把整个流程跑一遍,让大家有一个大致的印象。
我们搞了一个 demo 项目,模拟一个典型的 AI 图片生成服务架构,演示完整的流程。 整体架构如下。

为什么选图片生成? 因为图片/视频生成是 KEDA 扩容最典型的 AI 场景。这类请求处理时间长,没法像 ChatGPT 那样实时返回,必须走异步架构。用户提交请求后拿到一个 task ID,过几秒再来查结果。这个架构和真实 Stable Diffusion API、ComfyUI 工作流、甚至 Sora 类视频生成服务的后端,逻辑是一样的。
为什么加了 DB? 因为真实的图片生成服务,用户提交请求后需要能查到任务状态,从 queued 到 processing 到 completed,这个生命周期需要持久化存储。MQ 只负责通知 Worker 有新任务,不负责存状态。
真实的 AI 推理当然不会用 sleep,这里用 sleep 只是为了模拟推理耗时,让你不用 GPU 也能跑通完整的 KEDA 扩缩容流程。架构、队列、扩缩容逻辑,跟生产环境一模一样。
代码和全部部署文件在 GitHub,keda-ai-queue-demo,下面逐个讲关键部分。
需要一个跑着 Kubernetes 的集群,并且装好 KEDA。
|
|
装完确认一下。
|
|
应该能看到 keda-operator 和 keda-operator-metrics-apiserver 两个 Pod 在运行。
克隆项目,直接 apply 即可,镜像已推送到 Docker Hub。
|
|
一行搞定部署,PostgreSQL、RabbitMQ、API、Worker、KEDA ScaledObject 全部拉起来。
|
|
确认 Pod 状态。
|
|
应该看到 PostgreSQL、RabbitMQ、API 三个 Pod 在运行,Worker 的 replicas 是 0,因为队列里还没消息,KEDA 不会给它扩容。
整个事件驱动扩缩容的配置就这一个 YAML。
|
|
逐个解释一下。
scaleTargetRef: worker,KEDA 只管 Worker 这个 Deployment,API 和 PostgreSQL 跟 KEDA 无关。
pollingInterval: 5,每 5 秒检查一次队列状态。AI 推理服务对响应时间敏感,轮询间隔可以设短一点,默认是 30 秒。
cooldownPeriod: 60,队列清空后 60 秒才缩容。demo 里设短一点方便观察,实际使用建议设 300 秒以上,避免模型反复加载。
minReplicaCount: 0,启用 scale to zero,没请求的时候不占资源。
mode: QueueLength,value: "5",每个 Pod 处理 5 条消息。队列里有 23 条消息,KEDA 就算出需要 ceil(23/5) = 5 个 Pod。
至于 KEDA 怎么连接 RabbitMQ,它通过
TriggerAuthentication从 Kubernetes Secret 里读取连接信息,不在 ScaledObject 里硬编码。具体可以看 demo 仓库的deploy/keda.yaml。
端口转发 API。
|
|
创建一批任务。
|
|
查一下任务状态。
|
|
状态流转是 queued -> processing -> completed,整个过程都有时间戳可以看。
打开另一个终端,观察 Worker Pod 数量变化。
|
|
你会看到 Worker 从 0 个 Pod 开始,KEDA 检测到队列有积压后,根据我们的配置把 Worker 扩到 4 个 Pod 并发消费。队列清空、冷却期结束后,Worker 又缩回 0。
KEDA 的操作日志也能看到完整过程。
|
|
|
|
0 -> 1 -> 4 -> 0,这就是 KEDA 事件驱动扩缩容的完整过程。

清理环境。
|
|
配置写起来简单,但真正使用起来还是有一些需要注意的地方:
冷启动问题。 从 0 扩到 N 意味着 Pod 要从零开始启动,包括加载模型到 GPU。大型图片生成模型的加载可能要 30 秒到 2 分钟,这段时间请求只能排队。如果业务对延迟敏感,最好把 minReplicaCount 设为 1,始终保留一个热 Pod。
cooldownPeriod 别设太短。 缩容后又来请求,重新扩容 + 模型加载,这个周期可能要一两分钟。频繁缩容再扩容的代价很大,Pod 反复启停,GPU 资源白白浪费在模型加载上。建议至少设 300 秒,让流量真正稳定下来再缩。
HPA 会被 KEDA 接管。 KEDA 会在底层创建一个 HPA,所以同一个 Deployment 不能再手动创建 HPA,会冲突。
轮询间隔要权衡。 pollingInterval 设得短,响应快,但对外部系统的 API 调用量也大。如果你有几百个 ScaledObject,每个 5 秒轮询一次,对消息队列的压力不小。根据实际场景在响应速度和资源消耗之间找个平衡点。
聊了这么多,最后收一下。
HPA 在 AI 推理场景下失灵,说到底是它的设计假设,资源使用率跟负载成正比,被 GPU 工作模式打破了。GPU 显存常驻、使用率脉冲、CPU/内存不跟负载走,HPA 看到的信号全是失真的。
KEDA 的思路是绕过资源指标,直接看需求侧的信号。消息队列里有多少消息在排队,就是最真实的扩容依据。
这个思路其实不只适用于 AI 推理,所有异步处理场景都有类似的扩缩容需求。只不过 AI 推理把这个问题放大了,因为 GPU 太贵了,扩错一台就是真金白银的浪费。
几个要点:
如果你的 AI 推理服务还在用 HPA,建议重新审视一下扩容策略,可能它已经在「失灵」了,只是你还没注意到。
2026-06-03 04:00:00

KubeClipper 发布了 1.6.0 版本:支持 Kubernetes 1.36,容器运行时从 Containerd 1.x 升级到 2.x,Calico 更新到 v3.31.5。kcctl 新增了 kcctl set cluster、kcctl operation 等命令,并优化了 Registry 管理体验,修复了大量稳定性问题。
KubeClipper 是一个轻量便捷的 Kubernetes 多集群全生命周期管理工具,旨在提供易使用、易运维、极轻量、生产级的 Kubernetes 多集群管理服务,让运维工程师从繁复的配置和晦涩的命令行中解放出来,实现一站式管理跨区域、跨基础设施的多 K8S 集群。
如果你是第一次接触 KubeClipper,可以通过以下步骤快速上手:
curl -sfL https://oss.kubeclipper.io/get-kubeclipper.sh | KC_REGION=cn bash -
kcctl deploy
kcctl create cluster --name demo --master YOUR_IP --untaint-master
http://YOUR_IP:8080,账号 admin/Thinkbig1
四条命令,从零到一个跑起来的 K8s 集群。
1.6.0 的主要变化集中在三块:K8s 最新版本支持、Containerd v2 适配、kcctl 命令行改进。
本次更新的主要亮点:
| Kubernetes | Calico | Containerd |
|---|---|---|
| v1.36.1(默认) | v3.31.5 | v2.2.4 |
| v1.35.0 | v3.29.6 | v1.7.29 |
| v1.34.2 | v3.29.6 | v1.7.29 |
Containerd v2:容器运行时大版本跃迁
config.toml version=3:CRI v1alpha2 移除,仅保留 CRI v1Calico v3.31:eBPF 数据面走向成熟
kcctl set cluster
新增 kcctl set cluster 命令,支持在集群创建后修改外部访问配置:
|
|
同时 kcctl create cluster 也新增了 --external-port 和 --external-domain-port 参数,支持在创建集群时直接指定外部访问端口。
kcctl operation 命令
1.6.0 重写了 kcctl operation 命令,支持按集群名称查看操作记录,还加了 TUI 交互模式:
|
|
还有三个新子命令:
|
|
旧的 kcctl logs 命令已标记为废弃,后续版本将移除。
kcctl registry 优化
kcctl registry deploy 完成后自动将节点信息(IP、端口、SSH 凭据)保存到 ~/.kc/registry-config.yaml,后续执行 list、push、delete 等命令时无需重复指定 --node 和 SSH 参数,CLI 参数优先级高于配置文件kcctl registry push 支持 tar 格式的镜像包另外清理了多个废弃命令,修复了 CLI 参数解析和错误处理等问题。
这一版包含大量的稳定性修复:
registry.configs.{host}.auth 部分,保留用户已有的配置(如 GPU Operator 添加的配置)
|
|
输出示例:
|
|
|
|
看到下面的 banner 就说明部署完成了:
|
|
安装过程中需要去阿里云下载离线安装包,大概 1 分钟即可下载完成。
安装完成后,打开浏览器,访问 http://$IP 即可进入 KubeClipper 控制台。

您可以使用默认帐号密码 admin / Thinkbig1 进行登录。
用 kcctl 创建一个集群试试:
查看当前 agent 节点
|
|
创建集群:
|
|
大概两分钟装完,用以下命令看实时日志:
|
|
新版还加了 TUI 日志查看器:
|
|

集群装好了,用 kubectl 看一下状态:
|
|
几条命令,一个单节点 K8s 集群就起来了。
1.5.0 版本引入的工作负载管理功能,可以直接在 Web UI 中管理 Deployment、StatefulSet 等工作负载。

这一版主要干了三件事:跟上 K8s 最新版本、适配 Containerd v2、把 kcctl 顺手打磨了一遍。
组件升级:
命令行优化:
稳定性提升:
2026-06-02 04:00:00

随着 Vibe Coding 的流行,开源社区也是涌现出了大量的 AI 编程工作流,例如: Superpowers、Everything Claude Code、Spec Kit、OpenSpec、gstack、Get Shit Done 等等。 今天给大家分享的是 OpenSpec + Superpowers 的协同工作流。
官方的描述是:Spec-driven development (SDD) for AI coding assistants.
即:OpenSpec 就是为 AI 编程助手打造的 Spec 驱动开发(SDD)框架。
在 Vibe Coding 时代,Spec 是很重要的,在开始之前很有必要和 AI 在需求上达成一致。
使用 OpenSpec 之后每个变更(Change)对应一个文件夹,包含以下四类结构化文档:
proposal.md:变更意图,定义为什么做specs/:具体 Spec,定义做什么design.md:具体技术实现方案,定义怎么做tasks.md:具体实现顺序,将变更拆分为一个个小任务简而言之:OpenSpec 可以让 AI 在写代码之前先和人对齐需求,并提供完整的 Spec 生命周期管理,便于追踪。
官方的描述是:An agentic skills framework & software development methodology that works.
即:Superpowers 是一套 AI 编程 Agent 的技能体系 + 开发方法论。
它的核心不是生成代码,而是通过一套可组合的"技能"系统,强制 AI 像资深工程师一样工作,遵循测试驱动开发、代码审查等最佳实践。
简而言之:Superpowers 可以让 AI 像资深工程师一样干活。
在 Vibe Coding 时代,文档的重要性大幅提高。古法编程时代,需求对齐主要靠开会,Vibe Coding 时代,则需要通过具体的 Spec 变更文档来对齐。而 OpenSpec 则是提供了很好的 Spec 生命周期管理功能,可以通过 Spec 追溯到具体变更细节。
AI 生成代码是很容易堆砌成屎💩山的(当然,古法编程也一样哈哈哈)。
因为 AI 上下文有限,很难掌控全局,因此每次 Vibe Coding 产出的或许都是局部最优解,但是放到整体看就不一定合适,另外 AI 生成的代码都不够精简,代码量是很大的,毕竟 Vibe Coding 主打一个快。
这也是为什么 Github 上很多开源项目都吐槽遭受到了 PR 攻击。Agent 批量扫描代码提交 PR,可能整个过程都没有人介入,但是维护者要挨个 review,面对成堆的 PR 直摇头。
Superpowers 通过强制的 TDD、YAGNI、DRY 等工程原则,能很大程度上能够提升代码质量。
协同工作的核心思想是:用 OpenSpec 生成和管理 Spec,然后用 Superpowers 这个"资深工程师"来执行 Spec。
单独使用各有短板,二者结合刚好形成 “规格驱动开发(SDD) + 测试驱动开发(TDD)” 的完整工作流。
单独使用 OpenSpec 时工作流是这样的:
流程简单,文档驱动,但代码实现阶段(apply)的质量保障较弱。
单独使用 Superpowers 时的工作流是这样的:
产出代码质量高,但前期的需求输入和最终的设计决策缺乏一个结构化的、可长期保存的载体,容易丢失在聊天历史中。
二者协作后的工作流是这样的:
就是使用 /superpowers:brainstorm → /superpowers:write-plan → /superpowers:subagent-driven-development 来替代比较弱的 /opsx:apply 以提升代码质量。
安装
OpenSpec 直接通过 npm 安装
|
|
然后在项目根目录进行初始化
|
|
需要在项目目录下执行 init 命令初始化,初始化之后就可以在 claude 终端里使用了。
之后,重新打开 claude 就可以用上 OpenSpec 了。
OpenSpec 使用整体流程就是围绕 proposal 的:
创建 proposal
|
|
命令执行完成后会自动生成:openspec/changes/create-todo-app/ 目录、提案文档、设计模板、任务清单
应用 proposal
当 proposal 完全确定下来之后,就可以让 AI 根据 Spec 一步一步实现了。
|
|
归档 proposal
当 proposal 实现完成后,就可以进行归档保存。
|
|
安装
Superpowers Claude Code 插件
|
|
使用
Superpowers 安装后会在 Claude Code 中注册多个技能,可手动触发,部分环节会自动流转,比如最常见的几个 Skill:
brainstorming:头脑风暴,澄清需求和技术细节write-plan:基于 brainstorming 结果生成实现计划,brainstorming 完成后自动进入subagent-driven-development:按计划启动子代理逐任务实现,强制 TDD + 两阶段代码审查,plan 写好后自动开始第一步:使用 OpenSpec 生成初步 Spec
|
|
效果
|
|
第二步:使用 Superpowers 继续细化上一步的 Spec
|
|
输出如下:
|
|
第三步:使用 Superpowers write plan,根据 Design 文档生成 Plan。
|
|
第四步:使用 Superpowers 的 subagent-driven-development 实现
输出
|
|
第五步:OpenSpec + Superpowers 双重验证,分别验证是否完成。
|
|
第六步:OpenSpec 归档 Spec,纯 OpenSpec 操作:
|
|
所有变更将被保存至 openspec/changes/archive/,支持版本追溯与团队共享。
之前按照 Demo 中的流程,通过 Prompt 串联基本实现了 OpenSpec + Superpowers 的协同工作流。
然后刚好最近在 Github 上发现了 Comet 工具,它把整个协作流程自动化了。

Comet 将 OpenSpec、Superpowers 串联为五阶段自动化流水线:
同时 Comet 注册了以下技能,其中 5 个主流程技能对应协同工作流的各个阶段,另外 2 个是快捷路径:
/comet:主入口,自动检测当前阶段并分派到对应子命令/comet-open:阶段 1 — 打开变更(提案、设计、任务分解)/comet-design:阶段 2 — 深度设计(头脑风暴、设计文档)/comet-build:阶段 3 — 规划与构建(实现计划、代码提交)/comet-verify:阶段 4 — 验证与完成(测试、验证报告)/comet-archive:阶段 5 — 归档(delta spec 同步、状态标注)/comet-hotfix:快捷路径 — 快速 bug 修复,跳过头脑风暴,不需要能力设计/comet-tweak:快捷路径 — 小改动,如文案调整、配置调整、文档或 Prompt 优化一般我们只会用到/comet 命令,由 Comet 根据当前状态自动判断下一步该执行什么。
同时为了提升效率,对于一些小改动 comet 也提供了 /comet-hotfix 和 /comet-tweak 命令。
comet 通过 npm 安装
|
|
首先到项目根目录进行初始化
|
|
然后直接使用 comet 命令即可。
|
|
comet 会自动按照 open-design-build-verify-archive五阶段工作流执行。
这里以 kcctl 命令优化为例,展示具体流程:
首先会判断当前处于什么阶段,这里是新需求,所以是重头开始
|
|
然后自动进入第一阶段 Open,也就是 openspec-explore + openspec-propose 生成 Spec。
|
|
阶段完成后会运行阶段守卫(就是一个检测脚本),确保当前阶段真的完成了。当前 open 阶段则是判断 OpenSpec 的 Spec(proposal、specs、design、tasks) 是否正常生成了,如果检测发现没有则不会进入下一阶段。
通过脚本硬性检测,保证 AI 不会漏掉任何一个阶段。
检测通过后自动进入第二阶段,Superpowers brainstorming:
|
|
这个就是 Superpowers 头脑风暴的优点,可以确认很多细节上的需求,避免后续做出来不是自己想要的。
然后进入第三阶段:build,使用 Superpowers 的 subagent-driven-development 开始实现:
|
|
这一阶段耗时会比较长,AI 会根据前面的计划挨个实现,用上 Superpowers 之后,代码质量比直接 opsx:apply 会好不少。
Build 完成后会自动进入 Verify 阶段,对照 Spec 以及代码,确认所有功能都已经实现完成。
|
|
发现问题也会自动进行修复
最后就是归档了,Verify 之后自动流转到 Archive:
|
|
至此,整个流程就完成了。
OpenSpec + Superpowers 组合的核心思想是:用 OpenSpec 生成和管理 Spec,然后用 Superpowers 这个"资深工程师"来执行 Spec。
单独使用各有短板,二者结合刚好形成 “规格驱动开发(SDD) + 测试驱动开发(TDD)” 的完整工作流。

如果觉得手动切换两个工具比较繁琐,可以尝试 Comet 将整个流程自动化。
2026-05-27 04:00:00

前三篇我们完成了 DRA 的部署实战、核心概念拆解和工作流程分析。但一直使用的是 NVIDIA 官方的 DRA Driver,如果我们的硬件不是 GPU,或者只是想暴露自定义资源给 Pod 使用,该怎么办?
本文从零实现一个自定义 DRA Driver —— i-dra-driver,沿用之前 Device Plugin 文章 中的 “gopher” 资源隐喻,将节点上的文件作为设备暴露给 Pod。通过对比同一个功能在 DevicePlugin 和 DRA 两种框架下的实现差异,加深对 DRA 机制的理解。
完整源码: github.com/lixd/i-dra-driver
在开始编码之前,先通过一张表直观对比两种框架在实现同一个功能时的差异:
| 维度 | DevicePlugin | DRA |
|---|---|---|
| 设备发现 |
ListAndWatch gRPC 上报设备列表 |
创建 ResourceSlice API 对象 |
| 资源可见性 | Node capacity 字段(只有数量) |
ResourceSlice 对象(含完整属性) |
| 调度参与 | 调度器不参与,kubelet 本地分配 | 调度器参与,PreFilter/Filter/Reserve |
| 设备选择 | 只能按数量申请 | CEL 表达式精确匹配属性 |
| 设备注入 |
Allocate 返回 env/mounts |
CDI spec 定义注入规则 |
| 注册方式 | kubelet Registration gRPC | plugins_registry socket |
| 核心接口 |
ListAndWatch + Allocate
|
PrepareResourceClaims + UnprepareResourceClaims
|
| 辅助库 | 无(直接实现 gRPC) | k8s.io/dynamic-resource-allocation/kubeletplugin |
一句话总结:DevicePlugin 是 “kubelet 本地管理”,DRA 是 “API Server 全局调度 + CDI 标准注入”。

DevicePlugin 通过 ListAndWatch 流式上报设备,而 DRA Driver 需要将设备信息写入 ResourceSlice API 对象。
pkg/device/discovery.go:
|
|
与 DevicePlugin 的区别:
ListAndWatch 是一个长连接流,设备变化时主动推送Discover 是一个普通函数,调用后返回当前设备列表,变化通过定期 rescan 或后续更新 ResourceSlice 来体现pkg/device/resourceslice.go:
|
|
关键点:
Attributes 存放字符串/布尔/版本等设备属性,可用于 CEL 选择器筛选Capacity 存放数值型容量信息(如文件大小、GPU 显存)nvidia.com/gpu: "4")强大得多这是 DRA Driver 的核心:实现 kubeletplugin.DRAPlugin 接口的三个方法。
|
|
当 kubelet 需要为 Pod 准备设备时调用。我们需要:
status.allocation 中提取已分配的设备
|
|
与 DevicePlugin Allocate 的区别:
DevicePlugin Allocate
|
DRA PrepareResourceClaims
|
|
|---|---|---|
| 输入 | 容器维度(ContainerAllocateRequest) | ResourceClaim 维度(可能跨容器) |
| 设备确定 | kubelet 本地决定 | 调度器已分配,status.allocation 指定 |
| 注入方式 | 返回 env/mounts/devices | 返回 CDI device ID |
| 幂等性 | 非强制 | 必须幂等 |
Pod 删除时调用,清理 CDI spec:
|
|
注意:此方法只收到 NamespacedObject(UID + Name + Namespace),拿不到完整的 ResourceClaim 对象,因为 claim 可能已被删除。必须幂等。
辅助库的后台错误回调:
|
|
可恢复错误(如 ResourceSlice 更新暂时失败)只记录日志,致命错误则退出进程。
CDI (Container Device Interface) 是 DRA 的设备注入机制。Driver 创建 CDI spec 文件,容器运行时读取后按 spec 将设备注入容器。
对于我们的 “gopher” 设备,CDI spec 主要负责注入环境变量。文件挂载通过 Pod 的 hostPath volume 实现(因为普通文件的 bind mount 通过 CDI 注入时存在兼容性问题,hostPath volume 是更可靠的方式)。
pkg/cdi/handler.go:
|
|
与 DevicePlugin Allocate 的注入方式对比:
| DevicePlugin | DRA + CDI | |
|---|---|---|
| 环境变量 |
Allocate 返回 Envs map
|
CDI spec 的 ContainerEdits.Env
|
| 文件挂载 |
Allocate 返回 Mounts
|
CDI spec 的 ContainerEdits.Mounts 或 Pod hostPath volume |
| 设备节点 |
Allocate 返回 Devices
|
CDI spec 的 ContainerEdits.DeviceNodes
|
| 标准 | Kubernetes 私有 | OCI 标准规范 |
CDI 是开放容器标准,不绑定 Kubernetes,容器运行时原生支持。对于 GPU 等真实硬件设备,CDI 的 DeviceNodes 和 Mounts 是首选注入方式;对于文件类的简单设备,环境变量 + hostPath volume 的组合更实用。
cmd/main.go:
|
|
与 DevicePlugin 启动流程对比:
| 步骤 | DevicePlugin | DRA Driver |
|---|---|---|
| 启动 gRPC Server | 手动创建 grpc.Server + 监听 unix socket |
kubeletplugin.Start 自动处理 |
| 注册到 Kubelet | 手动连接 KubeletSocket 发送 RegisterRequest
|
kubeletplugin.Start 自动处理 |
| 发布设备信息 |
ListAndWatch 流式推送 |
helper.PublishResources() 创建 ResourceSlice |
| 监听 Kubelet 重启 | 手动 fsnotify 监听 socket |
辅助库自动处理 |
DRA 的 kubeletplugin.Start 一行代码就解决了 DevicePlugin 需要几十行代码处理的注册和生命周期管理。
以下在 Kubernetes v1.35.0 + containerd v1.7.29 的单节点集群上实际部署验证。
|
|
在节点上创建测试用的 gopher 设备文件:
|
|
|
|
验证 Driver Pod 已启动:
|
|
查看 Driver 日志,确认设备发现和 ResourceSlice 发布成功:
|
|
|
|
查看详情,确认设备属性和容量信息:
|
|
可以看到,与 DevicePlugin 只能在 Node capacity 上显示 nvidia.com/gpu: "4" 不同,DRA 的 ResourceSlice 记录了每个设备的完整属性(type=gopher)和容量(size=20)。
|
|
查看 DeviceClass 详情,确认 CEL 选择器:
|
|
CEL 表达式定义了"什么样的设备属于这个 Class":driver 是 gopher.example.com 且 type 是 gopher。用户申请资源时只需指定 deviceClassName: gopher.example.com,不需要关心具体的设备名。
|
|
验证 Pod 状态:
|
|
查看 ResourceClaim 分配结果:
|
|
状态为 allocated,reserved,说明调度器已成功分配设备并预留。查看分配详情:
|
|
查看 Pod 日志,确认设备已成功注入:
|
|
环境变量 GOPHER=gopher-a 通过 CDI spec 注入,文件内容 hello from gopher-a 通过 hostPath volume 读取。整个 DRA 流程从设备注册到 Pod 使用,全链路打通!
进一步在 Pod 内部验证:
|
|
从 Driver 启动到 Pod 使用设备,完整数据流如下:
|
|
对比 DevicePlugin 的流程:
|
|
核心区别:DRA 的调度器在调度阶段就选好了具体设备并绑定节点,不会出现 DevicePlugin 中"调度到节点后才发现资源不匹配"的问题。
本文通过实现 i-dra-driver,从代码层面对比了 DevicePlugin 和 DRA 的核心差异:
ListAndWatch gRPC 流 → ResourceSlice API 对象kubeletplugin 辅助库一行搞定DRA 的设计理念是让设备信息对调度器可见、让设备注入遵循开放标准、让驱动开发者专注于业务逻辑。虽然目前只在 Kubernetes 1.34 GA,但作为 DevicePlugin 的继任者,DRA 是 Kubernetes 资源管理的未来方向。
系列回顾: