在 Amazon,我们构建的服务必须满足极高的可用性目标。这意味着我们需要仔细考虑我们的系统所具有的依赖关系。我们对系统进行设计以保持弹性,即使这些依赖关系受损,也不会受到干扰。在本文中,我们将定义一种名为静态稳定性的模式,我们将使用这种模式实现此等水平的弹性。我们将向您展示如何将此概念应用于可用区,这是 AWS 中的关键基础设施构建块,因此是构建我们所有服务的基础依赖关系。

在静态稳定的设计中,即使依赖关系受损,整个系统仍能继续工作。系统可能看不到其依赖关系本应提供的任何更新信息(如新内容、删除的内容或修改的内容)。然而,虽然依赖关系已受损,但在依赖关系受损之前所执行的一切工作都将继续发挥作用。我们将介绍如何构建 Amazon Elastic Compute Cloud (EC2) 以保持静态稳定。然后,我们将提供两个静态稳定的示例架构,我们发现这些架构对于在可用区之上构建高度可用的区域系统非常有用。

最后,我们将深入探讨 Amazon EC2 背后的一些设计理念,包括如何在软件级别提供可用区独立性。此外,我们将讨论使用这种架构构建服务时要进行的一些权衡。

可用区的作用
可用区是 AWS 区域的逻辑隔离部分:每个 AWS 区域都有多个可用区,这些可用区可独立运行。可用区通过有意义的距离进行物理分隔,以防止受到潜在问题(如雷击、龙卷风和地震)的相关影响。他们不共享电源或其他基础设施,但是,它们通过快速加密的专用光纤网络相互连接,使应用程序能够快速实现故障转移而不会中断。换言之,可用区可为我们的基础设施隔离提供抽象层。需要可用区的服务允许调用方告知 AWS 在区域内的哪个位置以物理方式预置基础设施,以便他们可以从这种独立性中受益。在 Amazon,我们构建了区域 AWS 服务,可利用这种区域独立性实现他们自己的高可用性目标。Amazon DynamoDB、Amazon Simple Queue Service (SQS) 和 Amazon Simple Storage Service (S3) 等服务就是此类区域服务的示例。
 
当与在 Amazon Virtual Private Cloud (VPC) 内预置云基础设施的 AWS 服务交互时,其中许多服务要求调用方不仅指定区域,还要指定可用区。可用区通常在所需的子网参数中隐式指定,例如在启动 EC2 实例、预置 Amazon Relational Database Service (RDS) 数据库时,或创建 Amazon ElastiCache 集群时。尽管一个可用区中有多个子网很常见,但单个子网完全位于一个可用区内,因此通过提供子网参数,调用方还可隐式提供要使用的可用区。

静态稳定性

在可用区之上构建系统时,我们学到的一个教训是要在损害发生之前做好准备。可以采取的一种不太有效的方法是部署到多个可用区,预期如果一个可用区内发生损害,服务将会扩展(可能使用 AWS Auto Scaling)到其他可用区中,并恢复到完全正常的运行状况。这种方法效果欠佳,因为它依赖于损害发生时所做出的响应,而不是在这些损害发生之前做好准备。换言之,它缺乏静态稳定性。相比之下,静态稳定且更加高效的服务将超额预置基础设施,使其在无需启动任何新的 EC2 实例的情况下仍能正常运行,即使可用区将受到损害也不例外。
 
为了更好地说明静态稳定性的属性,我们来看看 Amazon EC2,这项服务本身就是根据这些原则设计的。
 
Amazon EC2 服务由控制层面和数据层面组成。“控制层面”和“数据层面”是网络术语,但我们在 AWS 中到处都使用它们。 控制层面是对系统进行更改(添加资源、删除资源、修改资源)以及将这些更改传播到需要生效的任何位置的机制。相比之下, 数据层面是这些资源的日常事务,即他们需要什么才能正常工作。
 
在 Amazon EC2 中,控制层面是 EC2 启动新实例时所发生的一切。控制层面的逻辑通过执行多项任务将新 EC2 实例所需的所有内容汇集在一起。以下是几个示例:
 
• 它在考虑置放群组和 VPC 租赁要求的同时,为计算查找物理服务器。
• 它从 VPC 子网中分配网络接口。
• 它准备 Amazon Elastic Block Store (EBS) 卷。
• 它生成 AWS Identity and Access Management (IAM) 角色凭证。
• 它安装安全组规则。
• 它将结果存储在各种下游服务的数据存储中。
• 它根据需要将所需的配置传播到 VPC 中的服务器和网络边缘。
 
相比之下,Amazon EC2 数据层面会按预期保持现有 EC2 实例正常运行,执行如下任务:
 
• 它根据 VPC 的路由表路由数据包。
• 它从 Amazon EBS 卷读取并向其中写入内容。
• 如此等等。
 
