多租户定制化开发(多租户应用)

软件架构 | 架构学习更新

租户概念是众多用户共享计算、网络和存储资源,而彼此之间看不到数据。多租户应用程序是为每组用户(所谓的租户)定制的,而整个架构和核心功能保持不变。多租户是软件即服务 (SaaS) 供应商的典型方法。

多租户定制化开发(多租户应用)(1)

多租户应用程序——作者的图片。© 版权所有

听起来像是一个明智的商业决定?

是的,它确实。资源利用率增加,因此,项目所有权通常更合理。但它不能用于每个应用程序。让我解释一下多租户是如何工作的,它与单租户有何不同,以及何时应用它。

用例

多租户架构最适合以下类型的应用程序:

  1. SaaS(软件即服务)或 AaaS(应用程序即服务)
  2. PaaS(平台即服务)
  3. IaaS(基础设施即服务)
  4. 对于多个客户端使用相同算法堆栈的所有其他应用程序。主要功能是相同的或模块化的,并且可以根据客户的任何需求进行定制。
多租户与单租户

要选择模范架构,我们必须了解两者的工作原理。

1. 单租户

每个用户都会收到专用的计算、网络和存储资源。每个环境都是单独开发和管理的。本质上,此选项不允许任何共享。

2. 多租户

此模型支持在一个环境中托管多个客户并重用核心应用程序功能。每个租户都是孤立的,对其他人是不可见的。居民在逻辑上是分开的,而不是物理上的;因此,需要较少的硬件投资。其他好处包括简化的安全支持和更实惠的许可。在许多不同的解决方案上花费的时间/精力更少,意味着将更多的时间/精力投入到核心解决方案上。在这种情况下,整体质量获胜。

多租户定制化开发(多租户应用)(2)

多租户和单租户应用程序——作者的图片。© 版权所有

示例解决方案架构

让我们继续使用一个简单但真实的多租户解决方案。

这里的数据层由单个共享的 Cloud SQL (MySQL) 实例和对象存储组成。客户端(租户)使用组成无服务器业务层的共享云功能堆栈。

多租户定制化开发(多租户应用)(3)

Google Cloud 上的无服务器单数据库多租户架构 — 作者的图片。© 版权所有

等等……一个数据库?

多租户应用程序中的数据管理

根据存储数据的方式,可能会有不同的多租户数据库解决方案:

1.单库 单模式

= 一个数据库模式包含所有租户的数据

在这种情况下,关心租户的数据分离是最重要的。将TenantIDClientID等数据库索引添加到对租户的行进行分区。

好处:

  • 维护一个数据库很容易(与其他选项相比)
  • 您可以根据需要添加任意数量的新客户(添加就像在表中键入新记录一样简单)。

陷阱:

  • 对于可能需要不同数据使用模式的不同客户而言,灵活性并不大。当非典型客户到达时,需要进行拐杖和修补。
  • 您可能会浪费时间和精力来尝试正确分离权限
  • 备份和恢复可能会带来额外的问题。因为它包含来自其他客户的数据,所以不能轻易删除和重新创建表。必须向上搜索并覆盖所需的行真是让人头疼。

判决:

进行匹配操作的客户的绝佳选择。

2.单库 多模式

在这种情况下,需要多个表来存储来自不同客户端的数据。模式成为包含特定表、过程和权限的“命名空间”。

好处:

  • Schemas 支持DBMS 级别的访问共享,所以我们不需要做任何进一步的事情(我们节省了我们的精力和金钱,哈利路亚!)
  • 更少的数据库——更少的硬件资源。又一个胜利!
  • 良好的可扩展性。如果您需要一个新架构,您可以基于另一个架构来构建它。每次,您都可以根据需要对其进行修改。

陷阱:

  • 来自多个客户端的数据保存在一起,因为表是在逻辑上而不是物理上划分的;
  • 备份和恢复都是有问题的。我们有一个数据库,如果某个元素发生故障,一切都可以回滚到以前的状态,这是不可接受的。然后您可能需要合并新旧信息。这简单吗?一点也不,因为它不仅仅是还原。

判决:

对于愿意在共享环境中工作的客户来说,这是一个绝佳的选择。

3. 分离数据库

另一种可能的模型是代码和数据在客户端之间(通过通用用户界面和业务逻辑)在概念上(并且可能在物理上)共享。

好处:

  • 一键扩展。要添加新客户端,您只需配置新存储
  • 缩放很简单。您可以支持不同服务器上的客户端数据库
  • 定制。您可以更改每个客户端的设置(甚至将存储移动到不同的 DBMS)
  • 简单的备份。即使一个组件发生故障,其他组件也不受影响。

陷阱:

  • 它并不便宜。单个服务器支持的存储量受到限制。你可能需要更多的硬件(比所有东西都放在一个地方要多得多)。请记住,更多硬件 = 更多管理员、服务器机房空间和电力费用。
  • 再次,它并不便宜。当您的服务器托管许多位置时,总容量大于 RAM 大小;数据溢出到交换文件中,导致对硬盘驱动器的访问非常缓慢。解决方案是购买额外的服务器。

判决:

对于希望安全高于一切并愿意付款的客户(例如银行),这是最佳选择。

外卖

我希望您已经掌握了有关多租户的足够信息,以便根据您的要求做出正确的选择。

以下是本文的主要内容:

  1. 多租户环境托管共享计算、网络和存储资源的隔离用户
  2. 在多租户中运行时,您可以从成本节约、资源利用率提高、可重复使用的数据处理元素、简单的发布管理和更低的维护成本中受益。
  3. 另一方面,您应该准备好面对诸如复杂的开发、高发布风险、有限的个性化、脆弱的安全性、较低的性能以及进行更改的复杂性等缺点。
  4. 在应用于多租户方案之前请三思。但您可以肯定,在为SaaS、AaaS、PaaS、IaaS和其他应用程序选择它时,核心功能要么对所有用户都相同,要么严格模块化,并且可以轻松地根据典型客户的需求进行定制。
为什么初创公司采用多租户架构?

