我的书单

前言

无服务器计算模型在开发者中已相当流行。在完全托管的计算环境中消费计算资源(无需管理服务器)使SaaS开发者和架构师能够摆脱对难以实现的扩展性和成本优化策略的追求。无服务器计算的函数中心化特性也会影响您设计和实现多租户SaaS架构的方式。基于此,我认为专门探讨无服务器计算模型中多租户策略的实现具有现实意义。

本章的目标是深入剖析在通过无服务器计算交付应用服务时,构建SaaS环境所面临的具体挑战与影响。为了便于理解,我将这些策略映射到AWSLambda服务,该服务提供了配置、托管和扩展环境中函数的托管计算能力。在本章开头,我将首先概述SaaS环境与无服务器模型之间的天然契合点。我们不会在此过多展开,但对于SaaS架构师、开发者、运维人员及业务决策者而言,理解无服务器模型为SaaS提供商带来的整体价值主张至关重要。我在第八章中曾简要提及这一点,作为探讨无服务器计算如何影响存储的一部分。

现在,我将深入探讨采用无服务器计算模型所能实现的一些动态特性和效率提升。在明确价值主张后,我将开始探讨无服务器计算的函数式特性如何影响我们构建分层部署的方法。重点将放在探索如何通过无服务器函数创建池化与隔离租户环境的含义。这将包括对不同模式和方法的分析,这些模式和方法将决定何时以及如何隔离租户函数。在此基础上,我们将开始探讨在无服务器计算模型中如何实现租户的上下文路由。这将基于部署模型讨论,重点介绍用于将租户请求映射到其环境中函数的机制。

本章还将回顾无服务器模型带来的入门和部署自动化细节。您将看到构建分层感知入门和部署自动化所面临的独特挑战。虽然有现成的工具可帮助实现这些功能,但您可能需要引入自定义构造来应对多租户环境的实际需求。本章的下一部分将探讨在无服务器环境中引入租户隔离时遇到的特殊情况。您将看到一些用于防止跨租户访问的无服务器特定技术和构造。本章最后将总结一些更高层次的无服务器设计考虑因素。我们将探讨如何配置管理函数的规模配置文件,利用预留并发数来控制工作负载在多租户环境中不同Lambda函数间的扩展方式。

最后,我们将简要探讨无服务器架构的整体影响,探索将其应用于架构所有层级的意义。本章内容与第10章关于EKS的讨论并行展开。其目的是说明无服务器架构与EKS如何通过在某些情况下采用截然不同的方法和工具链,实现相似的目标。

SaaS与无服务器架构的适配性

对于许多组织而言,采用SaaS模式的核心在于实现规模经济,以推动增长、提升效率和促进创新。这种思维方式的核心需求是构建一个与租户活动特征相匹配的基础设施资源消耗模式的SaaS环境

虽然我们的目标是无论采用何种技术都能实现这种匹配,但某些技术能够简化实现这一目标所需的努力程度。这正是无服务器计算策略大放异彩之处。为了更好地理解其原因,让我们先看看团队在构建SaaS环境时所面临的动态(如图11-1所示)。

图11-1 无服务器SaaS:活动与消耗的对齐

在此图中,我试图展示一种概念化的视图,说明如何对SaaS架构中的活动进行剖析。该图表旨在呈现对租户基础设施使用情况(虚线)和租户活动(实线)的运营视图。租户活动试图传达租户活动的不确定性。这种不确定性由多个因素驱动。活跃租户的数量、租户工作负载的变动性以及其他诸多因素,都使得预测满足租户不断变化需求所需的计算基础设施量变得极为困难。

此外,租户在系统生命周期中可能随时加入或退出,进一步增加了复杂性。尽管难以精确预测租户活动,我们仍明确以最小化资源过度配置为目标。在图中,我展示了理想情况下基础设施消耗如何与租户活动在环境中同步的幻想版本。在此示例中,基础设施消耗曲线与租户活动完美同步,在任何时刻都能精准提供满足租户需求的恰当基础设施。尽管这种模式未必适用于所有解决方案,但它仍是SaaS架构师在设计系统时努力遵循的思维模型。他们希望系统能动态响应,优化基础设施成本并实现规模经济,这是构建成功SaaS业务的关键。

现在,您可能会认为云弹性和水平扩展的概念是解决此问题的答案。从整体来看,这种期望是完全合理的。然而,大多数水平扩展技术都是通过扩展策略来实现的,这些策略决定了环境在何时以及如何进行扩展。这就是问题变得复杂的地方。尽管计算资源可以动态扩展,但仍需有人(即你)定义系统何时以及如何进行扩展。

你需要编写并应用这些扩展策略,并希望所采用的策略既高效又可靠。如果你的环境具有相对可预测的工作负载,这种方法可以很好地工作。然而,在多租户环境(如前所述)中,构建一套能够普遍应对租户工作负载不可预测性的策略集非常困难。这通常导致团队选择过度预配置资源并采用更保守的扩展策略,以限制其对噪声邻居、性能和弹性问题的暴露。这些挑战的根源在于,您认为环境中的计算资源是您的责任。虽然这些资源可以根据您的策略动态调整,但您仍需确保在需要时提供适当的计算资源。

