休斯顿,我们有一个问题。系统设计失败

1970年,美国工程师将阿波罗13号太空船发射升空。船上有三节燃料电池,无需担心,所有东西都可靠且重复地重复。但是没有人能想到,氧气瓶的爆炸会使三个电池中的两个失效。悲剧!宇航员返回家乡,与汤姆·汉克斯(Tom Hanks)合作制作了一部有关该事件的故事片,宇航员杰克·斯威格特(Jack Swigert)的话说:“休斯顿,我们有问题!”,历史悠久。



阿波罗13号的故事再次证明了您无法为所有可能的麻烦做好准备这一众所周知的事实。这是世界的自然财产:铁定期断裂,代码崩溃和人们犯错。不可能完全消除这一点。

对于大型分布式系统,此行为是正常现象;这是规模经济和统计数据的结果。这就是为什么“失败设计”(AWS)是AWS云服务的基本设计原则的原因。最初构建系统的方式是:尽快恢复全时运行,并最大程度地减少已知故障和未知故障的损坏。HighLoad ++上, Vasily Pantyukhin以战斗服务中的现实问题为例,展示了AWS开发人员使用的分布式系统的设计模式。

瓦西里·潘秋可欣母鸡)是欧洲,中东和非洲的Amazon Web Services的架构师。他最初是一名Unix管理员,在Sun Microsystem工作了6年,讲授技术课程,并在EMC教授了11年的以数据为中心的世界。在一个国际团队中,他设计和实施了从开普敦到奥斯陆的项目。现在,它可以帮助大型和小型公司在公共云中工作。


1949年,在加利福尼亚空军基地对一次飞机事故进行了调查。做到这一点的工程师之一是爱德华·墨菲。他对本地技术人员的工作描述如下:“如果有两种方法可以做某事,而其中一种会导致灾难,那么有人会选择这种方法。”

后来,由于亚瑟·布洛赫(Arthur Bloch)的支持,该声明作为墨菲的法律之一而载入史册。俄语-卑鄙的法律。其实质是不可能避免故障和人为错误,而必须以某种方式忍受它。这就是为什么在设计时,我们会立即将故障和单个组件的故障放入我们的系统中。

失败设计


在针对故障的设计中,我们试图改善三个特征:

  • 辅助功能(相同的“ nines”);
  • 可靠性-提供必要服务水平的系统属性;
  • 容错-系统的一项属性,可防止出现问题并在出现问题后迅速恢复。

可靠性具有“ 已知未知数”的特性。我们保护自己免受已知问题的影响,但不知道它们何时会发生。

的Un已知的未知”添加到容错-这是奇怪的问题,我们一无所知。云中的许多问题都与规模经济有关:当出现新的,惊人的和意想不到的影响时,系统的规模将扩大。

失败通常不是二进制现象。它的主要属性是“爆炸半径”或服务的退化程度,破坏半径。我们的任务是减小系统的“爆炸半径”。

如果我们认识到无法避免的问题,那么我们必须积极准备。这意味着我们以某种方式设计服务,以防万一出现问题(肯定会发生),我们控制问题,反之亦然。
当我们回答问题时,它控制了我们。

数据平面和控制平面


当然,您家里有可以通过遥控器控制的电子设备,例如电视。电视屏幕是数据平面的一部分-直接执行功能。遥控器是用户界面-控制平面。它用于管理和配置服务。在云中,我们尝试将数据平面和控制平面分开以进行测试和开发。

用户通常看不到控制平面的复杂性。但是,其设计和实现中的错误是造成大规模故障的最常见原因。这就是为什么我的建议集中在控制平面上的原因-有时是明确的,有时不是。

一个麻烦的故事


2012年7月,北弗吉尼亚州发生了一场强烈风暴。数据中心具有保护装置,柴油发电机等,但是碰巧,在北弗吉尼亚州其中一个可用区(亚利桑那州可用区)的一个数据中心中,断电了。电力很快恢复了,但是服务的恢复却持续了数小时。



我将通过一种基本服务示例-CLB(经典负载均衡器)向您介绍原因。它的工作原理很简单:在每个可用区中启动新的平衡器时,将创建单独的实例,其IP将解析DNS。



