按需扩展:Serverless 如何轻松应对流量高峰

按需扩展:Serverless 如何轻松应对流量高峰

上一篇文章中,我们讨论了 Serverless 的两种处理模型:运行至完成和长时间运行进程。这两种模型的关键区别在于函数实例执行完毕后是否立即终止。此外,我们还探讨了两种场景:数据编排和服务编排。

你可能会想,这些场景是否可以通过长时间运行进程来实现?答案是肯定的,但值得注意的是,运行至完成才是 Serverless 最纯粹的形式。那么,这背后的逻辑是什么呢?

要完全理解这一点,我们需要引入复杂互联网应用架构演进中的一个关键概念:扩展,这也是本文的重点。

想象一下,200 个用户同时访问你在本地开发的 Web 应用的 index.html 主页。你的本地 Web 服务器实例会发生什么?

让我们来描述一下你的 PC 的状态。首先,客户端和你的 PC 之间建立了 200 个 TCP/IP 连接,你的 PC 勉强能处理。然后,所有 200 个客户端同时发起 HTTP “GET/” 请求。你的 Web 服务器的主进程会并发创建 “CPU 核心数 – 1” 个子进程来处理这些请求。请注意,我们之所以减去一个 CPU 核心,是为了给主进程保留一个。

例如,一个 4 核 CPU 会创建三个子进程,并发处理三个客户端请求,而其余请求则排队等待。子进程开始处理 “GET/” 请求,匹配路由规则,进入相应的控制函数,并将 index.html 返回给客户端。一旦某个子进程发送完 index.html 文件,主进程会回收它并创建一个新子进程来处理下一个请求,直到所有请求处理完毕。

理解了这一点,接下来的问题就简单了。我们如何提高客户端队列的处理速度?

垂直扩展 vs. 水平扩展

一个显而易见的解决方案是增加 CPU 核心数。我们可以通过升级单台机器的配置来实现,例如从 4 核升级到 8 核,这样就会有 7 个并发子进程。

除了直接增加 CPU 核心数,我们还可以增加更多机器(每台都是 4 核)。通过将 500 个客户端分配到两台机器上,我们同样可以将并发子进程数增加到 6 个。

增加或减少单台机器的性能属于垂直扩展,随着性能提升,成本曲线往往非常陡峭。因此,在采用这种方法时需要慎重考虑。另一方面,增加或减少机器数量属于水平扩展,这是一种成本效益更高的方法,也是我们默认的扩展方式。

现在,我们来增加一些复杂性。虽然 index.html 是一个单一文件,但数据呢?无论是垂直扩展还是水平扩展,我们都需要重启机器。在我们的待办事项列表示例中,数据存储在内存中,每次重启都会重置。那么,在扩展过程中我们如何保留数据呢?

有状态 vs. 无状态

网络拓扑中的节点可以根据是否存储状态分为有状态节点和无状态节点。有状态节点保留状态,也就是说它们存储数据。因此,它们需要额外关注,要求稳定且不易频繁变动。例如,数据库通常采用主从结构,如果主节点出现问题,可以立即切换到从节点,确保持续的服务可用性。

无状态节点则相反,它们不存储任何状态,或者只暂时持有不可靠的数据。由于没有状态,无状态节点可以水平扩展以应对高并发,并且在没有流量时可以缩减到零(听起来很熟悉吧?)。但有状态节点却做不到这一点。在流量峰谷差异很大的场景中,我们需要设计有状态节点来应对峰值流量,同时在低流量时段仍需承担运营成本。

数据库是典型的有状态节点,因为它持久化存储用户的待办任务。同样,负载均衡器也是有状态的,非常类似于我们思维实验中维护客户端队列的主进程。它需要存储客户端连接,以便将 Web 应用处理的结果返回给客户端。

回到我们的处理模型,运行至完成本质上是无状态的,因为它执行后即终止,无法单独用于持久化数据存储。而长时间运行进程则天生有状态,因为其主进程不退出,可以存储一些值。

然而,在 Serverless 中,即使我们在长时间运行进程的主进程中存储了值,云提供商仍然可能回收它。即使使用了预留实例,扩展出的节点内存中的数据仍然是隔离的。

