你可能听说过英国《卫报》。有可能从诸如爱德华·斯诺登泄密事件或者2011年与罗伯特·默多克的国际新闻相关的电话窃听丑闻中听说过它。你所不知道的是它的在线和技术团队获得过数个奖项,被许多圈子视为英国最好的产品团队。
2008年的情况有些奇怪,《卫报》运营在一套企业级软件系统上,这套复杂的代码又分成数个服务,这些服务与一个单体数据库耦合在一起。系统运行了相当长的一段时间并且平安无事,但是随着互联网的增长,你永远都不知道未来会发生什么事情。如果我们推出一篇确实重磅的文章,流量就可能达到一天内以前峰值的一倍以上。问题是当有些部分出现问题时,所有的其他部分都开始出故障。这好像整个产品共享一个巨大的熔丝——当熔丝熔断时,整个系统都会因此失败。
“坦尼娅·科德里向其eBay的前同事们求助,作为增长咨询公司AKF Partners公司,他们现在帮助那些需要这方面指导的客户。在AKF的帮助下,我们开始重新设计网站,增加故障隔离区或泳道。基本上,相当于把熔丝盒中的单根熔丝变成了多根。在任何时候,如果网站的一部分出现问题,只有那个部分会出故障。这种故障隔离办法让软件和数据库子系统部署在独立的泳道里,每个泳道都完全独立于其他的泳道。网站的不同内容区域由不同的泳道提供服务。如果一个泳道失败,比如天气版面,我们可以继续提供时效性强的新闻。更近一步,我们可以在每个不同泳道的可用性方面投入不同的时间和精力。像新闻之类的泳道可以有比其他重要性低的(如天气)泳道显著不同的冗余解决方案。在过去的解决方案中,每个部分都有同样的可用性以及相同的成本;现在我们可以让新闻有更高的可用性而不必大幅度提高成本。
格兰特说道,“回想2008年,你会看到责任编辑总在关注网站是否能应对大流量新闻。而今,刚刚有1400万唯一的读者阅读了2015年11月恐怖分子袭击巴黎的报道。现在的责任编辑再也不顾虑在线系统,他们知道该系统有这个能力。”
“受到这种关注的并不仅仅是网站。当在互联网上使用一款产品时,要依靠一定数量的工具以及独特的基础设施。”格兰特总结说,“现在,我们把相同的概念应用到监控和代码发布等地方。每个关键业务都应该有自己的泳道,这样某个工具的故障就不会导致其他工具和监控的故障”。
对故障隔离和故障控制的需求并不局限于格兰特与《卫报》。根据我们的经验,大多数的技术团队都非常擅长,并且非常专注于交付能正常工作的系统。此外,大多数工程师都明白不可能研发出完美无缺的解决方案,因此,不可能构建永远不会故障的系统。即使考虑到了这一点,几乎没有工程师愿意花费大量时间来勾勒和圈定任何给定故障的“爆炸半径”。
在我们的业务中,可用性和可扩展性具有同等重要性。可用性不高的产品确实不需要扩展,当需求来临时不能扩展的网站也不会是高可用的。因为系统故障无法避免,所以我们不得不花时间来控制故障对系统的影响。本章提供了几种限制故障影响的方法,总之,减少故障的频率,提高产品的整体可用性。
规则1——用“泳道”隔离故障
内容:在设计中实现故障隔离区或泳道。
场景:为可扩展性开始拆分持久层(例如数据库)或服务时。
用法:沿Y或Z轴拆分持久层和服务,禁止故障隔离的服务和数据间同步通信或访问。
原因:提高可用性和可扩展性。减少发现和解决故障的时间。缩短上市时间和成本。
要点:故障隔离包括根除故障隔离域间的同步请求,限制异步调用和处理同步调用失败,以及避免泳道之间的服务和数据共享。
在拆分服务和数据方面的术语丰富而混乱,而且有时候相互矛盾。不同组织经常使用一些词,如豆荚(pod)、池(pool)、集群(cluster)和分片(shard)。在同一组织内这些术语经常交替使用加剧了这种困惑。在某个场景,团队可以使用分片来确定服务和数据的分组,而在另一场景下,它仅意味着在数据库中分割数据。鉴于现有术语的混淆和差异,在实践中我们创造了泳道一词,尝试打造故障隔离的重要概念。一些客户开始采用这个词来表示为实现故障隔离而在生产中按照服务或客户拆分,它最重要的贡献是在设计领域。表9-1是常用术语的列表,它包括对这些术语最普通的描述以及何时及如何在实践中交替使用的说明。
在我们看来,这些术语中最重要的区别是大多数都集中在分工或事务上,但只有一个聚焦在控制故障传播上。而池、分片、集群和豆荚指的是如何在生产环境中实施或如何拆分或扩展客户或者服务,游道是围绕设立故障隔离域提出的架构概念。故障隔离域是这样的一个域,当物理或逻辑服务因故障无法正常工作时,无论该故障是响应缓慢还是根本无法响应,唯一受到影响的是那些在故障域中的服务。泳道通过故障域把分片和豆荚的概念拓展到了服务的前门——数据中心的入口。在极端情况下,它意味着按照功能提供独立的网络、应用和数据库服务器(或叫故障隔离区)。在本质上,泳道旨在提高可扩展性和可用性,而不仅仅是一个可以扩展事务处理的机制。
我们借用CSMA/CD(带有冲突检测的载波侦听多路存取,通常称为以太网)中的概念,其中故障隔离域相当于冲突域。在全双工交换机之前,为了抵消碰撞的影响,以太网段控制碰撞以确保所有连接的系统感觉不到影响。我们觉得用“泳道”这个术语来描述故障隔离是一个非常好的比喻,泳道是在游泳池中设置的隔离线,有助于游泳选手在游泳过程中避免相互干扰。与此类似,不同组别的客户或者功能之间存在隔离带,不允许跨越不同组别进行同步事务处理,有助于确保一条泳道上的故障不会对其他泳道上的用户操作产生不利的影响。
故障隔离泳道的好处已经超越了通过故障隔离提高可用性的想法。因为泳道把客户或者把跨客户共享的功能做了拆分,当出现故障时,可以更快地定位问题来源。从网络服务器到持久层,如果已经对客户沿Z轴做了拆分,影响单个客户的唯一故障将会很快被隔离在那条泳道里的那组客户。你会对要找的缺陷或问题(由数据或操作触发)了然于胸,因为对于游道中的客户它是唯一的。如果已经沿Y轴进行了拆分,而“购物车”泳道出现了问题,你马上就会知道问题是与构成该泳道的代码、数据库或者服务器相关。事件检测与解决以及问题的定位和解决都明显受益于故障隔离。
故障隔离的其他好处还包括更好的可扩展性、更快的上市时间以及更低的成本。因为我们专注于系统分区,开始考虑水平扩展,所以可扩展性提高。如果通过Y轴隔离泳道,那么我们可以把代码库也进行拆分,这样可以更有效地使用工程师。因此,我们获得到了更大的工程吞吐量,每个单位开发的成本也更低。如果吞吐量越来越大,显然产品推向市场的速度加快。最终所有这些好处使我们能够处理“预期但意外之外的事情”:那些知道迟早会发生但不清楚其后果的事情。换句话说,我们知道事情将要发生,只是不知道会发生什么或何时会发生。故障隔离使我们能够更优雅地处理这些故障。
讨论了为什么应该为产品建立泳道或设置故障隔离,现在我们把注意力转向更重要的问题,如何实现故障隔离。依靠四条原则来定义和帮助我们设计泳道。第一个原则是泳道之间什么都不共享。通常不包括网络组件,如网络入口的边界路由器和一些核心路由器,但包括为故障隔离服务专用的交换机。经常共享一些其他的设备,诸如非常大的存储区域网络或者小网站中的负载均衡器。在任何可能的场合,而且在特定成本范围内,试着尽量不要共享。永远不要共享数据库和服务器。因为泳道的部分定义不共享,所以服务器和数据库共享一直是确定泳道边界所在的起始点。鉴于网络设备和存储子系统的成本问题,有时在系统增长的初期可以考虑跨越不同的泳道。
泳道的第二个原则是在泳道之间不进行同步调用。因为同步调用会捆绑服务,所以被调用服务的失败会蔓延到所有其他以同步和阻塞方式调用的系统。因此,这会违反故障隔离的概念,如果部署在一条泳道里的服务失败可能会导致部署在另一条泳道里的服务失败。
第三条原则限制泳道之间的同步调用。比起同步调用,异步调用失败蔓延到其他泳道的机会很小,但仍然有机会降低系统可用性。突然激增的请求可能使某些系统变慢,例如拒绝服务攻击后发布消息。这些消息铺天盖地阻塞队列,开始占满TCP端口,如果实施不当甚至导致同步请求的数据库处理停滞。因此,我们试图限制跨越泳道界限的事务处理数量。
泳道的最后一个原则是,当绝对必要时如何实现跨越泳道边界的异步传输。简单地说,每次要跨越泳道进行异步通信时,我们需要对事务处理有“不在乎”的能力。在某些情况下,事务处理可能会超时,可以忽略它。我们可能只是“通知”另一条泳道有些行动,并不在乎是否得到回应。在所有情况下,我们应该实现逻辑以实现在手动自动或两者兼有的基础上“断开”或“关闭”通信。监控人员通过系统监控发现故障(手动开关)应该有能力关闭通信,当情况不好时,系统应该能感知并停止(自动开关)通信。
回想一下本书的前言,还记得里克·达尔泽尔提到过亚马逊吗?亚马逊的第一次拆分是把商店功能从订单履行功能中分离出来。如果由于某种原因,亚马逊店面失败,亚马逊可以继续履行已收到的订单。如果订单履行系统出问题,商店可以继续接收订单并把它们放入队列。显然,两个子系统需要互相通信,但从客户角度来看不是“同步”。工作以履行订单的形式从前端传到后端,以订单状态更新的形式从后端传向前端。这种拆分提供了两个好处,故障隔离和更高水平的可扩展性,因为每个子系统都可以独立扩展。
在我们希望故障隔离,但需要同步通信或访问另一个数据源的情况下怎么办?前一种情况下,可以复制需要的服务,然后把它配置在泳道里。支付网关就是这种方法的一个例子。如果我们沿着Z轴按客户划分泳道,可能不希望每个客户的泳道为了某个服务(如结账)而同步(阻塞)调用单一的支付网关。我们可以简单地实施N个支付网关,其中N是客户细分度或客户游道的数量。
如果有一些共享信息需要访问每个泳道,如登录凭据,应该怎么办?也许我们已经把认证和登录沿Y轴做了拆分,但我们需要在只读基础上从每个客户(Z轴)的泳道上取得相关的凭据。我们经常使用数据库的只读副本来满足这样的要求,把只读副本放在每个泳道中。许多数据库天然提供这种复制技术,甚至允许把数据切成小片,这意味着我们不需要在每个泳道中复制100%的客户数据。有些客户为了只读目的,把相关的信息缓存在相应泳道的分布式对象缓存中。
我们经常遇到的一个问题是如何在虚拟化服务器世界中实施泳道。虚拟化为故障隔离增加了新的维度——除了物理故障之外的逻辑(或虚拟)维度。如果实现虚拟化主要是为了把较大的机器分解成更小的机器,那么应该继续把物理服务器作为泳道的边界。换句话说,不要把来自于不同泳道的虚拟服务器放在同一物理设备上。然而,我们的一些客户常年有各种不同需求特性的产品,他们依靠虚拟化作为横跨所有产品的弹性容量。在这种情况下,我们试图限制混合在虚拟服务器上的泳道数量。理想情况下,把整个物理服务器用在一个游道上,而不是在该服务器上混合几个泳道。
泳道与虚拟化
当使用虚拟化技术将较大的服务器分割成较小服务器时,尝试沿物理服务器边界保持泳道。在同个物理服务器上混合不同泳道的虚拟服务器抵消了故障隔离泳道的许多好处。
规则2——拒绝单点故障
内容:永远不要实施会带有单点故障的设计,一直要消除单点故障。
场景:在架构审查和新系统设计时。
用法:在架构图上寻找单个实例。尽最大可能配制成主动/主动模式。
原因:通过多实例配置最大化可用性。
要点:努力实施主动/主动而非主动/被动配置。使用负载均衡器在服务的不同实例之间实现流量平衡。对需要单例的情形,可以在主动/被动模式的实例中采用控制服务。
在数学中,单元素集合是只有一个元素{A}的集合。按照编程的说法,单例模式是模拟数学概念的设计模式,把类的实例化限制在只有一个对象。这种设计模式对于资源协调很有用,但经常被研发人员出于便捷的目的而过度使用。在系统架构中,单例模式(或更恰当地说,反模式的单件情况)称为单点故障(SPOF)。这是指系统中仅有一个实例,当它失败时将导致系统范围的事故。
SPOF可以存在于系统的任何地方,包括单个网络服务器或单个网络设备,但最常见的是数据库系统中。原因是数据库往往最难跨越多个节点扩展,因此成为单例。在图9-1中,即使有冗余的登录、搜索和结账服务器,数据库也是SPOF。更糟糕的是,所有的服务池都依赖于那个单个数据库。虽然SPOF不好,但是数据库作为SPOF问题更大,因为如果数据库减慢或崩溃,所有同步调用该数据库的服务池都会遇到问题。
我们有个与客户分享的口头禅:“一切皆可能出故障。”这包括服务器、存储系统、网络设备和数据中心。凡能说出来的,都可能出故障,而且可能我们已经看到了这些故障。虽然大多数人认为数据中心永远不会出故障,但这些年我们亲身经历了十多次数据中心的服务中断。这同样适用于高可用的存储区域网络。尽管它明显比旧的SCSI磁盘阵列更可靠,但它仍然会出故障。
大多数SPOF的解决方案是直接部署一个硬件,通过复制X轴刻度所描述的服务确保每个服务至少运行在两个或者多个实例上。但是,这并不总是那么容易。让我们追溯编程步骤的单例模式。虽然不是所有的单例类都会阻止服务在多个服务器上运行,但是有一些实施绝对会避免可怕的后果。举个简化的例子,如果在处理从用户账户扣减资金的代码中有个类,可能会对此实施一个单例,以防止像用户账户余额为负数这样不愉快的事情发生。如果我们将此代码放在两个独立的服务器上,而不实施额外的控制或设置信号量,两个并发的事务有可能会都从用户账户上扣款,从而导致错误或不希望的情况发生。因此,要么修复代码来处理这种情况,要么依靠外部的控制来防止。最理想的解决方案是修复代码,以便在许多不同主机上实施服务,通常我们需要迅速修复代码以解除SPOF。作为本规则最后的重点,我们下一步将讨论一些快速修复的方法。
第一个和最简单的解决方案是采用主动/被动配置。将服务部署在主动服务器上运行,同时也部署在不处理流量的被动服务器上。热/冷配置通常用在数据库上作为去除SPOF的第一步。下一个选择是使用系统中的另一个组件来控制数据访问。如果数据库是SPOF,可以配置成主/从模式,应用可以控制数据访问,由主数据库完成写入/更新,由从数据库完成阅读/选择。除了消除SPOF,引入具有高读写比的只读数据库副本将减少主数据库的负载,并可以利用更经济实用的硬件,如第3章中规则11所讨论的那样。可以解决SPOF问题的最后一种配置是采用负载均衡器。如果网络或应用服务器上的服务是SPOF而且无法在代码中解决,通常可以采用负载均衡器,来解决用户请求只能由服务池中一台服务器来服务的问题。这可以通过设置在用户浏览器中的会话cookie来完成,利用负载均衡器把用户的每次请求重定向到相同的网络或应用服务器上,从而确保状态的一致性。
我们讨论了当无法及时通过修改代码解决SPOF时,几种可以快速实施的解决方案。尽管最佳而且最终的解决方案应该是修复代码,以允许服务的多个实例运行在不同的物理服务器上,但是首先是要尽早消除SPOF。记住,“一切皆可以出故障”,所以当修复SPOF的方案失败时,不要感到惊讶。
规则3——避免系统串联
内容:减少以串联方式连接的组件数量。
场景:每次考虑添加组件的时候。
用法:删除不必要的组件、收起组件或添加多个并行组件以减少影响。
原因:串联组件受多重失败乘法效应的影响。
要点:避免向串联系统添加组件。如果有必要这样做,添加多个版本的组件,如果一个出故障,其他组件可以取代它的位置。
电路中的元件有多种连接方式。两种最简单的连接方式是串联和并联。串联电路的元件(可能是电容、电阻或其他元件)沿电路连接。在这种类型的电路中,电流流过每个元件,电阻和电压是在这个基础上产生的。图9-2显示了两个电路,一个有三个电阻,一个有三节电池,由此产生电阻和电压。请注意,在此图中,如果有任何元件出故障,如电阻烧掉,就会造成整个电路出故障。
图9-3显示了两个并联电路,上面的有三个电阻(和一个电源或电容),下面的有三节电池。在这个电路中,总电阻的倒数等于每个电阻的倒数之和。定义的总电阻必须小于最小电阻。请注意,电压不改变,但电池只贡献了一小部分的电流,这有延长其使用寿命的效果。请注意,在这些电路中,元件的故障不会导致故障整个电路出故障。
系统架构和电路在许多方面有相似之处。像电路一样,系统也由不同组件组成,如Web和应用服务器、负载均衡器、数据库和网络设备,而且也可以并联或串联。让我们以一个有大流量的静态网站为例。你可能把相同的静态内容配置在10个Web服务器上以提供网站服务。要么使用负载均衡器引导流量,要么利用DNS通过为相关域名指定10个独立的IP地址。这些Web服务器像图9-3中的电池一样是并联的。Web服务器所处理的流量是总量的一小部分,如果一个Web服务器失败,该网站仍然可用,因为还有其他9个Web服务器。
作为一个更典型的串联架构例子,让我们添加一些层。如果以一个包括一个网络服务器、一个应用服务器和一个数据库服务器的标准的三层网站为例,我们会有一个串联的架构。要满足请求,Web服务器必须先接受请求,然后将其传递给应用服务器(它查询数据库)。应用服务器接收并处理数据后,将其发送回Web服务器,最终满足客户的请求。如果电路或架构中的任何组件发生故障,整个系统将出故障。
回到现实世界的架构。几乎总是有些组件需要串联。当考虑到负载均衡、Web和应用层、数据库、存储系统等时,为了保持系统运行需要许多组件。当然,添加并联组件,即使层之间串联,有助于降低由组件故障引起系统故障的总风险。如果只有一个Web服务器出故障,多台Web服务器可以分散流量负载并避免系统故障。对于网络与应用层,大多数人很容易接受这个概念。数据库和网络层的这个问题却被大多数人忽视。如果并联的Web和应用服务器都串联到单个数据库,就可能会有一个可以导致灾难性故障的组件。
关于网络组件,我们经常看到架构对并联服务器非常关注,但完全忽略网络设备,尤其是防火墙。流量通过防火墙、负载均衡器、防火墙、交换机,然后到Web服务器、应用服务器、数据库服务器,然后再一路返回。这个过程至少有7个串联的组件。如果已经有6个组件了,那么再增加一个有什么大不了的?
串联组件出故障的风险具有乘法效应。举个简单例子,如果我们有两个串联的服务器,各有99.9%的可用性或正常运行时间,那么该系统的总可用性不能大于99.9%×99.9%=99.8%。如果在串联中增加可用性为99.9%的第三个组件,我们就得到一个更低的总可用性99.9%×99.9%×99.9%=99.7%。放置的串联组件越多,系统的总可用性就越低。表9-4列出了一些简单的计算,来说明可用性降低,那么每月由此产生的停机时间增加。对串联的系统,每增加一个组件(可用性99.9%),每月停机时间就增加大约43分钟。然而,对并联系统,每增加一对组件(可用性99.9%),每月停机时间就减少大约26分钟。假如并联的每个组件有更低的可用性,这种改善效果甚至更加显著。
就像今天的大多数电路一样,系统也远比简单的串联和并联更加复杂,对可用性的精确计算要比简单的例子复杂得多。然而,可以明确的是,串联组件显著增加了系统停机的风险。当然,可以通过减少串联组件或增加并联组件来降低风险。
规则4——启用与禁用功能
内容:搭建一个框架来启用与禁用产品的功能。
场景:考虑使用上线和下线框架控制新研发的、非关键性的或者依赖第三方的功能。
用法:研发共享库以自动或基于请求的方式控制功能的启用与禁用,参见表9-5中的推荐。
原因:为了保护对最终用户很重要的关键功能,关闭有问题或非关键性的功能。
要点:当实施成本低于风险损失时,实现上线和下线框架。开发可以复用的共享库以降低未来实施的成本。
在讨论故障隔离设计方法时也提过它。最终这些类型的框架有助于确保系统可以优雅地出故障(在事件自诊断框架下)或在通过人为干预禁用某些功能的基础上继续提供服务。有时公司将类似的功能称为“功能切换”或“断路器”。
过去有几种方法可以控制功能的上线和下线,每种方法都有一定的优点与缺点。启用和禁用服务很可能取决于技术团队和运营团队的能力,以及出现问题的服务的业务关键性。表9-5涵盖了一些方法。
表9-5并不是启用和禁用功能的所有可能性的完整清单。事实上,许多公司融合了一些选项。他们可以在启动时从数据库读取参数或者文件,以控制应用代码显示或不显示某组功能。PayPal在第一次实现国际化时所实施的就是这样的一个例子。有些国家的银行或资金转移规定只允许一些有限的支付功能。根据用户使用网站的地理位置,他可能只看到主网站提供的功能中的一部分。
当考虑功能的上线/下线框架时,要解决的同等重要的问题是,在哪里和什么时候应该使用决策。显然,实施框架意味着额外的工作以及由此带来的额外业务成本。让我们以(不太可能而且可能不正确)某些永远都不会出故障的功能为起点。如果知道哪些功能永远不会出故障,我们将不想为这些功能实现此控制功能,因为这是没有回报的投入。以此为起点,我们就可以确定投资在哪里具有价值或能带来业务回报。使用率高(高吞吐量)并且其故障会影响网站上其他重要功能的任何功能是合适的选择。另一个选择是在给定版本中正在经历大幅修改的那些功能。选择这两类功能的想法是,实施上线/下线的成本小于给业务带来的风险(风险是失败概率和失败影响的函数)。如果开发这些功能花费额外1000美元的成本,该功能无法处理的故障可能造成1万美元的业务损失,这个成本投入划算吗?
如果处理得当,技术团队可以通过实现一组跨功能的共享库,降低实施上线/下线框架的成本。对新的开发,这种方法不会把实施框架的成本降低为零,但它确实有助于降低未来几代框架启用功能的成本。
我们建议实施上线/下线框架有几个重要的原因。首先,新功能或正在积极开发的功能很有可能有缺陷。有能力关闭有问题的功能非常有价值。其次,如果功能对所提供的服务不重要,有可能想要关闭非关键功能。当计算资源成为瓶颈时,也许存在内存泄漏,把应用送入垃圾回收过程,关闭非关键功能以保护更多关键功能是个很好的选择。第三,调用第三方服务经常要以同步方式进行。当供应商的API开始响应缓慢时,能够关闭功能可以防止它减缓整个应用或服务,是非常可取的。显然,我们不相信一切都应该能够启用/禁用或上线/下线。这种做法成本昂贵而且不建议的,但运转良好的团队应该能够发现风险,为实施合适的保障措施共享组件
总结
我们相信可用性和可扩展性紧密相关。可用性不高的产品不需要扩展,因为用户过不了多久就不来了。无法扩展的网站不会有高可用性,因为网站会变慢,甚至完全停下来。因此不能顾此失彼。本章提供了四个规则,它们有助于确保网站保持高可用性以及持续扩展。不要因为专注于可扩展性而使你忘记可用性对客户多么重要。
本文选自架构真经:互联网技术架构的设计原则(原书第2版)一书,由马丁·阿伯特、迈克尔·费舍尔著,陈斌翻译。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31562044/viewspace-2648269/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/31562044/viewspace-2648269/