第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、网络这些资源是怎么分配的,以及如何做性能调优。到时候见。