Serverless(无服务器架构)已经越来越成熟了,我在几个项目中使用了Serverless架构,踩了很多坑,也积累了很多实战经验。今天,分享一下Serverless架构的踩坑总结和实战经验,包括冷启动、性能优化、成本控制、调试部署、安全等方面,希望对正在使用或者准备使用Serverless的朋友有所帮助。

先说说我和Serverless的故事。我第一次接触Serverless,是在两年前,那时候Serverless还比较新,生态也不够完善,很多人对它持怀疑态度。但我被它的理念吸引了:不需要管理服务器,按需付费,自动扩缩容,开发者只需要关注业务逻辑,不需要关心基础设施。这对于小团队和个人开发者来说,太有吸引力了。

于是,我开始在一些小项目中尝试Serverless架构,比如一些工具类应用、API服务、定时任务等。最开始,踩了很多坑,比如冷启动慢、调试困难、成本超预期、厂商锁定等,有几个项目甚至因为这些问题,差点放弃了Serverless。

但我没有放弃,而是不断地学习、摸索、优化,慢慢地,我掌握了Serverless架构的最佳实践,踩过的坑也越来越少。现在,我已经在好几个生产项目中使用了Serverless架构,运行得很稳定,成本也比传统的服务器架构低了很多。

今天,就把这些年踩过的坑和积累的实战经验,分享出来。

一、Serverless的优势和适用场景

在说踩坑之前,先说说Serverless的优势和适用场景,帮助大家判断,你的项目适不适合用Serverless。

1. Serverless的优势

Serverless架构,有很多传统架构没有的优势:

  • 不需要管理服务器:开发者不需要关心服务器的购买、配置、运维、扩容等,只需要写业务代码,部署到Serverless平台就行。这大大降低了运维成本,让开发者可以更专注于业务逻辑。
  • 自动扩缩容:Serverless平台会根据请求量,自动扩缩容,请求多的时候,自动增加实例,请求少的时候,自动减少实例,甚至缩到零。这意味着,你不需要担心流量突增导致服务器崩溃,也不需要在流量低的时候浪费服务器资源。
  • 按需付费:Serverless是按实际使用量付费的,比如按请求次数、按执行时间、按内存使用量等。没有请求的时候,不收费。这对于流量不稳定的应用来说,成本比传统的服务器架构低很多,因为传统架构,即使没有流量,服务器也要一直开着,也要花钱。
  • 开发效率高:Serverless架构,把很多通用的功能都封装好了,比如API网关、身份认证、数据库、消息队列、存储等,开发者可以直接使用,不需要自己搭建,大大提升了开发效率。
  • 高可用:Serverless平台,一般都有很高的可用性,多可用区部署,自动故障转移,不需要开发者自己做高可用。

2. Serverless的适用场景

Serverless虽然有很多优势,但不是所有场景都适合。它有自己的适用场景:

  • API服务:Serverless非常适合做API服务,尤其是请求量不稳定、有明显峰谷的API服务。API网关 + 函数计算 + 数据库,就能快速搭建一个高可用、自动扩缩容的API服务。
  • 定时任务:Serverless非常适合做定时任务,比如每天凌晨跑的数据统计、报表生成、数据备份等。只需要配置定时触发器,到时间自动执行,执行完就释放,成本很低。
  • 事件驱动的应用:Serverless非常适合事件驱动的应用,比如文件上传后自动处理、消息队列消费、Webhook处理等。事件来了,触发函数执行,执行完就释放,非常高效。
  • 工具类应用:一些小的工具类应用,比如图片处理、PDF转换、二维码生成等,非常适合用Serverless,请求量不大,但需要高可用,Serverless成本低,运维少,很合适。
  • 初创项目和MVP:初创项目和MVP,需求变化快,流量不确定,用Serverless,可以快速开发上线,成本低,不需要担心服务器的问题,等项目起来了,再考虑要不要迁移到传统架构。

3. Serverless不适合的场景