多租户架构的主要优点是

1- 更快的维护:由于多租户应用程序使用应用程序的单个实例,因此所有用户都可以立即进行集中、更改或修复错误。这将减少维护和错误纠正时间,并增加应用程序的可用性。

2- 可扩展性使用共享资源将使入门成本非常低,并且可扩展性更容易和更快地应用。

微服务设计的多租户注意事项

许多软件供应商转向软件即服务——SaaS——以实现快速扩展。SaaS 支持一种新形式的快速分发并实现规模经济——因为客户共享相同的代码。

多租户定制化开发(多租户应用)(4)

客户的分离被称为租赁。或者只是多租户。

虽然多租户构成了一个已知领域,但微服务的新兴用途通常不包括在多租户的设计中。

本文是在处理微服务使用多租户的设计原则时分享一些注意事项。

多租户的数据库策略

如果您不熟悉多租户的不同策略,这里有一个快速指南。

  • 单一数据库,相同的方案。所有租户共享相同的数据库和相同的架构。通常,租户被隔离并由每个表上的 TenantId 列标识,并具有对租户表的引用约束。在电子商务应用程序中,Order 表和 Customer 表只存在一次,但按列分隔租户。
  • 单一数据库,不同的架构。所有租户共享同一个数据库,但是每个租户都有自己的架构。通常,租户也被隔离并由每个表上的 TenantId 列标识,并具有对租户表的引用约束。在电子商务应用程序中,Order 表和 Customer 表只存在一次,但按列分隔租户并分隔数据架构。
  • 多数据库。每个租户都将自己的数据库保存在专用数据库中。通过使用连接字符串路由到每个租户,电子商务应用程序将拥有相同模式的多个数据库。这就是多租户的企业战略。
进入多租户微服务

微服务是目前编码行业最热门的话题。

微服务的概念定义

  • 高度可维护和可测试
  • 松耦合
  • 可独立部署
  • 围绕业务能力组织
  • 由一小部分人所有
  • 单数据域

进入本文的主要问题,后一个元素提出了一组新的设计考虑。在微服务设计中,每个服务都处理它拥有的数据域。这是一个设计事实。那么如何处理多租户呢?

在我们的电子商务应用程序示例中,我们将应用程序分解为 OrderService 和 CustomerService。每个人都将通过提供自己的数据域来构成。如果我们应用多租户模式,您会很快发现微服务设计的简单性给结构带来了复杂性。

让我们检查一下:

  • 单一数据库,相同的方案。微服务的设计原则禁止这种方法。这表明每个服务都有一个专用的数据库,该数据库具有相同的架构,但例如由一列分隔。这种方法将限制数据库的数量,并使每个服务的数据域保持隔离。然而,我们无法满足个别要求。
  • 单一数据库,不同的架构。与上述一样,每个服务都有自己的数据域,提供由租户隔离器(通常是列)分隔的不同模式。这也限制了数据库的数量。
  • 多数据库。或者每个租户的数据库是最安全和可扩展的设计。然而,随着微服务原则的引入,您将迎合每个租户、每个服务一个数据库的数据库设计。那是维护和部署的噩梦。

因此,当您开始在现代微服务环境中设计 SaaS 多租户解决方案时,您需要考虑您的元素。

多租户系统中的公平性

本文着眼于亚马逊采取的一些方法来管理对其系统的 API 请求,以避免通过实施 API 速率限制(也称为“限制”或“准入控制”)来避免过载。没有这些保护,系统就会过载当流入系统的流量超过当时的处理规模时。API 速率限制使我们能够以不同的方式塑造传入流量,例如优先考虑保持在其计划使用范围内的客户端工作负载,同时将背压施加到无法预测的客户端工作负载。在本文中,我将涵盖从准入控制算法到在生产系统中安全更新配额值的操作注意事项等主题。我还关注亚马逊设计 API 时考虑到公平性的方式,以提供可预测的性能和可用性,并帮助客户避免对可能导致速率限制的工作负载的需求。

但是,在详细了解 Amazon 系统如何实施速率限制之前,我将首先描述为什么 Amazon 发现基于速率的配额很重要,以及为什么它们会为多租户系统的客户端带来更高的可用性。尽管您可以单独阅读这篇文章,但如果您想了解系统在过载时如何运行的背景知识,请查看文章使用负载脱落避免过载。

多租户架构——租户建模

多租户定制化开发(多租户应用)(5)

多租户隔离模型

在过去的几周里,我与人们就多租户系统进行了一些有趣的对话。这些对话促使我提炼我的知识并概述如何设计、构建和操作多租户系统。这篇文章是多部分系列的第一篇。这篇文章将介绍设计多租户系统的基础知识和租户建模的各个方面。未来的帖子将涵盖生命周期管理、定价策略、故障和更新域、容量规划以及构建多租户系统的架构方法。

租户可以定义为来自单个组织或公司的一组用户。多租户解决方案是由多个租户使用的解决方案。多租户应用程序的示例包括:

  • 软件即服务 (SaaS) 产品。这些可能是企业对企业 (B2B) 解决方案,如工资管理或企业对消费者 (B2C) 解决方案,如Slack
  • 企业范围的平台解决方案,例如内部开发人员平台 (IDP) 或组织内多个团队和业务部门使用的共享 Kubernetes 集群。

在构建多租户架构时,您需要做出几个决定和需要考虑的替代方案。其中许多决策是业务驱动的,并且可能导致不同的架构模型。但是,我发现提出正确的问题有助于开发正确的架构。

  • 什么为您定义了租户?它是单个用户、团队、特定产品组合还是整个组织?
  • 您支持哪些定价模型,定价模型将如何影响需求?
  • 您为每个租户提供什么级别的服务?例如,您是否从可用性或弹性的角度平等对待每个租户?
  • 随着租户数量的增长,您将如何进行容量规划或处理规模?
  • 您的基础架构将如何支持多租户,以及需要什么级别的隔离?
  • 您将如何处理租户的特定要求?
  • 您将如何监控、管理、自动化和扩展您的环境?
租户建模