与通常情况下的数据层面和控制层面一样,Amazon EC2 数据层面比其控制层面简单得多。由于相对简单,Amazon EC2 数据层面的设计目标是提供比 Amazon EC2 控制层面更高的可用性。
 
重要的是,Amazon EC2 数据层面经过精心设计,可在控制层面出现可用性事件(例如启动 EC2 实例的能力受损)的情况下保持静态稳定。例如,为了避免网络连接中断,Amazon EC2 数据层面被设计为可使运行 EC2 实例的物理机能够本地访问将数据包路由到 VPC 内部和外部的点所需的所有信息。Amazon EC2 控制层面受损意味着在事件发生期间,物理服务器可能无法看到如下更新:添加到 VPC 中的新 EC2 实例或新的安全组规则。但在事件发生之前它能够发送和接收的流量将继续工作。
 
控制层面、数据层面和静态稳定性的概念广泛适用,不仅仅限于 Amazon EC2。能够将系统分解为它的控制层面和数据层面是设计高可用性服务的有用概念工具,原因如下:
 
• 通常,对于采用服务的客户的成功而言,数据层面的可用性比控制层面更为重要。例如,对于大多数 AWS 客户而言,EC2 实例在运行后的持续可用性和正常运行比启动新 EC2 实例的能力更重要。
• 数据层面的运作容量通常要高于其控制层面(通常按数量级来计算)。因此,最好将它们分开,以便可以根据各自的相关扩展维度进行扩展。
• 多年来,我们发现,系统的控制层面往往比其数据层面具有更多的活动部件,因此仅从统计角度来说,它就更有可能因此原因而受损。
 
综合考虑这些因素,我们的最佳实践是沿控制层面和数据层面的边界分离系统。
 
为了在实践中实现这种分离,我们采用静态稳定性原则。数据层面通常依赖于来自控制层面的数据。但是,要实现更高的可用性目标,数据层面要保持其现有状态,即使在控制层面受损的情况下也要继续工作。数据层面在受损期间可能无法获得更新,但之前一直工作的一切都将继续工作。
 
我们之前注意到,可以针对可用区受损采用一种方案,这种方案需要更换 EC2 实例,效果欠佳。这并非因为我们无法启动新的 EC2 实例,而是因为,为了应对受损问题,系统必须立即获取 Amazon EC2 控制层面上恢复路径的依赖关系,以及新实例开始执行有用工作所必需的所有特定于应用程序的系统。根据应用程序,这些依赖关系可能包括下载运行时配置、向发现服务注册实例、获取凭证等步骤。控制层面系统必然比数据层面中的系统更复杂,当整个系统受损时,它们更有可能出现行为异常。

静态稳定性模式

在本节中,我们将介绍 AWS 中使用的两种高级模式,通过利用静态稳定来设计系统以实现高可用性。每种模式都适用于一组特定的情况,但都利用了可用区抽象。

可用区中的主动-主动示例:负载均衡服务
多个 AWS 服务内部由横向扩展的无状态 EC2 实例队列或 Amazon Elastic Container Service (ECS) 容器组成。我们在三个或更多可用区的 Auto Scaling 组中运行这些服务。此外,这些服务还会超额预置容量,因此,即使整个可用区受损,其余可用区中的服务器也可以承载负载。例如,当我们使用三个可用区时,我们超额预置了 50%。换言之,我们通过超额预置,使每个可用区仅以我们已对其进行负载测试的级别的 66% 运行。
 
最常见的示例是负载均衡的 HTTPS 服务。下图显示了提供 HTTPS 服务的面向公众的 Application Load Balancer。负载均衡器的目标是一个跨 eu-west-1 区域中三个可用区的 Auto Scaling 组。这是使用可用区的主动-主动高可用性示例。

如果出现可用区受损问题,上图中显示的架构不需要采取任何措施。受损可用区中的 EC2 实例将无法进行运行状况检查,Application Load Balancer 将从这些实例转移流量。实际上,Elastic Load Balancing 服务是根据这一原则设计的。它预置了足够的负载均衡容量,可以承受可用区受损,而无需进行扩展。

即使没有负载均衡器或 HTTPS 服务,我们也会使用此模式。例如,处理来自 Amazon Simple Queue Service (SQS) 队列消息的 EC2 实例队列也可遵循此模式。这些实例部署在多个可用区的 Auto Scaling 组中,并适当地进行超额预置。如果可用区受损,服务不会执行任何操作。受损的实例停止工作,而其他实例则会收拾残局。

