Cloud Run scale-to-zero 部署模型
B1→B8 我们把能力底座一层层垒起来:评测方法论(B1)→ 真 token/cost 测量(B1)→ A/B 统计(B3)→ agent 运行时与工具(B6-B7)→ 真 API + 流式 + Docker 多阶段镜像(B8)。到 Day 80 收口时,镜像(Dockerfile.mcp)已经能本地 docker build 出来、容器内能跑 eval,但它还只活在我本机的 docker dae
阶段: B9 · 部署 + 韧性 + 可观测 + 独立红队(Day 81-90) 标签: #cloud-run #scale-to-zero #cold-start #serverless
今日导引(由浅入深)
B1→B8 我们把能力底座一层层垒起来:评测方法论(B1)→ 真 token/cost 测量(B1)→ A/B 统计(B3)→ agent 运行时与工具(B6-B7)→ 真 API + 流式 + Docker 多阶段镜像(B8)。到 Day 80 收口时,镜像(Dockerfile.mcp)已经能本地 docker build 出来、容器内能跑 eval,但它还只活在我本机的 docker daemon 里。
B9 这一批的主题是「让它离开我的笔记本」——部署 + 韧性 + 可观测 + 独立红队。今天是 B9 第一天,先攻部署模型:把昨天那个容器放到一个 serverless 容器平台(Cloud Run)上,理解 scale-to-zero 的成本/延迟权衡。这是从「能在容器里跑」到「能被公网请求驱动地跑」的桥。
放到 B1→B18 整条能力曲线看:B1-B5 是「能不能测、测得准不准」(评测/tokenizer/统计),B6-B8 是「能不能跑、跑得稳不稳」(agent 运行时/工具/真 API/容器),到 B9 才进入「能不能上线、上线后看不看得见、扛不扛得住攻击」。Day 81 的部署是这条「上线韧性可观测」支线的入口;它紧接 Day 80「容器内回归绿」,因为只有先确认容器里跑出来和本地一致,把它推上云才有意义——否则只是把一个不可信的东西部署得更远。
今天的最小可判定产出:复用 B8 的 Dockerfile.mcp 本地 build 出镜像,docker run 起 agent-evals runner,注入 model key 跑通 1 条 eval(与本地结果一致的 task 通过/失败记录)。云端 gcloud run deploy 本身留作「待云」,但部署模型的机理今天必须吃透。
1. 机理精读
Cloud Run 是请求驱动的 serverless 容器平台。 你交给它一个容器镜像(声明监听某端口的 HTTP 服务),它替你管理「有请求就拉起实例、没请求就缩到 0」的全过程。这与传统常驻 VM 的本质区别是:你不再为「等请求」的闲置时间付费。资源:Google Cloud Run docs — Container runtime & autoscaling (2026-01)。
scale-to-zero 的代价是 cold start。 当实例数为 0 时,第一个进来的请求要等一条冷启动链路:拉镜像(若节点无缓存)→ 启动容器 → 容器内进程初始化(Node 进程、依赖加载、首次 JIT)。这条链路的耗时直接加到首请求的端到端延迟上。对低/突发流量的 eval runner——它一天可能只被触发几次——scale-to-zero 是巨大的成本节省;但对延迟敏感的在线链路,冷启动是用户能感知的卡顿。
成本曲线的形状变了。 常驻 VM 是「按小时计费」,无论有没有流量都在烧钱;Cloud Run 是「按 vCPU·秒 + 内存·秒 + 请求数」计费,只在实例真正处理请求的那几秒收费。对一个偶发触发的 eval runner,这把闲置成本从「24×7」压到「实际跑 eval 的那几分钟」。这正是把 eval runner 放上 serverless 而不是常驻 VM 的核心理由。
两个旋钮决定实例数与单价的平衡:
concurrency(单实例并发):一个实例同时处理几个请求。并发设高 → 同样的流量需要的实例更少 → 单位请求更便宜,但单实例内存/CPU 争用风险上升。对 LLM 调用这种 I/O 等待为主的负载,适度调高并发能摊薄成本;对 CPU 密集型负载则要谨慎。min-instances(最小常驻实例):设成 0 才是真 scale-to-zero;设成min-instances=1则永远留一个热实例,用一点常驻成本换掉冷启动延迟。延迟敏感链路用它,纯 eval runner 不用它。
与「常驻容器编排(K8s)」的边界。 Cloud Run 帮你隐藏了节点、调度、扩缩容策略;K8s 给你完全控制权但要自己运维。对一个个人作品集 + 偶发 eval 的规模,serverless 容器是正确的复杂度档位——不要为了「显得专业」上 K8s,那是给当前流量过度工程。
冷启动的三个可优化层。 既然冷启动是 scale-to-zero 的唯一痛点,理解它由哪几段构成才能逐段优化:
- 镜像拉取:镜像越小,节点没缓存时拉得越快——这正是 B8 把镜像压到
<300MB、用多阶段构建剥掉 build 依赖的回报。 - 容器启动:node:20-slim 这种瘦基础镜像启动快;臃肿的 base(含一堆系统包)会拖慢这段。
- 进程初始化:Node 进程起来后还要加载依赖、首次 JIT、建连接池。把「重初始化」(如预热模型 client)做成懒加载或挪到健康检查之前,能让首请求更快返回。
scale-to-zero 不是「要么忍受冷启动、要么常驻」的二选一——它是「把冷启动各段压到可接受、再决定要不要 min-instances=1」的连续权衡。
Cloud Run 的请求契约(容易忽略的硬约束)。 Cloud Run 期望容器:① 监听 $PORT(它注入,不是你硬编码);② 在收到请求时才被计费(请求外 CPU 默认被限流,除非开 "CPU always allocated");③ 无状态——实例随时可能被回收,不能把状态写在本地磁盘当持久层。本仓 Dockerfile.mcp 已用 ENV PORT=8765 且服务读 $PORT,满足①;MCP server 注释写明「stateless HTTP」,满足③。
2. 代码走读
今天复用的是 B8 的两件已建产物,先走读它们的真实行为:
Dockerfile.mcp(已建,824 字节)的关键事实:
- 多阶段构建:
base(node:20-slim + corepack 启用 pnpm)→deps(pnpm install --frozen-lockfile)→runtime(从 deps 拷node_modules,再拷src/scripts)。分层让依赖层可缓存,改代码不必重装依赖。 ENV PORT=8765+EXPOSE 8765——服务监听 8765。Cloud Run 会把它期望的端口注入PORT环境变量,本镜像已用PORT而非硬编码,符合 Cloud Run「读$PORT」的契约。CMD ["pnpm", "mcp:serve"]——入口跑的是 MCP server(注释明确「no API key needed」),用 tsx 直接执行 TS 入口,无构建步骤。- 镜像头注释写明了运行约定:
-e MCP_AUTH_SECRET=dev-secret注入鉴权密钥;省略该 env 则不挂 OAuth gate(本地 dev 用)。这条与 Day 86 的「公网必须接住 B7 鉴权」直接呼应。
.dockerignore(已建,102 字节):把 node_modules、构建产物等排除出构建上下文,避免把本机臃肿目录灌进镜像层——这是镜像 <300MB 目标(B8 里程碑)的前提。
注意边界:今天 seed 让容器内跑的是 agent-evals runner + 挂 src/agent/eval/tasks.ts 的 1 个 task,而 Dockerfile.mcp 的默认 CMD 是 mcp:serve。所以这条 eval 是用 docker run ... <override cmd> 覆盖入口、或另起一条 run 命令跑的——镜像本体复用,入口按需覆盖。
一笔成本直觉的手算(说明 scale-to-zero 为何省)。 设一个 eval runner 一天被触发 5 次,每次实际跑 60s(1 vCPU + 512MB):
- serverless(Cloud Run,scale-to-zero):只为 5×60s = 300 vCPU·秒/天 ≈ 9000 vCPU·秒/月付费,加少量请求费。闲置的 ~99% 时间不计费。
- 常驻 VM(同规格 1 vCPU):730 小时/月 × 3600 = 约 2,628,000 vCPU·秒/月全额计费,无论有没有流量。
两者差着约 290 倍的计费基数——这就是「偶发流量用 serverless、别配常驻 VM」的量化理由。(具体单价以执行当周 Cloud Run pricing 为准,这里只算计费基数的量级差。)
3. 今日实战
可执行步骤(指向真实仓库路径):
- 本地 build:
docker build -f Dockerfile.mcp -t momoweb3-mcp .(手动步骤,需本机 docker daemon)。 - 容器内跑 1 条 eval:
docker run起 runner,注入 model key(env,绝不打进镜像),挂src/agent/eval/tasks.ts里的 1 个 task(如aml-structuring-classic),跑通拿到一条通过/失败记录。 - 比对:该条记录应与本地直接跑同 task 的结果一致(环境一致性回归,承接 Day 80 的「容器内通过率 == 本地通过率」绿线)。
- 云端(待云):
gcloud run deploy,配--concurrency、min-instances=0(eval runner 用 scale-to-zero),key 走--set-env-vars或 Secret Manager。
参数选择的对应关系(把机理落到旋钮):
| 链路类型 | min-instances | concurrency | 理由 |
|---|---|---|---|
| eval runner(偶发) | 0(真 scale-to-zero) | 适中 | 闲置占比高,省成本优先,冷启动可忍 |
| 在线延迟敏感链路 | 1(留热实例) | 调高摊薄 | 不能让用户吃冷启动尾延迟 |
本批次的 agent-evals runner 属第一行——这也是为什么今天主线是 scale-to-zero 而非 min-instances=1。
4. 今日实测 / 产出
Dockerfile.mcp+.dockerignore:已建(✅,docker build 为手动步骤)。- 容器内跑通 1 条 eval 结果:待跑(需 key 跑 + 本地 docker);目标产出 = 1 条与本地一致的 task 通过/失败记录。
- 云端
gcloud run deploy:待云。
诚实状态不升级:今天的可演示物只到「镜像已建」,「容器内 1 条 eval」和「云端部署」都还没发生。
5. 常见误区 / 陷阱
- 把 eval runner 错配成常驻 VM:偶发流量却 24×7 烧钱,是 serverless 场景最典型的成本反模式。eval runner 该用 scale-to-zero。
- 延迟敏感链路忘了
min-instances=1:在线链路若放任 scale-to-zero,用户会周期性吃到冷启动尾延迟。这要靠测量(Day 87 的 p95)暴露,而不是靠感觉。 - 把 key 打进镜像:镜像是可分发产物,硬编码 key 等于泄露。key 必须运行时注入(env / Secret Manager)——这条 Day 86 会再次强调。
- 引用已弃的 App Engine Flex 叙事:那是过时的 GCP 容器路径,今天的主线是 Cloud Run。
- concurrency 一刀切设 1:默认 concurrency=1 会让每个请求独占一个实例,实例数随流量线性暴涨、成本飙升。对 I/O 等待为主的 LLM 调用应适度调高,让单实例并发摊薄成本——但要先压测确认单实例内存够。
- 把 scale-to-zero 当成「永远省钱」:若链路延迟敏感、又被持续小流量打,反复冷启动既慢又未必省(每次冷启动也耗 CPU·秒)。这种场景
min-instances=1反而更优——决策要靠 Day 87 的 p95 测量,不能拍脑袋。
6. 学习资源(每条带 YYYY-MM)
- Google Cloud Run docs — Container runtime contract & autoscaling,2026-01。
- Google Cloud Run docs — Cold starts & min-instances,2026-01。
- Google Cloud — Cloud Run pricing(vCPU·秒 / 请求计费模型),执行当周复查单价,2026-01。
- Docker docs — Multi-stage builds(镜像分层缓存),2025-12。
- Google Cloud — Cloud Run container runtime contract(
$PORT注入 / 无状态 / 请求计费),2026-01。 - Google Cloud — Configuring concurrency on Cloud Run,2026-01。
一句话 ADR(把今天的选型固化)。 上下文:偶发触发的 agent-evals runner 需要一个可公网触发的运行环境。决策:用 Cloud Run(serverless 容器,min-instances=0),复用 B8 的 Dockerfile.mcp 镜像,key 运行时注入。理由:闲置占比高(计费基数比常驻 VM 小约两个数量级),冷启动对非延迟敏感的 eval 可忍。代价:首请求有 cold-start 延迟;放弃了常驻热实例的稳定低延迟。被否方案:常驻 VM(闲置烧钱)、K8s(当前流量过度工程)。这条 ADR 正是 AISA 作品集里「为什么这样部署」一问的标准答案形态。
SOTA检查 (2026-06 更新)
- 当前主流:Cloud Run 仍是 GCP serverless 容器主线,scale-to-zero / concurrency / min-instances 模型稳定,仍是 SOTA。
- 执行当周复查:计费单价(vCPU·秒 / 请求数)与
min-instances配置默认值——serverless 计费条款会调整,部署前对当周 pricing 页核对。 - 过时黑名单:App Engine Flex 叙事(已弃);把 eval runner 配成常驻 VM 的闲置成本反模式。
- 下次复查点:执行
gcloud run deploy当周,复查 Cloud Run 计费页与 cold-start 相关 release notes。
衔接
- 昨天:Day 80 — 端到端固化(容器内重跑 eval 回归,确认与本地一致,设计 CI 触发 docker build 链路)。
- 今天:用 Cloud Run 的 scale-to-zero/concurrency 模型,把昨天的容器从「本机能跑」推向「请求驱动地跑」,先吃透部署机理,1 条容器内 eval 待跑。
- 明天:Day 82 — OTel GenAI 语义约定 + Langfuse/Phoenix(部署之后,给模型调用埋上可移植的 trace)。