1. 项目概述:从“资源”到“请求”的范式转变
最近和几个做云原生的朋友聊天,发现一个挺有意思的现象:无论是刚接触Serverless的新手,还是已经用它跑生产负载的老手,很多人对“并发度”这个概念的理解,还停留在比较模糊的阶段。大家知道它很重要,是计费和扩缩容的核心,但为什么是它?为什么不是更传统的CPU使用率、内存占用,或者请求的QPS(每秒查询率)?这背后其实是一场从“管理机器”到“管理请求”的深刻范式转变。
简单来说,Serverless计算产品(比如函数计算FaaS)选择并发度作为扩缩容的核心指标,是因为它最直接、最纯粹地反映了“业务请求”这个最终价值单元。在传统服务器或容器时代,我们买的是“资源”(比如2核4G的虚拟机),然后在这台机器上部署应用,应用同时处理多个请求。我们监控的是这台“机器”的资源水位(CPU用了50%),水位高了就扩容——加机器。但在Serverless的世界里,你购买的从来不是一台“机器”,你购买的是“处理请求的能力”。平台向你承诺的是:来一个请求,我就立刻分配一份计算能力(一个函数实例)来处理它;来一万个请求,我就瞬间拉起一万份计算能力。这份“计算能力”的多少,其天然、最准确的度量衡,就是同时正在处理的请求数量,也就是并发度。
这就像你去餐厅吃饭。传统模式是:你租下整个厨房和厨师(虚拟机),不管今天有没有客人,你都得付租金,客人多了厨师忙不过来,你就得再租一个厨房(扩容)。而Serverless模式是:你按“一道菜被烹饪”的次数付费(请求次数),并且餐厅保证,无论同时来了多少客人点菜,它都能立刻派出对应数量的厨师来同时炒菜,你只为这些实际工作的厨师工时付费。这里,“同时有多少道菜正在被烹饪”就是并发度。它直接关联你的业务流量、用户体验(等待时间)和你的成本。理解了这一点,你才能用好Serverless,而不是简单把它当成一个“更便宜的虚拟机”。接下来,我们就深入拆解这背后的设计逻辑、技术实现以及对你实际架构和成本的影响。
2. 核心设计逻辑:为什么是并发度?
要理解并发度为何成为黄金指标,我们需要跳出技术的细枝末节,从Serverless要解决的根本问题、其商业模型和技术实现约束三个层面来看。
2.1 解决的根本问题:极致弹性与精细化成本
Serverless的终极承诺是“免运维”和“按需付费”。其核心价值在于,让开发者彻底摆脱对服务器资源(CPU、内存、磁盘、网络)的容量规划、运维管理和闲置付费。传统扩缩容指标(如CPU利用率)存在几个固有缺陷:
- 滞后性与不精确性:CPU利用率高,可能意味着正在处理请求,也可能只是后台任务。一个CPU跑满的实例,可能只处理了1个计算密集型请求,也可能处理了100个I/O密集型请求。它无法直接告诉你“有多少个独立的用户请求正在被服务”。
- 与用户感知脱节:用户不关心你的CPU用了多少,只关心他的请求是否快速得到响应。如果因为资源竞争导致请求排队,即使CPU利用率不高,用户体验也已经受损。
- 资源混池的复杂性:在高度虚拟化、资源混池的云环境中,精确、实时地度量单个函数实例的CPU利用率本身成本高昂,且易受“邻居噪声”影响。
而并发度完美规避了这些问题。一个并发代表一个正在被处理的请求生命周期。计数并发度在逻辑上非常清晰,它直接回答了“现在有多少个用户在同时使用我的服务?”这个业务问题。平台根据并发度的增长,直接决定需要准备多少个独立的、隔离的执行环境(函数实例),实现了供给与需求在“单位”上的对齐。
2.2 商业模型对齐:为价值付费,而非为资源占用付费
云厂商提供Serverless服务,其成本主要构成是:为你的每一个并发请求所临时分配和占用的计算资源(物理CPU、内存)、运行时环境、网络栈等。这些资源在请求结束后会被快速回收并分配给其他用户。因此,向用户收费的天然基础,就是“你占用了多少份这样的临时资源”以及“占用了多久”。
并发度*持续时间,恰好构成了“资源占用量”的精确度量。例如,一个函数实例处理一个请求耗时100ms,那么它就占用了1个并发资源100ms。如果这个函数配置了1GB内存,那么成本就可以清晰地计算为“1GB-秒”的消耗。如果同时有10个请求,就需要10个实例,总成本就是10倍的“GB-秒”。这种模型使得计费极度精细化,用户只为实际使用的计算量付费,没有任何闲置浪费。相比之下,如果按QPS计费,一个耗时1秒的请求和一个耗时10毫秒的请求价值相同,这显然不公平;如果按CPU利用率计费,则无法区分一个高配置实例处理轻量请求和低配置实例处理重量请求的成本差异。
2.3 技术实现约束:冷启动、池化与调度效率
在技术实现层面,以并发度为扩缩容依据,极大地简化了调度系统的复杂性,并优化了全局资源利用率。
- 冷启动优化:平台需要预分配或快速启动函数实例。以并发度为目标,调度器可以更准确地预测需要准备多少个实例。例如,它可以根据历史并发模式进行预测性扩容,提前准备好一批实例(预热),以应对预期的并发增长,从而减少冷启动对延迟的影响。如果以CPU利用率为目标,调度器很难判断下一个到来的请求会是“重”是“轻”,预分配策略会变得低效。
- 资源池化与管理:云平台将海量CPU、内存资源池化。当一个新的并发请求到来时,调度系统只需要从池中分配一份固定大小的资源块(如1CPU+1GB内存)来承载一个函数实例。这种“一份资源块处理一个请求”的模型,资源隔离简单(每个实例独立),调度决策快(只需判断有无空闲资源块),回收也干脆(请求结束即释放)。管理“并发数”远比动态调整一个个异构实例的CPU配额要简单高效。
- 公平性与多租户隔离:并发度是一个强隔离的指标。每个用户的函数实例独立,一个用户的函数崩溃或陷入死循环,不会通过CPU抢占等方式影响到其他用户的函数实例。以并发度作为容量限制,可以更直接、更公平地实施租户间的资源配额管理(例如,限制单个账户的最大并发度)。
3. 并发度的技术实现与扩缩容流程
理解了“为什么”,我们再来看看“怎么做”。Serverless平台如何以并发度为核心,实现秒级甚至毫秒级的自动扩缩容?这个过程远比简单的“计数-扩容”要精细。
3.1 并发度的定义与计量
首先,我们需要明确在Serverless语境下“并发度”的准确定义。它通常指“在同一时刻,正在执行中的函数实例数量”。注意几个关键点:
- 一个实例一个并发:即使一个函数实例内部通过异步或多线程同时处理多个请求(某些运行时支持),在标准的Serverless模型中,这通常仍被视为一个并发。更先进的平台可能会引入“请求并发”的概念,但基础扩缩容单位仍是实例。
- 生命周期:从平台接收到请求并开始初始化或调用执行环境算起,到函数执行完毕并返回响应结束。
- 计量粒度:通常是账户级别、函数级别,甚至是函数特定版本或别名级别。你可以为每个函数设置最大并发度限制,以防止意外流量导致成本失控。
3.2 基于并发度的扩缩容闭环流程
一个典型的、以并发度为驱动的自动扩缩容系统,其内部运作是一个持续的监控-决策-执行闭环:
- 监控与采集:分布在各个计算节点上的Agent会实时采集每个函数实例的状态(启动、运行中、空闲、销毁)。汇聚层服务持续聚合这些数据,得到全局的实时并发度指标。这个采集频率非常高,可能达到秒级甚至亚秒级。
- 决策与预测:调度器(Controller)接收到实时并发度数据后,将其与预设的目标值(通常是平滑处理后的值)进行比较。决策逻辑不仅仅是简单的“当前并发 > 现有实例数”,它通常包含:
- 反应式扩缩容:基于当前实时并发度进行快速调整。例如,设置一个扩容阈值(如平均实例负载达到80%),一个缩容阈值(如平均负载低于20%)。
- 预测式扩缩容:基于历史流量模式(如每天早高峰)进行时间序列预测,提前准备实例,以消除冷启动影响。这需要机器学习模型分析历史并发度曲线。
- 平滑与防抖动:为了避免因流量瞬时脉冲导致实例数量剧烈震荡(抖动),算法会引入冷却窗口、扩容缩容步长控制、以及基于一段时间内(如1分钟)平均并发度的决策。
- 执行与调度:决策完成后,调度器向底层的资源管理系统(如Kubernetes)发出指令,要求创建新的函数实例(Pod)或销毁空闲实例。创建实例的过程包括:选择合适的工作节点、拉取函数代码镜像、初始化运行时环境、注入配置等。
- 流量路由:新的实例启动并注册到负载均衡器(或API网关)后,新的请求就会被调度到新的实例上。优秀的平台能做到在实例启动过程中,就将等待的请求分配过去,最大化利用资源。
注意:这里存在一个关键优化点——实例复用。一个实例处理完一个请求后,不会立即销毁,而是会进入一个“空闲池”保留一段时间(如几分钟)。如果在此期间有新的请求到来,该实例可以直接复用,避免了冷启动。此时,该实例虽然空闲,但仍会计入“已分配实例”的容量,但不一定计入“活跃并发”。扩缩容决策会更倾向于先利用空闲实例,而不是盲目创建新实例。
3.3 关键参数与配置解析
在实际使用中,你需要关注几个与并发度相关的核心配置,它们直接决定了函数的弹性行为和成本:
| 配置项 | 含义 | 影响与调优建议 |
|---|---|---|
| 单实例并发数 | 单个函数实例允许同时处理的请求数。默认为1。 | 设为 >1:适用于I/O密集型、高吞吐、低延迟要求的场景,能极大提升资源利用率和降低冷启动影响。但需确保函数代码是线程安全或无状态的。 保持为1:适用于CPU密集型、或代码非线程安全的场景,逻辑简单,隔离性好。 |
| 最大并发度 | 该函数允许同时运行的最大实例数(或总并发请求数)。 | 安全阀:必须设置!防止代码异常或流量攻击导致无限扩容,产生天价账单。应根据业务峰值和预算设定。 容量规划:结合函数平均执行时间和预期峰值QPS估算。例如,目标峰值QPS为1000,平均耗时200ms,则所需最大并发度 ≈ 1000 QPS * 0.2s = 200。 |
| 预留实例 | 长期保持一定数量的“热”实例,即使没有请求。 | 消除冷启动:为延迟敏感型业务设置。你需要为预留实例的存活时间付费,即使无请求。 成本与性能的权衡:通常与按量付费实例混合使用,预留部分基线流量,弹性部分应对波峰。 |
| 扩容冷却时间 | 两次扩容操作之间的最小时间间隔。 | 防抖动:避免因流量微小波动导致的频繁扩容。通常设置为30-60秒。在流量快速爬升场景可适当调小。 |
| 缩容冷却时间 | 实例空闲后,到被回收前的最小等待时间。 | 平衡复用与成本:时间太短(如10秒),可能导致请求间隔稍长就触发冷启动;时间太长(如10分钟),则闲置成本高。通常设置在1-5分钟。 |
4. 与其他扩缩容策略的对比分析
为了更深刻理解并发度指标的优势,我们将其与几种常见的传统扩缩容策略进行对比。
4.1 并发度 vs. CPU/内存利用率
这是最直接的对比。我们通过一个表格来看:
| 维度 | 并发度扩缩容 | CPU/内存利用率扩缩容 |
|---|---|---|
| 核心视角 | 业务请求视角 | 资源消耗视角 |
| 度量目标 | 同时服务的请求数 | 计算/内存资源的繁忙比例 |
| 与成本关联 | 直接。并发数*时长=资源占用量。 | 间接。高利用率可能由少数重请求或大量轻请求导致,成本模型复杂。 |
| 弹性速度 | 极快。请求到来即触发扩容决策。 | 较慢。需要等待资源利用率采集、聚合、评估,存在滞后。 |
| 应对流量波峰 | 精准。来多少请求,理论上就准备多少容量。 | 模糊。需要根据经验设定利用率阈值,可能过度供给或供给不足。 |
| 适用场景 | 请求粒度清晰、执行时间相对可预测的短期任务。 | 长运行、背景作业、资源消耗稳定的传统应用。 |
实操心得:在Serverless架构中,试图用CPU利用率来扩缩容,就像用油箱的油表指针变化速度来决定需要多少辆车来运客,而不是直接看车站里有多少乘客在等待。前者复杂、滞后,后者直观、高效。
4.2 并发度 vs. QPS(每秒查询数)
QPS是另一个常见的流量指标,但它更适合衡量吞吐量,而非实时负载。
| 维度 | 并发度扩缩容 | QPS扩缩容 |
|---|---|---|
| 反映状态 | 当前瞬时负载。有多少活正在干。 | 一段时间内的平均吞吐。活干得有多快。 |
| 与资源关系 | 线性相关。假设每个请求处理时间恒定,并发度 ≈ QPS * 平均延迟。 | 非线性相关。高QPS可能由高并发实现,也可能由低延迟实现。 |
| 应对突发流量 | 敏感。一个瞬时的大流量脉冲会立刻推高并发度,触发扩容。 | 迟钝。QPS是平均值,脉冲可能被平滑掉,导致扩容不及时。 |
| 计费公平性 | 公平。为每个请求的实际执行时间付费。 | 不公平。无法区分处理耗时1秒和10毫秒的请求,成本核算粗糙。 |
举例说明:假设一个函数平均处理一个请求需要100ms。当QPS为10时,平均并发度是1(10 * 0.1)。如果突然涌入50个请求,QPS的瞬时值会飙升,但基于1秒窗口的平均QPS可能变化没那么剧烈。而并发度会瞬间从1跳到50,系统立刻感知到需要49个新实例。显然,并发度指标对突发流量的响应更直接。
4.3 并发度 vs. 消息队列深度
对于由事件源(如消息队列)触发的函数,队列中积压的消息数也是一个潜在的扩缩容指标。但两者有本质区别:
- 队列深度:代表未开始的工作量。它是一个待办事项列表的长度。
- 并发度:代表正在并行执行的工作量。它是正在处理这些待办事项的“工人”数量。
平台根据并发度来调配“工人”(实例)数量。更智能的调度器会结合队列深度和当前并发度来决策:如果队列深度持续增加而并发度已达上限,则可能需要提高最大并发度限制或加快扩容速度。但最根本的、决定单个“工人”生命周期的,仍是它是否正在处理任务(即并发度)。
5. 对开发者架构与成本的影响
采用并发度作为核心指标,不仅仅是平台侧的技术选择,它深刻地影响了我们设计和优化Serverless应用的方式。
5.1 架构设计启示
- 函数职责单一化与粒度设计:既然扩缩容单位是函数实例,那么一个函数应该只做一件事。避免设计“巨函数”,因为它会成为一个不可分割的扩缩单元。将复杂流程拆分为多个函数,每个函数可以独立根据其自身的并发模式进行伸缩,资源利用更合理。
- 追求无状态与幂等性:函数实例随时可能被创建和销毁。任何会话状态、内存中的缓存都不应依赖。状态必须存储到外部服务(如数据库、Redis)。幂等性设计能确保重试机制的安全运行。
- 拥抱异步与事件驱动:Serverless天然适合事件驱动架构。将耗时操作异步化,函数快速响应事件后即结束,能快速释放并发度,降低成本,并提高系统整体吞吐量。例如,上传文件后触发一个函数生成缩略图,而不是在API响应中同步处理。
- 冷启动优化成为必选项:由于实例随并发请求创建,冷启动延迟是影响用户体验的关键。需要通过预留实例、缩小部署包体积(剔除不必要的依赖)、使用更轻量运行时、以及Provisioned Concurrency(预置并发)等技术来 mitigating 冷启动影响。
5.2 成本优化实战
理解并发度计费模型后,我们可以有针对性地进行成本优化:
- 优化函数执行时间:这是最直接的优化手段。因为费用 = (配置内存) * (执行时间)。减少代码中的低效循环、优化数据库查询、使用更高效的序列化库,都能线性降低费用。一个黄金法则:将执行时间从1秒优化到100毫秒,成本直接降低90%。
- 合理设置内存大小:大多数Serverless平台,CPU性能与配置的内存大小成正比。费用按“GB-秒”计算。你需要找到一个平衡点:增加内存可能提升CPU性能从而缩短执行时间,但内存单价也高了。通常需要通过压力测试,绘制“内存大小-执行时间-单次调用成本”曲线,找到成本最低的“甜点”内存配置。
- 利用单实例多并发:对于I/O密集型函数(如大量网络请求、数据库查询),将“单实例并发数”从1提高到10或50,可以让一个实例同时处理多个请求。这样,在相同的总请求吞吐下,需要的活跃实例数(并发度)大大减少,既降低了冷启动概率,又因为实例复用率提高,潜在降低了成本(减少了实例创建/销毁的开销)。
- 设置最大并发度限额:这是成本的“安全绳”。必须根据业务可接受的峰值和预算,为每个函数设置一个最大并发度上限。同时,利用云平台的告警功能,当并发度持续接近上限时发出警报,以便人工介入或触发自动化预案。
- 流量整形与异步化:对于非实时性的业务,可以通过消息队列(如Kafka, SQS)对流量进行缓冲和整形,让函数以相对平稳的并发度从队列中拉取消息处理,避免突发流量导致并发度激增和成本飙升。
6. 常见问题与排查技巧实录
在实际开发和运维中,围绕并发度会遇到各种典型问题。这里记录一些实战中踩过的坑和解决思路。
6.1 问题一:函数响应变慢,怀疑达到并发上限
现象:API延迟增高,监控显示函数被频繁调用。排查步骤:
- 查看并发度监控:首先检查该函数的“并发执行数”图表。如果图表曲线出现平台状的“削峰”,即并发数达到一个上限后不再增长,而请求数仍在增加,则基本可断定触发了最大并发度限制。
- 检查限流错误:同时查看函数调用日志或平台指标,是否有“限流”(Throttling)或“429 Too Many Requests”错误。
- 分析根本原因:
- 配置问题:检查函数配置的最大并发度是否设置过低。
- 下游瓶颈:函数本身可能没问题,但它依赖的下游服务(如数据库、第三方API)达到连接数或QPS上限,导致函数处理变慢,单个请求执行时间拉长。在总QPS不变的情况下,
并发度 = QPS * 平均延迟,延迟增加直接导致并发度堆积,更容易触顶。此时需要排查下游依赖。 - 函数性能退化:代码变更引入了性能问题,同样导致延迟增加,进而推高并发度。
技巧:在监控面板上,将“调用次数”(QPS)和“并发执行数”两个曲线放在一起看。理想情况下,它们应该形状相似。如果并发度曲线持续高于QPS曲线(考虑延迟),或先于QPS达到平台,就是瓶颈的信号。
6.2 问题二:成本意外飙升
现象:月度账单显著高于预期。排查步骤:
- 定位高消耗函数:使用云平台提供的成本分析工具,按函数维度分解费用。找到“罪魁祸首”。
- 分析费用构成:费用 = 调用次数 * 平均每次调用的资源消耗(GB-秒)。因此,从两个方向排查:
- 调用次数激增:是否被恶意攻击?是否有循环调用BUG?事件源配置是否正确?检查该函数的调用来源和日志。
- 单次调用资源消耗增加:查看该函数的“平均执行时间”和“平均内存使用”历史趋势。如果执行时间突然变长,回顾最近的代码部署。可能是新增了低效操作、或下游服务变慢。
- 检查并发配置:是否不小心将“单实例并发数”调得过高,导致单个实例负载过重,执行时间反而增加?或者预留实例数量设置过多且闲置?
一个真实案例:一个生成报告的函数,原本平均运行3秒。某次更新后,引入了一个未加索引的数据库查询,平均运行时间暴增至30秒。调用量不变,但总费用增长了10倍。通过监控平均执行时间曲线,很容易定位到问题发布时间点。
6.3 问题三:冷启动影响用户体验
现象:部分请求,特别是流量低谷后的第一个请求,延迟异常高(可能达到秒级)。排查与优化:
- 确认是冷启动:查看函数实例级别日志,寻找“Init Duration”或“Cold Start”相关的指标。平台监控通常也会提供冷启动比例。
- 优化方向:
- 减小部署包:检查
node_modules,vendor等目录,移除未使用的依赖。对于Python,避免打包整个site-packages。每减少10MB代码包,冷启动时间可能减少100-200ms。 - 使用层(Layer)分离依赖:将不常变动的运行时依赖(如特定版本的SDK、数据库驱动)放在层中。层会被缓存和复用,加速实例启动。
- 调整运行时:比较不同运行时的冷启动性能。通常,编译型语言(Go, Rust)的冷启动远快于解释型语言(Python, Node.js)。但也要考虑开发效率。
- 应用预留实例:对延迟绝对敏感的核心函数,配置预留实例。你为这些“常驻热实例”支付费用,但换来了零冷启动的体验。
- 预置并发(Provisioned Concurrency):这是更高级的功能,平台会提前初始化好指定数量的实例,并保持它们处于热状态,专门用于服务你的函数,彻底消除指定数量并发下的冷启动。
- 减小部署包:检查
6.4 并发度相关的配置陷阱
- 最大并发度设置过小:这是最危险的。一旦遭遇突发流量,请求会被限流丢弃,直接导致业务故障。建议基于压力测试结果,并留出至少50%的安全余量。
- 单实例并发数设置不当:对于CPU密集型函数,设置大于1会导致单个实例上的请求相互竞争CPU,所有请求都变慢,总吞吐量可能反而下降。需要通过测试来确定最佳值。
- 忽略下游服务的连接池限制:如果你的函数大量并发访问同一个MySQL数据库,而函数并发度很高,可能会瞬间打满数据库的最大连接数。需要在函数代码中实现连接池,并确保池大小与函数并发度匹配,或者使用数据库代理。