Serverless 真相:它没有你想象得那么“无服务”

302 阅读5分钟

Serverless 听起来像是前端的“部署即上线”终极形态,后端的“自动伸缩”,DevOps 的“管它呢”,老板的“降本利器”。但你真的了解 Serverless 的真相吗?

这篇文章从以下几个方面深挖 Serverless 的底层运行机制与真实世界的问题:

  • Serverless 架构背后的冷启动与资源复用机制
  • 云函数中共享上下文的灰色行为(以及它是否可取)
  • 如何写出“像函数又像服务”的代码
  • 实战中不可避免的“运行环境陷阱”
  • Serverless 的异步梦魇和可观测性问题

一、Serverless ≠ 无服务器,那它到底是什么?

Serverless 的准确理解是:

  • 你不需要管理服务器,但服务器依然存在
  • 按调用计费,按需运行,不常驻内存
  • 事件驱动 + 函数式编程模型

主流平台:

  • AWS Lambda
  • 阿里云函数计算(FC)
  • Cloudflare Workers(边缘 Serverless)
  • 腾讯云函数(SCF)
  • Vercel Functions / Netlify Functions(前端集成型)

在掘金和知乎上你看到的“部署成本几毛钱”的故事,多半基于非常轻量的场景,没有考虑吞吐量、冷启动延迟、连接池失效等“暗流”。


二、冷启动:Serverless 里那个“半夜爬起来上班”的工人

Serverless 的最大性能瓶颈叫“冷启动”,发生在以下情况:

  • 云平台无可用实例可复用(例如你晚上 3 点调用)
  • 实例被平台“回收”后重新启动
  • 更换运行环境(环境变量改变、依赖更新)

冷启动消耗的步骤:

  1. 容器调度(一般是 Firecracker 或轻量 VM)
  2. 下载代码包
  3. 初始化运行时(Node.js/Python/Java 等)
  4. 执行你的初始化代码(比如 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 应用?

  1. 函数包大小越小越好
    决定冷启动时长;去掉无用依赖、按需打包
  2. 避免复杂依赖初始化
    require('puppeteer')require('firebase') 会占用冷启动时间,最好放到函数内部懒加载
  3. 使用中间层连接缓存资源
    比如 Redis、数据库连接池、认证 token
  4. 用异步触发替代同步长流程
    例如上传后立即返回,再通过队列触发处理任务
  5. 集中日志收集平台
    自定义日志系统 or 使用官方日志服务,不要只靠 console.log

八、Serverless 最容易踩的坑清单(来自真实项目)

坑点描述原因后果
冷启动初始化太慢包体大、依赖多、外部请求多首次访问白屏、超时
setTimeout 模拟任务延迟Serverless 有最大执行时长限制超时被强行终止
把浏览器 Puppeteer 放 ServerlessChromium 包大,冷启动卡死甚至启动失败、OOM
依赖 package.json 中 "optionalDependencies"某些 Serverless 平台不支持运行时报错找不到模块
频繁日志输出云平台日志计费 & 限制带宽成本暴涨、日志截断

总结

  • Serverless 是好工具,但不是银弹
  • 它适合轻量、事件驱动、Stateless 的逻辑
  • 不适合长时间计算、实时连接、大规模资源复用
  • 云函数不是你熟悉的传统服务,它有自己的运行逻辑和“隐藏规则”