可扩展的Web架构和分布式系统
回复数(2) 浏览数(36)
Mr.D 03月08日 22:58 最后回复来自: Mr.D 学习
{{topic.upvote_count || 0}} 编辑 回复

1.1、Web分布式系统设计原则
可用性:网站的正常运行时间对许多公司的声誉和功能至关重要。对于一些较大的在线零售网站而言,连续几分钟不可用会导致数千或数百万美元的收入损失,因此设计他们的系统以便持续可用并且能够抵御故障,这既是基本业务,也是技术要求。分布式系统中的高可用性要求仔细考虑关键组件的冗余,部分系统故障时的快速恢复以及出现问题时的优雅降级。
性能:网站性能已成为大多数网站的重要考虑因素。网站的速度会影响使用率和用户满意度,以及搜索引擎排名,这是与收入和保留直接相关的因素。因此,创建一个针对快速响应和低延迟进行优化的系统是关键。
可靠性:系统需要是可靠的,这样对数据的请求才会一致地返回相同的数据。如果数据发生了更改或更新,那么相同的请求应该返回新的数据。用户需要知道,如果某个东西被写入或存储到系统中,那么它将持久存在,并且可以依赖于它的位置以便将来检索。
可扩展性:当涉及到任何大型分布式系统时,大小只是需要考虑的规模的一个方面。同样重要的是增加处理更大负载的容量所需的改动,通常称为系统的可扩展性。可伸缩性可以指系统的许多不同参数:它可以处理多少额外流量,添加更多存储容量的容易程度,甚至可以处理多少事务。
可管理性:设计易于操作的系统是另一个重要的考虑因素。系统的可管理性等同于操作的可扩展性:维护和更新。可管理性需要考虑的事项是在出现问题时易于诊断和理解问题,易于进行更新或修改,以及系统操作的简单性。(即它是否经常运行而没有失败或例外?)
成本:成本是一个重要因素。这显然可能包括硬件和软件成本,但考虑部署和维护系统所需的其他方面也很重要。系统构建所需的开发人员时间,运行系统所需的操作工作量,甚至所需的培训量都应予以考虑。

这些原则中的每一个都为设计分布式Web架构的决策提供了基础。然而,它们也可能彼此不一致,因此实现一个目标是以另一个目标为代价的。一个基本的例子:选择通过简单地添加更多服务器来解决容量(可扩展性)可能以可管理性(您必须运行额外的服务器)和成本(服务器的价格)为代价。
在设计任何类型的Web应用程序时,重要的是要考虑这些关键原则,即使是要承认设计可能会牺牲其中的一个或多个。
1.2、基础
在系统架构方面,有几点需要考虑:正确的部分是什么,这些部分如何组合在一起,以及什么是正确的权衡。在需要之前投资扩展通常不是一个明智的商业主张; 然而,对设计的一些深谋远虑可以在未来节省大量的时间和资源。

示例:图像托管应用程序
在某些时候,您可能已经在线发布了一张图片。对于托管和提供大量图像的大型站点,构建具有成本效益,高可用性和低延迟(快速检索)的架构存在挑战。

想象一下这样一个系统,用户可以将他们的图像上传到中央服务器,并且可以通过网络链接或API请求图像,就像Flickr或Picasa一样。为简单起见,我们假设此应用程序有两个关键部分:将图像上载(写入)到服务器的能力,以及查询图像的能力。虽然我们当然希望上传效率高,但我们最关心的是当有人请求图像时(例如,可以为网页或其他应用程序请求图像)快速交付。这与Web服务器或内容交付网络(CDN)边缘服务器(服务器CDN用于在许多位置存储内容,因此内容在地理上/物理上更接近用户,导致更快的性能)可能提供的功能非常相似。

该系统的其他重要方面是:

对于要存储的图像数量没有限制,因此需要考虑存储可扩展性,就图像数量而言。
图像下载/请求需要低延迟。
如果用户上传图像,则图像应始终存在(图像的数据可靠性)。
系统应易于维护(可管理性)。
由于图像托管的利润率不高,因此系统需要具有成本效益

image.png
image.png

在此图像托管示例中,系统必须具有可感知的快速,其数据可靠地存储,并且所有这些属性都具有高度可扩展性。构建此应用程序的小版本将是微不足道的,并且可以轻松地托管在单个服务器上; 然而,这章本章并不重要。让我们假设我们想要构建一些可以像Flickr一样大的东西。

服务
在考虑可扩展的系统设计时,它有助于解耦功能,并将系统的每个部分视为具有明确定义的接口的自己的服务。实际上,以这种方式设计的系统据说具有面向服务的体系结构(SOA)。对于这些类型的系统,每个服务都有自己独特的功能上下文,并且通过抽象接口(通常是另一个服务的面向公众的API)与该上下文之外的任何内容进行交互。

将系统解构为一组互补服务将这些部分的操作彼此分离。此抽象有助于在服务,其底层环境和该服务的使用者之间建立清晰的关系。创建这些清晰的描述可以帮助隔离问题,但也允许每个部分彼此独立地扩展。这种面向服务的系统设计非常类似于面向对象的编程设计。

在我们的示例中,所有上传和检索图像的请求都由同一服务器处理; 但是,由于系统需要扩展,因此将这两个功能分解为自己的服务是有意义的。