当实例之一发生故障时,有关此消息将发送到特殊数据库。



作为响应,该过程开始:从DNS删除记录,启动新实例,并将新IP添加到DNS。

注意:这是过去系统的工作方式,现在一切都大不相同。

一切都很简单-没有什么可以打破的。但是,在大规模故障期间,当数千个实例同时崩溃时,数据库中会出现大量待处理消息,积压下来的待办事项列表



但情况变得更糟。控制平面是一个分布式系统。由于存在错误,我们收到了重复的记录,数据库中的数千条记录激增到数十万条。使用它变得非常困难。

当其中一个实例发生故障时,所有流量几乎都会立即切换到幸存的计算机,并且负载翻倍(在此示例中,为简单起见,只有两个访问区域)。



资源不足,活动实例会自动开始扩展。该过程花费相对较长的时间。一切都在高峰期进行,同时有大量实例-可用区的可用资源即将用完。资源争夺战开始了。

在北维吉尼亚州,自动化无法应对大规模故障,工程师手动(使用脚本)将服务恢复了正常工作。这种麻烦很少见。在汇报过程中,出现了有关故障原因的疑问,他们决定不再重复这种情况,应更改整个服务。

我将介绍的八个模式是对某些问题的解答。

注意。这是我们在服务设计方面的经验,而不是广泛使用的普遍智慧。模式用于特定情况。

— . AWS . — , . . — . !


为了使故障的影响最小化,有很多方法。其中之一是回答以下问题:“我该如何确保不知道问题的用户在故障期间和恢复期间对它一无所知?”

我们庞大的Backlog不仅会收到崩溃消息,还会收到其他消息,例如有关扩展或某人正在启动新平衡器的消息。此类消息需要彼此隔离,在功能上进行分组:发生故障后,将恢复消息分为一组,分别启动新的均衡器。

假设有十个用户注意到一个问题-平衡器的一个节点掉了。服务以某种方式在剩余的资源上工作,但是感觉到了问题。



我们有十个沮丧的用户。第十一位出现-他对这个问题一无所知,只是想要一个新的平衡器。如果他请求放下队列进行处理,那么他很可能不会等待。其他处理过程完成后,请求时间将结束。而不是十个沮丧的用户,我们将拥有十一个。

为了防止这种情况的发生,我们对一些请求进行了优先排序 -例如,将队列放在顶部,例如对新资源的请求。如果发生大规模故障,则此类请求的数量相对较少,不会影响其他客户资源的恢复时间。但是在恢复过程中,我们将限制涉及此问题的用户数量。

全职工作


对问题报告的响应是启动恢复过程,尤其是与DNS一起使用。质量故障是控制平面上的巨大峰值负载。在这种情况下,第二种模式有助于控制平面更加稳定和可预测



我们使用一种称为“ 恒定工作-永久工作”的方法



例如,可以使DNS更加智能:它将不断检查平衡器的实例,无论它们是否存在。结果每次都会是一个位图:实例响应-1,已死-0。DNS

每隔几秒钟检查一次实例,而不管系统是否在发生整体故障后恢复或正常运行。他做同样的工作-没有高峰,一切都是可预测的和稳定的。

另一个简化的示例:我们想更改大型机队的配置。用我们的术语来说,车队是一组虚拟机,它们共同完成一些工作。



我们将配置更改放入S3存储桶中,然后每10秒(例如)将所有这些配置推送到我们的虚拟机机群中。两个要点。

  • 我们会定期这样做,永远不会违反规则。如果选择10秒的时间段,则无论情况如何,都只能以这种方式推动。
  • 我们总是给出整个配置,无论它是否已更改。数据平面(虚拟机)本身决定如何处理它。我们不推三角洲。随着大规模的中断或变更,它可能变得非常大。潜在地,这可能会导致不稳定和不可预测性。

当我们执行某种永久性工作时,我们会为此付出更多。例如,每秒有100个虚拟机请求配置。每年费用约为1200美元。这个数额基本上少于程序员的薪水,我们可以委托程序员用经典的方法来开发控制平面-对故障的反应以及仅配置更改的分配。