当然,Serverless也不是银弹,有些场景不适合用Serverless:

  • 长时间运行的任务:Serverless函数,一般都有执行时间限制,比如最多15分钟,超过就会被强制终止。所以,长时间运行的任务,比如视频渲染、大数据处理等,不适合用Serverless。
  • 对延迟非常敏感的应用:Serverless有冷启动的问题,虽然可以通过一些方法优化,但还是会有一定的延迟。如果你的应用对延迟非常敏感,比如实时通信、游戏服务器等,可能不太适合用Serverless。
  • 需要长连接的应用:Serverless函数,执行完就释放了,不适合维持长连接,比如WebSocket、Socket.io等。虽然有些平台支持WebSocket,但体验不如传统架构。
  • 有状态的应用:Serverless函数是无状态的,状态需要存在外部存储里,比如数据库、缓存等。如果你的应用有很多状态,而且状态访问很频繁,用Serverless可能会比较麻烦,性能也可能不好。
  • 流量非常稳定且很大的应用:如果你的应用流量非常稳定,而且很大,用传统的服务器架构,可能成本更低,因为Serverless的单价,在高流量下,可能比服务器贵。

了解了Serverless的优势和适用场景,再来说说我踩过的坑。

二、踩坑一:冷启动慢,用户体验差

冷启动,是Serverless最常见的坑,也是最影响用户体验的问题。

什么是冷启动呢?就是当一个函数很长时间没有被调用的时候,Serverless平台会把这个函数的实例释放掉,节省资源。当有新的请求进来的时候,平台需要重新创建一个实例,加载代码,初始化运行环境,然后才能执行函数。这个过程,就是冷启动,可能需要几百毫秒到几秒的时间,用户会感觉到明显的延迟。

我最开始用Serverless的时候,就踩了这个坑。有一个API服务,用的是Serverless架构,用户访问的时候,经常会感觉很慢,尤其是第一次访问的时候,要等好几秒才能响应。用户投诉了很多次,说我们的API太慢了。

后来排查发现,就是冷启动的问题。因为我们的API请求量不大,有明显的峰谷,白天请求多,晚上和凌晨请求少,函数实例经常被释放,每次有新请求进来,都要冷启动,所以很慢。

针对冷启动的问题,我试了很多方法,总结了以下几个优化方案:

1. 保持实例常驻(预热)

最简单的方法,就是保持函数实例常驻,不要让它被释放。可以用定时任务,每隔几分钟,调用一次函数,让函数实例一直保持活跃,不会被释放。这样,就不会有冷启动了,每次请求都是热实例,响应很快。

但这个方法,有一个缺点,就是会增加成本,因为函数实例一直常驻,即使没有请求,也要占用资源。不过,对于请求量不大但对延迟敏感的应用来说,这点成本是值得的。

我用的就是这个方法,配置了一个定时任务,每隔5分钟调用一次函数,保持实例常驻。这样,冷启动的问题就解决了,API的响应时间,从平均2-3秒,降到了200毫秒以内,用户体验提升了很多。

2. 减少函数的初始化时间

冷启动的时间,很大一部分是花在函数的初始化上,比如加载依赖、连接数据库、初始化配置等。减少初始化时间,就能减少冷启动的时间。

优化的方法:

  • 减少依赖:只引入必要的依赖,不要引入整个库,比如用lodash,只引入需要的函数,不要引入整个lodash。依赖越少,加载越快。
  • 延迟初始化:把不是马上需要的初始化,延迟到第一次使用的时候再做,比如数据库连接,可以在第一次查询的时候再建立,不要在函数启动的时候就建立。
  • 全局复用:把可以复用的东西,放在函数外面,全局初始化,比如数据库连接、HTTP客户端、配置等,这样,实例复用的时候,不需要重新初始化,只需要第一次冷启动的时候初始化一次。
  • 优化代码体积:压缩代码,去掉无用的代码,减少代码体积,加载更快。

3. 选择合适的运行时和内存

