分布式世界中的软件设计

本章是关于使用分布式计算技术来进行服务设计的概述。所有大型网站用这些技术来保证其大小、规模、速度和可靠性。

  构建一个软件设计的方式有两种:一种方式是让它变得非常简单, 即明显没有缺陷, 另一种方式是让它变得非常复杂, 即没有明显的缺陷。

        -C.A.R.霍尔, 1980 ACM 图灵奖讲座

谷歌搜索是如何工作的?你的 Facebook 日程表如何在24小时的时间里更新的?亚马逊如何扫描日益增长的商品目录,并告诉你购买这个商品的人也买了袜子?

这是魔术吗?不,这是分布式计算。

分布式计算是构建大型系统的艺术,它将工作分配到许多机器上。与传统的计算机系统只有一台计算机运行提供服务的软件或者客户端 - 服务器计算机可以远程访问的集中式服务相比,分布式计算中通常有数百或数千台机器一起工作来提供一个大型服务。

分布式计算在许多方面与传统计算不同。这些差异大多是由于系统本身庞大的规模所造成的。在这个系统中,可能会涉及到成百上千台计算机,服务于数百万用户,处理数十亿甚至数万亿次的查询。

概念需知

服务器:提供一个功能或者应用程序接口的软件(不包括硬件)

服务:一个由许多服务器组成的用户可见的系统或产品。

机器:一个虚拟或物理机器。

每秒查询率:每秒的查询数,通常用每秒接收的网页点击数或者API调用来表示。

流量:查询数,API调用或发送到服务器的其他请求的通用术语。

高性能:其性能符合 (达到或超过) 设计要求的系统。是融合了“性能”和“符合要求”的新词汇。

应用程序接口(API):管理一个服务器与另一个服务器通信的协议。

速度很重要。对于一个服务来说,快速响应是一个巨大的竞争优势,如果一个网站在200毫秒或者更少的时间没有响应,那么用户就会认为这个网站缓慢。网络延迟会消耗大部分时间,从而没有多少时间为服务去组装页面。

在分布式系统中,出现故障是正常的。当机器规模变得很大时,罕见的硬件故障会变得很常见。因此假设故障的存在,围绕这项假设进行设计工作,并用软件进行预测。故障就成了工作中可以预测的一部分。

分布式系统的庞大规模决定了操作必须自动化。手动执行涉及数百或数千机器的任务是不可想象的,因此自动化对于准备和部署软件,定期操作和处理故障变得至关重要。

10.1 规模的可见性

要管理大型分布式系统,必须具有对系统的可见性。我们把检查内部状态的能力称为内省,这种能力是操作、调试、优化和修复大型系统所必需的。

在传统系统中,我们可以想象一个对系统足够了解的工程师能够关注所有的关键组件,或者根据经验“知道”什么是错的。而在大型系统中,可见级别必须由设计系统来主动创建,该系统是可提取信息和可见的。没有人或者团队可以手动保留所有部件的标签。

因此,分布式系统需要组件生成丰富的日志, 来详细说明系统中发生的情况。然后将这些日志聚合到一个集中位置进行收集、存储和分析。系统可能会记录一些级别非常高的信息,例如每当用户进行购买时的每个网络查询或接口调用。系统也可能会记录一些低级别信息,例如关键代码段中每个函数调用的参数。

系统应该导出度量标准。它们应该计算有趣的事件, 例如特定的API 被调用了多少次, 并使这些计数器可访问。

