本篇文章英文版链接:Scalable Web Architecture and Distributed Systems Kate Matsudaira。这篇文章简略地描述了分布式架构是怎么来解决系统扩展性问题。
开源软件已经成为一些大型网站的基本构件。随着这些网站的发展,围绕其架构的最佳实践和指导原则也出现了。本章旨在介绍设计大型网站时需要考虑的一些关键问题,以及实现这些目标所需的一些构建模块。
本篇文章主要关注web系统,尽管其中一些内容也适用于其他分布式系统。
1. Web分布式系统设计原则
构建和运行一个可扩展的web站点或应用程序究竟意味着什么?在最原始级别上,它只是通过互联网将用户与远程资源连接起来 -- 使其可伸缩的部分是资源,或对这些资源的访问,分布在多个服务器上。
就像生活中的大多数事情一样,在构建web服务时提前做计划从长远来看是有帮助的;了解大型网站背后的一些考虑因素和权衡可以在创建小型网站时做出更明智的决策。以下是影响大型web系统设计的一些关键原则:
- 可用性:网站的正常运行时间对许多公司的声誉和功能都至关重要。对于一些规模较大的在线零售网站来说,即使只有几分钟时间不可用,也会导致成千上万甚至数百万美元的收入损失,因此,设计能够持续可用的系统,并对失败保持弹性,既是一项基本业务,也是一项技术要求。分布式系统中的高可用性要求仔细考虑关键组件的冗余、部分系统故障时的快速恢复以及出现问题时的优雅降级。
- 性能:网站性能已经成为大多数网站的一个重要考虑因素。网站的速度会影响用户的使用和满意度,以及搜索引擎的排名,这是一个与收入和用户留存率直接相关的因素。因此,创建一个针对快速响应和低延迟进行优化的系统是关键。
- 可靠性:系统需要可靠,这样对数据的请求将始终返回相同的数据。如果数据发生更改或更新,那么相同的请求应该返回新数据。用户需要知道,如果向系统写入或存储了某些内容,那么这些内容将会持久存在,并且可以依赖于这些内容以备将来检索。
- 可伸缩性:对于任何大型分布式系统,规模只是需要考虑的一个方面。同样重要的是增加处理更大负载的能力所需要的精力,通常称为系统的可伸缩性。可伸缩性可以参考系统的许多不同参数:它可以处理多少额外的流量、添加更多的存储容量有多容易,甚至可以处理多少事务。
- 可管理性:设计一个易于操作的系统是另一个重要的考虑因素。系统的可管理性等同于操作的可伸缩性:维护和更新。要考虑可管理性,需要考虑的问题包括:当问题发生时,诊断和理解问题是否容易,更新或修改是否容易,以及系统操作是否简单。(即它是否正常运行,没有故障或异常?)
- 成本:成本是一个重要因素。这显然包括硬件和软件成本,但是考虑部署和维护系统所需的其他方面也很重要。应该考虑开发人员构建系统所需的时间、运行系统所需的操作工作量,甚至需要的培训量。
这些原则中的每一个都为设计分布式web体系结构的决策提供了基础。然而,它们也可能彼此不一致,以至于实现一个目标的代价是另一个目标。一个基本示例:选择通过简单地添加更多服务器(可伸缩性)来解决容量问题,可能要付出可管理性(必须操作额外的服务器)和成本(服务器的价格)的代价。
2. 基础
当涉及到系统架构时,有一些事情需要考虑:什么是正确的部分,这些部分如何组合在一起,以及什么是正确的权衡。在需要之前投资于规模通常不是明智的商业主张;但是,对设计进行一些前瞻性的考虑可以在未来节省大量的时间和资源。
本节主要讨论几乎所有大型web应用程序的核心因素:服务、冗余、分区和故障处理。每一个因素都涉及选择和妥协,特别是在上一节描述的原则的上下文中。为了详细解释这些,最好从一个例子开始。
2.1 示例:图像托管应用程序
在某些时候,你可能要在网上发布一张图片。对于承载和传送大量图像的大型站点,在构建具有成本效益、高可用性和低延迟(快速检索)的体系结构方面存在挑战。
设想这样一个系统,用户可以将自己的图像上传到中央服务器,并且可以通过web链接或API请求图像,就像Flickr或Picasa一样。为了简单起见,我们假设这个应用程序有两个关键部分:上传(写)图像到服务器的能力,以及查询图像的能力。虽然我们当然希望上传是高效的,但我们最关心的是当有人请求一个图像时(例如,可以为web页面或其他应用程序请求图像)能够非常快速地传输。这与web服务器或内容交付网络(CDN)边缘服务器(服务器CDN用于在许多位置存储内容,因此内容在地理上/物理上更接近用户,从而提高性能)可能提供的功能非常相似。
该系统的其他重要方面包括:
- 存储的图像的数量没有限制,因此需要考虑存储的可扩展性(就图像数量而言)。
- 图像下载/请求需要较低的延迟。
- 如果用户上传了一个图像,那么图像应该始终存在(图像的数据可靠性)。
- 系统应该易于维护(可管理性)。
- 由于图像托管没有很高的利润率,系统需要具有成本效益
图1.1是功能的简化图。
图1.1:图像托管应用程序的简化架构图在这个图像托管示例中,系统必须速度快,数据存储可靠,并且所有这些属性都具有很高的可伸缩性。构建这个应用程序的小版本非常简单,并且很容易托管在一台服务器上;然而,这对于本文章来说并不有趣。假设我们想建立一个像Flickr一样大的东西。
2.2 服务
在考虑可扩展的系统设计时,它有助于将功能解耦,并将系统的每个部分看作具有明确定义的接口的自己的服务。在实践中,以这种方式设计的系统被称为具有面向服务的体系结构(SOA)。对于这些类型的系统,每个服务都有自己独特的功能上下文,并且通过抽象接口(通常是另一个服务的面向公共的API)与上下文之外的任何东西进行交互。
将系统分解为一组互补的服务,可以将这些部分的操作彼此解耦。这种抽象有助于在服务、其基础环境和服务的消费者之间建立清晰的关系。创建这些清晰的描述可以帮助隔离问题,但也允许每个部分独立地伸缩。这种面向服务的系统设计与面向对象的编程设计非常相似。
在我们的例子中,所有上传和检索图像的请求都由同一台服务器处理;然而,由于系统需要扩展,将这两个功能分解为各自的服务是有意义的。
假设服务正在大量使用;这样的场景很容易看出写操作的时间会影响读取图像所需的时间(因为这两个函数将争夺共享资源)。根据架构的不同,这种效果可能非常显著。即使上传和下载速度是相同的(这是不正确的IP网络,因为大多数网络用于下载速度:上传速度的比例至少为3:1),读取文件通常会被从缓存中读取,写入最终将不得不去磁盘。即使所有内容都在内存中或从磁盘读取(如ssd),数据库写操作也几乎总是比读取操作慢。
这种设计的另一个潜在问题是,像Apache或lighttpd这样的web服务器通常对它可以维护的并发连接的数量有一个上限(默认值大约是500个,但是可以更高),并且在高流量中,写操作可以快速地消耗所有这些连接。因为可以异步读取,或利用其他性能优化,如gzip压缩或分块传输编码,web服务器可以切换为读取速度更快,迅速切换服务更多的每秒请求的最大连接数(与Apache和最大连接设置为500,它并不少见为每秒几千读请求)。另一方面,写操作倾向于在上传期间保持开放连接,因此上传1MB的文件在大多数家庭网络上可能需要超过1秒的时间,因此web服务器只能同时处理500个这样的写操作。
图1.2:划分读和写
如图1.2所示,针对这类瓶颈的规划很好地将图像的读写分离到它们自己的服务中。这使我们能够独立地对它们进行伸缩(因为我们很可能总是读多于写),但也有助于阐明在每个点上发生了什么。最后,这将分离未来的关注点,这将使故障排除和扩展慢读等问题变得更容易。
这种方法的优点是我们能够独立于其他方法解决问题 — 我们不必担心在相同的上下文中写入和检索新图像。这两种服务仍然利用全局图像库,但是它们可以自由地使用适合服务的方法优化自己的性能(例如,排队请求,或缓存流行的图像 — 下面将详细介绍)。从维护和成本的角度来看,每个服务都可以根据需要独立伸缩,这很好,因为如果将它们组合在一起并混合在一起,一个服务可能会在不经意间影响另一个服务的性能,就像上面讨论的场景一样。
当然,当你有两个不同的端点时,上面的示例可以很好地工作(事实上,这与几个云存储提供商的实现和内容交付网络非常相似)。尽管有很多方法可以解决这些类型的瓶颈,但是每种方法都有不同的权衡。
例如,Flickr通过分发用户在不同的分配上来解决这个读/写问题,每一个分片只能处理一定数量的用户,当用户增加则更多的分片被添加到集群。在第一个例子中,根据实际使用情况(整个系统的读和写数量)对硬件进行伸缩比较容易,而Flickr则根据用户基数进行伸缩(但是必须假设用户之间的使用情况相同,这样才能有额外的容量)。在前一种情况下,一个服务的宕机或问题会导致整个系统的功能下降(例如,没有人可以写文件),而Flickr的一个分片宕机只会影响这些用户。在第一个例子更容易在整个数据集执行操作 - 例如,更新写服务包括新的元数据或搜索在所有图像元数据 - 然而Flickr的每个分片需要更新或搜索(或搜索服务将需要创建整理元数据 - 实际上他们就是这么做的)。
这些系统没有正确答案,但它有助于回到在这一章的开始的原则,确定系统需要(读频繁,写频繁,或两者都比较频繁,并发性的级别,整个数据集查询,范围,排序,等等),选择不同的备选方案,了解系统如何将失败,当失败发生时,有一个可靠的计划。
2.3 冗余
为了优雅地处理故障,web体系结构必须有其服务和数据的冗余。例如,如果单个服务器上只有一个文件副本,那么丢失该服务器就意味着丢失该文件。丢失数据很少是件好事,处理它的一种常见方法是创建多个或冗余的副本。
同样的原则也适用于服务。如果应用程序有一个核心功能,那么确保同时运行多个副本或版本可以防止单个节点发生故障。
在系统中创建冗余可以消除单点故障,并在危机中提供备份或备用功能。例如,如果在生产环境中运行两个相同服务的实例,其中一个出现故障或降级,则系统可以故障转移到正常副本。故障转移可以自动发生,也可以需要手动干预。
服务冗余的另一个关键部分是创建无共享体系结构。使用这种体系结构,每个节点都能够独立于其他节点进行操作,并且没有中央“大脑”管理其他节点的状态或协调活动。这对可伸缩性有很大帮助,因为可以在没有特殊条件或知识的情况下添加新节点。然而,最重要的是,在这些系统中没有单一的故障点,因此它们对故障更有弹性。
例如,在我们的图片服务器应用程序中,所有图片会在某处另一块硬件冗余副本(理想情况下是在不同的地理位置,以防在数据中心发生灾难如地震或火灾),所有潜在的服务请求访问图片的服务访问图像也是冗余的。(请参见图1.3)(负载平衡器是实现这一点的一种很好的方法,但是下面还有更多内容)。
图1.3:带有冗余的图像托管应用程序2.4 分区
可能有非常大的数据集无法安装在单个服务器上。也可能是一个操作需要太多的计算资源,性能下降,需要增加容量。无论哪种情况,你都有两个选择:垂直扩展还是水平扩展。
垂直扩展意味着向单个服务器添加更多资源。因此,对于非常大的数据集,这可能意味着添加更多(或更大)的硬盘驱动器,以便单个服务器可以包含整个数据集。在计算操作的情况下,这可能意味着将计算转移到具有更快CPU或更多内存的更大的服务器上。在每种情况下,垂直扩展都是通过使单个资源能够更多地独立处理来实现的。
另一方面,水平扩展就是添加更多的节点。在大数据集的情况下,可能是第二个服务器来存储数据集的一部分,对于计算资源,这意味着将操作或负载分散到一些额外的节点上。为了充分利用水平伸缩,应该将其作为系统体系结构的一个内在设计原则包含进来,否则修改和分离上下文来实现这一点可能会非常麻烦。
当涉及到水平扩展时,最常见的技术之一是将服务划分为分区或碎片。这些分区可以是分布式的,以便每个逻辑功能集是独立的;这可以通过地理边界来实现,也可以通过其他标准来实现,比如非付费用户和付费用户。这些方案的优点是,它们提供了具有附加容量的服务或数据存储。
在我们的图像服务器示例中,用于存储图像的单个文件服务器可能被多个文件服务器替换,每个文件服务器都包含自己独特的图像集。(参见图1.4)。这样的体系结构将允许系统用图像填充每个文件服务器,并在磁盘满时添加额外的服务器。该设计需要一个命名方案,将图像的文件名绑定到包含它的服务器。图像的名称可以由跨服务器映射的一致哈希方案形成。或者,可以为每个图像分配一个增量ID,这样当客户机请求一个图像时,图像检索服务只需要维护映射到每个服务器的ID范围(比如索引)。
图1.4:带有冗余和分区的图像托管应用程序当然,跨多个服务器分发数据或功能也存在挑战。其中一个关键问题是数据局部性;在分布式系统中,数据越接近操作或计算点,系统的性能就越好。因此,将数据分散在多个服务器上可能存在问题,因为任何需要数据的时候,数据都可能不是本地的,这迫使服务器在网络上执行代价高昂的获取所需信息的操作。
另一个潜在的问题是不一致。当从共享资源(可能是另一个服务或数据存储)读取和写入不同的服务时,就有可能出现竞争条件(某些数据应该被更新,但读取发生在更新之前),在这些情况下,数据是不一致的。例如,在图像托管场景中,如果一个客户端发送一个请求,要求用一个新标题更新狗的图像,将其从“dog”更改为“Gizmo”,但同时另一个客户端正在读取图像,则可能发生竞争情况。在这种情况下,不清楚第二个客户将收到哪个标题,“Dog”或“Gizmo”。
当然,分区数据存在一些障碍,但是分区允许将每个问题按照数据、负载、使用模式等分割为可管理的块。这有助于提高可扩展性和可管理性,但也不是没有风险。有很多方法可以降低风险和处理失败;但是,为了简洁起见,本章不讨论这些问题。
3. 快速和可扩展数据访问的构建块
在介绍了设计分布式系统时的一些核心考虑事项之后,现在让我们讨论比较困难的部分:扩展对数据的访问。
大多数简单的web应用程序,例如LAMP栈应用程序,如图1.5所示。
图1.5:简单的web应用程序
随着它们的增长,有两个主要的挑战:扩展对应用服务器和数据库的访问。在高度可伸缩的应用程序设计中,应用程序(或web)服务器通常是最小化的,并且通常包含无共享的体系结构。这使得系统的app服务器层可以水平伸缩。本设计的结果是将重物从栈下推到数据库服务器和支持服务上;正是在这一层,真正的伸缩性和性能挑战开始发挥作用。
本章的其余部分将专门讨论一些更常见的策略和方法,通过提供对数据的快速访问,使这些类型的服务快速且可伸缩。
图1.6:过于简化的web应用程序
大多数系统都可以过度简化为图1.6。这是一个很好的开始。如果你有很多数据,你想要快速便捷的访问,就像在你桌子的最上面抽屉里放一盒糖果。虽然过于简化,但是前面的陈述暗示了两个难题:存储的可伸缩性和数据的快速访问。
对于本节,假设您有许多TB的数据,并且希望允许用户随机访问其中的一小部分数据。(参见图1.7)。这类似于在图像应用程序示例中的文件服务器上定位一个图像文件。
这尤其具有挑战性,因为将几个TB的数据加载到内存中可能非常昂贵;这直接转换为磁盘IO。从磁盘读取要比从内存读取慢很多倍。对于大型数据集来说,这种速度差异实际上是累积起来的;在实际数字中,顺序读取的内存访问速度是磁盘读取的6倍,随机读取的内存访问速度是磁盘读取的10万倍。此外,即使使用惟一的id,要解决知道在何处查找少量数据的问题也是一项艰巨的任务。
值得庆幸的是,您可以使用许多选项来简化这一过程;其中比较重要的四个是缓存、代理、索引和负载平衡器。本节的其余部分将讨论如何使用这些概念使数据访问更快。
3.1 缓存
缓存利用了引用原则的局部性:最近请求的数据可能会被再次请求。它们几乎被用于计算的每一层:硬件、操作系统、web浏览器、web应用程序等等。缓存类似于短期内存:它的空间有限,但通常比原始数据源更快,并且包含最近访问的项。缓存可以存在于体系结构的所有级别,但通常位于最接近前端的级别,在那里实现缓存可以快速返回数据,而不会占用下游的级别。
在我们的API示例中,如何使用缓存加快数据访问速度?在本例中,有两个地方可以插入缓存。一个选项是在请求层节点上插入缓存,如图1.8所示。
图1.8:在请求层节点上插入缓存
将缓存直接放置在请求层节点上可以实现响应数据的本地存储。每次向服务发出请求时,如果存在,节点将快速返回本地缓存的数据。如果不在缓存中,请求节点将从磁盘查询数据。一个请求层节点上的缓存也可以位于内存(非常快)和节点的本地磁盘(比网络存储快)中。
图1.9:多个缓存
当把这个扩展到很多节点时会发生什么?正如图1.9中所看到的,如果请求层扩展到多个节点,那么每个节点仍然可以拥有自己的缓存。然而,如果负载均衡器在节点之间随机分配请求,那么相同的请求将被发送到不同的节点,从而增加缓存丢失。克服这个障碍的两个选择是全局缓存和分布式缓存。
3.2 全局缓存
全局缓存就像它听起来的那样:所有节点都使用相同的单个缓存空间。这涉及到添加服务器或某种类型的文件存储,速度比原始存储快,并且所有请求层节点都可以访问。每个请求节点以与本地节点相同的方式查询缓存。这种缓存方案可能有点复杂,因为它很容易被淹没当客户端和请求的数量增加,但在一些架构中非常有效(特别是那些专门的硬件,使这个全局缓存非常快,或者有固定的数据集需要缓存)。
下面两图描述了两种常见的全局缓存形式。在图1.10中,当缓存中没有找到缓存响应时,缓存本身将负责从相应的存储中检索缺失的数据块。在图1.11中,请求节点负责检索缓存中没有找到的任何数据。
图1.10:全局缓存,其中缓存负责检索 图1.11:全局缓存,其中请求节点负责检索
大多数利用全局缓存的应用程序倾向于使用第一种类型,其中缓存本身管理清除和获取数据,以防止客户机对相同数据的大量请求。然而,在某些情况下,第二个实现更有意义。例如,如果缓存用于非常大的文件,那么低的缓存命中率将导致缓存缓冲区被缓存丢失所淹没;在这种情况下,在缓存中保存大量的总数据集(或热数据集)是有帮助的。另一个例子是一个架构,其中缓存中存储的文件是静态的,不应该被删除。(这可能是因为应用程序对数据延迟的需求—对于大型数据集,某些数据块可能需要非常快—在这种情况下,应用程序逻辑比缓存更好地理解清除策略或热点。)
3.3 分布式缓存
在一个分布式缓存(图1.12),它的每个节点缓存数据的一部分,所以如果冰箱作为杂货店的缓存,分布式缓存就像把你的食物在几个位置--你的冰箱、橱柜,和午餐盒--方便的位置获取零食,没有去商店。通常,缓存使用一致的哈希函数进行划分,这样,如果请求节点正在寻找某段数据,它可以快速知道在分布式缓存中何处查找,以确定该数据是否可用。在本例中,每个节点都有一小部分缓存,在向源节点发送数据请求之前,将向一个缓存节点发送数据请求。因此,分布式缓存的优点之一是增加了缓存空间,只需要向请求池添加节点即可。
分布式缓存的一个缺点是修复丢失的节点。一些分布式缓存通过在不同的节点上存储数据的多个副本来解决这个问题;但是,您可以想象这种逻辑如何快速变得复杂,尤其是在从请求层添加或删除节点时。尽管即使节点消失并且部分缓存丢失,请求也只会从源文件中拉出—所以这并不一定是灾难性的!
图1.12:分布式缓存
缓存的好处是,它们通常使事情更快(当然是正确实现的!)你选择的方法只允许你更快地处理更多的请求。然而,所有这些缓存的代价是必须维护额外的存储空间,通常以昂贵的内存的形式出现;没有什么是免费的。缓存对于提高速度非常有用,而且在高负载条件下提供系统功能,否则将导致服务完全降级。
流行的开源缓存的一个例子是Memcached (http://memcached.org/)(它既可以作为本地缓存,也可以作为分布式缓存);然而,还有许多其他选项(包括许多特定于语言或框架的选项)。
Memcached用于许多大型web站点,尽管它非常强大,但它只是内存中的键值存储,针对任意数据存储和快速查找进行了优化(O(1))。
Facebook使用几种不同类型的缓存来获得站点性能。它们在语言级别使用$GLOBALS
和APC缓存(以函数调用为代价在PHP中提供),这有助于更快地进行中间函数调用和结果。(大多数语言都有这些类型的库来提高web页面性能,而且几乎总是应该使用它们。) 然后,Facebook使用一个分布在多个服务器上的全局缓存,这样一个访问缓存的函数调用就可以同时发出多个请求,以获取存储在不同memcached服务器上的数据。这使它们能够为用户信息数据获得更高的性能和吞吐量,并拥有一个更新数据的中心位置(这很重要,因为当您运行数千台服务器时,缓存失效和维护一致性可能会很有挑战性)。
现在让我们讨论一下当数据不在缓存中时该做什么……
3.4 代理
在基本级别上,代理服务器是接收来自客户机的请求并将其转发到后端源服务器的中间硬件/软件。通常,代理用于过滤请求、日志请求,有时还用于转换请求(通过添加/删除头、加密/解密或压缩)。
图1.13:代理服务
代理在协调来自多个服务器的请求时也非常有用,提供了从系统范围的角度优化请求流量的机会。使用代理加速数据访问的一种方法是将相同(或类似)请求合并为一个请求,然后将单个结果返回给请求客户机。这就是所谓的折叠转发。
假设有一个跨多个节点的对相同数据的请求(我们称之为littleB),而该数据块不在缓存中。如果将该请求路由到代理,那么所有这些请求都可以分解为一个请求,这意味着我们只需从磁盘中读取一次littleB。(参见图1.14)。这种设计会带来一些成本,因为每个请求可能会有稍微高的延迟,有些请求可能会稍微延迟,以便与类似的请求分组。但是在高负载的情况下,它将提高性能,特别是在反复请求相同的数据时。这类似于缓存,但它不是像缓存那样存储数据/文档,而是优化这些文档的请求或调用,并充当这些客户机的代理。
例如,在LAN代理中,客户端不需要自己的ip连接到Internet, LAN将折叠来自客户端对相同内容的调用。这里很容易混淆,因为许多代理也是缓存(因为它是放置缓存的逻辑位置),但是并不是所有缓存都充当代理。
图1.14:使用代理服务器折叠请求
使用代理的另一种好方法不仅是折叠相同数据的请求,还可以折叠源存储中空间上相邻的数据的请求(连续存储在磁盘上)。使用这种策略可以最大化请求的数据局部性,从而降低请求延迟。例如,假设一堆节点请求B的部件:partB1、partB2等。我们可以设置代理来识别单个请求的空间位置,将它们折叠成单个请求并只返回bigB,从而极大地减少对数据源的读取。(参见图1.15)。当您随机访问TB级数据时,这将在请求时间上产生非常大的差异!代理在高负载的情况下,或者在缓存有限的情况下尤其有用,因为它们本质上可以将多个请求批处理为一个请求。
图1.15:使用代理折叠空间上紧密相连的数据请求
值得注意的是,您可以同时使用代理和缓存,但是通常最好将缓存放在代理前面,这与在拥挤的马拉松比赛中让跑得更快的人先开始是相同的道理。这是因为缓存是为内存中的数据提供服务的,它非常快,并且不介意对同一个结果的多个请求。但是,如果缓存位于代理服务器的另一端,那么缓存之前的每个请求都会有额外的延迟,这可能会影响性能。
如果你正在考虑将代理添加到系统中,有许多选项可以考虑;Squid
和Varnish
都经过测试,并在许多生产网站上广泛使用。这些代理解决方案提供了许多优化,以充分利用客户机-服务器通信。在web服务器层将其中之一安装为反向代理(在下面的负载平衡器一节中进行了解释)可以显著提高web服务器性能,减少处理传入客户机请求所需的工作量。
3.5 索引
使用索引快速访问数据是优化数据访问性能的著名策略;可能是数据库中最著名的名词。索引可以在增加的存储开销和较慢的写操作之间进行权衡(因为必须同时写数据和更新索引),以获得更快的读取速度。
与传统的关系数据存储一样,您也可以将此概念应用于更大的数据集。索引的诀窍在于,您必须仔细考虑用户将如何访问您的数据。在数据集有很多TBs大小,但是有效负载非常小(例如,1kb)的情况下,索引是优化数据访问的必要条件。在这么大的数据集中找到一个小的有效负载可能是一个真正的挑战,因为您不可能在任何合理的时间内遍历那么多数据。此外,如此大的数据集很可能分布在多个(或多个)物理设备上—这意味着您需要某种方法来找到所需数据的正确物理位置。索引是最好的方法。
图1.16:索引
索引可以像目录表一样使用,将你引向数据所在的位置。例如,假设你正在寻找一段数据(B的part 2),你如何知道在哪里可以找到它?如果你有一个按数据类型排序的索引—比如数据A、B、C—它将告诉你数据B在原点的位置。然后你只需要找到那个位置,读你想读的B部分。(参见图1.16)。
这些索引通常存储在内存中,或者对传入的客户机请求非常邻近的某个地方。Berkeley DBs (BDBs)和树状数据结构通常用于将数据存储在有序列表中,非常适合使用索引访问。
通常有许多层索引充当映射,将你从一个位置移动到下一个位置,等等,直到你获得所需的特定数据。(参见图1.17)。
图1.17:多个索引层
索引还可以用于创建相同数据的多个不同视图。对于大型数据集,这是一种定义不同筛选器和排序的好方法,无需创建许多额外的数据副本。
例如,假设前面的图像托管系统实际上托管图书页面的图像,并且该服务允许客户端跨这些图像中的文本进行查询,搜索关于某个主题的所有图书内容,与搜索引擎允许搜索HTML内容的方式相同。在这种情况下,所有这些图书图像都需要许多服务器来存储文件,找到一个页面呈现给用户可能有点麻烦。首先,查询任意单词和单词元组的反向索引需要易于访问;接下来的挑战是导航到书中的准确页面和位置,并为结果检索正确的图像。因此,在本例中,反向索引将映射到一个位置(例如book B),然后B可能包含一个索引,其中包含每个部分中出现的所有单词、位置和次数。
反向索引(可以在上面的图中表示Index1)可能如下所示—每个单词或单词元组提供书籍包含它们的索引。
Word(s) | Book(s) |
---|---|
being awesome | Book B, Book C, Book D |
always | Book C, Book F |
believe | Book B |
中间索引看起来类似,但是只包含book b的单词、位置和信息。这种嵌套索引体系结构允许每个索引占用的空间比所有这些信息都必须存储在一个大的反向索引中少。
这在大型系统中是关键,因为即使压缩,这些索引也会变得非常大,而且存储起来非常昂贵。在这个系统中,如果我们假设世界上有很多书—1亿本---而且每本书只有10页长(为了简化计算),每页250个单词,这意味着有2500亿个单词。如果我们假设每个单词平均有5个字符,每个字符占用8位(或1个字节,即使有些字符是2字节),那么每个单词占用5个字节,那么一个只包含每个单词一次的索引就超过了1tb的存储空间。因此,你可以看到,创建包含许多其他信息的索引(比如单词元组、数据位置和出现次数)可以非常快地累加起来。
创建这些中间索引并将数据表示为更小的部分,使得大数据问题易于处理。数据可以分布在许多服务器上,并且仍然可以快速访问。索引是信息检索的基石,也是当今现代搜索引擎的基础。当然,这一节只触及了表面,关于如何使索引更小、更快、包含更多信息(比如相关性)和无缝更新,还有很多研究正在进行中。(竞争条件和添加新数据或更改现有数据所需的更新数量都存在一些可管理性挑战,特别是在涉及相关性或得分的事件中)。
能够快速、轻松地找到你的数据是很重要的;索引是实现这一目标的有效而简单的工具。
3.6 负载均衡器
最后,任何分布式系统的另一个关键部分是负载均衡器。负载均衡器是任何体系结构的主要部分,因为它们的角色是在负责服务请求的一组节点之间分配负载。这允许多个节点透明地为系统中的相同功能提供服务。(参见图1.18)。它们的主要目的是处理大量的并发连接,并将这些连接路由到一个请求节点,从而允许系统通过添加节点扩展来处理更多的请求。
图1.18:负载均衡器
可以使用许多不同的算法来为请求提供服务,包括随机选择节点、轮询,甚至根据特定的标准(如内存或CPU利用率)选择节点。负载均衡器可以实现为软件或硬件设备。一个被广泛采用的开源软件负载均衡器是HAProxy)。
在分布式系统中,负载均衡器通常位于系统的最前端,因此所有传入的请求都会相应地路由。在复杂的分布式系统中,请求路由到多个负载平衡器的情况并不少见,如图1.19所示。
图1.19:多个负载均衡器
与代理一样,一些负载均衡器也可以根据请求的类型以不同的方式路由请求。(从技术上讲,这些也称为反向代理。)
负载均衡器面临的挑战之一是管理特定于用户会话的数据。在一个电子商务网站,当你只有一个客户端很容易允许用户把东西放在购物车和保存这些内容之间的访问(这是很重要的,因为当顾客返回时如果这些商品仍然在他们的购物车中,你更有可能卖出这些商品)。但是,如果用户被路由到一个节点进行会话,然后在下次访问时路由到另一个节点,则可能会出现不一致性,因为新节点可能丢失了该用户的购物车内容。(如果你在购物车里放了6包山露水,然后回来发现里面是空的,你会高兴吗?)。解决这个问题的一种方法是使会话具有粘性,以便用户总是路由到相同的节点,但是这样就很难利用一些可靠性特性,比如自动故障转移。在这种情况下,用户的购物车总是有内容,但是如果他们的粘性节点不可用,那么就需要有一个特殊的情况,并且假设在其中的内容将不再有效(尽管希望这个假设不会构建到应用程序中)。当然,这个问题可以使用本章中的其他策略和工具来解决,比如服务,以及许多没有涉及到的(比如浏览器缓存、cookie和URL重写)。
如果一个系统只有几个节点,那么像轮询DNS这样的系统可能更有意义,因为负载均衡器可能很昂贵,并且增加了不必要的复杂性。当然,在较大的系统中有各种不同的调度和负载均衡算法,包括简单的随机选择或循环调度算法,以及考虑利用率和容量等因素的更复杂的机制。所有这些算法都允许分发流量和请求,并且可以提供有用的可靠性工具,比如自动故障转移或自动删除坏节点(比如当它变得无响应时)。然而,这些高级特性会使问题诊断变得很麻烦。例如,在高负载情况下,负载平衡器将删除可能很慢或超时的节点(由于请求太多),但这只会加剧其他节点的情况。在这些情况下,广泛的监测是很重要的,因为总体系统流量和吞吐量可能看起来在下降(因为节点服务的请求更少),但是单个节点已经达到最大。
负载平衡器是一种允许你扩展系统容量的简单方法,与本文中的其他技术一样,它在分布式系统体系结构中扮演着重要角色。负载均衡器还提供了能够测试节点健康状况的关键功能,这样,如果一个节点没有响应或负载过重,就可以从处理请求的池中删除它,从而利用系统中不同节点的冗余。
3.7 队列
到目前为止,我们已经介绍了许多快速读取数据的方法,但是扩展数据层的另一个重要部分是有效地管理写操作。当系统很简单,处理负载很小,数据库很小时,写操作的速度可以预见;然而,在更复杂的系统中,写操作几乎要花费很长时间。例如,数据可能必须写在不同服务器或索引的多个位置,或者系统可能只是处于高负载下。在写操作或任何相关任务可能需要很长时间的情况下,实现性能和可用性需要在系统中构建异步性;一种常见的方法是使用队列。
图1.20:同步请求
设想一个系统,其中每个客户端都请求一个任务进行远程服务。这些客户机中的每一个都将其请求发送到服务器,服务器在服务器上尽可能快地完成任务并将结果返回给各自的客户机。在小型系统中,一台服务器(或逻辑服务)可以像传入客户一端样快速地为它们提供服务,这种情况应该可以很好地工作。然而,当服务器接收到超过其处理能力的请求时,每个客户机都必须等待其他客户机的请求完成后才能生成响应。这是一个同步请求的例子,如图1.20所示。这种同步行为会严重降低客户端性能;客户端被迫等待,直到它的请求得到响应。增加额外的服务器来处理系统负载也不能解决这个问题;即使有了有效的负载平衡,也很难确保实现最大化客户性能所需的工作的均匀和公平分配。此外,如果处理请求的服务器不可用或失败,则上游客户机也会失败。有效地解决这个问题需要在客户端请求和为其提供服务的实际工作之间进行抽象。
图1.21:使用队列管理请求
输入队列。队列听起来很简单:一个任务进入队列,被添加到队列中,然后工作人员在他们有能力处理下一个任务时接手它。(参见图1.21)。这些任务可以表示对数据库的简单写入,也可以表示为文档生成缩略图预览图像这样复杂的操作。当客户端向队列提交任务请求时,他们不再被迫等待结果;相反,他们只需要确认请求已正确接收。当客户机需要时,这个确认稍后可以作为工作结果的参考。
队列使客户机能够以异步方式工作,提供了客户机请求及其响应的策略抽象。另一方面,在同步系统中,请求和应答之间没有区别,因此不能单独管理它们。在异步系统中,客户端请求一个任务,服务用一条确认任务已接收到的消息进行响应,然后客户端可以定期检查任务的状态,只在任务完成后请求结果。当客户机等待异步请求完成时,它可以自由地执行其他工作,甚至可以对其他服务发出异步请求。后者是分布式系统中如何利用队列和消息的一个例子。
队列还提供了一些防止服务中断和故障的保护。例如,创建一个高度健壮的队列非常容易,它可以重试由于临时服务器故障而失败的服务请求。使用队列执行服务的质量保证比直接将客户端暴露给间歇性服务中断要好得多,后者需要复杂且常常不一致的客户端错误处理。
队列是管理任何大型分布式系统的不同部分之间的分布式通信的基础,有很多方法可以实现它们。有相当多的开源队列,如RabbitMQ、ActiveMQ、BeanstalkD,但也有一些使用Zookeeper等服务,甚至使用Redis等数据存储。
4. 结论
设计能够快速访问大量数据的高效系统是令人兴奋的,而且有许多优秀的工具可以支持各种新应用程序。这一章只涉及了几个例子,仅仅触及了表面,但是还有很多,而且在这个领域还会有更多的创新。
网友评论