如果像示例中那样每隔几秒钟更改一次配置,则速度很慢。但是在许多情况下,更改配置或启动服务需要几分钟-几秒钟无法解决任何问题。

对于必须立即更改配置的服务(例如更改VPC设置),秒是非常重要的。在此“永久性工作”不适用。这只是一种模式,而不是规则。如果这在您的情况下不起作用,请不要使用。

初步缩放


在我们的示例中,当平衡器实例崩溃时,第二个尚存的实例几乎立即收到加倍的负载并开始扩展。如果发生大规模故障,它将消耗大量资源。第三种模式有助于控制此过程- 预先缩放

在两个可用区的情况下,我们在处理少于50%时进行缩放。



如果一切都提前完成,那么在发生故障的情况下,尚存的平衡器实例将准备好接受两倍的流量。

以前,我们仅以高利用率(例如80%)进行扩展,而现在只有45%。该系统大部分时间处于空闲状态,并且变得更加昂贵。但是我们准备好忍受这一点并积极使用该模式,因为这是保险您必须支付保险费,但是在遇到严重麻烦的情况下,胜利将支付所有费用。如果决定使用该模式,请提前计算所有风险和价格。

蜂窝架构


有两种构建和扩展服务的方式:整体蜂窝结构(基于单元)。



整体物作为单个大容器发展并生长。我们添加资源,系统膨胀,遇到不同的限制,线性特征变得非线性并变得饱和,并且系统的“爆炸半径”-整个系统。

如果整料堆未经良好测试,就会增加发生意外的可能性-“未知未知数”。但是大型整体无法完全测试。有时,为此,您将必须构建一个单独的访问区域,例如,对于在访问区域(这是很多数据中心)中作为整体构建的流行服务。除了以某种方式创建类似于当前的巨大测试负载之外,从财务角度来看,这是不可能的。

因此,在大多数情况下,我们使用蜂窝体系结构 -一种配置,其中,系统是根据固定大小的单元构建的。通过添加单元格,我们对其进行缩放。

蜂窝架构在AWS云中很流行。它有助于隔离毛刺并减小爆炸半径。一个或多个单元格。我们可以完全测试中型电池,这可以大大降低风险。

在造船中使用了类似的方法:将船舶的壳体通过隔板分成多个隔间。万一发生孔洞,一个或多个舱室被淹没,但船不沉没。是的,它没有帮助泰坦尼克号,但我们很少遇到冰山问题。

我将以简单形状服务为例来说明网格方法的应用。这不是AWS服务,我自己提出了。这是一组用于处理简单几何形状的简单API。您可以创建一个几何形状的实例,通过其ID请求形状的类型,或计算给定类型的所有实例。例如,在request上put(triangle)创建一个具有某些ID的“三角形”对象。getShape(id)返回类型“三角形”,“圆形”或“菱形”。



为了使服务变得多云,必须由不同的用户同时使用它。让我们使其成为多租户。



接下来,您需要提出一种分区方法-将图形分隔为单元格。有几种选择分区键的选项。最简单的是几何形状:第一个单元格中的所有菱形,第二个单元格中的圆形,第三个单元格中的三角形。

这种方法有利有弊。

  • 如果圈数明显少于其他数字,则相应的单元格将未被充分利用(分布不均)。
  • 一些API请求很容易实现。例如,计算第二个单元格中的所有对象,我们可以找到系统中的圆圈数。
  • 其他查询则更为复杂。例如,要通过id查找几何形状,必须遍历所有单元格。

第二种方法是按范围使用对象id:第一个单元格中的前1000个对象,第二个单元格中的第二个。因此,分配更加均匀,但是还有其他困难。例如,要计算所有三角形,您需要使用以下方法scatter/gather:我们在每个单元格中分配请求,它计算自身内部的三角形,然后收集答案,汇总并产生结果。

第三种方式- 租户划分(对用户)。在这里,我们面临一个经典的问题。云中通常有许多“小型”用户尝试某些操作,但实际上并未加载该服务。有乳齿象使用者。它们很少,但是它们消耗大量资源。这样的用户永远不会适合任何一个单元。您必须想办法将它们分布在许多单元中。