而无服务器计算,顾名思义,完全摒弃了服务器概念。您的代码由一个托管服务执行,该服务负责提供系统所需的计算资源。这使您能够将所有扩展责任推给托管服务(在此案例中为AWSLambda)。在我看来,这对于SaaS架构师而言是一个变革性的转变。在此模型中,您无需再追逐那套难以捉摸的扩展策略。这使SaaS团队能够将更多时间专注于功能开发,摆脱构建有效且高效扩展策略所带来的繁重工作。

成本是另一关键因素。在无服务器模型中,您通常只需为代码的实际执行付费。无需进行任何过度配置或为可能发生(或不发生)的峰值预留闲置容量。在无服务器架构中,您只需为实际调用的单个管理函数付费。如果某个函数从未被调用,则不会产生任何成本。想象一下这些动态如何影响我们最初绘制的图表(图11-1)。

如果我们仅关注计算资源并试图将资源消耗与实际活动相匹配,无服务器架构现在使这一目标变得更加可实现。在无服务器计算的按需付费模式中,图表中的计算基础设施消耗应与租户的活动相匹配。更重要的是,您将实现这一效率而无需依赖任何策略。作为托管服务,其本质将确保计算消耗和成本得到优化。

无服务器计算模型的函数中心化特性还带来了一些超越效率的潜在额外价值。通常,由于函数是部署的基本单位,您的环境将拥有更细粒度的部署模型。这使您能够以更小的影响范围推出更改和更新。

在强调实现零停机时间的多租户环境中,这一点尤为有用。无服务器模型的更小部署单元使您能够更好地最小化新发布代码的影响范围和影响程度。无服务器计算模型还为将资源消耗归因于单个租户提供了更简便的途径。由于每个函数一次只能被一个租户调用和消耗,因此将计算资源消耗归因于特定租户变得更加容易。

这种动态还为捕获和分析租户级别的计算遥测数据创造了新机会。总体而言,这些因素可以使您更轻松地为多租户架构的计算层构建一个租户感知型运营体验。虽然我将主要关注如何利用托管函数来扩展应用程序的服务,但SaaS与无服务器架构的融合远不止于此。

无服务器架构已逐渐渗透到越来越多的基础设施服务中。消息传递、分析、存储以及其他众多托管基础设施服务已开始在其计算模型中集成无服务器能力。这使您能够将无服务器架构的价值主张扩展到SaaS架构的更多层级。这在设计多租户存储策略时尤为重要,团队常常面临如何合理调整数据库计算资源规模的挑战。

在第10章中,我们还看到了AWSFargate计算模型如何使组织在容器化环境中实现无服务器计算的优势。总体而言,向更多无服务器计算模型迁移将使开发者进一步提升SaaS环境的效率。

部署模型

让我们转向探讨如何使用AWSLambda作为托管计算服务来构建多租户环境。逻辑起点是部署模型。我们需要先明确如何在隔离、池化及混合模式下,使用Lambda部署租户环境中的不同服务。

在深入探讨之前,我们需要先明确应用程序的微服务在Lambda环境中如何表示。在Lambda环境中,所有代码均以独立函数的形式编写和部署。为了帮助理解,我提供了一个Lambda微服务的基本组成部分视图(图11-2)。

图11-2 逻辑微服务

该图包含两个简单的示例:订单微服务和产品微服务,每个微服务支持若干操作。这些服务通过一个入口点(通常是API)暴露其与系统之间的契约。服务底层实现可以自由变更,只要不破坏该契约即可。这些服务通常会引用、封装并拥有存储资源。这些只是微服务的基本原则,即创建可独立构建和部署的自主服务。

现在,当我们深入探讨这些服务内部时,情况变得更加有趣。每个微服务中的操作都与一个独立的Lambda函数相关联,该函数负责实现该操作的功能。这些函数共同负责实现服务契约。与此同时,Lambda托管服务对这些函数之间的任何关系都没有实际感知。

这就是为什么我经常将这些服务称为逻辑微服务。虽然Lambda没有在这些函数之间建立任何绑定,但我们的团队仍然会将它们视为一组映射到整体微服务契约和实现的函数集合。

负责这些服务的开发人员通常会集体协作。他们会对这些服务进行版本控制、部署和测试,将其作为一个整体进行处理。我们本质上是将微服务模型带来的所有价值体系整合起来,构建一个与这些基本微服务原则一致的函数视图。

尽管你的微服务通常可能被表示为一组函数,但你也可以有一个由单个函数表示的服务。关键在于,你不需要将微服务与函数建立一对一映射。当然,关于逻辑微服务的整个讨论与我们对多租户部署模型的思考直接相关。

当我们开始描述部署的特征时,它们不再只是函数——而是以逻辑微服务的形式存在,这些微服务作为实现微服务契约/功能的函数集合进行部署。

池化部署与孤岛部署

在使用无服务器函数时,实现孤岛式和池化部署的思路略有不同。在其他架构(如EKS)中,通常会提供分组构造来定义计算资源的部署方式,从而在计算资源之间划定边界。

然而,在Lambda中,实际上并不存在允许将函数放置到特定组中的机制(除了标签,但标签并不适合我们试图创建的多租户分组)。这意味着我们的部署模型实际上是通过为租户部署独立的函数分组,并使用路由机制将租户与对应的函数连接起来来实现的。