不同的运行时,冷启动时间不一样。一般来说,解释型语言(比如Python、Node.js)的冷启动时间,比编译型语言(比如Go、Rust)长,因为解释型语言需要启动解释器,加载运行时。

如果对冷启动要求很高,可以考虑用Go或者Rust来写函数,冷启动时间会短很多。

另外,内存大小也会影响冷启动时间,内存越大,CPU越强,冷启动越快。因为Serverless平台,是按内存比例分配CPU的,内存越大,CPU越强,初始化越快。所以,如果冷启动慢,可以适当增加函数的内存,虽然成本会高一些,但冷启动会快很多。

4. 使用预置并发

现在,很多Serverless平台,都支持预置并发(Provisioned Concurrency),就是预先创建一定数量的函数实例,保持常驻,这样,请求进来的时候,直接用预置的实例,不需要冷启动。

预置并发,比自己用定时任务预热更稳定,因为平台会保证预置的实例一直存在,不会被释放。当然,预置并发是要收费的,而且比普通的调用贵一些,但对于对延迟要求高的应用来说,是值得的。

我的那个API服务,后来也用了预置并发,预置了2个实例,这样,不管什么时候,都有2个热实例在等着,请求进来,直接响应,延迟非常低,用户体验很好。

三、踩坑二:调试困难,出了问题很难排查

Serverless的调试,比传统的服务器架构困难很多,因为函数运行在云端,你不能像在本地一样,随便打断点,看日志,查状态。我最开始用Serverless的时候,调试非常痛苦,出了问题,不知道怎么排查,只能加日志,重新部署,看日志,反复试,效率很低。

后来,我摸索出了一些调试的方法和工具,调试效率提升了很多。

1. 本地调试

最好的调试方法,还是在本地调试,因为本地可以打断点,可以单步执行,可以看变量,非常方便。

现在,大部分Serverless平台,都提供了本地调试的工具,比如AWS的SAM CLI、阿里云的Funcraft、腾讯云的Serverless Framework等,可以在本地模拟Serverless环境,运行函数,调试代码。

我一般是在本地把代码写好,调试好,确保没有问题了,再部署到云端。这样,大部分问题,在本地就解决了,不需要到云端去调试。

2. 完善的日志

虽然本地调试能解决大部分问题,但有些问题,只有在云端环境才会出现,比如网络问题、权限问题、第三方服务的问题等。这时候,日志就非常重要了。

我在写函数的时候,会加很完善的日志,包括:

  • 函数的入口和出口,记录请求参数和返回结果。
  • 关键步骤的日志,记录每一步做了什么,结果是什么。
  • 错误日志,记录错误的详细信息,包括错误类型、错误信息、堆栈等。
  • 性能日志,记录关键步骤的耗时,方便排查性能问题。

有了完善的日志,出了问题,就能很快通过日志,定位到问题所在,知道是哪一步出了问题,为什么出问题。

另外,很多Serverless平台,还提供了日志查询和分析的工具,可以按时间、按关键词、按级别查询日志,还可以做日志分析,很方便。

3. 分布式追踪

对于复杂的Serverless应用,一个请求可能会经过多个函数、多个服务,出了问题,很难知道是哪个环节出了问题。这时候,分布式追踪就很重要了。

分布式追踪,可以把一个请求在各个服务中的调用链,都记录下来,包括每个服务的耗时、返回结果、错误信息等。出了问题,通过调用链,就能很快定位到是哪个服务出了问题,耗时在哪里。

现在,很多Serverless平台,都集成了分布式追踪的功能,比如AWS X-Ray、阿里云的链路追踪等,只需要简单配置,就能使用。

4. 灰度发布和回滚

Serverless的部署,虽然很方便,但也可能会部署出问题的代码,导致线上故障。所以,灰度发布和回滚机制,非常重要。

灰度发布,就是先把新版本部署到一小部分流量上,观察有没有问题,如果没问题,再逐步扩大流量,直到全量发布。如果有问题,马上回滚到旧版本,减少影响。

