ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AWS Lambda云压测实战:并发、冷启动与成本拐点分析

AWS Lambda云压测实战:并发、冷启动与成本拐点分析 做性能验证这件事最怕的不是测出问题而是不知道该测什么。上个月我接手了一轮 AWS Lambda 云压测任务目标很明确给一个处理订单事件的服务函数做性能验证搞清楚它在突发流量下到底能扛住多大的并发以及成本拐点在哪里。压测做完之后我最大的感慨是Lambda 的压测跟传统服务器压测完全不是一回事指标口径、限流行为、冷启动影响每一项都值得单独拆开讲。这篇就记录我这次压测的全过程从方案细化到动手执行再到结果解读和排坑过程里踩过的坑也一并写出来给后面要碰 Lambda 压测的人做个参考。文章不会只停在“怎么跑起来”的层面更想讲清楚背后的判断逻辑哪些参数要盯哪些数据要留哪些问题可以忽略。1. 为什么要把 Lambda 拉出来压测1.1 从一次线上抖动说起这次压测的起因不算复杂某天晚高峰业务侧反馈订单状态回调出现了明显延迟用户支付的订单过了十几分钟才更新状态。我看了一眼监控面板发现负责处理订单事件的 Lambda 函数并发数被冲到近千日志里开始出现Task timed out下游 MySQL 的 CPU 使用率也一路上涨。当时第一反应是把函数内存调大、超时时间放宽但改完之后我心里其实没底——因为我根本不知道这个函数的性能上限到底在哪。传统服务器压测很简单起一台压测机对着某台 EC2 的 IP 拼命发请求看 CPU、内存、带宽谁先到瓶颈。Lambda 不一样它是事件驱动的无服务器计算函数实例由平台自动调度你不需要关心底层虚拟机但你必须关心并发数、冷启动、执行时长、限流策略还有最重要的成本。对于这种形态的服务性能验证不能沿用老办法必须重新设计一套压测方案。这次压测的目标函数是一个典型的中间处理函数接收 API Gateway 传过来的订单事件做简单的数据清洗然后调用下游订单服务的 HTTP 接口最后把处理结果写进 DynamoDB。代码本身不复杂但整个调用链路有三处外部依赖下游 HTTP 接口响应质量不稳定DynamoDB 写入有吞吐限制Lambda 平台本身还有账户级并发配额。任何一个环节出问题都会直接反映到压测结果里。所以这次压测我不只是测函数本身而是把整个链路当成一个系统来看。1.2 云压测到底验什么稳定性、扩展性、成本拐点在做压测方案之前我先给自己列了三个问题第一函数在正常压力和峰值压力下响应时间是否能满足业务要求第二并发从低到高的过程中函数是否会出现雪崩式的性能劣化第三为了扛住峰值并发我需要付出多少成本代价。这三个问题对应到性能验证上就是稳定性、扩展性和成本拐点。稳定性好理解就是压测过程中错误率不能异常升高P95 和 P99 延迟不能突破业务阈值。扩展性则是看 Lambda 在并发增长时能不能平滑承接流量曲线是线性的还是会出现断崖。成本拐点这个很多人会忽略但恰恰是云压测最有价值的部分Lambda 是按执行次数和运行时长计费的当延迟从 100ms 涨到 500ms同样的 QPS 下计费时间就会翻五倍再加上错误重试带来的额外调用次数成本增长往往比流量增长更快。我给自己定的性能基线是这样正常压力下平均延迟低于 200msP95 低于 500ms峰值压力下错误率低于 1%P99 低于 1500ms。如果超过这个线就算函数功能正常也不能通过性能验证。这些数字不是拍脑袋定的而是从业务侧的可用性目标反推的订单回调的最终一致性容忍时间是 5 分钟核心链路单次处理必须在 2 秒内完成否则积压会越来越严重。有了目标和检验标准剩下的事情就简单了——选工具、定模型、跑压测、看数据、写结论。下面我会把每一步的取舍都展开讲尤其是那些容易被忽略的细节。2. 压测方案选型工具与模型怎么定2.1 工具对比我为什么选了 serverless-artillery云压测工具有很多主流的有 Locust、Gatling、k6以及 AWS 官方解决方案里常见的 Distributed Load Testing。这些工具本身都没问题但它们在压测 Lambda 时都存在一个共同的尴尬压测机在传统的固定资源上运行而压测目标却是自动伸缩的无服务器架构。如果压测机本身的并发能力不够你还没把 Lambda 压垮压测机先把自己跑死了。我用过一个比较省心的方案基于 Serverless Framework 的serverless-artillery插件。它的思路是把 Artillery 压测引擎跑在 Lambda 上相当于用“无服务器”来压“无服务器”。压测引擎会按需启动多个 Lambda 实例每个实例负责一部分请求流量天然具备水平扩展能力。你不用担心单台压测机的 CPU 不够也不用处理复杂的分布式压测调度只要把压测配置写好一条命令就能跑起来。工具对比其实可以列得很细但我的核心选择标准只有三条压测引擎本身不能成为瓶颈最好能随负载自动扩展配置方式要简单直接最好用 YAML/JSON 描述场景而不是写一堆 Java 代码最终能输出稳定的延迟分布数据方便后续做性能画像。按这个标准筛下来serverless-artillery确实是最贴合 Lambda 压测场景的。Locust 和 k6 我保留做备用方案用于真正需要自定义复杂协议或者需要长连接保持的场景但常规 HTTP 接口压测Artillery 的配置范式已经足够了。2.2 并发模型与冷启动的坑选定工具后第一个要处理的问题是并发模型。Lambda 的并发不是传统意义上的“连接数”而是指同时运行的函数实例数量。理解这一点非常关键因为压测工具里设置的arrivalRate每秒新增请求数和我们关注的并发实例数不是一个概念两者之间存在一个换算关系并发实例数 每秒请求数 × 函数平均执行时间秒举个例子如果函数平均执行时间为 200ms压测工具以每秒 100 个请求的速率发送流量那么稳定状态下需要的并发实例数约为 100 × 0.2 20 个。如果函数平均执行时间因为下游变慢涨到了 1000ms同样每秒 100 个请求并发实例数就会变成 100 个。这意味着压测过程中如果只盯着请求速率而不看函数实际执行时间很容易低估并发压力。除了并发模型“冷启动”是压测时绕不开的另一个话题。所谓冷启动是指 Lambda 首次调用时需要从零创建运行环境包括下载代码、初始化运行时、加载依赖库这个过程会额外消耗几百毫秒甚至更久直接反映在延迟上。在压测场景中如果流量从零突然拉高大量并发实例需要同时冷启动P95 和 P99 会被瞬间拉到一个很难看的位置。所以我的压测计划里专门安排了一个“预热”环节先用较小流量让 Lambda 运行一段时间把一部分实例热起来然后再开始正式加压。如果不是为了专门验证冷启动对系统的影响压测前最好都做一次预热否则你会很难分清延迟劣化到底是代码问题还是单纯的冷启动代价。2.3 指标口径从 P95 到错误率、初始化耗时压测报告里最容易糊弄人的指标就是平均延迟。说实话平均延迟在性能验证里几乎没有参考价值因为 Lambda 的延迟分布有严重的长尾特征大部分请求可能只要 80ms但偶尔几个冷启动或下游慢请求会拉到 3 秒以上。平均值会被极端值平均掉完全掩盖了长尾问题。我在这次压测中主看的指标是这三个P95 延迟百分之九十五的请求都达到的延迟水平代表绝大多数用户的真实体验错误率非 2xx 响应以及函数执行异常所占比例反映系统的可靠性Throttles 次数Lambda 平台限流导致的执行被拒绝次数这个指标在传统压测里没有对应物但恰恰是无服务器架构的核心瓶颈之一。另外在 CloudWatch 指标里有一个容易被忽略的字段叫InitDuration它单独统计了冷启动初始化的时间。如果这个值持续大于 0说明压测过程始终有新的实例在冷启动。通过对比Duration总执行时长和InitDuration可以精确拆分出冷启动对端到端延迟的影响有多大。工具选型和指标口径确认之后接下来就是写压测脚本、跑流程了。这个阶段最容易出细节问题我从头到尾走一遍给你看。3. 压测全流程实录从脚本到报告3.1 准备被测函数与环境压测的第一步是确认被测函数处于一个干净且可复现的状态。我这次被测函数是订单处理的核心逻辑部署在ap-southeast-1区域运行时是 Node.js 20内存配置 1024MB超时设置为 10 秒。在压测开始之前我做了三件事第一把函数日志级别调到最详细并给关键步骤打上了自定义日志第二把下游 HTTP 调用的地址指向压测专用的 Mock 服务避免压测流量污染生产环境第三单独申请了一个专门的函数版本和别名保证压测期间代码不会被其他同事意外更新。这个函数的业务逻辑本身不算复杂我从生产代码里抽了一个精简版本用于压测核心处理流程如下exports.handler async (event) { const startTime Date.now(); // 1. 解析 API Gateway 传入的订单事件 const order JSON.parse(event.body || {}); // 2. 模拟数据清洗业务耗时 await sleep(50); // 3. 调用下游订单服务压测时指向 Mock 地址 const downstreamRes await callDownstream(order.orderId); // 4. 将处理结果写入 DynamoDB await ddbClient.put({ TableName: process.env.ORDER_TABLE, Item: { orderId: order.orderId, status: downstreamRes.status, processedAt: Date.now(), }, }).promise(); return { statusCode: 200, body: JSON.stringify({ orderId: order.orderId, duration: Date.now() - startTime, }), }; };之所以要专门准备一个精简版本是因为压测期间会打出大量日志和请求如果用生产版本跑外部依赖的数据污染和日志噪音会让人崩溃。压测环境的另一个好处是可以在下游接口前面加一个可变的延迟参数方便模拟不同压力下的外部依赖表现。3.2 编写压测脚本附示例serverless-artillery的配置分两部分一个是serverless.yml负责定义压测引擎的部署参数另一个是 Artillery 的场景配置文件负责描述流量模型和请求内容。先看serverless.yml的部分核心配置如下service: order-function-lt plugins: - serverless-artillery - serverless-python-requirements custom: artillery: mode: lambda config: artillery/config.yml functionName: artillery-runner provider: name: aws runtime: python3.12 region: ap-southeast-1 timeout: 300这里的关键是把mode设为lambda这样压测引擎会运行在 Lambda 上而不是本地的 Node 进程。functionName是运行时自动生成的压测引擎函数名不需要手动提前创建。serverless-python-requirements用来处理引擎的 Python 依赖serverless-artillery的底层其实是 Python 封装的 Artillery 运行器。真正定义流量压力的是config.yml文件。我这次采用阶梯加压的方式而不是一上来就打满压力config: target: https://your-api-gateway-url.execute-api.ap-southeast-1.amazonaws.com/prod/order phases: - duration: 60 arrivalRate: 10 rampTo: 50 name: 预热阶段 - duration: 60 arrivalRate: 50 rampTo: 100 name: 稳态加压 - duration: 60 arrivalRate: 100 rampTo: 200 name: 峰值冲击 - duration: 30 arrivalRate: 5 name: 冷却观察 ensure: p95: 800 scenarios: - name: process order event flow: - post: url: /order json: orderId: {{ $randomString() }} amount: 100 channel: mobile说一下phases的设计逻辑。第一个阶段arrivalRate: 10, rampTo: 50意思是这一分钟里每秒新建请求数从 10 逐渐涨到 50用来预热实例并观察曲线走向。第二个阶段从 50 涨到 100第三个阶段从 100 涨到 200每一阶段都留了整整一分钟的稳定期让并发实例数和下游连接池充分达到稳态。如果没有留够观察时间压测反应出来的只是瞬态表现而不是稳态性能。ensure.p95: 800是 Artillery 的断言配置压测结束后如果 P95 延迟超过 800ms命令会以非零状态退出方便接入 CI/CD 流程时直接判断性能是否合格。3.3 执行压测阶梯加压与停顿设计脚本写完之后执行本身反而简单。首先运行serverless deploy把压测引擎部署到指定区域然后执行serverless artifact启动压测。这里有一点特别重要压测引擎和被测函数最好在同一个区域内否则跨区域网络延迟会掺进数据里导致延迟偏高却找不到原因。压测执行过程中我是边压边盯 CloudWatch 的实时指标。重点看四个数据Invocations、Errors、Throttles 和 Duration。阶梯加压到第二个阶段时Invocations 曲线比较平滑每个阶段的变化符合预期但加到第三个阶段时Throttles 指标开始出现非零值这说明当前账户的并发配额已经接近极限了。这里补充一个背景知识Lambda 账户有一个默认的并发配额不同区域默认值不同通常是每账户 1000 并发实际需要到管理控制台确认但这个配额是整个区域所有函数共享的。如果你在同一个区域还跑着其他业务函数它们也会占用这个配额压测时很容易出现“别人的函数把我的配额占满了”的情况。我在压测前专门把被测函数的reservedConcurrency设为一个固定值比如 300确保它至少能使用 300 个并发实例。这样做有两个好处一是避免压测流量把区域总配额耗尽影响其他线上服务二是可以精确控制被测函数同时运行的最大实例数方便观察它在达到自身并发上限后的表现。整个压测执行下来大概五分钟但数据整理花了我将近半个小时。压测引擎输出的原始结果是一堆 JSON 报告我把里面的指标提取出来做成了下面这样的性能画像表阶段目标 RPS实际 QPSP50(ms)P95(ms)P99(ms)错误率Throttles预热10→50432103506200.0%0稳态50→100962404107800.0%0峰值100→20017831069019801.2%37冷却552303905600.0%0看到这个表格你应该能立刻意识到问题在哪里峰值阶段的 P99 冲到 1980ms错误率从 0 跳到了 1.2%Throttles 也出现了 37 次。这正好对应到前面说的并发配额限制当并发实例数达到预留上限后新的请求会直接被 Lambda 平台拒绝表现为 429 或函数重试。3.4 结果记录与性能画像正式压测结束之后我习惯再做一次“稳定性回放”用峰值阶段一半的流量跑三分钟看延迟曲线是否会回落。这一步很关键因为很多系统的问题是能扛一阵但不能持续半流量回放可以有效检验当前函数在长时间高负载下的稳定性。回放结果基本符合预期延迟曲线在第三分钟后趋于平稳P95 稳定在 500ms 左右没有出现逐步爬升的现象。这说明函数本身没有内存泄漏或连接泄漏这类问题峰值阶段的性能劣化主要来自并发配额和下游 Mock 服务的瓶颈而不是代码缺陷。最终我给这次压测下的初步结论是该函数在 100 QPS 以下表现良好P95 低于 500ms可以满足常规业务需求但在 200 QPS 的峰值压力下会出现明显限流和延迟劣化必须通过预留并发或拆分函数的方案来优化。这个结论在后面的结果解读中还会进一步细化。4. 结果解读性能指标背后的真实含义4.1 延迟分布与“尾部延迟”问题压测报告打印出来后最容易让人困惑的是 P50 、 P95 和 P99 之间的差异。我这个场景里P50 是 310msP95 是 690msP99 直接跳到 1980ms。如果你只看 P50会觉得函数性能不错但 P99 暴露了一个严重问题每 100 个请求里就有 1 个请求的延迟接近 2 秒这种长尾对用户体验的伤害极大。我后来用 CloudWatch Logs Insights 跑了一次查询把压测期间的日志按耗时分布拉出来看发现所有超过 1500ms 的请求都有一个共同特征日志里的InitDuration字段不为零。也就是说这些超长请求几乎都是冷启动请求。虽然预热阶段已经让一批实例热起来了但峰值阶段 200 RPS 远远超过了热实例的承载能力平台不得不频繁创建新实例冷启动代价被成倍放大。这个发现非常重要因为它把问题从“代码性能差”引向了“并发扩容速度跟不上流量增长”。解决办法也不是简单的优化代码而是要考虑使用预置并发Provisioned Concurrency提前把一批实例初始化好或者调整函数内存配置以加快执行速度从根上减少冷启动发生的概率。延迟分布问题还揭示了一个压测经验性能验证报告如果只给平均值等于耍流氓。一定要按分位数拆开看最好能把延迟分布直方图一起附上这样后续排查性能问题才有据可查。4.2 并发限制与限流表现前面表格里的 Throttles 字段可能是云压测和传统压测最大的不同。传统服务器高并发时表现是进程数暴涨、CPU 打满但请求还会被处理只是响应变慢Lambda 则是并发一超限请求直接被平台拒绝连进入函数执行的机会都没有。我这次遇到的情况正好演示了两种限流表现。当峰值流量接近预留并发上限时有一部分请求返回 200但执行时长被显著拉长这是资源竞争的表现另一部分请求则直接抛ThrottlingException或 HTTP 429这是配额耗尽的硬限制。所谓性能验证就是要把这两种表现都量化出来延迟劣化到哪个量级拒绝率达到多少然后判断是否在业务可接受范围内。这里还有一个值得新手注意的点错误重试会掩盖一部分限流问题。Lambda 的事件源比如 SQS、API Gateway在收到 429 后可能会自动重试如果你的压测工具统计的是重试后的成功请求错误率会被人为拉低。所以我统计错误率时专门把 CloudWatch 的Throttles和函数执行的Errors分开看Throttles 代表“根本没执行”Errors 代表“执行了但失败了”两者不能混为一谈。4.3 性能验证结论怎么写性能验证的最终产出不是一串数字而是一份能指导决策的结论。我这轮压测结束后写报告时遵循了一个简单的三段式结构现象、归因、建议。现象部分直接用数据说话。写出各阶段的实际 QPS、P95、P99、错误率和 Throttles并把数据和业务阈值做对比。归因部分要分清主次我的报告中明确指出峰值阶段延迟劣化的主要原因是冷启动和区域并发配额次要原因是下游 Mock 服务在并发下的响应变慢函数自身代码没有明显问题。建议部分则按优先级排列短期为该函数设置预留并发保证核心业务在峰值时段有确定性的资源配额中期对函数做内存配置调优用更少的时间完成业务逻辑提高单实例吞吐能力长期评估把步骤 3 的下游 HTTP 调用改为异步事件驱动模式降低对同步响应的依赖。写结论时还有一个原则不要写“系统性能良好”这种模糊表述。要写清楚系统在什么条件下表现良好在什么条件下会恶化恶化到什么程度。只有这样的性能验证结论才能让团队在后续做容量规划时有真正的参考依据。5. 常见问题与排查技巧实录5.1 冷启动拉高 P95 怎么定位压测中最常见的异常就是 P95 大幅爬升但看不出具体原因。我排查冷启动的方法是打开 CloudWatch 控制台找到函数的Duration和InitDuration两个指标前者是总执行时间后者是冷启动初始化时间。正常执行的请求InitDuration为 0冷启动请求则会出现一个明显的初始化耗时通常是几百毫秒到一秒以上。如果你发现压测期间的InitDuration一直大于零去掉冷启动的影响最直接的办法是使用预置并发。这个功能允许你预先指定一批并发实例平台会提前把运行环境初始化好请求到达时直接复用热实例。但要注意预置并发是按运行时长计费的哪怕没有流量也要付费所以一定要根据业务峰值时间来设定数量而不是无脑把配额拉满。另一种侧面排查方法是在代码里记录环境变量的取值。比如在初始化阶段打印进程 ID通过函数日志里 process ID 出现的次数可以估算单实例被复用的频率。如果日志里出现了大量不同的进程 ID说明实例正在频繁冷启动本质上是负载扩容速度跟不上流量增长速度。5.2 内存配置调整对性能的影响Lambda 的内存配置和 CPU 分配是成正比的内存越大分配的 vCPU 越多。很多开发者习惯把所有函数都设置在 128MB 或 1024MB但这是一个很容易被忽视的性能调优点。我专门用同样的压测脚本对被测函数分别跑了 512MB、1024MB、2048MB 三档内存的压测。结果是512MB 时 P95 约为 780ms1024MB 时 P95 约为 410ms2048MB 时 P95 进一步降到 330ms但降幅明显变小。这说明对于这个函数1024MB 已经接近性价比拐点继续加到 2048MB 只能获得零星改善却会把单次执行的计费时间成本拉高不少。内存调优的标准做法是使用 AWS 官方的 Lambda Power Tuning 工具。你只需要提供一个函数 ARN工具会自动依次测试多个内存档位并输出每个档位的执行时间和成本对比。配合压测结果一起看就能找到“性能够用、成本最低”的配置点。需要注意的是内存配置调整之后一定要重新跑一次相同的压测不能只看单次函数调用的耗时变化。内存影响的不只是单次执行速度还会改变单实例在同一时刻能承载的并发请求数进而影响整个函数的并发表现。5.3 压测工具本身成为瓶颈怎么办压测进行到一半我注意到 Artillery 的日志里开始出现Under pressure的警告同时压测引擎函数自身的执行时间从平均 30 秒涨到了一分多钟。这个信号说明压测引擎已经不堪重负自己成了瓶颈。这类问题产生的根源是压测引擎函数在同一时刻处理的请求任务超过了它自身的处理能力也就是压测引擎的“一个实例”内并发任务数太高。解决思路有两个一是减少单个压测引擎函数实例要处理的请求量将 Artillery 的arrivalRate按多个引擎分片二是检查serverless-artillery配置中的超时时间和内存大小给压测引擎本身留足资源。我最后用了一个更轻量级的思路把压测流量拆成两个场景文件分别从两个压测引擎跑一份压测正常峰值流量另一份专门模拟随机突发流量。这样压测引擎的总负载分散了单实例的瓶颈也随之消失。还有一个容易踩的坑压测脚本里如果大量使用随机字符串生成函数会消耗大量 CPU导致压测引擎的执行速度变慢。我后来把请求体里的随机字段提前在本地生成好用payload数组直接读取压测引擎的 CPU 消耗立刻降了三分之一。这种细节在传统压测工具里几乎不用关心但在 serverless 压测引擎上省一点 CPU 都是实打实的性能提升。5.4 账户配额突然后备并发不够怎么办压测的最后阶段我还碰到一个问题区域并发配额不够用。当时我以为预留并发设了 300 就万事大吉结果压测时发现整个区域的总并发水位已经接近账户默认配额导致其他服务的 Lambda 也开始报 Throttles。这个问题的排查思路是到 Lambda 控制台的“配额”页面查看当前区域已分配的并发数。Lambda 这里有两层概念需要分清楚一是账户级区域并发配额这是所有函数共享的总池子二是函数级预留并发这是从池子里单独划给某个函数的份额。如果你不设置预留并发所有函数争抢同一个池子压测时的流量洪峰完全可能把池子耗尽。我当时把被测函数以外的几个非核心函数的预留并发调低同时给被测函数保留了足够的份额然后重新跑了一次峰值压测Throttles 才基本消失。这件事提醒我在云压测开始之前先检查账户配额和现有函数的并发分配情况非常重要否则很容易出现“没被业务打垮却被平台限流拖垮”的场面。从这轮压测里我个人印象最深的一个体会写到最后如果再让我总结一点那就是云压测最难的不是把流量发出去而是看懂结果背后的系统行为。AWS Lambda 的延迟、错误和成本背后牵扯着冷启动、预留并发、区域配额、下游依赖、内存配置任何一个环节都会把数据带偏。只看一个指标做判断几乎一定会得出错误结论。对这轮压测本身我现在会把 P99 延迟和 Throttles 作为未来每次发布的强制验证项凡是核心函数变更都要先跑一轮五分钟的云压测再上生产。压测的过程其实就是提前暴露系统弱点的过程问题暴露得越早修复成本越低。希望这篇实战记录能帮你少走一些弯路下次做 Lambda 性能验证的时候至少知道该从哪几个方向下手。
返回列表