这使得部署部分相对直观。图11-3展示了应用程序的无服务器服务以池化和隔离模型部署时的可能结构。

图11-3 支持无服务器隔离和池化部署

在该示意图的左侧和中央位置,有两个高端服务层的租户,它们各自独立部署了无服务器计算资源。这意味着我已为这些租户分别配置并部署了独立的订单和产品功能副本。这些孤岛中的功能完全专用于各自的premium级租户。右侧是运行在池化模型中的基础级租户。这些功能将被所有基础级租户共享。

需要注意的是,尽管我们为每个租户部署了独立的实例,但这些租户中的函数均运行同一版本的代码。实际上,若要更新某个函数,需为每个租户单独部署该函数的新副本。在查看这些部署模型时,您可能会疑惑支持隔离的Lambda函数是否真的能带来价值。Lambda函数的本质就是不共享。当一个租户调用一个函数时,该函数的执行范围和生命周期将专属于该租户。

如果多个租户调用同一个函数,Lambda会根据需求自动创建更多该函数的实例。这意味着Lambda函数本身就是隔离的。那么,支持独立隔离部署能带来什么价值?部署隔离的租户函数仍可带来多重优势。噪音邻居问题无疑是其中重要一环。尽管Lambda可扩展函数,但其并发限制仍可能影响单个函数的并发执行数量。若仅部署一个由所有租户共享的函数,存在触发Lambda并发限制的风险。

这可能触发速率限制并导致噪音邻居问题。通过为隔离的租户部署独立的专用函数,我可以确保仅有一个租户调用其函数。这使我能够为隔离部署和共享部署分别应用并发策略。同时,我还能更好地控制租户对这些函数的访问方式。将函数隔离还可能影响环境中的租户隔离模型,使您能够在部署时附加隔离策略。

这可以简化隔离的实现方式,减少定义无服务器租户隔离模型时所需的影响和复杂性。我将在本章后面的部分中更深入地探讨不同无服务器租户隔离策略的权衡。

混合模式部署

如我们在部署讨论中看到的,资源隔离和池化并非非此即彼的选择。借助无服务器技术,我们确实可以选择性地将部分租户功能(微服务)进行隔离,以满足噪声邻居、分层、隔离等需求。在无服务器环境中,这实际上意味着我们可以采取更精细的策略来确定函数的部署方式。图11-4展示了在多租户架构中应用混合模式无服务器部署的一个示例。

图11-4 无服务器与混合模式部署

在此示例中,我选择了一组无服务器微服务,这些服务将以孤岛模式运行(如左侧所示)。其核心思想是,订单(Order)和产品(Product)服务代表了业务确定应以孤岛模式提供功能的关键领域。同时,其余服务(如右侧所示)可采用池化模式运行。我们之前讨论过这种混合模式。无服务器计算引入了一些新挑战。

在传统计算环境中,您需要权衡为每个隔离租户环境分配专用资源的价值、成本和复杂性。您还需考虑如何有效扩展共享计算资源以满足多租户需求。在使用无服务器计算时,这些因素不再那么重要。例如,无论哪些资源是隔离的还是共享的,您最终只需为实际消耗的资源付费。

确定这些隔离和共享函数如何扩展所需的努力也减少了。相反,您可以更多地依赖Lambda服务来高效扩展计算资源。无服务器计算更简单的成本和扩展故事可能对某些团队更具吸引力。至少,无服务器减少了与支持混合部署模型相关的部分摩擦和挑战。

更多部署考虑因素

无服务器部署模型中的一些细微差别可能会影响您选择将哪些计算资源进行孤立或池化的决策。要理解这些选项,我们需要先了解由Lambda服务管理的函数的生命周期。

由Lambda服务管理的函数的生命周期。每次调用函数时,Lambda有两种可能的执行路径。如果这是第一次调用该函数,Lambda需要创建该函数的第一个实例。随后,当请求完成后,后续请求可以复用该函数。其核心思想是通过复用最近执行的实例来提升效率。该生命周期中有两个特定维度是我们需要重点关注的。

第一个是冷启动。冷启动指的是调用一个近期未被执行的函数。在这种情况下,您可能会观察到处理此请求时出现一些轻微的额外延迟。这种延迟的影响会因您使用的技术栈、函数代码的性质及其依赖关系,以及其他因素而有所不同。在池化环境中,冷启动的影响很可能微乎其微,因为会有许多租户同时使用系统,这应能限制触发冷启动条件的情况。

然而,对于仅由单个租户使用的孤岛环境,您可能会遇到更多冷启动影响租户体验的场景。这可能影响您选择孤岛化的对象,并可能导致引入针对性预热策略以减少冷启动的影响。另一个生命周期问题与状态残留相关。每次Lambda处理一个租户的函数调用时,该函数仅针对该租户执行。Lambda会通过启动更多函数实例来扩展以满足多个租户的需求。虽然可能存在多个函数实例同时运行,但每个调用仍映射到单个租户。

这在大部分情况下是积极的。然而,一旦函数完成对租户请求的处理,系统可复用该实例处理其他租户的请求。在大多数情况下,这不会引发问题。但若函数实现中存在未在完成后释放状态信息的逻辑,后续租户请求可能访问该状态。这一点在池化环境中尤为重要,因为在此类环境中,函数会在租户之间大量共享。理想情况下,您的代码不应使用任何允许状态从一个请求传递到下一个请求的构造。然而,鉴于此处的潜在风险,您的函数应采用策略/库来确保在执行完成后清除状态。

