Serverless/Faas/BaaS 等概念在这几年的技术圈中是绝对的热点词汇之一,国内外众多云厂商也纷纷推出自家的 Serverless 和函数计算产品,微信也依托腾讯云推出了基于 Serverless 和 FaaS 理念的「小程序·云开发」,对应的新型岗位也不断涌现。

如今,我们确实看到很多中小型业务在拓展新领域、开发新功能时会优先考虑使用 Serverless 产品作为后端架构基建,但却鲜有见到面向 C 端的一线大厂中,有核心业务在 Web 后端层面改为采用 Serverless 作为开发模式,这又是何缘由呢?

什么是Serverless

作为业界 Serverless 和 FaaS 领域的先锋,Amazon 的 Serverless 产品页面对 Serverless 作出了这样的定义:

无服务器是一种用于描述服务、实践和策略的方式,使您能够构建更敏捷的应用程序,从而能够更快地创新和响应变化。凭借无服务器计算,容量预置和补丁等基础设施管理任务由 AWS 处理,以便您能够专注于编写为客户服务的代码。AWS Lambda 等无服务器服务具有自动扩展、内置高可用性以及按价值付费的计费模型。Lambda 是一种事件驱动的计算服务,使您能够运行代码来响应来自 200 多个本地集成的 AWS 和 SaaS 源的事件 — 所有这些都无需管理任何服务器。 

根据 Amazon 的定义及各个厂商和业务对 Serverless 的实践,我们可以进一步将 Serverless 的特点归纳如下:

  • 解耦开发与运维:业务只需要关心自身的业务实现,不需要关心机器情况,也无需进行复杂的基础组件配置甚至环境搭建。
  • 弹性:借助于近几年容器技术的发展,可实现毫秒级快速扩容,并可根据流量弹性扩缩容。
  • 按需付费:借助弹性伸缩能力,使用者无需大量冗余机器,按需扩缩容,根据请求量等指标快速冷启动,进而按需付费降低成本。
  • 精细计费:理想情况下 Serverless 架构中的每个原子应用都应该是基本无副作用的函数,各函数可各自弹性扩展,进而实现函数级别的细粒度计费。

极速扩容并不极速

借助于容器技术的发展,Serverless 可做到毫秒级别的新容器弹出,如果是 Runtime 类型的实现,函数初始化速度还会更快,云层面的「极速扩容」是可行且实用的。

然而在应用层面,虽然 Web 后端服务本身通常是无状态的,但为了根据请求返回对应的业务数据,应用还要与有状态的 DB 、Redis 等持久层进行交互,内网的微服务间也需要相互连接以进行通信,这通常都是基于 TCP 长连来进行的。而由于创建连接本身就是一件很耗时的事情,再加上有的组件为了避免建连过程中的并发问题采用了串行建连,后端为了提升性能、避免服务因建连而导致的卡顿通常会在应用初始化过程中、向外提供服务前就通过建立一批预留连接的方式来池化连接,再加上 Java 类应用本身还有 JVM 即时编译器需要逐步优化字节码导致的初启动时性能差的问题,Web 后端应用的启动常常至少是分钟级别的,在这样的耗时比较下,毫秒级的容器或函数初始化能力只能说一定程度上优化了应用启动的耗时,却不能称其为解决了整个链路的快速扩容问题。 试想如果 C 端的 Web 后端服务完全依赖 Serverless 的弹性伸缩,不再根据预估的流量变化进行机器冗余,甚至流量到来时才冷启动函数,那么在秒杀、其他系统雪崩带来的流量大幅上涨的情况下,一旦出现扩容速度慢于流量上涨速度的情况,服务将被瞬间打垮。

另一方面,按用量扩容的逻辑通常会有并发度的限制,以便于扩容时可做到尽量准确进而尽量贴近按需付费的设计。但不同类型服务的监控项和监控阈值不尽相同,平台很难不依赖业务压测报告就能智能给出准确结论。而在突发流量的场景下,按并发度扩容受限于上文提到的应用部署时间长的问题,很有可能出现扩容速度追赶不上流量上涨速度的问题。

按需付费难以实践

结合极速扩容在实际后端场景的局限性,中大型 Web 后端服务往往会更倾向于仍然按传统的「人工评估流量峰值」、「压测确定单机性能」、「按压测结论提前手动扩容进行冗余」和「回归压测集群性能是否可满足目标」的流程来进行流量运维。但同时,后端通常对弹性扩缩容接受度很高,因此会在手动扩容的同时接入 Serverless 的弹性伸缩能力来保证极端流量发生时有扩容的兜底手段。也就是说,很多场景下,后端仍要和过去一样冗余机器来应对流量,成本很难实质性地被降低。而如果公司的基建是自建的云体系,那么弹性伸缩意味着需要预留机器资源池用于扩容,所以整体的实际成本相较传统方式可能还会有所上升。

中大型Web后端逻辑难以真正函数化

在 C 端业务发展的初期,从 MVP 到产品全量上线,后端逻辑大多面临着两天一小改、三天一大改的需求迭代状态,所以初期的代码结构、架构设计都很难做到足够合理化。而如果产品能存活到稳定期,那么在没有明确的性能、扩展性问题的情况下,大部分团队都只会选择以打补丁的方式进行迭代而非大范围重构优化。这样一来,合理的函数化微服务设计对于存量服务来说也就很难落地。