现在,很多Serverless平台,都支持灰度发布和版本管理,可以很方便地发布新版本,灰度流量,回滚版本。

我一般是先部署到测试环境,测试没问题了,再部署到生产环境,而且是灰度发布,先放10%的流量,观察一段时间,没问题再放50%,再放100%。如果出了问题,马上回滚,把影响降到最低。

四、踩坑三:成本超预期,比服务器还贵

很多人用Serverless,是因为觉得它便宜,按需付费,没有请求不收费。但实际上,如果用得不好,Serverless的成本可能比传统的服务器架构还贵。我就踩过这个坑,有一个项目,用了Serverless之后,月底一看账单,比之前用服务器还贵,吓了一跳。

后来,我仔细分析了账单,发现了成本高的原因,也总结了一些成本优化的方法。

1. Serverless成本高的原因

Serverless的成本,主要由以下几个部分组成:

  • 请求次数:按调用次数收费,调用越多,费用越高。
  • 执行时间:按执行时间收费,执行时间越长,费用越高,一般是按100毫秒为单位计费。
  • 内存使用量:按内存大小收费,内存越大,费用越高。
  • 其他服务费用:比如API网关、数据库、存储、消息队列等,这些服务也是按使用量收费的。

成本高的常见原因:

  • 函数执行时间太长:函数执行时间长,费用就高。比如,一个函数执行10秒,比执行100毫秒,贵100倍。
  • 内存设置太大:内存设置太大,费用就高。比如,1G内存的函数,比128M内存的,贵8倍。
  • 请求次数太多:有些不必要的请求,也调用了函数,增加了请求次数。
  • 没有合理利用缓存:每次请求都重新计算,或者重新查数据库,没有利用缓存,增加了执行时间和数据库的费用。
  • 其他服务的费用超预期:比如数据库的费用、存储的费用、API网关的费用等,加起来也不少。

2. 成本优化的方法

针对这些原因,我总结了以下成本优化的方法:

  • 优化函数执行时间:这是最有效的成本优化方法。优化代码,减少不必要的计算,优化数据库查询,减少IO操作,让函数执行得越快越好。执行时间减少一半,成本就减少一半。
  • 合理设置内存:不要把内存设置得太大,根据函数的实际需求,设置合适的内存。可以做一些测试,看看函数在不同内存下的执行时间和成本,找到性价比最高的内存配置。有时候,增加内存,CPU更强,执行时间更短,总成本反而更低。
  • 减少不必要的请求:在API网关层,做一些过滤,比如限流、鉴权、参数校验等,把无效的请求挡在外面,不要让它们调用函数。
  • 合理利用缓存:把不经常变化的数据,缓存起来,比如用Redis、或者平台提供的缓存服务,减少数据库查询和计算,降低执行时间和数据库费用。
  • 使用数据库连接池:数据库连接的建立和销毁,很耗时,也很耗资源。使用数据库连接池,复用连接,减少连接建立的开销,降低执行时间。
  • 优化其他服务的使用:比如数据库,选择合适的规格,不要买太大的;存储,设置生命周期,把不常用的数据转到低频存储;API网关,合理设置缓存,减少后端调用。
  • 设置预算告警:在平台上设置预算告警,当费用达到预算的一定比例时,发送告警,及时发现异常的费用增长,避免月底看到账单才吓一跳。

通过这些优化,我的那个项目,成本降低了60%多,比用服务器还便宜了很多。

五、踩坑四:厂商锁定,迁移困难

Serverless架构,还有一个很大的坑,就是厂商锁定。不同的Serverless平台,API、触发器、事件模型、工具链,都不一样,你在一个平台上写的函数,很难直接迁移到另一个平台上。如果哪天你想换平台,或者想多云部署,就会非常困难。

我最开始用的是一个平台的Serverless,后来因为成本和功能的原因,想换到另一个平台,结果发现,代码、配置、工具链,都要改,几乎相当于重写,花了很多时间和精力。从那以后,我就很注意避免厂商锁定的问题。

避免厂商锁定的方法:

