1. 从“ax”这个标题说起:一个被低估的运行时编排切口
“ax”这个标题乍看像某个命令行工具的缩写,或者某个内部项目的代号。但把热搜词摊开来看——agentic、orchestration、runtime、Kubernetes——这几个词凑在一起,指向的其实是一个非常具体的工程命题:在 Kubernetes 之上,为 agentic 工作负载构建一套可编排的运行时层。这不是又一个 CRD 封装,也不是简单的 Operator 套壳,而是要解决“智能体任务如何在集群里被调度、被隔离、被观测、被回收”这一整条链路的问题。
我最早接触这类需求,是在一个内部平台项目里。当时团队想把几个 LLM 驱动的自动化任务跑在现有的 K8s 集群上,最初的想法很朴素:写个 Deployment,把 agent 的镜像塞进去,配个 Service 就完事。结果上线第三天就出问题了——任务之间互相抢 GPU 显存,一个长尾任务把节点拖垮,日志散落在各个 Pod 里根本串不起来,更别提任务重试和状态恢复了。那次之后我才意识到,agentic 工作负载和传统无状态服务在运行时特征上完全是两码事。
所以这篇博文,我想围绕“ax”这个切口,把 agentic orchestration runtime 在 Kubernetes 上的落地思路完整拆一遍。适合谁看?如果你正在做 AI 平台、任务编排、或者想把 agent 类任务跑进 K8s,但被调度、隔离、观测这些问题卡住,那这篇内容应该能帮你少走一些弯路。我会从整体设计讲到具体实现,包括参数选择、踩坑记录和排查技巧,尽量做到可以直接抄作业。
2. 整体设计思路:为什么不能直接用原生 K8s 跑 agent
2.1 agentic 负载的三个特殊之处
传统微服务是无状态的、短请求、可水平扩展。agentic 负载不一样,它有三个很明显的特征:
- 长时运行且状态敏感:一个 agent 任务可能跑几分钟到几十分钟,中间要维护对话历史、工具调用结果、中间推理状态。Pod 一重启,这些状态就没了。
- 资源需求动态且异构:有的步骤吃 CPU,有的步骤要 GPU,有的步骤只是等外部 API 返回。资源画像在任务生命周期内是变化的。
- 任务之间有依赖和编排关系:一个 agent 可能派生多个子任务,子任务之间还有先后顺序和数据传递。这不是简单的 Pod 副本数能表达的。
我试过直接用 Job + CronJob 来跑,结果是 Job 的完成语义和 agent 的“可能还要继续”语义对不上。Job 认为容器退出就是完成,但 agent 可能只是阶段性结束,后面还要被唤醒。这个矛盾在早期版本里让我踩了不少坑。
2.2 为什么选择在 K8s 之上做 runtime 层
有人会问,既然原生 K8s 不顺手,为什么不直接自己写个调度器?我的判断是:K8s 在节点管理、网络、存储、RBAC 这些基础设施层面已经足够成熟,重新造轮子的成本远高于在它之上做一层 runtime 抽象。而且团队已有的运维体系、监控告警、日志采集都是围绕 K8s 建的,脱离它反而增加维护负担。
所以“ax”这个项目的核心思路是:把 agentic 任务抽象成一种新的运行时对象,由一层 runtime controller 负责生命周期管理,底层仍然复用 K8s 的调度和隔离能力。这样既拿到了 K8s 的稳定性,又补上了 agent 场景需要的编排语义。
2.3 分层架构的取舍
整体分成三层:
| 层级 | 职责 | 关键技术点 |
|---|---|---|
| 编排层 | 任务图解析、依赖调度、状态机管理 | 自定义 CRD + controller |
| 运行时层 | 容器生命周期、资源配额、状态持久化 | Pod + PVC + sidecar |
| 基础设施层 | 节点调度、网络、存储、隔离 | 原生 K8s |
这个分层的好处是每一层可以独立演进。编排层改调度策略不影响运行时层,运行时层换容器运行时也不影响编排逻辑。代价是层间接口要设计得足够清晰,否则调试时会很痛苦——我后面会讲怎么用 trace ID 把三层串起来。
3. 核心细节解析:runtime 层到底要解决什么
3.1 任务状态机的设计
agent 任务不能只有“运行中/完成/失败”三个状态。实际需要的是这样一组状态:
Pending:已创建,等待调度Provisioning:正在拉镜像、挂载存储Running:主容器在执行Waiting:等待外部事件或子任务Checkpointing:正在保存状态Succeeded/Failed/Cancelled:终态
这里最关键的是Waiting和Checkpointing。Waiting让 Pod 可以缩容到零但任务不结束,Checkpointing保证状态能落盘。我最初没设计Waiting,结果 agent 等外部 API 的时候 Pod 一直占着资源,集群利用率很低。
状态机的实现建议用有限状态机 + 事件驱动,而不是在 controller 里写一堆 if-else。我用的是一个轻量的状态机库,把每个状态的进入/退出动作注册成回调,controller 只负责投递事件。这样后面加状态不用改核心逻辑。
3.2 状态持久化的三种方案对比
agent 状态怎么存,是个绕不开的问题。我实际试过三种:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| EmptyDir + 定期快照 | 实现简单,读写快 | Pod 漂移后恢复慢 | 短任务、可重算 |
| PVC 挂载 | 状态持久,Pod 重建可恢复 | 存储成本高,跨节点挂载有延迟 | 长任务、状态重要 |
| 外部状态存储(如 Redis/对象存储) | 与 Pod 解耦,恢复快 | 需要额外组件,网络依赖 | 高可用要求 |
我的选择是PVC 为主 + 关键检查点写对象存储。PVC 保证 Pod 重建时本地状态还在,对象存储保证节点级故障时还能恢复。检查点频率不要太高,我一般设成每 30 秒或每完成一个子任务写一次,太频繁会拖慢主流程。
3.3 资源隔离与配额控制
agent 任务最容易出的问题就是资源互相干扰。我的做法是:
- 每个任务一个 ResourceQuota:限制 CPU、内存、GPU 上限,防止单个任务吃满节点。
- GPU 用 device plugin + 显式申请:不要用共享模式,agent 任务对显存很敏感。
- 网络用 NetworkPolicy 隔离:默认拒绝所有出站,按需放行。
这里有个细节:K8s 的 ResourceQuota 是按 namespace 算的,如果每个任务一个 namespace,namespace 数量会爆炸。我的折中是按团队或按任务类型分 namespace,任务级配额用 LimitRange + 自定义 admission webhook 控制。webhook 里校验任务请求的资源是否超过该类型的上限,超了直接拒绝创建。
4. 实操过程:从零搭一个最小可用的 ax runtime
4.1 环境准备与前置检查
假设你已有一个 K8s 集群,版本 1.26 以上。先确认几个前置条件:
# 检查节点资源和 GPU 插件 kubectl get nodes -o wide kubectl get pods -n kube-system | grep -E "device-plugin|nvidia" # 检查存储类 kubectl get storageclass # 检查 admission webhook 是否可用 kubectl get validatingwebhookconfigurations如果 GPU 插件没装,先装对应的 device plugin。存储类至少要有一个支持 ReadWriteOnce 的,PVC 才能正常挂载。
4.2 定义 CRD:AgentTask
核心 CRD 我命名为AgentTask,关键字段如下:
apiVersion: ax.io/v1alpha1 kind: AgentTask metadata: name: demo-task spec: image: agent-runtime:latest command: ["python", "-m", "agent.main"] resources: cpu: "2" memory: "4Gi" gpu: "1" checkpoint: intervalSeconds: 30 storageClass: fast-ssd dependencies: - taskRef: upstream-task timeoutSeconds: 3600dependencies字段是编排层用的,controller 会等上游任务成功后才创建这个任务的 Pod。checkpoint控制检查点行为。timeoutSeconds是硬超时,防止任务卡死。
4.3 controller 的核心 reconcile 逻辑
controller 的 reconcile 循环大致是这样:
- 获取 AgentTask 对象,如果不存在就返回。
- 检查是否有未满足的依赖,有就更新状态为
Pending并重新入队。 - 检查是否已有对应的 Pod,没有就创建。
- 根据 Pod 状态更新 AgentTask 状态。
- 如果任务进入终态,清理关联资源(PVC 可选保留)。
关键点是幂等。reconcile 可能被多次触发,创建 Pod 前一定要先查是否已存在。我用的是ownerReference+ label selector 来关联,避免重复创建。
4.4 状态检查点的实现
检查点逻辑放在 agent 容器内部,通过一个 sidecar 暴露的 HTTP 接口触发:
import requests import pickle def save_checkpoint(state, path="/checkpoint/state.pkl"): with open(path, "wb") as f: pickle.dump(state, f) # 通知 sidecar 上传到对象存储 requests.post("http://localhost:8080/checkpoint", json={"path": path})sidecar 收到请求后把文件同步到对象存储,并更新 AgentTask 的 annotation 记录最新检查点位置。Pod 重建时,init container 先从对象存储拉最新检查点,再启动主容器。
4.5 观测与日志串联
三层架构最大的调试痛点是日志散落。我的做法是:
- 统一 trace ID:在 AgentTask 创建时生成一个 UUID,注入到 Pod 的 annotation 和容器环境变量。
- 日志采集按 trace ID 聚合:用 Fluent Bit 采集时带上 trace ID 标签,查询时按标签过滤。
- 关键事件写 Event:controller 在每个状态转换时写 K8s Event,方便
kubectl describe查看。
这样出问题时,先kubectl describe agenttask看事件,再用 trace ID 查日志,基本能定位到具体环节。
5. 常见问题与排查技巧实录
5.1 任务卡在 Pending 不动
最常见的原因是依赖没满足或资源不足。排查顺序:
kubectl describe agenttask <name>看 Events 里有没有FailedScheduling。- 检查依赖任务状态,
kubectl get agenttask看上游是否成功。 - 检查 ResourceQuota,
kubectl describe quota -n <ns>。
我遇到过一次是 webhook 把请求拒了但没写清楚原因,后来在 webhook 里加了详细错误信息才定位到。
5.2 Pod 反复重启
agent 任务重启通常是因为 OOM 或检查点恢复失败。先看kubectl logs --previous拿上次崩溃日志。如果是 OOM,调大 memory limit 或优化 agent 的内存使用。如果是恢复失败,检查对象存储里的检查点文件是否完整,init container 的拉取逻辑是否有超时。
5.3 GPU 申请了但用不上
这个坑我踩过。原因是节点上的 device plugin 没正确上报,或者 Pod 的 resource limit 写成了nvidia.com/gpu: 1但节点标签不匹配。排查:
kubectl describe node <node> | grep -A5 "Allocatable" kubectl get pod <pod> -o jsonpath='{.spec.containers[*].resources}'确认节点有 GPU 资源且 Pod 正确申请。
5.4 检查点写入慢导致任务超时
检查点写对象存储如果网络不好会很慢。我的优化是:异步写 + 本地先落盘。主流程只写本地 PVC,sidecar 异步上传。如果上传失败,下次检查点会覆盖,不影响主流程。另外检查点文件不要太大,只存必要状态,别把整个内存 dump 进去。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| Pending 不动 | 依赖未满足/资源不足 | kubectl describe agenttask |
| Pod 反复重启 | OOM/检查点恢复失败 | kubectl logs --previous |
| GPU 不可用 | device plugin 异常 | kubectl describe node |
| 检查点超时 | 对象存储慢 | 查 sidecar 日志 |
| 日志串不起来 | trace ID 未注入 | 检查 Pod annotation |
6. 一些实操心得与后续扩展方向
跑了一段时间后,我最大的体会是:agentic runtime 的难点不在调度,而在状态管理和可观测性。调度有 K8s 兜底,但状态怎么存、怎么恢复、怎么让运维看得懂,这些才是真正花时间的地方。我建议早期就把 trace ID 和检查点机制做进去,后面加功能会轻松很多。
另外,如果你的任务图比较复杂,可以考虑引入 DAG 编排引擎,但不要一上来就上重型框架。我见过团队为了跑几个 agent 任务引入了一整套工作流引擎,结果运维成本比业务本身还高。先从 CRD + controller 做起,够用再扩。
后续可以扩展的方向:任务优先级和抢占、跨集群调度、agent 之间的消息传递。这些我还在摸索,等有成熟经验再单独写一篇。