3、云函数进阶:异步调用、冷启动优化与并发扩缩容
好,咱们接着聊云函数。前面几章我们把云函数的基本用法过了一遍,能跑起来了,也能接 API 了。但说实话,那只是入门。真正到了生产环境,你会发现事情没那么简单。
我记得第一次把云函数部署到线上,用户一多,函数直接超时、报错、甚至卡死。我当时就懵了——明明本地测试好好的啊?后来才明白,云函数不是「写好了就能扛住流量」的。你得懂它的异步机制、冷启动的坑、还有并发扩缩容的脾气。
这一章,我就把这三个核心问题掰开揉碎讲清楚。你想想看,搞懂了这些,你才算真正入了 Serverless 的门。
3.1 云函数的异步调用
先说异步调用。说白了,就是函数调用方不等着结果返回,丢个请求就走。这在很多场景下非常有用,比如用户注册后发个欢迎邮件、上传图片后做压缩处理、或者定时跑个数据清洗任务。
我习惯把异步调用分成两种:事件驱动型异步和任务型异步。
3.1.1 事件驱动型异步
这种模式最常见。比如用户上传了一张图片到对象存储,存储桶触发一个事件,云函数自动被调用去处理图片。调用方(存储桶)不关心函数什么时候执行完,它只管「通知」一下。
代码示例(以腾讯云 SCF 为例):
// 处理图片上传事件
exports.main_handler = async (event, context) => {
console.log('收到文件上传事件:', JSON.stringify(event));
// 从事件中提取文件信息
const fileInfo = event.Records[0].cos.object;
const bucket = event.Records[0].cos.bucket;
// 异步处理:压缩图片
await compressImage(bucket, fileInfo.key);
// 这里不需要返回任何东西给调用方
return 'ok';
};
嗯,这里要注意:事件驱动的异步调用,函数本身不需要关心返回值。调用方(比如对象存储)也不会等你的结果。所以如果你的函数里有什么错误,调用方是不知道的——你得自己做好日志和告警。
3.1.2 任务型异步
另一种异步是「我主动调用一个函数,但我不想等它执行完」。比如用户注册后,你希望发一封邮件,但发邮件可能要几秒钟,你不想让用户干等着。
这时候可以用云平台提供的异步调用接口:
// 使用 SDK 异步调用云函数
const { SCF } = require('tencentcloud-sdk-nodejs-scf');
async function sendWelcomeEmailAsync(userEmail) {
const client = new SCF.Client({
credential: { secretId, secretKey },
region: 'ap-guangzhou'
});
// 关键:设置 InvokeType 为 'Event' 表示异步调用
await client.Invoke({
FunctionName: 'sendEmail',
InvocationType: 'Event', // 异步!
Payload: JSON.stringify({ email: userEmail })
});
// 这里立即返回,不等待函数执行完
return '邮件发送任务已提交';
}
为什么用异步?说白了,就是用户体验。用户注册完,你直接返回「注册成功」,后台慢慢发邮件。用户不用等那几秒钟,体验好得多。
3.2 云函数的冷启动优化
冷启动,这是 Serverless 的老大难问题。你想想看,一个函数第一次被调用时,平台需要拉起一个容器、加载代码、初始化运行时——这个过程就是冷启动。它可能耗时几百毫秒甚至几秒。
我刚开始做 Serverless 时,有个 API 接口,用户反馈「第一次访问特别慢,后面就快了」。这就是典型的冷启动问题。那怎么优化呢?
3.2.1 减少代码包体积
代码包越大,冷启动越慢。这是最直接的优化点。
- 只打包必要的依赖:别把整个 node_modules 都塞进去。用
npm install --production只装生产依赖。 - 使用层(Layer):把公共依赖(比如 SDK、工具库)放到层里,函数代码只包含业务逻辑。层会被缓存,冷启动时不用重新加载。
- 代码拆分:一个函数只做一件事。别把整个项目塞到一个函数里。
代码示例:使用层优化后的目录结构
# 项目结构
my-function/
├── src/
│ └── index.js # 业务代码,很小
├── layer/ # 层目录
│ └── nodejs/
│ └── node_modules/
│ ├── axios/
│ └── lodash/
└── serverless.yml
3.2.2 初始化代码延迟加载
很多人在函数外面做初始化,比如建立数据库连接。这会导致冷启动时所有初始化都跑一遍。
我建议:把初始化放到函数内部,按需加载。
// ❌ 不好的做法:全局初始化
const db = new Database(process.env.DB_URL); // 冷启动时一定会执行
exports.handler = async (event) => {
return db.query('SELECT * FROM users');
};
// ✅ 好的做法:延迟初始化
let db;
exports.handler = async (event) => {
if (!db) {
db = new Database(process.env.DB_URL); // 只在第一次调用时初始化
}
return db.query('SELECT * FROM users');
};
嗯,这里有个小技巧:用 lazy 模式初始化,冷启动时间能减少 30%-50%。我在一个高并发项目里试过,效果很明显。
3.2.3 预留并发实例
这是最「暴力」但最有效的方法。说白了,就是让平台一直保持几个函数实例是「热」的,永远不会被回收。
大多数云平台都支持预留并发(Provisioned Concurrency)。你设置一个最小值,比如 5 个实例,平台就会一直保持 5 个实例在线。冷启动?不存在的。
3.3 云函数的并发与扩缩容
最后一个话题,也是生产环境最容易出问题的——并发和扩缩容。
你想想看,你的函数平时可能只有几十个请求,突然搞了个活动,流量暴涨到几万。云函数能扛住吗?
3.3.1 并发上限
每个云平台都有并发上限。比如腾讯云 SCF 默认单个函数的并发上限是 1000(可以提工单提升)。AWS Lambda 默认是 1000。
这个上限是什么意思?就是同一时刻最多只能有 1000 个函数实例在运行。如果第 1001 个请求来了,它会排队等待,或者直接报错。
| 云平台 | 默认并发上限 | 可提升至 |
|---|---|---|
| 腾讯云 SCF | 1000 | 数万(需申请) |
| AWS Lambda | 1000 | 数万(需申请) |
| 阿里云 FC | 100 | 数千(需申请) |
我遇到过最惨的一次:双十一活动,并发直接冲到 3000,函数报错率飙升到 40%。后来紧急提工单提升并发上限才解决。所以,提前评估并发需求,提前申请上限,别等到出事了再处理。
3.3.2 扩缩容策略
云函数的扩缩容是自动的,但不同平台的策略不一样。
- 快速扩容:流量突增时,平台会快速创建新实例。但创建需要时间(冷启动),所以会有短暂的延迟。
- 慢速缩容:流量下降时,平台不会立刻销毁实例。它会保留一段时间(比如 5-15 分钟),以防流量再次上涨。
这里有个坑:缩容太慢会导致成本上升。我记得有一次,一个定时任务跑完后,函数实例还保留了 10 分钟,白白浪费了资源。后来我设置了更短的超时时间,让实例尽快释放。
3.3.3 避免并发冲突
并发高了,容易出另一个问题——资源竞争。比如多个函数实例同时写同一个文件、同时更新数据库同一条记录。
解决方案很简单:避免共享状态。
- 用数据库的事务机制处理并发写
- 用分布式锁(比如 Redis 锁)控制临界资源
- 尽量用幂等操作,同一个请求执行多次和一次结果一样
代码示例:使用 Redis 分布式锁
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function processOrder(orderId) {
const lockKey = `order:${orderId}:lock`;
// 尝试获取锁,超时 5 秒
const lock = await redis.set(lockKey, 'locked', 'NX', 'EX', 5);
if (!lock) {
throw new Error('订单正在处理中,请稍后重试');
}
try {
// 处理订单逻辑
await updateOrderStatus(orderId, 'processed');
} finally {
// 释放锁
await redis.del(lockKey);
}
}
嗯,这里要注意:锁的超时时间要设置合理。太短了可能业务没处理完锁就过期了,太长了会影响其他请求。我一般根据业务耗时设置,比如 5 秒或 10 秒。
小结
这一章我们聊了三个核心问题:异步调用怎么玩、冷启动怎么优化、并发扩缩容怎么应对。说白了,云函数不是「写代码」那么简单,你得理解它的运行机制,才能用好它。
我个人觉得,冷启动优化和并发管理是 Serverless 进阶的关键。搞懂了这两点,你就能写出既快又稳的云函数。下一章我们会聊更高级的话题——云函数与数据库的深度集成,到时候见。