控制平面部署

在无服务器架构(以及我们所有的SaaS部署模型中),我们需要决定如何以及在何处部署多租户架构中的控制平面组件。您可用的选项实际上由更广泛环境中包含的不同架构组件所决定。在Lambda环境中,我们的选择主要局限于用于分组和隔离任何云资源。在图11-5中,我展示了在无服务器模型中部署控制平面两种可能的策略。

图11-5 部署无服务器控制平面

在大多数情况下,您的选择取决于确定哪个AWS账户将托管您的控制平面。在该图的顶部,控制平面和应用程序平面部署在不同的账户中。如果您有安全、合规或其他要求,需要在控制平面和应用程序平面之间建立绝对边界,则可能选择此选项。

此外,性能和安全要求也可能促使您将控制功能部署到另一个账户,从而限制控制平面影响应用程序平面相关并发需求的能力。当然,这需要配置跨账户访问权限以实现控制平面与应用程序平面之间的交互,实施起来会稍显复杂。

在图11-5的底部,我们可以看到这个模型的简化版本,其中控制平面与应用程序平面位于同一个AWS账户中。这种模型本质上是将控制平面函数和支持基础设施与运行应用程序平面的函数和基础设施部署在同一环境中。

这确实简化了控制平面的部署和配置。然而,采用这种方法,您需要熟悉此部署模式带来的安全、并发和隔离模型相关考虑因素。

如您所料,我这里只是略微触及了表面。还有其他AWS技术可能会影响您选择部署无服务器控制平面方式。主要结论是,这需要在您考虑无服务器架构整体部署范围时纳入考量。

运维影响

每次我们分布式部署SaaS架构的足迹时,都必须考虑这种更分散的部署方式对整体运营复杂性的影响。在这些孤立且池化的配置中部署功能的多份副本,无疑会引发人们对这种部署方式对解决方案运营足迹影响的质疑。对于部分用户而言,按租户复制功能副本的传播可能被视为增加了环境管理与部署的复杂性。

这是一个普遍存在的问题,适用于任何具有分布式部署的环境。然而,我认为无服务器架构放大了这一问题的潜在影响。在无服务器架构中,我们可以实现更细粒度的部署和管理单元。以传统计算模型为例,我的管理和运营可见性通常集中在微服务层面,其中微服务代表了该服务支持的所有操作的组合。

而在无服务器架构中,每个操作都可能对应独立的函数。现在,如果再加上支持多租户环境的需求,您不难想象这将如何迅速增加环境的运营复杂性。这些因素并不意味着无服务器架构是个糟糕的选择。它们确实表明,您可能需要投入更多精力来实现一种能够考虑这种更细粒度视角的运营体验。您希望运营遥测能够让您专注于系统中的单个函数。

能够定位健康、可用性和扩展问题意味着对这些单个函数(而不仅仅是服务)的性能有更深入的洞察。实现这一目标的机制和工具已经存在,但这应该是你在架构系统时需要关注的重点。这一点在预期支持大量租户环境时尤为重要。

路由策略

如果您计划支持多种部署模型,还必须考虑如何根据上下文将流量路由到与不同层级和部署配置文件关联的函数。启用无服务器路由模型的实现机制相对简单。然而,根据解决方案的需求,您可以采用不同的路由模式。图11-6展示了最简单的路由模型概念图。

图11-6 到租户部署的路由

在该图的底部,我引入了多种使用不同部署模型的租户环境。存在一组通用的函数,用于实现我们的应用程序平面服务。在此,我需要创建这组函数的三个独立副本,以满足租户的部署需求。现在,当请求流入我的系统时,我需要能够使用租户上下文将这些请求路由到相应的Lambda函数。

在此示例中,您会看到此函数映射由API网关定义。在此特定解决方案中,我展示了一个API网关的单实例,作为所有函数的入口点。这意味着我需要为租户部署中包含的每个函数定义独立的路由。虽然通过单个API网关实例解决所有路由问题非常方便,但随着规模扩大,这种架构可能变得难以管理。需要支持的租户数量、映射的路由数量——这些因素都可能表明您需要采用另一种方法。一种解决方法是为每个租户部署考虑支持独立的API网关实例。图11-7展示了引入独立API网关后系统架构的整体概念。

图11-7 每个租户独立的API网关实例

在此模型中,我为每个租户创建了独立的API网关实例。这意味着每个API网关仅负责处理并路由来自特定租户隔离环境或共享租户池的请求至对应的一组函数。这在逻辑上增强了每个API网关与所属部署中具体函数之间的绑定关系。同时,它还允许在API网关层面对每个部署实施更精细的策略控制。

虽然这种模型有其优点,但它确实需要您在租户与其对应的API网关URL之间建立一些映射关系。当每个租户提交请求时,您需要使用租户上下文和租户层级来确定应由哪个API网关处理该请求。对于一些人来说,这种额外的间接层可能感觉不太自然。成本也应作为选择路由策略时的考量因素。

例如,我们可以为每个租户单独添加API网关。如果租户数量较少且各自拥有独立的API网关,这种策略完全合理。然而,若尝试将此架构扩展至数百或数千个租户,可能会对成本、运维、部署以及SaaS环境的其他多个维度产生影响。

