Cloud Run单实例:长期Agent为什么不该硬套无状态服务

我们搭 Zyentor 的行为引擎时踩过一个坑:最初把它当成普通 Serverless 部署,结果它每隔 3 分钟要跑一轮任务,服务一被缩到 0,下次冷启动要等十几秒,任务全挤在一起。

更糟的是有一次它把自己卡死了——同步调用 DeepSeek API 阻塞了事件循环,整个网站 HTTP 请求全部超时,但进程还活着、8080 端口还通、curl localhost:8000/ 返回 200,健康检查一直显示 healthy。我们过了大半天才发现,最后靠 Docker 健康检查 + 每 5 分钟检测一次、连续失败就重启的 cron 才兜住。

所以看到 Google Cloud 8月27日进入 Preview 的 Cloud Run instances,我是真的觉得它切中了长期 Agent 的部署痛点:固定单实例、不自动横向扩容,单次可连续运行最长7天,默认自动重启,HTTPS URL 在更新与重启后保持不变,也可以手工 Stop / Resume。官方给出的成本锚点是:

1 vCPU + 1 GiB RAM
连续运行30天
约 $5.70

OffDeal 对其长运行 Agent 的公开反馈是 Cold Start 下降 88%。

真正的问题不是"要不要用 Cloud Run instances",而是长期 Agent 到底应该按什么运行模型部署。

单实例不等于状态全放本地

看到"只有1个Instance"后,最容易把 Memory、Task Queue、Checkpoint、Credential、Artifact 全部塞进本地盘。

我们也干过这事:最早把 Checkpoint 直接写进容器本地盘,想着"反正只有一个实例"。结果一次自动重启之后,任务从零开始跑,之前做到一半的进度全丢了。

因为实例仍然可能重启、更新、7天滚动或故障恢复。所以状态应分三类:

可丢:
连接池、短期Token Cache

可重建:
Prompt Cache、依赖Cache

不可丢:
Checkpoint、Memory、Approval、Side Effect Ledger、Task Cursor

不可丢状态必须外置。

Cloud Run Instance
├─ Agent Runtime
├─ Local Cache
└─ Worker Loop
      ↓
External State
├─ Postgres
├─ Redis
├─ Object Storage
└─ Secret Manager

实例重启后应该是"重新启动 → 加载Checkpoint → 恢复Cursor → 继续",而不是从头做。

单实例仍然需要Lease和Fencing Token

基础设施说"只有一个实例",业务层也不应该完全相信它。

更新或异常恢复时,最危险的是短窗口重叠:旧实例还没完全退出,新实例已经接管,两个 Worker 都认为自己是唯一执行者。

可以用:

create table agent_runtime_lease (
    agent_id varchar(128) primary key,
    holder_id varchar(128) not null,
    fencing_token bigint not null,
    expires_at timestamptz not null
);

新实例拿 Lease,fencing_token 递增到 919,所有关键写入都携带 919。旧实例还拿着 918,就算恢复,也不能再写。

为什么只有Redis Lock还不够

假设锁 TTL=30 秒。旧实例网络暂停 40 秒,锁过期;新实例拿到锁。旧实例恢复后并不知道自己已经失去领导权。

所以 Lock 决定现在谁有权,Fencing Token 阻止旧 Holder 继续写,两者职责不同。

7天运行上限意味着"重启必须被当成正常事件"

不要等第7天才验证恢复。我们现在的做法是:测试环境定期手动 kill 掉 Runtime,看它能不能自动重启、重新抢 Lease、加载 Checkpoint、确认任务没有重复。这套演练跑过几次之后,我们才对"重启"这件事真正放心。

长期 Agent 的可靠性应该建立在"重启会发生",而不是"希望永不重启"。

固定HTTPS URL不等于应该公开

固定 URL 很适合 Webhook、Agent Gateway 和状态查询。但生产上不要因为入口稳定就直接 --public

更合理的是 Signed Webhook、Identity-Aware Access、Private Service、API Gateway。URL 稳定解决的是寻址,不是授权。