没有理想的方法,每种服务都是单独的。好消息是,世俗的智慧在这里起作用-沿着纤维切碎柴火比将纤维切碎更方便。在许多服务中,这些“纤维”或多或少是显而易见的。然后,您可以进行实验并找到最佳的分区键。

单元相互连接(尽管很弱)。因此,必须有一个连接级别。通常将其称为路由或映射层。需要了解向哪个单元发送特定请求。此级别应尽可能简单。尽量不要在其中放置业务逻辑。



问题出在细胞的大小上:小-坏,大-也坏。没有通用的建议-根据情况决定。

在AWS云中,我们使用不同大小的逻辑和物理单元。有一些具有较大像元大小的区域服务,也有一些区域较小的区域服务。



注意。我在今年四月初的Saint Highload ++ Online上谈论了微单元。在这里,我详细讨论了在我们的Amazon EBS核心服务中特定使用此模式的示例。

多租户


用户启动新的平衡器时,他会在每个可用性区域中接收实例。无论是否使用资源,资源都被分配并完全属于云的此租户。

对于AWS,此方法效率不高,因为服务资源的利用率平均非常低。这会影响成本。对于云用户,这不是灵活的解决方案。它不能适应快速变化的条件,例如,无法在最短的时间内为资源提供意外增加的负载。



CLB是亚马逊云中的第一个平衡器。今天的服务使用多租户方法,例如NLB(网络负载平衡器)。这种网络服务的基础是HyperPlane这是最终用户看不到的内部庞大虚拟机(节点)群。



所述的优点多租户方法或第五图案。

  • 容错能力从根本上更高在HyperPlane中,大量节点已经在运行,并且正在等待负载。节点彼此知道状态-当某些资源发生故障时,负载会立即分配给其余节点。用户甚至没有注意到大规模的崩溃。
  • 峰值负载保护租户过着自己的生活,他们的负担通常彼此不相关。HyperPlane的总平均负载非常平稳。
  • 此类服务的使用从根本上来说更好因此,提供更好的性能,它们更便宜。

听起来不错!但是多租户方法有缺点。在图中,HyperPlane机队具有三个租户(菱形,圆形和三角形),这些租户分布在所有节点上



这引发了经典的“嘈杂邻居”问题:租户的破坏性行为会产生超高流量或不良流量,将潜在地影响所有用户。



在这样的系统中,“爆炸半径”是所有租户。实际的AWS可用性区域中发生破坏性“嘈杂邻居”的可能性不高。但是我们必须始终为最坏的情况做好准备。我们使用网格方法为自己辩护-我们选择节点组作为单元。在这种情况下,我们称它们为碎片。单元,碎片,分区-在这里是相同的。



在此示例中,菱形作为“嘈杂的邻居”,将仅影响一个租户-一个三角形。但是三角形会很痛苦。要平滑效果,请应用第六种模式-混合分片。

混洗分片


我们将租户随机分配到节点。例如,菱形降落在1、3和6个节点上,三角形降落在2、6和8个节点上。我们有8个节点和大小为3的碎片。



在这里,简单的组合运算起作用。租户之间只有一个交叉点,概率为54%。



“吵闹的邻居”只会影响一个租户,不会影响整个负载,而只会影响30%。

考虑一个接近实际的配置-100个节点,分片大小为5。概率为77%,根本没有交集。



随机分片可以显着减小“爆炸半径”。许多AWS服务都使用此方法。

“小型机队会导致大型机队,反之亦然”


从整体故障中恢复时,我们将更新许多组件的配置。在这种情况下,一个典型的问题是将更改的配置推入项目符号或以项目符号显示?谁是更改的发起者:包含配置更改的源或其使用者?但是这些问题是错误的。正确的问题是哪个机队更大?

考虑一个简单的情况:大量的前端虚拟机和一定数量的后端。



我们使用网格方法-前端实例组将与某些后端一起使用。为此,请确定路由-后端和与其一起工作的前端的映射。