入驻与部署自动化

无服务器环境的入驻、配置和部署策略通常依赖于传统工具来预配置和配置每个租户的基础设施。如果您使用AWS(本文将重点讨论AWS),这通常通过结合使用DevOps工具实现,包括CDK、CloudFormation、Terraform等。

AWS还提供了多种构建和部署编排工具,可自动化这些流程(CodeBuild、CodePipeline和CodeDeploy)。除这些工具外,还有专门针对无服务器配置和部署体验的Serverless Application Model(SAM)。让我们先从入门流程开始。

如您所想,入门体验的性质和复杂性直接受系统所选部署和分层策略的影响。如果所有资源都集中在单一池中,这相对简单。然而,如果采用分层模型,部署过程中将涉及更多变量。这在任何SaaS架构中都是成立的。我将重点关注与无服务器架构自动化相关的自动化组件。虽然我们有多种工具可用于实现入驻流程,但我将重点介绍SAM,因为它专为配置、预配置和更新无服务器架构而设计。图11-8展示了如何使用SAM描述每个租户层级的配置。

图11-8 定义无服务器分层环境

在示意图的右侧,您将看到一个租户部署。这是一个概念性的占位符,用于表示支持租户部署所需的基础设施和资源的通用模板(作为应用程序平面的一部分)。在此示例中,每个部署包含一个API网关、一组实现应用程序平面微服务的函数,以及存储(在此示例中为RDS数据库)。

我们的基础和高级层租户将各自拥有与该架构匹配的部署。它们可能配置不同,但共享相同的底层架构。关键点在于,随着租户的接入,我们需要要么为高级层租户预配置一个新的部署,要么配置租户加入现有部署(基础层)。这些任务的配置和部署将由图示中心位置的SAM模板处理。该基础模板定义了每个租户部署中包含的所有基础设施。

在此示例中,它负责配置并部署右侧租户部署中显示的所有基础设施,包括设置API网关、部署Lambda函数、配置路由,以及为系统预配置RDS数据库。值得注意的是,一个更现实的示例将包含多个微服务,每个微服务都可能拥有自己的存储基础设施。在左侧,您可以看到我创建的独立分层配置文件,这些文件提供了用于定义每个分层相关变体的所有参数。在此示例中,我包含了基础和高级两个分层。

此简化模型主要专注于为每个分层设置特定的性能和扩展参数。您会发现,每个参数集都引用了已分配和已预留的并发设置,这两者都会影响每个层级的扩展能力和性能特征。已分配并发设置用于控制您在Lambda环境(层级)中希望预初始化的执行环境数量。对于基础层租户,我将此设置为0,假设跨多个租户的并发活动将使大多数函数保持活跃状态,从而减少对基础层函数预热的需求。

同时,对于高级层租户,我选择使用一定程度的预分配并发,以克服在孤立环境中可能更频繁出现的冷启动问题。这些配置文件中的数据将作为输入传递给SAM模板,用于填充并配置模板中存在的参数占位符。可以肯定的是,在完全成熟的环境中,随着层级配置需要定义更复杂的隔离和池化部署,此处将涉及更多复杂组件。

虽然图11-8展示了此入驻体验的关键组件,但并未解释如何引入工具和流程以在完全自动化的入驻体验中应用这些构造。在图11-9中,我提供了一个示例,说明如何将这些概念集成到入驻流程中。

图11-9 接入编排

显然,这个体验中有很多动态组件。让我们从左到右逐步分析,首先是租户触发入驻流程(步骤1)。控制平面的入驻服务将处理与创建新租户、身份等相关的基本操作。它还会调用租户配置服务,该服务负责创建和配置与租户相关的资源。现在,在图的左下角,我引入了一个新表来支持入驻体验。该表对本特定无服务器入驻流程至关重要。它用于跟踪无服务器环境中不同租户的部署状态。

它是将租户与对应的基础设施堆栈和Lambda函数关联的机制。租户配置服务会在入驻过程中(步骤2)查询此表。如果正在入驻的租户是基础层租户,且该租户是首次被添加到我们环境中的基础层租户,则配置服务会向该表插入新行(如图所示的第一行)。在此示例中,我展示了表在基础层租户已完成入驻后的状态,因此表中仅保留了第一行。该行中的部署列也表明此堆栈使用了池化模型。

由于池化模型适用于多个租户,该列没有特定的租户ID。相反,租户ID列的值为“pooled”,表示此条目对应于所有基础级租户。

创建此条目后,租户配置服务将调用我们的入驻管道,该管道使用AWSCodePipeline自动化入驻流程(步骤3)。此代码管道使用AWSCodeBuild检索并处理描述租户环境的通用SAM模板。在此示例中,模板从AWS CodeCommit仓库中检索(步骤4)。我们的构建过程将打包该模板并部署到S3存储桶,以便后续可从标准的可访问位置引用(步骤5)。该流程的最后一步是实际执行打包后的SAM模板。

这通过调用一个Lambda步骤函数实现(步骤6)。该步骤函数会检索之前讨论的分层配置设置,并将它们作为参数传递给一个SAM部署请求,该请求引用了打包后的S3模板(步骤7)。执行此部署将创建我们的第一个基本分层、池化租户环境(步骤8)。我确实想强调这个过程的最后一个维度。在图表的右下角,您会看到我展示了一个包含租户API网关入口点的表格。对于此解决方案,我选择为每个部署使用独立的API网关。