在很多情况下,可以通过访问一个特殊的URL来查看这个内部状态。例如,Apache HTTP Web 服务器有一个 "服务器状态" 页(http://www.example.com/server-status/)。另外,分布式系统的组件经常会评估自己的健康状况,并使这些信息可见。例如,一个组件可能有一个URL来输出系统是否准备好(OK)接收新的请求。作为输出接收除字节“O”之后的字节“K”(包括根本没有响应)之外的任何东西表示系统不希望接收新的请求。负载平衡器使用此信息来确定服务器是否正常运行,是否准备接收通信。当服务器启动并且仍在初始化, 或者当它正在关闭并且不再接受新的请求时,服务器将发送否定的答复,除非服务器正在处理仍在传输的请求时,将发送肯定的答复。

10.2  简单的重要性

让设计尽可能简单同时仍能满足服务的需求,这点是非常重要的。随着时间的推移,系统将会变得越来越复杂。系统开始变得复杂意味着系统开始处于一个不利地位。

提供有能力的操作需要在头脑中保持系统的心智模型。在工作中,我们可以去想象系统的运行, 并使用这个心智模型来跟踪它是如何工作的,或者在系统不工作的时候去调试它。越复杂的系统就越难有一个精确的心智模型。一个过度复杂的系统会导致一种情况,这种情况就是在任何时间没有人能够理解这个系统。Kernighan和Plauger在《编程风格元素》一书中写道:调试的难度是第一次编写代码难度的两倍。所以, 如果你打算尽可能巧妙地去编写代码,那么当它发生错误的时候,你根本无法调试它。

这项法则同样适用于分布式系统,花在简化设计的每一分钟都将会在系统的一次又一次的运行中得到补偿。

10.3 系统组成

分布式系统由许多较小的系统组成。在本节中, 我们将会详细探讨三中基本的组合模式:具有多个后端副本的负载平衡器、具有多个后端的服务器和服务器树。

10.3.1 具有多个后端副本的负载平衡器

第一个组合模式是具有多个后端副本的负载平衡器。如图10.1 所示, 请求被发送到负载平衡器服务器。对于每个请求, 它选择一个后端并把请求转发到那里。响应返回到负载均衡器服务器,再由其转发给原始请求者。

分布式世界中的软件设计

图10.1具有多个副本的负载均衡器

后端也被称为副本,因为它们都是彼此的复制品或克隆体。发送到任何副本的请求都应产生相同的响应。

负载均衡器必须知道哪些后端处于活动状态并准备好接受请求。负载平衡器每秒钟发送数十次运行状况检查查询,并在运行状况检查失败时停止向该后端发送信息。运行状况检查是一个应当快速执行的简单查询,并返回系统是否应该接收通信。

选择哪个后端发送查询有简单和复杂两种算法。简单的算法是循环交替的选择后端,这种算法叫做轮转。然而有些后端可能比其他后端更强大,而且可能会更多地使用比例轮转方案进行选择。更为复杂的解决算法是最少加载。在这种算法中,负载均衡器会跟踪每个后端的加载情况,并始终选择最少的一个进行加载。

选择最少加载的后端听起来合理,但简单的去执行就有可能是一场灾难。后端可能不会显示超载的迹象,直到它实际上变得过载为止。出现这个问题原因是要准确地测量系统的负载量是很困难的。如果以最近发送到服务器的连接数作为负载的度量标准,但利用连接数作为度量标准是盲目的。因为有些连接可能会持续很长时间,而有一些则可能会很快结束。如果测量标准是基于CPU利用率的,那么这个定义对于输入/输出过载就是盲目的。通常使用的是最后5分钟的负载平均值。尾平均值的问题在于,作为平均值,它们反映了过去,而不是现在。因此一个急剧的、突然的负荷增长不会在一段时间的平均值中反映出来。

想象一下有10个后端的负载均衡器。每个后端在80%的负载下运行,然后添加一个新的后端。因为它是新的,所以它没有负载,是最少加载的后端。一个朴素最少加载算法会把所有的流量请求到这个新的后端,没有流量会发送到那10个后端。流量快速增长,新后端被淹没在请求中。单个后端无法处理以前由十个后端所处理的流量。尾随平均值的使用意味着较旧的后端将继续报告几分钟的人为高负载,而新的后端将报告一个人为的低负载。

在这个算法下, 负载平衡器认为,新的机器比其他的机器加载时间要少。在这种情况下, 机器可能会过载, 它会崩溃和重启, 或者系统管理员试图纠正情况而去重启它。但当它重返服务时, 这一循环将再次从开始。

这种情况使循环法看起来不错。一个不那么天真的最少加载的实现会有一些控制,即永远不会有超过一定数量的请求连续发送到同一台机器。这种算法被称为慢启动算法。

朴素最小加载算法的弊端:如果没有慢启动, 负载平衡器就会导致许多问题。一个著名的例子发生在9·11事件中的CNN.com(美国有线电视新闻网)。许多人尝试访问CNN.com后端导致其变得超载,网站崩溃后重启,然后再次崩溃,因为朴素最小加载算法把流量全发送到一台机器上。当一台服务器崩溃后,其他后端变得超载进而崩溃,每次都会有一个后端会超载崩溃,重启后要重新接收所有的流量而再次崩溃。结果,当服务不能用的时候,系统管理员迫切的想弄明白发生什么事情,在他们的解释中,网络是新的,足够处理这么庞大的流量,但是他们却没有处理类似9·11事件的经验。

CNN使用的解决方案是暂停所有的后端,并同时启动它们,以便它们都显示零负载,并接收相同数量的流量。

CNN团队后来发现,几天前负载平衡器的软件更新已经到达,但尚未安装。更新中增加了一个慢启动机制。

10.3.2 具有多个后端的服务器

下一个组合模式是一个具有多个后端的服务器。服务器接收请求后,向多个后端服务器发送查询,并通过组合这些应答形成最终结果。当原始查询可以很容易地分解为多个独立的查询时, 通常会使用这种方法来形成最终的结果。

图10.2a说明了一个简单的搜索引擎如何在多个后的帮助下处理一个查询。前端接收请求。它将查询转发到许多后端服务器。拼写检查器回复信息, 因此搜索引擎可能建议交替拼写。web 和图像搜索后响应与查询相关的网站和图像的列表。广告服务器响应与查询相关的广告。收到回答后, 前端将使用此信息构造 HTML, 以便为用户生成搜索结果页, 然后作为结果发送。

分布式世界中的软件设计

图10.2: 此服务由服务器和许多后端组成

图10.2b显示了和具有复制服务器、负载平衡、后端的相同体系结构。同样的原理也适用具有多个服务器的系统结构, 但这个系统具有更好地扩展性和容错性。

这种组合方式具有许多优点。后端并行工作,应答不必等待一个后端进程在下一个开始之前完成。系统是松散耦合的,一个后端的工作可能会失败,但仍然可以通过填写一些默认信息或将该区域留空来构建页面。

这种模式还允许一些相当复杂的延迟管理。假设此系统在200毫秒或更少的时间内返回结果。如果某个后端因为某种原因而响应缓慢, 那么前端就不必等待。如果需要10毫秒来编写并发送生成的 HTML页面,则在190毫秒时, 前端可以放弃缓慢的后端, 而生成当前所拥有信息的页面。像这样管理时间预算的能力是十分强大的。例如, 如果广告系统速度缓慢,那么搜索结果可以不带任何广告显示。

要清楚的是,“前端”和“后端”是一个相对的概念,前端向后端发送请求,后者返回结果。服务器既可以是前端,也可以是后端。在前面的例子中,服务器是Web浏览器的后端,但也是拼写检查服务器的前端。

这种模式有很多变化,可以复制每个后端以提高容量或恢复能力。缓存可以在不同的级别完成。

术语扇出指的是一个查询会产生许多新查询, 每一个用于对应的后端。查询 "扇出" 到达每个独立的后端, 再把回应扇入到前端,在前端中组合成最终的结果。

任何情况下扇入都有可能出现拥塞的危险。通常小的查询可能会产生较大的响应。因此, 使用少量的带宽来扇出, 但却可能没有足够的带宽来支持扇入,这可能会导致网络链接阻塞和服务器超载。在查询和应答的大小一致或者偶尔有较大的应答的情况下,使系统具有适当数量的网络和服务器容量是很容易的。但在有突然的、不可预知的、爆发应答的情况下,就有些困难。一些网络设备是专门为处理这种情况而设计的, 在突发情况发生时,它能够动态地提供更多的缓冲区空间。同样, 后端也可以限制自身的速率,以避免出现这种情况。最后, 前端可以通过控制他们发出的新查询来管理拥塞, 通过通知后端减慢速度, 或者通过实施紧急措施来更好地处理这种情况。

10.3.3 服务器树

另一个基本的组合模式是服务器树。如图10.3 所示, 在这个方案中, 许多服务器协同工作,其中一个作为树的根,以及它下面的父服务器以及树底部的叶服务器(在计算机科学中, 树是上下颠倒的)。通常, 这种模式用于访问大型数据集或语料库,这个语料库比任何一个机器都能容纳的都要大。因此, 每个叶存储整个语料库的一部分。

分布式世界中的软件设计

图10.3:服务器树

若要查询整个数据集, 根服务器接收原始查询并将其转发给父服务器,父服务器将查询转发到叶服务器, 叶服务器搜索其拥有的语料库。每个叶服务器向其父服务器发送它的查询结果, 父服务器将结果进行排序和过滤, 然后将它们转发到根服务器。最后根服务器从所有的父服务器那里得到响应, 合并结果, 向前端响应完整的结果集。

想象一下,如果你想知道在一本百科全书中提到乔治·华盛顿有多少次,你可以依次阅读每一章,找出答案。或者你可以将每个章目分配给不同的人,并让各个人并行搜索。后一种方法可以更快地完成任务。

这种模式的最大好处是它允许并行搜索一个大型语料库。不仅这些叶服务器可以平行地搜索着它们的一部分, 父服务器的排序也可以同时进行。

例如, 想象一下在美国国会图书馆的每本书中摘录文本,这所有信息不能放在一台计算机中, 因此会分布在成百上千的叶子服务器上。除叶子服务器之外是父服务器和根服务器。搜索查询将上传到根服务器, 根服务器又将查询转发到所有父服务器,每个父服务器都将查询转发到它下面的所有叶节点,一旦叶服务器应答,父服务器就会根据相关性对结果进行排序和分类。

例如,叶服务器可能需要应答查询的所有单词存在于同一本书中的同一段落,但是对于另一本书,只有一些单词存在(不太相关),或者存在但不在同一段落或页面中(甚至不相关)。如果查询最好的50个答案,那么父服务器可以将最前面的50个结果发送到根服务器,然后放弃其余的答案。根服务器从每个服务器收到结果,并选择最好的50条记录构建结果。

该方案还允许开发人员在延迟预算内工作。如果响应速率比结果更重要,那么如果延迟期限接近,父服务器和根服务器就不必等待迟迟未响应的应答。

这种模式也有可能产生多种变体。冗余服务器可能存在一个负载平衡方案,以便将它们之间的工作分开并绕过故障服务器。扩展叶服务器的数量可以为每个叶服务器提供一个更小的部分来搜索, 或者每个语料库的碎片可以放在多个叶服务器上以提高可用性。在每个级别上扩展父服务器的数量会在增强排序和分级结果的能力。可能会有更多级别的父级服务器,使树的高度增加。额外的级别允许更广泛的扇出,这对于非常大的语料库是重要的。父服务器可以提供缓存功能来缓解叶子服务器的压力;在这种情况下,更多同级别的父服务器可能会提高缓存效率。正如前面所讨论的那样,这些技术也可以帮助缓解与扇入有关的拥塞问题。

10.4  分布式状态

大型系统通常存储或处理大量的状态信息。状态由经常更新的数据 (如数据库) 组成。与数据库相比, 语料库是相对静态的, 或者只在发布新版本时定期更新。例如, 一个搜索美国国会图书馆的系统每周可能会收到一个新的语料库。相比之下, 电子邮件系统在不断的改动,新的数据不断到达, 当前的数据正在更新 (电子邮件被标记为 "读取"或在文件夹之间移动), 或者数据正在被删除。分布式计算系统有多种处理状态的方式。但是它们都涉及某种复制和切分, 这会带来一致性、可用性和分区容错方面的问题。

存储状态信息最简单的方法是将它们放在一台机器上,如图10.4所示。不幸的是,这种方法会使机器很快达到极限状态:单个机器只能存储有限的状态,如果一台机器崩溃,我们将失去全部的状态。单个机器的处理能力有限,这意味着它可以处理的同时读写数量是有限的

分布式世界中的软件设计

图10.4: 保存在一个位置的状态,非分布式计算

在分布式计算中,我们通过在单独的机器上存储整体的一部分或碎片。这样我们可以存储的状态数量仅受我们可以获得的机器数量的限制。另外,每个分区存储在多台机器上,因此单个机器故障不会失去对任何状态的访问。每个副本可以每秒处理一定数量的查询,所以我们可以设计系统通过增加副本的数量来处理任意数量的同时读写请求。这在图10.5中进行了说明,其中N个QPS被接收并分布在三个分区中,每个分区以三种方式进行复制。最终,平均有九分之一的查询到达特定的副本服务器。

分布式世界中的软件设计

图10.5:被分割和复制的分布式状态。

写入或更新状态的请求需要更新所有副本。在更新过程发生时,有些客户端可能会从尚未更新的陈旧副本中读取数据。图10.6说明了写操作如何被读取混淆到过时的缓存。这将在下一节中进一步讨论。

分布式世界中的软件设计

图10.6:使用缓存数据的状态更新导致视图不一致。

在最简单的模式中, 根服务器接收存储或检索状态的请求。它确定哪个碎片包含该部分状态, 并将请求转发到相应的叶服务器。随后响应就会返回到树上。这与上一节中描述的服务器树模式类似, 但存在着两种差异。首先,查询转到单个叶服务器而不是所有的叶服务器;其次,请求可以是更新 (写入) 请求, 而不仅仅是读取请求。当碎片存储在许多副本上时,更新更复杂。当一个切片被更新时, 所有的副本也必须更新。这可以通过使根服务器更新全部叶服务器或叶服务器在本身之间进行通信来完成更新。

在传输大量数据时,这种模式的变体更合适。在这种情况下, 根服务器会回答有关如何获取数据而不是数据本身的说明,然后请求程序直接从源请求数据。

例如,假设分布式文件系统在数千台计算机上分布有数PB的数据。每个文件被分割成千兆字节大小的块。每个块都存储在多台机器上以实现冗余。这种方案还允许创建大于适合一台机器的文件。主服务器跟踪文件列表并确定其块的位置。如果您熟悉UNIX文件系统,则可以将主节点视为存储结点或每个文件列表的数据块,另一台机器存储实际的数据块。文件系统操作通过使用类似结点的信息的主服务器来确定涉及哪些机器来进行操作。

想象一个大的读请求进来,主机确定该文件存储在一台计算机上的数TB和另一台计算机上的数TB。它可以向每台机器请求数据, 并将数据传递给发出请求的系统, 但是当主机在接收海量数据的时候会很快变得超载。因此主机用一个列表来回答哪些机器有数据,请求者直接与这些机器联系以获得数据。这种方式并不是主机作为中间件调动数据的发送。图10.7 说明了这种情况。

分布式世界中的软件设计

图10.7主服务器将答复委派给其他服务器。

10.5  CAP 原则

CAP代表一致性,可用性和分区容错性。CAP原则指出,建立一个可以保证一致性,可用性和分区容错性分布式系统是不可能的,我们可以实现其中任何一个或两个特性,但不可能全部同时实现。在使用此类系统时,我们必须了解到系统中哪些特性是可以保证的。

10.5.1 一致性

一致性意味着所有节点在同一时间看到的数据是相同的。如果有多个副本, 并且正在处理更新, 则所有用户都可以看到更新的进行, 即使他们正在从不同的副本执行读取操作。不保证一致性的系统可能会提供最终一致性。例如,该系统可以保证任何更新都会在一定时间内传播到所有副本。在达到该截止日期之前, 某些查询可能会收到新数据, 而另一些则会接收较早的、过期的数据。

完全同步的一致性有时候不是那么重要。设想一个社交网络,为用户提供积极行为的信誉点,用户的信誉点总数可以像名字一样显示在任何地方。信誉点的复制数据库分别分布在美国、欧洲和亚洲。欧洲的用户获得积分,而这种变化可能需要几分钟的时间才能传播到美国和亚洲的数据库。对于像社交网络这样的系统来说这可能是足够的,因为绝对准确的信誉评分不是必需的。如果一个美国的用户和一个亚洲的用户在通话中获得积分,另一个用户会在几秒钟后看到更新,这样就可以了。即使因为网络中断或者拥塞导致更新花费了几分钟甚至数小时的时间,延迟依然是可以接收的一件事。

现在设想一下在这个系统上构建的银行应用程序。一个美国用户和一个欧洲用户同时从同一个帐户取钱。每个人使用的取款机将查询其最近的数据库副本, 系统返回信息称该钱是可用的, 并可以被取走。如果更新的传播速度比较慢, 在银行意识到钱已被取走之前,用户账户内依然有现金。

10.5.2 可用性

可用性是保证每个请求都收到有关是成功还是失败的响应。换句话说,这意味着系统已经启动。例如使用多个副本来存储数据,以便客户机始终可以访问至少一个工作副本,以确保可用性。

CAP原则指出可用性还可以保证系统能够报告故障。例如, 系统可能检测到它已超载, 然后使用错误代码回复请求, 这意味着 "稍后再试"。用户立即被告知比必须等待几分钟甚至数小时后放弃访问更有利。

10.5.3 分区容错性

分区容错意味着系统在任意消息丢失或故障的情况下能够继续运行。其中最为简单的例子是即使在提供服务的计算机由于网络链路阻塞而导致通信失败时,系统仍可以继续运行 (请参见图 10.8)。

分布式世界中的软件设计

图10.8: 彼此分开的节点

回到我们副本服务器的例子,如果系统是只读的,那么很容易使系统分区具有容错性,因为副本不需要相互通信。但是请考虑包含状态的副本的示例, 它首先在一个副本上更新, 然后复制到其他副本。如果副本无法与每个进行通信,那么系统无法保证更新会在一定的时间内传播到其他服务器,从而成为一个失败的系统。

现在考虑两台服务器以主从关系协作的情况。两者都保持完整的状态副本,如果主服务器发生故障,则从服务器接管主服务器的角色,这是由于心跳丢失(即通过专用网络在两台服务器之间进行的定期健康检查)所决定的。如果两者之间的心跳网络被断开,两台服务器无法在心跳网络上进行通信,从服务器不知道主服务器已启动,于是从服务器将自己提升为主服务器。在这一点上有两个服务器同时工作,造成系统冲突。这种情况被称为脑裂。

存在一些特殊的分区情况。分组丢失被认为是系统的临时分区,因为它适用于CAP原则。另一个特例是网络完全中断,分区容错性再好的系统也无法在这种情况下工作。

CAP原则认为任何一个或两个属性都是可以组合的,但三个属性无法结合。2002年,吉尔伯特和林奇发表了原始猜想的正式证明,使这项规定成为一个定理。人们可以把这看作是为了实现另外两个而牺牲的第三个属性。

CAP原理如图10.9中的三角形所示。传统的关系数据库如Oracle,MySQL和PostgreSQL具有一致性和可用性(CA),它们使用事务和其他数据库技术来确保数据更新是原子性的,更新信息要么传播到所有服务器,要么一个服务器也不传播。因此,它们保证所有的用户在同一时间看到的状态是相同的。新型的存储系统如Hbase,Redis和Bigtable关注一致性和分区容错性(CP)。分区时,它们变成只读状态或者拒绝响应任何请求,这些系统不具有一致性,允许一些用户看到旧数据,而其他用户看到新的数据。最后,例如Cassandra,Risk和Dynamo等系统专注于可用性和分区容错性(AP)。他们始终强调能够满足要求,即使这意味着某些客户收到了过时的结果。这样的系统通常用于全球分布式网络,每个副本服务器通过不太可靠的媒介传播,例如互联网。

分布式世界中的软件设计

图10.9 CAP原则

SQL和其他关系数据库使用术语 ACID 来描述CAP三角形的边。 ACID代表原子性(事务是“全部执行或者都不执行”),一致性(每次事务之后数据库处于有效状态),隔离性(并发事务提供的结果与串行执行的结果相同)和持久性(一个已经提交的事务数据不会因为系统崩溃或者其他问题而丢失)。提供弱一致性模型的数据库通常被称为NoSQL,用BASE进行描述,即具有最终一致性的基本可用软状态服务。

10.6  松散耦合的系统

预计分布式系统具有高可用性,并且可以持续很长时间,可以在不中断的情况下发展和变化。整个子系统在系统启动和运行期间经常被替换。

为了实现这一目标, 分布式系统使用抽象构建松散耦合的系统。抽象意味着每个组件提供一个以隐藏具体实现细节的方式定义的接口。如果每个组件都很少或根本不了解其他组件的内部,那么这个系统是松散耦合的。因此一个子系统可以被一个提供相同的抽象接口的系统所取代, 即使它们的实现方式完全不同。

例如拼写检查服务,一个好的抽象级别是接受文本, 然后返回错误单词的拼写描述, 以及每个词的更正列表。一个坏的抽象级别只会提供对前端可以查询类似词汇的词典访问。后者不是一个好的抽象的原因是如果发现了一个全新检查方式, 当前端换位这个新的拼写检查服务,系统需要重写。

假设这个新版本不依赖于词典, 而是应用了一种叫做机器学习的人工智能技术。有了良好的抽象, 没有前端需要改变,它会向新服务器发送相同类型的请求,而使用坏抽象的用户就不会这么幸运了。

鉴于以上和许多其他原因,松散耦合的系统更容易随时间的变化演变。继续我们的例子,在推出新的拼写检查服务要做好两个版本可以并行运行的准备。位于拼写检查系统前面的负载平衡器将所有请求发送到新旧两个系统上。旧系统直接将结果发送给用户,而新系统将结果收集和对比,对结果进行质量控制。起初,新系统产生的结果可能不是很好,但随着时间的推移,结果的质量会得到加强,直到得到可量化的更好的结果。基于这一点,新系统将被投入生产。但要小心的是,也许只有百分之一的查询结果通过新的系统,如果没有用户抱怨,新的系统将占据更大的比例。最终,所有的响应都将来自新系统,旧系统被淘汰掉。

其他系统比拼写检查系统需要更高的精确度和准确性。例如,可能要求新系统有与旧系统兼容的错误,才能提供新功能。即新系统不仅要重现旧系统的特征,还必须再现其缺陷。在这种情况下,将请求发送到两个系统和比较结果的能力对部署的操作任务至关重要。

案例分析:改进之前的仿真

Tom曾经在Cibernet参与了一个项目,该项目是替换一个旧系统,因为这个系统是关于金融的系统,所以在新系统部署之前,必须检测新系统能够兼容旧系统的程序缺陷。旧系统是在过时的网络技术上建立起来的,整个架构变得非常复杂和僵化,根本不可能再添加新的功能。新系统是在更新的更好的技术上建立的,有一个更为清晰的设计,更容易去容纳新的功能。新旧系统并行运行,并且结果可以比较。

这个时候,工程师在旧系统中发现了一个程序错误。货币换算是以非标准的方式进行的, 结果略有偏差。为了使两个系统之间的结果可以比较, 开发人员对该错误进行了****, 并在新系统中对它进行了模拟仿真。

现在, 新旧系统的结果可以匹配到便士。随着公司对新系统兼容旧系统bug能力有了信心, 新系统被提升为主系统,旧系统被禁用。

现在可以对系统进行新的功能添加和改进。毫无疑问第一个需要改进的就是删除模拟货币换算错误的代码。

10.7  速度

到目前为止,我们已经详细阐述了设计大型分布式系统所涉及的许多因素。对于网络和其他交互式服务,速度一个项目可能是最重要的。需要花费时间去获取信息、存储信息、计算和转换信息以及传输信息,这些事情都不可能立刻完成。

交互式系统需要快速响应。用户在感知200毫秒更快的响应速度时,往往认为响应是一瞬间的事,用户更喜欢快而不是慢。研究表明, 当人为地在网站上添加50毫秒延迟后,收入会大幅下降。在批处理和非交互式系统中,时间对于总吞吐量必须满足或超过输入的工作量也很重要。

设计一个高性能系统的一般策略是使用我们的最佳估计值来进行设计,它能够快速处理请求,然后构建原型来测试我们的假设。如果我们错了,我们就回到第一步,然后至少在下一次迭代中将我们所了解到的信息应用到系统设计中。当我们构建系统的时侯,如果发现我们的估计和原型没有像希望的那样去指导我们,那么我们就需要重新测量,调整设计。

在设计过程开始的时候,我们经常会创建许多设计,评估每一个设计的速度,把那些不够快的消除掉。我们不会自动选择最快的设计,因为最快的设计成本可能非常高。

那么我们如何确定一个设计是否是值得追求的呢?构建原型非常耗时,我们可以通过一些简单的估计训练进行推断。选择一些常见的事务,然后将它们分解为较小的步骤,估计每个步骤需要耗费的时间。

花费时间最多的两个步骤是磁盘访问和网络延迟。磁盘访问速度很慢,因为它涉及机械操作。要从磁盘读取数据块,需要将磁头移动到正确的磁道上,然后盘片必须旋转, 直到所需的数据块是在磁头下。这个过程通常需要10毫秒,而从RAM读取相同数量的信息只需要0.002 毫秒,速度是磁盘访问的5000倍。磁头和盘片(称为主轴) 一次只能处理一个请求。然而一旦磁头在正确的磁道上,它可以读取许多顺序块。因此, 如果两个块相邻,读取两个数据块几乎与读取一个块同样快。固态硬盘没有机械旋转盘片, 而且速度要快得多, 但更加昂贵.

网络访问速度很慢,因为它受光速度的限制。数据包需要大约75毫秒的时间才能从加利福尼亚到达荷兰。大约一半的路程时间是由于光传播的时间,其他延迟可能是由于每个路由器上的处理时间、从有线到光纤通信的信号转换时间,以及在每个端上组装和解析数据包所需的时间等.

同一网段上的两台计算机看起来好像是即时通信,但事实并非如此。这里的时间尺度是太小,有一个更大的因素导致延迟。例如当通过本地网络传输数据时,第一个字节很快到达,但接收数据的程序通常不会处理它,直到接收到整个包才开始处理。

在许多系统中,与网络和磁盘操作的延迟相比,数据计算所需的时间很少。因此,只需要知道从用户到数据中心的距离以及所需的磁盘查找次数, 就可以估计事务处理的时间。这样的估计结果通常会很好地帮助你丢弃明显的不好的设计。

为了说明这一点, 假设你正在构建一个电子邮件系统, 它需要能够从消息存储系统中检索消息并在300毫秒内显示它。我们将使用图10.10 中列出的各项操作的近似时间来帮助我们设计解决方案。

谷歌的一位同事Jeff将一份含有许多数据的表格普及,目的是帮助工程师们更好的做出决策,正如你所看到的,在一些选项中有极大的不同,这些数据每年都在增长,我们可以在网络上找到这些数据的更新。

动作

所花时间

一级缓存引用

0.5ns

分支缓存预测

5ns

二级缓存引用

7ns

互斥锁定

100ns

主存引用

100ns

压缩1K字节

10000ns(0.01ms)

通过1Gbps网络发送2K字节

20000ns(0.02ms)

从内存中连续读取1MB的数据

250000ns(0.25ms)

统一数据中心的往返路程

500000ns(0.5ms)

从固态硬盘中读取1MB数据

1000000ns(1ms)

磁盘寻找

10000000ns(10ms)

从网络中连续读取1MB数据

10000000ns(10ms)

从硬盘中连续读取1MB数据

30000000ns(30ms)

从加利福尼亚发送数据包到荷兰再返回加利福尼亚

150000000ns(150ms)

图10.10: 每个工程师应该知道的数字

首先, 我们跟踪事务的执行流程。该请求来自于可能在另一个大陆上的网页浏览器,服务器必须对请求进行身份验证, 查询数据库索引以确定在何处获取消息文本,然后检索消息文本, 最后将响应数据格式化并传输回用户。

现在让我们对不能控制的时间进行估算。数据包从加利福尼亚发到欧洲一般需要花费75毫秒的时间,除非物理学能够改变光的速度,否则这个时间不会变化。因为不仅要考虑到请求发送的时间,也要考虑数据响应的时间,所以我们300毫秒的时间预算减少到了150毫秒。剩下的一般时间预算由我们不能控制的事情耗费掉了。

我们和研发身份验证系统的团队谈话,要求他们把身份验证的时间控制在3毫秒以内。

格式化数据只需要花费很少的时间,比其他操作所花费时间的增长小的多,因此可以忽略它。

剩下的147毫秒主要用于在存储中信息的检索。如果一个典型的索引查找需要进行三次磁盘查找,每次需要花费10毫秒,读取1兆的字节信息大约要耗费60毫秒,这些操作总需要花费60毫秒。读取数据本身需要进行四次磁盘查找,读取2兆的字节大约需要花费100毫秒。总共是160毫秒,超过了147毫秒的时间预算。

我们是如何得知的呢?我们如何得知读取索引需要进行三次磁盘查找呢?这就涉及到UNIX系统的内部工作原理了,如何利用文件目录查找到索引节点以及如何利用索引节点找到数据块了。这就是为什么了解你所使用的操作系统的内部结构是能够设计和操作分布式系统的关键。UNIX和类似UNIX操作系统的内部有很好的文档记录, 因此它们比其他系统具有优势。

虽然我们的设计没有满足设计参数,但值得高兴的是,灾难已经避免。现在知道了要好得多,当系统上线的时候再发现那就为时已晚了。

60毫秒的索引查找时间似乎很长,但我们可以大大改善。如果索引是在 RAM 中进行的呢?这有可能吗?一些快速计算估计查找树必须是3层深, 才能扇出足够多的机器,从而覆盖这么多的数据。如果数据都在同一个数据中心,那么向上和向下的树是5层, 或者是耗费2.5毫秒。新的总数 (150 毫秒+3 毫秒+ 2.5 毫秒+100 毫秒 = 255.5 毫秒) 小于我们的300 毫秒的预算。

对于对时间敏感的其他请求, 我们将重复这个过程。例如我们发送电子邮件的频率比我们阅读电子邮件的要少, 所以耗费时间的关键可能不是发送电子邮件。相反, 删除邮件几乎和阅读邮件一样频繁。我们可以一些删除方法来的计算比较它们的效率。

一个设计可能会与服务器联系, 并从存储系统和索引中删除消息。另一个设计可能使存储系统仅做一个删除标记,表示信息已经删除。这样操作时间将大大加快, 但需要一个新数据项作标记位, 并偶尔压缩索引, 移除掉标记的选项。

更快的响应时间可以用异步来实现,这意味着客户端向服务器发送请求, 并在不等待请求完成的情况下迅速将控制权返回给用户。即使实际工作滞后, 用户也能很快地感知到这个系统。异步设计实现起来更为复杂,服务器很可能去将请求放到一个队列里而不是实际的去执行这些动作,其他进程将请求从队列中读取出来,并且在后台执行它们,或者客户端可以只发送请求并在稍后检查答复,或者分配线程或子进程等待答复。

所有这些设计都是可行的, 但每个设计都有不同的速度和实现复杂度。随着对速度和成本进行估算,通过对原型的支持,我们可以做出业务决策。

10.8  摘要

分布式计算在许多方面与传统计算不同。规模较大,机器众多,每个机器都负责特定的任务。可以复制服务来增加容量。硬件故障不作为紧急状况或异者常处理, 而是作为系统的一个预期的部分。因此,这个系统中充满着失败操作。

大型系统由许多小部分构成。我们讨论了三种典型的组合模式:具有多个后端副本的负载平衡器,具有多个后端的服务器和服务器树。

负载平衡器在许多重复的系统之间划分通信量。与不同后端通信的前端并行使用不同的后端,每个执行不同的程序。服务器树使用树形结构配置, 树中不同层次的节点有不同的功能。

在分布式系统中维护状态是复杂的,无论它是不断更新的信息的大型数据库还是许多系统需要的几个关键性持续访问。CAP 原则指出,不可能建立一个同时保证一致性、可用性和分区容错性的分布式系统,最多实现三个特性中的两个。

系统预计将随着时间的推移而变化。为了更好的使用变化,系统组件是松散耦合的。每一个都体现了它所提供的服务的抽象化,这样就可以在不改变抽象化的情况下更换或改进内部构件。因此,服务的依赖性不需要更改,而是从新功能中改进系统。

设计分布式系统需要了解运行各种操作所需的时间,以便在设计对时间要求较高的程序时,可以满足这些系统的时间预算延迟。