4、并发与扩缩容策略:并发执行模型、扩缩容触发条件、如何避免冷启动导致的并发瓶颈
并发执行模型:别让请求排长队
无服务器架构里,并发是个绕不开的话题。说白了,就是同一时刻有多少请求在跑。
我习惯把并发模型分成两种来看:请求级并发和实例级并发。
请求级并发,就是你的函数同时被调用了多少次。比如用户疯狂点击下单按钮,一秒内触发了500次函数调用。实例级并发呢,指的是底层有多少个函数实例在同时处理这些请求。
这里有个关键点——一个实例一次只能处理一个请求。你想想看,如果500个请求同时进来,但只有10个实例,那剩下的490个就得排队等着。
我在项目中遇到过这种情况:某次大促活动,流量突然暴涨,函数实例数没跟上,结果请求排队时间从50ms飙升到3秒。用户体验直接崩了。
扩缩容触发条件:什么时候该加人?
无服务器平台的扩缩容,不是你想扩就能立刻扩的。它有一套自己的触发逻辑。
常见的触发条件有这么几种:
- 请求到达率:每秒新来的请求数超过阈值,平台就会尝试创建新实例
- 并发实例数:当前活跃实例数接近上限,触发扩容
- CPU/内存使用率:实例内部资源吃紧,平台会考虑扩容
- 队列深度:等待处理的请求太多,说明实例不够用了
但这里有个坑——扩缩容不是瞬时的。我记得有一次,平台检测到并发升高,开始创建新实例。但创建过程花了2秒,这2秒里进来的请求全被堵住了。
具体怎么配?我一般这样操作:
| 场景 | 预留实例数 | 最大并发数 |
|---|---|---|
| 日常API | 2-5 | 100 |
| 大促活动 | 20-50 | 500 |
| 后台任务 | 0 | 50 |
嗯,这里要注意:预留实例是要花钱的,哪怕没请求也在计费。所以别一股脑配太多,得根据实际流量来。
冷启动导致的并发瓶颈:最头疼的问题
冷启动,说白了就是函数第一次被调用时,平台需要初始化环境。这个过程很慢,可能几百毫秒甚至几秒。
为什么会这样?因为平台要下载代码、启动运行时、执行初始化逻辑。你想想看,如果并发突然上来,新创建的实例都得经历冷启动,那性能直接就崩了。
我遇到过最夸张的一次:某个函数冷启动耗时1.2秒,结果并发从10涨到100时,新实例的冷启动时间叠加,导致前几秒的请求全部超时。
怎么避免?我总结了几个实战经验:
实战策略:让冷启动不再是瓶颈
策略一:预留并发实例
这是最直接的办法。提前创建好一批实例,让它们处于「热身」状态。请求来了直接处理,不用等冷启动。
# 伪代码示例:配置预留并发
reserved_concurrency:
min: 10 # 最少保持10个实例
max: 100 # 最多扩展到100个
我个人习惯在核心接口上配置5-10个预留实例。成本可控,效果明显。
策略二:代码层面优化初始化
冷启动慢,很多时候是因为初始化逻辑太重。比如加载配置文件、建立数据库连接。
我建议把初始化逻辑放到全局作用域里,而不是每次请求都执行。这样冷启动只发生一次,后续请求复用。
# 好的做法:全局初始化
import heavy_library
# 这里只执行一次
client = heavy_library.Client()
def handler(event, context):
# 每次请求复用 client
return client.process(event)
策略三:使用预热机制
写一个定时任务,每隔几分钟调用一次函数。这样实例就不会被回收,始终保持「热」状态。
# 定时预热配置(cron表达式)
schedule: "rate(5 minutes)"
target: your-function-name
但要注意:预热频率别太高,否则白白浪费钱。我一般设5-10分钟一次。
策略四:控制依赖大小
冷启动时间跟代码包大小直接相关。包越大,下载越慢。
我见过有人把整个机器学习框架打包进去,结果冷启动花了5秒。其实只需要用到的几个函数,完全可以按需导入。
总结一下
并发与扩缩容,说白了就是三个字:快、准、稳。
- 快:冷启动要快,预留实例、预热机制安排上
- 准:扩缩容触发条件要准,别等请求堵死了才扩容
- 稳:并发模型要稳,实例数和请求数尽量匹配
你想想看,如果这三个点都做到了,无服务器应用的性能基本就稳了。剩下的就是根据实际流量不断调优。
嗯,这一章就聊到这儿。下一章我们聊聊数据库连接池,那也是个容易踩坑的地方。