做过 Serverless 的同学应该都有过这种体验白天一切正常半夜一条监控告警把你叫醒某个接口的 P99 延迟从平时的 30ms 突然飙到了 4 秒点开链路一看平台给你弹出一条函数实例冷启动耗时 3.8s的记录。于是你意识到无服务器架构的延迟账本里,冷启动才是那笔最不可控的支出。这几年我在几个云平台上线过几十个函数也帮团队处理过不少超时告警和流量洪峰打崩函数的线上事故绝大部分问题的根源都能追溯到冷启动延迟。这篇文章不打算讲太多概念我想把我自己实际做冷启动延迟测试的完整思路、脚本、踩坑记录和优化对比数据整理出来给正在排查无服务器架构性能问题、或者准备在项目里引入函数计算的朋友一份可以直接参考的实操手册。你不需要理解太多底层原理也能照做但我会在关键步骤后面解释为什么要这么做因为只有理解了延迟从哪来测试数据才有意义。1. 冷启动延迟到底贵在哪儿——先把底层延迟账本摊开算1.1 一次冷启动背后发生了什么很多人以为冷启动就是平台把函数跑起来但真实过程比你想的复杂得多。一次完整的冷启动大概要经过下面这些环节请求到达函数计算平台的路由层平台发现当前没有可用的函数实例平台从资源池里分配一个沙箱/容器初始化运行环境把函数的代码包从对象存储拉取下来解压到本地启动运行时进程比如 Node.js、Python 解释器、Java 虚拟机会在这个阶段被拉起来执行模块加载和初始化代码也就是你在函数文件顶层写的那些连接数据库、加载配置、实例化 SDK 的逻辑最后才进入你真正的 handler 逻辑开始处理业务你可以把它想象成一家没有预留食材的餐厅。热启动就像厨房里已经备好了洗好切好的菜客人下单直接起锅冷启动则是客人下单之后厨师才从仓库拿菜、洗菜、切菜、烧锅全部就绪才轮到炒菜。你说这中间要多花多少时间。1.2 延迟账单哪些环节占了时间大头不同平台、不同运行时的冷启动时间构成差异非常大但大体上可以分成四块延迟环节典型耗时主要决定因素沙箱/容器分配几十毫秒到几百毫秒平台资源池的空闲程度、账号并发水位代码包下载与解压几十毫秒到数秒部署包体积、依赖数量、存储类型运行时进程启动几十毫秒到数秒语言运行时差异Java 明显最重用户初始化代码执行几毫秒到数秒你在代码顶层做了什么操作这里有个很多人忽略的点代码包下载解压这个环节往往比你想象的更贵。我曾经把一个 Java 函数从 80MB 的依赖包瘦身到 25MB冷启动时间直接降了一半以上。所以如果你测出来冷启动特别慢第一步就该去看部署包大小。1.3 为什么冷启动延迟测试不能只看平均值这是一个老生常谈但你一定要刻在脑子里的原则冷启动在请求总量里占比很低平均值会把它稀释得毫无意义。我举个具体例子。假设你发了 1000 次请求990 次热启动单次耗时 28ms剩下 10 次是冷启动单次耗时 5 秒。算一下平均值(990 × 0.028 10 × 5) ÷ 1000 ≈ 0.078 秒也就是说平均值只有 78ms。如果只看均值你会觉得系统性能好得很根本没毛病。但实际有 1% 的用户真实体感是等了整整 5 秒。尾延迟 P99 才是你要盯的那个数字。无服务器架构的冷启动延迟测试本质上就是一次针对尾部延迟的专项体检如果没有围绕 P95、P99 来设计测试那测出来的数据基本是自欺欺人。2. 测试目标和环境设计——没有对照组和标尺的测试都是自嗨2.1 先回答四个问题测哪个环节、用什么流量模型、在什么环境、测哪些指标动手测之前我建议你先明确四件事。不然很容易出现数据测出来一堆但根本不知道说明什么问题的情况。第一测哪个环节。你是想测端到端延迟也就是从发起 HTTPS 请求到收到响应的完整链路包括 API 网关转发、函数调度、执行、返回还是只想测函数运行时本身的冷启动耗时这两个数据用途不一样。端到端数据用来评估用户真实体感运行时数据用来定位优化方向。第二用什么流量模型。是单发一次请求看冷启动还是先冷后热连续对比还是模拟突发流量做并发冷启动压测我建议三类都要测因为线上事故往往发生在第三种场景。第三在什么环境测。冷启动延迟非常受平台整体水位影响生产环境和测试环境的数据可能差出一倍以上。最好在专用测试账号、独立区域的测试环境里做并且记录下环境指纹避免跑了两轮数据来自完全不同的配置再拿来对比。第四测哪些指标。核心是冷启动耗时、P50/P95/P99 延迟、请求成功率、实例扩容数量和速率。有条件的话把平台日志里的 InitializeDuration、Duration 也拿下来方便拆分。2.2 环境隔离与基线控制测试环境设计最容易被忽略的是混杂。如果测试函数和其他业务函数混在同一个资源池别的高负载函数突然扩容可能把平台的资源抢走你的冷启动延迟会莫名其妙变高而且复现不出来。所以做冷启动测试之前我通常会对环境做几项固定处理使用独立测试账号或者独立区域不跟生产环境混用测试函数尽量放在同一个平台账号下专门为测试开通的命名空间里压测机和服务端之间的网络路径保持固定最好在同一个地域发起请求避免公网抖动干扰关闭压测机上的代理、DNS 缓存、HTTP 连接复用机制保证每次请求都是干净的新连接把所有环境参数记录下来函数内存、运行时版本、部署包哈希、依赖层版本、VPC 配置、网关超时时间有些朋友会图省事直接用浏览器 F5 刷一刷当测试我不建议这么做。浏览器有连接复用、缓存、同源并发限制测出来的数据基本不具备参考价值。后面我会给你一套脚本直接命令行跑。2.3 指标口径对不齐后面全白做定义指标时最容易打架的是冷启动到底从哪一秒开始算。我的建议是分开两个口径端到端冷启动延迟从客户端发起请求到收到完整响应的总时长前提是你确认该请求命中的是冷实例运行时冷启动耗时从运行时进程开始加载模块到 handler 真正被调用的时间加上平台日志里的 initDuration确认命中的是不是冷实例很关键。我常用的办法是看响应头或者日志里的实例 ID。同一个实例 ID 第一次出现基本就是冷启动如果实例 ID 是之前出现过的那只是热调用。你还可以在函数代码里打印一条带实例 ID 的日志配合压测脚本一起分析。3. 一套可以直接抄的冷启动延迟测试方案3.1 工具选型压测工具与观测工具怎么搭配工具不需要很复杂但要根据你的目的选对。我只拿自己用顺手的几款说单发请求、冷热对比curl 就够了配合 time 指令或者 Python 的 perf_counter固定 QPS 压测考虑 wrk、hey它们轻量适合短时间压测复杂场景编排我用 Python 脚本 requests 库灵活控制并发、间隔、请求头还能把数据落盘观测端平台自带日志 链路追踪另外在函数里埋点打印关键时间戳两边的数据互相印证这里有一个经验压测工具负责制造负载但延迟的分段明细一定要靠平台日志和服务端埋点不能只信客户端测出来的总时长。客户端测到的总时长里包含网络 RTT 和网关排队时间这些不是冷启动本身混在一起会让你误判。3.2 最简单的一套冷热交替测试脚本下面这个 Python 脚本是我平时最常用的探针脚本用来快速感知冷启动是否存在、以及冷热差距有多大。它不是压测工具而是定位工具。import time import statistics import requests URL https://your-function-endpoint.example.com/hello def timed_request(): start time.perf_counter() try: resp requests.get(URL, timeout30) elapsed_ms (time.perf_counter() - start) * 1000 return elapsed_ms, resp.status_code, resp.headers.get(x-fc-request-id, ) except Exception as exc: elapsed_ms (time.perf_counter() - start) * 1000 return elapsed_ms, 0, str(exc) # 第一轮制造并观察冷启动 elapsed, code, rid timed_request() print(ffirst-cold: {elapsed:.1f} ms, status{code}, req_id{rid}) # 第二轮立即请求命中热实例 elapsed, code, rid timed_request() print(fsecond-hot: {elapsed:.1f} ms, status{code}, req_id{rid}) # 连续10次热请求统计P50/P95 hot_times [] for i in range(10): elapsed, code, rid timed_request() hot_times.append(elapsed) hot_times.sort() p50 hot_times[len(hot_times) // 2] p95 hot_times[int(len(hot_times) * 0.95) - 1] print(fhot p50{p50:.1f} ms, p95{p95:.1f} ms)跑完这个脚本你能立刻得到三个信息冷启动大概是多少、热启动大概是多少、两者差了几个数量级。如果冷热差距在 3 到 5 倍以内说明系统还算健康如果冷启动比热启动慢了 50 倍以上恭喜你基本上确认了延迟大头在冷启动。3.3 如何确保每次都能测到真正的冷启动很多人测着测着发现怎么全是热启动根本测不到冷启动。原因是平台有实例存活机制你上一个请求刚结束实例还在存活窗口里下一个请求过来直接被复用了。我总结了三种能稳定制造冷启动的方法按可靠性排序等待实例完全回收后再测。大多数平台的空闲实例会在几分钟到十几分钟后被回收你可以通过监控实例数曲线来确认回收完成再发首个请求通过平台的管理 API 或 CLI 主动释放/更新函数实例。比如更新函数配置或代码平台一般会触发旧实例重建但注意这样测出来的是更新后的首启和纯冷启动略有区别直接修改测试函数的资源标签或环境变量触发滚动重建再把测试请求打到新版本上第一种方法最干净但等待时间长。如果你要连续多轮采集冷启动样本我建议写一个定时任务发一个请求然后睡眠 20 分钟等实例回收再发下一个请求收集 20 个样本。虽然慢但是数据极干净。3.4 在函数内部埋点把运行时耗时单独拆出来端到端数据拿到之后我还习惯在函数代码里加一个最朴素的埋点用来算模块加载到 handler 被调用的时长。以 Python 为例import time _init_start time.perf_counter() # 这里是各种模块加载、连接池、配置初始化 # ... def handler(event, context): init_cost_ms (time.perf_counter() - _init_start) * 1000 print(fcontainer_init_to_handler{init_cost_ms:.1f} ms) return {statusCode: 200, body: ok}注意我特意用 perf_counter 而不是 time.time()因为 perf_counter 是单调时钟不受系统时间调整影响用来测间隔更可靠。这个日志会和你压测端到端的时间戳一起进入链路追踪在后端把两者对照你就能清楚地看到网络和网关花了多少、运行时初始化花了多少、业务执行又花了多少。4. 实测数据拆解谁在悄悄吃掉你的延迟预算4.1 内存配置的影响不是线性增长但有明显拐点函数计算平台通常以内存大小作为分配 CPU 和资源的依据。我在某个平台用同一个空函数只改内存配置测出来的冷启动中位数如下内存配置冷启动耗时中位数热启动耗时中位数128MB1320ms25ms256MB850ms24ms512MB620ms23ms1024MB480ms22ms2048MB390ms21ms可以看到从 128MB 升到 512MB 时冷启动时间下降非常明显但到了 1GB 以上收益就变得很平缓。这说明内存档位只要过了一个基本阈值平台分配的基础资源就够用了继续加内存对初始化速度帮助有限。所以如果你的函数冷启动偏高我建议先把内存从 128MB 提到 512MB 试试这个动作基本不花什么改造成本却往往能拿到 40% 以上的收益。4.2 运行时差异Java 和 .NET 的初始化开销真的不小很多无服务器项目选语言时只看写起来顺不顺手到压测时才后悔。我同样用一个空函数不同运行时在同一平台、同一内存档位下测过运行时冷启动耗时中位数备注Go280ms编译后的二进制启动极快Python520ms依赖少的场景表现不错Node.js460ms表现稳定依赖多时会涨Java (GraalVM)700ms比普通 JVM 好很多但仍有成本Java (普通 JVM)2100msJVM 启动 类加载开销大.NET1300ms运行时初始化较重这里我想强调两点。第一不要只看语言本身还要看你的依赖生态。一个带着 40MB 第三方包的 Python 函数冷启动可能比轻量 Java 函数还慢。第二Java 函数如果一定要用优先考虑 GraalVM 原生镜像或者平台提供的快照启动方案否则每一个新实例的诞生都要付一次 JVM 冷启动的罚款。4.3 部署包体积和初始化代码容易被忽视的大头我做过一个对照实验。同一个 Node.js 函数代码逻辑完全一样只在部署包里多塞了一个 30MB 的纯静态模块。结果冷启动从 420ms 涨到了 1650ms而热启动几乎没变。原因很直接冷启动要拉包、解压、加载模块包越大这个环节越慢热启动因为是复用已加载好的进程所以感知不到。另外初始化代码的质量也很关键。我见过一个函数在全局作用域里初始化了一个 Redis 客户端、一个消息队列生产者、还加载了一大堆配置 SDK冷启动 3 秒多但其实业务只是返回一句hello。把这些重初始化改成惰性加载——也就是首次真正用到时才创建连接冷启动往往能降到原来的三分之一。4.4 VPC 和突发并发线上事故的两大主谋如果你的函数要访问私有数据库或内部服务一般要配置 VPC。但 VPC 模式的函数在新实例创建时经常需要挂载网络接口这个动作挂得慢的话会让冷启动显著增加。我测试的某个平台无 VPC 函数冷启动约 500ms接入 VPC 后变成 900ms 到 1200ms。优化办法通常是把网络接口的创建改成异步模式或者让平台复用热网卡但这需要你按平台能力去配置。突发并发则是最考验冷启动的场景。我模拟过一次 50 并发打到同一个函数上平台需要同时扩张 50 个新实例这时单个实例的冷启动时间从平时的 600ms 被拉到了接近 1.8 秒。原因也不难理解短时间新增实例太多平台的调度、拉包、初始化都在抢资源互相拖累。所以做无服务器容量评估时单独测一个冷启动的数据是不够的一定要做并发冷启动测试看平台在突发流量下能撑到什么水位。5. 降低冷启动延迟的优化手段与优化后的再验证5.1 预留实例最直接但最贵的选择如果你已经确认冷启动对业务有实质影响最简单的方案是购买预留实例/预置并发。用完这些配额后请求直接命中热实例延迟瞬间回到几十毫秒。代价是费用会明显上升而且预留多了会造成资源浪费预留少了在流量高峰依然会触发冷启动。我的建议是给核心链路、延迟敏感的函数做 20% 到 40% 的预留剩下的流量还是走弹性伸缩。预留量可以结合你过去一个月的实际并发峰值以及 P99 目标延迟来反推。先跑一段时间再根据成本报表调整比例。5.2 代码层瘦身代价最小、收益最稳的优化这是我最推荐的优化方向因为它既减小冷启动又不增加费用还提升安全性。具体操作思路如下清理掉部署包里没有被实际引用的依赖必要时用工具扫描依赖树把重型 SDK 的导入从顶层搬到 handler 内的分支里牺牲少量可读性换取启动速度使用平台提供的依赖层能力把不常变的依赖放在层里让平台缓存层内容减少请求时的拉包量对于自定义运行时考虑裁剪基础镜像去掉不必要的系统库数据库连接、消息客户端等重对象在连接池里跨请求复用不要在每次调用时重建注意一个细节模块懒加载不是所有场景都合适。如果你的函数会被频繁调用懒加载收益很小反而可能因为初始化逻辑在每次调用时都要判断一遍而增加一点点执行时间。适合懒加载的是那些绝大多数请求用不到的依赖比如冷备用的告警上报 SDK、管理后台才触发的大计算库等。5.3 快照启动和自定义运行时平台级黑科技现在主流平台基本都提供了快照启动或者类似的加速机制原理是提前把一个函数实例初始化好把它的内存状态打成快照后面创建新实例时直接恢复快照省掉代码加载和初始化阶段。我自己的实测里快照启动能把一个 2 秒的 Java 函数冷启动压到 400ms 以内效果非常明显。用快照启动要特别注意两点第一快照里如果包含随机数种子、加密 key 生成器等状态恢复出来的多个实例可能会有状态一致性问题需要做专门的调用前重置第二初始化代码里如果有往外部系统写数据、发消息的副作用打快照时会被执行一次恢复后又会执行一次存在重复计数的风险。所以你需要在代码里做一个当前是否为快照恢复的标记处理。另外如果你的平台支持自定义镜像可以通过预装依赖来减少拉包和解压的时间。但镜像也不是越大越好镜像文件超过一定体积之后平台拉取镜像的开销反而会抵消掉依赖预装的收益。理想情况是用一个刚好包含所需系统库和运行时的精简镜像。5.4 优化前后的回归测试同一套脚本、同一套环境、同一个时间段优化做完之后最忌讳的就是换了个环境、换个脚本再测一轮然后对比数字。冷启动延迟受平台当前水位影响很大不同时间段的测试数据波动可能比优化收益还大。我的回归测试方法如下把测试环境固定下来记录函数的内存、运行时、部署包哈希、关联层版本在某个相对空闲的时间窗口先跑一轮完整脚本作为优化前基线应用优化后在同一个时间窗口、用同一套脚本再跑一轮每轮至少采集 20 到 30 个冷启动样本取中位数和 P95不要拿单次数据说话最后把关键指标摆在一起对比比如冷启动中位数、P95 端到端延迟、突发并发下实例创建速率这里我放一个我实际优化某个 Python 函数前后的对比摘要给你做个格式参考指标优化前优化后降幅部署包体积52MB18MB-65%冷启动中位数1680ms620ms-63%冷启动 P952240ms890ms-60%50 并发冷启动中位数4100ms1450ms-65%这个函数的优化动作很简单把两个几乎没用到的 SDK 从顶层导入改成了函数内惰性加载同时把部署包里的静态资源挪到了一个独立的对象存储服务里。整个过程大约花了一个下午收益却非常可观。6. 我在实测中踩过的坑和给你留的几点建议6.1 最容易让数据失效的五个坑第一测试过程中平台可能已经养热了实例。我试过等了一个小时再发请求结果因为监控探针或者健康检查一直在打这个函数实例根本没被回收测出来的冷启动实际是热的。排查办法是看实例数曲线确认降到 0 之后才开始。第二网关和客户端的超时设置太短会把冷启动请求直接掐断。我见过一个人把 API 网关超时设成了 1 秒冷启动需要 2 秒结果所有冷启动都被网关 504 截断他还以为是平台在报错。测冷启动之前一定要把链路中所有超时阈值放宽到预期冷启动时间的 3 倍以上。第三请求客户端使用了长连接复用。Python 的 requests 库默认是每个请求新连接但如果你用连接池第二次请求会复用同一个 TCP 连接省掉了 TLS 握手和连接建立的耗时这会掩盖真实冷启动成本。测试脚本里要明确禁用连接复用或者至少在同一批测试里保持一致的配置。第四只测一轮就下结论。同一平台的冷启动本身就是一个分布很宽的随机变量单次测出来 600ms下一次可能就变成 1.2 秒。必须多轮采样看中位数和 P95否则你基于单次数据做的优化决策很可能是在追噪音。第五并发冷启动测试撞上平台限流。有些平台对新实例的创建速度有上限突发并发过大时会直接返回 429 或者 503。这不代表你的函数有问题而是平台的资源调度策略导致的。遇到这种情况要把错误响应单独分类统计不能一股脑算进延迟里。6.2 给新手的实操建议如果你刚接触无服务器架构冷启动测试我个人建议按这个顺序起步先用最简单的冷热交替脚本确认冷启动是否存在然后打开平台日志看看 initDuration 和 duration 分别是多少判断瓶颈在初始化阶段还是在业务阶段接着试着把内存从 128MB 调到 512MB重新跑一轮一般很快就能看到变化最后再考虑代码瘦身和预留实例这些更高阶的手段。我还会建议你把测试脚本和结果文件一起放进项目的仓库里旁边写清楚环境指纹、测试时间、云平台版本信息。因为这类性能问题具有很强的时效性平台底层一升级旧数据就基本失效了。有留存的数据和脚本下次再遇到延迟告警你能很快复用这套流程去排查而不是重新发明轮子。6.3 最后再分享一个小技巧如果你有定期巡检无服务器应用的习惯我建议把冷启动探针集成到已有的定时健康检查里比如每 5 分钟做一次带实例 ID 记录的热探测每天在低峰期主动制造一次纯冷启动请求并把耗时指标上报到监控系统。这样你看到的不是某一次测试的快照而是一条连续的冷启动延迟趋势线。当平台升级、依赖调整或者基础镜像变化时这条趋势线会在第一时间告诉你冷启动延迟有没有悄悄变差。我现在团队里的无服务器服务监控看板上就长期挂着两个数字一个是热启动 P95一个是每日冷启动中位数。只要冷启动中位数出现连续两天的上升我会主动去查部署包大小和平台配置变更而不是等到用户投诉了才被动响应。
阅读完成 · 觉得有帮助?