可用区中的主动-备用示例:关系数据库
我们构建的一些服务是有状态的,需要一个主节点或领导节点来协调工作。其中一个示例是使用关系数据库的服务,如使用 MySQL 或 Postgres 数据库引擎的 Amazon RDS。此类关系数据库的典型高可用性设置具有一个主实例(所有写入操作都必须使用该实例)和一个备用候选实例。我们还可能有其他读取复本,这在下表中并未显示。当我们使用此类有状态的基础设施时,在与主节点不同的可用区中会有一个热备用节点。
 
下图显示了 Amazon RDS 数据库。当我们使用 Amazon RDS 预置数据库时,它需要一个子网组。 子网组是一组子网,它跨越多个数据库实例将被预置到的可用区。Amazon RDS 将备用候选实例置于与主节点不同的可用区中。这是使用可用区的主动-备用高可用性示例。

与主动-主动无状态示例一样,当包含主节点的可用区受损时,有状态服务对基础设施将不起任何作用。对于使用 Amazon RDS 的服务,RDS 将管理故障转移并将 DNS 名称重指向到工作可用区中的新主节点。此模式也适用于其他主动-备用设置,即使它们不使用关系数据库。特别是,我们将此应用于具有集群架构(具有领导节点)的系统。我们在多个可用区中部署这些集群,并从备用候选节点中选择新的领导节点,而不是“及时”启动替换节点。

这两种模式的共同之处在于,在可用区受损时,它们都在产生任何实际影响之前预置了所需的容量。在这两种情况下,服务都不会特意采取任何控制层面依赖关系,例如,为应对可用区受损而预置新的基础设施或进行修改。

幕后原理:Amazon EC2 内部的静态稳定性

本文的最后一节将更深入地介绍弹性可用区架构,涵盖我们遵循 Amazon EC2 中的可用区独立性原则的一些方式。当我们构建的服务不仅需要自身具有高可用性,而且还需要提供其上的其他服务可以高度可用的基础设施时,了解其中一些概念很有帮助。作为低级别 AWS 基础设施的提供商,Amazon EC2 是应用程序可用于实现高可用性的基础设施。有时其他系统可能也希望采用这一策略。

我们在部署实践中遵循 Amazon EC2 中的可用区独立性原则。在 Amazon EC2 中,软件被部署到托管 EC2 实例的物理服务器、边缘设备、DNS 解析器、EC2 实例启动路径中的控制层面组件以及 EC2 实例所依赖的许多其他组件。这些部署遵循分区部署日历。这意味着同一区域中的两个可用区将在不同的日期收到给定的部署。在 AWS 中,我们采用分阶段部署方法。例如,我们遵循最佳实践(无论我们部署的服务类型如何),即首先部署整体,然后部署服务器的 1/N 等。但是,在特定服务情况下(例如 Amazon EC2 中的服务),我们的部署将更进一步,并特意与可用区边界保持一致。这样,部署问题将影响一个可用区,并会回滚和修复。它不会影响其他可用区,这些可用区将继续正常工作。

在 Amazon EC2 中构建时,我们使用独立可用区原则的另一种方法是将所有数据包流设计为处于可用区内,而不是跨越边界。第二点是网络流量保持在可用区本地,这点值得详细了解。这是一个有趣的插图,说明了我们在构建高可用性区域系统(该系统为独立可用区的使用者)时如何进行不同的思考(即,它使用可用区独立性作为构建高可用性服务的基础),这与我们向其他人提供独立于可用区的基础设施(允许他们针对高可用性进行构建)的情况相反。

下图介绍了一种高度可用的外部服务(以橙色显示),该服务依赖于另一种内部服务(以绿色显示)。简单的设计将这两种服务都视为独立 EC2 可用区的使用者。每种橙色和绿色服务都由 Application Load Balancer 进行领导而且每种服务都有一组妥善预置的后端主机,分布在三个可用区中。一种高度可用的区域服务可调用另一种高度可用的区域服务。这是一种简单的设计,对于我们构建的许多服务来说,这也是一种很好的设计。

但是,假设绿色服务是一项基础服务,亦即假设它不仅要具有高可用性,而且本身要作为提供可用区独立性的构建块。在这种情况下,我们可能会将其设计为区域本地服务的三个实例,并在此基础上遵循可用区感知部署实践。下图说明了高可用性区域服务调用高可用性分区服务的设计。

我们将构建块服务设计为独立于可用区的原因要归结为简单的算法。假设可用区已受损。对于黑白故障,Application Load Balancer 将自动从受影响的节点上进行故障转移。然而,并非所有的故障都如此明显。可能存在灰色故障(例如软件中的错误),负载均衡器无法在其运行状况检查中看到该错误,也无法进行彻底处理。