快进并假设该服务正在大量使用; 这样的场景使得很容易看出写入的时间会影响读取图像所需的时间(因为它们的两个功能将竞争共享资源)。根据架构,这种影响可能很大。即使上传和下载速度相同(大多数IP网络都不是这样,因为大多数设计的速度至少为3:1下载速度:上传速度比),通常会从缓存中读取读取文件,并且写入最终必须转到磁盘(并且可能在最终一致的情况下写入几次)。即使一切都在内存中或从磁盘(如SSD)读取,数据库写入几乎总是比读取慢。(Pole Position,一个用于数据库基准测试的开源工具, http://polepos.org/和结果 http://polepos.sourceforge.net/results/PolePositionClientServer.pdf。)。

这种设计的另一个潜在问题是,像Apache或lighttpd这样的Web服务器通常对它可以维护的同时连接数量有一个上限(默认值大约为500,但可以更高),而在高流量时,写入可以快速消耗所有这些。由于读取可以是异步的,或者利用其他性能优化(如gzip压缩或分块传输编码),因此Web服务器可以更快地切换服务读取并在客户端之间切换,每秒快速提供比最大连接数更多的请求(使用Apache和最大连接数设置为500,每秒提供数千个读取请求并不罕见。另一方面,写入往往在上载期间保持开放连接,

image.png
image.png

规划这种瓶颈可以很好地将图像的读写分成自己的服务,如图1.2所示。这允许我们独立地扩展每个(因为我们可能总是会比写作更多地阅读),但也有助于澄清每个点上发生的事情。最后,这将把未来的问题分开,这样可以更容易地进行故障排除和扩展问题,例如慢速读取。

这种方法的优点是我们能够独立地解决问题 - 我们不必担心在相同的上下文中编写和检索新图像。这两种服务仍然利用全局图像语料库,但是它们可以使用适合服务的方法(例如,排队请求或缓存流行图像 - 以下更多内容)自由优化自己的性能。从维护和成本的角度来看,每个服务可以根据需要独立扩展,这很好,因为如果它们被组合和混合,则可能无意中影响另一个服务的性能,就像上面讨论的场景一样。

当然,当您有两个不同的端点时,上面的示例可以很好地工作(实际上这与几个云存储提供商的实现和内容交付网络非常相似)。有很多方法可以解决这些类型的瓶颈,每种方法都有不同的权衡。

例如,Flickr通过在不同的分片中分配用户来解决这个读/写问题,这样每个分片只能处理一定数量的用户,并且随着用户的增加,更多的分片被添加到集群中(参见Flickr缩放的演示文稿, http: //mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html)。在第一个示例中,根据实际使用情况(整个系统中的读取和写入次数)更容易扩展硬件,而Flickr随其用户群进行扩展(但强制假设用户具有相同的使用率,因此可以有额外的容量)。在前者中,其中一个服务的中断或问题会降低整个系统的功能(例如,没有人可以写文件),而使用Flickr的一个分片的中断只会影响这些用户。在第一个示例中,更容易在整个数据集中执行操作 - 例如,

当涉及到这些系统时,没有正确的答案,但它有助于回到本章开头的原则,确定系统需求(重读或写入或两者,并发级别,跨数据集的查询,范围,排序等),对不同的替代方案进行基准测试,了解系统将如何失败,并对失败发生时制定可靠的计划。

冗余
为了优雅地处理故障,Web体系结构必须具有其服务和数据的冗余。例如,如果单个服务器上只存储一个文件副本,则丢失该服务器意味着丢失该文件。丢失数据很少是一件好事,处理它的常用方法是创建多个或多余的副本。

同样的原则也适用于服务。如果应用程序具有核心功能,则确保同时运行多个副本或版本可以防止单个节点发生故障。

在系统中创建冗余可以消除单点故障,并在危机中根据需要提供备用或备用功能。例如,如果在生产中运行同一服务的两个实例,并且一个实例出现故障或降级,则系统可以故障转移 到正常副本。故障转移可以自动发生或需要手动干预。

服务冗余的另一个关键部分是创建无共享架构。通过这种架构,每个节点能够彼此独立地操作,并且没有中央“大脑”管理状态或协调其他节点的活动。这对可扩展性有很大帮助,因为可以在没有特殊条件或知识的情况下添加新节点。但是,最重要的是,这些系统中没有单点故障,因此它们对故障更具弹性。

例如,在我们的图像服务器应用程序中,所有图像都会在某个地方的另一块硬件上具有冗余副本(理想情况下,如果发生地震或数据中心火灾等灾难,则位于不同的地理位置),以及要访问的服务图像将是多余的,所有可能的服务请求。(参见 图1.3。)(负载平衡器是实现这一目标的好方法,但下面还有更多内容)。

image.png
image.png

分区
可能存在无法容纳在单个服务器上的非常大的数据集。也可能是操作需要太多计算资源,性能降低并且需要增加容量的情况。在任何一种情况下,您都有两种选择:垂直或水平缩放。

垂直扩展意味着向单个服务器添加更多资源。因此,对于非常大的数据集,这可能意味着添加更多(或更大)的硬盘驱动器,因此单个服务器可以包含整个数据集。在计算操作的情况下,这可能意味着将计算移动到具有更快CPU或更多内存的更大服务器。在每种情况下,垂直缩放是通过使单个资源能够自己处理更多来完成的。

另一方面,水平缩放是添加更多节点。在大数据集的情况下,这可能是存储数据集的部分的第二服务器,并且对于计算资源,这将意味着在一些附加节点上分割操作或加载。为了充分利用水平扩展,它应该作为系统体系结构的内在设计原则包含在内,否则修改和分离上下文以使其成为可能会非常麻烦。

在涉及水平扩展时,一种比较常见的技术是将服务分解为分区或分片。可以分布分区,使得每个逻辑功能集是分开的; 这可以通过地理边界或其他标准来完成,例如非付费用户和付费用户。这些方案的优点是它们提供具有增加容量的服务或数据存储。

在我们的图像服务器示例中,用于存储图像的单个文件服务器可能被多个文件服务器替换,每个文件服务器包含其自己唯一的图像集。(参见图1.4。)这样的体系结构将允许系统用图像填充每个文件服务器,在磁盘变满时添加额外的服务器。该设计需要一个命名方案,将图像的文件名绑定到包含它的服务器。图像的名称可以由跨服务器映射的一致哈希方案形成。或者,可以为每个图像分配增量ID,以便当客户端请求图像时,图像检索服务仅需要维护映射到每个服务器的ID范围(如索引)。

image.png
image.png

当然,跨多个服务器分发数据或功能存在挑战。其中一个关键问题是数据局部性 ; 在分布式系统中,数据越接近操作或计算点,系统的性能越好。因此,将数据分布在多个服务器上可能存在问题,因为在需要它的任何时候它可能不是本地的,迫使服务器在网络上执行昂贵的所需信息提取。

另一个潜在的问题是不一致的形式 。当从共享资源(可能是另一个服务或数据存储)读取和写入不同的服务时,存在竞争条件的可能性 - 其中一些数据应该被更新,但是在更新之前发生读取 - 并且在那些情况下数据不一致。例如,在图像托管方案中,如果一个客户端发送了使用新标题更新狗图像的请求,将其从“狗”更改为“Gizmo”,但同时另一个客户端正在阅读,则可能发生竞争情况图片。在那种情况下,不清楚哪个标题“狗”或“Gizmo”将是第二个客户收到的标题。

分区数据肯定存在一些障碍,但分区允许将每个问题按数据,负载,使用模式等分成可管理的块。这有助于提高可扩展性和可管理性,但并非没有风险。有很多方法可以降低风险并处理故障; 但是,为了简洁起见,本章不涉及它们。如果您有兴趣阅读更多内容,可以查看我的博客文章, 了解容错和监控。

1.3、快速和可扩展数据访问的构建块
在介绍了设计分布式系统时的一些核心考虑因素之后,我们现在谈谈困难的部分:扩展对数据的访问。

大多数简单的Web应用程序,例如LAMP堆栈应用程序,如图1.5所示。

image.png
image.png

图1.5:简单的Web应用程序
随着它们的发展,存在两个主要挑战:扩展对应用服务器和数据库的访问。在高度可扩展的应用程序设计中,应用程序(或Web)服务器通常被最小化,并且通常体现为无共享体系结构。这使得系统的app服务器层可以水平扩展。由于这种设计,繁重的工作被压缩到数据库服务器和支持服务; 在这一层,真正的扩展和性能挑战发挥作用。

本章的其余部分致力于通过提供对数据的快速访问,使这些类型的服务快速且可扩展的一些更常见的策略和方法。

image.png
image.png

大多数系统都可以简化为图1.6。这是一个很好的起点。如果您有大量数据,则需要快速方便地访问,例如在桌面的顶部抽屉中保存糖果。虽然过于简化,但之前的声明暗示了两个难题:存储的可扩展性和数据的快速访问。

为了本节的目的,我们假设您有许多TB的数据,并且您希望允许用户随机访问该数据的一小部分。(参见图1.7。)这类似于在图像应用程序示例中在文件服务器上的某处定位图像文件。

image.png
image.png

图1.7:访问特定数据
这特别具有挑战性,因为将TB的数据加载到存储器中可能非常昂贵; 这直接转换为磁盘IO。从磁盘读取速度比从存储器读取速度快几倍,与Chuck Norris一样快,而磁盘访问速度比DMV速度慢。这种速度差异实际上增加了大数据集; 实际上,对于顺序读取,内存访问速度比从磁盘读取速度快6倍,对于随机读取速度快10万倍(参见“大数据病理学”,http://queue.acm.org/detail。 cfm?id = 1563874)。而且,即使有唯一的ID,解决知道在哪里找到这么少的数据的问题也是一项艰巨的任务。这就像试图从你的糖果藏匿中获取最后一个Jolly Rancher而不看。

值得庆幸的是,您可以使用许多选项来简化这一过程; 其中四个比较重要的是缓存,代理,索引和负载均衡器。本节的其余部分讨论了如何使用这些概念中的每一个来更快地进行数据访问。

高速缓存
高速缓存利用了参考原理的局部性:可能再次请求最近请求的数据。它们几乎用于每个计算层:硬件,操作系统,Web浏览器,Web应用程序等。缓存就像短期内存:它具有有限的空间,但通常比原始数据源更快,并包含最近访问的项目。高速缓存可以存在于体系结构的所有级别,但通常位于最靠近前端的级别,在那里它们被实现为快速返回数据而不会对下游级别产生负担。

在我们的API示例中,如何使用缓存来加快数据访问速度?在这种情况下,有几个地方可以插入缓存。一种选择是在请求层节点上插入缓存, 如图1.8所示。

image.png
image.png

图1.8:在请求层节点上插入缓存
直接在请求层节点上放置缓存可以本地存储响应数据。每次向服务发出请求时,节点将快速返回本地缓存数据(如果存在)。如果它不在缓存中,请求节点将从磁盘查询数据。一个请求层节点上的缓存也可以位于内存(非常快)和节点的本地磁盘上(比进入网络存储更快)。

image.png
image.png

将此扩展到多个节点时会发生什么?正如你可以看到图1.9,如果该请求层扩展到多个节点,它仍然是完全有可能的每个节点托管其自己的高速缓存。但是,如果负载均衡器在节点之间随机分配请求,则相同的请求将转到不同的节点,从而增加了缓存未命中。克服这个障碍的两个选择是全局缓存和分布式缓存。

全局缓存
全局缓存就像它听起来一样:所有节点都使用相同的单个缓存空间。这涉及添加某种服务器或某种文件存储,比原始存储更快,并且可由所有请求层节点访问。每个请求节点以与本地节点相同的方式查询缓存。这种缓存方案可能会变得有点复杂,因为随着客户端和请求数量的增加,很容易压倒单个缓存,但在某些体系结构中非常有效(特别是那些使用这种全局缓存非常快的专用硬件,或者有一个需要缓存的固定数据集)。

图中描述了两种常见的全局缓存形式。在图1.10中,当在缓存中找不到缓存的响应时,缓存本身负责从底层存储中检索丢失的数据。在图1.11中 ,请求节点负责检索缓存中找不到的任何数据。

image.png
image.png

图1.10:缓存负责检索的全局缓存

image.png
image.png

图1.11:请求节点负责检索的全局缓存
利用全局缓存的大多数应用程序倾向于使用第一种类型,其中缓存本身管理逐出和获取数据以防止来自客户端的相同数据的大量请求。但是,在某些情况下,第二种实现更有意义。例如,如果缓存用于非常大的文件,则低缓存命中百分比将导致缓存缓冲区因缓存未命中而变得不堪重负; 在这种情况下,它有助于在缓存中占据总数据集(或热数据集)的大部分。另一个例子是一种架构,其中存储在缓存中的文件是静态的,不应该被驱逐。

分布式缓存
在分布式缓存(图1.12)中,每个节点都拥有缓存数据的一部分,因此如果冰箱充当杂货店的缓存,分布式缓存就像将食物放在几个位置 - 冰箱,橱柜,和午餐盒 - 方便的地点,可以从商店取回小吃。通常,使用一致的散列函数来划分高速缓存,使得如果请求节点正在寻找某个数据片段,则它可以快速知道在分布式高速缓存中的哪个位置以确定该数据是否可用。在这种情况下,每个节点都有一小部分缓存,然后在转到源之前向另一个节点发送数据请求。因此,分布式缓存的一个优点是增加了缓存空间,只需将节点添加到请求池即可。

分布式缓存的缺点是修复丢失的节点。一些分布式缓存通过在不同节点上存储多个数据副本来解决这个问题。但是,您可以想象这种逻辑如何快速复杂化,尤其是在您从请求层添加或删除节点时。虽然即使节点消失并且部分缓存丢失,但请求只会从原点拉出 - 所以它不一定是灾难性的!

image.png
image.png

图1.12:分布式缓存
关于缓存的好处在于它们通常会使事情变得更快(当然,正确实现!)您选择的方法只允许您更快地处理更多请求。然而,所有这些缓存都需要以维持额外的存储空间为代价,通常是以昂贵的内存形式; 没有什么是免费的 高速缓存非常适合于使事情变得更快,而且在高负载条件下提供系统功能,否则会出现完全的服务降级。

流行的开源缓存的一个例子是Memcached(http://memcached.org/)(它可以作为本地缓存和分布式缓存工作); 但是,还有许多其他选项(包括许多特定于语言或框架的选项)。

Memcached用于许多大型网站,即使它非常强大,它只是一个内存中的键值存储,针对任意数据存储和快速查找进行了优化(O(1))。

Facebook使用几种不同类型的缓存来获取其网站性能(请参阅“ Facebook缓存和性能”)。他们$GLOBALS在语言级别使用和APC缓存(以函数调用为代价在PHP中提供),这有助于使中间函数调用和结果更快。(大多数语言都有这些类型的库来提高网页性能,它们应该几乎总是被使用。)Facebook然后使用分布在许多服务器上的全局缓存(参见“ 在Facebook上扩展memcached”)),这样一个访问缓存的函数调用可以为存储在不同Memcached服务器上的数据并行地发出许多请求。这使他们能够为其用户配置文件数据获得更高的性能和吞吐量,并且有一个更新数据的中心位置(这很重要,因为当您运行数千台服务器时,缓存失效和维护一致性可能具有挑战性)。
代理
在基本级别,代理服务器是一个中间件硬件/软件,它接收来自客户端的请求并将它们中继到后端源服务器。通常,代理用于过滤请求,记录请求或有时转换请求(通过添加/删除标头,加密/解密或压缩)。

image.png
image.png

图1.13:代理服务器
在协调来自多个服务器的请求时,代理也非常有用,从整个系统的角度提供优化请求流量的机会。使用代理加速数据访问的一种方法是将相同(或类似)请求一起折叠为一个请求,然后将单个结果返回给请求客户端。这称为折叠转发。

想象一下,在几个节点上存在对相同数据的请求(让我们称之为littleB),并且该数据不在缓存中。如果该请求被认为是代理路由,那么所有这些请求都可以折叠成一个,这意味着我们只需要读取一次磁盘上的littleB。(参见图1.14。)此设计存在一些成本,因为每个请求的延迟可能略高,而某些请求可能会稍微延迟以与类似的请求分组。但它会提高高负载情况下的性能,特别是在反复请求相同数据时。这类似于缓存,但它不是像缓存那样存储数据/文档,而是优化对这些文档的请求或调用,并充当这些客户端的代理。

例如,在LAN代理中,客户端不需要自己的IP来连接到Internet,LAN将折叠来自客户端的相同内容的呼叫。这里很容易混淆,因为许多代理也是缓存(因为它是放置缓存的一个非常合理的位置),但并非所有缓存都充当代理。

image.png
image.png

使用代理的另一个好方法是不仅要折叠对相同数据的请求,还要折叠对原始存储中空间上靠近的数据的请求(连续地在磁盘上)。采用这种策略可以最大化请求的数据位置,从而减少请求延迟。例如,假设一堆节点请求B的部分:partB1,partB2等。我们可以设置我们的代理来识别各个请求的空间局部性,将它们折叠成单个请求并仅返回bigB,从而大大减少了从数据源读取。(见图1.15。)当您随机访问跨TB数据时,这会对请求时间产生很大的影响!代理在高负载情况下或缓存有限时特别有用,因为它们实际上可以将多个请求合并为一个。

image.png
image.png

值得注意的是,您可以将代理和缓存一起使用,但通常最好将缓存放在代理服务器前面,原因与最好让速度较快的跑步者在拥挤的马拉松比赛中首先启动一样。这是因为缓存正在从内存中提供数据,速度非常快,并且不介意对同一结果的多个请求。但是如果缓存位于代理服务器的另一端,那么缓存之前的每个请求都会有额外的延迟,这可能会影响性能。

如果您正在考虑为系统添加代理,可以考虑许多选项; Squid和 Varnish都经过道路测试,并广泛用于许多生产网站。这些代理解决方案提供了许多优化,以充分利用客户端 - 服务器通信。在Web服务器层安装其中一个作为反向代理(在下面的负载平衡器部分中解释)可以显着提高Web服务器性能,减少处理传入客户端请求所需的工作量。

索引
使用索引快速访问数据是优化数据访问性能的众所周知的策略; 可能是最知名的数据库。索引使得存储开销增加和写入速度变慢(因为您必须同时写入数据并更新索引)以获得更快的读取。

就传统的关系数据存储而言,您也可以将此概念应用于更大的数据集。索引的技巧是您必须仔细考虑用户将如何访问您的数据。在数据集大小为TB但具有非常小的有效载荷(例如,1KB)的情况下,索引是优化数据访问的必要条件。在如此大的数据集中查找小的有效负载可能是一个真正的挑战,因为您无法在任何合理的时间内迭代那么多数据。此外,很可能这么大的数据集分布在几个(或许多!)物理设备上 - 这意味着您需要某种方法来找到所需数据的正确物理位置。索引是执行此操作的最佳方式。

image.png
image.png

索引可以像目录一样用于引导您到达数据所在的位置。例如,假设您正在寻找一条数据,B部分的第2部分 - 您如何知道在何处找到它?如果您有一个按数据类型排序的索引 - 比如数据A,B,C-它会告诉您原点的数据B的位置。然后你只需要寻找那个位置并阅读你想要的B部分。(见图1.16。)

这些索引通常存储在内存中,或者存储在传入客户端请求的本地。Berkeley DB(BDB)和树状数据结构通常用于在有序列表中存储数据,非常适合使用索引进行访问。

通常有许多索引层用作地图,将您从一个位置移动到下一个位置,依此类推,直到您获得所需的特定数据。(见图1.17。)

image.png
image.png

图1.17:多层索引
索引还可用于创建相同数据的多个不同视图。对于大型数据集,这是定义不同过滤器和排序的好方法,而无需创建许多额外的数据副本。

例如,假设前面的图像托管系统实际上托管了图书页面的图像,并且该服务允许客户端查询这些图像中的文本,搜索关于某个主题的所有图书内容,就像搜索引擎允许您一样搜索HTML内容。在这种情况下,所有这些书籍图像都需要许多服务器来存储文件,并且找到一个要呈现给用户的页面可能有点牵扯。首先,需要易于访问查询任意单词和单词元组的反向索引; 然后有一个挑战是导航到该书中的确切页面和位置,并检索结果的正确图像。因此,在这种情况下,反向索引将映射到位置(例如书B),然后B可以包含具有每个部分中的所有单词,位置和出现次数的索引。

可以在上图中表示Index1的倒排索引可能看起来像下面这样 - 每个单词或单词的元组提供书籍包含它们的索引。
Word(s) Book(s)
being awesome Book B, Book C, Book D
always Book C, Book F
believe Book B
中间索引看起来很相似,但只包含书B的单词,位置和信息。这种嵌套索引体系结构允许每个索引占用的空间比所有信息都必须存储到一个大的倒排索引中的空间要小。

这对于大规模系统来说至关重要,因为即使是压缩,这些索引也会变得非常庞大且存储成本也很高。在这个系统中,如果我们假设我们拥有世界上很多书籍 - 100,000,000(参见Inside Google Books博客文章) - 并且每本书只有10页长(为了使数学更容易),每页250字,这意味着有2500亿字。如果我们假设每个字平均有5个字符,并且每个字符占用8位(或1个字节,即使某些字符是2个字节),那么每个字5个字节,那么只包含每个字一次的索引超过1TB存储。因此,您可以看到创建具有许多其他信息的索引,例如单词元组,数据位置和出现次数,可以非常快速地加起来。

创建这些中间索引并以较小的部分表示数据可以使大数据问题易于处理。数据可以分布在许多服务器上,并且仍然可以快速访问 索引是信息检索的基石,也是当今现代搜索引擎的基础。当然,本节仅涉及表面,并且正在进行大量研究,以便如何使索引更小,更快,包含更多信息(如相关性),并无缝更新。(竞争条件存在一些可管理性挑战,以及添加新数据或更改现有数据所需的大量更新,特别是在涉及相关性或评分的情况下)。

能够快速,轻松地找到您的数据非常重要; 索引是实现这一目标的有效而简单的工具。

负载均衡器
最后,任何分布式系统的另一个关键部分是负载均衡器。负载平衡器是任何体系结构的主要部分,因为它们的作用是在负责服务请求的一组节点之间分配负载。这允许多个节点透明地为系统中的相同功能提供服务。(参见图1.18。)它们的主要目的是处理大量并发连接并将这些连接路由到其中一个请求节点,从而允许系统通过添加节点来扩展以服务更多请求。

image.png
image.png

有许多不同的算法可用于服务请求,包括挑选随机节点,循环,甚至根据某些标准选择节点,例如内存或CPU利用率。负载平衡器可以实现为软件或硬件设备。一个广泛采用的开源软件负载均衡器是 HAProxy)。

在分布式系统中,负载平衡器通常位于系统的最前端,以便相应地路由所有传入请求。在复杂的分布式系统中,将请求路由到多个负载均衡器的情况并不少见,如图1.19所示 。

image.png
image.png

与代理一样,某些负载均衡器也可以根据请求的类型不同地路由请求。(从技术上讲,这些也称为反向代理。)

负载平衡器面临的挑战之一是管理特定于用户会话的数据。在电子商务网站中,当您只有一个客户端时,很容易让用户将内容放入购物车并在访问之间保留这些内容(这很重要,因为如果您出售产品,则更有可能它们返回时仍然在用户的购物车中)。但是,如果用户被路由到一个节点进行会话,然后在下次访问时被路由到另一个节点,则可能存在不一致,因为新节点可能缺少该用户的购物车内容。(如果你把6包Mountain Dew放在你的购物车然后回来并且它是空的,你不会感到沮丧吗?)解决这个问题的方法之一就是让会话变得粘稠,这样用户总是被路由到同一个节点,但是很难利用自动故障转移等可靠性功能。在这种情况下,用户的购物车将始终具有内容,但是如果他们的粘性节点变得不可用,则需要特殊情况并且内容的假设将不再有效(尽管希望这个假设不会被内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。但是如果他们的粘性节点变得不可用,则需要有一个特殊情况,并且内容的假设将不再有效(尽管希望这个假设不会内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。但是如果他们的粘性节点变得不可用,则需要有一个特殊情况,并且内容的假设将不再有效(尽管希望这个假设不会内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。

如果系统只有几个节点,那么像循环DNS这样的系统可能会更有意义,因为负载均衡器可能很昂贵,并且增加了不必要的复杂层。当然,在较大的系统中,存在各种不同的调度和负载平衡算法,包括诸如随机选择或循环的简单算法,以及考虑利用率和容量之类的更复杂的机制。所有这些算法都允许分发流量和请求,并且可以提供有用的可靠性工具,如自动故障转移或自动删除坏节点(例如,当它变得无响应时)。但是,这些高级功能可能会使问题诊断变得繁琐。例如,当涉及高负载情况时,负载平衡器将删除可能很慢或超时的节点(因为请求太多),但这只会加剧其他节点的情况。在这些情况下,广泛的监控很重要,因为整体系统流量和吞吐量可能看起来正在下降(因为节点服务的请求较少),但各个节点正在变得最大化。

负载平衡器是一种允许您扩展系统容量的简单方法,与本文中的其他技术一样,在分布式系统架构中发挥着至关重要的作用。负载平衡器还提供了能够测试节点运行状况的关键功能,这样,如果节点没有响应或过载,可以从池处理请求中删除它,利用您的不同节点的冗余系统。

队列
到目前为止,我们已经介绍了很多快速读取数据的方法,但扩展数据层的另一个重要部分是有效的写入管理。当系统简单,处理负载最小且数据库较小时,写入速度可以预测很快; 但是,在更复杂的系统中,写入可能需要几乎不确定的长时间。例如,可能必须将数据写入不同服务器或索引上的多个位置,否则系统可能处于高负载状态。在写入或任何相关任务可能需要很长时间的情况下,实现性能和可用性需要在系统中建立异步; 一种常见的方法是使用队列。

image.png
image.png

想象一个系统,每个客户端都要求远程服务任务。这些客户端中的每一个都将其请求发送到服务器,服务器尽快完成任务并将结果返回给各自的客户端。在小型系统中,一台服务器(或逻辑服务)可以像它们一样快地为传入的客户端提供服务,这种情况应该可以正常工作。但是,当服务器接收到的请求数超出其处理能力时,则会强制每个客户端等待其他客户端的请求完成,然后才能生成响应。这是同步请求的示例,如图1.20所示。

这种同步行为会严重降低客户端性能; 客户端被迫等待,有效地执行零工作,直到其请求得到解答。添加额外的服务器以解决系统负载也无法解决问题; 即使有效的负载平衡到位,也很难确保最大化客户绩效所需的工作均匀公平分配。此外,如果服务器处理请求不可用或失败,则上游客户端也将失败。有效地解决这个问题需要在客户端的请求和为服务它而执行的实际工作之间进行抽象。

image.png
image.png

图1.21:使用队列管理请求
输入队列。队列就像听起来一样简单:任务进来,被添加到队列中,然后工作人员接受下一个任务,因为他们有能力处理它。(参见图1.21。)这些任务可以表示对数据库的简单写入,或者像为文档生成缩略图预览图像那样复杂的任务。当客户端向队列提交任务请求时,他们不再被迫等待结果; 相反,他们只需要确认请求已被正确接收。此确认可以在以后作为客户需要时工作结果的参考。

队列使客户端能够以异步方式工作,提供客户端请求及其响应的战略抽象。另一方面,在同步系统中,请求和回复之间没有区别,因此它们不能单独管理。在异步系统中,客户端请求任务,服务以确认收到任务的消息进行响应,然后客户端可以定期检查任务的状态,仅在结果完成后请求结果。当客户端等待异步请求完成时,它可以自由地执行其他工作,甚至可以对其他服务进行异步请求。后者是如何在分布式系统中利用队列和消息的示例。

队列还提供一些服务中断和故障保护。例如,创建一个高度健壮的队列非常容易,该队列可以重试因瞬态服务器故障而失败的服务请求。最好使用队列来强制执行服务质量保证,而不是直接将客户端暴露给间歇性服务中断,这需要复杂且经常不一致的客户端错误处理。

队列是管理任何大型分布式系统的不同部分之间的分布式通信的基础,并且有许多方法来实现它们。有很多开源队列,如RabbitMQ, ActiveMQ, BeanstalkD,但有些也使用像Zookeeper这样的服务 ,甚至像Redis这样的数据存储。

{{topic.upvote_count || 0}}

1.1、Web分布式系统设计原则
可用性:网站的正常运行时间对许多公司的声誉和功能至关重要。对于一些较大的在线零售网站而言,连续几分钟不可用会导致数千或数百万美元的收入损失,因此设计他们的系统以便持续可用并且能够抵御故障,这既是基本业务,也是技术要求。分布式系统中的高可用性要求仔细考虑关键组件的冗余,部分系统故障时的快速恢复以及出现问题时的优雅降级。
性能:网站性能已成为大多数网站的重要考虑因素。网站的速度会影响使用率和用户满意度,以及搜索引擎排名,这是与收入和保留直接相关的因素。因此,创建一个针对快速响应和低延迟进行优化的系统是关键。
可靠性:系统需要是可靠的,这样对数据的请求才会一致地返回相同的数据。如果数据发生了更改或更新,那么相同的请求应该返回新的数据。用户需要知道,如果某个东西被写入或存储到系统中,那么它将持久存在,并且可以依赖于它的位置以便将来检索。
可扩展性:当涉及到任何大型分布式系统时,大小只是需要考虑的规模的一个方面。同样重要的是增加处理更大负载的容量所需的改动,通常称为系统的可扩展性。可伸缩性可以指系统的许多不同参数:它可以处理多少额外流量,添加更多存储容量的容易程度,甚至可以处理多少事务。
可管理性:设计易于操作的系统是另一个重要的考虑因素。系统的可管理性等同于操作的可扩展性:维护和更新。可管理性需要考虑的事项是在出现问题时易于诊断和理解问题,易于进行更新或修改,以及系统操作的简单性。(即它是否经常运行而没有失败或例外?)
成本:成本是一个重要因素。这显然可能包括硬件和软件成本,但考虑部署和维护系统所需的其他方面也很重要。系统构建所需的开发人员时间,运行系统所需的操作工作量,甚至所需的培训量都应予以考虑。

这些原则中的每一个都为设计分布式Web架构的决策提供了基础。然而,它们也可能彼此不一致,因此实现一个目标是以另一个目标为代价的。一个基本的例子:选择通过简单地添加更多服务器来解决容量(可扩展性)可能以可管理性(您必须运行额外的服务器)和成本(服务器的价格)为代价。
在设计任何类型的Web应用程序时,重要的是要考虑这些关键原则,即使是要承认设计可能会牺牲其中的一个或多个。
1.2、基础
在系统架构方面,有几点需要考虑:正确的部分是什么,这些部分如何组合在一起,以及什么是正确的权衡。在需要之前投资扩展通常不是一个明智的商业主张; 然而,对设计的一些深谋远虑可以在未来节省大量的时间和资源。

示例:图像托管应用程序
在某些时候,您可能已经在线发布了一张图片。对于托管和提供大量图像的大型站点,构建具有成本效益,高可用性和低延迟(快速检索)的架构存在挑战。

想象一下这样一个系统,用户可以将他们的图像上传到中央服务器,并且可以通过网络链接或API请求图像,就像Flickr或Picasa一样。为简单起见,我们假设此应用程序有两个关键部分:将图像上载(写入)到服务器的能力,以及查询图像的能力。虽然我们当然希望上传效率高,但我们最关心的是当有人请求图像时(例如,可以为网页或其他应用程序请求图像)快速交付。这与Web服务器或内容交付网络(CDN)边缘服务器(服务器CDN用于在许多位置存储内容,因此内容在地理上/物理上更接近用户,导致更快的性能)可能提供的功能非常相似。

该系统的其他重要方面是:

对于要存储的图像数量没有限制,因此需要考虑存储可扩展性,就图像数量而言。
图像下载/请求需要低延迟。
如果用户上传图像,则图像应始终存在(图像的数据可靠性)。
系统应易于维护(可管理性)。
由于图像托管的利润率不高,因此系统需要具有成本效益

image.png
image.png

在此图像托管示例中,系统必须具有可感知的快速,其数据可靠地存储,并且所有这些属性都具有高度可扩展性。构建此应用程序的小版本将是微不足道的,并且可以轻松地托管在单个服务器上; 然而,这章本章并不重要。让我们假设我们想要构建一些可以像Flickr一样大的东西。

服务
在考虑可扩展的系统设计时,它有助于解耦功能,并将系统的每个部分视为具有明确定义的接口的自己的服务。实际上,以这种方式设计的系统据说具有面向服务的体系结构(SOA)。对于这些类型的系统,每个服务都有自己独特的功能上下文,并且通过抽象接口(通常是另一个服务的面向公众的API)与该上下文之外的任何内容进行交互。

将系统解构为一组互补服务将这些部分的操作彼此分离。此抽象有助于在服务,其底层环境和该服务的使用者之间建立清晰的关系。创建这些清晰的描述可以帮助隔离问题,但也允许每个部分彼此独立地扩展。这种面向服务的系统设计非常类似于面向对象的编程设计。

在我们的示例中,所有上传和检索图像的请求都由同一服务器处理; 但是,由于系统需要扩展,因此将这两个功能分解为自己的服务是有意义的。

快进并假设该服务正在大量使用; 这样的场景使得很容易看出写入的时间会影响读取图像所需的时间(因为它们的两个功能将竞争共享资源)。根据架构,这种影响可能很大。即使上传和下载速度相同(大多数IP网络都不是这样,因为大多数设计的速度至少为3:1下载速度:上传速度比),通常会从缓存中读取读取文件,并且写入最终必须转到磁盘(并且可能在最终一致的情况下写入几次)。即使一切都在内存中或从磁盘(如SSD)读取,数据库写入几乎总是比读取慢。(Pole Position,一个用于数据库基准测试的开源工具, http://polepos.org/和结果 http://polepos.sourceforge.net/results/PolePositionClientServer.pdf。)。

这种设计的另一个潜在问题是,像Apache或lighttpd这样的Web服务器通常对它可以维护的同时连接数量有一个上限(默认值大约为500,但可以更高),而在高流量时,写入可以快速消耗所有这些。由于读取可以是异步的,或者利用其他性能优化(如gzip压缩或分块传输编码),因此Web服务器可以更快地切换服务读取并在客户端之间切换,每秒快速提供比最大连接数更多的请求(使用Apache和最大连接数设置为500,每秒提供数千个读取请求并不罕见。另一方面,写入往往在上载期间保持开放连接,

image.png
image.png

规划这种瓶颈可以很好地将图像的读写分成自己的服务,如图1.2所示。这允许我们独立地扩展每个(因为我们可能总是会比写作更多地阅读),但也有助于澄清每个点上发生的事情。最后,这将把未来的问题分开,这样可以更容易地进行故障排除和扩展问题,例如慢速读取。

这种方法的优点是我们能够独立地解决问题 - 我们不必担心在相同的上下文中编写和检索新图像。这两种服务仍然利用全局图像语料库,但是它们可以使用适合服务的方法(例如,排队请求或缓存流行图像 - 以下更多内容)自由优化自己的性能。从维护和成本的角度来看,每个服务可以根据需要独立扩展,这很好,因为如果它们被组合和混合,则可能无意中影响另一个服务的性能,就像上面讨论的场景一样。

当然,当您有两个不同的端点时,上面的示例可以很好地工作(实际上这与几个云存储提供商的实现和内容交付网络非常相似)。有很多方法可以解决这些类型的瓶颈,每种方法都有不同的权衡。

例如,Flickr通过在不同的分片中分配用户来解决这个读/写问题,这样每个分片只能处理一定数量的用户,并且随着用户的增加,更多的分片被添加到集群中(参见Flickr缩放的演示文稿, http: //mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html)。在第一个示例中,根据实际使用情况(整个系统中的读取和写入次数)更容易扩展硬件,而Flickr随其用户群进行扩展(但强制假设用户具有相同的使用率,因此可以有额外的容量)。在前者中,其中一个服务的中断或问题会降低整个系统的功能(例如,没有人可以写文件),而使用Flickr的一个分片的中断只会影响这些用户。在第一个示例中,更容易在整个数据集中执行操作 - 例如,

当涉及到这些系统时,没有正确的答案,但它有助于回到本章开头的原则,确定系统需求(重读或写入或两者,并发级别,跨数据集的查询,范围,排序等),对不同的替代方案进行基准测试,了解系统将如何失败,并对失败发生时制定可靠的计划。

冗余
为了优雅地处理故障,Web体系结构必须具有其服务和数据的冗余。例如,如果单个服务器上只存储一个文件副本,则丢失该服务器意味着丢失该文件。丢失数据很少是一件好事,处理它的常用方法是创建多个或多余的副本。

同样的原则也适用于服务。如果应用程序具有核心功能,则确保同时运行多个副本或版本可以防止单个节点发生故障。

在系统中创建冗余可以消除单点故障,并在危机中根据需要提供备用或备用功能。例如,如果在生产中运行同一服务的两个实例,并且一个实例出现故障或降级,则系统可以故障转移 到正常副本。故障转移可以自动发生或需要手动干预。

服务冗余的另一个关键部分是创建无共享架构。通过这种架构,每个节点能够彼此独立地操作,并且没有中央“大脑”管理状态或协调其他节点的活动。这对可扩展性有很大帮助,因为可以在没有特殊条件或知识的情况下添加新节点。但是,最重要的是,这些系统中没有单点故障,因此它们对故障更具弹性。

例如,在我们的图像服务器应用程序中,所有图像都会在某个地方的另一块硬件上具有冗余副本(理想情况下,如果发生地震或数据中心火灾等灾难,则位于不同的地理位置),以及要访问的服务图像将是多余的,所有可能的服务请求。(参见 图1.3。)(负载平衡器是实现这一目标的好方法,但下面还有更多内容)。

image.png
image.png

分区
可能存在无法容纳在单个服务器上的非常大的数据集。也可能是操作需要太多计算资源,性能降低并且需要增加容量的情况。在任何一种情况下,您都有两种选择:垂直或水平缩放。

垂直扩展意味着向单个服务器添加更多资源。因此,对于非常大的数据集,这可能意味着添加更多(或更大)的硬盘驱动器,因此单个服务器可以包含整个数据集。在计算操作的情况下,这可能意味着将计算移动到具有更快CPU或更多内存的更大服务器。在每种情况下,垂直缩放是通过使单个资源能够自己处理更多来完成的。

另一方面,水平缩放是添加更多节点。在大数据集的情况下,这可能是存储数据集的部分的第二服务器,并且对于计算资源,这将意味着在一些附加节点上分割操作或加载。为了充分利用水平扩展,它应该作为系统体系结构的内在设计原则包含在内,否则修改和分离上下文以使其成为可能会非常麻烦。

在涉及水平扩展时,一种比较常见的技术是将服务分解为分区或分片。可以分布分区,使得每个逻辑功能集是分开的; 这可以通过地理边界或其他标准来完成,例如非付费用户和付费用户。这些方案的优点是它们提供具有增加容量的服务或数据存储。

在我们的图像服务器示例中,用于存储图像的单个文件服务器可能被多个文件服务器替换,每个文件服务器包含其自己唯一的图像集。(参见图1.4。)这样的体系结构将允许系统用图像填充每个文件服务器,在磁盘变满时添加额外的服务器。该设计需要一个命名方案,将图像的文件名绑定到包含它的服务器。图像的名称可以由跨服务器映射的一致哈希方案形成。或者,可以为每个图像分配增量ID,以便当客户端请求图像时,图像检索服务仅需要维护映射到每个服务器的ID范围(如索引)。

image.png
image.png

当然,跨多个服务器分发数据或功能存在挑战。其中一个关键问题是数据局部性 ; 在分布式系统中,数据越接近操作或计算点,系统的性能越好。因此,将数据分布在多个服务器上可能存在问题,因为在需要它的任何时候它可能不是本地的,迫使服务器在网络上执行昂贵的所需信息提取。

另一个潜在的问题是不一致的形式 。当从共享资源(可能是另一个服务或数据存储)读取和写入不同的服务时,存在竞争条件的可能性 - 其中一些数据应该被更新,但是在更新之前发生读取 - 并且在那些情况下数据不一致。例如,在图像托管方案中,如果一个客户端发送了使用新标题更新狗图像的请求,将其从“狗”更改为“Gizmo”,但同时另一个客户端正在阅读,则可能发生竞争情况图片。在那种情况下,不清楚哪个标题“狗”或“Gizmo”将是第二个客户收到的标题。

分区数据肯定存在一些障碍,但分区允许将每个问题按数据,负载,使用模式等分成可管理的块。这有助于提高可扩展性和可管理性,但并非没有风险。有很多方法可以降低风险并处理故障; 但是,为了简洁起见,本章不涉及它们。如果您有兴趣阅读更多内容,可以查看我的博客文章, 了解容错和监控。

1.3、快速和可扩展数据访问的构建块
在介绍了设计分布式系统时的一些核心考虑因素之后,我们现在谈谈困难的部分:扩展对数据的访问。

大多数简单的Web应用程序,例如LAMP堆栈应用程序,如图1.5所示。

image.png
image.png

图1.5:简单的Web应用程序
随着它们的发展,存在两个主要挑战:扩展对应用服务器和数据库的访问。在高度可扩展的应用程序设计中,应用程序(或Web)服务器通常被最小化,并且通常体现为无共享体系结构。这使得系统的app服务器层可以水平扩展。由于这种设计,繁重的工作被压缩到数据库服务器和支持服务; 在这一层,真正的扩展和性能挑战发挥作用。

本章的其余部分致力于通过提供对数据的快速访问,使这些类型的服务快速且可扩展的一些更常见的策略和方法。

image.png
image.png

大多数系统都可以简化为图1.6。这是一个很好的起点。如果您有大量数据,则需要快速方便地访问,例如在桌面的顶部抽屉中保存糖果。虽然过于简化,但之前的声明暗示了两个难题:存储的可扩展性和数据的快速访问。

为了本节的目的,我们假设您有许多TB的数据,并且您希望允许用户随机访问该数据的一小部分。(参见图1.7。)这类似于在图像应用程序示例中在文件服务器上的某处定位图像文件。

image.png
image.png

图1.7:访问特定数据
这特别具有挑战性,因为将TB的数据加载到存储器中可能非常昂贵; 这直接转换为磁盘IO。从磁盘读取速度比从存储器读取速度快几倍,与Chuck Norris一样快,而磁盘访问速度比DMV速度慢。这种速度差异实际上增加了大数据集; 实际上,对于顺序读取,内存访问速度比从磁盘读取速度快6倍,对于随机读取速度快10万倍(参见“大数据病理学”,http://queue.acm.org/detail。 cfm?id = 1563874)。而且,即使有唯一的ID,解决知道在哪里找到这么少的数据的问题也是一项艰巨的任务。这就像试图从你的糖果藏匿中获取最后一个Jolly Rancher而不看。

值得庆幸的是,您可以使用许多选项来简化这一过程; 其中四个比较重要的是缓存,代理,索引和负载均衡器。本节的其余部分讨论了如何使用这些概念中的每一个来更快地进行数据访问。

高速缓存
高速缓存利用了参考原理的局部性:可能再次请求最近请求的数据。它们几乎用于每个计算层:硬件,操作系统,Web浏览器,Web应用程序等。缓存就像短期内存:它具有有限的空间,但通常比原始数据源更快,并包含最近访问的项目。高速缓存可以存在于体系结构的所有级别,但通常位于最靠近前端的级别,在那里它们被实现为快速返回数据而不会对下游级别产生负担。

在我们的API示例中,如何使用缓存来加快数据访问速度?在这种情况下,有几个地方可以插入缓存。一种选择是在请求层节点上插入缓存, 如图1.8所示。

image.png
image.png

图1.8:在请求层节点上插入缓存
直接在请求层节点上放置缓存可以本地存储响应数据。每次向服务发出请求时,节点将快速返回本地缓存数据(如果存在)。如果它不在缓存中,请求节点将从磁盘查询数据。一个请求层节点上的缓存也可以位于内存(非常快)和节点的本地磁盘上(比进入网络存储更快)。

image.png
image.png

将此扩展到多个节点时会发生什么?正如你可以看到图1.9,如果该请求层扩展到多个节点,它仍然是完全有可能的每个节点托管其自己的高速缓存。但是,如果负载均衡器在节点之间随机分配请求,则相同的请求将转到不同的节点,从而增加了缓存未命中。克服这个障碍的两个选择是全局缓存和分布式缓存。

全局缓存
全局缓存就像它听起来一样:所有节点都使用相同的单个缓存空间。这涉及添加某种服务器或某种文件存储,比原始存储更快,并且可由所有请求层节点访问。每个请求节点以与本地节点相同的方式查询缓存。这种缓存方案可能会变得有点复杂,因为随着客户端和请求数量的增加,很容易压倒单个缓存,但在某些体系结构中非常有效(特别是那些使用这种全局缓存非常快的专用硬件,或者有一个需要缓存的固定数据集)。

图中描述了两种常见的全局缓存形式。在图1.10中,当在缓存中找不到缓存的响应时,缓存本身负责从底层存储中检索丢失的数据。在图1.11中 ,请求节点负责检索缓存中找不到的任何数据。

image.png
image.png

图1.10:缓存负责检索的全局缓存

image.png
image.png

图1.11:请求节点负责检索的全局缓存
利用全局缓存的大多数应用程序倾向于使用第一种类型,其中缓存本身管理逐出和获取数据以防止来自客户端的相同数据的大量请求。但是,在某些情况下,第二种实现更有意义。例如,如果缓存用于非常大的文件,则低缓存命中百分比将导致缓存缓冲区因缓存未命中而变得不堪重负; 在这种情况下,它有助于在缓存中占据总数据集(或热数据集)的大部分。另一个例子是一种架构,其中存储在缓存中的文件是静态的,不应该被驱逐。

分布式缓存
在分布式缓存(图1.12)中,每个节点都拥有缓存数据的一部分,因此如果冰箱充当杂货店的缓存,分布式缓存就像将食物放在几个位置 - 冰箱,橱柜,和午餐盒 - 方便的地点,可以从商店取回小吃。通常,使用一致的散列函数来划分高速缓存,使得如果请求节点正在寻找某个数据片段,则它可以快速知道在分布式高速缓存中的哪个位置以确定该数据是否可用。在这种情况下,每个节点都有一小部分缓存,然后在转到源之前向另一个节点发送数据请求。因此,分布式缓存的一个优点是增加了缓存空间,只需将节点添加到请求池即可。

分布式缓存的缺点是修复丢失的节点。一些分布式缓存通过在不同节点上存储多个数据副本来解决这个问题。但是,您可以想象这种逻辑如何快速复杂化,尤其是在您从请求层添加或删除节点时。虽然即使节点消失并且部分缓存丢失,但请求只会从原点拉出 - 所以它不一定是灾难性的!

image.png
image.png

图1.12:分布式缓存
关于缓存的好处在于它们通常会使事情变得更快(当然,正确实现!)您选择的方法只允许您更快地处理更多请求。然而,所有这些缓存都需要以维持额外的存储空间为代价,通常是以昂贵的内存形式; 没有什么是免费的 高速缓存非常适合于使事情变得更快,而且在高负载条件下提供系统功能,否则会出现完全的服务降级。

流行的开源缓存的一个例子是Memcached(http://memcached.org/)(它可以作为本地缓存和分布式缓存工作); 但是,还有许多其他选项(包括许多特定于语言或框架的选项)。

Memcached用于许多大型网站,即使它非常强大,它只是一个内存中的键值存储,针对任意数据存储和快速查找进行了优化(O(1))。

Facebook使用几种不同类型的缓存来获取其网站性能(请参阅“ Facebook缓存和性能”)。他们$GLOBALS在语言级别使用和APC缓存(以函数调用为代价在PHP中提供),这有助于使中间函数调用和结果更快。(大多数语言都有这些类型的库来提高网页性能,它们应该几乎总是被使用。)Facebook然后使用分布在许多服务器上的全局缓存(参见“ 在Facebook上扩展memcached”)),这样一个访问缓存的函数调用可以为存储在不同Memcached服务器上的数据并行地发出许多请求。这使他们能够为其用户配置文件数据获得更高的性能和吞吐量,并且有一个更新数据的中心位置(这很重要,因为当您运行数千台服务器时,缓存失效和维护一致性可能具有挑战性)。
代理
在基本级别,代理服务器是一个中间件硬件/软件,它接收来自客户端的请求并将它们中继到后端源服务器。通常,代理用于过滤请求,记录请求或有时转换请求(通过添加/删除标头,加密/解密或压缩)。

image.png
image.png

图1.13:代理服务器
在协调来自多个服务器的请求时,代理也非常有用,从整个系统的角度提供优化请求流量的机会。使用代理加速数据访问的一种方法是将相同(或类似)请求一起折叠为一个请求,然后将单个结果返回给请求客户端。这称为折叠转发。

想象一下,在几个节点上存在对相同数据的请求(让我们称之为littleB),并且该数据不在缓存中。如果该请求被认为是代理路由,那么所有这些请求都可以折叠成一个,这意味着我们只需要读取一次磁盘上的littleB。(参见图1.14。)此设计存在一些成本,因为每个请求的延迟可能略高,而某些请求可能会稍微延迟以与类似的请求分组。但它会提高高负载情况下的性能,特别是在反复请求相同数据时。这类似于缓存,但它不是像缓存那样存储数据/文档,而是优化对这些文档的请求或调用,并充当这些客户端的代理。

例如,在LAN代理中,客户端不需要自己的IP来连接到Internet,LAN将折叠来自客户端的相同内容的呼叫。这里很容易混淆,因为许多代理也是缓存(因为它是放置缓存的一个非常合理的位置),但并非所有缓存都充当代理。

image.png
image.png

使用代理的另一个好方法是不仅要折叠对相同数据的请求,还要折叠对原始存储中空间上靠近的数据的请求(连续地在磁盘上)。采用这种策略可以最大化请求的数据位置,从而减少请求延迟。例如,假设一堆节点请求B的部分:partB1,partB2等。我们可以设置我们的代理来识别各个请求的空间局部性,将它们折叠成单个请求并仅返回bigB,从而大大减少了从数据源读取。(见图1.15。)当您随机访问跨TB数据时,这会对请求时间产生很大的影响!代理在高负载情况下或缓存有限时特别有用,因为它们实际上可以将多个请求合并为一个。

image.png
image.png

值得注意的是,您可以将代理和缓存一起使用,但通常最好将缓存放在代理服务器前面,原因与最好让速度较快的跑步者在拥挤的马拉松比赛中首先启动一样。这是因为缓存正在从内存中提供数据,速度非常快,并且不介意对同一结果的多个请求。但是如果缓存位于代理服务器的另一端,那么缓存之前的每个请求都会有额外的延迟,这可能会影响性能。

如果您正在考虑为系统添加代理,可以考虑许多选项; Squid和 Varnish都经过道路测试,并广泛用于许多生产网站。这些代理解决方案提供了许多优化,以充分利用客户端 - 服务器通信。在Web服务器层安装其中一个作为反向代理(在下面的负载平衡器部分中解释)可以显着提高Web服务器性能,减少处理传入客户端请求所需的工作量。

索引
使用索引快速访问数据是优化数据访问性能的众所周知的策略; 可能是最知名的数据库。索引使得存储开销增加和写入速度变慢(因为您必须同时写入数据并更新索引)以获得更快的读取。

就传统的关系数据存储而言,您也可以将此概念应用于更大的数据集。索引的技巧是您必须仔细考虑用户将如何访问您的数据。在数据集大小为TB但具有非常小的有效载荷(例如,1KB)的情况下,索引是优化数据访问的必要条件。在如此大的数据集中查找小的有效负载可能是一个真正的挑战,因为您无法在任何合理的时间内迭代那么多数据。此外,很可能这么大的数据集分布在几个(或许多!)物理设备上 - 这意味着您需要某种方法来找到所需数据的正确物理位置。索引是执行此操作的最佳方式。

image.png
image.png

索引可以像目录一样用于引导您到达数据所在的位置。例如,假设您正在寻找一条数据,B部分的第2部分 - 您如何知道在何处找到它?如果您有一个按数据类型排序的索引 - 比如数据A,B,C-它会告诉您原点的数据B的位置。然后你只需要寻找那个位置并阅读你想要的B部分。(见图1.16。)

这些索引通常存储在内存中,或者存储在传入客户端请求的本地。Berkeley DB(BDB)和树状数据结构通常用于在有序列表中存储数据,非常适合使用索引进行访问。

通常有许多索引层用作地图,将您从一个位置移动到下一个位置,依此类推,直到您获得所需的特定数据。(见图1.17。)

image.png
image.png

图1.17:多层索引
索引还可用于创建相同数据的多个不同视图。对于大型数据集,这是定义不同过滤器和排序的好方法,而无需创建许多额外的数据副本。

例如,假设前面的图像托管系统实际上托管了图书页面的图像,并且该服务允许客户端查询这些图像中的文本,搜索关于某个主题的所有图书内容,就像搜索引擎允许您一样搜索HTML内容。在这种情况下,所有这些书籍图像都需要许多服务器来存储文件,并且找到一个要呈现给用户的页面可能有点牵扯。首先,需要易于访问查询任意单词和单词元组的反向索引; 然后有一个挑战是导航到该书中的确切页面和位置,并检索结果的正确图像。因此,在这种情况下,反向索引将映射到位置(例如书B),然后B可以包含具有每个部分中的所有单词,位置和出现次数的索引。

可以在上图中表示Index1的倒排索引可能看起来像下面这样 - 每个单词或单词的元组提供书籍包含它们的索引。
Word(s) Book(s)
being awesome Book B, Book C, Book D
always Book C, Book F
believe Book B
中间索引看起来很相似,但只包含书B的单词,位置和信息。这种嵌套索引体系结构允许每个索引占用的空间比所有信息都必须存储到一个大的倒排索引中的空间要小。

这对于大规模系统来说至关重要,因为即使是压缩,这些索引也会变得非常庞大且存储成本也很高。在这个系统中,如果我们假设我们拥有世界上很多书籍 - 100,000,000(参见Inside Google Books博客文章) - 并且每本书只有10页长(为了使数学更容易),每页250字,这意味着有2500亿字。如果我们假设每个字平均有5个字符,并且每个字符占用8位(或1个字节,即使某些字符是2个字节),那么每个字5个字节,那么只包含每个字一次的索引超过1TB存储。因此,您可以看到创建具有许多其他信息的索引,例如单词元组,数据位置和出现次数,可以非常快速地加起来。

创建这些中间索引并以较小的部分表示数据可以使大数据问题易于处理。数据可以分布在许多服务器上,并且仍然可以快速访问 索引是信息检索的基石,也是当今现代搜索引擎的基础。当然,本节仅涉及表面,并且正在进行大量研究,以便如何使索引更小,更快,包含更多信息(如相关性),并无缝更新。(竞争条件存在一些可管理性挑战,以及添加新数据或更改现有数据所需的大量更新,特别是在涉及相关性或评分的情况下)。

能够快速,轻松地找到您的数据非常重要; 索引是实现这一目标的有效而简单的工具。

负载均衡器
最后,任何分布式系统的另一个关键部分是负载均衡器。负载平衡器是任何体系结构的主要部分,因为它们的作用是在负责服务请求的一组节点之间分配负载。这允许多个节点透明地为系统中的相同功能提供服务。(参见图1.18。)它们的主要目的是处理大量并发连接并将这些连接路由到其中一个请求节点,从而允许系统通过添加节点来扩展以服务更多请求。

image.png
image.png

有许多不同的算法可用于服务请求,包括挑选随机节点,循环,甚至根据某些标准选择节点,例如内存或CPU利用率。负载平衡器可以实现为软件或硬件设备。一个广泛采用的开源软件负载均衡器是 HAProxy)。

在分布式系统中,负载平衡器通常位于系统的最前端,以便相应地路由所有传入请求。在复杂的分布式系统中,将请求路由到多个负载均衡器的情况并不少见,如图1.19所示 。

image.png
image.png

与代理一样,某些负载均衡器也可以根据请求的类型不同地路由请求。(从技术上讲,这些也称为反向代理。)

负载平衡器面临的挑战之一是管理特定于用户会话的数据。在电子商务网站中,当您只有一个客户端时,很容易让用户将内容放入购物车并在访问之间保留这些内容(这很重要,因为如果您出售产品,则更有可能它们返回时仍然在用户的购物车中)。但是,如果用户被路由到一个节点进行会话,然后在下次访问时被路由到另一个节点,则可能存在不一致,因为新节点可能缺少该用户的购物车内容。(如果你把6包Mountain Dew放在你的购物车然后回来并且它是空的,你不会感到沮丧吗?)解决这个问题的方法之一就是让会话变得粘稠,这样用户总是被路由到同一个节点,但是很难利用自动故障转移等可靠性功能。在这种情况下,用户的购物车将始终具有内容,但是如果他们的粘性节点变得不可用,则需要特殊情况并且内容的假设将不再有效(尽管希望这个假设不会被内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。但是如果他们的粘性节点变得不可用,则需要有一个特殊情况,并且内容的假设将不再有效(尽管希望这个假设不会内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。但是如果他们的粘性节点变得不可用,则需要有一个特殊情况,并且内容的假设将不再有效(尽管希望这个假设不会内置到应用程序中)。当然,这个问题可以通过本章中的其他策略和工具来解决,例如服务,还有许多未涵盖的(如浏览器缓存,cookie和URL重写)。

如果系统只有几个节点,那么像循环DNS这样的系统可能会更有意义,因为负载均衡器可能很昂贵,并且增加了不必要的复杂层。当然,在较大的系统中,存在各种不同的调度和负载平衡算法,包括诸如随机选择或循环的简单算法,以及考虑利用率和容量之类的更复杂的机制。所有这些算法都允许分发流量和请求,并且可以提供有用的可靠性工具,如自动故障转移或自动删除坏节点(例如,当它变得无响应时)。但是,这些高级功能可能会使问题诊断变得繁琐。例如,当涉及高负载情况时,负载平衡器将删除可能很慢或超时的节点(因为请求太多),但这只会加剧其他节点的情况。在这些情况下,广泛的监控很重要,因为整体系统流量和吞吐量可能看起来正在下降(因为节点服务的请求较少),但各个节点正在变得最大化。

负载平衡器是一种允许您扩展系统容量的简单方法,与本文中的其他技术一样,在分布式系统架构中发挥着至关重要的作用。负载平衡器还提供了能够测试节点运行状况的关键功能,这样,如果节点没有响应或过载,可以从池处理请求中删除它,利用您的不同节点的冗余系统。

队列
到目前为止,我们已经介绍了很多快速读取数据的方法,但扩展数据层的另一个重要部分是有效的写入管理。当系统简单,处理负载最小且数据库较小时,写入速度可以预测很快; 但是,在更复杂的系统中,写入可能需要几乎不确定的长时间。例如,可能必须将数据写入不同服务器或索引上的多个位置,否则系统可能处于高负载状态。在写入或任何相关任务可能需要很长时间的情况下,实现性能和可用性需要在系统中建立异步; 一种常见的方法是使用队列。

image.png
image.png

想象一个系统,每个客户端都要求远程服务任务。这些客户端中的每一个都将其请求发送到服务器,服务器尽快完成任务并将结果返回给各自的客户端。在小型系统中,一台服务器(或逻辑服务)可以像它们一样快地为传入的客户端提供服务,这种情况应该可以正常工作。但是,当服务器接收到的请求数超出其处理能力时,则会强制每个客户端等待其他客户端的请求完成,然后才能生成响应。这是同步请求的示例,如图1.20所示。

这种同步行为会严重降低客户端性能; 客户端被迫等待,有效地执行零工作,直到其请求得到解答。添加额外的服务器以解决系统负载也无法解决问题; 即使有效的负载平衡到位,也很难确保最大化客户绩效所需的工作均匀公平分配。此外,如果服务器处理请求不可用或失败,则上游客户端也将失败。有效地解决这个问题需要在客户端的请求和为服务它而执行的实际工作之间进行抽象。

image.png
image.png

图1.21:使用队列管理请求
输入队列。队列就像听起来一样简单:任务进来,被添加到队列中,然后工作人员接受下一个任务,因为他们有能力处理它。(参见图1.21。)这些任务可以表示对数据库的简单写入,或者像为文档生成缩略图预览图像那样复杂的任务。当客户端向队列提交任务请求时,他们不再被迫等待结果; 相反,他们只需要确认请求已被正确接收。此确认可以在以后作为客户需要时工作结果的参考。

队列使客户端能够以异步方式工作,提供客户端请求及其响应的战略抽象。另一方面,在同步系统中,请求和回复之间没有区别,因此它们不能单独管理。在异步系统中,客户端请求任务,服务以确认收到任务的消息进行响应,然后客户端可以定期检查任务的状态,仅在结果完成后请求结果。当客户端等待异步请求完成时,它可以自由地执行其他工作,甚至可以对其他服务进行异步请求。后者是如何在分布式系统中利用队列和消息的示例。

队列还提供一些服务中断和故障保护。例如,创建一个高度健壮的队列非常容易,该队列可以重试因瞬态服务器故障而失败的服务请求。最好使用队列来强制执行服务质量保证,而不是直接将客户端暴露给间歇性服务中断,这需要复杂且经常不一致的客户端错误处理。

队列是管理任何大型分布式系统的不同部分之间的分布式通信的基础,并且有许多方法来实现它们。有很多开源队列,如RabbitMQ, ActiveMQ, BeanstalkD,但有些也使用像Zookeeper这样的服务 ,甚至像Redis这样的数据存储。

36
回复 编辑