Stop / Resume真正需要Durable Inbox

如果 Agent 停掉期间仍可能收到 Webhook、消息、队列事件,就必须先落 Durable Inbox。

我们最初为了省成本试过 Stop,结果停掉期间丢了几条 webhook 消息,事后才知道要先接一个 Inbox。Stop 时 Inbox 继续积累,Resume 后从 Cursor 继续消费——否则 Stop 不是成本优化,而是数据丢失。

健康检查不能只看HTTP 200

这一点我们是真的用血换来的教训。前面说的那次行为引擎卡死,进程活着、端口通、HTTP 200,健康检查一直 healthy,但实际所有请求都在排队超时。进程活着不等于 Agent 能正确工作。

真正 Ready 至少满足:

Lease Owned
DB Connected
Queue Connected
State Loaded
Policy Loaded

例如:

{
  "ready": true,
  "lease": "owned",
  "fencing_token": 919,
  "checkpoint": "loaded",
  "queue": "connected"
}

我会监控这7个指标

agent_runtime_restart_total
agent_lease_acquire_total
agent_lease_lost_total
agent_checkpoint_restore_seconds
agent_duplicate_task_blocked_total
agent_event_backlog
agent_uptime_seconds

其中 duplicate_task_blocked_total 很有价值:它能告诉你是否真实发生过重叠执行。

$5.70也不是Agent总成本

基础实例可能很便宜,但真正的大头往往是模型 Token、Tool API、数据库、日志、对象存储。所以长期 Agent 应看 Runtime Cost + Inference Cost + Tool Cost,而不是只看 Serverless 账单。

什么时候单实例更合适

适合:一个用户一个长期 Agent、一个会话一个常驻 Worker、后台 Agent 持续监听、低并发但需要持续状态。

不适合:高并发、请求独立、无共享状态——后者仍然更适合普通 Cloud Run Service。


长期 Agent 的部署模型不能简单照搬普通 Web API。四个容易混淆的概念是:

Singleton ≠ 永不重启
Stateful ≠ 状态全放本地
Fixed URL ≠ 直接公开
One Instance ≠ 不需要Fencing

Cloud Run instances 最有价值的地方,是把长期、低占用、偶尔突发的 Agent Runtime 变成一等部署形态。但生产可靠性最终仍取决于你有没有把 Lease、Checkpoint、Durable Inbox、Fencing 和 External State 一起补上。

一个可直接落地的Lease获取逻辑

如果用 PostgreSQL,可以把"抢Lease"和"递增 Fencing Token"放在一个事务里。

insert into agent_runtime_lease(
    agent_id,
    holder_id,
    fencing_token,
    expires_at
)
values (
    :agent_id,
    :holder_id,
    1,
    now() + interval '30 seconds'
)
on conflict (agent_id)
do update set
    holder_id = excluded.holder_id,
    fencing_token =
        agent_runtime_lease.fencing_token + 1,
    expires_at =
        now() + interval '30 seconds'
where
    agent_runtime_lease.expires_at = fencing_token;

更严谨可以单独在数据库里维护 latest_fencing_token,任何旧 Token 写入直接拒绝。这样即使旧进程恢复,它也无法污染新实例状态。

队列领取也要带Lease版本

Task 领取时记录"谁、以哪个 Token 拿走任务",后续完成时必须匹配。否则会出现旧实例完成旧任务、却覆盖新实例已经重试后的结果。

Restart Recovery要分三种任务

  • 幂等任务(如重新生成摘要):可以直接 Retry。
  • 可 Reconcile 任务(如发邮件、更新 CRM):先查 Side Effect Ledger。
  • 不可安全恢复任务(如某些无幂等外部写操作):进入 MANUAL_REVIEW。

不要用一个统一的 restart → retry all 策略。

public enum RecoveryPolicy {
    RETRY_SAFE,
    RECONCILE_THEN_RETRY,
    MANUAL_REVIEW
}

Task Definition 在创建时就明确。这会让长期 Agent 的重启恢复从"希望能续上"变成确定性状态机。


更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界

https://www.zyentor.com/