Cloud Run 上跑小接口,冷启动和并发把我卡了三天
外包接了个小活,客户要一个给自家后台调用的地址查询接口,数据量不大,全国区县约 2900 条,压成 JSON 不到 400KB。我图省事,直接扔到 Cloud Run 上,用的是 Node 20 运行时,最小实例数一开始是 0,内存 256MB,并发用默认的 80。上线第二天客户说早上第一次打开要等三四秒,后面就快了。啊这,典型冷启动。
我先把数据从每次请求读文件改成启动时读一次进内存,容器实例里做模块级缓存:
const fs = require('fs');
const RAW = fs.readFileSync('./districts.json', 'utf8');
const DATA = JSON.parse(RAW);
exports.handler = (req, res) => {
const q = (req.query.q || '').trim();
const hit = DATA.filter(d => d.name.includes(q)).slice(0, 20);
res.set('Cache-Control', 'public, max-age=3600');
res.json({ total: hit.length, items: hit });
};
然后我把最小实例数从 0 调到 1,内存提到 512MB。冷启动从三四秒降到 1 秒出头,但账单上多了一个常驻实例的钱,对这个小单来说勉强能接受。真正让我改主意的是并发:默认 80 并发是给 IO 密集型的,我这个 handler 里 filter 是同步 CPU 活,2900 条虽然不大,但 80 个请求同时压进来,单个实例的 event loop 会排队。我压测时用 200 并发打,p95 从 180ms 涨到 900ms 多。
所以我把并发上限降到 20,同时给 handler 加了一层 60 秒的内存缓存,同一个查询串直接返回:
const cache = new Map();
function get(q) {
if (cache.has(q)) return cache.get(q);
const r = DATA.filter(d => d.name.includes(q)).slice(0, 20);
if (cache.size > 500) cache.clear();
cache.set(q, r);
return r;
}
代价是每个实例多占几 MB 内存,以及缓存命中率完全看客户的查询分布,冷门词该算还是算。另外并发调低意味着同样流量下实例数会变多,冷启动概率反而上升,这是个跷跷板。
失效的地方我也踩了:一是 Cloud Run 的实例不是一直活着,空闲回收后内存缓存全没,别指望它当 Redis 用;二是 readFileSync 放在模块顶层,如果文件路径写错,容器启动直接崩,日志里只看到一行 exit,我找了半天才发现是打包时 districts.json 没进镜像。三是请求里带中文查询串时记得确认客户端有没有做 URL 编码,我有一次用 curl 直接拼中文,服务端拿到的是乱码,查半天以为是字符集问题。
现在这个接口算是稳了,但客户量再涨我肯定得把数据挪到 Firestore 或者干脆静态生成。想问下各位:你们在 Cloud Run 上放这种「小数据 + 同步过滤」的接口时,是直接调低并发保延迟,还是干脆换 Cloud Functions 或者干脆扔 CDN 上做静态 JSON?
0 条评论
还没有评论,写下第一条。