Serverless冷启动:别只盯着预热,恢复路径才是硬骨头
先吐槽一句:把冷启动叫「偶发延迟」的人,大概率没在凌晨两点被 P99 报警叫醒过。真的。函数平时跑得挺好,一到大促、爬虫回源、定时任务撞车,延迟曲线直接竖起来。用户端看到的是什么?转圈。你看到的是什么?一条条 Init Duration。然后群里开始问:加预热了吗?加预置并发了吗?预算批了吗?
问题在于,冷启动不是「启动慢」三个字能盖住的。它更像一条链子,哪一节卡住都算冷。调度器要把请求放到某台机器上,机器要准备执行环境,镜像要拉,运行时得起来,代码要加载,框架要初始化,SDK 要握手,业务代码里可能还藏着一个连数据库的连接池。每一段都短,叠起来就难看了。
Serverless冷启动函数调用链路时序图
我见过一个函数,代码只有 80 行,冷启动 3.8 秒。查到最后,不是语言慢,是启动时初始化了一个 Redis 客户端,还同步拉了配置中心,超时重试两次。你说这算运行时的锅?算平台的锅?都不完全。它就是把「初始化」塞进了不该塞的地方。
冷启动的账,得按链路拆
冷启动的账,得按链路拆
学术界这些年对冷启动的拆法很一致:调度延迟、沙箱/容器/microVM 准备、运行时初始化、函数代码加载、用户初始化。区别只是各平台实现不同。比如 Firecracker 那篇 NSDI 2020 的论文,核心就是把 microVM 做轻:启动大约百毫秒级,单实例内存开销压到几 MB,给多租户隔离和快速启动找了个平衡点。这个方向很关键。没有轻量沙箱,后面谈快照、谈池化都悬。
但工程里最容易忽略的是「用户初始化」。平台把运行时启动从 800ms 压到 200ms,很努力。你一个 SDK 初始化 1.2 秒,直接白给。对吧?所以我看冷启动优化方案,先问一句:初始化代码里有没有网络 IO?有没有重试?有没有加载大模型/大字典/大依赖?有的话,先别怪平台。
还有个常见误区:拿平均冷启动说事。平均值没意义。冷启动是长尾问题,P99 和 P999 才是用户能感知的。你平均 300ms,P99 4 秒,照样炸。尤其是突发流量,第一批请求全是冷的,队列一堆积,尾延迟会互相踩踏。这个在 Azure 那篇大规模 trace 研究里也能看出来:大量函数调用间隔很长,很多函数一天就几次,靠「一直热着」不现实。
快照恢复很香,但不是万能药
这几年最实在的进展,是把「启动」变成「恢复」。AWS Lambda SnapStart 是比较典型的:函数初始化完成后打快照,后续从快照恢复,跳过一部分初始化。行业里阿里云函数计算的弹性快照、Google Cloud Run 的 min instances、Azure Functions Premium 的预暖实例,思路各有差异,但都在往「减少冷路径」上走。说实话,方向没问题。
Firecracker microVM 快照恢复冷启动架构图
可快照不是拍完就完事。恢复之后,网络连接可能是旧的,随机数状态可能是旧的,临时文件、日志句柄、时间戳、唯一性标识,全要重新处理。Java 生态里 SnapStart 对唯一性、连接池、临时文件的限制,踩过坑的团队应该懂。你从快照里醒来,世界已经变了。还拿着旧连接去握手,当然超时。
所以快照优化的正确姿势,不是「拍个快照就上线」,而是把初始化拆成两段:可快照的纯计算部分,和必须在恢复后重建的 IO 部分。配置缓存、模型权重、字典、编译产物,适合进快照。数据库连接、消息队列消费者、临时凭证、随机种子,恢复后重建。这个边界划不清,快照恢复会把冷启动变成「恢复后超时」,更难查。
科研上还有一条路是「重放式启动」:把启动过程录下来,下次直接重放到某个点,减少重复解释和初始化。思路挺巧。但落到生产,还是绕不开依赖治理。平台再快,也架不住函数包 200MB、依赖树里塞了三个版本的 gRPC。
预置并发很香,账也要会算
预置并发、最小实例、常驻池,本质都是花钱买确定性。好使。尤其核心交易链路,P99 要求硬,该买就买。但别把它当默认答案。你给 200 个函数全开最小实例,月底账单会教你做人。更尴尬的是,很多函数的流量曲线根本撑不起常驻。白天热,夜里空;平时空,整点炸。预置少了没用,预置多了浪费。
Serverless冷启动预置并发成本对比曲线图
我比较认的组合拳是:核心链路预置 + 边缘函数快照 + 全局限流和队列保护。核心链路要确定性,预置并发或最小实例兜底。边缘、异步、低频任务,用快照和池化扛。再加一层队列削峰,别让突发流量直接打到冷启动上。听起来不酷,但有效。真的。
还有个细节:预热脚本别瞎写。定时 ping 一下函数,看着热了,实际只热了一个实例。并发一上来,照样冷。预热要打到真实调用路径,最好带业务参数,别只打健康检查。健康检查路径可能根本不初始化数据库连接。你热了个寂寞。
工程上,我会先动这几刀
第一刀,砍依赖。函数包能小就小,分层镜像、按需加载、把重依赖挪到初始化后第一次调用。Webpack/esbuild 打包不是万能,但 Tree Shaking 能救命。第二刀,拆初始化。能并行的并行,能懒加载的懒加载,别在模块顶层同步连数据库。第三刀,加观测。冷启动要按函数、版本、运行时、镜像大小、内存规格拆开看,不然你只知道「冷」,不知道「哪冷」。
第四刀,做灰度。快照、预置、运行时升级,别一次性全量。冷启动优化最怕「实验室里很好,线上一抖」。先拿 5% 流量验证,盯 P99、错误率、恢复后的连接异常。第五刀,接受现实。冷启动不可能归零。只要平台是多租户、按需分配,冷路径就存在。目标是把它压到用户无感,不是把它消灭。
最后说句不太中听的:很多团队不是败在平台能力,是败在初始化代码太野。平台给你 200ms 的沙箱启动,你反手一个 2 秒的同步配置拉取。然后怪 Serverless 不适合生产。这就有点不讲理了。先把初始化路径画出来,标上网络调用和重试,再谈预热和快照。顺序别反。