设计多租户系统的第一步是从多租户的角度定义租户是谁以及所需的隔离程度和机制。

您对租户的定义将影响您在构建解决方案时需要考虑的一些事项。例如,如果您的租户是最终消费者 (B2C),您可能需要考虑地理和区域特定要求等因素。另一方面,如果您的租户是企业 (B2B),则需要考虑法规遵从性、数据隔离和服务级别目标 (SLO) 的要求,例如可用性。

租户隔离和隔离

租户可以在逻辑上或物理上进行隔离。

物理隔离是指您为每个租户维护单独的基础架构。尽管这听起来很吸引人,但随着您的扩展和租户数量的增加,它很快就会变得昂贵。它通常还会导致资源利用不足。如果有严格的合规和监管要求,请考虑物理隔离。

逻辑隔离是租户共享基础设施。该系统将客户映射到他们的数据和应用程序所在的基础设施,以便您可以将他们的流量路由到正确的基础设施。

逻辑隔离和物理隔离之间的一个关键区别是如何强制隔离。当多个逻辑租户共享基础架构时,您通常依靠应用程序代码和租户标识符来分隔每个租户的数据。但是,当您有物理租户时,他们有自己的专用基础架构,因此您的代码可能不太重要的是要知道它在多租户环境中运行。

设计多租户架构时最重要的考虑因素之一是每个租户所需的隔离级别。不幸的是,我通常看到人们在这方面犯错。他们倾向于以二进制方式看待隔离,其中在频谱的一端有一个无共享模型,另一方面是一个完全共享的模型。

然而,将孤立视为一个极端的无共享和另一个极端的完全共享的连续统一体是有帮助的。在大多数情况下,最佳位置介于两者之间,一些组件是共享的,而另一些是隔离的。

隔离级别会影响架构的许多方面,包括:

  • 安全。如果您在多个租户之间共享基础架构,则需要特别小心地隔离数据。此外,您用于身份管理的策略需要包括租户和用户身份。
  • 成本。多个租户可以使用共享基础设施,因此更便宜。
  • 关于性能和可靠性的嘈杂邻居问题。
  • 对个别租户需求的响应能力。当您部署专用于一个租户的基础架构时,您可以针对该特定租户的要求调整资源配置。
多租户的情况

面向服务的架构 (SoA) 是亚马逊文化的核心部分,即团队和系统的强所有权和松散耦合。这种架构还有另一个重要的好处——通过资源共享提高了硬件效率。如果另一个应用程序想要使用现有服务,则服务所有者不需要做太多工作来接受新租户。在审查用例并执行安全审查后,服务所有者授予新客户端系统访问权限以调用特定 API 或访问特定数据。服务所有者无需进行额外的基础设施设置或安装,即可为这个新用例创建和运行服务的新副本——它只是重复使用现有的。

资源共享是多租户系统的核心优势。多租户系统处理多个工作负载,例如同时处理多个客户的工作。该系统还可以处理低优先级、非紧急工作负载以及高优先级、紧急工作负载。另一方面,单租户系统处理来自单个客户的工作负载。

资源共享是 SoA 和云架构背后的一个强大概念,可节省基础设施成本和人力运营成本。此外,资源共享可以降低环境成本,因为更高的利用率意味着更少的服务器,因此需要更少的能源来为基础设施供电和冷却。

多租户数据库的演变

当我比较单租户和多租户系统时,我喜欢考虑数据库系统类型之间的差异。作为一名软件工程师,我从事的几乎所有系统都需要某种数据库来存储状态。一些系统是低流量工具,偶尔可以让人们的生活更轻松,而其他系统是关键任务并服务于大量流量。

在我在亚马逊的职业生涯早期,我曾在一个负责自动化 Amazon.com 网络服务器群的运营的团队中工作。我和我的同事构建的系统可以预测随着时间的推移我们需要为每个网站队列配置多少台服务器,监控服务器的运行状况,并执行自动修复和更换损坏的服务器。我们还构建了帮助团队将软件部署到网站的系统。

为了管理其中许多工具的状态,我们需要一个数据库。在今天的亚马逊,默认选择是 NoSQL 数据库,因为这些数据库专为可管理性、可扩展性、可用性和可预测的性能而设计。然而,这个故事发生在这些技术被广泛使用之前,所以我们设置了一些运行 MySQL 的服务器,并使用复制对来实现高可用性和冗余。我们还实施了备份和恢复,并进行了故障测试,以确保我们可以依靠这个单点故障。

当我们构建需要新数据库的新服务和工具时,我们经常想通过简单地向它们添加更多表来重用现有数据库。为什么这种诱惑如此强烈?一方面,我们查看了数据库服务器的利用率,发现服务器负载并不重。请记住,这些特定数据库的规模与 2000 年代中期向网站部署代码的开发人员数量以及我们当时拥有的 Web 服务器的数量相当。此外,配置新数据库、监控它们、升级它们以及尝试自动化其操作的各个方面的操作开销是痛苦的。

下图显示了几年前 Amazon.com 网站车队运营使用多个网站工具的示例架构。该图表明部署服务和定期车队运营商共享同一个数据库,由我们的团队运营。

多租户定制化开发(多租户应用)(6)

然而,当我们屈服于将同一组数据库服务器用于多个应用程序的诱惑时,我们后悔了。我记得我正在随叫随到,并因部署工具的性能下降而被寻呼。我挠了挠头,试图找出问题所在。然后我意识到我们拥有的一个完全独立的工具(某种舰队审计员)正在运行它的夜间状态同步 cron 作业。这给共享数据库带来了很多额外的负担。车队审计工具并不介意数据库速度慢,但部署工具(及其用户)确实如此!

共享数据库的愿望(为了降低基础设施和一些运营的成本)与分离数据库的需求(为了更好地隔离工作负载和以其他方式降低运营成本)之间的持续紧张感觉就像是一种双赢的情况——Kobayashi Maru的排序。我们使用的数据库类型是为单个租户设计的,因此,当我们尝试以多租户方式使用它们时,不出所料,我们遇到了痛苦。

