概述
随着我们深入探索SaaS环境中的多租户服务,必须开始关注这些服务在多租户模型中如何表示、访问和管理数据。虽然多租户数据的基本概念相对容易理解,但选择与环境要求相匹配的多租户存储策略时,会涉及许多细节和复杂性。
在您的SaaS环境中,存储特定工作负载数据的方式会受到多种因素的直接影响。合规性、噪声邻居、隔离性、性能、成本——这些因素中的任何一个都可能对您在多租户环境中选择如何表示数据产生重大影响。技术在其中也扮演着重要角色。每种存储技术都有其独特的约束条件、构造和机制,这些都需要在您的数据分区策略中予以考虑。
在本章中,我们将全面探讨数据分区考虑因素,重点突出通常会影响数据分区模型设计的不同因素。我们将首先回顾数据分区的核心概念,梳理适用于各类存储技术的通用原则与考量因素。同时,我们将探讨数据分区与租户隔离之间的内在关联,帮助您理解隔离机制如何在数据分区模型选择中发挥关键作用。
在掌握核心概念后,我们将重点转向探讨数据分区在不同存储技术和服务中的具体实现方式。在此阶段,我们将深入剖析每种技术,理解其存储服务特性如何影响数据分区设计。这也将帮助您更清晰地认识不同存储技术如何解决多租户存储的关键挑战(如噪声邻居、租户隔离、合规性等)。此外,我们将探讨多租户数据模型相关的权衡取舍,多租户数据模型,并识别在存储不同类型数据(对象、关系型、NoSQL等)时需要重点关注的关键领域。
数据分区基础知识
在探讨具体的多租户存储架构之前,我们需要先回顾一些基础的数据分区概念。首先,让我们明确我所指的数据分区概念。在我看来,当考虑在多租户环境中存储数据时,我始终关注的是如何根据数据类型、存储和管理技术、租户的访问方式等因素,对单个租户的数据进行分区。
数据分区并不假设每个租户的数据必须以某种方式存储在完全独立的存储结构中。事实上,正是这里,孤岛和池的概念再次发挥了关键作用。这些术语将在我们探讨如何描述租户数据的表示方式时再次出现。为了澄清这一点,让我们看看孤岛和池模型如何应用于存储。图8-1展示了隔离和池化策略在多租户数据分区领域的基本概念视图。
图8-1
在此示例中,我展示了Product和Order服务,它们均运行在由所有租户共享的池化计算资源上。然而,你会注意到,这些服务都采用了不同的数据分区模型。在左侧,产品服务为每个租户使用独立的专用存储结构。基于此架构,我们将这种存储称为孤岛式存储。
在右侧,您会看到我有一个使用不同数据分区模型的订单服务。这里仅有一个共享存储结构,用于存储和管理所有租户的订单。这就是我们所说的池化存储模型。
您还会注意到,我为每个数据分区方案映射租户到其数据的方式不同。对于隔离模式,租户通常基于某种命名规则与对应的隔离存储结构关联。在此示例中,我在每个存储结构名称前添加了租户标识符。您还会注意到,实际产品数据中没有任何内容引用或连接每个项与租户。这是不需要的。另一方面,订单表包含所有租户的数据,因此需要某种方式将单个项与每个租户关联。在此示例中,我引入了一个TenantId列来标识属于每个租户的项。
关键在于,无论您使用何种技术来存储数据,您仍然会使用“数据孤岛”和“数据池”这些术语来描述数据的表示方式。虽然这些概念是通用的,但它们在不同技术中实现的方式可能存在显著差异。这些差异可能在决定数据最终以数据孤岛还是数据池模型存储方面发挥关键作用。
值得注意的是,这两种策略在扩展性方面也存在差异。例如,孤岛策略在为每个租户添加独立的存储结构时,可能会引入扩展和管理挑战。另一方面,如果租户数据规模过大,将所有租户数据混杂在单一结构中也可能因数据过多或分布不均而导致扩展限制。
在探讨这些存储模型如何具体应用于特定存储技术之前,让我们先考虑一些在设计多租户存储策略时需要关注的跨领域问题。
工作负载、服务级别协议(SLA)和用户体验
每个多租户存储策略都必须高度关注规模和性能。多租户数据消费的性质在您解决方案的不同服务中可能存在显著差异,这要求您采取创新方法来支持租户不断变化的工作负载和消费模式。找到一种能够有效满足这些需求的存储策略具有挑战性。您今天看到的工作负载和模式可能与明天不同。新租户的加入也可能为系统存储架构带来全新挑战。
在设计数据分区策略时,您需要确保充分考虑每个存储方案在多租户环境中的性能表现。如何满足特定租户层级所需的任何服务级别协议(SLA)?如何检测并处理租户耗尽存储资源的场景?如何高效地为存储服务分配计算资源?这些只是在选择数据分区模型时需要考虑的性能和扩展性问题的一部分。
这一过程还包括估算数据的占用空间。了解租户在系统不同服务中存储的数据量,可帮助您预测系统如何扩展以满足租户的SLA要求。这对于理解数据如何可能突破所用存储技术的性能极限也至关重要。对于某些服务,您可能会发现,拥有过多数据的租户可能最终会破坏您的数据分区策略,并导致系统关键性能指标下降。了解这一点可能促使您采用不同的分区方法,以实现多租户数据的有效扩展。
噪音邻居是构建多租户存储策略时需重点关注的另一个领域。我们之前已将其作为更广泛的问题进行讨论,但噪音邻居在多租户存储场景中带来了独特的挑战。在此阶段,您需要制定工作负载配置文件,并理解租户如何消费系统中的多租户数据,识别可能需要引入分区、隔离或限流机制的区域,以防止租户施加可能影响其他租户体验的负载。这些策略同样对管理环境的可用性至关重要——尤其是当您在共享存储中遇到噪声邻居问题时。
计算资源的规模规划也是其中重要一环。确定支持工作负载和服务级别协议(SLAs)所需的计算资源水平往往充满挑战。在不导致为满足吞吐量要求而过度配置存储资源的情况下,找到平衡点尤为困难。
影响范围
尽管任何架构的目标都是尽可能预防故障,但仍存在一些场景需要团队采取额外措施,确保一个租户的故障不会影响其他租户的体验。这在多租户环境中尤为重要,因为系统中的一个故障可能影响所有客户。
现在,在选择存储模型时,一些团队可能会将影响范围纳入整体策略,倾向于将数据进行隔离以减少潜在故障的影响范围。这可能意味着将特别关键的数据家族进行隔离。在这种方法下,例如,如果一个隔离的数据库遭遇致命故障,该故障的影响范围可被限制在单个租户内。这同时允许运维团队在隔离环境中处理该问题。
这种策略在分析SaaS环境的部署足迹时尤为有效。随着新版本和功能的推出,每个租户的隔离数据可独立更新,从而实现对数据结构的平滑部署和更新,而不会影响所有租户。
虽然将数据隔离以限制故障影响范围有其优点,但我仍会谨慎考虑这一方案。这可能适合某些环境,但也可能成为影响环境整体敏捷性、成本效率和运营特性的依赖性措施。
隔离的影响
隔离影响多租户架构的方方面面,它无疑在塑造您选择的数据分区模型中发挥着作用。事实上,这往往是数据分区与隔离之间界限变得模糊的地方。例如,我可能会基于隔离要求(至少部分基于此)选择一种分区策略。
然而,这里容易产生混淆的是,团队有时会将数据分区策略等同于租户隔离。因此,如果他们为某类数据选择了孤岛模型,就会描述该数据为“隔离的”。虽然你可能选择孤岛模型来实现隔离,但仅将数据孤立并不意味着数据是隔离的。如第9章将详细讨论,隔离代表了一种超越数据存储方式的强制执行层。它无论数据是孤立还是共享,都强制执行并限定对数据的访问权限。
我更倾向于将隔离视为影响数据分区模型选择的因素,同时承认实现隔离仍需通过独立方式完成。这种隔离对数据分区的影响在选择不同存储技术时尤为重要。如果你至少部分基于存储技术对隔离级别的支持能力来选择数据分区策略,那么你必须同时考虑所选存储技术如何支持在隔离策略。例如,我可能将租户数据隔离到独立数据库以实现隔离,却发现该数据库不支持在数据库级别定义租户级隔离策略。这可能促使我考虑其他方案。
当考虑共享数据分区策略与隔离时,这一问题更为突出。如您所想,支持强制执行隔离的策略在此类场景下更为罕见。
管理与运维
作为架构师和开发者,我们在选择数据分区策略时自然会关注性能和隔离性。然而,同样重要的是要考虑数据分区策略如何影响SaaS环境的管理和运维开销。例如,在权衡孤岛式或池化分区选项时,您应考虑数据足迹如何影响运维团队的体验和敏捷性(备份、部署、迁移、遥测等)。
出于显而易见的原因,池化数据分区模型通常能提供最高的运营效率。在池化模型中,数据以更简洁的结构进行管理和运营。例如,对数据的任何更新只需通过单一操作即可应用于所有租户。此外,当所有租户使用共享构造时,将运营视图整合为存储服务性能和健康状态的分析过程也将变得更加简便。
采用孤岛式数据分区时,您将采用更分布式的模型,这会增加管理和运营的复杂性。在此场景下,您的DevOps工具需单独处理每个租户的数据变更或迁移。在大量孤岛式数据结构中同步这些变更无疑会增加复杂性和时间成本。在数据孤岛划分方案中,您的运维工具也将面临额外负担。您的工具现在需要从环境中各个孤立的租户数据库中汇总健康状况和活动视图。
备份和恢复也应纳入讨论范围。您的分区模型将如何影响您备份和恢复租户数据的能力?对于孤立数据,这相对简单。然而,对于共享数据,您需要考虑如何从共享存储服务中获取租户数据的快照(并考虑如何在不影响其他租户的情况下恢复租户数据)。这里存在许多影响决策的变量。关键在于,在选择数据分区策略时,您需将此问题纳入考量范围。
最终,这通常需要在效率和可管理性之间权衡。请记住,您的选择可能会对实现业务的规模和敏捷性目标产生重大影响。
选择合适的工具
部分团队将数据分区视为一个非此即彼的决策点,即选择一种能满足解决方案需求的存储技术。例如,团队会进行分析后,全盘采用看似最适合解决方案需求的单一数据库技术。这种决策甚至可能受开发人员经验驱动,即团队基于对技术的整体熟悉度选择存储策略。
很明显,这种方法在选择多租户存储方案时并不理想。现实是,影响存储技术选择的变量众多,且更重要的是,这些变量可能因系统不同部分而异。
若将系统视为由多个封装底层存储的服务组成的集合,则应以服务为单位评估存储技术选项。适合某一服务需求的存储技术,可能并不适合另一服务的需求。目标是将每个服务孤立考虑,使其能够基于所支持的工作负载、隔离需求、合规性要求、管理特性以及所有其他可能影响其资源占用因素,提供最佳体验。这包括根据每个服务的需求,选择独立的或共享的数据分区策略。是的,您可能会发现一些适用于许多服务的通用存储模式。然而,您仍然需要为构建的每个服务提出这些问题。
默认采用池化模型
在某些情况下,可能无法明确哪种数据分区模型(孤岛或池化)最适合特定工作负载。您可能需要等待观察并分析租户工作负载的规模、性能和运营行为,以更好地确定首选方案。
一般来说,在这些情况下,我的建议是默认将系统中的所有存储采用池化模型。池化存储在成本、运营和灵活性方面的优势往往非常明显,因此团队会尽可能长时间地坚持使用该模型。在此模式下,您本质上是在迫使任何孤岛数据通过满足一组明确的业务需求来证明其价值,从而获得脱离孤岛的资格。这些需求需能够合理化在孤岛模型中管理、运营和部署数据所带来的权衡。
确实,某些工作负载和数据类型在系统上线之初就可能是孤岛化的理想候选对象。然而,你需要避免的陷阱是,对于那些可能并不需要孤岛化体验的系统组件,过度倾向于采用孤岛化模型。每次在SaaS环境中选择将任何资源进行孤岛化时,您都应挑战自己的假设,并确保已将孤岛的范围界定在适当的边界内。
支持多个环境
当我们深入探讨不同技术和服务中的数据分区细节时,会在孤岛模型中遇到创建租户特定构造(表、数据库、集群等)的场景。每次创建此类租户特定构造时,都必须为其分配一个与租户关联的名称。
解决此问题通常需要制定一种命名约定,确保资源具有唯一标识。当考虑支持多个环境(如QA、staging、生产环境)时,情况会变得更复杂。此时,您需要判断命名约定是否需要包含对托管孤岛存储资源的环境的引用。
对于某些服务和策略,您可能有一个隔离构造,不会导致名称冲突。例如,对于某些云存储服务,您可能为每个环境创建独立的账户。在此模式下,资源名称可能在账户级别进行作用域限制,确保仅在该账户内可访问。然而,也存在名称在所有账户中全局有效的场景。在这种情况下,您仍需制定包含环境支持的命名规范。这通常不是一个重大问题。然而,在定义数据分区模型时,值得考虑这一点。
适配规模的挑战
贯穿本书的一个核心主题是将基础设施资源消耗与租户活动相匹配。这是每个SaaS企业追求效率的根本所在。在这一扩展挑战中,容易被忽视的一个维度是存储计算资源消耗的效率。在此领域,团队往往难以找到一种数据分区和存储策略,以实现计算资源消耗的优化。
在多租户环境中,存储计算的规模调整更为复杂。借助我们的服务,我们可以根据负载需求水平扩展计算资源,这使得根据租户工作负载动态扩展或缩减计算资源变得更加容易。然而存储则通常不具备这一灵活性。存储服务在初始环境配置时,往往要求用户选择特定的计算配置文件。这意味着存储计算资源在所有租户工作负载中保持固定。
这为多租户环境带来了特别棘手的问题。图8-2更清晰地展示了多租户环境中存储容量规划的挑战。
图8-2
在此示例中,我有两个租户正在使用一个池化模型中的服务。该服务根据租户的工作负载水平进行水平扩展。所有该服务的实例都指向某个池化存储服务以管理其数据。在此示例中,假设我的存储服务是运行在云中的某个关系型数据库服务,且该服务在配置过程中要求我们选择计算规格。
问题是:您应该为该存储服务的计算资源选择多大规格?在图中,我已标出这一难题,展示了计算规格随时间变化的情况。例如,下午4点时计算需求较低,而到晚上7点负载会大幅增加。更糟糕的是,多租户环境中不断有新租户和新工作负载加入,这些因素会持续改变资源消耗模式。
对于孤岛式和池化数据分区模型,解决此问题的方法有所不同。在池化模型中,很难准确配置存储服务的计算资源。通常,为了应对多个租户对池化存储的波动性工作负载和突发需求,您需要过度配置计算资源,并接受这将对整体环境的成本效率策略产生负面影响。
如果您采用的是数据孤岛模型,需要考虑的变量较少,这应能使资源配置相对更为简单。各租户的负载特性可能更为可预测,因此您可能能够选择更适合孤岛租户需求的计算规格。即便在这种模型下,您仍需接受一定程度的资源超配。在一天中的某些时段,孤岛租户的存储可能处于空闲状态或仅轻度使用。
当然,对存储消耗进行profiling可为您提供更多数据,以更好地为隔离和共享模型配置计算资源。在某些情况下,团队会利用这些数据持续调整存储的计算配置,以尽量减少过度配置。然而,这种持续的调整可能会增加运营复杂性和效率低下。
总体而言,这个问题没有万能解决方案。如果您的存储服务要求绑定到固定的计算配置文件,您将不得不接受随之而来的挑战。理想情况下,资源过度配置带来的成本不会对您的SaaS业务整体利润率产生重大影响。
吞吐量与流量控制
解决资源适配问题的一种方法是采用存储吞吐量和限流策略。这些机制允许您引入政策,以更好地管理存储资源的消耗,包括根据租户层级和用户角色提供不同的体验。核心思想是利用存储技术的内置性能配置选项,找到最适合存储工作负载的性能和规模挑战的设置组合。
关键在于,作为数据分区模型的一部分,您需要考虑如何应用这些策略。这包括思考这些策略在池化存储和孤岛存储模型中的差异。这两种分区方案可能需要对每个存储体验的吞吐量和限流配置采取不同的方法。
无服务器存储
在探讨这一容量规划问题时,我们完全聚焦于那些依赖预先规划计算资源占用空间的存储技术。好消息是,越来越多的存储技术正在采用无服务器模型,摆脱对特定计算配置的绑定。无服务器存储服务会隐藏存储计算配置的细节,根据租户当前活动水平自动调整底层计算资源。计算资源会根据系统负载自动调整,这与多租户环境的需求完美契合。
无服务器存储模型已应用于多种不同的存储技术。以AWS为例,您可使用AmazonAuroraServerless,这是一款采用无服务器模型的关系型数据库存储服务。AmazonDynamoDB同样支持无服务器架构,提供无需手动配置计算资源的托管NoSQL存储服务。我预计,未来大多数云存储技术都将以不同形式支持无服务器模型。
在接下来的章节中,当我们探讨不同的存储技术时,您需要特别关注无服务器模型。例如,在使用池化数据分区模型时,您会发现无服务器模型可能是一个极具吸引力的选择。关键在于,考虑到多租户存储消耗的不确定性,您很可能会倾向于将无服务器作为首选方法,以使存储消耗和成本与租户的活动保持一致。无服务器模型还简化了存储服务的运营和管理,消除了不断追踪不断变化的计算规模需求的需求。
在这些基础数据分区概念确立后,我们可以开始探讨这些原则如何通过不同存储技术实现。在接下来的章节中,我将把这里讨论的通用概念映射到更具体的实施策略,并突出每种存储技术带来的不同细节。可用的存储选项数量庞大,无法一一涵盖,但我已选取了一些常见的存储技术示例,这些示例应能很好地展示这些技术内部的差异如何影响您的数据分区策略。
关系型数据库分区
现在我们已经了解了数据分区的基本原理,让我们开始探讨这些概念在关系型数据库中的具体实现。在讨论如何在关系型数据库中实现数据分区时,大部分讨论都集中在系统规模、可扩展性和运营效率等方面。
关系型数据库通常支持多种不同的数据分区构造,这些构造可用于对数据进行分区。一个不变的特性是依赖于模式。这意味着,无论我们选择哪种数据分区模型,数据都将以与模式绑定的方式进行表示。这在多租户环境中可能带来挑战,因为我们可能需要在不同模式表示之间平滑迁移租户。这会增加存储模型整体敏捷性的复杂性。
这并不意味着关系型数据库和SaaS不适合。事实上,存在一些有说服力的用例,其中关系型模型可能与特定工作负载。同时,在选择存储策略时,您应确保充分评估关系型模型在迁移和敏捷性方面的影响。
池化关系数据分区
让我们先看看如何在关系型数据库中实现池化模型。在关系型数据库中存储数据的机制相当简单。这里,您只需在现有数据库设计中引入一个租户标识符,将表中的项与特定租户关联。最终结果,如图8-3所示,正是您所期望的。
图8-3
我展示了一个存储多租户数据的客户表,该表采用池化模型。表中新增了一个TenantId列,作为该表的外键,并作为访问数据的主索引。该列中的租户标识符GUID代表与给定行关联的每个租户的ID。
任何池化存储模型的基本思想是,我们将所有租户的数据混合存储在一个共享的存储结构中。随后,原本作为该表外键的数据将转变为二级索引或过滤条件。
在此示例中,我们的CustomerId列已成为该表的二级键。这意味着,我们与该表的交互必须在访问该表数据的查询中引入租户标识符。在此示例中,我希望包含一个包含与不同租户关联的行数据的表。行1和行3与租户1关联(如图左侧所示)。行2和行6与租户2关联。这种模式将在系统中所有存储租户数据的池化表中重复出现。
引入TenantId列还为我们提供了潜在应用租户隔离策略的途径。这些策略(在第9章中描述)将展示如何过滤表的视图并防止跨租户访问数据。每种数据库技术可能有其独特的策略定义方法。关键在于,在某些情况下,池化数据的分区模型可能会影响可应用于数据的隔离策略。
隔离的关系型数据分区
虽然池化关系模型较为简单,但在关系数据库中对数据进行隔离时,通常需要考虑更多选项。关系数据库技术可能提供多种构造来定义数据的隔离边界。图8-4展示了在关系数据库中隔离数据时可使用的部分不同构造。
图8-4
在该图的左侧,我展示了按租户划分的数据库实例模型。以亚马逊关系型数据库服务(RDS)为例,这本质上相当于为每个租户单独部署独立的基础设施。实例的计算资源及所有基础设施资源均专属单一租户。
中间部分展示了数据库的概念。在此模型中,每个租户在数据库实例内运行一个专属数据库。尽管每个数据库完全独立,但它们共享父数据库实例的底层计算资源。最后,右侧展示了通过独立表对租户数据进行隔离的模型。这些表均创建于共享数据库中。
无论采用哪种模型,将名称或标签与这些隔离的关系型存储结构关联起来的责任都落在环境层面。运行时,您的代码需要将给定的租户请求映射到其对应的数据库实例、数据库或表。
对于这些不同的数据孤岛架构,您需要评估该架构是否能够扩展以满足您的需求。以RDS为例,其中存在一些限制会影响您可用的选项。您在单个数据库实例中只能创建一定数量的数据库。表的数量同样受限。您选择的技术可能还会引入其他类型的限制,这些限制也应纳入您的扩展计算中。一般建议是确保您已仔细评估所选关系型技术带来的各种限制范围。通过对潜在增长进行基本建模,潜在增长进行基本建模,将有助于判断您是否在关系型数据库环境的扩展限制范围内。
选择数据隔离可能源于业务领域对安全性和合规性的隔离需求。确实,关系型数据库的粗粒度隔离结构通常是满足此类需求的吸引人方案。然而,在评估不同关系型技术时,您可能会发现仅通过存储隔离仍无法完全满足隔离需求。在某些情况下,这些隔离的关系型构造可能不支持定义防止跨租户访问的策略。因此,即使数据被隔离,也没有机制可用于实施robust隔离策略。关键在于,您选择的隔离构造是否支持通过隔离策略防止跨租户访问(更多内容请参见第9章)。
使用AmazonRDS,您还可以选择多种数据库引擎:MySQL、PostgreSQL、MariaDB、SQLServer、Oracle等。这些引擎内部存在细微差别,可能为您的隔离分区模型增添新的维度。尽管每个云服务提供商和/或关系型数据库供应商可能会对这一故事添加自己的特色,但整体思维方式和方法与我在此概述的模式和考虑因素基本一致。
NoSQL数据分区
接下来我们将探讨NoSQL存储。NoSQL存储提供了多种不同选项,每种选项都可能引入新的构造,从而改变您实现池化与孤岛式数据分区策略的方法。为了使内容更具concreteness,我将重点介绍亚马逊DynamoDB,该服务为开发者提供了一项托管的NoSQL存储服务。
NoSQL存储的无模式特性为多租户开发者带来了显著优势。想象一下在大型池化环境中部署包含新数据结构的变更。在关系型数据库中,您可能需要协调一系列复杂的迁移步骤来更新模式,作为新功能发布的一部分。而在NoSQL环境中,相同变更通常更容易实施。缺乏模式通常消除了对复杂迁移脚本的依赖,使团队能够以更快的速度推出新功能并减少部署次数。这与SaaS的核心价值主张高度契合,有助于提升运营敏捷性、创新能力和效率。
使用DynamoDB,您会发现我们用于分区数据的存储结构非常有限。这里没有数据库或实例的概念。DynamoDB的优势在于它没有继承支持不同现有关系型数据库引擎所带来的复杂性。这也意味着DynamoDB与其他AWS云服务具有更紧密的集成,能够与身份和访问管理(IAM)机制实现更细粒度的集成,这些机制可用于定义隔离策略(详见第9章)。
我经常收到关于团队在SaaS解决方案中如何选择NoSQL和关系型数据库的提问。在我看来,这主要取决于应用场景和使用案例——隔离需求、敏捷性影响以及性能考量。通常,我建议团队从NoSQL开始,让实际业务需求引导你选择关系型数据库,当确实有必要时再转向关系型数据库。
NoSQL数据池化分区
在NoSQL中池化数据与关系型数据库中的做法非常相似。在DynamoDB中,所有数据都存储在表中。这些表中的数据包含系统中所有租户的信息。它们基于一个租户标识符进行混合存储,该标识符将表中的每个项与对应的租户关联起来。图8-5展示了一个通过租户标识符索引的NoSQL表的简化视图。
图8-5
这与我们之前查看的池化关系型存储示例完全对应。我们有一个DynamoDB表,其中包含多个项,每个项都有一个JSON文档,用于存储该项的数据。本示例中的项目恰好代表员工。我已在每个项目中插入TenantId属性以将其与租户关联。该表中存在两个租户:租户1与第一个和第三个项目关联,租户2与第二个项目关联。
我已标注表中的每个租户标识符为DynamoDB中所指的分区键。这本质上将该属性设为表的主键,从而提升访问单个租户数据时的性能。在引入租户功能前,EmployeeId是这些表的主键。现在它将在DynamoDB中被视为排序键。
孤岛式NoSQL数据分区
在许多孤岛模型中,我们倾向于寻找某种逻辑映射,将数据映射到一个数据库实例中以存储孤岛数据。然而,在DynamoDB(及其他托管存储服务)中,这种结构并不存在。在此场景下,我们唯一的孤岛方案是采用“每租户一张表”的模型来隔离数据。图8-6展示了这种DynamoDB孤岛模型的概念视图。
图8-6
使用DynamoDB进行数据孤岛化并没有什么特别之处。我们本质上为每个租户创建了一个独立的表。然后,在服务实现中,我们将每个传入的请求映射到相应的表。在此示例中,我在表名前缀添加了租户标识符以关联特定租户。命名策略需谨慎选择。DynamoDB中的表名全局管理,因此必须确保命名方案能为每个表分配唯一名称。部分团队采用生成标识符与“友好名称”的混合方案。每次考虑采用隔离存储模型时,我们还必须评估隔离策略是否能满足解决方案的扩展需求。与关系型数据库类似,我们也需考虑DynamoDB(或所用其他NoSQL解决方案)的性能限制。此外,还需思考“每租户一张表”模型对环境部署及运维模式的影响。
NoSQL调优选项
总体而言,DynamoDB的数据分区选项相当直观。然而,在使用DynamoDB存储多租户数据时,还需考虑一些额外因素。例如,DynamoDB允许您为表配置容量模式。对于每个表,您可以选择按需容量模式或预配置容量模式。在按需模式下,DynamoDB会根据租户的实际负载自动扩展。这显然非常适合多租户工作负载中租户消费的不确定性。
对于采用池化分区模型的情景,这种模式尤为有用。预配置模式更适合您对所需支持的活动水平有较好预估的环境。在此模式下,您可根据工作负载特征配置最小容量、最大容量及目标利用率。系统将维持一个稳定的目标容量水平,同时限制租户超出最大容量的可能。您可能将此策略应用于系统关键瓶颈中的表,以确保最大化吞吐量(尽管可能导致一定程度的过度配置)。您还可以看到,在某些情况下,这可能与消费模式更可预测的孤岛表相契合。
显然,当您探索其他NoSQL解决方案时,可能会遇到其他分区和配置选项。但本质上,这归结于确定可用于表示孤岛化场景的构造。池化模型在不同NoSQL实现中通常会采用类似的实现方式。
对象数据分区
关系型和NoSQL存储服务中的数据分区是开发人员容易理解的领域。对于这些存储技术,您有相对自然的映射到池化和孤岛模型。然而,当我们进入其他存储技术时,映射就不再那么直观。为了更清楚地说明这一点,让我们看看如何在对象存储服务中实现这些多租户数据分区模型。
在对象存储中,我们不再通过数据库和表的视角查看数据。相反,我们将管理的资产视为一系列对象,这些对象本质上相当于存储在不同上下文中并可被检索的文件。为了便于讨论,本文将聚焦于亚马逊的简单存储服务(S3),探讨S3提供的不同机制来分区多租户数据。
本文中介绍的技术特定于S3,可能与其他云环境中的对象存储服务存在差异。尽管如此,我们希望对S3策略的回顾能帮助您更好地理解,随着存储技术从一种迁移到另一种,数据分区方法会如何变化。原则保持不变,但实现这些原则的具体机制往往需要您考虑更多样化的选项。
池化对象数据分区
在对象存储中,我们主要采用经典的分层文件夹结构来组织和访问对象。例如,在S3中,所有对象都存储在桶(bucket)中,桶是顶级存储单元。这些桶还可以使用前缀键来分组和访问对象。最终效果类似于传统的文件/文件夹结构。
使用池化模型时,我们首先需要确定租户对象在S3中的存储位置。由于所有租户对象将被混合存储,因此无需为每个租户单独创建桶或前缀键。同时,应用程序服务可能需要对租户对象进行分组操作。这表明我们应使用前缀键来确定每个租户对象的存储位置。
让我们通过一个示例来了解如何在S3中实现池化数据分区模型。图8-7展示了使用前缀键在池化模型中存储租户对象的一系列桶的概念视图。
图8-7
在左侧,您可以看到我创建的简单桶层次结构。在树的顶层,我为每个环境(prod、dev和staging)创建了桶。然后,在每个桶内,我添加了前缀键。在此示例中,假设这些前缀键对应于各自管理S3对象的独立服务。
在图的右侧,我们展示了来自saasco-dev-objects桶中与catalog-service前缀键关联的一系列对象的池化存储表示。由于这是池化模型,我们为对象名前缀了租户标识符,以将每个对象与对应的租户关联。访问这些对象时,调用方需在对象名称前添加租户标识符,才能成功获取特定租户的对象。虽然这种方法代表了对池化模型和S3更为纯粹的理解,但它确实似乎增加了不必要的复杂性。
租户标识符的前缀添加似乎难以管理,并为任何调用客户端增加了摩擦。随着我们现在转向孤岛模型,您将看到孤岛模型如何为构建者提供一种与池化模型略有不同的变体,消除了池化模型带来的部分命名开销和映射。
隔离对象数据分区
使用S3进行对象隔离有两种方法。最简单的选择是采用“每个租户一个桶”的模型。该模型本质上要求为每个新租户创建一个新桶,并将所有对象存储在该桶中。如果您的租户总数少于S3服务的桶上限(目前为1,000个桶),则可以考虑此选项。但这意味着您的系统在部署时需要动态创建符合S3命名规范和唯一性要求的全新存储桶。
同时,系统中需包含一个机制,用于将租户映射到其对应的存储桶。如果这些限制存在问题或您希望减少命名冲突的担忧,您可以对之前讨论的池化策略稍作调整,并借助前缀键来实现隔离模型。图8-8展示了如何修改前缀键结构以实现租户对象的隔离。
图8-8
你会发现,这与池化模型之间存在微妙差异。我基本上将前缀键作为数据隔离的边界,将所有租户数据归类到单个键下。在我们之前关于隔离存储与池化存储的讨论中,我们主要强调了数据隔离带来的额外复杂性和开销。而在S3模型中,您实际上并未承担过多的额外开销。
在池化设置中,如果我真正混合对象,就必须通过某种方式扩展对象名称来实现租户绑定。而在此处,只需通过细化键,即可获得一个更易于使用、消费和管理的模型,且未引入显著的缺点。在某些情况下,数据孤岛架构可能为通过IAM实现隔离提供更好的机会。然而,这对于本S3孤岛架构影响甚微。我可以在桶级别或前缀级别配置IAM策略,从而防止跨租户访问,无论采用何种策略。
数据库访问管理
虽然S3确实提供了用于访问对象的API,但某些用例需要更动态的方法来定位租户对象。在某些情况下,您可能需要支持更奇特的、元数据驱动的方法来定位租户的S3对象。例如,想象一下,想要查找所有符合用户指定条件的对象。此时,您要搜索的元数据可能位于对象存储之外的某个位置。
这时,您可能需要在对象和数据库之间引入一个层,用于保存您要查询的所有元数据和属性。图8-9提供了此用例的概念视图。
图8-9
在图8-9的顶部,我有一个目录服务。该服务管理目录中产品的一系列属性(如Active、Category等)。目录服务管理的属性之一是每个目录项关联的图像。这些图像的实际对象存储在S3中。现在,假设你的服务需要请求一组具有特定产品属性的对象。为了支持这一功能,你会发现我引入了一张表,该表按租户标识符进行索引,并包含目录中项的其他元数据。
在该表的完整版本中,我们会包含更多属性。然而,为了简化示例,我将属性限制为几个可能作为良好搜索条件的属性(如活动状态和类别)。我还添加了列来引用前缀键和映射到S3中对象的文件名。在图8-9的底部是存储目录图像对象(以及潜在的其他服务数据)的存储桶。所有组件就位后,我的目录服务现在可以查询该表以查找满足指定条件的租户对象。
事实上,你会发现该表的第一行和最后一行与同一租户相关联。我的查询可以要求获取某个租户在服装类别中的所有活跃目录项,查询结果将返回可用于引用S3中存储的每个项的前缀和对象名称。我承认,这确实是一个比较特殊的情况。然而,这是一种特别有用且灵活的方式,可以为对象的分区提供一种替代思路。在这里,分区操作全部在表中完成,从而将租户映射从对象数据库中移除。我可以利用这个表为对象引入元数据,这些元数据可以在访问特定对象时作为过滤条件应用,以满足特定用例的需求。
OpenSearch数据分区
如你所见,多租户数据分区方案在不同存储服务之间会呈现出截然不同的形态。为了全面探讨这一主题,我认为有必要看看搜索与分析领域中数据分区是如何实现的。本示例将以亚马逊的OpenSearch服务为例,该服务基于ElasticSearch开发。在OpenSearch中,我们现在使用新的存储结构和机制来分区租户数据。
数据如何进入OpenSearch、如何最大化成本和运营效率,以及如何隔离数据,在将孤立和共享模型映射到OpenSearch集群的各个组件时,都会有所不同。为了更好地理解OpenSearch的数据分区选项,让我们先看看在OpenSearch环境中存储和管理租户数据时会用到的不同机制(如图8-10所示)。图8-10展示了OpenSearch服务核心组件的高级视图。
最外层是域,代表数据分区的最粗粒度单元。这些域本质上是构成搜索和分析体验的集群。在此处,您将配置节点大小和资源占用,从而确定体验的扩展配置文件。然后,在右侧,是一系列与给定域关联的索引。这些索引存储了将用于搜索和分析体验的索引文档。
图8-10
池化OpenSearch数据分区
在OpenSearch的池化模型中,我们需要采用一种模型,其中租户文档在单个索引中混合存储。这种方法在许多方面遵循我们在所有池化构造中观察到的模式。每次我们有混合数据时,都需要有一种方式来识别与给定租户相关的数据。如果我们查看存储在索引中的各个文档,会发现必须在每个租户文档中插入一个租户标识符。该标识符将在任何搜索操作中用于限制对特定租户数据的访问范围。
图8-11展示了OpenSearch池化模型的示例。在此示例中,我们的池化存储在单个域中运行所有租户,且对于此产品样本数据,所有租户文档均存储在共享索引中。该索引中的每个文档包含一个TenantId属性,在此示例中由GUID表示。这些文档中的每个产品还被分配一个ProductId,用于唯一标识单个产品。我基本上是将通常用于表示产品的文档添加了一个TenantId属性。
图8-11
通过这种经验,您将获得池化数据分区模型带来的所有优势和劣势。规模经济和运营特性均可从共享基础设施中获益。然而,域的规模调整可能较为困难,并可能导致过度配置。
这又是一个场景,此时借助无服务器技术可带来实际价值,并引导您更倾向于使用OpenSearch无服务器服务,该服务可简化解决方案的规模调整和扩展特性。您还需考虑OpenSearch如何对数据进行分片,并确定租户数据的占用空间和分布是否影响环境性能。
孤岛式OpenSearch数据分区
在实现隔离的OpenSearch数据分区模型时,您有两种选择。我们将首先探讨基于域的租户模型。采用此方法,您需要为每个租户创建一个完全独立的集群。此模型的示例如图8-12所示。
图8-12
这是一个非常直观的模型,它依赖于OpenSearch中最粗粒度的数据存储结构来存储每个租户的数据。这种方案能够满足那些对合规性、隔离性、噪声邻居等问题有严格要求的租户。然而,它也会增加运维复杂性并影响环境的成本效率。实现OpenSearch隔离数据分区的另一种方式是通过租户级索引模型。如图8-13所示。
图8-13
采用这种方法,我们现在拥有一个由所有孤岛式租户共享的单一域。然而,每个租户都拥有自己的索引。这使您能够在无需为每个新租户验证整个集群的情况下实现隔离。这也意味着您同意在这些租户请求之间共享域的计算资源。对于部分用户而言,这是一种不错的折中方案:您既能通过计算资源的规模化利用实现成本优化,同时仍为租户提供确保数据不被混杂的隔离单元,其数据不会被混杂,并可能受益于更明确的隔离边界。
然而,这种隔离模型也存在缺点。虽然该模型更易于管理,但我们现在面临资源适配问题。域的共享计算资源必须根据多个租户的工作负载和活动模式进行规模调整。这可能导致“噪音邻居”现象或其他与存储计算资源共享相关的性能问题。此外,与任何隔离资源一样,您需要考虑该模型的可扩展性。服务可能对允许的孤岛索引数量存在限制。
混合模式分区模型
除了上述的孤岛式和池化模型外,您还可以考虑实施一种混合模式模型,该模型为平衡租户需求提供了另一种方法。在这种混合模式下,您本质上是在共享域内同时支持孤岛式和池化模型(如图8-14所示)。
图8-14
在左侧,我们仍然使用共享域并消除了为租户单独验证群集的需要。不同之处在于,我们在此域中支持孤岛和池化索引。在顶部,我展示了两个具有专用索引的premium层租户,它们拥有专用的索引。然后,在底部,我有一个池化索引,将所有基础级数据混合在一个单一结构中。
这种方法确实使我们能够限制OpenSearch基础设施的复杂性和占用空间,同时仍支持隔离和池化模型。然而,这种策略在域的规模和配置方面可能会带来实际挑战。您还可以考虑一种模型,即为所有基础级租户使用单独的域。这将消除基础级租户影响高级租户规模的场景。
分片租户数据
到目前为止,我主要关注的是基于环境中不同存储结构的孤岛式和池化数据分区策略。通常,如果能利用存储服务内置的能力,会使事情更加简单。然而,当存储模型的规模和性能无法满足解决方案需求时,您可能需要考虑引入自定义机制来分片租户存储。
分片(Sharding)通常指将资源拆分为多个部分以应对规模、容量等需求。我见过的一种模式是自定义分区,即代码负责将租户映射到不同的存储结构。例如,假设您有一个使用池化模型的关系型数据库。然而,您发现租户的扩展性和性能已超出您通过池化模型充分处理其工作负载的能力。您希望保持租户的池化,但将所有租户放在一个存储数据库中并不现实。
在这种情况下,一些团队会考虑实施自己的分片策略,将租户池分配到不同的存储结构中。图8-15展示了分片模型的概念视图。
图8-15
在此示例中,我在图的右侧以池化模型运行两个独立的租户分片。您会注意到,我已将这些租户分布在不同的数据库中。因此,我基本上拥有两组池化租户。现在,如果左侧的服务需要向数据库发起请求,其数据访问库必须查找与给定租户请求关联的分片。这与我们在第3章中讨论的Pod部署模型非常相似,当时我们将租户组部署在Pod中。
这里的概念类似,但仅应用于环境的存储层。这允许您限制租户的影响范围,并通过将这些租户的工作负载分布到不同的分片来潜在克服池化噪声邻居问题。随着新租户的加入,您可以添加新的分片以继续分布负载。这绝非理想模型,且无疑会为您的SaaS环境增加复杂性和摩擦。然而,我在此提及是因为某些工作负载或业务需求可能认为这是一种合理的权衡。
数据生命周期考虑
在探讨数据如何分区和表示时,我们还需考虑数据的整体生命周期。租户可能经历的不同状态变化可能会影响其数据的表示方式。为了更好地理解这些影响,让我们来看一个具体的场景:您有一个分层环境,为不同租户层级采用了不同的存储策略。
基础层租户完全使用共享数据池,而高级层租户则拥有部分独立的存储结构。现在,假设有一个租户需要从基础层升级到高级层,这将意味着什么?在这种情况下,我们需要部署自动化流程,能够平滑地将数据从共享环境迁移到独立存储模型。自动化流程还需考虑数据迁移对系统整体负载的影响。若迁移导致过高负载,可能影响环境的可用性。
这是一个难题,目前尚无优雅的解决方案。然而,您必须将此问题纳入考量范围。退役是另一个可能影响数据分区策略的领域。退役一个租户时,最核心的部分通常在于如何处理租户的数据。您是计划遍历整个系统并删除租户数据?还是将数据归档,以便租户在返回时恢复数据?这些都是在考虑租户数据在环境中如何表示时需要考虑的因素。这是另一个需要仔细构建自动化工具以执行退役策略的领域,确保在不影响环境性能的情况下应用这些策略。
备份与恢复也是租户生命周期讨论的一部分。如果存在共享数据,这将特别复杂。此外,您的环境中可能存在跨多个存储结构分布的租户数据,可能采用多种孤立或共享的存储模型。这显然需要一个精心协调的机制,能够成功获取并备份租户数据。对于部分场景,不存在按租户进行备份与恢复的概念。相反,数据状态被视为全局构造。哪种模式最适合您的环境,将取决于您的业务领域特性、租户类型以及其他诸多因素。
多租户数据安全
保护租户数据是任何SaaS环境的基本要求,您的架构应已采取强有力的措施确保一个租户无法访问另一个租户的资源。事实上,第9章将专门探讨这一主题,探讨一系列可用于确保资源(包括数据)免受跨租户访问的策略。然而,某些领域可能对数据存储提出额外安全要求。
对于部分场景,这可能更侧重于通过加密确保数据在静止和传输状态下的安全性。您对数据的加密能力主要取决于所使用存储服务的加密功能。不过,大多数AWS存储服务均支持多种加密策略。对于某些用户而言,数据加密可能仍不足以满足需求。您可能需要为租户提供加密功能,并要求其拥有管理数据访问权限的密钥。在这种情况下,您需要引入机制来创建并分发这些密钥给各租户。
这些密钥可能具有生命周期,需要作为SaaS环境运营足迹的一部分进行管理。这种按租户分配密钥的策略将对您在孤岛式与池化模型之间的选择产生明确影响。如果租户希望拥有自己的密钥,您很可能需要采用孤岛式存储模型。
我们首先重新审视了孤立资源与共享资源的概念,并回顾了这些多租户模型如何映射到数据分区。在此过程中,我强调了这两种模型的优缺点。我特别指出,这两种选项并非相互排斥,可以结合使用以满足SaaS环境的合规性、性能和隔离需求。
核心概念部分还识别了在任何存储策略中都需要权衡的具体因素。例如,我讨论了扩展性和噪声邻居考虑因素,以及多租户工作负载的性质如何影响租户对存储服务的影响。我还探讨了爆破半径、隔离模型、容量规划及运营经验如何影响数据分区决策。关键在于超越存储技术的表面特征,考虑业务和运营因素如何影响租户数据的表示方式。在此阶段做出的选择将对SaaS业务的敏捷性、运营效率及成本效益产生深远影响。在奠定基础之后,我开始关注这些概念在具体存储服务中的实现方式。
目标是让大家更好地理解,当这些孤岛式和池化模型在特定存储服务中实现时,它们是如何具体呈现的。我涵盖了关系型、NoSQL、对象和搜索存储模型,重点阐述了使用这些服务进行数据分区时出现的细微差别。在此过程中,您看到了这些服务不同的存储结构如何影响数据分区模型的设计。每个服务都会为这一故事增添独特的元素。
由于每种存储技术都各不相同,逐一探讨所有类型并不现实。然而,我希望我们讨论的概念和示例能为你提供一个思维框架,帮助你在考虑如何在架构中使用各种存储技术实现数据分区时进行参考。本章还应强调,数据分区并非一个非此即彼的决策。您所做的分区选择和所选技术应由应用程序服务的具体需求驱动。
虽然存储的设计可能与您的隔离策略相关,但需要注意的是,将存储进行分隔并不会隔离该资源。在下一章中,我们将开始深入探讨隔离的细节,并回顾用于确保租户资源(包括存储)免受跨租户访问的构造和机制。我们将要讨论的隔离策略和模式是SaaS的基础,对于提供一个安全的、多租户体验至关重要——无论该环境是如何部署、设计和实现的。