1. 先别急着写代码,把“Serverless到底解决什么问题”想清楚
干了这么多年后端架构,我见过太多团队一上来就喊“我们要上Serverless”,结果把单体服务原封不动拆成几十个函数扔上去,然后被冷启动、连接数、超时限制折磨得死去活来。Serverless不是一个“把代码部署到云端”的简单动作,它首先是一种架构模式的选择——你选它,是为了让某一部分业务逻辑摆脱对服务器的显式管理,让基础设施的伸缩、容错、资源分配全部下沉到平台侧。
用大白话说,传统开发是你自己租服务器、装环境、配负载均衡、盯磁盘水位,半夜报警了还要爬起来扩容;Serverless模式下,你只需要把“一段逻辑”交出去,平台帮你搞定“这段逻辑跑在哪台机器上、需要几台、什么时候销毁”。这不是简单的部署方式变化,而是职责边界的变化——运维关注的粒度从“实例”变成了“函数”,从“基础设施”变成了“应用行为”。
但这句话得说完整:Serverless并不意味着“没有服务器”,而是“服务器对你不可见”。作为开发者,你要接受这种“不可见”带来的约束——你的代码必须无状态、必须短平快、必须能容忍平台随时回收资源。如果违背了这些约束,你会把Serverless用成一场灾难。
这篇文章不是入门科普,而是想跟你聊聊我这些年做Serverless架构设计踩过的坑、沉淀下来的套路,以及到底什么样的业务才适合套用哪种Serverless架构模式。我会用几个真实项目作为例子,把选型、拆分、性能调优、成本控制这些环节完整过一遍。看完之后,你应该能自己判断:你的项目适不适合Serverless,适合的话,该用哪种模式切入。
先说清楚两类最核心的Serverless形态,这是后面所有选型讨论的基础。
1.1 FaaS(函数即服务)和BaaS(后端即服务)的区别,别混为一谈
很多人把Serverless等同于FaaS,也就是Lambda、函数计算这类“上传一段代码,按调用次数计费”的产品。但严格来说,Serverless架构包含两大类:FaaS负责承载“你写的业务逻辑”,BaaS负责承载“你不想自己维护的后端能力”——对象存储、消息队列、身份认证、数据库、推送服务,这些都是BaaS的范畴。
理解这个区别特别重要,因为它决定了你的系统边界怎么画。举个例子:一个图片上传功能,如果你用FaaS去接收上传请求、校验格式、压缩图片、存到对象存储,再把消息投递到队列,这一段流程是FaaS;而对象存储本身、消息队列本身、鉴权服务本身,这些你并没有写代码的环节,是BaaS。
FaaS和BaaS组合在一起,才构成了完整的Serverless体验。FaaS给你“按需运行的逻辑容器”,BaaS给你“可以直接调用的托管服务”。很多团队只盯着FaaS,忽略了BaaS的选型,结果函数里塞满了SDK调用、重试逻辑、状态记录——这等于把本该由托管服务承担的职责,硬生生搬回了自己的代码里,非常不划算。
1.2 按量付费、自动伸缩、无状态约束——三个关键词藏着三层代价
FaaS按调用次数和运行时长计费,听起来很美好:不用就不花钱。但这里有个隐藏逻辑——你的代码必须是可重入的、幂等的。因为平台会在你不知情的情况下杀掉实例,再在任何一台新机器上把函数拉起来。如果你的函数里存了本地状态(比如把数据写进了/tmp目录,或者用了内存缓存),那下一次调用时这些状态可能全部丢失。
我见过最典型的事故:一个团队把数据库连接池初始化写在函数初始化阶段,平时没问题,一旦实例被回收后重建,连接池冷启动要花5秒,结果大量请求直接超时。后来改成每次调用时动态创建连接并配合外部缓存,才解决了问题。
自动伸缩的模式也要重新理解。传统架构你是“先买好机器再承接流量”,Serverless是“流量来了平台帮你拉起实例”。听起来很灵活,但实例拉起来是有延迟的——这就是业界常说的冷启动。后面我会专门讲怎么控制和优化这部分损耗。
2. 六种常见的Serverless架构模式,和它们各自的适用边界
选型不能靠感觉,得靠业务形态来判断。我把自己做过的项目归了归类,形成了六种相对固定的模式。每种模式都有明确的使用场景和坑点,我们一个个过。
2.1 事件驱动型:API网关触发函数,最经典也最容易上手
这是绝大多数人接触Serverless的第一个模式:客户端请求打到API网关,网关直接把请求转发给函数,函数处理完返回结果。典型场景是轻量级REST API、表单提交、Webhook接收。
这个模式最大的价值是“无状态HTTP服务”可以直接替换传统后端,而且天然支持按请求并发伸缩。但要注意几点:
- 函数超时时间通常有限制(各平台不同,大概在几分钟到十几分钟之间),如果你的接口要执行长任务,需要把任务异步化。
- API网关到函数的链路多了一层网络跳转,时延会比直连ECS略高,对毫秒级延迟敏感的业务要慎重。
- 函数实例的并发数受账号配额限制,峰值流量超过配额会直接拒请求,这个必须提前做好压测。
我通常建议:新项目的前端BFF(Backend for Frontend)层、中小流量的管理后台API,用这个模式快速搭建非常舒服。但如果你的接口逻辑特别复杂、依赖特别多,函数包体积会被撑大,冷启动会明显变慢——这时候你可能需要拆分函数,或者考虑容器化Serverless产品。
2.2 流处理型:对象存储或消息队列触发,异步处理的主力
这个模式的艺术在于“生产者和消费者完全解耦”:文件上传到对象存储,事件触发函数去处理;消息发送到队列,函数去消费。我做过最典型的案例就是图片压缩服务:用户上传原图到存储桶,对象存储的创建事件触发函数,函数拉取图片、压缩、写回另一个存储桶,整个过程用户无感知。
这个模式有几个天然优势:削峰填谷能力极强、存储和计算完全分离、函数实例随事件量伸缩。但它也有隐蔽的问题:
- 事件源投递是“至少一次”语义,也就是说同一个事件可能被投递多次,你的函数必须把幂等性做扎实,否则会出现重复处理。
- 如果处理失败,消息会被反复重试,重试策略不设置好,可能造成积压和费用飙升。
- 函数处理完需要手动确认删除消息,这个步骤漏了,后果就是同一批数据在队列里反复触发执行。
2.3 定时任务型:按固定时间触发,替代传统Crontab的现代玩法
传统架构里跑定时任务,你得有台长期在线的服务器,哪怕任务每天只跑一分钟,机器也得24小时待命。Serverless模式下,定时触发器按你设定的cron表达式唤起函数,跑完即销毁,按实际执行时长计费。
这个模式对“低频但周期性的任务”特别友好:每日数据汇总、定时发送报表、定期清理过期数据、轮询第三方接口状态。成本对比很直观——一台最便宜的云服务器一个月也要几十块钱,而一个每天运行几分钟的函数,一个月可能就几毛钱。
需要注意:定时任务的调度间隔最小粒度是分钟级(部分平台支持秒级),所有需要秒级响应的任务都不适合。另外,任务错过执行窗口导致的数据延迟,要有补偿机制,比如用消息队列兜底。
2.4 任务编排型:用工作流把多个函数串成一条流水线
单一函数适合做单一职责的事情,但真实业务往往是多步骤的:订单创建之后要扣库存、生成发票、发送通知、更新报表。如果把这些流程全部塞进一个函数,你会得到一堆耦合的代码;如果拆成多个函数独立调用,你又要处理步骤间的状态传递和失败重试。
工作流编排服务(比如云厂商的Step Functions或Serverless Workflow)就是干这个的:定义状态机,每个步骤映射到不同的函数,步骤间自动传递输入输出,失败时自动重试或跳到补偿逻辑。这个模式让业务逻辑的流转变得可视化,排障的时候能清晰地看到卡在哪个环节。
实际项目中,我通常把它用于订单处理、审批流、数据加工流水线这类“有明确阶段划分”的场景。但注意,编排器的执行步骤数量对计费有影响,步骤特别多的时候,流程编排产生的费用可能比函数本身还高,这个要提前拉账单看看。
2.5 全托管BaaS组合型:数据库、认证、对象存储都交给云,代码只做胶水层
有些业务,尤其是一些工具类应用,核心价值不在后端逻辑,而在于数据模型和前端交互。这类项目最适合的模式是“BaaS大集合”:数据库用托管数据库(或Serverless数据库),用户认证用托管身份认证服务,文件存储用对象存储,后端只用一小部分函数做胶水逻辑,比如校验数据、触发通知。
这样做能把后端的开发量压到极低,全栈一个人也能快速上线产品。国内外的低代码平台、小程序后端,很多都是这个套路。我做个人项目的时候就特别喜欢这种方式——白天上班写复杂的分布式系统,晚上做自己的小产品,只想快点上线验证想法,这时候让我写一套完整的用户体系加权限管理,我是不愿意的。
但这个模式的坑在于供应商锁定非常严重——你用了一个云厂商的数据库、认证、存储、函数,每一个组件都迁移成本极高。做产品验证可以,做长期业务要事先评估这点。
2.6 混合模式:Serverless和传统架构协同,别搞“一刀切”
很多系统的改造路径不是“从0迁移到Serverless”,而是“局部引入Serverless解决特定问题”。比如你有一套基于Kubernetes的服务,某个功能流量波动极大、又没什么状态,那就单独把这个功能抽出来做成函数,让K8s服务通过HTTP调用它。再比如你有一个重度依赖消息队列的旧系统,可以在消息消费者侧引入函数,替代原来的常驻消费进程。
这种混合模式的好处是:风险可控、改动可控、可以增量演进。我强烈建议大多数有存量系统的团队走这条路,而不是搞激进的全量重构。Serverless是工具,不是信仰,适合你的业务的那部分才值得引入。
3. 实操实录:我用Serverless重写了一个图片处理服务
理论说再多也不如动手做一遍。我拿一个真实做过的小项目来拆解整个实操过程——一个给博客用的图片处理服务,需求很简单:用户上传图片后生成缩略图、加水印、并托管到CDN。
传统方案需要一个常驻服务,比如一个Node.js进程监听上传请求,用Sharp库处理图片再推送到对象存储。听起来不复杂,但“常驻”两个字意味着你要维护一台服务器、处理进程守护、考虑并发瓶颈。换成Serverless之后,整个链路变成了这样:客户端直传对象存储 → 存储桶生成创建事件 → 事件触发函数 → 函数处理图片 → 结果写回存储桶 → CDN刷新。
3.1 架构选型的思路:为什么选事件触发而不是API网关
一开始我在两个方案之间纠结过:方案A是上传请求先到API网关,再进函数处理;方案B是客户端直接上传到对象存储,用存储事件触发函数。方案A好理解,但有两个问题:一是文件流经过API网关再进函数,会消耗函数执行时间和网络带宽,成本高且延迟不低;二是大文件上传会碰到函数体大小的限制,体验很糟糕。
方案B把文件传输的逻辑交给了对象存储和CDN,函数只负责“文件已经到存储桶之后”的处理——这种模式正是上面说的流处理型架构。上传走HTTP直传、处理走异步事件,两者的关注点完全分离,链路也更健壮。
从安全角度看,方案B还有一个好处:客户端直传对象存储可以用临时凭证,不需要把账户级别的密钥暴露给浏览器。我实践下来,这个方案对图片类、音视频类业务特别合适,几乎成了我给这类需求设计架构时的默认选项。
3.2 函数拆分粒度:一个函数干一件事,还是一个函数包办所有
图片处理可能包含多个环节:读取原图、生成缩略图、叠加水印、优化压缩、更新CDN缓存。是把这些环节全写在同一个函数里,还是拆成独立函数分步执行?
我的选择是拆成两个函数。原因很实际:
- 缩略图生成和加水印虽然都需要图片处理库,但缩略图对延迟要求更高、水印处理对画质参数要求更高,独立开来可以分别调整资源配置。
- 两个函数之间可以用消息队列传递进度,这样某个环节失败时可以独立重试,不会把整条链路拉死。
- 代码体积更小,冷启动更快——一个函数只加载自己依赖的库,模块加载时间明显更短。
但拆分也不是越细越好。如果你把一个函数拆成5个步骤,每步之间都靠队列或状态存储传递数据,那数据序列化、网络交互、中间存储的成本也会上升。我给自己定的经验法则是:一个函数内部逻辑的预估响应时间如果在几百毫秒内、且不会因为某个子环节失败导致整条请求无谓重跑,那就没必要拆。
3.3 冷启动问题:我用了哪些手段把P95延迟降了60%
这是Serverless绕不过去的话题。函数实例被唤醒的那一刻,平台要完成代码加载、运行时初始化、依赖解析,然后才能执行你的入口函数。这一步消耗的时间,就是冷启动延时,在小流量场景下它可能占据整个接口耗时的80%。
我的调优手段,按效果从高到低排列:
第一是缩小代码包体积。图片处理库Sharp的体积很大,我把它拆分到专门的函数里,主入口函数不加载它。去掉这些重依赖之后,函数包从几十兆降到了几兆,冷启动时间肉眼可见地缩短了。
第二是选择更轻的运行时。同样一段逻辑,用Node.js写的冷启动比用Java快得多,Python介于两者之间。在这个服务里我用Node.js,就是为了牺牲一部分运行时性能换取更快的启动速度。
第三是设置预启动并发数。主流的FaaS平台都支持配置实例的最小并发度,说白了就是平台提前预热一定数量的实例等着请求进来。这个功能能完美消灭白噪音场景下的冷启动,但也会产生持续的费用——相当于你提前租了几个“闲置”实例。我一般只在核心链路上开这个配置。
第四是省去不必要的初始化。在函数实例存活期间重复执行的初始化逻辑,尽量往后推迟,或者在模块顶层只加载最必要的依赖,其他按需require。
3.4 成本核算:一次真实调用的费用明细
很多朋友问我Serverless到底便不便宜。我拿这个图片服务一个月的真实数据来算笔账:
假设每月处理30万张图片,平均每次函数执行200毫秒,函数分配512MB内存,单次调用费用约等于(内存/1024)乘以执行时长乘以单价。不同平台单价有差异,大概单次在0.00002到0.00005元之间,一个月函数执行费用大概10到20元。再加上30万次请求的网关费用、对象存储的读写费用,总成本大约在30到50元之间。
如果换成一台2核4G的云服务器,一个月起码60到100元,这还不算负载均衡和公网IP的费用。最关键的是,服务器一个月至少有三分之二的时间是空闲的,但费用照收不误。
当然,如果流量非常稳定且持续满负荷运行,Serverless的成本优势会被压缩。比如每天24小时都在大量处理请求,包年包月的ECS确实更划算。这就是为什么我说:Serverless省钱的前提是“流量存在明显的波峰波谷”,它的计费模型本质上是让你“为实际使用付费,而不是为可能性付费”。
4. 兜住底:Serverless架构里那些躲不开的坑和排查方法
这一节是重头戏。我见过太多团队上了Serverless之后,线上出问题的时候一脸懵——日志在哪查?实例去哪了?请求怎么超时的?这些都是传统运维经验覆盖不到的盲区。我把常见问题整理成一套排查手册,按症状分类,方便你直接对症下药。
4.1 冷启动导致超时:为什么一个“偶尔失败”的请求影响了整个体验
现象:接口整体延迟正常,但总有大约1%到5%的请求特别慢,明显比其他请求慢一个数量级。
排查思路:先看是不是冷启动。在函数日志里搜索“初始化耗时”或“冷启动”字样,对比慢请求的时间分布。如果慢请求恰好发生在实例扩容之后,基本可以确定是冷启动问题。再看有没有“容器重建”的日志,如果平台频繁回收实例,就可能导致很多请求命中冷启动。
解决办法按优先级排列:
- 代码瘦身:移除重依赖、较少文件数量、用打包工具把依赖打成一个文件。
- 运行时可执行文件优化:如果是Python或Node.js,可以用压缩打包技术减少解压时间。
- 配置预启动实例:核心接口额外开销部分,直接买“预热”。
- 前端兜底:对冷启动影响比较大的接口,在客户端做超时重试,同时加一层缓存。
这里有个很重要的认知:冷启动不是“bug”,而是FaaS平台必然的副产品。你的目标不是消灭它,而是把它控制在用户可接受的范围内。如果一个请求的冷启动时间是2秒,但你整个接口的预算才800毫秒,那合适的做法不是调平台参数,而是重新思考这个接口该不该用FaaS承载。
4.2 幂等性和数据一致性问题:重复执行可能造成灾难
现象:用户上传一张图片,处理完之后发现生成了两张缩略图;或者一条订单消息被处理了两次,导致库存扣了两次。
这类问题最隐蔽,因为错误不是每次都出现,只在消息重复投递、平台重试的时候冒出来。
我的防御习惯是“不做假设、专门设计”:
- 每次函数处理业务的时候,先查一下业务主键是否已经处理过——可以用数据库的唯一约束,也可以用缓存记录处理过的消息ID。
- 写入操作尽量采用“条件更新”而不是“先读后写”:比如更新库存时用“库存数量减1”这种原子操作,避免先获取再设置的模式。
- 对于同一条消息,函数的重试间隔和最大次数要提前设计好,宁可少重试几次也不无限重试。
幂等性问题其实是Serverless架构下面最难啃的硬骨头。无状态、并发执行、平台自动重试,这些特性结合在一起,要求你从设计之初就把“同一份数据可能被处理多次”当成默认前提。
4.3 连接数爆炸:数据库连接池在Serverless里最容易翻车
现象:流量上来之后,数据库报“too many connections”,或者后端服务开始大量超时。
传统架构里,你一台应用服务器可能维持几十个数据库连接,这些连接可以复用。但Serverless为了提升并发处理能力,会同时拉起大量函数实例,每个实例都会各自建立数据库连接。如果每实例连接数是10,并发100个实例那就是1000个连接——任何一个数据库都很难撑住这种连接风暴。
我踩过一次很深的坑:一个报表服务用到MySQL,函数每次调用都会新建连接,结果压测到200并发的时候,数据库直接拒绝连接。后来的补救方案是:
- 使用连接池代理组件(比如各种云数据库自带的代理),把函数实例的短连接统一收敛成后端的持久连接池。
- 函数内部也做轻量级连接复用——虽然多个实例之间不能共享,但同一个实例的连续多次调用是可以复用连接的,我把连接提取到模块顶层,效果很明显。
- 数据库连接的最大空闲时间调短,避免大量空闲连接堆积。
这里也牵出一个更深层的问题:你有多少后端资源能撑住Serverless的“无限并发”?数据库有连接上限、第三方API有速率限制、支付接口有并发要求——这些外部依赖都可能成为Serverless伸缩的“隐形天花板”。做架构的时候,一定要把这些限制考虑进去,否则你的函数是弹性了,数据库却崩了。
4.4 调试与观测:Serverless排障为什么难,怎么破
传统服务你可以ssh登录服务器看日志、看监控、甚至用调试器attach上去。Serverless模式下,实例在你无法访问的隔离环境中运行,日志分散在各个请求上下文里,排查问题的思路必须转变。
我给新团队的排障三板斧:
第一,日志必须结构化。每个日志都要包含请求ID、函数名、业务流水号,方便跨函数串联。平台自带的日志检索能力往往不够,建议直接把日志推送到统一的日志平台,按请求ID聚合搜索。
第二,全链路追踪必须从第一天就接好。每个函数通过SDK上报调用链的span信息,配合平台的全链路追踪面板,能够清晰地看到请求从API网关到函数A再到函数B再到数据库的完整调用链。
第三,回归测试要火力全开。Serverless环境部署完就是一个独立版本,我习惯在代码合并后就自动触发一次全量回归测试,包含接口正确性、幂等性、延迟指标。一旦有异常,马上在开发环境复现。
说实话,Serverless带来的可观测性挑战,是很多团队低估的一环。传统监控告警体系上了Serverless之后经常“失明”——你看见请求失败,但不知道具体是哪个函数、哪一次调用、哪个环节出的问题。所以我的建议是:不要等出了问题再搭观测体系,从架构设计第一天就把它当核心需求对待。
5. 选型决策:你的业务到底适合哪种Serverless模式
讲完了架构模式、实操细节和避坑经验,最后这一节把话题拉回决策层面。毕竟我们的最终目的是回答那个最朴素的问题:我的项目,该不该用Serverless,该用哪种模式。
5.1 适合与不适合:一张多维判断表
我给团队做技术方案评审时,通常用下面这几个维度来判断一个模块适不适合Serverless:
适合度高的特征:
- 流量弹性明显:波峰波谷差距数倍以上,甚至会有突发性尖峰。
- 状态极少或无状态:业务逻辑不依赖本地文件、内存缓存、本机会话。
- 调用频率分布不均:大量时间空闲,偶尔密集调用。
- 开发部署频率高:希望快速上线、频繁迭代。
适合度低的特征:
- 延迟极其敏感:比如交易系统对接口延迟有硬性要求,需要稳定在几十毫秒内。
- 长时运行任务:任务本身要跑几分钟甚至几小时,函数超时时间根本无法满足。
- 强依赖本地状态:比如需要长时间维护大量内存映射或本地文件缓存。
- 重CPU计算:会在高配置实例上长时间运转,离线计算比在线函数更划算。
5.2 从单体到Serverless的演进路径
如果你手上有一套传统的单体服务,不要想着一次性把它拆成一堆函数。我建议分阶段演进:
先挑出那些“游离于核心业务之外的边缘功能”,比如文件处理、定时报表、Webhook接收、图片压缩、通知推送——这类功能先独立成函数。因为它们和核心交易链路耦合度低,引入Serverless的风险小、收益体现快,也能让团队积累经验。
等团队对Serverless的运维、排障、发布流程都很熟悉之后,再考虑对流量的核心入口做改造。但即便是核心入口,也不要全量替换,最好以API网关做切换层,保留旧系统作为兜底,灰度放流量。
最后一个阶段是“全链路Serverless化”,这个时候你的业务形态大概率是:BaaS托管了数据层、消息层、存储层,FaaS承载了所有计算逻辑,工作流引擎串联了复杂业务。这样一套体系,团队规模可以很小,但系统的伸缩能力和交付速度非常可观。
5.3 给团队和个人的建议:什么时候“梭哈”,什么时候“再想想”
我做技术决策有一个坚持了多年的原则:技术选型从来不只是技术问题,还取决于团队状态和业务阶段。
创业初期、快速验证想法的阶段,SPA(单页应用)+ Serverless后端是效率最高的组合。你可以把90%的时间花在业务逻辑和产品设计上,基础设施的复杂性被平台吸收,这是Serverless最划算的时期。
业务进入稳定期、流量有了清晰形态之后,有必要做一次“Serverless/非Serverless”的成本对比评估。如果一年下来的账单明显高于同等性能的包年包月资源,并且团队有成熟的运维能力,可以考虑把稳定流量的部分迁回容器服务,只保留弹性区域的函数。这是一种很务实的“混合运营”策略,我在好几个项目里就这么调整过。
至于团队的技术债问题——如果存量系统代码质量很差、模块边界混乱,我不建议在迁移到Serverless的同时顺手重构。一次只做一件高风险的事。先把代码按边界理清楚,再谈技术栈升级,否则你会同时面对“业务逻辑理解不清楚”和“新架构特性不熟悉”两个难题。
回到我自己做项目的经验:Serverless最打动我的不是“省了服务器费用”,而是它逼着我把系统设计得更简洁、更标准化、更面向失败——无状态设计、幂等操作、自动化观测,这些恰恰是分布式系统设计里最核心的能力。哪怕你以后不用Serverless了,这些思维方式也会让你写出的代码更健壮。
如果你正在看这篇文章并犹豫要不要尝试,我的建议是:不用想太深,先挑一个低风险、可灰度的小功能,用其中一种架构模式把它落了地,跑一个月看数据、看成本、看团队感受,再决定要不要扩大边界。纸上谈兵永远没有真刀真枪的实践来得可靠。