当 Amazon Relational Database Service (RDS) 推出时,它通过自动化大部分操作工作让我们的生活变得更轻松。我们更容易将单租户系统作为单独的数据库运行,而不是在多个应用程序之间共享同一个数据库。但是,有些工作负载非常小,而其他工作负载大小不一,因此我们仍然需要注意每个数据库的利用率,以使实例大小恰到好处。另外,我们需要足够的净空来处理负载的周期性波动。

后来在亚马逊,当我在寻找新的挑战时,我了解到我们正在 AWS 中构建的一种新型数据库。数据库的目标是高度弹性、可扩展、可用、低延迟和完全托管。这些目标对我来说很有吸引力,因为作为一名软件工程师,我真的不喜欢一遍又一遍地做同样的事情,尤其是如果很难做到的话。因此,我花费了大量精力尝试自动化那些可重复的任务(另请参阅https://xkcd.com/1319/)。这个新数据库似乎是一个完美的机会,最终完全自动化了我觉得痛苦的数据库维护的各个方面,所以我加入了 2012 年推出 Amazon DynamoDB 的团队。就像柯克船长一样,我们使用编程通过了小林丸测试!

DynamoDB 利用多租户来提供高度弹性、持久且可用的数据库。与使用 Amazon RDS 时不同,当我在 DynamoDB 中创建资源时,我什至不预置整个 Amazon Elastic Compute Cloud (EC2) 实例。我只是通过 API 与我的数据库进行交互,在幕后 DynamoDB 计算出工作负载所需的服务器比例。(实际上,它使用多个可用区中的多个服务器的一小部分来实现高可用性和持久性。)随着我的工作负载的增长和缩小,DynamoDB 会调整这部分,并根据需要加入更多或更少的服务器。

与数据库一样,通用计算服务器也存在一定程度的多租户。借助 AWS Lambda,计算资源共享以亚秒级的间隔发生,使用Firecracker轻量级虚拟化进行资源隔离。使用 Amazon API Gateway,资源共享是在 API 请求级别。这些服务的客户受益于多租户系统的优势:弹性、效率和易用性。在幕后,这些服务致力于解决多租户带来的挑战。在这些挑战中,我觉得最有趣的是公平。

多租户系统中的公平性

任何多租户服务都与系统协同工作以确保公平。公平意味着多租户系统中的每个客户端都提供单租户体验。在多租户系统中确保公平的系统类似于执行装箱算法的系统,这是计算机科学中的经典算法。这些公平系统执行以下操作:

  • 执行放置算法以在队列中找到新工作负载的位置。(类似于为工作量找到一个有空间的垃圾箱。)
  • 持续监控每个工作负载和每个服务器的利用率,以移动工作负载。(类似于在 bin 之间移动工作负载以确保没有 bin 太满。)
  • 监控整体机队利用率,并根据需要添加或删除容量。(类似于在它们都已满时添加更多垃圾箱,并在它们为空时移除垃圾箱。)
  • 只要底层系统未被充分利用,就允许工作负载超出硬分配的性能边界,并在系统充分利用时将工作负载保持在其边界内。(类似于允许工作负载在每个垃圾箱内延伸,只要它们不挤占其他工作负载。)

先进的公平系统以有趣的方式结合了这些技术。例如,公平系统可以监控每个工作负载的利用率,估计任何两个工作负载一起运行良好的可能性,并将它们一起移动到同一个 bin 中。只要一个工作负载没有充分利用其预置资源,同一个 bin 中的另一个工作负载就可以借用这些资源。

为了使这种资源共享发挥作用,借款需要被工作负载忽视。如果工作负载需要使用其所有预置资源,则归还这些借用资源的时间需要几乎是即时的。此外,工作负载需要在箱之间快速移动。如果繁忙的工作负载习惯于通过向邻居借用而超出其预置资源,但其邻居改变了行为并开始使用更多预置资源,则需要将繁忙的工作负载转移到另一个容器中。

减载加公平

随着系统负载的增加,它应该自动扩展。最简单的方法是增加更多容量和水平扩展。对于采用无服务器架构的服务(例如基于 AWS Lambda 构建的服务),水平扩展几乎是在瞬间发生的,因为容量会按需增加以处理工作。对于非无服务器服务,自动扩展需要更长的时间。

通常,即使在几分钟内缩放也可以。但是,如果服务负载增加的速度快于 Auto Scaling 增加容量的速度会怎样?该服务有两个选择:对于所有请求,它可能会变得超载和缓慢,或者它可以摆脱多余的负载并为它所接受的请求保持一致的性能。在亚马逊,我们非常喜欢保持一致、可预测的性能(选项二)——尤其是在任何过载情况下。在过载情况下增加延迟(选项一)可能会在分布式系统中产生连锁反应,从而将影响传播到其他系统。选项二的快速失败策略有助于过载系统继续取得进展并做有用的工作。

我们发现,减载是一种在过载情况下减少多余负载的有用工具。减载是一种廉价地拒绝工作而不是在其上花费有限资源的行为。对于 HTTP 服务,减载意味着立即返回错误。这方面的一个示例是返回 HTTP 503 错误代码。这为 Auto Scaling 启动并添加必要的容量赢得了时间,因为返回错误的替代方法是让所有请求变慢。因为对请求返回负载脱落响应比完全处理请求要便宜得多,所以这种方法让服务器继续为它决定完全处理的请求提供可预测的性能。

通常,我们将服务设计为尽快向客户端返回负载卸载响应,以最大限度地减少服务器执行的工作量。但是,在某些情况下,我们会故意稍微放慢这些响应速度。例如,当负载均衡器与最少未完成请求算法一起使用时,我们会减慢快速错误响应以匹配成功响应的延迟。这样我们就可以避免负载均衡器向可能已经超载的服务器发送额外的流量。

然而,在多租户服务中,甩负荷不足以使多租户服务对每个客户都显示为单租户服务。通常,来自多个租户的负载是不相关的(即,每个客户都有自己的用例和请求率)。因此,如果服务的整体负载突然增加,那么这种增加很可能是由单个租户驱动的。考虑到公平性,我们希望避免因单个租户的计划外负载增加而导致所有租户的一些请求失败。

为了增加多租户系统的公平性,我们使用速率限制来塑造流量的计划外增长,但我们以每个租户或每个工作负载的粒度强制执行配额(资源和操作的最大值)。这样,如果多租户服务遇到计划外的负载增加,该工作负载的计划外部分将被拒绝,而其他工作负载继续以可预测的性能运行。

然而,自相矛盾的是,配额的使用既增加又减少了服务的可用性。当一个租户的工作负载超过其配额时,它将开始看到其过多的请求失败——这可以被视为可用性下降。然而,实际上,该服务可能有足够的能力来满足这些请求。API 速率限制是保护我们服务可用性的一种有用技术,但我们也努力帮助我们的调用者避免不必要地超出其配额。

与减载一样,执行基于速率的配额涉及廉价地发送错误响应而不是处理请求。但是,此响应表明客户端已超出其配额,而不是服务器容量不足。因此,“超出 API 速率限制”响应通常会返回 429 状态代码。一般来说,500 范围内的状态代码意味着服务器由于某种原因失败,但 400 范围内的状态代码意味着客户端正在执行一些意外的事情,或者在这种情况下是计划外的。

注意 您可能会注意到,某些 AWS 服务实际上会返回 503 状态代码以表示超出速率。429 状态码直到 2012 年才在RFC 6585中正式添加到 HTTP 规范中。许多 AWS 服务都是在此之前创建的,从 2004 年发布的 Amazon Simple Queue Service (SQS) 开始。AWS 非常注重向后兼容性,因此我们没有更改现有服务的行为以避免不必要地破坏客户端。

配额可见性和灵活性

服务所有者通常为每个客户端配置配额。例如,对于 AWS 服务,客户通常是 AWS 账户。有时配额被放置在比客户端更细粒度的东西上,例如客户端拥有的特定资源,如 DynamoDB 表。服务所有者定义为每个调用者提供默认配额的规则。如果客户在正常业务过程中增加其使用量并接近其限制,或者如果客户预计即将到来的负载增加,他们通常会要求服务提高其配额。

有几种类型的配额,每种都有自己的单位。一种类型的配额管理为“客户端可以同时运行的事物的数量”。例如,Amazon EC2 对特定 AWS 账户可以启动的实例数量实施配额。另一种配额是基于费率的配额。基于速率的配额通常以“每秒请求数”等单位衡量。尽管本文关注基于费率的配额的细微差别,但适用于基于费率的配额的许多概念也适用于其他类型,因此在本文中我将只使用“配额”一词。

下图演示了配额的使用。它显示了具有有限容量的服务(总供应容量由 y 轴的最大值表示)。该服务有三个客户端:Blue、Orange 和 Gray。该服务已硬分配每个客户端其总容量的三分之一。该图显示客户端 Blue 正试图超过其硬分配的吞吐量,但它无法做到这一点。

多租户定制化开发(多租户应用)(7)

为了使这种配额分配在操作上进行扩展,服务向客户公开有关其配额的信息,以及它们与达到配额的距离。毕竟,当客户端超出其配额时,它很可能会向其客户端返回错误作为响应。因此,这些服务为客户提供他们可以看到的指标,并在他们的利用率接近最大配额值时发出警报。例如,DynamoDB 发布 Amazon CloudWatch 指标,这些指标显示为表预置的吞吐量,以及随着时间的推移消耗了多少吞吐量。

某些 API 的服务成本远高于其他 API。因此,服务可能会为每个客户端提供较低的昂贵 API 配额。同样,操作的成本并不总是预先知道的。例如,返回单个 1 KB 行的查询比返回最多 1 MB 行的查询便宜。分页可防止此费用超出控制范围,但最小和最大页面大小之间的成本差异仍然足以使设置正确的阈值具有挑战性。为了处理这个问题,一些服务简单地将较大的响应计为多个请求。该技术的一种实现是首先将每个请求视为最便宜的请求,然后在 API 调用完成后,返回并根据真实的请求成本扣除客户端的配额,

在实施配额方面可以有一些灵活性。考虑客户端 A 的限制为每秒 1,000 个事务 (TPS),但该服务已扩展为处理 10,000 TPS,并且该服务当前为其所有客户端提供 5,000 TPS。如果客户端 A 从 500 TPS 飙升至 3,000 TPS,则仅允许其 1,000 TPS,而其他 2,000 TPS 将被拒绝。但是,服务可以允许它们,而不是拒绝这些请求。如果其他客户端同时使用更多的配额,则服务可以开始丢弃客户端 A 的“超出配额”请求。深入了解这种“计划外容量”也应该向客户和/或服务的运营商发出信号。客户应该知道它超出了配额,并且将来有可能出现错误。

为了演示这种情况,我们创建了一个类似于之前用于显示将容量硬分配给其客户端的服务的图表。但是,在下图中,服务为其客户端堆叠容量,而不是硬分配它。堆叠允许客户端使用未使用的服务容量。由于橙色和灰色没有使用它们的容量,因此允许蓝色超出其预置阈值并利用(或突入)未使用的容量。如果 Orange 或 Gray 决定使用他们的容量,他们的流量应该优先于 Blue 的突发流量。

多租户定制化开发(多租户应用)(8)

在亚马逊,我们还通过考虑典型的客户用例流量模式来研究灵活性和突发性。例如,我们发现 EC2 实例(及其附加的 Amazon Elastic Block Store (EBS) 卷)在实例启动时通常比稍后更忙。这是因为实例启动时,需要下载并启动其操作系统和应用程序代码。当我们考虑这种流量模式时,我们发现我们可以更慷慨地预先设置突发配额。这会减少启动时间,并且仍然提供我们需要的长期容量规划工具,以在工作负载之间提供公平性。

我们还想方设法让配额随着时间的推移变得灵活,并根据客户业务增长而增加的流量进行调整。例如,随着客户的增长,一些服务会随着时间的推移自动增加客户的配额。但是,在某些情况下,客户希望并依赖于固定配额,例如用于成本控制的配额。请注意,这种类型的配额很可能作为服务的功能而不是幕后发生的保护机制公开。

实现准入控制层

塑造流量、卸载负载和实施基于速率的配额的系统被称为准入控制系统。

亚马逊的服务采用多层准入控制架构,以防止大量被拒绝的请求。在亚马逊,我们经常在我们的服务前面使用Amazon API Gateway,让它处理一些维度的配额和速率限制。API Gateway 可以通过其庞大的车队吸收激增的流量。这意味着我们的服务车队仍然没有负担,可以免费为实际流量提供服务。我们还配置Application Load Balancer、API Gateway 或Amazon CloudFront以使用 Web 应用程序防火墙服务AWS WAF来进一步卸载准入控制行为。对于除此之外的一层保护,AWS Shield提供 DDoS 保护服务。

多年来,我们使用了多种技术在 Amazon 的系统中实施这些准入控制层。在本节中,我们将探讨其中的一些技术,包括我们如何构建服务器端准入控制、我们如何实现客户端以优雅地响应他们调用的服务的背压,以及我们如何考虑这些系统的准确性。

本地准入控制

实现准入控制的一种常用方法是使用令牌桶算法。令牌桶保存令牌,每当请求被接受时,就会从桶中取出一个令牌。如果没有任何可用的令牌,则请求被拒绝,并且存储桶保持为空。令牌以配置的速率添加到存储桶中,直至达到最大容量。这个最大容量被称为突发容量,因为这些令牌可以立即被消耗,从而支持流量的突发。

这种瞬间爆发的代币消耗,是一把双刃剑。它允许流量中有一些自然的不均匀性,但如果突发容量太大,它会破坏速率限制的保护。

或者,可以将令牌桶组合在一起以防止无限突发。一个令牌桶可以具有较低的速率和较高的突发容量,而第二个桶可以具有较高的速率和较低的突发容量。通过检查第一个桶然后是第二个桶,我们允许高突发,但有一个有限的突发率。

对于传统服务(没有无服务器架构的服务),我们还考虑了针对给定客户的请求在我们的服务器上的统一或不统一。如果请求不统一,我们使用更宽松的突发值或分布式准入控制技术。

有许多现成的本地速率限制实现可用,包括 Google Guava 的RateLimiter类。

分布式准入控制

本地准入控制对于保护本地资源很有用,但配额执行或公平通常需要在水平扩展的队列中执行。亚马逊的团队采取了许多不同的方法来解决分布式准入控制的问题,包括:

在本地计算费率并将配额除以服务器数量。使用这种方法,服务器根据它们在本地观察到的流量率执行准入控制,但是它们将每个键的配额除以为该限制键提供流量的服务器数量。这种方法假设请求相对均匀地分布在服务器上。当 Elastic Load Balancing 负载均衡器以循环方式跨服务器分发请求时,这通常是正确的。

下图显示了一个服务架构,该架构假定跨实例的流量相对一致,并且可以使用单个逻辑负载均衡器进行处理。

多租户定制化开发(多租户应用)(9)

但是,在某些机群配置中,关于跨服务器一致性的假设可能并不总是正确的。例如,当负载平衡器用于连接平衡模式而不是请求平衡模式时,连接数不足的客户端将一次将其请求发送到服务器的子集。当每个键的配额足够高时,这在实践中可能很好。当存在具有多个负载均衡器的非常大的舰队时,关于跨服务器一致性的假设也会被打破。在这种情况下,客户可以通过负载均衡器的子集进行连接,从而导致只有服务实例的子集为请求提供服务。同样,如果配额足够高,或者客户不太可能接近他们的配额最大值以适用这种情况,这在实践中可能很好。

下图说明了这样一种情况,即由多个负载均衡器提供的服务发现来自给定客户端的流量由于 DNS 缓存而没有均匀地分布在所有服务器上。当客户端随着时间的推移打开和关闭连接时,这往往不是大规模的问题。

多租户定制化开发(多租户应用)(10)

使用一致的散列进行分布式准入控制。一些服务所有者运行单独的队列,例如 Amazon ElastiCache for Redis 队列。他们将油门键上的一致哈希应用于特定的速率跟踪器服务器,然后让速率跟踪器服务器根据本地信息执行准入控制。这个解决方案甚至在密钥基数很高的情况下也可以很好地扩展,因为每个速率跟踪器服务器只需要知道密钥的一个子集。但是,当以足够高的速率请求特定的节流键时,基本实现会在缓存队列中创建一个“热点”,因此需要将智能添加到服务中,以逐渐更多地依赖于特定键的本地准入控制随着其吞吐量的增加。

下图说明了对数据存储使用一致的散列。即使在流量不均匀的情况下,使用一致的哈希计算跨某种数据存储(例如缓存)的流量也可以解决分布式准入控制问题。然而,这种架构引入了扩展挑战。

多租户定制化开发(多租户应用)(11)

采取其他方法。在亚马逊,随着时间的推移,我们已经在 Web 服务中实现了许多分布式准入控制算法,根据具体的用例,开销和准确性程度不同。这些方法涉及定期共享一组服务器中每个节流键的观察速率。在这些方法中,在可扩展性、准确性和操作简单性之间存在许多权衡,但他们需要自己的文章来深入解释和比较它们。对于一些起点,请查看关于公平和准入控制的研究论文,其中许多来自网络空间,我在本文末尾链接到。

下图说明了使用服务器之间的异步信息共享来解决非均匀流量。这种方法有其自身的缩放和准确性挑战。

多租户定制化开发(多租户应用)(12)

反应式准入控制

配额对于处理常规的意外流量高峰很重要,但服务应该准备好遇到各种意外的工作负载。例如,有问题的客户端可能会发送格式错误的请求,或者客户端可能会发送比预期更昂贵的处理工作负载,或者客户端可能有一些失控的应用程序逻辑并请求帮助以过滤掉他们的意外流量。灵活性很重要,因此我们可以放置一个准入控制系统,它可以对请求的各个方面做出反应,例如用户代理等 HTTP 标头、URI 或源 IP 地址。

除了确保正确的可见性和挂钩之外,我们还结合了用于快速、谨慎地更改速率限制规则的机制。在进程启动时加载到内存中的规则配置似乎是一个不错的选择。但是,当情况需要这些更改时,快速部署规则更改可能会非常尴尬。实施具有安全性的动态配置解决方案也很重要,并跟踪随时间发生的变化。亚马逊的一些系统首先在评估模式下部署配额值配置更改,我们在使规则生效之前验证规则会影响正确的流量。

高基数维度的准入控制

对于我们迄今为止探索的大多数配额类型,准入控制系统需要跟踪当前观察到的速率和相当少数事物的配额值。例如,如果一个服务被十个不同的应用程序调用,准入控制系统可能只需要跟踪十个不同的速率和配额值。然而,当处理高基数维度时,准入控制变得更加复杂。例如,系统可能会为世界上的每个 IPv6 地址、DynamoDB 表中的每一行或 Amazon Simple Storage Service (S3) 存储桶中的每个对象设置基于速率的配额。这些东西的数量实际上是无限的,因此没有合理的内存量可以跟踪它们中的每一个的速率和配额。

为了设置准入控制系统需要为这些维度使用的内存量的上限,我们使用了诸如 Heavy Hitters、Top Talkers 和 Counting Bloom 过滤器之类的算法,它们都提供了关于准确性和错误界限的有趣保证,同时限制使用的内存。

当我们运行像这样具有高基数维度的系统时,我们还需要对流量随时间变化的操作可见性。在 Amazon 内部,我们使用 Amazon CloudWatch Contributor Insights来分析我们自己的服务的高基数流量模式。

对超出速率的响应做出反应

当服务的客户端收到速率超出错误时,它可以重试或返回错误。Amazon 的系统可以以两种方式之一来响应速率超出错误,具体取决于它们是同步系统还是异步系统。

同步系统需要不耐烦,因为它们有人在等待它们响应。重试请求将有一定的机会在下一次尝试中成功。但是,如果依赖服务经常返回超出速率的响应,那么重试只会减慢每个响应,并且会占用已经负载很重的系统上的更多资源。这就是为什么 AWS 开发工具包会在服务频繁返回错误时自动停止重试的原因。(在撰写本文时,此行为需要客户端应用程序在 SDK中设置STANDARD重试模式。)

许多异步系统更容易。为了响应接收到的超出速率的响应,他们可以简单地施加背压并减慢他们的处理速度一段时间,直到他们的所有请求再次成功。一些异步系统会定期运行,并且预计它们的工作需要很长时间才能完成。对于这些系统,它们可以尝试尽可能快地执行,并在某些依赖性成为瓶颈时放慢速度。

其他异步系统预计不会遇到明显的处理延迟,如果它们不能足够快地完成工作,它们可能会积压大量工作。这些类型的异步系统具有与同步系统更相似的要求。避免无法克服的队列积压一文更深入地介绍了这些系统中的错误处理技术。

评估准入控制精度

无论我们使用哪种准入控制算法来保护服务,我们都发现评估该算法的准确性很重要。我们使用的一种方法是在每个请求的服务请求日志中包含节流键和速率限制,并执行日志分析以测量每个节流键每秒的实际队列范围请求。然后我们将其与配置的限制进行比较。由此,对于每个键,我们分析“真阳性率”(正确拒绝的请求率)、“真阴性率”(正确允许的请求率)、“假阳性率”(被错误拒绝的请求率)和“假阴性率”(被错误接受的请求率)。

我们使用许多工具来执行这样的日志分析,包括 CloudWatch Logs Insights 和 Amazon Athena。

避免配额的架构方法

将准入控制添加到服务以提高其服务器端可用性、保护客户免受彼此影响并宣告胜利似乎很容易。但是,我们也认为配额给客户带来不便。当客户试图完成某件事时,配额会减慢他们的速度。当我们在服务中建立公平机制时,我们也在寻找帮助客户快速完成工作的方法,而不会让他们的吞吐量受到配额的限制。

我们帮助客户避免超出基于速率的配额的方法取决于 API 是控制平面 API 还是数据平面 API。数据平面 API 操作旨在随着时间的推移以越来越高的速率被调用。数据平面 API 操作的示例包括 Amazon Simple Storage Service (S3) GetObject、Amazon DynamoDB GetItem 和 Amazon SQS ReceiveMessage。另一方面,控制平面 API 操作可用于不随客户数据平面使用而增长的偶尔、低容量用例。控制平面 API 操作的示例包括 Amazon S3 CreateBucket、Amazon DynamoDB DescribeTable 和 Amazon EC2 DescribeInstances。

避免超出配额的容量管理方法

数据平面工作负载具有弹性,因此我们将数据平面服务设计为具有弹性。为了使服务具有弹性,我们设计了底层基础架构以自动扩展以适应客户工作负载的变化。我们还需要帮助客户在管理配额时保持这种弹性。亚马逊的服务团队使用各种技术来帮助他们的客户管理配额并满足他们的弹性需求:

  • 如果车队配备了一些未充分利用的“松弛”容量,我们会让调用者突入其中。
  • 我们实施 Auto Scaling 并随着每个呼叫者在正常业务过程中的增长而增加他们的限制。
  • 我们让客户可以轻松查看他们离极限有多近,并让他们配置警报,让他们知道何时达到这些极限。
  • 我们注意呼叫者何时接近并达到他们的极限——他们可能没有注意到。至少,当服务以较高的整体速率限制流量或同时限制太多客户时,我们会发出警报。

避免超出配额的 API 设计方法

特别是对于控制平面,我之前讨论的一些技术可能不适用。控制平面被设计为相对不经常被调用,而数据平面被设计为被大量调用。但是,当控制平面的客户​端最终创建了许多资源时,他们仍然需要能够对这些资源进行管理、审计和执行其他操作。客户在大规模管理许多资源时可能会用完他们的配额并遇到 API 速率限制,因此我们寻找替代方法来通过不同类型的 API 操作来满足他们的需求。以下是 AWS 在设计 API 时采用的一些方法,这些方法可帮助客户避免可能导致用尽其基于速率的配额的调用模式:

  • 支持变更流。例如,我们发现一些客户会定期轮询 Amazon EC2 DescribeInstances API 操作以列出他们的所有 EC2 实例。通过这种方式,他们可以找到最近启动或终止的实例。随着客户 EC2 队列的增长,这些调用变得越来越昂贵,从而导致超出其基于费率的配额的机会增加。对于某些用例,我们能够通过AWS CloudTrail提供相同的信息来帮助客户完全避免调用 API 。CloudTrail 公开操作的更改日志,因此客户可以对来自流的更改做出反应,而不是定期轮询 EC2 API。
  • 将数据导出到支持更高呼叫量的另一个地方。S3 Inventory API是一个真实的示例。我们从客户那里得知,他们的 Amazon S3 存储桶中有大量对象,需要对其进行筛选,以便找到特定对象。他们正在使用 ListObjects API 操作。为了帮助客户实现高吞吐量,Amazon S3 提供了一个 Inventory API 操作,该操作将存储桶中的对象列表异步导出到称为 Inventory Manifest 文件的不同 S3 对象中,该文件包含存储桶中所有对象的 JSON 序列化列表。现在,客户可以以数据平面吞吐量访问其存储桶的清单。
  • 添加批量 API 以支持大量写入。我们从客户那里听说,他们希望调用一些写入 API 操作来创建或更新控制平面中的大量实体。一些客户愿意容忍 API 施加的速率限制。但是,他们不想处理编写长时间运行的导入或更新程序的复杂性,也不想处理部分失败和速率限制的复杂性以避免挤占其他写入用例。一项服务 AWS IoT 通过 API 设计解决了这个问题。它添加了异步批量供应 API. 使用这些 API 操作,客户上传一个包含他们想要进行的所有更改的文件,当服务完成这些更改后,它会向调用者提供一个包含结果的文件。这些结果的示例包括哪些操作成功,哪些失败。这使得客户可以方便地处理大批量的操作,但他们不需要处理重试、部分失败以及随着时间的推移分散工作负载的细节。
  • 将控制平面数据投影到需要经常引用的地方。Amazon EC2 DescribeInstances 控制平面 API 操作将有关实例的所有元数据从每个实例的网络接口返回到块设备映射。但是,其中一些元数据与在实例本身上运行的代码非常相关。当实例很多时,每个实例调用 DescribeInstances 的流量都会很大。如果调用失败(由于速率超出错误或其他原因),这将是客户在实例上运行的应用程序的问题。为了完全避免这些调用,Amazon EC2 在每个实例上公开一个本地服务,该服务为该特定实例提供实例元数据。通过将控制平面数据投影到实例本身,

准入控制作为一项功能

在某些情况下,客户发现准入控制比无约束弹性更可取,因为它可以帮助他们控制成本。通常,服务不会向客户收取拒绝请求的费用,因为它们往往很少发生并且处理起来相对便宜。例如,AWS Lambda 的客户要求能够通过限制潜在昂贵函数的并发调用次数来控制成本。当客户想要这种控制时,重要的是让他们可以通过 API 调用轻松地自行调整限制。他们还需要有足够的可见性和警报能力。通过这种方式,他们可以看到系统中的问题,并在他们认为有必要时通过提高限制来做出响应。

结论

多租户服务具有资源共享属性,使它们能够以更低的基础设施成本和更高的运营效率运行。在 Amazon,我们在多租户系统中构建公平性,为我们的客户提供可预测的性能和可用性。

服务配额是实现公平的重要工具。基于速率的配额通过防止一个工作负载的不可预测的增加影响其他工作负载,使多租户客户的 Web 服务更加可靠。但是,实施基于费率的配额并不总是足以提供良好的客户体验。客户可见性、控制、突发共享和不同风格的 API 都可以帮助客户避免超出配额。

分布式系统中准入控制的实现是复杂的。幸运的是,随着时间的推移,我们找到了推广此功能并将其公开在各种 AWS 服务中的方法。对于 API 速率限制,API Gateway 提供多种限制作为功能。AWS WAF 提供了另一层服务保护,它集成到 Application Load Balancer 和 API Gateway 中。DynamoDB 在单个索引级别提供预置吞吐量控制,让客户隔离不同工作负载的吞吐量要求。同样,AWS Lambda 公开了每个函数的并发隔离以将工作负载彼此隔离。

在亚马逊,我们发现使用配额的准入控制是构建具有可预测性能的高弹性服务的重要方法。然而,准入控制是不够的。我们也确保解决简单的问题,例如使用 Auto Scaling,这样如果出现意外的减载,我们的系统会通过 Auto Scaling 自动响应增加的需求。

从表面上看,将服务公开为单租户服务与多租户服务之间似乎存在成本和工作负载隔离之间的内在权衡。但是,我们发现通过在多租户系统中实施公平性,我们的客户可以获得多租户和单租户世界的最佳体验。结果,他们可以吃蛋糕,也可以吃。

本文取自关于多租户架构的多个想法。

链接
  • 亚马逊 API 网关
  • AWS WAF — Web 应用程序防火墙
  • Lambda 每函数并发
  • 谷歌番石榴速率限制器
  • DynamoDB 使用令牌桶算法
  • Zhang, et al Online Identification of Hierarchical Heavy Hitters: Algorithms, Evaluation, and Applications , AT&T Labs, 2004
  • Floyd 等人用于避免拥塞的随机早期检测网关,IEEE/ACM,1993
  • Charny具有反馈的分组交换网络中速率分配的算法,麻省理工学院,1994 年
  • Feng, et al BLUE: A New Class of Active Queue Management Algorithms,密歇根大学,2013
  • 麦肯尼,随机公平排队,2002
  • Kemp 等人基于 Gossip 的聚合信息计算,IEEE,2003
,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页