为了使这套方案正常工作,我需要跟踪每个API网关URL对应的租户或层级。这些映射数据将用于将租户请求路由到对应的租户函数。为了实现这一点,我们需要跟踪并存储此映射信息。我们的入驻自动化流程必须包含一个步骤,将此数据存储在映射表中(步骤9)。基础层租户将共享一个API网关入口点,而高级层租户将在该表中各自拥有独立的条目。此时,基础层租户所需的所有基础设施已就位。

然而,如果要为另一个基础层租户进行入驻(由于这些租户运行在共享基础设施上),这意味着什么?当为下一个基础级租户执行流程时,租户配置服务会发现租户堆栈映射表中已存在基础级条目。因此,它不会重新部署基础设施,而是仅引入该新租户所需的增量配置条目。现在,让我们考虑一下如何将孤立的、高级别租户的入驻流程融入这个流程中。整个端到端流程与常规流程基本相同。

关键区别在于,孤立的租户会在租户堆栈映射表中拥有其独特的条目。这使得我们可以为拥有专用基础设施和Lambda函数的租户维护一个完全独立的堆栈,并对其进行跟踪和更新。这涵盖了无服务器环境中入驻自动化流程的基本组件。拼图的另一部分是更新的部署。

一旦这些环境都正常运行,我们仍然需要一种方式来将更改推送到我们的无服务器架构,该架构需具备对不同层级和部署模型的感知能力。图11-10展示了如何自动化新功能和更新的发布流程。

图11-10 应用层级感知更新

此图专注于开发者的更新体验。左侧显示一位开发者正在将新微服务引入已部署多个不同层级租户的环境中。为确保顺利运行,开发者应仅需构建并提交微服务代码,无需关心代码如何部署到所有租户环境。我们还需要更新SAM模板以反映新微服务的存在。我已示例了这两个操作均被提交到Cod eCommit仓库中。

从这里开始,我们将使用CodeBuild打包更新后的模板。随后,我们的步进函数将遍历租户堆栈映射表中的所有条目,并将更新后的模板应用到每个租户环境中。在我看来,这是无服务器部署自动化过程中至关重要且常被忽视的关键环节。目前没有内置的构造或工具可以直接支持跟踪租户堆栈并跨所有不同环境应用更新的需求。使用步骤函数和表是否是实现此功能的正确方式?也许是。

这只是我在此示例中展示的方式,但可能存在其他更适合您整体自动化体验的选项。然而,最终必须有一种机制来跟踪租户堆栈映射信息,并将其融入部署策略中。值得注意的是,相同的机制也可用于分阶段推出修复程序或新功能。您可以扩展此租户堆栈映射表,添加额外标记以指示租户接收更新的方式和时间。这可作为金丝雀部署或波次部署策略的一部分。

租户隔离

尽管在无服务器环境中,租户隔离的基本原则和核心理念保持不变,但无服务器环境特有的一些隔离细节需要进一步探讨。在无服务器环境中,隔离策略可以在多租户架构的多个层级中应用。例如,您可以在API网关层引入隔离策略,监控入站租户请求并控制每个租户可调用的函数和操作。

您还可以直接将隔离策略附加到函数上。关键在于评估这些选项,并确定哪种隔离方式最适合您的无服务器SaaS架构。以下各节将详细阐述这些无服务器隔离模型的组成部分。

基于动态注入的池化隔离

在池化环境中隔离租户资源始终更具挑战性。通常,任何池化模型都需要在隔离模型中采用某种形式的运行时策略。对于运行时策略,这意味着开发人员需要在代码中引入部分逻辑,以将隔离策略应用于每个租户请求。当然,我们希望这个过程尽可能简单直观,减少对团队遵守复杂隔离机制的依赖。

同时,我们希望策略能够在开发人员视图之外进行集中管理。解决此问题的一种方法是通过隔离凭据注入。该策略将隔离实现中的大部分关键组件移至API网关,作为对每个入站请求应用的预处理步骤。我们在第9章中讨论了凭据注入作为一种通用技术。然而,我希望更详细地探讨该策略在无服务器环境中的具体实现方式。

图11-11展示了无服务器凭证注入模型的整体架构。在此示例中,我们有一个依赖于DynamoDB表存储订单信息的Order服务。该Order表采用池化存储模型,将同一租户的所有数据混杂存储在同一表中。该表通过分区键存储租户标识符,以将表中的项与特定租户关联。通过注入,我们的目标是在请求进入Order微服务之前生成隔离凭证。微服务仅接收凭证,并在与Order表交互时应用这些凭证,根据注入凭证的租户上下文限制访问权限。

图11-11 基于凭证注入的无服务器隔离

此注入机制的所有组件均在架构的API网关层实现。如图11-11所示,我已将一个Lambda授权器绑定到API网关。该授权函数从请求中提取租户上下文,确定操作类型,并识别用于限制访问的隔离策略。在图示右下角,我展示了一个示例策略,该策略根据当前租户上下文对访问Order表的权限进行范围定义。

该策略会被填充租户上下文,并通过assumeRole()调用发送至身份与访问管理(IAM)服务。角色会根据策略/租户上下文生成一组凭据,用于限制访问范围至策略定义的范围。