在前面一个高可用性区域服务调用另一个高可用性区域服务的示例中,如果通过系统发送请求,然后作出一些简单的假设,请求避开受损可用区的可能性为 2/3 * 2/3 = 4/9。也就是说,这一请求比避开事件的可能性还要低。相比之下,如果我们将绿色服务构建为分区服务(如当前示例中所示),然后,橙色服务中的主机可以调用同一可用区中的绿色终端节点。使用此架构,避开受损可用区的可能性为 2/3。如果此调用路径中包含 N 个服务,然后这些数字对于 N 个区域服务一般化为 (2/3)^N,而对于 N 个分区服务则保持不变,仍为常数 2/3。

因此,我们将 Amazon EC2 NAT 网关构建为分区服务。NAT 网关是一种 Amazon EC2 功能,它允许来自私有子网的出站 Internet 流量,它不是区域 VPC 范围的网关,而是作为分区资源,客户可按可用区单独实例化,如下图所示。NAT 网关位于 VPC 的 Internet 连接路径中,因此它是该 VPC 中所有 EC2 实例的数据层面的组成部分。如果一个可用区存在连接障碍,我们希望将该障碍控制在该可用区内,而不是将其传播到其他区域。最后,我们希望已构建类似本文上述架构的客户(即通过在三个可用区中预置一个在任意两个可用区具有足够容量的队列来承载全部负载)知悉这样一个事实,即其他可用区将完全不受受损可用区中发生的任何情况的影响。实现此目的的唯一方法是确保所有基础组件(如 NAT 网关)都确实处于可用区内。

这种选择会造成额外的复杂性成本。对于我们来说,在 Amazon EC2,额外的复杂性表现为管理分区而非区域服务环境。对于 NAT 网关的客户来说,额外的复杂性表现为拥有多个 NAT 网关和路由表,以便在 VPC 的不同可用区中使用。这种额外的复杂性是适当的,因为 NAT 网关本身是一项基础服务,是 Amazon EC2 数据层面的一部分,应该提供分区可用性保证。

在构建独立于可用区的服务时,我们还需要考虑一个因素,即数据的耐用性。虽然前面介绍的每个分区架构都显示了包含在单个可用区中的整个堆栈,但我们会在多个可用区中复制任何硬状态以实现灾难恢复。例如,我们通常在 Amazon S3 中存储定期数据库备份,并跨可用区边界维护数据存储的只读副本。主可用区不需要这些副本即可正常工作。它们可确保我们将客户或业务关键型数据存储在多个位置。

在设计将在 AWS 中运行的面向服务的架构时,我们已学会使用以下两种模式之一或两者的组合:

• 更简单的模式:区域-调用-区域。这通常是面向外部的服务的最佳选择,也适用于大多数内部服务。例如,当在 AWS 中构建更高级别的应用程序服务(如 Amazon API Gateway 和 AWS 无服务器技术)时,我们会使用此模式来提供高可用性,即使在可用区受损的情况下也是如此。
• 更复杂的模式:区域-调用-分区或分区-调用-分区。在 Amazon EC2 内设计内部(在某些情况下是外部)数据层面组件时(例如,直接位于关键数据路径中的网络设备或其他基础设施),我们遵循可用区独立性模式,并使用在可用区中孤立的实例,这样网络流量就会保持在同一可用区中。此模式不仅有助于将损害隔离到可用区,而且在 AWS 中具备有利的网络流量成本特性。

结论

在本文中,我们讨论了在 AWS 上使用的一些简单策略,以便成功地获取可用区的依赖关系。我们已经了解到,静态稳定性的关键要素是在产生损害之前进行预测。无论系统是否在主动-主动水平可扩展队列上运行,或者它是否为有状态的主动-备用模式,我们都可以使用可用区来实现高可用性级别。我们部署我们的系统,以便在产生损害时所需的所有容量都已完全预置并准备就绪。最后,我们深入了解了 Amazon EC2 本身如何使用静态稳定性概念来保持可用区相互独立。


关于作者

Becky Weiss 是 Amazon Web Services 的高级首席工程师。她目前在 AWS 主要从事 Identity and Access Management 方面的工作,并且通常专注于为客户在云中提供灵活、全面和权威的安全控制。过去,她曾从事 Amazon Virtual Private Cloud(即联网)和 AWS Lambda 方面的工作,还曾与 AWS 专业服务合作,帮助企业客户在 AWS 中成功保护其环境。Becky 恰好也是 AWS 的超级粉丝,在业余时间,她在 AWS 上构建了各种有用和无用的有趣事物。在 AWS 就职之前,Becky 曾为 Microsoft 工作,主要从事 Windows 和 Windows Phone 相关的工作。

Mike Furr 是 Amazon Web Services 的首席工程师。他于 2009 年加入 Amazon,当时他已在马里兰大学帕克分校完成计算机科学博士学位。在 Amazon 任职期间,他曾从事 Virtual Private Cloud、Direct Connect 以及 AWS 计量和计费堆栈方面的工作。他现在专门从事 EC2 方面的工作,帮助团队扩展云。

通过负载卸除机制来避免过载