Serverless 函数
为什么前端工程师需要认真理解它
很多资料会把 Serverless 讲成一个云厂商计费模型。
这种说法不算错,但对前端工程师不够有用。
更实用的理解是:
Serverless 是一种执行模型。
平台负责服务器生命周期、扩缩容和一部分运行时管理。
团队负责交付一小段一小段的后端逻辑。
这也是为什么它会持续进入前端工程师的工作范围。
今天的前端项目很少只负责渲染页面。
团队通常还要处理:
- 表单提交
- 鉴权校验
- Webhook 接收
- 短期令牌交换
- 缓存失效
- 内容聚合
- 轻量 BFF
- 定时任务
在 Next.js App Router 里,这些需求常常落在 route.ts。
在 Vercel、AWS、Cloudflare、Netlify 这些平台里,它们又会以不同的 Serverless 运行时出现。
如果你只记住一句“无服务器”,很容易误判它适不适合自己的场景。
如果你把它看成“由平台托管、按事件触发、以小执行单元为边界的后端模型”,很多工程判断就会清楚得多。
一个更准确的定义
Serverless 不是没有服务器。
服务器仍然存在。
只是它们不再由你的团队直接配置、扩容、打补丁和维护。
这个区分很重要。
因为 Serverless 并没有消灭后端问题。
它只是把问题的重心从“机器管理”转成了“执行模型设计”。
你仍然要关心:
- 延迟
- 并发
- 超时
- 重试
- 幂等
- 数据边界
- 权限控制
- 监控与告警
- 成本
平台通常会替你处理:
- 机器准备
- 基础扩缩容
- 部分请求路由
- 运行环境启动
- 部署承载
但团队仍然要自己负责:
- 代码逻辑
- 运行时选择
- 数据访问方式
- 依赖体积
- 超时策略
- 安全边界
- 可观测性
所以真正的问题不是“要不要服务器”。
真正的问题是:
这个需求是否适合一个由平台管理、按请求或事件触发、倾向无状态的小执行单元。
执行模型:它到底是怎么工作的
可以先用一个简化流程理解:
- 请求或事件到达平台。
- 平台找到一个可用执行环境,或者新建一个执行环境。
- 函数代码开始执行。
- 返回响应,或者完成事件处理。
- 执行环境可能被复用,也可能被销毁。
这个流程看起来简单,但会直接影响工程设计。
无状态是默认前提
很多人第一次接触 Serverless,会把“无状态”理解成“每次执行后什么都不剩”。
这并不准确。
像 AWS Lambda 这样的服务,明确支持执行环境复用。
这意味着:
- 函数外层初始化的对象,后续调用可能继续复用
- 某些连接可能继续存活
- 某些临时文件可能暂时还在
这对性能是好事。
但它只是优化手段。
不是持久化承诺。
你不能假设下一次请求一定命中同一个执行环境。
所以持久状态仍然应该放在外部系统里,例如:
- 数据库
- 对象存储
- 队列
- 缓存
- KV 存储
冷启动是执行模型的一部分
当平台需要为一个请求准备新的执行环境时,就会发生冷启动。
冷启动成本通常来自:
- 运行时准备
- 代码装载
- 依赖初始化
- 网络连接建立
所以在 Serverless 里,包体积和初始化逻辑会比传统常驻服务更敏感。
不是因为平台“慢”。
而是因为它的执行边界更短、更频繁。
复用执行环境有价值,但不能当成契约
如果执行环境被复用,性能可能会更好。
常见收益包括:
- SDK client 不需要重复初始化
- 数据库连接可以复用
- 配置解析只做一次
- 短时缓存可能还在
但正确性不能建立在这些条件上。
系统要先能在“完全重新启动”的情况下正常工作。
然后再利用复用带来的性能收益。
前端团队最容易混淆的点:Node Serverless 和 Edge Runtime 不是一回事
很多文章会把它们混在一起讲。
实际做项目时,最好把它们明确分开。
Node 型 Serverless 函数
常见代表:
- AWS Lambda 的 Node.js 运行时
- Vercel Functions 的 Node.js runtime
- Netlify Functions
这一类运行时的特点通常是:
- Node.js 兼容性更完整
- npm 生态兼容性更强
- 更适合依赖较重的后端逻辑
- 冷启动成本通常高于基于 isolate 的 edge 运行时
适合的场景包括:
- 依赖复杂 SDK
- 需要完整 Node API
- 数据库客户端依赖 Node 能力
- PDF 生成
- 内容转换
- 逻辑较重的 API
Edge Runtime
常见代表:
- Cloudflare Workers
- Vercel Edge Functions
- Next.js 中显式选择 edge runtime 的路由
这一类运行时通常:
- 启动更轻
- 更接近用户
- 延迟更低
- API 面更窄
- 并不支持完整的 Node.js 能力
适合的场景包括:
- 鉴权前置判断
- 重定向
- 地理位置相关逻辑
- Header 和 Cookie 改写
- 简单个性化
- 轻量 API 组合
这里的工程判断并不复杂:
如果你更在意全量 Node 兼容和复杂逻辑承载,优先考虑 Node 型 Serverless。
如果你更在意低延迟入口处理,而且代码确实能适配更窄的运行时,Edge 才值得优先考虑。
放到 Next.js 语境里怎么理解
这个项目本身就是 Next.js。
所以把 Serverless 放到 Next.js 语境里理解会更实际。
在 App Router 中,Route Handlers 是定义后端请求处理的标准方式。
这让前端团队可以不额外拆出一个服务,就先交付一层轻量后端逻辑。
典型用途有:
- 表单提交接口
- Session 辅助接口
- Webhook 验签
- 上传签名
- 内容聚合接口
- 面向页面的 BFF
这里最常见的误区是:
以为只要放进 Next.js,就天然是一个“适合 Serverless”的逻辑。
其实不是。
你仍然要判断:
- 这段逻辑是否需要完整 Node 环境
- 是否对冷启动敏感
- 是否依赖大体积 SDK
- 是否可能超时
- 是否应该拆成后台任务
Node runtime 通常是更稳妥的默认值。
只有当需求真的适合 edge 时,再切换到更轻的运行时。
更适合 edge 的逻辑
- 简单鉴权判断
- 区域感知重定向
- 轻量请求改写
- 少量数据聚合
更适合 Node Serverless 的逻辑
- 使用 Node 专属依赖
- 数据库操作较复杂
- 逻辑较重的内容处理
- 需要更强兼容性的第三方 SDK
- 生成文件或处理较大 payload
前端工程里的典型使用场景
1. BFF 接口
BFF 的核心价值不是“转发一下请求”。
而是把后端数据整理成页面真正需要的形状。
一个好的 Serverless BFF 往往会做这些事:
- 鉴权
- 聚合多个后端服务
- 去掉不该暴露给浏览器的字段
- 做轻量缓存控制
- 输出更稳定的前端契约
这样做的结果通常是:
- 客户端逻辑更简单
- 页面拿到的数据更干净
- 后端接口变更对页面的影响更可控
2. 密钥相关操作
浏览器不能直接持有高权限密钥。
这类操作很适合放进 Serverless:
- 第三方 API 的安全调用
- 上传签名
- 短期 Token 交换
- 带权限校验的窄接口
但这里要避免一个误区:
不是所有请求都值得“再包一层”。
真正应该迁移到 Serverless 的,是那些需要服务端保密信息或安全校验的操作。
3. Webhook 接收
Webhook 天然是事件驱动的。
所以很适合 Serverless。
常见例子:
- 支付回调
- CMS 发布事件
- Git 仓库事件
- 身份系统通知
这类函数通常应该优先完成:
- 验签
- 基本校验
- 幂等判断
如果后续任务很重,就应该尽快交给队列或后台流程,而不是把整个流程都塞在一次请求里。
4. 定时任务
很多平台都支持 Cron 触发。
这让 Serverless 不只用于用户请求。
它也能承担:
- 缓存刷新
- 数据同步
- 报表生成
- 清理过期数据
这时要特别注意失败重试和告警。
因为这类任务经常没有用户在前面盯着页面是否正常返回。
性能问题:真正需要关心的不是“快不快”,而是“哪一段在慢”
初始化成本要尽量小
很多冷启动问题,本质上是初始化问题。
比如:
- 引入了过大的 SDK
- 一个函数承担了过多职责
- 在模块加载阶段做了太多工作
- 每次都建立昂贵连接
常见优化方向:
- 保持 bundle 小
- 按需引入依赖
- 不把所有逻辑塞进一个 handler
- 把可复用初始化放到函数外层
- 延迟那些不是每次都需要的初始化
数据库连接是一个高频坑
不少团队第一次做 Serverless API,会在每次调用里新建数据库连接。
请求量一上来,就会出现连接数压力。
更稳妥的方案通常包括:
- 结合平台特性的连接复用
- 使用适合 Serverless 的连接池方案
- 在合适场景下使用 HTTP 型数据库驱动
- 通过 data proxy 或托管中间层吸收连接压力
这里没有一句通用口号可以解决问题。
必须结合数据库类型、平台能力和流量特征来判断。
把用户请求和重任务拆开
如果用户请求只需要完成输入校验、写入一条记录、触发后续处理,那么就不要在同步请求里把所有工作做完。
更健康的模式通常是:
- 校验输入
- 写入最小必要数据
- 投递队列或触发后台流程
- 尽快返回响应
这样做的好处是:
- 响应时间更稳定
- 超时风险更低
- 失败边界更清晰
- 后台任务更容易重试
可靠性问题:Serverless 不是“自动扩容”四个字就结束了
幂等要提前设计
真实系统里,重试会反复出现。
重试可能来自:
- 平台
- Webhook 发送方
- 客户端
- 中间网络
所以只要函数会产生副作用,就应该认真考虑幂等。
常见手段:
- 用事件 ID 去重
- 用 upsert 代替盲目 insert
- 标记已处理记录
- 让队列消息可重复消费
如果没有幂等设计,Serverless 经常会把“偶发重试”放大成“重复写入、重复扣费、重复发消息”。
部分失败要有模型
函数可能在很多中间状态下失败:
- 写库前失败
- 写库后、返回前失败
- 调用下游服务一半时失败
- 响应超时但后台逻辑已部分执行
所以工程上不能只有 happy path。
你需要考虑:
- 事务边界
- 补偿机制
- 重试策略
- 死信处理
- 关联日志
可观测性必须从第一天开始做
Serverless 的一次执行很短。
分布又比较散。
如果日志和指标做得差,定位问题会非常慢。
至少应该准备:
- request ID
- 结构化日志
- 错误聚合
- 延迟指标
- 调用次数指标
- 下游依赖耗时
否则看起来是“平台自动管理”,实际上会变成“问题很难复盘”。
安全问题:换了运行方式,不代表天然更安全
Serverless 不会自动帮你完成安全设计。
它只是提供了更方便部署服务端代码的方式。
常见安全要点包括:
- 在边界处做输入校验
- 验证 Webhook 签名
- 使用最小权限凭证
- 把密钥放在平台的 secret 管理里
- 响应里不要泄露内部错误细节
- 给容易被滥用的接口做限流
还有一个很现实的问题:
很多前端团队第一次接触服务端敏感逻辑,就是从 Serverless 开始。
这时更需要安全评审,而不是因为“代码量小”就掉以轻心。
成本:按调用付费不等于一定省钱
“按使用付费”是 Serverless 的重要优点。
但这句话不应该被理解成“任何场景都更便宜”。
真实成本通常和这些因素一起决定:
- 调用次数
- 执行时长
- 内存或 CPU 规格
- 网络出口
- 子请求数量
- 绑定的数据库、队列、缓存等服务
它通常更适合:
- 流量波动明显的业务
- 突发请求
- 还不值得养重运维体系的项目
- 需要快速验证的功能
它未必适合:
- 长时间常驻的重计算任务
- 大量持续运行的后台管道
- 明显更适合容器或专用服务的工作负载
做决策时,最好基于真实调用链和平台账单模型做估算,而不是凭印象选型。
常见误区
误区一:把 Serverless 当成架构替代品
Serverless 可以减少基础设施管理。
但它不能替代系统设计。
请求边界、数据边界、缓存策略、后台任务拆分,这些问题仍然需要认真设计。
误区二:一个函数里塞太多职责
职责过重的函数通常会带来:
- 体积更大
- 冷启动更重
- 测试更困难
- 失败影响面更大
职责单一、边界清晰的函数通常更容易维护。
误区三:本地能跑通,就默认线上也没问题
线上环境和本地环境经常不一样。
真正影响线上稳定性的,往往是:
- 超时限制
- edge 运行时 API 不兼容
- 包体积限制
- 连接数压力
- 区域网络延迟
误区四:把长任务硬塞进同步请求
如果任务时间长、容易失败、需要多步重试,那么它大概率更适合队列、工作流引擎或容器型后台任务,而不是同步 Serverless 请求。
面试表达可以怎么说
一个比较稳妥、专业、但不空泛的回答可以是:
Serverless 函数是一种由平台托管的后端执行单元,适合处理事件驱动或请求驱动的轻量后端逻辑。
对前端团队来说,它常用于 BFF、Webhook、Token 交换、上传签名和轻量编排,因为它能降低基础设施负担,并且对突发流量扩展得比较自然。
但它并不意味着不需要后端设计。
实际工程里仍然要处理冷启动、无状态设计、数据库连接、重试幂等、可观测性,以及 Node runtime 和 Edge runtime 的选择。
如果能把这些点说清楚,说明你理解的是工程边界,而不是宣传口号。
实践检查清单
在决定某个需求是否要用 Serverless 前,可以先问自己:
- 这是请求驱动还是事件驱动?
- 这段逻辑能否保持边界清晰、职责单一?
- 它是否需要完整 Node.js 兼容?
- 它真的需要 edge 级低延迟吗?
- 持久状态放在哪里?
- 失败后如何重试?
- 是否需要幂等?
- 日志、指标、追踪怎么做?
- 目标平台的超时、体积、子请求限制是什么?
如果这些问题没有答案,通常说明函数边界还没设计好。