把 Cloud Run 冷启动从 9 秒压到 1.2 秒,我改的是镜像不是配置

手上的活是给一个做本地生活的小客户写查询接口,Node 20 + Express,跑在 Cloud Run 上,最小实例数 0(客户不愿意为闲置付费),内存 512Mi。压测时发现一个很尴尬的现象:服务热着的时候 P95 是 90ms,但只要闲五分钟,第一个请求稳定卡 8~9 秒。前端那边 loading 转圈转到用户以为挂了,我的外包口碑也跟着转圈。

先确认钱花在哪。开了启动 CPU boost 有一点点用,从 9s 到 7s 左右,杯水车薪。真正定位靠的是在入口打时间戳:

const t0 = Date.now();
app.use((req, res, next) => {
  res.on('finish', () => {
    console.log(JSON.stringify({ path: req.path, cold: Date.now() - t0 }));
  });
  next();
});

日志显示 require 阶段就吃掉 6 秒多。把 package.json 拉出来一看,之前为了省事把 firebase-admin、googleapis 整包、一个图像处理库全塞进去了,其中图像库只在一个后台脚本里用。整包 googleapis 尤其离谱,光是加载它引的一堆 API 描述就要好几秒。

做法很土:把后台脚本拆到另一个 Cloud Run 服务,主服务只留 express 和 @google-cloud/firestore;googleapis 换成按需的细粒度包;再把构建改成多阶段,运行镜像只拷 dist 和 production 依赖。额外加了一条启动探针,配合最小实例 1 但只在白天开:

startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  initialDelaySeconds: 2
  periodSeconds: 3
  failureThreshold: 5

改完冷启动 1.2s 左右,热请求没变化。

为什么有效:Cloud Run 冷启动其实是「拉镜像 + 起容器 + 跑你的代码」三段,配置能动的只有第一段的并发拉取和 CPU boost,代码加载时间它帮不了你。所以真正的大头是把 require 树砍短、把镜像压小。代价是多了一个服务要部署、要监控,环境变量得两边同步,我为此写了个部署脚本,不然迟早漏。另一个代价是最小实例 1 在白天是实打实的钱,客户看到账单问了我两次。

哪里会失效:如果你的冷启动大头在拉镜像(镜像几个 G,或者用了远程层),砍依赖没用,得先看 gcloud run revisions describe 里的启动耗时分布;如果服务要连 VPC 或挂 Cloud SQL,连接建立可能又占一两秒,这部分不在代码里;还有一次我砍依赖砍过头,把一个运行时才 require 的模块干掉了,热路径不报错,只有某条定时任务炸,日志里才看见。所以砍完一定把每条分支都跑一遍。

现在这个客户白天还行,晚上回到最小实例 0,第一个访问的人还是得等一秒多。你们在这种低流量小客户的项目里,是让客户为最小实例付费,还是干脆把冷启动写进页面的加载文案里糊弄过去?

0
赞同度 100% · 0 条评论 · 4 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。