此过程返回的凭据会被注入到发送到微服务请求的头部。服务仍需承担应用这些凭据的部分责任。在此示例中,凭据将在初始化数据库(DynamoDB)客户端时使用,并应用于每个尝试访问订单表数据的请求。这将微服务开发者的工作量降至最低,使其只需获取并应用注入的凭据。

此方法还为在API网关中缓存凭据提供了机会,帮助团队克服为每个租户请求获取凭据带来的延迟和开销。这在无服务器环境中尤为重要,因为函数不应在不同租户请求之间维护状态。这种隔离策略将策略从微服务中分离出来。它们现在在API网关层集中管理和处理。这还为优化隔离模型提供了机会。

在此,您可以缓存获取的租户凭据,并减少在每个请求中调用assumeRole()带来的开销。您还可以利用网关的生存周期(TTL)来控制凭证的缓存生命周期。这种性能提升在某些环境中可能至关重要。虽然这种方法有许多优点,但也存在一些缺点。

一些团队更倾向于让这些隔离策略由每个微服务自行拥有、版本控制和管理——尤其是因为这些策略通常与特定的微服务紧密相关。我们在第9章中讨论的另一种方法是让每个微服务负责定义策略并生成其凭据。当然,你可以认为策略应由服务封装,并视为其底层实现的一部分。采用这种方法,你的实现会将租户上下文传递到无服务器函数中,每个函数都会包含获取租户范围凭证所需的代码。

部署时隔离

对孤立函数应用隔离是一个更为直观的方案。若采用孤立模型,这意味着这些孤立租户将拥有专属函数,且仅能被单一租户调用。基于此,我们可以采用更简洁的函数隔离模型:在部署时将隔离策略绑定至专属租户函数。在此,您的DevOps工具将在隔离租户的入驻过程中负责配置函数的隔离策略。图11-12展示了这种部署时隔离模型在无服务器架构中的工作原理。

图11-12 基于Lambda函数的部署时隔离模型

图11-12包含与前例相同的Order微服务。唯一区别在于它现在以隔离模型部署。在预配置过程中创建此微服务时,会为该微服务中的每个函数附加一个Lambda执行角色。此执行角色引用了图示底部右侧的策略。该策略限制了该函数对Order_Tenant1表的访问权限。任何尝试访问其他租户表的操作都将被拒绝。

如您所见,这相较于任何运行时隔离策略都具备显著优势。所有注入的凭据和针对共享租户所需的特殊处理均已消除。如今,所有操作均在部署阶段完成,且在这些部署函数的生命周期内,微服务仅限于访问租户1的订单表。这种设计更简洁,且运行时开销更低。

另一个优势是,您的隔离策略可以作用于函数级别,这意味着它们可以更加精细化,并专注于实现单个函数的隔离需求。在其他计算模型中,您的策略可能涵盖微服务中所有操作。虽然这并非巨大优势,但确实为隔离模型的范围定义和管理提供了额外的控制层。

同时支持孤岛和池隔离

我们已经看到了两种针对孤岛和池部署模型截然不同的方法。不太明显的是,单个函数可能部署在孤岛和池环境中。该函数可能访问池化的DynamoDB表以处理基础层租户,同时访问孤岛化的Order表以处理高级层租户。

挑战在于,每个层级可能采用不同的隔离方案。这意味着函数中的通用代码需要根据上下文支持不同的数据访问方式和隔离策略应用。为了更好地理解这可能如何实现,让我们看一段用于访问订单数据的代码片段(该数据可能被隔离或池化,具体取决于租户层级的上下文):

