Kubernetes 上 agentic 工作负载的运行时调度层设计与实践
1. 从“ax”这个标题说起一个被低估的运行时调度命题第一次看到“ax”这个标题很多人会以为是某个命令行工具的缩写或者某个内部代号。但把热搜词摊开来看——ax、agentic、orchestration、runtime、Kubernetes——这几个词凑在一起指向的其实是一个非常具体的技术命题在 Kubernetes 之上如何为 agentic 工作负载做一套轻量、可编排、可观测的运行时调度层。我接触这类需求是从一个很朴素的场景开始的团队里跑了一批自动化任务早期用 CronJob 加脚本就能糊弄过去后来任务之间开始有依赖、有状态、有重试、有并发控制再后来这些任务本身变成了“会自己决策下一步做什么”的 agent问题就彻底变了。传统 Job 模型假设的是“一次提交、一次执行、一次结束”而 agentic 负载是“持续对话、动态派生、随时中断恢复”。这两者的调度语义根本不在一个层面上。“ax”在这个语境里我更愿意把它理解成一个面向 agentic 场景的调度抽象层——它不替代 Kubernetes而是站在 Kubernetes 的调度能力之上补上 agent 特有的那部分任务图的动态展开、运行时上下文的传递、跨 Pod 的状态协调、以及失败后的语义化重试。热搜里同时出现了 Karmada 毕业、华为云 agentic cloud 这类词说明这个方向不是小众玩具而是正在被社区和大厂同时推进的基础设施议题。这篇文章适合三类人看一是已经在 Kubernetes 上跑批处理任务、开始觉得 Job/CronJob 不够用的工程师二是正在设计 agent 编排系统、纠结调度层该放多厚的架构师三是想搞清楚 agentic runtime 到底和普通容器运行时差在哪里的技术负责人。我会尽量把“为什么这么设计”讲透而不是只丢一堆 YAML。2. 为什么 agentic 负载不能直接套用 Kubernetes 原生调度2.1 原生 Job 模型的三个硬伤Kubernetes 的 Job 和 CronJob 是为“确定性批处理”设计的。你提交一个 Job它创建 PodPod 跑完退出Job 标记完成。这套模型在 CI、数据清洗、定时报表场景里非常稳。但 agentic 负载有三个特征直接撞在它的短板上。第一个硬伤是执行时长不可预测。一个 agent 可能三步就收敛也可能因为工具调用失败反复重试几十轮。Job 的activeDeadlineSeconds是个静态值设短了误杀设长了僵尸 Pod 占着资源。我见过最离谱的一次一个 agent 因为外部 API 限流进入退避循环硬生生跑了六个小时把节点上的内存吃满最后是 OOM 才结束的。第二个硬伤是任务图是运行时才确定的。传统 DAG 调度器比如 Airflow要求你提前把依赖关系写死。但 agent 的下一步动作往往取决于上一步的返回结果——它可能调用搜索、可能调用代码执行、可能直接给答案。你没法在提交时就知道要起几个 Pod、它们之间什么依赖。第三个硬伤是状态需要跨步骤存活。Job 的 Pod 是无状态的跑完即焚。但 agent 的对话历史、中间产物、工具调用凭证都需要在多个执行单元之间传递。用 emptyDir 传Pod 一换就没了。用 PVC粒度太粗而且并发写会打架。2.2 “ax”要解决的核心矛盾所以“ax”这类调度层的核心矛盾就清楚了Kubernetes 擅长管“资源”但不擅长管“意图”。它知道怎么把一个 Pod 放到合适的节点上但它不知道这个 Pod 代表的是一段会自我演化的推理过程。我的理解是ax 的设计目标应该是在两者之间架一层“意图翻译”上层接收 agent 的语义化描述要做什么、能用哪些工具、失败怎么退下层翻译成 Kubernetes 能理解的资源原语Pod、Service、ConfigMap、自定义 CRD中间再维护一份运行时状态负责把动态派生的子任务接回主流程。这层抽象的价值在于它让 agent 的开发者不用关心 Pod 怎么调度、节点怎么选、网络怎么通只需要描述“我要完成什么”。而平台侧则获得了统一的观测点、配额控制点和故障恢复点。说白了就是把 agent 的“乱”收敛到一个可控的边界里。2.3 和 Karmada、多集群调度的关系热搜里 Karmada 毕业是个重要信号。Karmada 解决的是多集群分发和调度而 agentic cloud 的愿景里agent 很可能不是跑在单个集群里的——它可能需要在边缘节点做轻量推理在中心集群做重计算在另一个集群访问特定数据源。ax 如果只做单集群内的编排格局就小了。合理的演进路径是ax 负责 agent 语义层的编排Karmada 负责把展开后的工作负载分发到合适的集群。两者是互补的不是竞争的。我在设计时会把“集群选择”作为一个可插拔的策略点而不是硬编码在调度逻辑里这样后面接多集群才不会推倒重来。3. 核心架构拆解ax 调度层应该长什么样3.1 三层结构意图层、编排层、执行层我倾向于把 ax 拆成三层每层职责单一边界清晰。意图层是面向开发者的 API。你用声明式的方式描述一个 agent 任务它的目标、可用的工具集、最大步数、超时策略、失败重试语义。这一层不涉及任何 Kubernetes 概念纯粹是业务语义。好处是开发者心智负担低而且这层描述可以被不同后端实现复用。编排层是 ax 的大脑。它把意图层的描述翻译成一张动态任务图维护每个节点的状态pending、running、succeeded、failed、retrying决定下一步该起哪个执行单元。这一层需要处理并发控制、依赖解析、状态持久化。我实测下来编排层的状态存储用 etcd 或者带事务的关系库都行关键是写入要幂等因为调度器重启后需要能恢复现场。执行层就是真正跑 agent 逻辑的地方通常是一个 Pod 里跑一个 runtime 进程。这个 runtime 负责和编排层通信拉取任务、上报状态、请求派生新任务同时管理本地的工具调用。执行层要尽量无状态所有需要跨步骤存活的东西都交给编排层或外部存储。3.2 为什么用 CRD 而不是自己造一套 API有人会问为什么不干脆自己写个调度服务用 gRPC 暴露接口非要套 Kubernetes 的 CRD我的经验是CRD 的最大价值是复用 Kubernetes 的控制器模式和生态。你定义AgentTask、AgentStep这样的 CRD就能直接用 client-go 的 informer 机制做事件驱动用 RBAC 做权限控制用 kubectl 做调试用现有的监控体系做观测。自己造一套 API这些都得重写而且和现有运维体系割裂。当然 CRD 也有代价它的 schema 校验比较死动态字段处理起来别扭。我的做法是把真正动态的部分比如工具调用的参数塞进一个rawExtension字段schema 只约束稳定的骨架。这样既保留了类型安全又不失灵活性。3.3 运行时上下文怎么传递这是最容易踩坑的地方。agent 的上下文包括对话历史、中间文件、工具凭证体积可能很大而且需要频繁读写。我的方案是分层存储小体积的元数据比如当前步数、状态标记直接放在 CRD 的 status 里读写走 API Server中等体积的对话历史放在一个专门的 Context Store可以是 Redis 或者带 TTL 的数据库大体积的中间产物比如生成的代码、下载的文件放对象存储CRD 里只存引用。注意不要把大对象塞进 CRD 的 annotation 或 statusetcd 有单对象大小限制默认 1.5MB超了会直接写入失败而且会拖慢整个 API Server。我踩过一次坑把一段 base64 编码的图片塞进 status结果 etcd 报警整个集群的写入延迟飙升。后来改成对象存储加引用问题消失。4. 实操从零搭一个最小可用的 ax 调度原型4.1 环境准备与依赖清单要复现这套东西你需要一个能跑的 Kubernetes 集群。单节点用 kind 或 minikube 就够版本建议 1.28 以上因为要用到一些较新的调度特性。本地开发机装好 kubectl、helm、docker。核心依赖我列一下组件用途选型建议状态存储存编排层任务图etcd 或 PostgreSQL消息队列执行层与编排层通信NATS 或 Redis Stream对象存储存大体积中间产物MinIO本地或云对象存储观测指标与日志Prometheus Loki选 NATS 是因为它轻量、支持请求-响应模式很适合执行层主动拉任务的场景。如果你团队已经有 Kafka用 Kafka 也行但运维成本高一些。4.2 定义 AgentTask CRD先写 CRD 的 schema。我把它精简到最核心的字段apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: agenttasks.ax.example.com spec: group: ax.example.com versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: goal: type: string tools: type: array items: type: string maxSteps: type: integer default: 20 timeoutSeconds: type: integer default: 600 retryPolicy: type: object properties: maxRetries: type: integer default: 3 backoffSeconds: type: integer default: 5 status: type: object properties: phase: type: string currentStep: type: integer contextRef: type: string scope: Namespaced names: plural: agenttasks singular: agenttask kind: AgentTask这里contextRef指向外部 Context Store 的键而不是把上下文塞进 status。maxSteps和timeoutSeconds是双保险防止 agent 无限循环。4.3 编排控制器的核心循环控制器用 client-go 写核心是一个 reconcile 循环。逻辑大概是拿到 AgentTask 对象看它的 phase如果是 Pending 就初始化上下文并置为 Running如果是 Running 就检查当前步是否完成完成则推进下一步或判定收敛如果超时或超过 maxSteps 就置为 Failed。关键点是幂等。reconcile 可能被重复触发所以每一步的状态变更都要先检查当前状态再决定动作。我习惯在 status 里加一个observedGeneration只有 spec 的 generation 变了才重新规划避免无谓的重算。func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var task axv1.AgentTask if err : r.Get(ctx, req.NamespacedName, task); err ! nil { return ctrl.Result{}, client.IgnoreNotFound(err) } switch task.Status.Phase { case , Pending: return r.initialize(ctx, task) case Running: return r.advance(ctx, task) case Failed, Succeeded: return ctrl.Result{}, nil } return ctrl.Result{}, nil }advance里要做的事从消息队列拉取执行层上报的结果更新上下文判断是否满足终止条件不满足就派生下一个执行单元。4.4 执行层 runtime 的最小实现执行层是一个跑在 Pod 里的进程启动后先向编排层注册然后循环拉任务。每个任务包含当前上下文引用、可用工具列表、本步的目标。runtime 的职责是调用具体的 agent 逻辑可以是 LLM 调用、可以是规则引擎、可以是任意函数拿到结果后上报。这里有个设计选择runtime 要不要内置 LLM 客户端我的建议是不要把它做成插件。因为模型迭代太快硬编码在 runtime 里会导致每次换模型都要重新构建镜像。用 sidecar 或者独立的工具服务更灵活。def run_step(task): context load_context(task.context_ref) tools load_tools(task.tools) result agent_loop(context, tools, task.goal) save_context(task.context_ref, result.context) report(task.id, result.status, result.next_actions)agent_loop就是具体的推理逻辑这部分因场景而异ax 不关心它怎么实现只关心它的输入输出契约。4.5 参数计算超时和重试怎么定超时和重试不是拍脑袋定的。我的经验公式是单步超时 工具调用 P99 延迟 × 2 推理时间预算。比如你的工具调用 P99 是 3 秒推理预算 10 秒那单步超时设 16 秒比较合理。整体超时 单步超时 × 预期步数 × 1.5留缓冲。重试策略要看失败类型。工具调用超时这种瞬时故障指数退避重试 3 次足够如果是参数错误这种确定性失败重试多少次都没用应该直接失败并上报。我在编排层加了一个failureClassifier根据错误码决定是重试还是终止这个分类器可以配置不同工具可以有不同的策略。5. 常见问题与排查技巧实录5.1 执行层 Pod 起不来或反复重启这是最高频的问题。排查顺序我总结成一张表现象可能原因排查命令Pod Pending资源不足或调度约束kubectl describe pod看 EventsCrashLoopBackOffruntime 启动报错kubectl logs --previous一直 ContainerCreating镜像拉取慢或挂载失败看 Events 里的 FailedMountOOMKilled内存 limit 太小kubectl top pod对比 limit我遇到过一次很隐蔽的runtime 启动时要连消息队列但网络策略没放行导致连接超时后进程退出Kubernetes 判定失败重启形成循环。后来在启动脚本里加了带退避的重连逻辑并且把“连不上”和“连上后出错”区分开前者不触发 Pod 重启。提示给 runtime 加一个 readiness probe只有真正能拉取任务了才标记 Ready。否则编排层可能把任务派给一个还没准备好的 Pod白白浪费一次重试。5.2 任务卡在 Running 不动这种通常是编排层和执行层之间的状态不同步。可能的原因执行层上报了结果但编排层没收到消息丢失或者编排层更新了状态但执行层没感知缓存过期。我的排查套路是先看编排层的日志确认最后一次状态变更是什么时候再看执行层的日志确认它是否上报了最后看消息队列的堆积情况。如果消息队列有堆积说明消费端有问题如果没堆积但状态没更新说明是编排层的处理逻辑有 bug。根治办法是加一个心跳机制执行层每隔 N 秒上报一次心跳编排层如果超过 3N 秒没收到心跳就把该执行单元标记为失联并触发重试。N 设 10 秒比较合适太短会增加消息量太长故障发现慢。5.3 上下文丢失或串号这是最危险的问题因为会导致 agent 基于错误的历史做决策。根因通常是 contextRef 的生成或传递有 bug。我的做法是给每个 AgentTask 生成一个全局唯一的 taskIDcontextRef 用taskID:stepNumber的格式这样天然隔离。执行层拉任务时必须带上 taskID编排层校验不匹配就拒绝。另外上下文写入用 CAScompare-and-swap语义防止两个执行单元同时写同一个 key 导致覆盖。5.4 资源配额被单个 agent 吃光agent 的并发度如果不控制很容易把集群资源打满。我在编排层加了并发闸门每个 AgentTask 有一个maxConcurrency字段控制它同时能起多少个执行单元命名空间级别再用 ResourceQuota 兜底。还有一个技巧是给 agentic 负载单独打一个 taint用专门的节点池跑避免影响在线业务。这个在共享集群里特别重要我见过 agent 把整个集群的 CPU 抢光导致线上服务抖动的案例。6. 观测与调优让 ax 跑得稳、看得清6.1 必须埋的几类指标没有观测的调度系统就是个黑盒。我至少会埋这几类指标任务级任务总数、成功数、失败数、平均步数、P99 耗时步骤级每步耗时分布、重试次数分布、失败原因分类资源级执行单元的数量、CPU/内存使用、队列深度编排层reconcile 耗时、reconcile 错误率、状态存储延迟这些指标用 Prometheus 采集配几个关键的告警规则任务失败率超过阈值、队列深度持续增长、reconcile 错误率飙升。告警要能定位到具体的 AgentTask否则排查起来很痛苦。6.2 调优的几个实战经验第一reconcile 要限流。如果 AgentTask 数量大每个都触发 reconcile 会把 API Server 打爆。用 workqueue 的 rate limiter并且对同一对象的多次变更做合并。第二状态存储要分片。如果所有任务的状态都写同一个 etcd热点会很严重。按 taskID 哈希分片或者干脆用外部数据库。第三执行单元的启动要预热。如果 runtime 启动慢比如要加载大模型可以维护一个 warm pool任务来了直接从池里取省去冷启动时间。这个在延迟敏感场景里效果很明显我实测能把首步延迟从十几秒降到两秒以内。6.3 和现有 CI/CD 体系的衔接ax 不是孤岛。它需要和现有的 CI/CD 打通agent 的镜像怎么构建、怎么发布、怎么回滚。我的做法是把 runtime 镜像纳入现有的镜像流水线用同样的 tag 规范AgentTask 的 CRD 用 GitOps 管理变更走 PR 流程。这样运维团队不用学新东西接受度高很多。7. 我对这套东西的一点个人判断折腾了这么久我最大的体会是agentic 调度的难点不在调度算法而在状态管理。Kubernetes 把无状态服务管得很好但 agent 天生是有状态的而且状态的生命周期和 Pod 的生命周期不一致。谁能把这层状态抽象做好谁就能真正把 agent 跑在生产环境里。另外别一上来就追求大而全。我见过太多团队想做一个通用 agent 平台结果半年过去连一个稳定跑通的场景都没有。正确的路径是先找一个具体的、有价值的场景比如自动化工单处理、代码审查辅助把它跑通、跑稳再抽象出通用的调度层。ax 这种抽象应该是从实践中长出来的而不是设计出来的。最后分享一个我一直在用的小技巧给每个 AgentTask 加一个debugMode开关打开后把每一步的完整上下文、工具调用参数、返回结果都落盘到对象存储。平时关着省成本出问题时打开复现排查效率能提升好几倍。这个开关救过我很多次尤其是在处理那些“偶发失败”的诡异问题时。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →