文章目录
- [第17章 云计算和分布式计算](#第17章 云计算和分布式计算)
-
- [17.1 云基础](#17.1 云基础)
- [17.2 云中的故障](#17.2 云中的故障)
- [17.3 利用多个实例提升性能和可用性](#17.3 利用多个实例提升性能和可用性)
- [17.4 小结](#17.4 小结)
- [17.5 扩展阅读](#17.5 扩展阅读)
- [17.6 问题讨论](#17.6 问题讨论)
第17章 云计算和分布式计算
分布式系统是这样一种系统:在其中,一台你甚至都不知道其存在的计算机出现故障,就可能会使你自己的计算机无法使用。
---Leslie Lamport
"云计算" 是指按需提供资源。这个术语被用于指代多种计算能力。例如,你可能会说:"我所有的照片都备份到云端了。" 但这意味着什么呢?这意味着:
-
我的照片存储在别人的计算机上。他们负责资金投入、维护、保养以及备份工作。
-
我可以通过互联网访问我的照片。
-
我只需为我使用或申请的空间付费。
-
存储服务是弹性的,这意味着它可以根据我的需求变化而扩展或收缩。
-
我对云服务的使用是自行配置的:我创建一个账户后就可以立即开始使用它来存储我的资料。
从云端提供的计算能力包括从照片(或其他类型的数字制品)存储等应用程序,到通过 API(应用程序接口)暴露的细粒度服务(例如,文本翻译或货币转换),再到低层级的基础设施服务,如处理器、网络和存储虚拟化。
在本章中,我们将重点关注软件架构师如何使用来自云端的基础设施服务来交付其正在设计和开发的服务。在此过程中,我们将深入探讨分布式计算的一些最重要的原理和技术。这意味着使用多台(真实的或虚拟的)计算机协同工作,从而产生比单台计算机完成所有工作更快的性能和更健壮的系统。我们将这一主题包含在本章中,是因为分布式计算在基于云的系统中体现得最为深入。我们在此给出的内容是对与架构最相关的原理的简要概述。
我们首先讨论云如何提供和管理虚拟机。
17.1 云基础
公有云由云服务提供商拥有和提供。这些机构向任何同意服务条款并且能够为使用服务付费的人提供基础设施服务。一般来说,使用这种基础设施构建的服务可通过公共互联网访问,不过你可以设置防火墙等机制来限制可见性和访问权限。
一些组织运营私有云。私有云由一个组织拥有并运营,仅供该组织的成员使用。一个组织可能出于控制、安全和成本等方面的考虑而选择运营私有云。在这种情况下,云基础设施及其上开发的服务仅在组织内部网络中可见和可访问。
混合云方法是一种混合模式,其中一些工作负载在私有云中运行,其他工作负载在公有云中运行。在从私有云向公有云(反之亦然)迁移期间可能会使用混合云,或者也可能因为某些数据在法律上需要受到比公有云所能提供的更强的控制和审查而使用混合云。
对于使用云服务设计软件的架构师来说,从技术角度看,私有云和公有云之间没有太大差异。因此,我们在这里将讨论重点放在基础设施即服务(IaaS)公有云上。
一个典型的公有云数据中心拥有数以万计的物理设备 ------ 接近 10 万而不是 5 万。数据中心规模的限制因素是其消耗的电力以及设备产生的热量:将电力引入建筑物、分配给设备以及消除设备产生的热量都存在实际限制。图 17.1 展示了一个典型的云数据中心。每个机架包含 25 台以上的计算机(每台计算机有多个 CPU),确切数量取决于可用的电力和冷却能力。数据中心由一排排这样的机架组成,高速网络交换机连接着这些机架。云数据中心是能源效率(在 第 6 章 讨论的主题)在某些应用中成为关键质量属性的原因之一。
图 17.1 云数据中心
当你通过公有云提供商访问云时,实际上你正在访问分布在全球各地的数据中心。云提供商将其数据中心划分为 "区域"。云区域既是一种逻辑结构,也是一种物理结构。由于你开发并部署到云中的服务是通过互联网访问的,云区域可以帮助你确保服务在物理上靠近其用户,从而减少访问服务的网络延迟。此外,一些监管限制,如《通用数据保护条例》(GDPR),可能会限制某些类型的数据跨境传输,所以云区域有助于云提供商遵守这些法规。
一个云区域有许多物理上分布的、具有不同电力来源和互联网连接的的数据中心。一个区域内的数据中心被分组为 "可用区",这样,两个不同可用区中的所有数据中心同时发生故障的概率极低。
选择你的服务将运行的云区域是一个重要的设计决策。当你请求提供一个在云中运行的新虚拟机(VM)时,你可以指定该虚拟机将在哪个区域运行。有时可用区可能会被自动选择,但出于可用性和业务连续性的原因,你通常会希望自己选择可用区。
对公有云的所有访问都通过互联网进行。进入云有两个主要网关:管理网关和消息网关([图 17.2][ch15fig02])。在这里,我们将重点关注管理网关;我们在 第 15 章 中讨论过消息网关。
图 17.2 公有云入口网关
假设你希望在云中为自己分配一个虚拟机(VM)。你向管理网关发送一个请求,要求创建一个新的虚拟机实例。这个请求有许多参数,但三个基本参数是新实例将运行的云区域、实例类型(例如,CPU 和内存大小)以及一个虚拟机映像的标识(ID)。管理网关负责管理数以万计的物理计算机,每台物理计算机都有一个虚拟机管理程序(hypervisor)来管理其上的虚拟机。所以,管理网关将通过询问来确定一个能够管理你所选类型的额外虚拟机的虚拟机管理程序:那台物理机上是否有足够未分配的 CPU 和内存容量来满足你的需求?如果有,它将要求该虚拟机管理程序创建一个额外的虚拟机;虚拟机管理程序将执行此任务并将新虚拟机的 IP 地址返回给管理网关。然后管理网关将该 IP 地址发送给你。云提供商确保其数据中心有足够的物理硬件资源可用,这样你的请求就永远不会因为资源不足而失败。
管理网关不仅返回新分配的虚拟机的 IP 地址,还返回一个主机名。分配虚拟机后返回的主机名反映了该 IP 地址已被添加到云域名系统(DNS)这一事实。任何虚拟机镜像都可用于创建新的虚拟机实例;也就是说,虚拟机镜像可能包含一个简单的服务,或者只是创建一个复杂系统的部署过程中的一个步骤。
除了分配新虚拟机之外,管理网关还执行其他功能。它支持收集有关虚拟机的计费信息,并且提供监控和销毁虚拟机的能力。
管理网关可通过互联网向其应用程序接口(API)发送消息来访问。这些消息可以来自另一个服务,例如部署服务,或者它们可以由你计算机上的命令行程序生成(允许你编写操作脚本)。管理网关也可以通过云服务提供商运营的基于网络的应用程序来访问,不过这种交互界面对于除最基本操作之外的操作来说效率不高。
17.2 云中的故障
当一个数据中心包含数以万计的物理计算机时,几乎可以肯定每天都会有一台或多台计算机出现故障。亚马逊报告称,在一个拥有约 64,000 台计算机(每台计算机有两个旋转磁盘驱动器)的数据中心里,每天大约会有 5 台计算机和 17 个磁盘发生故障。谷歌也报告了类似的统计数据。除了计算机和磁盘故障之外,网络交换机也可能发生故障;数据中心可能过热,导致所有计算机故障;或者一些自然灾害可能会使整个数据中心瘫痪。尽管你的云提供商总体停机次数相对较少,但你的特定虚拟机正在运行的物理计算机可能会出现故障。如果可用性对你的服务很重要,你就需要仔细考虑你希望达到何种级别的可用性以及如何实现它。
我们将讨论两个与云中故障特别相关的概念:超时和长尾延迟。
超时
回顾 第 4 章 可知,超时是一种提高可用性的策略。在分布式系统中,超时被用于检测故障。使用超时会产生以下几个结果:
-
超时无法区分是计算机故障或网络连接中断,还是对消息的回复过慢(超出了超时期限)。这将导致你把一些缓慢的响应标记为故障。
-
超时不会告诉你故障或缓慢发生在哪里。
-
很多时候,对一个服务的请求会触发该服务向其他服务发出请求,而其他服务又会发出更多请求。即使这个链中的每个响应的延迟接近(但慢于)预期的平均响应时间,总体延迟可能(错误地)暗示存在故障。
超时(即判定一个响应耗时过长)通常被用于检测故障。超时无法确定故障是由于被请求服务的软件、该服务运行的虚拟或物理机器,还是与服务的网络连接出现问题。在大多数情况下,原因并不重要:你发出了一个请求,或者你在等待一个周期性的保活或心跳消息,但没有及时收到响应,现在你需要采取行动来补救这种情况。
这看似简单,但在实际系统中可能很复杂。恢复行动通常是有成本的,例如延迟惩罚。你可能需要启动一个新的虚拟机,这可能需要几分钟才能准备好接受新请求。你可能需要与不同的服务实例建立一个新的会话,这可能会影响你系统的可用性。云系统中的响应时间可能会有很大差异。在实际只是暂时延迟的情况下就断定存在故障,可能会在不必要的时候增加恢复成本。
分布式系统设计者通常会对超时检测机制进行参数化,以便能够针对某个系统或基础设施进行调整。一个参数是超时间隔 ------ 系统在判定一个响应失败之前应该等待多长时间。大多数系统不会在仅仅错过一次响应后就触发故障恢复。相反,典型的做法是在较长的时间间隔内查看错过的响应次数。错过的响应次数是超时机制的第二个参数。例如,可以将超时设置为 200 毫秒,并在 1 秒的时间间隔内错过 3 条消息后触发故障恢复。
对于在单个数据中心运行的系统,可以激进地设置超时和阈值,因为网络延迟极小,错过响应很可能是由于软件崩溃或硬件故障。相比之下,对于在广域网、蜂窝无线网络甚至卫星链路运行的系统,在设置参数时应该更加慎重,因为这些系统可能会经历间歇性但较长的网络延迟。在这种情况下,可以放宽参数以反映这种可能性,避免触发不必要的恢复操作。
长尾延迟
不管原因是实际故障还是仅仅是响应缓慢,对你原始请求的响应可能会表现出所谓的长尾延迟。图 17.3 展示了对亚马逊网络服务(AWS)的 1000 个 "启动实例" 请求的延迟直方图。注意,有些请求需要很长时间才能得到满足。在评估像这样的测量数据集时,你必须谨慎选择用于描述数据集特征的统计量。在这种情况下,直方图在 22 秒的延迟处达到峰值;然而,所有测量的平均延迟是 28 秒,中位数延迟(一半的请求在小于此值的延迟内完成)是 23 秒。甚至在 57 秒的延迟之后,仍有 5% 的请求尚未完成(即第 95 百分位数是 57 秒)。所以,尽管对基于云的服务的每个服务到服务请求的平均延迟可能在可容忍的范围内,但相当数量的这些请求可能会有长得多的延迟 ------ 在这种情况下,是平均延迟的 2 到 10 倍。这些就是直方图右侧长尾部分的测量值。
图 17.3 对亚马逊网络服务(AWS)1000 个 "启动实例" 请求的长尾分布
长尾延迟是服务请求路径中某处拥塞或故障的结果。许多因素可能导致拥塞 ------ 服务器队列、虚拟机管理程序调度或其他因素 ------ 但作为服务开发者,拥塞的原因不在你的控制范围内。你的监控技术以及实现所需性能和可用性的策略必须反映长尾分布的实际情况。
处理长尾问题的两种技术是对冲请求(hedged requests)和替代请求(alternative requests)。
-
对冲请求 :发出比所需更多的请求,然后在收到足够的响应后取消请求(或忽略响应)。例如,假设要启动 10 个微服务(见 第 5 章)实例。发出 11 个请求,在 10 个请求完成后,终止尚未响应的那个请求。
-
替代请求:对冲请求技术的一种变体称为替代请求。在刚刚描述的场景中,发出 10 个请求。当 8 个请求完成时,再发出 2 个请求,当总共收到 10 个响应时,取消仍未完成的 2 个请求。
17.3 利用多个实例提升性能和可用性
如果云中托管的服务收到的请求数量超出了它在规定延迟内能够处理的数量,该服务就会过载。这可能是因为 I/O 带宽、CPU 周期、内存或其他资源不足。在某些情况下,你可以通过在能提供更多所需资源的不同实例类型中运行服务来解决服务过载问题。这种方法很简单:服务的设计无需改变;相反,服务只是在更大的虚拟机上运行。这种被称为纵向扩展(垂直扩展)或向上扩展的方法,对应于 第 9 章 中的增加资源性能策略。
纵向扩展能够达成的效果是有限的。特别是,可能不存在足够大的虚拟机实例类型来支持工作负载。在这种情况下,横向扩展(向外扩展)能提供更多所需类型的资源。横向扩展涉及拥有同一服务的多个副本,并使用负载均衡器在这些副本之间分配请求 ------ 这分别等同于 第 9 章 中的维持多个计算副本策略和负载均衡器模式。
分布式计算和负载均衡器
负载均衡器可以是独立系统,也可以与其他功能捆绑在一起。负载均衡器必须非常高效,因为它位于从客户端到服务的每条消息的路径上,而且即使它与其他功能打包在一起,在逻辑上也是独立的。在这里,我们将讨论分为两个主要方面:负载均衡器如何工作,以及位于负载均衡器后面的服务必须如何设计以管理服务状态。一旦我们理解了这些过程,我们就能探究系统健康状况的管理以及负载均衡器如何提高其可用性。
负载均衡器解决了以下问题:在虚拟机或容器中运行的单个服务实例收到了太多请求,以至于无法提供可接受的延迟。一种解决方案是拥有该服务的多个实例并在它们之间分配请求。在这种情况下,分配机制是一个独立的服务 ------ 负载均衡器。图 17.4 展示了负载均衡器在两个虚拟机(服务)实例之间分配请求。如果是两个容器实例,同样的讨论也适用。(容器在 第 16 章 中已经讨论过。)
图 17.4 负载均衡器将来自两个客户端的请求分配到两个服务实例
你可能想知道什么是 "过多请求" 和 "合理响应时间"。在本章后面讨论自动扩展时我们会再回到这些问题。现在,让我们先关注负载均衡器是如何工作的。
在 图 17.4 中,每个请求都被发送到负载均衡器。为了便于我们的讨论,假设负载均衡器将第一个请求发送到实例 1,第二个请求发送到实例 2,第三个请求再发回实例 1,依此类推。这样就将一半的请求发送到每个实例,在两个实例之间实现了 "负载" 的 "均衡"------ 这就是它名字的由来。
关于这个简单的负载均衡器示例的一些观察:
-
我们提到的算法(在两个实例之间交替分配消息)被称为 "轮询(round - robin)" 算法。只有当每个请求在响应时消耗大致相同的资源时,该算法才能在服务实例间均匀地平衡负载。当处理请求所需的资源消耗不同时,还存在其他用于分配消息的算法。
-
从客户端的角度来看,服务的 IP 地址实际上就是负载均衡器的地址。这个地址可能与域名系统(DNS)中的主机名相关联。客户端不知道,也不需要知道存在多少个服务实例或者这些服务实例中任何一个的 IP 地址。这使得客户端能够适应此类信息的变更 ------ 这是 第 8 章 中讨论的使用中间件的一个例子。
-
可能存在多个客户端共存的情况。每个客户端将其消息发送到负载均衡器,而负载均衡器并不关心消息的来源。负载均衡器在消息到达时对其进行分配。(我们暂时先忽略所谓的 "粘性会话" 或 "会话亲和性" 概念。)
-
负载均衡器可能会过载。在这种情况下,解决方案是对负载均衡器的负载进行均衡,有时这被称为全局负载均衡。也就是说,消息在到达服务实例之前要经过一系列的负载均衡器。
到目前为止,我们对负载均衡器的讨论主要集中在增加可处理的工作量上。在这里,我们将考虑负载均衡器如何也用于提高服务的可用性。
图 17.4 展示了来自客户端的消息经过负载均衡器,但没有展示返回消息。返回消息直接从服务实例发送到 IP 消息头 "源" 字段中标识的服务(通常,这会绕过负载均衡器,尽管在某些配置中返回消息可能会发送到负载均衡器)。在绕过负载均衡器的情况下,它无法得知某个消息是否已被服务实例处理,或者处理一个消息花费了多长时间。如果没有额外的机制,负载均衡器就不会知道任何服务实例是否处于存活并正在处理请求的状态,或者是否有某个实例或者所有实例都已发生故障。
运行状况健康检查是一种允许负载均衡器确定实例是否正常运行的机制。这就是 第 4 章 中可用性策略里 "故障检测" 类别的目的。负载均衡器将定期检查分配给它的实例的健康状况。如果一个实例未能对健康检查做出响应,它就会被标记为不健康,并且不会再向其发送任何消息。健康检查可以包括负载均衡器向实例发送 ping(网络检测)、与实例建立 TCP 连接,甚至发送消息进行处理。在后一种情况下,返回的 IP 地址是负载均衡器的地址。
一个实例有可能从健康状态变为不健康状态,然后又恢复健康。例如,假设实例有一个过载的队列。当最初被联系时,它可能不会对负载均衡器的健康检查做出响应,但一旦队列被清空,它可能就又能做出响应了。出于这个原因,负载均衡器在将一个实例移到不健康列表之前会进行多次检查,然后定期检查不健康列表以确定某个实例是否又能做出响应了。在其他情况下,严重故障或崩溃可能会导致故障实例重新启动并向负载均衡器重新注册,或者可能会启动一个新的替换实例并向负载均衡器注册,以维持整体的服务交付能力。
具有健康检查功能的负载均衡器通过向客户端隐藏服务实例的故障来提高可用性。服务实例池的规模可以设置为能够容纳一定数量的同时发生的服务实例故障,同时仍然提供足够的整体服务能力,以便在期望的延迟内处理所需数量的客户端请求。然而,即使使用健康检查,服务实例有时可能会开始处理客户端请求但永远不返回响应。在设计客户端时必须考虑到,如果没有及时收到响应就要重新发送请求,这样负载均衡器就可以将请求分配到不同的服务实例。相应地,服务也必须被设计为能够处理多个相同的请求。
分布式系统中的状态管理
"状态" 是指服务内部影响对客户端请求响应计算的信息。状态 ------ 或者更准确地说,存储状态的变量或数据结构的值的集合 ------ 取决于对服务请求的历史记录。
当一个服务能够同时处理多个客户端请求时(要么是因为服务实例是多线程的,要么是因为负载均衡器后面有多个服务实例,或者两者皆有),状态管理就变得很重要。关键问题是状态存储在哪里。有以下三个选项:
-
在每个服务实例中维护历史记录,在这种情况下,这些服务被描述为 "有状态的"。
-
在每个客户端中维护历史记录,在这种情况下,这些服务被描述为 "无状态的"。
-
历史记录在服务和客户端之外的数据库中持久化,在这种情况下,这些服务被描述为 "无状态的"。
通常的做法是设计和实现无状态的服务。有状态的服务如果发生故障会丢失其历史记录,而且恢复该状态可能很困难。此外,正如我们将在下一节看到的,可能会创建新的服务实例,而将服务设计为无状态可以使新的服务实例处理客户端请求并产生与任何其他服务实例相同的响应。
在某些情况下,设计无状态的服务可能很困难或者效率低下,所以我们可能希望来自客户端的一系列消息由同一个服务实例处理。我们可以通过让该系列中的第一个请求由负载均衡器处理并分配到一个服务实例,然后允许客户端直接与该服务实例建立会话,并且后续请求绕过负载均衡器来实现这一点。或者,一些负载均衡器可以被配置为将某些类型的请求视为粘性(sticky)请求,这会使负载均衡器将来自客户端的后续请求发送到处理该客户端上一个消息的同一个服务实例。这些方法(直接会话和粘性消息)应该只在特殊情况下使用,因为存在实例故障的可能性以及消息所粘性(附着)的实例可能会有过载的风险。
通常,需要在一个服务的所有实例之间共享信息。这些信息可能包括前面讨论过的状态信息,也可能是服务实例高效协作所需的其他信息 ------ 例如,该服务的负载均衡器的 IP 地址。接下来将讨论一种用于管理在一个服务的所有实例之间共享的相对少量信息的解决方案。
分布式系统中的时间协调
确切知道当前时间看似是一项简单的任务,但实际上并非易事。计算机中的硬件时钟大约每 12 天就会快或慢 1 秒。可以说,如果你的计算设备处于外界环境中,它可能能够接收来自全球定位系统(GPS)卫星的时间信号,这种信号提供的时间精度可达到 100 纳秒或更精确。
让两个或更多设备就当前时间达成一致可能更具挑战性。网络上两个不同设备的时钟读数肯定会有所不同。网络时间协议(NTP)用于在通过局域网或广域网连接的不同设备之间同步时间。它涉及在时间服务器和客户端设备之间交换消息以估计网络延迟,然后应用算法将客户端设备的时钟与时间服务器同步。在局域网中,NTP 的精度约为 1 毫秒,在公共网络中约为 10 毫秒。网络拥塞可能会导致 100 毫秒或更多的误差。
云服务提供商为其时间服务器提供非常精确的时间参考。例如,亚马逊和谷歌使用原子钟,其漂移几乎可以忽略不计。因此,两者都能对 "现在是什么时间?" 这个问题提供极其精确的答案。当然,当你得到答案时是什么时间那就是另一回事了。
幸运的是,在很多情况下,近似准确的时间就足够了。然而,实际上,你应该假定两个不同设备的时钟读数之间存在一定程度的误差。出于这个原因,大多数分布式系统在设计时都确保应用程序正常运行不需要设备之间的时间同步。你可以使用设备时间来触发周期性操作、为日志条目添加时间戳以及用于其他一些不需要与其他设备精确协调的目的。
同样幸运的是,在很多情况下,知道事件的顺序比知道这些事件发生的时间更为重要。股票市场上的交易决策属于这种情况,任何形式的在线拍卖也是如此。两者都依赖于按照数据包传输的相同顺序来处理数据包。
对于设备之间的关键协调,大多数分布式系统使用诸如向量时钟(实际上不是时钟,而是随着动作在应用程序中的服务间传播而进行跟踪的计数器)之类的机制来确定一个事件是否发生在另一个事件之前,而不是比较时间。这确保了应用程序能够以正确的顺序执行操作。我们在下一节讨论的大多数数据协调机制都依赖于这种动作顺序。
对于架构师来说,成功的时间协调包括知道你是否真的需要依赖实际的时钟时间,或者确保正确的顺序是否就足够了。如果前者很重要,那么了解你的精度要求并据此选择解决方案。
分布式系统中的数据协调
考虑创建一个在分布式机器之间共享的资源锁的问题。假设某个关键资源正在由运行在两台不同物理计算机上的两个不同虚拟机中的服务实例访问。我们假设这个关键资源是一个数据项 ------ 例如,你的银行账户余额。更改账户余额需要读取当前余额、加上或减去交易金额,然后写回新的余额。如果我们允许两个服务实例独立地对这个数据项进行操作,就有可能出现竞争条件,例如两笔同时进行的存款相互覆盖。在这种情况下的标准解决方案是锁定该数据项,这样一个服务在获取锁之前就无法访问你的账户余额。我们避免了竞争条件,因为服务实例 1 被授予了对你银行账户的锁,并且可以独立地进行存款操作,直到它释放锁。然后一直在等待锁可用的服务实例 2 就可以锁定银行账户并进行第二笔存款。
当服务是在单台机器上运行的进程时,使用共享锁的这种解决方案很容易实现,请求和释放锁是简单的内存访问操作,速度非常快且具有原子性。然而,在分布式系统中,这种方案会出现两个问题。首先,传统上用于获取锁的两阶段提交协议需要在网络上传输多个消息。在最好的情况下,这只会给操作增加延迟,但在最坏的情况下,这些消息中的任何一个都可能无法传递。其次,服务实例 1 在获取锁之后可能会发生故障,从而阻止服务实例 2 继续进行操作。
解决这些问题涉及复杂的分布式协调算法。本章开头引用的莱斯利・兰波特(Leslie Lamport)开发了最早的此类算法之一,他将其命名为 "Paxos"。Paxos 和其他分布式协调算法依赖于一种共识机制,即使在计算机或网络出现故障时也能让参与者达成一致。这些算法的设计正确性非常复杂,而且由于编程语言和网络接口语义的微妙之处,即使实现一个已被证明的算法也很困难。实际上,分布式协调是那些你不应该尝试自己解决的问题之一。使用现有的解决方案包,如 Apache Zookeeper、Consul 和 etcd 等,几乎总是比自己编写更好的主意。当服务实例需要共享信息时,它们将信息存储在一个使用分布式协调机制的服务中,以确保所有服务看到相同的值。
我们最后一个分布式计算主题是实例的自动创建和销毁。
自动扩展:实例的自动创建和销毁
考虑一个传统的数据中心,你的组织拥有所有的物理资源。在这种环境下,你的组织需要为一个系统分配足够的物理硬件来处理它承诺处理的最大工作量峰值。当工作量低于峰值时,分配给该系统的部分(或大部分)硬件容量处于闲置状态。现在将其与云环境进行比较。云的两个决定性特征是,你只需为你申请的资源付费,并且你可以轻松快速地添加和释放资源(弹性)。这些特征结合在一起,使你能够创建具有处理工作量能力的系统,并且你无需为任何多余的容量付费。
弹性适用于不同的时间尺度。一些系统的工作量相对稳定,在这种情况下,你可能会考虑以月或季度为时间尺度手动审查和更改资源分配,以匹配这种缓慢变化的工作量。其他系统的工作量更具动态性,请求速率有快速的增减,因此需要一种自动添加和释放服务实例的方法。
自动扩展是一种基础设施服务,它在需要时自动创建新实例,并在不再需要时释放多余的实例。它通常与负载均衡协同工作,以增加和减少负载均衡器后面的服务实例池。自动扩展容器与自动扩展虚拟机略有不同。我们先讨论自动扩展虚拟机,然后讨论自动扩展容器时的差异。
自动缩放是一项基础结构服务,可在需要时自动创建新实例,并在不再需要时释放多余的实例。它通常与负载平衡结合使用,以增加和收缩负载均衡器后面的服务实例池。自动缩放容器与自动缩放 VM 略有不同。我们首先讨论自动缩放 VM,然后讨论自动缩放容器时的差异。
自动扩展虚拟机
回到 图 17.4,假设两个客户端产生的请求数量超过了图中所示的两个服务实例所能处理的数量。自动扩展会基于用于前两个实例的相同虚拟机镜像创建第三个实例。新实例向负载均衡器注册,这样后续请求就会在三个实例而不是两个实例之间分配。图 17.5 展示了一个新组件 ------ 自动扩展器(autoscaler),它监控服务器实例的利用率并进行自动扩展。一旦自动扩展器创建了一个新的服务实例,它就会将新的 IP 地址通知负载均衡器,以便负载均衡器除了向其他实例分配请求之外,还能向新实例分配请求。
图 17.5 监控利用率的自动扩展器
由于客户端不知道存在多少个实例,也不知道哪个实例正在处理它们的请求,所以自动扩展活动对服务客户端是不可见的。此外,如果客户端请求速率降低,可以在客户端不知情的情况下将一个实例从负载均衡器池中移除、停止并释放。
作为基于云服务的架构师,你可以为自动扩展器设置一组规则来控制其行为。你提供给自动扩展器的配置信息包括以下内容:
- 创建新实例时要启动的虚拟机(VM)映像,以及云提供商要求的任何实例配置参数,如安全设置
- 任何实例的 CPU 利用率阈值(按时间测量),超过该阈值将启动一个新实例
- 任何实例的 CPU 利用率阈值(按时间测量),低于该阈值将关闭一个现有实例
- 创建和删除实例的网络 I/O 带宽阈值(按时间测量)
- 你希望在此组中的实例的最小和最大数量
自动扩展器不会基于 CPU 利用率或网络 I/O 带宽指标的瞬时值来创建或移除实例,原因有二。首先,这些指标有高峰和低谷,只有在合理的时间间隔内取平均值时才有意义。其次,分配和启动一个新的虚拟机需要相对较长的时间,大约是几分钟的量级。虚拟机映像必须被加载并连接到网络,并且操作系统必须启动后才能准备好处理消息。因此,自动扩展器规则通常采用这样的形式:"当 CPU 利用率在 5 分钟内高于 80% 时创建一个新的虚拟机"。
除了基于利用率指标创建和销毁虚拟机之外,你还可以设置规则来提供虚拟机的最小或最大数量,或者基于时间表创建虚拟机。例如,在典型的一周内,工作时间的负载可能会更重;基于这一情况,你可以在工作日开始前分配更多的虚拟机,并在工作日结束后移除一些。这些基于时间表的分配应该基于有关服务使用模式的历史数据。
当自动扩展器移除一个实例时,它不能直接关闭虚拟机。首先,它必须通知负载均衡器停止向该服务实例发送请求。其次,由于该实例可能正在处理一个请求,自动扩展器必须通知该实例它应该终止其活动并关闭,之后才能将其销毁。这个过程被称为 "排空"(draining)实例。作为服务开发者,你有责任实现适当的接口来接收终止和排空服务实例的指令。
自动缩放容器
由于容器在虚拟机(VM)上托管的运行时引擎上执行,扩展容器涉及两种不同类型的决策。在扩展虚拟机时,自动扩展器判定需要额外的虚拟机,然后分配一个新的虚拟机并加载适当的软件。扩展容器意味着做出两级决策。首先,判定当前工作负载需要一个额外的容器(或容器组 Pod)。其次,判定新的容器(或容器组 Pod)是否可以分配到现有的运行时引擎实例上,还是必须分配一个新的实例。如果必须分配一个新的实例,你需要检查是否有足够容量的虚拟机可用,或者是否需要分配一个额外的虚拟机。
控制容器扩展的软件独立于控制虚拟机扩展的软件。这使得容器的扩展可以在不同的云提供商之间具有可移植性。容器的发展有可能会整合这两种类型的扩展。在这种情况下,你应该意识到你可能正在你的软件和云提供商之间创建一种可能难以打破的依赖关系。
17.4 小结
云由分布式数据中心组成,每个数据中心包含数以万计的计算机。它通过一个可通过互联网访问的管理网关进行管理,该网关负责分配、释放和监控虚拟机(VM),以及计量资源使用情况和计算费用。
由于数据中心计算机数量众多,数据中心内计算机发生故障的情况相当频繁。作为服务架构师,你应该假定在某个时刻,运行你的服务的虚拟机将会发生故障。你还应该假定,你对其他服务的请求会呈现长尾分布,即多达 5% 的请求耗时会比平均请求长 5 到 10 倍。因此,你必须关注服务的可用性。
由于你的服务的单个实例可能无法及时满足所有请求,你可能决定运行多个包含服务实例的虚拟机或容器。这些多个实例位于负载均衡器之后。负载均衡器接收来自客户端的请求,并将请求分配到各个实例。
服务存在多个实例以及多个客户端,这对处理状态的方式有重大影响。关于状态存储位置的不同决策会导致不同的结果。最常见的做法是保持服务无状态,因为无状态服务更易于从故障中恢复并且更易于添加新实例。通过使用分布式协调服务,可以在服务实例之间共享少量数据。分布式协调服务的实现很复杂,但有几个经过验证的开源实现可供你使用。
云基础设施可以通过在需求增长时创建新实例、在需求减少时移除实例来自动扩展服务。通过一组给出创建或删除实例条件的规则来指定自动扩展器的行为。
17.5 扩展阅读
关于网络和虚拟化如何工作的更多细节可以在 [Bass 19] 中找到。
云环境下的长尾延迟现象首次在 [Dean 13] 中被提出。
Paxos 算法最初由 [Lamport 98] 提出。人们发现原始论文难以理解,但在维基百科(https://en.wikipedia.org/wiki/Paxos_(computer_science))上可以找到对 Paxos 非常详尽的描述。大约在同一时间,布赖恩・奥基(Brian Oki)和芭芭拉・利斯科夫(Barbara Liskov)独立开发并发表了一种名为视图标记复制(Viewstamped Replication)的算法,后来证明该算法与兰波特的 Paxos 算法等效 [Oki 88]。
可以在 https://zookeeper.apache.org/ 找到对 Apache Zookeeper 的描述。可以在 https://www.consul.io/ 找到 Consul 的相关信息,在 https://etcd.io/ 可以找到 etcd 的相关信息。
不同类型负载均衡器的讨论可在 https://docs.aws.amazon.com/AmazonECS/latest/developerguide/load-balancer-types.html 找到。
分布式系统中的时间问题在 https://medium.com/coinmonks/time-and-clocks-and-ordering-of-events-in-a-distributed-system-cdd3f6075e73 中有讨论。
分布式系统中的状态管理在 https://conferences.oreilly.com/software-architecture/sa-ny-2018/public/schedule/detail/64127 中有讨论。
17.6 问题讨论
1. 负载均衡器是一种中间件。中间件能增强可修改性但会降低性能,然而负载均衡器的存在是为了提高性能。解释这个明显的矛盾。
2. 上下文图展示一个实体以及它与之通信的其他实体。它将分配给所选实体的职责与分配给其他实体的职责分开,并展示完成所选实体职责所需的交互。绘制一个负载均衡器的上下文图。
3. 概述在云中分配虚拟机并显示其 IP 地址的一系列步骤。
4. 研究一个主要云提供商提供的服务。编写一组用于管理你将在此云上实现的服务的自动扩展的规则。
5. 一些负载均衡器使用一种称为消息队列的技术。研究消息队列并描述使用和不使用消息队列的负载均衡器之间的差异。