静态路由不合适。当您需要快速更改大多数路由时,哈希算法在大规模故障中效果不佳。因此最好使用动态路由。在庞大的前端和后端实例机群旁边,我们放置了一个仅处理路由的小型服务。他将在任何给定时间知道并分配后端和前端映射。



假设我们发生了严重的崩溃,许多前端实例崩溃了。他们开始大量恢复,几乎同时从路由服务请求映射配置。



小型路由服务受到大量请求的轰炸。他将无法应付负荷,在最佳情况下,负荷将降低,在最坏的情况下,他将丧命。

因此,不要求从小型服务请求配置更改而是建立系统以使“婴儿”本身正确是正确的启动配置更改为大量实例



我们使用不断工作的模式。小型路由服务将每隔几秒钟将配置发送到所有前端车队实例。他将永远无法负担出色的服务。第七种模式有助于提高稳定性和弹性

前七个模式增强了系统。后一种模式的工作原理有所不同。

掉落负荷


下面是延迟与负载的经典曲线图。图的右侧是“膝盖”,当负载非常高时,即使很小的增加也会导致延迟的显着增加。


在正常模式下,我们永远不会把我们的服务放在进度表的右边。一种简单的控制方法是按时添加资源。但是我们正在为任何麻烦做准备。例如,我们可以移至图表的右侧,以从质量故障中恢复。

我们将客户端超时记录在图表上。任何人都可以是客户,例如,我们服务中的另一个组件。为简单起见,我们绘制了一个50%的延迟图。



在这里,我们面临着一种叫做电力不足的局面当城市停电时,您可能对停电一词很熟悉。掉电是指某事有效,但又糟又慢,以至于它根本不起作用。

让我们看一下棕色区域的电压不足。该服务从客户端收到请求,对其进行处理并返回结果。但是,在一半的情况下,客户端已经超时,没有人在等待结果。在另一半中,结果返回的时间比超时快,但是在速度较慢的系统中,它会花费太多时间。

我们面临一个双重问题:我们已经超负荷工作,处于日程安排的右边,但与此同时,我们仍在“警告外界”,做了很多无用的工作。如何避免这种情况?

找到“膝盖” -图表上的拐点我们测量或理论上估计。

下降的交通迫使我们走到拐点的右边



我们应该简单地忽略部分请求。我们甚至不尝试处理它们,而是立即将错误返回给客户端。即使过载,我们也可以负担得起-这是“便宜”的。请求未得到满足,服务的整体可用性降低。尽管拒绝的请求将在客户端重试一次或几次后早晚处理。



同时,以保证的低延迟处理请求的另一部分。结果,我们不会做无用的工作,而我们所做的就是做得很好。

简要介绍故障设计系统的模式


隔离和调节。有时优先处理某些类型的查询是有意义的。例如,在创建新资源的请求量相对较小的情况下,可以将它们放在队列的顶部。请勿侵犯其他用户,这一点很重要。在大规模中断中,等待资源恢复的用户不会感觉到明显的差异。

全职工作。减少或完全消除服务模式的切换。无论紧急情况或工作情况如何,稳定且持续工作的一种模式从根本上提高了控制平面的稳定性和可预测性。

初步缩放。以较低的处置价值提前扩大规模。您将不得不为此付出更多,但这是在严重的系统故障期间能得到回报的保险。

蜂窝架构。与整体结构相比,许多松散耦合的单元更可取。网格方法减少了“爆炸半径”和意外错误的可能性。

多租户方法显着提高了服务的利用率,降低了服务成本并减小了“爆炸半径”。

改组分片。这是一种适用于多租户服务的方法。此外,它还允许您控制“爆炸半径”。

“小型机队会导致大型机队,反之亦然”。我们尝试构建服务,以便小型服务启动对大型配置的更改。我们经常将其与恒定负载模式结合使用。

减轻负荷。在紧急情况下,我们尝试只做有用的工作,并且要做好。为此,我们丢弃了仍然无法处理的部分负载。

— , Saint HighLoad++ Online. -- , Q&A-, , . - . , - .

telegram- @HighLoadChannel — , .

All Articles