ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes 上构建 Agentic 工作负载的运行时调度层实践

Kubernetes 上构建 Agentic 工作负载的运行时调度层实践 1. 从“ax”这个标题说起一个被低估的运行时调度命题“ax”这个词单独拎出来信息量其实非常低。它可能是某个内部项目的代号也可能是某个开源组件的缩写甚至可能只是某个团队在排期表上随手写下的一个占位符。但把热搜词拼在一起看方向就清晰了ax、agentic、orchestration、runtime、Kubernetes。这五个词组合起来指向的是一个非常具体的工程命题——在 Kubernetes 之上构建一套面向 agentic 工作负载的运行时调度层。我之所以对这个命题感兴趣是因为过去一年多里我陆续接触过好几个团队在做类似的事情。大家的起点各不相同有的从传统的微服务调度出发发现现有的 Deployment、Service、HPA 这套抽象根本描述不了 agent 的行为有的从 RAG 流水线出发发现检索、推理、工具调用这三段式流程在 K8s 上跑起来处处别扭还有的干脆是从零开始想做一个专门跑 agent 的 runtime结果发现底层还是绕不开容器编排这一层。“ax”在这个语境下我倾向于把它理解为一个面向 agentic 负载的调度抽象层。它不一定是某个具体的开源项目更可能是一类方案的统称在 Kubernetes 的 Pod、Job、CronJob 之上再包一层专门为 agent 设计的调度语义。这层语义要解决的问题很具体——agent 不是无状态服务也不是一次性批处理任务它是有状态、长周期、可中断、可恢复、且经常需要多步工具调用的混合体。用现有的 K8s 原语去硬套就像用螺丝刀去拧螺母能拧但费劲。这篇文章我想聊的就是这件事如果你手上有一个 agentic 的工作负载想把它稳稳地跑在 Kubernetes 上调度层该怎么设计runtime 该怎么选哪些坑是几乎一定会踩的。适合已经有 K8s 基础、正在做 agent 相关工程的读者也适合刚接触这块、想先搞清楚全貌再动手的人。2. 为什么 agentic 负载需要一套独立的调度语义2.1 传统 K8s 调度模型和 agent 负载的根本错配Kubernetes 的调度模型是围绕“服务”和“任务”这两个原语建立的。Deployment 管的是长期运行的无状态服务副本数固定滚动更新健康检查靠 liveness/readiness probe。Job 管的是跑完就结束的批处理任务成功一次就退出。这套模型在微服务和数据处理场景下非常成熟但放到 agent 身上问题立刻暴露。一个典型的 agent 执行过程是这样的接收一个用户请求规划步骤调用若干工具中间可能需要等待外部 API 返回可能因为上下文太长需要压缩重试可能在第三步发现前两步的结果不对需要回退最后汇总输出。这个过程有几个特征执行时间不确定可能几秒也可能几十分钟、状态需要跨步骤保持上下文、中间结果、工具调用记录、可能被中断和恢复用户取消、超时、节点故障、资源需求动态变化推理时吃 GPU等待时几乎不占资源。用 Deployment 跑副本数没法动态反映“当前有多少个活跃 agent 会话”用 Job 跑一个会话拆成多个 Job 之后状态传递就成了大问题用 CronJob 更不对agent 不是定时触发的。这就是错配的根源K8s 的原语假设负载是同质的、可预测的而 agent 负载是异质的、突发的、有状态的。2.2 “ax”层要解决的核心问题调度粒度与生命周期管理我理解的“ax”层核心要解决两个问题。第一是调度粒度。传统 K8s 调度的最小单位是 Pod一个 Pod 里可以跑多个容器但 Pod 本身是一个原子调度单元。agent 的场景下一个会话可能对应多个执行阶段每个阶段需要的镜像、资源、网络策略都不一样。如果每个阶段都起一个 PodPod 之间的状态传递和生命周期协调就变成了负担如果全塞进一个 Pod资源隔离和弹性又做不好。第二是生命周期管理。agent 的生命周期不是“启动-运行-停止”这么简单它有“创建-规划-执行-等待-恢复-完成-清理”多个状态而且状态之间可能循环。K8s 的 Pod 生命周期只有 Pending、Running、Succeeded、Failed 这几个粗粒度状态没法表达 agent 的中间态。所以“ax”层需要在 K8s 之上定义一套更细的状态机并且把这套状态机和 K8s 的控制器模式对接起来。我见过的一种做法是引入AgentSession 自定义资源CRD用 Operator 来管理。AgentSession 里描述会话的输入、工具集、资源配额、超时策略Operator 负责把它翻译成底层的 Pod、Service、ConfigMap并持续 reconcile 状态。这样做的好处是调度语义清晰坏处是 Operator 本身的复杂度不低而且 CRD 的设计一旦定下来后续改起来很痛苦。2.3 和现有方案对比为什么不用 Knative、Karmada 或普通 HPA有人会问Knative 不是能做 scale-to-zero 吗Karmada 不是能做多集群调度吗HPA 不是能根据指标扩缩容吗这些都没错但它们解决的不是同一个问题。Knative 的 scale-to-zero 针对的是无状态请求驱动的服务请求来了起 Pod请求走了缩到零。agent 会话是有状态的缩到零意味着状态丢失除非你把状态外置到 Redis 或数据库但那样又引入了新的延迟和一致性开销。Karmada 解决的是多集群分发和故障转移它假设工作负载本身是可迁移的而 agent 会话迁移涉及上下文序列化和工具连接重建不是简单换个节点就行。HPA 基于 CPU、内存或自定义指标扩缩容但 agent 的瓶颈往往不在 CPU而在外部 API 的速率限制或 GPU 显存这些指标 HPA 原生支持得并不好。所以“ax”层的价值不在于替代这些方案而在于在它们之上补一层 agent 专属的调度逻辑。你可以继续用 Karmada 做多集群底座用 HPA 做粗粒度扩缩但 agent 会话的创建、路由、状态保持、超时回收需要“ax”这层来管。3. Runtime 选型从容器运行时到 agent 运行时3.1 容器运行时层containerd、CRI-O 与 WebView2 的意外关联热搜词里出现了[error cri]: container runtime is not running和webview2 runtime这两个看似不相关其实反映了 runtime 这个词在不同语境下的歧义。在 K8s 语境下runtime 通常指容器运行时比如 containerd、CRI-O。container runtime is not running这个报错我遇到过好几次最常见的原因是 kubelet 和容器运行时的 socket 路径不匹配或者 containerd 服务挂了。排查步骤很固定先systemctl status containerd看服务状态再crictl info看 CRI 接口是否通然后检查/var/run/containerd/containerd.sock是否存在且权限正确。如果是 kubeadm 部署的集群还要确认/var/lib/kubelet/kubeadm-flags.env里的--container-runtime-endpoint指向正确。这个坑我踩过两次一次是 containerd 升级后 socket 路径变了一次是 SELinux 策略挡住了 kubelet 访问 socket。至于 WebView2 runtime那是 Windows 桌面应用的东西和 K8s 没关系。热搜词里把它和 agentic 放一起大概率是有人在 Windows 上跑某个 agent 工具时遇到了依赖缺失。could not find the webview2 runtime这个报错解决办法就是装 Microsoft Edge WebView2 Runtime或者用winget install Microsoft.EdgeWebView2Runtime一条命令搞定。这类问题在跨平台 agent 工具里很常见因为很多工具用 Electron 或 Tauri 做界面底层依赖 WebView2。3.2 Agent 运行时层为什么需要独立的 runtime 抽象容器运行时管的是“怎么跑一个容器”agent 运行时管的是“怎么跑一个 agent 会话”。这两者不在一个层次上。agent 运行时需要处理的事情包括上下文窗口管理、工具调用的序列化和反序列化、多步推理的状态机、失败重试和回退策略、以及和外部模型服务的连接池管理。我试过直接用 Python 脚本 K8s Job 来跑 agent结果是每个会话都要重新加载模型连接、重新初始化工具集启动开销巨大。后来改成常驻进程 会话队列启动开销没了但进程崩溃会丢所有会话状态。再后来引入 Redis 做状态外置又遇到了序列化开销和一致性问题。绕了一圈才明白agent 运行时需要的是一个有状态、可恢复、支持会话隔离的执行环境这个环境用现成的容器运行时搭不出来得自己设计。目前我比较看好的做法是用 containerd 做底层容器管理在上面跑一个常驻的 agent runtime 进程每个会话对应 runtime 里的一个轻量级执行单元可以是协程、可以是子进程、也可以是 WebAssembly 实例。runtime 负责会话的创建、调度、状态持久化和回收K8s 只负责 runtime 进程本身的存活和资源配额。这样分层之后K8s 的调度粒度是 runtime 进程agent 的调度粒度是会话两者解耦。3.3 选型对照表不同规模团队的 runtime 方案团队规模会话量级推荐 runtime 方案理由1-3 人每天几十到几百单进程 协程池状态存本地 SQLite部署简单调试方便不需要 K8s3-10 人每天几百到几千常驻进程 Redis 状态外置K8s Deployment 跑有状态但可恢复扩缩容靠副本数10 人以上每天几千到几万自研 agent runtime K8s Operator CRD调度语义清晰可精细控制资源多集群场景跨地域上述方案 Karmada 做集群分发故障转移和就近接入这个表不是绝对的但大致反映了复杂度递进。我见过不少团队一上来就上 Operator CRD结果发现会话量根本没到那个级别维护成本反而拖慢了迭代。选型的核心原则是让 runtime 的复杂度匹配你的会话量级不要提前优化。4. 在 Kubernetes 上落地 agentic 调度的实操路径4.1 第一步定义 AgentSession 的 CRD 结构如果你决定走 Operator 路线第一步是设计 CRD。我建议从最小可用字段开始不要一上来就设计大而全的 schema。一个够用的 AgentSession 大概长这样apiVersion: ax.example.com/v1alpha1 kind: AgentSession metadata: name: session-abc123 spec: input: query: 帮我分析这份销售数据 attachments: - ref: configmap://data/sales-q3 tools: - name: sql-query endpoint: http://tool-sql.default.svc:8080 - name: chart-render endpoint: http://tool-chart.default.svc:8080 resources: gpu: 1 memory: 8Gi timeoutSeconds: 1800 retryPolicy: maxAttempts: 3 backoffSeconds: 10 status: phase: Running currentStep: tool-call-2 startedAt: 2026-09-22T09:40:00Z这里的关键字段是tools和retryPolicy。tools定义了会话可用的工具集每个工具是一个独立的服务端点这样工具可以独立部署、独立扩缩容。retryPolicy定义了失败重试策略agent 执行过程中工具调用失败是常态没有重试策略的会话管理是不完整的。status.phase我建议至少定义这几个值Pending、Planning、Executing、Waiting、Succeeded、Failed、Cancelled。Waiting 状态特别重要因为 agent 经常在等外部 API 返回这个状态下不应该占用计算资源Operator 应该把对应的 Pod 缩到零或释放 GPU。4.2 第二步Operator 的 reconcile 逻辑设计Operator 的核心是 reconcile 循环。每次 reconcile它读取 AgentSession 的 spec 和 status计算期望状态和实际状态的差异然后执行动作。我建议把 reconcile 逻辑拆成几个独立的子控制器每个子控制器负责一个方面SessionController负责会话的创建、状态机推进、超时回收ToolBindingController负责把 spec.tools 翻译成 Service 和 Endpoint确保工具可达ResourceController负责根据 status.phase 调整资源配额Waiting 时释放 GPUExecuting 时申请 GPURetryController负责监听失败事件按 retryPolicy 决定是否重试拆成子控制器的好处是每个控制器的逻辑简单测试容易出问题也好定位。坏处是控制器之间的协调需要额外设计比如 SessionController 推进到 Executing 时ResourceController 需要知道该申请资源了。我通常用一个共享的 event bus 或者直接让 SessionController 更新 status其他控制器 watch status 变化。这里有个坑reconcile 必须是幂等的。因为 reconcile 可能被多次触发如果每次触发都创建新 Pod就会产生资源泄漏。我的做法是给每个会话的每个执行阶段生成一个确定性的名字比如session-abc123-step-2创建前先查是否存在存在就复用。这样即使 reconcile 跑十次也只会有一个 Pod。4.3 第三步状态持久化与恢复agent 会话的状态包括当前执行到哪一步、中间结果是什么、工具调用的输入输出、上下文窗口的内容。这些状态必须持久化否则 Pod 重启或节点故障后会话就丢了。持久化方案我试过三种。第一种是存 ConfigMap简单但容量有限而且 ConfigMap 的更新有延迟不适合高频写入。第二种是存 Redis读写快但需要额外维护 Redis 集群而且 Redis 持久化配置不当会丢数据。第三种是存对象存储比如 S3 兼容的存储容量无限成本低但读写延迟高不适合频繁更新的状态。我最终采用的是一种混合方案热状态存 Redis冷状态定期快照到对象存储。具体来说会话执行过程中的中间结果写 Redis每完成一个主要步骤就把完整状态序列化后写对象存储。Pod 重启时先从 Redis 恢复Redis 没有就从对象存储加载。这样兼顾了性能和可靠性。序列化格式我推荐用 MessagePack 或 Protobuf比 JSON 省空间序列化速度也快。4.4 第四步超时、取消与资源回收agent 会话最容易出问题的地方是超时和取消。用户发了一个请求等了五分钟没响应可能就取消了。如果系统不处理取消信号会话会一直跑下去占用资源。我的做法是在 AgentSession 的 status 里加一个cancelRequested字段用户取消时更新这个字段Operator watch 到变化后向执行中的 Pod 发送 SIGTERM并给一个 grace period比如 30 秒让 agent 保存状态然后强制终止。超时处理类似spec 里的timeoutSeconds到期后Operator 主动把 phase 置为 Failed并触发资源回收。资源回收要彻底删 Pod、删 Service、释放 GPU、清理 Redis 里的热状态、把冷状态标记为可归档。我见过有团队忘了清理 GPU 配额结果 GPU 被僵尸会话占满新会话调度不上去。提示资源回收一定要做成幂等的而且要有兜底机制。我通常会在 Operator 里加一个定时任务每隔十分钟扫描一遍所有 AgentSession把超过保留期的 Succeeded/Failed 会话彻底清理掉。这样即使某个会话的回收逻辑漏了兜底任务也能补上。5. 常见问题与排查技巧实录5.1 调度失败Pod 一直 Pending 的排查路径AgentSession 创建后 Pod 一直 Pending是最常见的问题。排查顺序我总结成一张表现象可能原因排查命令解决办法Pod Pending事件显示 Insufficient gpuGPU 资源不足kubectl describe pod检查是否有僵尸会话占 GPU或扩容节点Pod Pending事件显示 node selector 不匹配节点标签不对kubectl get nodes --show-labels修正 nodeSelector 或给节点打标签Pod Pending无事件调度器没工作kubectl get events -n kube-system检查 scheduler 是否正常Pod 创建了但容器起不来镜像拉取失败kubectl describe pod检查镜像地址和 imagePullSecret容器起不来报 CRI 错误容器运行时异常crictl info重启 containerd检查 socket 路径这个表我基本背下来了因为每周都会遇到几次。最隐蔽的是“无事件”那种Pod 创建了但调度器没反应通常是 scheduler 的 leader election 出了问题或者节点被 cordon 了但没注意。5.2 会话卡死状态机不推进的三种典型场景会话卡在某个状态不推进比调度失败更难排查因为表面上看一切正常。我遇到过三种典型场景。第一种是工具服务不可达。agent 调用一个工具工具服务挂了agent 一直等响应没有超时机制就卡住了。解决办法是在工具调用层加超时超时后触发重试或失败。我通常给每个工具调用设 30 秒超时重试两次还失败就整个会话失败。第二种是状态更新丢失。Operator 更新了 status但更新冲突resourceVersion 不匹配更新被拒绝Operator 没有重试状态就停在旧值。解决办法是用retry.RetryOnConflict包装 status 更新冲突时重新读取再更新。第三种是reconcile 死循环。Operator 每次 reconcile 都发现期望状态和实际状态不一致反复创建删除资源但状态机不推进。这通常是 reconcile 逻辑有 bug比如比较状态时用了不稳定的字段。解决办法是加日志把每次 reconcile 的输入输出打出来看是哪一步在反复。5.3 性能问题会话启动慢、工具调用延迟高的优化会话启动慢通常是镜像太大或初始化逻辑太重。我做过一次优化把 agent runtime 的镜像从 2GB 压到 300MB启动时间从 40 秒降到 8 秒。压缩手段包括用多阶段构建、用 alpine 或 distroless 基础镜像、把模型文件外置到对象存储按需加载、把工具依赖做成 sidecar 而不是塞进主镜像。工具调用延迟高通常是网络问题或连接池配置不当。我建议给每个工具服务配一个连接池池大小根据 QPS 调整。如果工具服务在同一个集群用 ClusterIP Service 而不是 NodePort减少一跳。如果工具服务在集群外考虑用 ServiceEntry 或直接配 Endpoint避免 DNS 解析开销。还有一个容易被忽略的点是GPU 显存碎片。多个会话共享一个 GPU 时如果每个会话申请的显存不是 2 的幂次容易产生碎片导致新会话申请不到显存。我的做法是显存配额统一按 2GB、4GB、8GB 这样的档位申请减少碎片。5.4 安全与隔离多租户场景下的注意事项如果多个团队共享一个集群跑 agent隔离就很重要。K8s 原生的 Namespace 隔离是基础但不够。agent 会话可能执行任意代码比如代码解释器工具需要更强的隔离。我建议用 gVisor 或 Kata Containers 做沙箱虽然性能有损耗但安全边界清晰。网络策略也要配。默认情况下同一个 Namespace 里的 Pod 可以互相访问agent 会话之间不应该能互相访问。用 NetworkPolicy 限制只有 Operator 和工具服务能访问 agent Podagent Pod 之间禁止互访。RBAC 也要收紧agent Pod 的 ServiceAccount 只给必要的权限不要用 default。注意agent 会话如果会执行用户提供的代码一定要做资源限制。我见过有会话跑了一个死循环把节点 CPU 打满影响了同节点其他服务。CPU 和内存的 limit 必须设而且不要设得太宽松。6. 我在这套方案上踩过的坑和总结的经验第一个坑是过早引入 CRD。我最早做 agent 调度时一上来就设计了复杂的 CRD结果发现会话量根本没到需要 CRD 的级别维护 Operator 的成本远大于收益。后来退回到 Deployment Redis 的方案简单直接迭代速度快了很多。CRD 应该是在会话量稳定在每天几千以上、且调度逻辑确实复杂到需要自定义资源时才引入。第二个坑是状态持久化做太重。我一度把每个工具调用的输入输出都持久化结果 Redis 写入量巨大成了瓶颈。后来改成只持久化关键状态当前步骤、中间结果摘要、上下文窗口工具调用的详细日志写到对象存储异步归档Redis 压力立刻降下来了。持久化的粒度要匹配恢复的需求不需要恢复的东西就别持久化。第三个坑是忽略工具服务的独立性。我最初把工具和 agent runtime 塞在同一个 Pod 里结果工具升级要重启 agentagent 升级要重启工具互相影响。后来把工具拆成独立 Deployment通过 Service 调用两边可以独立发布。代价是网络调用多了一跳但换来了部署灵活性值得。第四个坑是超时策略一刀切。我一开始给所有会话设了统一的 30 分钟超时结果有些长任务被误杀有些短任务占着资源不放。后来改成按会话类型设不同超时短查询 5 分钟数据分析 30 分钟批量处理 2 小时并且允许会话在接近超时时申请延期。这样资源利用率和用户体验都好了很多。如果让我给正在做类似事情的团队一个建议那就是先把单进程方案跑通再考虑分布式。很多问题在单进程阶段就能暴露出来而且单进程调试成本低得多。等到单进程扛不住了再往 K8s 上迁这时候你对 agent 的行为特征已经有了足够的理解设计调度层时不会拍脑袋。我见过太多团队一上来就搞分布式结果连 agent 的基本执行流程都没跑顺白白浪费了几个月。最后分享一个排查小技巧给每个 AgentSession 生成一个唯一的 trace ID这个 ID 贯穿 Operator 日志、agent runtime 日志、工具服务日志。出问题时用 trace ID 一搜整条链路一目了然。这个习惯帮我省了无数排查时间强烈建议你也加上。
返回列表