那么如果在业务创建伊始就采用函数化的设计呢?我们知道 FaaS 在后端领域通常意味着更加精细的微服务拆分设计。在微服务大行其道的当今,很多业务会采用 DDD 的思想进行领域划分进而自然而然地形成自洽的微服务体系。但微服务间调用的耗时增加、可用率低于单体服务、调用关系需要配置复杂的熔断降级策略等问题都是客观存在的,微服务体系实际上是在性能和架构合理性之间找寻平衡点。如果将微服务再进一步拆分为更加细化的函数,那么虽然架构的合理性可能得以进一步提升,但业务整体的性能和稳定性问题的复杂程度将会有一个数量级的上升。

既然将微服务进一步拆分为函数难以落地,那直接以原有的微服务作为函数呢?这样确实可行,可是传统架构依赖容器技术的进步已经足以支撑此类业务的发展,Serverless 的优势将只剩下运维层面的便捷,但上文中也讲到所谓的运维优势在 C 端场景中很难发挥实际作用,后端业务又有什么特别的理由必须选择 Serverless 呢?

开发模式的巨大差异

对于 Web 后端常用的 Java 体系来说,Spring 已经成为了既定的标准,而目前的 FaaS 实现通常需要在代码中配置触发器消息的消费者来实现功能定义,同时常常需要搭配 Runtime SDK 负责应用启动,这与传统的 SpringMVC 等组件的开发思路差异较大,除了需要开发人员改变开发习惯、积累新的开发经验外,存量的服务也将面临着巨大的改造成本。

即使脱离语言的特定场景,不同云厂家提供的 SDK 也不尽相同,这也就意味着更换服务商需要修改代码逻辑、重新测试回归功能。而对于能靠自身能力提供一揽子 Serverless 方案的大厂,其 SDK 在可预见的未来不会有根本上的变化。可一旦出于性能等问题的考虑,服务需要迁回 PaaS 类架构,或未来技术革新有了 FaaS 理念的替代者,那么工程师们所要面对的改造成本仍然十分巨大,如若处理不好,当年对前沿技术的实践就可能会变为沉重的技术债。

Serverless真的无法应用于传统后端吗?

对于 Serverless 的适用场景,阿里云的文档给出了如下理解:

  1. 事件触发的计算
  2. 实时视频广播的弹性调整大小
  3. 物联网数据处理
  4. 共享交付调度系统

可见,目前商用的使用场景大多集中在对响应耗时不敏感或完全异步的场景,那么传统的 Web 后端项目真的就无法适应 Serverless 架构了吗?

相比起传统的 PaaS 架构主要针对的是运维资源交付场景,FaaS 需要开发者显式地参与到函数的编写、配置和部署流程中,因此 FaaS 实际上是包含面向研发开放的理念的。既然如此,我们或许应该减少对于 Serverless 运维层面优势的讨论,聚焦于开发层面的问题解决。

函数内联

目前的 FaaS 平台中,函数定义大多与服务无异,函数间的调用仍然要通过网络调用进行。而如果我们将 FaaS 作为后端微服务的进阶状态,我们拆分的函数功能必然会更加干练,上下游依赖也会更加繁多,只将其作为独立服务向外提供功能就意味着必然会造成耗时的上涨、稳定性的降低,同时被加长了的调用链路也会提升维护难度。因此如果可以将云中的函数与代码中的函数的定义打通,向基础函数提供进程内通信方案,那么后端进行函数化改造的顾虑将会低很多,可行性也会有质的提升。

函数结构化

在微服务的架构下,每个服务的上下游依赖梳理向来都是件耗时耗力的工作。而如果将微服务拆分为更小的函数,那么虽然我们解决了哪些逻辑该拆为微服务、拆多少个微服务的问题,但梳理子域中数量庞大的函数间的逻辑关系将会让人更加望而却步。

因此我们需要建立一套基于函数理念的结构化视图,函数在定义时可以与服务是多对多的关系,在运行时函数实例与服务是多对一的关系,即函数组成服务,服务向外提供统一业务能力。

另一方面,有了结构化的函数视图和管理能力后,我们也有了将函数进行抽象公用、建立函数市场概念的可能,进而便可以一定程度上解决重复开发的问题。

消除开发模式差异

对于不同厂家 SDK 差异大、相互不兼容的问题,Spring 给出了 Spring Cloud Function 这个答案。Spring Cloud Function 的设计有些类似于 ORM 框架的理念,提供统一的 API ,底层可根据配置对接不同的 FaaS 方案,这样一来平台迁移的成本就被降到了最低。同时,我们还应考虑存量业务如何进行函数化改造以及已有的 FaaS 项目如何回退到传统架构的问题,做到向前和向后兼容,进而最大程度降低 FaaS 的接入成本,打消业务对 Serverless 架构发展前景的顾虑。

此外,目前对于如远程调试、链路追踪、日志统计等功能,平台给出的解决方案与成熟的开发模式也有着较大差异,而对于 Java 项目来讲 Runtime 类型的 FaaS 实现相当于做了调试能力的功能阉割,因此平台也需要迭代出与成熟开发模式相持平的能力,提高业务的研发和问题排查效率。

新老结合的伸缩方式

对于大流量的 C 端 Web 业务来说,完全依赖 Serverless 的弹性扩容来根据调用进行冷启动是不现实的,因此平台也需要支持传统架构的冗余部署策略,同时结合平台已有的弹性伸缩能力来一定程度地降低业务冗余倍数进而降低费用,并以此提供突发流量的兜底扩容方案。