因此,要让长时间运行进程变得无状态,我们需要避免在主进程中存储值,或者只存储临时变量。持久化数据应转移到专门的有状态节点,比如数据库。

通过将数据存储与主进程节点分离,并确保主进程不保留数据,我们的应用就变成了无状态。我们将数据存储在一个独立的、有状态的数据库节点中。这个例子就变成了上一篇文章中讨论的长时间运行 Serverless 场景:我们在主进程启动时连接数据库,然后通过子进程访问数据。然而,这种方法有一个显著的缺点:它直接增加了冷启动时间。是否有更好的解决方案呢?

让我们考虑另一种数据持久化的方法。为什么我们必须自己连接数据库?我们对数据的 CRUD(创建、读取、更新、删除)操作,本质上都是子进程复用主进程建立的 TCP 连接,发送数据库语句并获取数据。想象一下,如果我们能够使用 HTTP 请求(如 POST、DELETE、PUT 和 GET)向数据库发送指令,会怎样?那样我们是否就能利用上一节课中提到的数据编排和服务编排概念了?

什么是 BaaS

确实,所有这些铺垫最终引出了今天的主角:BaaS 化。数据接口操作 POST、DELETE、PUT 和 GET 对应 RESTful API 的语义 HTTP 方法。以 MySQL 为例,POST 映射到 CREATE 命令,DELETE 映射到 DELETE,PUT 映射到 UPDATE,GET 映射到 SELECT。这种语义上的一一对应关系使我们能够自然地将 MySQL 操作转化为 RESTful API 操作。

传统的数据库访问方式,由于 TCP 连接复用和通信开销低,在相同操作下比 HTTP 更快。虽然 Serverless 可以直接连接数据库,但在存在 VPC 隔离的云环境中,通过 IP 地址连接传统数据库往往比较困难。因此,Serverless 数据库连接通常依赖云提供商提供的 BaaS 服务,不过许多 BaaS 服务尚未成熟。

更进一步来说,如果 Serverless 不适合有状态节点,为什么不把所有有状态操作外部化为数据接口呢?这样 Serverless 函数就可以利用上一节课中讨论的数据编排方法,实现自由扩展。

总结

运行至完成模型之所以被认为比长时间运行进程模型更纯粹,是因为后者容易产生误导,让我们像使用 PaaS 一样将其当作有状态节点来永久存储数据。然而,在 Serverless 中,即使是长时间运行进程,云提供商仍然可能回收我们的函数实例。

就像我们示例中将数据存储在内存中导致每次重启都会重置一样,通过采用数据编排思维并将后端数据库操作转化为数据接口,我们可以将 Serverless 中的数据存储卸载到后端应用,并使用上一节课中介绍的数据编排与它们交互。然而,我们不仅需要为后端应用创建数据接口,还需要拥抱 BaaS 化,让后端工程师在开发过程中摆脱服务器端运维的困扰。

在扩展方面,我们可以选择垂直扩展或水平扩展。垂直扩展侧重于提升单机性能,但成本增加往往非常陡峭,因此需要谨慎选择。水平扩展则涉及增加机器数量,成本曲线更平滑,是我们默认的扩展方式。

有状态节点存储数据,而无状态节点处理数据但不保留数据。只有无状态节点才能自由扩展。负责存储关键数据的有状态节点需要谨慎处理。如果我们希望网络拓扑中的节点能够自由扩展,就需要将其数据操作外部化到专门的有状态节点。

当 Serverless 函数访问有状态节点时,最好让这些节点提供数据接口,而不是仅依赖数据库命令,因为数据库连接会给 Serverless 函数带来额外开销。此外,为了简化后端工程师的开发工作,我们应努力实现对有状态节点的 BaaS 化。我们将在后续文章中深入探讨 BaaS 化。

Novita AI 是一个一体化云平台,助力你的 AI 抱负。集成 API、Serverless、GPU 实例——你所需的成本效益工具。消除基础设施障碍,免费开始,让 AI 愿景成为现实。

推荐阅读

揭开革命序幕:探索 Serverless 计算的世界

Serverless 分析,从数据模型开始