第4章 函数即服务(FaaS)深度解析:触发器、事件源与生命周期
聊到 FaaS,很多人第一反应就是「写个函数跑一跑」。嗯,这话没错,但太粗糙了。你想想看,一个函数孤零零地放在那里,谁来叫它?什么时候叫?叫起来之后怎么活、怎么死?这些才是真正决定你架构质量的关键。
我个人习惯把 FaaS 比作一个「事件驱动的微型服务机器人」。它平时休眠,有活来了就弹起来干活,干完继续睡。而触发器,就是唤醒它的那个闹钟。
4.1 HTTP 触发器:最常用的入口
HTTP 触发器,说白了就是让你的函数变成一个 API 接口。客户端发个 HTTP 请求过来,函数就执行,然后返回响应。这是最直观、最常用的触发器类型。
核心要点:HTTP 触发器让函数直接暴露为 RESTful API,支持 GET、POST、PUT、DELETE 等方法。
我在项目中遇到过这样一个场景:一个电商小团队,需要快速上线一个优惠券查询接口。如果用传统方式,得搭服务器、配路由、搞负载均衡,折腾半天。用 FaaS 的 HTTP 触发器,十几分钟就搞定了。
来看一个简单的示例(以阿里云函数计算为例):
// index.js
exports.handler = function(req, resp, context) {
// 解析请求参数
const { userId } = req.queries;
// 业务逻辑:查询优惠券
const coupons = queryCoupons(userId);
// 返回响应
resp.send(JSON.stringify({
success: true,
data: coupons
}));
};
这里要注意几个细节:
- 请求上下文:req 对象里包含了 headers、queries、body 等信息,跟 Express 框架很像
- 响应格式:resp.send() 可以返回 JSON、文本或二进制数据
- 超时限制:HTTP 触发器通常有超时时间,比如 60 秒,长任务别放这里
我曾经踩过一个坑:把一个大文件上传处理逻辑直接写在 HTTP 触发器里,结果文件一超过 10MB,函数就超时了。后来改成先上传到 OSS,再用对象存储触发器异步处理,问题就解决了。
4.2 定时触发器:像闹钟一样准时
定时触发器,就是让函数按照你设定的时间表自动执行。比如每天凌晨清理日志、每小时同步数据、每 5 分钟检查一次服务健康状态。
它的配置方式通常有两种:
| 配置方式 | 说明 | 示例 |
|---|---|---|
| Cron 表达式 | 类似 Linux 的 crontab,灵活定义时间 | 0 0 2 * * ?(每天凌晨 2 点) |
| 固定频率 | 简单设置间隔时间 | 每 10 分钟、每小时、每天 |
我个人习惯用 Cron 表达式,虽然刚开始觉得有点绕,但熟悉之后真的很灵活。举个例子:
// 定时清理过期数据的函数
exports.handler = function(event, context, callback) {
// event 里包含触发时间等信息
const triggerTime = event.triggerTime;
console.log(`定时任务触发于: ${triggerTime}`);
// 清理 7 天前的日志
cleanExpiredLogs(7);
callback(null, '清理完成');
};
小技巧:定时触发器的执行时间不是绝对精确的,会有毫秒级的偏差。如果你的业务对时间精度要求极高(比如金融交易),建议用专门的定时服务。
4.3 对象存储触发器:事件驱动的经典模式
对象存储触发器,是 FaaS 里最体现「事件驱动」思想的触发器。当你在 OSS(对象存储服务)里上传、删除、修改文件时,自动触发函数执行。
我做过一个图片处理系统,用户上传原图到 OSS,自动触发函数进行压缩、生成缩略图、加水印,整个过程完全自动化,用户无感知。
// 图片处理函数
exports.handler = function(event, context, callback) {
// 解析 OSS 事件
const ossEvent = JSON.parse(event);
const bucket = ossEvent.events[0].oss.bucket.name;
const objectKey = ossEvent.events[0].oss.object.key;
console.log(`新文件上传: ${bucket}/${objectKey}`);
// 如果是图片,进行压缩处理
if (isImage(objectKey)) {
compressImage(bucket, objectKey);
generateThumbnail(bucket, objectKey);
}
callback(null, '处理完成');
};
这种模式的好处很明显:
- 解耦:上传和处理完全分离,互不影响
- 异步:用户上传完就走,后台慢慢处理
- 弹性:上传量大的时候,函数自动扩容
注意:对象存储触发器可能会产生循环触发。比如函数处理完图片后,又把处理结果写回同一个 bucket,结果再次触发函数,形成死循环。我曾经就遇到过,排查了半天才发现是这个问题。
4.4 函数的冷启动与热启动
这是 FaaS 里最容易被忽视、但影响最大的一个概念。说白了,就是你的函数从「睡着」到「醒来」需要多久。
热启动:函数实例还在内存里,请求来了直接复用,响应时间通常在毫秒级。
冷启动:函数实例被销毁了,请求来了需要重新创建容器、加载代码、初始化环境,响应时间可能达到秒级甚至更长。
| 启动类型 | 状态 | 响应时间 | 典型场景 |
|---|---|---|---|
| 热启动 | 实例存活 | 1-10ms | 高频请求、实时 API |
| 冷启动 | 实例已销毁 | 100ms-5s | 低频请求、定时任务 |
为什么会冷启动?因为云平台为了节省资源,会在函数空闲一段时间后销毁实例。下次请求来了,就得重新来一遍。
核心观点:冷启动是 FaaS 的固有特性,无法完全消除,但可以通过策略优化。
我总结了几条优化冷启动的经验:
- 减少依赖包体积:只引入真正需要的库,别一股脑全装进去
- 使用初始化缓存:把数据库连接、配置加载等放到全局作用域,只初始化一次
- 预留并发实例:一些云平台支持预留一定数量的实例,避免冷启动
- 定期保活:用定时触发器每隔几分钟调用一次函数,保持实例热状态
// 优化后的函数结构
// 全局作用域:只初始化一次
const redis = new RedisClient({
host: process.env.REDIS_HOST,
port: 6379
});
const config = loadConfig(); // 配置文件只加载一次
exports.handler = async function(event, context) {
// 这里才是真正的业务逻辑
// 数据库连接、配置都已经准备好了
const result = await processEvent(event, redis, config);
return result;
};
我的建议:对于对延迟敏感的业务(比如用户直接调用的 API),尽量用热启动策略。对于后台任务(比如数据清洗、日志处理),冷启动那几秒完全可以接受。
嗯,到这里,FaaS 的触发器、事件源和生命周期就讲得差不多了。你想想看,这些概念其实并不复杂,关键是要理解它们背后的设计思想——事件驱动、按需执行、自动伸缩。掌握了这些,你就能真正用好 FaaS,而不是仅仅把它当成一个「能跑代码的玩具」。
下一章,我们会深入 FaaS 的运行时环境,聊聊内存、CPU、网络这些资源是怎么分配的,以及如何做性能调优。到时候见。