Serverless 听起来像是前端的“部署即上线”终极形态,后端的“自动伸缩”,DevOps 的“管它呢”,老板的“降本利器”。但你真的了解 Serverless 的真相吗?
这篇文章从以下几个方面深挖 Serverless 的底层运行机制与真实世界的问题:
- Serverless 架构背后的冷启动与资源复用机制
- 云函数中共享上下文的灰色行为(以及它是否可取)
- 如何写出“像函数又像服务”的代码
- 实战中不可避免的“运行环境陷阱”
- Serverless 的异步梦魇和可观测性问题
一、Serverless ≠ 无服务器,那它到底是什么?
Serverless 的准确理解是:
- 你不需要管理服务器,但服务器依然存在
- 按调用计费,按需运行,不常驻内存
- 事件驱动 + 函数式编程模型
主流平台:
- AWS Lambda
- 阿里云函数计算(FC)
- Cloudflare Workers(边缘 Serverless)
- 腾讯云函数(SCF)
- Vercel Functions / Netlify Functions(前端集成型)
在掘金和知乎上你看到的“部署成本几毛钱”的故事,多半基于非常轻量的场景,没有考虑吞吐量、冷启动延迟、连接池失效等“暗流”。
二、冷启动:Serverless 里那个“半夜爬起来上班”的工人
Serverless 的最大性能瓶颈叫“冷启动”,发生在以下情况:
- 云平台无可用实例可复用(例如你晚上 3 点调用)
- 实例被平台“回收”后重新启动
- 更换运行环境(环境变量改变、依赖更新)
冷启动消耗的步骤:
- 容器调度(一般是 Firecracker 或轻量 VM)
- 下载代码包
- 初始化运行时(Node.js/Python/Java 等)
- 执行你的初始化代码(比如
require())
阿里云函数计算在 Node.js 环境下冷启动可能耗时在 300ms ~ 3s 不等,Java 更长,Rust/Go 最短(50ms 左右)。
热知识:
你以为平台帮你自动复用上下文,但这只是“可能”,不是“承诺”。
三、共享变量能用吗?为什么你觉得 browser 能共用,但偶尔又失效?
// 云函数外部
let browser;
exports.handler = async function (event, context) {
if (!browser) {
browser = await launchPuppeteer(); // expensive 初始化
}
const page = await browser.newPage();
await page.goto('https://example.com');
};
你以为这样就“只初始化一次”?不完全对:
- 同一个容器内是复用的(复用实例)
- 但容器会被平台定时销毁、迁移、缩容,且不可控
- 多并发下可能多个容器各自有自己的一份
browser
更糟糕的情况:
- 如果你用了非线程安全的全局变量(例如缓存数组、计数器),多个请求同时访问可能数据错乱
- 在阿里云 FC 中,cold-start 会重新载入整个函数包,旧的变量直接丢失
四、Serverless 与数据库连接的“老问题”:连接池与连接复用
当你用 Serverless 访问数据库(如 MySQL/PostgreSQL)时:
- 连接池机制失效:因为 Serverless 是无状态运行,函数冷启动时重新建立连接
- 连接泄漏风险大:如果你不手动关闭连接,平台回收前数据库连接不会自动断掉
- 高并发时数据库容易被打爆
最佳实践:
- 使用连接代理中间层(如 Aurora Proxy / 阿里云 PolarDB Proxy)
- Serverless 专用数据库服务(如 PlanetScale、Supabase)
- 利用 Prisma Data Proxy、Knex with pooling config 等方式
五、异步执行 = 可观测性灾难现场
你用了 Serverless 来异步执行任务,比如:
await invokeFunction('sendEmail', payload);
问题是:
- 一旦调用函数失败,平台不会自动重试(除非你显式加了 Retry Policy)
- 无法追踪子函数的调用链(Tracing)
- 日志碎片化严重,分布在不同冷/热容器中
可观测性补救方式:
- 使用链路追踪系统(如阿里云 ARMS、AWS X-Ray)
- 手动将 traceId、requestId 写入日志中并串联
- 使用中间件封装 log + trace(比如用 Express 模拟函数中间件)
六、实际项目中的 Serverless 使用模式(及反模式)
| 使用场景 | 是否推荐 | 原因说明 |
|---|---|---|
| 图片处理(upload 触发) | ✅ 推荐 | IO 密集、处理短 |
| 视频转码/长时间处理任务 | ❌ 不推荐 | 容易超时、费用高 |
| Webhook 消息监听 | ✅ 推荐 | 通常轻量、易事件驱动 |
| 登录鉴权逻辑 | ✅ 推荐 | 快速响应、Stateless |
| 实时多人协作(如 WebSocket) | ❌ 不推荐 | 需要状态保留,Serverless 不擅长 |
七、实战建议:如何写出优雅的 Serverless 应用?
- 函数包大小越小越好
决定冷启动时长;去掉无用依赖、按需打包 - 避免复杂依赖初始化
require('puppeteer')、require('firebase')会占用冷启动时间,最好放到函数内部懒加载 - 使用中间层连接缓存资源
比如 Redis、数据库连接池、认证 token - 用异步触发替代同步长流程
例如上传后立即返回,再通过队列触发处理任务 - 集中日志收集平台
自定义日志系统 or 使用官方日志服务,不要只靠console.log
八、Serverless 最容易踩的坑清单(来自真实项目)
| 坑点描述 | 原因 | 后果 |
|---|---|---|
| 冷启动初始化太慢 | 包体大、依赖多、外部请求多 | 首次访问白屏、超时 |
用 setTimeout 模拟任务延迟 | Serverless 有最大执行时长限制 | 超时被强行终止 |
| 把浏览器 Puppeteer 放 Serverless | Chromium 包大,冷启动卡死 | 甚至启动失败、OOM |
依赖 package.json 中 "optionalDependencies" | 某些 Serverless 平台不支持 | 运行时报错找不到模块 |
| 频繁日志输出 | 云平台日志计费 & 限制带宽 | 成本暴涨、日志截断 |
总结
- Serverless 是好工具,但不是银弹
- 它适合轻量、事件驱动、Stateless 的逻辑
- 不适合长时间计算、实时连接、大规模资源复用
- 云函数不是你熟悉的传统服务,它有自己的运行逻辑和“隐藏规则”