谷歌开源 AX:任务状态为什么不能放 Kubernetes CRD——用 Redis Streams 编排十亿级 agent 的架构拆解
一句话概括:AX 是谷歌开源的 agent 编排运行时。它值得看的地方不是"又一个 agent 框架",而是它为了扛住海量短生命周期任务,主动放弃了 Kubernetes CRD 作为任务状态存储——理由写在官方设计文档里,很直白。
所有事实来自 Google 官方仓库github.com/google/ax、其DESIGN.md/docs/与官网agentexecutor.io,抓取时间 2026-09-25。文末列了全部来源。
一、先对齐时间线:它不是今天才出生的
先把"新"这个字说准,避免误判成熟度:
| 时间 | 事件 |
|---|---|
| 2026-03-30 | 仓库google/ax创建 |
| 2026-08-20 | 已有doctor诊断子命令、sidecar PID 跟踪等提交 |
| 2026-09-20 | 一次改变项目定位的重构(commitdc4f36c) |
| 2026-09-23 | 补上对外路线图(PR #381) |
| 2026-09-25 | 单日 5 个提交;当天登上 GitHub 日榜第 4 |
仓库当前状态(2026-09-25 抓取):Apache-2.0,主语言Go,11,133 star、536 fork、41 个 open issue,最近一次 push 是当天 13:25 UTC。
真正让它值得写的是 9 月 20 日那次重构。commit message 原文写得很清楚:
AX is undergoing a significant redesign, moving away from a single CLI with an embedded Python harness toward a general-purpose orchestration layer for agentic tasks.
拆成三个二进制:
ax-server暴露 gRPC API 接收 Task manifest,ax-controller作为水平扩展的 reconciler消费Redis Streams,ax-task-runner在沙箱化 worker 内执行任务。任务状态现在存在 Redis 而不是 Kubernetes CRD,这样系统能处理数百万个短生命周期任务而不压垮 etcd。旧的 Python harness、ATE client、SQL 事件日志和 skill 示例全部删除。
翻成一句话:它原本是个带内嵌 Python harness 的 CLI,现在是一套通用编排层。这个转向本身就是本文的主题。
二、它的立论:agent 是一种新的工作负载
README 里那句话是整个项目的地基:
Agents are a new kind of workload. They are neither stateless microservices nor run-to-completion batch jobs.
(Agent 是一种新的工作负载。它既不是无状态微服务,也不是跑完即走的批处理作业。)
官网把这条论证展开得更完整:
Agent 型负载是有状态、突发、长时间运行的 actor:它高强度计算一分钟,然后等待模型响应、工具响应或人工审批。为无状态微服务或可预测批处理作业设计的传统编排器,在维持空闲沙箱运行这件事上会变得极其昂贵,同时又缺乏对亚秒级 suspend/resume 的原生支持。
这段话解释了两个设计选择的动机:
- 为什么要有
suspend/resume——agent 大部分时间在等,等待期间不该占着 CPU 和内存。 - 为什么不能复用现成的编排器——Kubernetes 擅长"让 N 个副本一直活着",而 agent 需要的是"活着、随时能冻住、随时能解冻、且解冻后内存和文件系统都还在"。
值得注意的是这句自我定位:AX 官方明说它不是 agent SDK,而是运行 agent 的系统。这和 LangChain 一类框架不在一个层面。
三、四个(或三个?)原语
这部分有个值得注意的细节:官方三处表述目前没对齐。
- 仓库 README 写的是 “three small primitives”:
Task/Workspace/Model - 官网首页写的是 “four small primitives”,多一个
Gateway(网络策略) docs/concepts.md目前详述的是三个:Task/Workspace/Model
我按最新最全的官网口径列四个,并标注文档现状:
| 原语 | 解决什么 | 文档状态 |
|---|---|---|
Task | 在隔离沙箱里跑不受信任的 agent 代码,带 CPU / 内存限额 | concepts.md 有 |
Workspace | 预先把 Git 仓库、MCP server、skill 包接好,让每个 agent 一启动就是热的 | concepts.md 有 |
Gateway | 网络策略:把出站流量锁到显式 allowlist 的主机和端口上,并给入站请求注入凭据 | concepts.md 暂无对应章节 |
Model | 平台自己用哪个 LLM、参数是什么、API key 从哪个 Kubernetes Secret 取 | concepts.md 有 |
Model的定义值得单独说一句,官方原话是 “AModelis not a model.”——它不是模型,而是一份具名的模型配置。把它做成一种资源的意义在于:轮换密钥、钉住新模型版本、收紧某个参数,都变成一次ax apply,而不是去翻遍所有 task 定义。AX 自己的组件也读它,比如根据goal规划 workspace 的时候。
Workspace里最有意思的是goal字段。它允许你用一句自然语言描述"环境该长什么样",首次启动时 runner 会把这句话交给一个 agent 去把环境装完:
apiVersion:ax.io/v1alpha1kind:Taskmetadata:name:data-analysisspec:workspaces:-name:python-envgoal:"Set up a Python 3 development environment"四、核心架构:为什么状态不放 CRD
这是全文最硬的部分。DESIGN.md开头直接给了理由:
把数百万个短生命周期任务存成 Kubernetes CRD 会把 etcd 推到它的舒适区之外(个位数 GB 的存储上限、写入速率瓶颈、控制面性能退化)。AX 把状态放在 Redis,并用 Redis Streams 作为 API server 与水平扩展的 controller 池之间的工作队列。
这是对 Kubernetes 能力边界一次相当坦率的承认:etcd 适合存"集群的期望状态",不适合存"每分钟产生几十万条的作业记录"。CRD 不是用来当高吞吐作业队列的。
数据流是这样的:
ax apply -f task.yaml │ ▼ ax-server ← 无状态 gRPC API(:8080)+ /healthz │ 存状态 & 发布事件 │ ▼ Redis ← Task Hashes + Event Streams + PubSub │ XREADGROUP(Streams) │ ▼ ax-controller ← 水平扩展的 reconciler 池 │ gRPC(Control API) │ ▼ Agent Substrate ← atespace 供给 / actor 创建与激活 / worker 分配四个二进制各司其职:
| 二进制 | 职责 |
|---|---|
ax | 开发者 CLI。应用 manifest、查看与 watch 资源、向集群打隧道 |
ax-server | 无状态gRPC API,监听 8080。校验 manifest、持久化到 Redis、发布事件 |
ax-controller | reconciler worker。消费 Redis stream,在 Agent Substrate 上供给 atespace 与 actor,把任务推向期望状态。加副本即扩容 |
ax-task-runner | 每个任务容器内的入口程序。引导 workspace、提供 metadata 服务、运行 agent 命令 |
注意ax-server是无状态的,状态全在 Redis——所以 API 层可以直接水平扩。控制面 API 是 gRPC 服务ax.v1alpha1.AX,健康检查则是普通 HTTPGET /healthz返回 200。
一个有意思的细节:Task的WatchTask是服务端流式 RPC,会实时推送 status 与 condition 的迁移,而不是让你轮询。
五、沙箱内部:PID 1 是谁
docs/sandbox.md写得很具体,而且官网演示里有一段真实终端输出可以互相印证:
$ ax ssh test -- ps -o pid,cmd PID CMD 1 /usr/local/bin/ax-task-runner 12 go build ./...每个任务容器都以ax-task-runner作为 PID 1 启动。它启动后做四件事:
- 加载
Task和所有绑定的Workspacespec - 在80 端口起一个 metadata / guest 管理守护进程
- 首次运行时按绑定顺序准备每个 workspace:克隆 Git 仓库、设置 skill 路径;如果绑定带
goal,就把这个 goal 交给一个 Antigravity agent 去完成环境安装。这个 agent 需要容器里有GEMINI_API_KEY,默认给 10 分钟,可用AX_BOOTSTRAP_TIMEOUT改。在包括 agent 运行在内的所有 workspace 完成之前,任务一直报 not-ready。 - 把
spec.command作为子进程拉起(工作目录是第一个 workspace),并把AX_METADATA_URL和spec.env注入环境变量,然后监督它
两个容易忽略的工程设计:
- runner 作为 PID 1 始终不退出,即使命令已经结束。所以命令跑完之后 metadata server 仍在应答,
ax ssh仍然能进得去; - 沙箱停止或挂起时,runner 给命令的进程组发
SIGTERM,等 10 秒,然后才强杀。这比"直接 SIGKILL"给了 agent 收尾的机会。
Metadata server 同时说 HTTP/1.1 和h2c,端点如下:
| 端点 | 方法 | 返回 | 说明 |
|---|---|---|---|
/healthz | GET | text/plain | 存活。永远 200 |
/readyz | GET | text/plain | 就绪。workspace 初始化期间返回503,克隆、MCP 配置、skill 都就位后返回 200 |
/metadata/v1alpha1/ax/task | GET | application/yaml | Task 启动配置(不含status 和挂起标志) |
/metadata/v1alpha1/ax/workspaces | GET | application/yaml | 所有绑定的 Workspace,按绑定顺序的多文档流 |
spec.debug: true时,同一个端口还会通过 gRPC 提供 Agent Substrate 的 guest services(进程服务 + 文件系统服务,前者正是ax ssh的底座)。它们默认关闭,因为允许在沙箱内任意执行进程和读写文件——ax ssh会拒绝连接没有开启它们的任务。这个默认值是合理的:把"能进容器执行任意命令"变成一个必须显式打开的开关。
六、网络:Task 没有 Service,也没有 Ingress
这一条对习惯了 Kubernetes 的人会有点反直觉。docs/networking.md明确写:
Task 不会得到属于自己的 Kubernetes Service 或 Ingress。
所有发往 task 的请求都走 Agent Substrate 的atenet router(ate-system命名空间下的atenet-routerService)。路由器只读一个 header:
ate-target-actor: <atespace>/<task>它的行为是:解析出这个 actor 跑在哪台 worker 上,如果它处于挂起状态就先唤醒,然后把请求代理过去。Host和:authority留给你自己的应用,只有这个 header 负责选目标。
从集群内访问:
curl-H"ate-target-actor: default/task123"\http://atenet-router.ate-system.svc.cluster.local/metadata/v1alpha1/ax/task从本机访问,先端口转发再照常说:
kubectl-nate-system port-forward svc/atenet-router8001:80curl-H"ate-target-actor: default/task123"http://localhost:8001/readyzgRPC 则把同一个 header 放进 outgoing metadata:
ctx=metadata.AppendToOutgoingContext(ctx,"ate-target-actor","default/task123")resp,err:=client.SomeMethod(ctx,req)"路由器发现目标是挂起的就先唤醒再代理"这一点是整套设计的关键。它意味着一个被 checkpoint 到磁盘的 agent 仍然持有一个稳定的寻址标识,外部无需知道它当前是否在内存里。没有这个能力,suspend就只能省自己的钱、断自己的路。
七、底座:Agent Substrate 凭什么能扛
AX 自己不做沙箱,它跑在Agent Substrate(github.com/agent-substrate/substrate)上面。这个底座的官方指标相当激进:
| 指标 | 官方口径 |
|---|---|
| 密度 | 运行数百万沙箱,密度比标准容器运行时高10 倍 |
| 恢复延迟 | 恢复操作亚 500 毫秒 |
| 吞吐 | 每秒 500 次以上suspend/resume 激活 |
| 隔离 | 原生零信任内核与网络隔离;支持microVM 与 gVisor |
| 复用 | 演示中把一个集群的约 250 个有状态 actor 复用到仅 8 个物理 pod 上(30 倍以上超配) |
它的核心思路一句话讲完:把一大组 actor 映射到一小撮就绪 worker 上,赌的是"agent 类应用大部分时间在发呆"。官方把这套能力叫 Actor Teleport——挂起时把易失内存和文件系统状态一起做全量快照,唤醒时在 worker 池里任意一台机器上恢复。
Substrate 自己并不排斥 Kubernetes。它用 K8s 做基础设施供给和 worker 生命周期管理(worker 就是 Pod),在 Pod 和 Pod 自动扩缩容之上再叠一层 agent 专用的调度,为的是更低延迟。这个分层是合理的:K8s 管"机器",Substrate 管"actor",AX 管"任务语义"。
顺带一提,Substrate 的生态里还有个 CNCF Sandbox 项目kagent也在用它跑沙箱化的有状态 agent 负载——说明这不是一个只服务自家产品的内部组件。
八、跑起来:前置条件 → 步骤 → 验证
8.1 前置条件(不满足会直接失败)
| 需要 | 说明 |
|---|---|
| 已装 Agent Substrate 的 Kubernetes 集群 | 这是硬前置。Substrate 落在ate-system命名空间,Control API 暴露在api.ate-system.svc.cluster.local:443,AX 默认去这里找它 |
Go 与kubectl | 装 CLI 用 |
ko与一个集群能拉取的镜像仓库 | 构建并部署控制面 |
| Redis | 由make deploy部署,状态与工作队列都在它上面 |
先确认 Substrate 起来了,再往下走:
kubectl get svc api-nate-system8.2 三步跑通第一个任务
# 1) 装 CLI(会装到 $(go env GOPATH)/bin,确认它在 PATH 里)goinstallgithub.com/google/ax/cmd/ax@latest# 2) 部署控制面(自动部署 Redis,然后用 ko 构建并部署控制面镜像)makedeployAX_IMAGE_REPO=<你的仓库># 3) 跑第一个任务(一个文件里同时有 Task + Workspace + Model)ax apply-fexamples/task.yaml ax get tasks# NAME ATESPACE PHASE ACTOR WORKER-IP AGE# task123 default Running task123 10.20.3.67 1max的 CLI 形状是刻意照着kubectl做的:apply/get/describe/watch/delete,外加几个 agent 特有的动词:
axwatchtask task123# 实时流式看 phase 与 condition 迁移axsshtask123 --ls-la/workspace# 进沙箱翻一翻axsuspendtask task123# 打检查点并暂停ax resume task task123# 从断点继续ax还会跟随你当前的 Kubernetes context——切集群后它会自动在后台解析并隧道到那个集群的控制面:
kubectx staging-cluster&&ax get tasks kubectx prod-cluster&&ax get tasks ax--context=dev-cluster get tasks# 或者不改 context 直接指定8.3 怎么验证"挂起真的保住了状态"
这一步别只看命令返回 0。官网演示给了一条可复现的验证路径——在挂起前写一个文件,恢复后读它:
axsshtest--touchnotes.txt# 挂起前落一个文件axsuspendtasktestax resume tasktestaxsshtest--lsnotes.txt# 期望输出:notes.txt官方演示的实际输出就是notes.txt。这说明恢复回来的不只是进程,而是文件系统状态。要验证内存状态,官方提供了 Counter Demo:一个有状态 Go HTTP server,反复 suspend/resume 后计数不回零。
8.4 在任务里读自己的配置
容器内不需要任何 SDK 就能自省配置。下面这段轮询/readyz直到 workspace 就绪,用的全是标准库:
importosimporttimeimporturllib.errorimporturllib.request BASE=os.environ["AX_METADATA_URL"].rstrip("/")defwait_ready(timeout=600.0,interval=2.0):"""轮询 /readyz,直到 workspace 初始化完成(503 -> 200)。"""deadline=time.monotonic()+timeoutwhiletime.monotonic()<deadline:try:withurllib.request.urlopen(f"{BASE}/readyz",timeout=5)asresp:ifresp.status==200:returnTrueexcepturllib.error.HTTPErrorasexc:ifexc.code!=503:raisetime.sleep(interval)returnFalseif__name__=="__main__":print("workspace ready:",wait_ready())九、五个容易踩的坑
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | 没装 Agent Substrate 直接部署 AX | 前置缺失不会立刻报"缺依赖",官方在 PR #399 里专门修过这个体验:改用例前 demo.sh 会做 preflight 检查 CLI、kubectl 和 Substrate Control API Service。自己部署时请先跑kubectl get svc api -n ate-system |
| 2 | ax ssh连不上 | guest services 默认关闭。必须在 Task 里设spec.debug: true,否则ax ssh会拒绝连接而不是报权限错误 |
| 3 | 在Model里设temperature | 官方在 2026-09-25 的 PR #397 里把示例中的temperature全部删掉,理由是 “We don’t recommend this parameter.”。照抄老示例会带上一个官方不推荐的参数 |
| 4 | bootstrap 超时 | 带goal的 workspace 会交给 agent 装环境,默认10 分钟。装大工具链会超时,用AX_BOOTSTRAP_TIMEOUT(Go duration 格式)调大 |
| 5 | ax delete看起来"卡住" | 这是设计如此:删除会把任务推进Terminating,controller 拆完沙箱后才移除记录,ax delete会一直阻塞到这一步完成 |
关于任务状态机,官方建议只等我Ready这一个 condition:
| Condition | 何时为 True |
|---|---|
WorkspaceReady | 所有 workspace 都完成初始化。此后一直为 True |
Ready | 任务在跑且WorkspaceReady为 True。这个才是该等的 |
挂起会把Ready置为 False、reason 为TaskSuspended;恢复再置回。
十、现状与风险(必须说清楚)
官方自己在 README 顶部挂了警告:
我们仍在积极调整核心概念、协议与规范。在稳定版发布之前,很可能会引入重大破坏性变更。
加上底座 Substrate 也标着 pre-1.0、明确不保证向后兼容,所以:
- 不适合现在押上生产。它的价值目前在于读它的设计,而不是把它当基础设施。
- 它不是托管服务。要自建 K8s 集群、自己装 Substrate、自己维护 Redis 和镜像仓库——"零运维"不在承诺里。
- Redis 是控制面的有状态依赖。这是换取 etcd 之外高吞吐的代价,也意味着备份和可用性要自己设计。
- "十亿级"是设计目标而非实测数据。官方措辞是 “allowing you to scale to billions of concurrent agent sessions per cluster”,我找到的公开演示规模是 250 actor / 8 pod。两者不矛盾,但不要混为一谈。
十一、路线图里最值得盯的四件事
官方路线图(docs/roadmap.md)里有几项一旦落地会明显改变能力边界:
- Task 之间可以分叉——把一个运行中或已挂起的 Task(含检查点内存与 workspace 文件系统状态)分叉成多个并行任务,用来并发探索投机执行路径。这是 suspend/resume 的自然延伸,也是"有状态"真正变成优势的地方。
- 空闲检测与自动挂起——持续监控 actor 活动(进程执行、I/O、网络流量、活跃 gRPC/SSH 会话),自动对空闲任务触发检查点,回收 worker 的 CPU 与内存来提高密度。
- 给每个 Task 发 SPIFFE 身份——签发 X.509-SVID,让任务、MCP server 与内部服务之间走零信任 mTLS。
- runner 层自动采集 OTel 指标、分布式 trace 与结构化 agent 轨迹(prompt、模型响应、工具调用、进程执行、生命周期迁移),且不需要用户容器做任何埋点。
第 4 条对做 agent 评测和强化学习的人价值最大:轨迹采集从"每个框架各写一遍"变成运行时自带。
十二、一句话总结
AX 最有信息量的地方不是它提供了Task/Workspace/Gateway/Model这几个声明式原语——那是自然的设计选择。最有信息量的是它为了这些原语,选择不把状态放进 Kubernetes CRD,并在设计文档里直说了 etcd 的存储上限与写入瓶颈。一家把 Kubernetes 做成行业标准的公司,在自己的新编排器上给 etcd 划了一条边界,这条边界比任何功能列表都更值得记住。
参考来源
| 来源 | URL | 核验状态 |
|---|---|---|
| Google 官方仓库 AX | https://github.com/google/ax | ✅ 正文与元数据均已抓取 |
| AX README | https://raw.githubusercontent.com/google/ax/main/README.md | ✅ 正文完整抓取 |
| AX 设计文档 DESIGN.md | https://raw.githubusercontent.com/google/ax/main/DESIGN.md | ✅ 正文完整抓取(含 etcd 论证与架构图) |
| AX 核心概念 docs/concepts.md | https://raw.githubusercontent.com/google/ax/main/docs/concepts.md | ✅ 正文完整抓取 |
| AX 沙箱 docs/sandbox.md | https://raw.githubusercontent.com/google/ax/main/docs/sandbox.md | ✅ 正文完整抓取 |
| AX 网络 docs/networking.md | https://raw.githubusercontent.com/google/ax/main/docs/networking.md | ✅ 正文完整抓取 |
| AX 路线图 docs/roadmap.md | https://raw.githubusercontent.com/google/ax/main/docs/roadmap.md | ✅ 正文完整抓取 |
| AX 官网 | https://agentexecutor.io/ | ✅ 正文完整抓取(含演示终端输出与四原语表述) |
| Agent Substrate 仓库 | https://github.com/agent-substrate/substrate | ✅ README 完整抓取(含密度/延迟指标与 demo 说明) |
| GitHub API 仓库元数据 | https://api.github.com/repos/google/ax | ✅ 2026-09-25 抓取 |
重构 commitdc4f36c说明 | https://github.com/google/ax/commit/dc4f36cdba647f110b34fd69adf31eb2bec37ca3 | ✅ commit message 完整抓取 |
| 2026-09-25 GitHub 日榜 | https://raw.githubusercontent.com/jobcher/new-blog/refs/heads/main/content/new/daily/github_trending_2026-09-25.md | ✅ 第三方日报,AX 列第 4 |
| 谷歌 Cloud 官方博客(Agent Executor) | https://cloud.google.com/blog/products/ai-machine-learning/agent-executor-googles-distributed-agent-runtime | ⚠️未抓取成功(连接失败)。该链接由 Substrate README 引用,本文未使用其中任何内容 |
标签:开源, Kubernetes, AI Agent, Go, 云原生