1. 使用开源的Serverless框架

现在,有一些开源的Serverless框架,比如Serverless Framework、Kubeless、OpenFaaS等,可以在不同的平台上运行,甚至可以在自己的Kubernetes集群上运行。用这些框架,可以在一定程度上避免厂商锁定,因为你的代码和配置,是基于框架的,不是基于某个厂商的,换平台的时候,只需要改一下平台的配置,不需要改代码。

我现在用的就是Serverless Framework,它支持很多平台,比如AWS、阿里云、腾讯云、华为云等,一套代码,可以部署到不同的平台,非常方便。

2. 抽象平台相关的代码

即使使用了开源框架,有些平台相关的功能,还是需要直接调用平台的API。这时候,要把这些平台相关的代码,抽象出来,封装成统一的接口,业务代码只调用统一的接口,不直接调用平台的API。这样,换平台的时候,只需要改抽象层的实现,不需要改业务代码。

比如,对象存储,不同平台的API不一样,我就封装了一个统一的存储接口,有上传、下载、删除等方法,然后针对不同的平台,做不同的实现。业务代码只调用统一的接口,不管底层是哪个平台的存储。

3. 尽量使用通用的标准

在选择技术栈和服务的时候,尽量使用通用的标准,而不是某个厂商特有的功能。比如,数据库用MySQL或者PostgreSQL,不要用厂商特有的数据库;消息队列用Kafka或者RabbitMQ,不要用厂商特有的消息队列;缓存用Redis,不要用厂商特有的缓存。这样,迁移的时候,就容易很多。

4. 做好多云部署的准备

如果担心厂商锁定,可以做多云部署的准备,把应用设计成可以在多个云上运行的。当然,多云部署的成本和复杂度会高一些,但可以避免厂商锁定,也可以提高可用性。

不过,对于大部分小团队和个人开发者来说,完全避免厂商锁定,是不现实的,成本太高了。我的建议是,在成本可控的范围内,尽量减少厂商锁定,比如用开源框架,抽象平台相关的代码,使用通用的标准。这样,即使以后要迁移,也不会太困难。

六、踩坑五:安全问题,不容忽视

Serverless架构,虽然平台会负责基础设施的安全,但应用层面的安全,还是需要开发者自己负责。我在使用Serverless的过程中,也遇到过一些安全问题,比如权限过大、注入攻击、敏感信息泄露等。

Serverless架构的安全,需要注意以下几点:

1. 最小权限原则

Serverless函数,一般都需要访问其他服务,比如数据库、存储、消息队列等,这就需要给函数配置权限。很多人为了方便,给函数配置了很大的权限,甚至管理员权限,这是非常危险的。一旦函数被攻破,攻击者就能获得很大的权限,造成严重的后果。

所以,一定要遵循最小权限原则,给函数只配置它需要的最小权限。比如,函数只需要读某个存储桶,就只给读权限,不要给写权限;函数只需要查询某张表,就只给查询权限,不要给修改和删除权限。

2. 输入验证和注入防护

Serverless函数,一般都是通过API网关或者事件触发的,输入是不可信的,一定要做严格的输入验证,防止注入攻击,比如SQL注入、命令注入、XSS等。

不要把用户输入直接拼接到SQL语句里,要用参数化查询;不要把用户输入直接拼接到命令里,要用安全的API;不要把用户输入直接输出到页面上,要做转义。

3. 敏感信息保护

函数中经常会用到一些敏感信息,比如数据库密码、API密钥、Token等,这些信息,不能硬编码在代码里,也不能提交到代码仓库里。要用平台提供的密钥管理服务,或者环境变量,来管理这些敏感信息,函数运行的时候,再动态获取。

另外,日志中也不要打印敏感信息,比如密码、Token、身份证号等,避免敏感信息通过日志泄露。

4. 依赖安全

函数的代码,一般会依赖很多第三方库,这些第三方库,可能存在安全漏洞。要定期检查依赖的安全漏洞,及时更新有漏洞的依赖。可以用一些工具,比如npm audit、pip-audit等,自动检查依赖的安全漏洞。