def __get_dynamodb_table(event, dynamodb):
    if(is_pooled_deploy=='true’):
        accesskey=event['requestContext']['authorizer']['accesskey’]
        secretkey=event['requestContext']['authorizer']['secretkey’]
        sessiontoken=event['requestContext']['authorizer']['sessiontoken’]
        dynamodb=boto3.resource(
            'dynamodb’,
            aws_access_key_id=accesskey,
            aws_secret_access_key=secretkey,
            aws_session_token=sessiontoken
        )
    else:
        if not dynamodb:
            dynamodb=boto3.resource('dynamodb’)
        returndynamodb.Table(table_name)

此代码是一个辅助函数,属于Order微服务的一部分。其作用是确定正在访问的Order表的类型。如果是共享租户,则需要使用API网关注入的凭据初始化数据库(DynamoDB)客户端。然而,如果是高级租户(隔离),则无需使用这些注入的凭据。

如果你查看这个辅助函数,它正是按照我这里描述的逻辑运行的。它有两个独立的分支,两者都返回一个用于访问数据的表对象。在函数开头,代码会检查当前是否为共享租户。如果是共享租户,则使用注入的凭据初始化数据库客户端。

如果是隔离租户,则使用默认凭据构建数据库客户端,这意味着它未被限制在特定租户范围内。由于在部署时已将执行角色绑定到函数,因此无需在代码中进行范围限制。本质上,范围限制已在函数部署时应用。无论通过上述哪种路径初始化的数据库客户端,都会在函数的最后一行用于创建表对象。

虽然我在这里是在讨论无服务器隔离模式的上下文中,但值得注意的是,这段代码在其他非无服务器环境中看起来会类似。我主要在这里包含它,是为了让你更好地理解如何在同一个无服务器函数中使用部署和运行时隔离策略。

路由基于隔离

每次尝试保护环境时,都应考虑可以在哪些层面上限制访问控制。这种思维方式也适用于我们的无服务器租户隔离模型。是的,我们可以且应该使用我之前描述的部署和运行时隔离模型。同时,您还可以在无服务器SaaS架构的API网关层引入更传统的保护措施。图11-13展示了如何在API网关层引入控制措施,作为隔离模型的扩展。

图11-13 在API网关层控制访问

在此示例中,我展示了我们在架构的API网关层可以使用的几种不同机制。如果我们从左到右依次分析这个图,你会看到整个流程始于包含租户上下文的入站请求(步骤1)。该请求携带的JWT进入API网关并由授权器处理。授权器会从JWT中提取租户上下文,并使用该上下文配置授权策略(步骤2)。

该策略可定义行为并启用API网关路由。为了更好地理解其应用场景,假设我们有一系列由API网关管理的REST路径。这些路径将租户请求路由到相应的租户函数(服务)。在此示例中,我展示了针对不同租户层级或配置文件的三个不同部署的Order服务。

当来自租户1的请求到达时,我希望确保该请求仅被路由到有效的租户1函数。此时,我的授权策略配置为阻止访问属于其他租户的路由(步骤3)。

您还可以考虑将此模型应用于无服务器环境,其中为每个函数组部署独立的API网关实例(隔离部署和池化部署)。图11-14展示了如何利用这些独立的API网关以更粗粒度的控制方式管理租户访问权限。

图11-14 通过独立的API网关限制访问

在此图中,我们有两个隔离的租户。每个租户都有自己的一组专用函数,通过专用API网关访问。现在,使用此模型,您可以直接将隔离策略应用于API网关,并附加租户特定的策略以防止跨租户访问。

需要注意的是,您应谨慎地为每个租户应用API网关。核心要点是隔离策略可能存在更多细节。虽然我们知道需要在资源被访问时进行保护,但您还可以在多租户架构的不同层级引入控制措施,从而提升SaaS环境的整体隔离能力。

并发与噪音邻居

在选择任何计算模型时,都必须考虑它如何控制租户对系统施加的负载。无服务器模型也不例外。人们可能会错误地认为,由于Lambda函数是托管的,因此无需担心租户会使函数过载或引发噪音邻居问题。

当然,我们知道这种想法并不现实。每个计算模型都必须施加限制以确保其可扩展性、健康性和弹性。那么,Lambda提供了哪些构造和机制来配置和控制函数的资源消耗?

为了更好地理解Lambda如何解决这个问题,让我们先看看Lambda如何扩展其函数。图11-15提供了getOrder()函数的Lambda扩展概念视图。

图11-15 无服务器函数的并发和扩展管理

在该图的左侧,您可以看到有一组租户正在调用getOrder()函数。每次向该函数发送请求时,Lambda都会执行该函数的唯一实例。这意味着在任意时刻,可能有多个该函数的实例正在运行。在此示例中,有六个函数实例并发运行;在实际场景中,并发实例的数量可能大得多。

由于Lambda无法无限扩展并发实例数量,我们需要考虑如何限制该函数允许的并发实例数量。此时,Lambda的“预留并发”概念就派上用场了。在此示例中,我将预留并发设置为100,表示同一时刻最多可运行100个该函数的并发实例。您可以想象这种机制在无服务器SaaS环境中引入了多种配置选项。

例如,它可以战略性地应用于解决方案中的微服务,为系统中关键的高流量部分分配更高的并发级别。它还可以与服务级别协议(SLA)关联,确保系统中的某些组件能够提供所需的吞吐量。

相同的机制也可用于定义应用程序的分层策略。例如,您可以在多租户环境中为每个分层函数部署分配不同的预留并发设置。您可以对基础层租户设置更严格的预留并发限制,以防止其产生的负载影响高级层租户。

相关内容将在第14章中详细讨论。关键要点是,预留并发量是多租户无服务器架构工具箱中的又一重要工具。在设计无服务器SaaS架构时,您应制定一个通用的并发策略,以确定如何最佳分配系统中各函数的并发资源。

超越无服务器计算

到目前为止,我的重点主要放在构建无服务器应用程序服务上。实际上,无服务器的范围远不止Lambda,它涵盖了AWS堆栈中各种服务。存储、消息传递、分析以及AWS堆栈中的其他众多服务都在积极添加对无服务器功能的支持。

传统上,运行在AWS上的许多服务都需要开发人员为特定服务实例选择并配置计算资源。例如,某些数据库要求开发人员预先确定数据库的计算资源占用量。这通常导致团队为确保数据库能够满足租户不断变化的数据库使用模式而过度配置数据库。

这对于SaaS组织而言是一个真正的挑战,并会削弱系统的成本和运营效率。现在,借助无服务器选项,这些服务可以减少对特定计算规模或配置的依赖。相反,计算成为服务的一部分,根据实际工作负载自动扩展和调整规模。目标是将无服务器模型的价值扩展到更广泛的服务范围,使您能够在SaaS架构的更多维度中实现无服务器的优势。

无服务器SaaS架构模式和策略 | 构建多租户SaaS系统 | IT书单