3. 函数即服务(FaaS)详解:FaaS的工作原理、冷启动与热启动、函数生命周期管理

好,咱们今天聊聊FaaS。说白了,函数即服务就是让你只关心代码逻辑,其他一切交给平台。我刚开始接触这个的时候,总觉得它像个黑盒子——把代码扔进去,它就能跑起来,但里面到底发生了什么?

这一章,我就带你把这个黑盒子拆开看看。

3.1 FaaS的工作原理

FaaS的核心思想很简单:事件驱动。你写一个函数,绑定一个事件源(比如HTTP请求、消息队列、对象存储事件),当事件发生时,平台自动拉起你的函数执行。

我习惯把FaaS比作一个外卖平台

  • 你(开发者)只管做菜(写函数代码)
  • 平台负责接单(接收事件)、派送(调度执行)、回收餐具(清理资源)
  • 你不需要自己开店(管理服务器),也不需要雇服务员(运维人员)

具体到技术层面,FaaS平台通常包含这几个组件:

组件 职责 我踩过的坑
事件网关 接收外部事件,解析请求参数 曾经没配好超时时间,导致长连接把网关打爆
调度器 决定哪个Worker来执行函数 调度策略选错,冷启动率飙升到40%
Worker池 实际运行函数的容器/沙箱 Worker预热数量不够,高峰期全在冷启动
存储层 存放函数代码、依赖、配置 代码包太大,下载时间比执行时间还长

你想想看,当用户发起一个HTTP请求时,流程是这样的:

  1. 事件网关收到请求,解析出函数名和参数
  2. 调度器检查是否有空闲的Worker
  3. 如果没有,就创建一个新的Worker(这就是冷启动)
  4. Worker加载函数代码,执行逻辑
  5. 返回结果,Worker可能被回收或保留一段时间

核心要点:FaaS的本质是按需分配、用完即走。这带来了弹性伸缩的优势,但也引入了冷启动的代价。

3.2 冷启动与热启动

这是FaaS里最让人头疼的问题,没有之一。我记得有一次线上告警,用户反馈接口响应时间从50ms飙到了3秒。查了半天,原来是冷启动搞的鬼。

先明确概念:

  • 热启动:Worker已经存在,函数代码已加载,直接执行。延迟通常在毫秒级。
  • 冷启动:需要创建新的Worker,下载代码,初始化运行时。延迟可能达到秒级。

为什么会冷启动?说白了就是平台为了节省资源,把空闲的Worker回收了。等你下次请求来了,再重新创建。这就像你租房子——房东为了省钱,你退租后就把水电断了,等你再租时得重新开通。

冷启动的耗时主要来自这几个环节:

阶段 耗时占比 优化方向
下载代码包 30%-50% 减小包体积,使用分层缓存
启动运行时 20%-30% 使用轻量级运行时,预置依赖
初始化代码 10%-20% 延迟加载,避免全局初始化
网络/调度 10%-20% 优化调度策略,就近部署

我的经验:我曾经把一个Node.js函数的代码包从50MB压缩到5MB,冷启动时间从2.5秒降到了0.8秒。怎么做的?把那些只在特定场景才用的依赖,改成按需加载。说白了,别一股脑把所有东西都塞进去。

3.3 函数生命周期管理

一个FaaS函数从创建到销毁,会经历几个阶段。理解这个生命周期,你才能写出健壮的代码。

3.3.1 初始化阶段

这是函数第一次被调用时执行的代码。在Node.js里,就是模块顶层的代码,不在handler函数内部的代码。

// 这是初始化阶段执行的
const redis = require('redis');
const client = redis.createClient();

// 这是每次调用执行的
exports.handler = async (event) => {
  // 你的业务逻辑
  const data = await client.get(event.key);
  return data;
};

嗯,这里要注意:初始化阶段的代码在冷启动时只执行一次,后续的热启动会复用。所以,把数据库连接、配置加载这些耗时操作放在初始化阶段,能显著提升性能。

我曾经犯过的错:把数据库连接放在了handler函数里,每次调用都新建连接。结果数据库连接数直接爆了,DBA差点没把我骂死。记住:连接复用是FaaS性能优化的第一课

3.3.2 调用阶段

每次事件触发时,执行handler函数。这个阶段要保证无状态——不要依赖上一次调用的上下文。

为什么?因为同一个Worker可能会被多个请求复用,但你不能保证下一个请求一定落在同一个Worker上。我习惯的做法是:

  • 所有状态都存到外部存储(Redis、数据库)
  • 不要在全局变量里缓存用户数据
  • 使用请求ID追踪每次调用

3.3.3 空闲与回收阶段

函数执行完后,Worker不会立即销毁。平台会保留一段时间(通常是5-15分钟),等待下一个请求。如果一直没有请求,Worker就会被回收。

这个阶段有个坑:优雅关闭。如果你的函数打开了文件、建立了长连接,在Worker被回收时应该主动关闭。否则可能造成资源泄漏。

// 不好的做法
let dbClient;

exports.handler = async (event) => {
  if (!dbClient) {
    dbClient = await createDbConnection();
  }
  // 使用dbClient
};

// Worker被回收时,dbClient还开着,泄漏了

// 好的做法
// 使用平台的生命周期钩子(如果有)
// 或者在每次调用后主动关闭(但会损失性能)

总结一下生命周期管理的关键点:

  • 初始化阶段:做一次性的、耗时的准备工作
  • 调用阶段:保持无状态,快速执行
  • 空闲阶段:注意资源清理,避免泄漏
  • 回收阶段:平台自动处理,但你要确保代码健壮

3.4 实战建议

说了这么多理论,最后给几个实在的建议:

  1. 代码包越小越好。我见过有人把整个node_modules都打包进去,里面一堆用不上的包。精简!精简!再精简!
  2. 合理设置并发和超时。别把超时设得太长,否则Worker被长时间占用,冷启动率会飙升。
  3. 使用预热策略。对于关键接口,可以定时发送请求,保持Worker的热度。但别太频繁,否则浪费钱。
  4. 监控冷启动率。这是FaaS最重要的性能指标之一。如果冷启动率超过10%,就该优化了。

你想想看,FaaS虽然方便,但也不是银弹。理解了它的工作原理和生命周期,你才能用好它,而不是被它坑。下一章,咱们聊聊事件驱动架构,看看怎么把FaaS和消息队列、事件总线结合起来,构建真正的弹性系统。