另外,不要使用来源不明的依赖,只使用官方源或者可信源的依赖,避免恶意代码。

七、Serverless的最佳实践

总结了这么多踩坑经验,最后,再说说Serverless架构的一些最佳实践,帮助大家少踩坑。

1. 函数设计要小而精

Serverless函数,要设计得小而精,每个函数只做一件事情,遵循单一职责原则。不要把所有的逻辑都写在一个函数里,函数太大,会导致冷启动慢、调试困难、部署慢、成本高。

把大的功能,拆分成多个小函数,每个函数负责一个小功能,通过事件或者API调用,组合起来完成大的功能。这样,每个函数都很小,冷启动快,调试容易,部署方便,成本也低。

2. 函数要无状态

Serverless函数,是无状态的,因为实例可能会被随时释放,也可能会创建新的实例,状态不能存在函数内部。状态要存在外部存储里,比如数据库、缓存、对象存储等。

不要在函数内部用全局变量存状态,因为不同的请求,可能会打到不同的实例上,状态不一致。也不要假设实例会一直存在,实例可能随时被释放。

3. 处理好重试和幂等

Serverless平台,为了保证可靠性,可能会对函数进行重试,比如函数执行失败了,或者超时了,平台可能会重新调用一次。这就意味着,同一个函数,可能会被执行多次。

所以,函数一定要设计成幂等的,也就是执行多次和执行一次,结果是一样的。比如,创建订单的函数,要检查订单是否已经存在,避免重复创建;发送消息的函数,要检查消息是否已经发送过,避免重复发送。

另外,要处理好重试的逻辑,对于可以重试的错误,比如网络错误、临时的服务不可用,可以重试;对于不可以重试的错误,比如参数错误、业务逻辑错误,就不要重试,直接返回错误。

4. 做好异常处理

Serverless函数,一定要做好异常处理,不要让函数因为未捕获的异常而崩溃。所有可能出错的地方,都要加try-catch,捕获异常,记录日志,返回友好的错误信息。

另外,要设置合理的超时时间,根据函数的实际执行时间,设置合适的超时,不要设置得太短,导致函数正常执行也被超时终止;也不要设置得太长,导致函数卡住的时候,浪费资源,增加成本。

5. 做好监控和告警

Serverless架构,虽然平台会负责基础设施的监控,但应用层面的监控,还是需要开发者自己做。要监控函数的调用次数、执行时间、错误率、成功率等指标,设置告警,当指标异常的时候,及时收到通知,及时处理。

很多Serverless平台,都提供了监控和告警的功能,可以很方便地配置。也可以用一些第三方的监控工具,做更详细的监控和分析。

八、写在最后

Serverless架构,是云计算发展的趋势,它让开发者不需要管理服务器,只需要关注业务逻辑,大大降低了运维成本,提升了开发效率。而且,按需付费,自动扩缩容,对于流量不稳定的应用来说,成本也比传统的服务器架构低很多。

但Serverless也不是银弹,它有自己的适用场景,也有很多坑,比如冷启动、调试困难、成本超预期、厂商锁定、安全问题等。只有了解了这些坑,掌握了最佳实践,才能用好Serverless,发挥它的优势,避免它的劣势。

我用Serverless已经两年多了,从最开始的踩坑无数,到现在的得心应手,积累了很多经验。今天分享的这些踩坑总结和实战经验,都是我亲身经历的,希望对正在使用或者准备使用Serverless的朋友有所帮助。

当然,Serverless技术还在快速发展,新的功能、新的工具、新的最佳实践,不断涌现。我们也要不断学习,不断更新自己的知识,跟上技术的发展。

最后,想说的是,技术没有好坏,只有适合不适合。Serverless也好,传统的服务器架构也好,都有自己的优势和劣势,根据自己的项目需求,选择合适的架构,才是最重要的。

如果有什么问题或者不同的看法,欢迎在评论区交流。