分布式系统学习---可扩展的Web体系结构和分布式系统

分布式系统学习(一) : 可扩展的Web体系结构和分布式系统

本文是学习用的自翻译版本,原文地址 : Scalable Web Architecture and Distribute Systems


开篇:

  • 出于系统学习了解分布式知识的想法,从耗子叔的极客时间专栏看到推荐的入门阅读文章,抱着一边学习一边翻译的的想法,开始尝试系统地了解分布式领域的技术知识。

译文:

  • 对于一些大型站点来说,开源软件已经成为基础设施模块一样的存在。而且随着这些站点规模的持续扩张,对应的最佳实践和设计原则也应运而生。这一章节力求涵盖大型站点设计的关键问题,以及用于实现站点架构设计的构建模块等。

1.1 WEB分布式系统设计原则

  • 构建和维持一个具备扩展性的系统/应用意味着什么?在底层的实现方式上仅仅是通过远程网络资源将用户关联起来–使得系统具备扩展性的是资源的分布或者说访问资源的方式,是分散在多台服务器上的。

  • 像生活中中的其他事情一样,在构建web服务之前先花时间仔细制定计划,有助于系统的长期稳定;理解大型站点设计背后的注意点权衡取舍会在设计小型站点时产生更优的决策。以下是一些影响大型扩展性系统设计的关键原则:

    • 可用性: 对于大多数企业来说,自身站点的正常可用时间会直接影响到企业自身的声誉和业务,对于一些大型的在线电商而言,数分钟的站点不可用将会导致百万收入的流失,因此它们的系统设计的持续可用和弹性容错不仅仅是基本的业务需求,也是技术要求。高可用系统要求谨慎的地考虑系统关键组建的冗余性,部分故障的快速恢复,问题出现时的服务降级。

    • 性能: 对大多数站点来说性能越来越成为重要的考量标准。站点的响应速度会直接影响系统使用和用户体验,就像搜索引擎排名,性能直接关联到站点收入和用户留存。因此,构建系统如何优化以达到快速响应和低延迟是关键

    • 可靠性: 系统需要可靠是毋庸置疑的,请求指定数据的请求应该直接返回同样的数据,但是当数据更新之后,同样的数据请求应该返回更新后的数据。用户需要知道系统是否写入或存储了新的内容,这些内容可以持久化并在将来也可以被检索获取到。

    • 扩展性: 谈到任何大型分布式系统时,系统大小只是分布式系统扩展需要考虑的一个方面。就像需要对系统扩容以支持更大的负载,通常指的都是系统的伸缩性。系统的伸缩性可以映射为系统的许多指标:能支多少额外的业务,是否能便利地增加存储容量,能够处理多少笔正常交易等。

    • 可管理性: 设计易于操作的系统也是另一个需要考量的方面。系统的可管理性等同于操作的伸缩性:保养和更新。具备可管理特性的系统,意味着当问题出现时很容易诊断问题根源,易于对系统进行更新,且易操作。

    • 成本: 成本是一个比较重要的因素。很明显,包含了软件和硬件的成本,但也要考虑到部署和维护系统所需的部分。系统开发所需的大量开发者,系统运行所需的准备工作,甚至包括系统使用的培训过程等,这所有的一切都可以归纳到成本的范畴中来。

  • 上面的每一条设计原则,为设计分布式系统架构提供了决策基准。然后它们之间可能会出现互相减益的效果,就像为了实现其中的一个目的,增加了其他部分的成本。一个比较基础的案例是:通过直接增加多台服务器提高系统的扩展性会带来系统可管理性的下降和成本的增加。

  • 在设计任何类别的web应用时考虑如何运用这些原则非常重要,即便我们也明白一个设计可能会牺牲其中的一个甚至多个原则。(tradeoffs?)

1.2 基础知识

  • 构建系统过程有这样一些事情需要考虑清楚:哪些是正确的组件?它们要如何组合到一起,如何在他们之间做出正确的权衡。当系统需要伸缩性时才考虑投资进行扩容并不是明智的决策,一些具备远见的决策可以在未来替你节省充足的时间和资源。

  • 这一章节集中于描述与所有大型web系统核心相关的关键因素:服务冗余分区错误处理。这些因素都涉及到选择妥协的观点,尤其是在上文中提到的这些原则的应用场景中。下面用一个例子详细地解释一下这些观点。


    样例:图片托管应用
    • 有时候你会想在网山上传一张图片,对于托管和传送大量图片的大型站点来说,设计系统架构时会面临这些挑战性的问题:成本适宜,高度可用,低延迟(快速响应)。

    • 试想一下这样一个系统,用户会往中心服务器上传它们的图片,这些图片可以通过链接或API的形式被访问到。简单起见,我们假设这个系统具备两个核心的功能:上传图片到服务器,从服务器检索图片。我们一定希望图片可以快速上传,我们最关心的是当用户访问图片的时候能够快速提供响应。这个是和一个web服务器或者CDN服务器能够提供的类似的功能。

    • 系统其他重要的点是:

      • 存储的图片数量无限制,因此存储能力需要可扩展,考虑到图片量的因素
      • 图片下载或请求需要低延迟
      • 用户上传的图片需要持久存在
      • 系统需要易于管理
      • 由于图片托管服务没有很大的利润空间,系统需要考虑经济划分。
    • 系统示意图:

    • 在这个图片托管服务器的样例中,这个系统必须要有可以查觉到的快速响应,数据存储持久化且支持高度扩展。构建一个小的单机应用显得没有太大价值,假设一下我们是要构建和Flickr一样的大型站点。


    服务
    • 当考虑可扩展性的系统设计时,有助于功能解耦并且考虑系统的每一个部分都作为它自身的,具备清晰接口规范的服务存在。在实践过程中,通过这种方式设计的系统通常称为面向服务架构(SOA, Service-Oriented Architecture)。对于这一类系统,每个服务都具有自身的功能场景,并且和外部服务通过抽象接口,通常是公共接口交互,互相影响。
    • 将系统分解为一组互补的服务可以使这些服务的操作解耦,这个抽象化的操作可以帮助梳理服务之间的关联关系,底层环境和服务的消费者。创建这个清晰的轮廓图有助于隔离问题,但是仍然允许彼此之间进行独立地扩展以提供服务。SOA的系统设计非常类似于编程的OO思想。
    • 在我们的样例中,上传和检索图片的服务都是同一台服务器提供的,然而当系统需要扩展时,将这两个功能分散到各自的服务器上是很有意义的。
    • 我们快速前进,继续假设这个服务的应用量很大,在这种情况下读取一张图片会受到写入图片速度的影响,因为它们使用的是共享资源。根据架构的不同,影响程度也会有所区分。即使上传和下载速度相同(对于大部分IP网络来说都是不可能的,因为它们的设计下载速度:上传速度网速配比3:1),读文件通常从缓存中读取,而写操作最终会写到磁盘上(在最终一致性的方案下,还有可能发生多次写入)。即便所有的数据都从内存或者硬盘上读取,数据库写操作的速度总是比读操作要慢的。
    • 这个设计另一个潜在的问题是,一个web服务器例如Apache 或者其他轻量级http服务器存在可以同时维持的最连接数限制,在业务频繁提交的情况下,写操作很容易达到这个上限,消耗掉所有的连接资源。由于读操作是异步的,或者通过其他压缩技术进行性能优化,web服务器可以快速地在读操作之间以及客户端之间进行快速地切换,每秒能够处理远超过服务器连接限制数的请求量。而写操作,换句话说,会试图在图片上传的过程中维持连接不中断,因此在大多数网络环境下上传一个1M的文件可能需要超过1秒的时间,因此web服务器只能同时维持500个类似的写操作。
    • 对于上述这类瓶颈,我们有足够的理由将图片的读写操作分散成独立的服务。这样可以分别对它们进行定制化,而且还能清晰地界定在每一端发生了什么事情。最终,这也分离了之后可能出现的问题,可以比较容易地排查类似于慢读取这样的问题。
    • 这种方式的好处是我们可以独立地解决它们各自部分的问题 – 在一个场景中不需要同时担心写图片和读取图片的问题。读和写的服务仍然共享所有的图片资源,但是他们可以通过服务化方法(例如,请求排队,缓存热点图片等)自由地优化自身的表现性能。而且从管理和成本的角度考虑每个服务都可以独立地按需扩展,这点非常好,因为如果这两个服务是混杂在一起的话,在上面我们讨论场景中,很容易不经意间影响到另一个服务的表现性能。
    • 当然,如果你有两个不同的服务终端上面的读写分离方案可以良好运转(实际上这很像云存储服务供应商的实现方案和CDN功能)。有多种方法可以解决这类瓶颈问题,每一条都做出了不同的权衡。
    • 例如,Flickr 通过将用户访问分散到不同的数据分片上且每一个分片处理指定用户的访问,当用户增加的时候,分片也随之增加。在第一个样例中,可以十分简单地根据用途扩展硬盘空间(系统接收到的读写请求量),而Flickr 选择了根据用户群扩展(但需要强制假设用户使用率均等,因此需要额外的容量)。在第一种方案中,其中一项服务故障会导致整个系统功能不可用(例如无法写入图片),而在Flickr的方案中,这只会影响其中一部分用户。在第一种方案设计中,可以很容易地对全局数据执行操作–例如更新写服务纳入新的元数据,或者全局数据检索–然而在Flickr的架构方案中,这种全部性的操作需要每一个分各自执行(或者提供专门的搜索服务,而这也是Flickr实际的使用方案)。
    • 对于系统设计而言,没有正确的方案,但是这样有助于将思路带回到最开始讨论的设计原则上去,决定什么才是系统所需要的东西(读多,写多或者两者都有,延迟级别,数据集的检索,划分,排序等),基于不同的方案,了解系统何时会发生故障,并且准备一个坚固的错误处理方案。

    冗余
    • 为了优雅地处理错误,一个web系统架构必须提供冗余的服务和数据。例如,只有一个文件备份存储单个服务器上,服务器丢失也就意味着文件的丢失。数据丢失并不是一个好事,而且通常的解决方案都是创建多个冗余备份。
    • 这个原则也一样适用于服务。假设一个系统功能的核心模块,确保同时有多个服务副本在运行可以有效避免单点故障导致的服务不可用。
    • 在系统中创建冗余备份可以移除单点故障错误并且在故障发生期间需要的时候提供备份功能。例如,产品中有两个服务实例在运行,而其中一个故障变得不可用,系统可以将故障转移到的正常的服务实例备份中,故障转移可以自动发生也可以手动干预。
    • 服务冗余的关键部分在于无共享架构。在这个架构模式下,所有的节点都是独立且对等的,没有所谓的中枢节点对它们进行状态管理或协调它们之间的活动等。这种方式十分易于扩展,因为增加新节点并不需要特殊的条件或者前提。不过最重要的是,这样的系统不存在单点故障,应对错误时更加弹性。
    • 例如,我们的图片服务器应用,所有的图片在某处的硬盘上的都具备冗余备份(最理想的环境是分布在不同地域上,避免中心数据所在地域因为自然灾害例如地震火灾而全部丢失),而且那些图片服务都具备冗余备份,潜在地提供服务(负载均衡可能更好实现这个方案,但是下面还有其他方案)。
    • 冗余服务后的图片服务器应用

    分区:
    • 有时候可能存在一个很大的数据集,无法存放到单个服务器上。也可能是一个操作需要过多的计算资源,性能下降以至于需要进行扩容。有两个选择:垂直扩容和水平扩容。
    • 垂直扩容意味着在单个服务器上增加更多的资源。因此为了存储较大的数据集,意味着需要在计算机上增加更多的硬盘驱动。在这个资源计算的例子中,这意味着把计算任务转移到具有更快速的CPU或者更大内存的机器上。在每一个场景中,垂直扩容都是通过提升单个服务器的处理能力完成的。
    • 水平扩容,换句话说就是增加更多的节点。还是上面这个场景,水平扩容可能会增加第二台服务器存储数据集的部分数据,而如果是需要计算资源的场景,可能意味着将计算操作或者负载分散到其他附加的节点上。想将水平扩容的优势发挥出来,需要将它作为系统架构的固有设计原则来考虑才行,否则后期再对业务场景进行拆分做水平扩容十分麻烦。
    • 对于水平扩容,一个公共的解决方案是服务拆分或数据分片。服务拆分可以按照功能逻辑性进行划分;可以通过地理边界划分,或者基于其他基准,例如用户类型。 这个方案的优点是为服务和数据存储提供了额外的空间。
    • 在我们的图片服务器应用中,用来存储图片的单个文件服务器可以使用多个文件服务器替代,每台服务器都存储一份独有的图片数据。这样的架构方案允许系统在每一个文件服务器存放图片,并且当磁盘容量不足时增加新的服务器提供存储。这种设计需要提供一种将图片名称绑定到服务器的方案。一个图片的名称可以由跨服务器的一致性哈希方案产生。或者每张图片分配一个递增的ID值,这样当客户端请求一张图片时,图片检索服务只需要检索某台服务器上一定ID范围内的图片文件即可(类似于索引)。
    • 具备冗余和分区架构的图片服务器
    • 当然将数据和服务拆分到多台服务器是具有挑战性的。其中一个关键性的问题是 数据局部性;在分布式系统中,所需的的数据和执行的操作点距离越近,系统性能越好。因此将数据拆分会存在这样的潜在问题,在任意需要非本地数据进行计算的时刻,会强制要求系统通过网络执行代价高昂的操作去加载数据。
    • 另一个潜在问题是 不一致性;当不同的服务对共享资源进行读写时,例如潜在的其他服务或者数据存储,可能会产生竞态条件 – 一些数据需要更新,但是在更新之前先被读取过了 – 在这种情况下就会存在数据不一致性。
    • 数据分区方案理所当然地存在着障碍,但是分区也将问题进行分离 – 通过数据,负载,使用模式等 – 变成更好管理的模块。可以在系统扩展性和管理性上带来优势,但并不是没有任何风险。有很多方法可以减轻风险发生的概率和处理错误。

1.3 快速且可扩展的数据访问构建模块

访问
  • 前面已经覆盖了分布式系统设计的一些核心注意事项,现在来讨论一下比较困难的部分 : 扩展数据访问

  • 大多数的简单web服务器,例如, LAMP 栈应用,如下图所示:

  • 当它们随着业务不断发展,会面临两个主要的挑战:扩展应用服务器和数据库的访问能力。在一个具备高度扩展性的系统设计中,应用服务器通常会被最小化并且体现出无共享架构的的特点。这使得系统的应用服务器可以水平地扩展。这种设计的结果,会将业务压力转移到下层的数据库和服务上;这一层也是真正扩容和性能的挑战所在。

  • 本章节的其他部分会集中于一些关于通过实现快速数据访问来提升服务的响应速度和扩展能力的公共策略和方法

  • 大部分系统的数据访问模式如上图所示,这个模型非常适合用来演示。如果你拥有很多数据,想快速且简单地进行访问。虽然上图的模型古过于简单,但是存在两个比较难以解决的问题:存储扩展和数据访问速度。

  • 为了实现本节的目标,我们假设你拥有TB级别的数据并且想让用户随机访问其中的一小部分。这很类似于我们之前提到的例子,在图片应用服务器中定位某一张图片。

  • 这个目标尤其具备挑战性的地方在于将TB级别的数据加载到内存中成本是很高昂的,会直接受限于磁盘IO。磁盘的读取速度比内存的读取速度慢上数倍。对于读取大的数据集来说这个速度差异会更加明显,对于顺序读操作内存的速度只比磁盘快6倍,但是随机读的速度要快上100000倍。而且,即便数据存在唯一ID标志,从海量数据中找到指定的一小撮数据也是一项艰巨的任务。就像你闭着眼睛从糖果堆掏出一块自己喜欢的糖一样。

  • 幸运的是有很多方案可以使这个问题变得更加简单,其中比较重要的四个是 : 缓存,代理,索引,负载均衡。本章节的其他部分会讨论这些因素是如何加快数据的访问速度。


缓存
  • 缓存受益于数据访问局部性原则:最近被请求的数据很可能下次还会被请求到。缓存几乎在所有涉及计算的层次中都会用到:硬盘,操作系统,web浏览器,web应用等。缓存就像是一个短期内存:拥有限定的空间,但是访问速度比当前数据源要快而且存储了最近被访问到的数据。缓存可以存在于系统架构的每一层,但是经常出现在接近前端那一层,可以实现快速返回数据而不需要再经历一次底层查询。
  • 在我们前面的例子中,缓存如何加快数据访问速度?在这个场景中,有许多可以插入缓存层的地方。其中一个选择是将缓存添加到请求层节点,如下图所示:
  • 将缓存置于请求层节点可以使响应数据进入本地存储。如果缓存的数据存在,每次对相同服务的请求都会立刻返回本地存储的数据。如果缓存中不存在需要的数据,则会从磁盘中检索。位于请求层节点的缓存既可以从内存中快速检索也可以从节点本地磁盘上快速查找。多缓存示例如下 :
  • 当你将缓存扩展到所有节点会怎样?如上图所示,即便将缓存扩展到所有节点,每个节点也可能仅持有自身的缓存数据。然而,如果你的负载均衡策略把请求随机分散到不同的节点上,那么同样的请求落到不同的节点,会提高缓存失效的概率。解决这个问题的两个方案是 : 全局缓存和分布式缓存。

全局缓存:
  • 就像它的名称所代表的含义那样 : 所有的节点共享一个缓存空间。这涉及到添加一个服务器,某种顺序的文件系统,比你现有的存储方案要快而且可以被所有的节点访问。每个请求节点都以相同的方式查询缓存就像查询本地缓存一样。当客户端或用户请求递增的时候可能会压垮缓存使得这种方案的设计变得复杂,但在某些架构方案中它会显得十分高效(尤其是通过一块特定的硬盘加速全局缓存或者缓存一个固定的数据集)。
  • 这里有两种全局缓存结构的图表示。
  • 当缓存中不存在响应数据,缓存自身会从底层去查询对应的数据更新到缓存中。
  • 另一种模式如下 :
  • 在这种模式下,则是用户请求本身从数据库查询到了数据,同时数据也更新一份到缓存中。
  • 应用本身更倾向于使用第一种全局缓存模式受益,由缓存自身来管理缓存数据的过期和更新可以防止来自客户端的泛洪攻击,因为请求不会到数据库那一层。但是基于某些场景第二种设计模式更加有意义,例如缓存用于存储非常大的文件,较低的缓存命中率会导致缓存缓冲区由于过多缓存失效被压垮,失去了缓存层的作用;在这种场景下,第二种构建模式会提高整个数据集的缓存命中率,保持热点数据存活。另一个例子是缓存用于静态文件存储或是永不过期的数据。(这种模式下往往是应用自身对数据延迟有要求 – 部分数据一定要更快速地加载 – 意味着应用自身控制了缓存策略或热点数据,而不是通过缓存本身的机制来实现。)

分布式缓存:
  • 如下图:
  • 在分布式缓存中,每一个节点自身都具备部分缓存数据,就像杂货铺里的冰箱,分布式缓存就是将你的食物放在冰箱的不同位置,你非常便利地取出部分食物而不需要在杂货铺里找来找去。通常分布式缓存通过一个一致性哈希算法实现,例如一个请求节点查询指定的一部分数据,当数据存在的情况下,可以通过这个一致性哈希算法快速地定位到当前数据应该在分布式缓存的哪一块缓存中进行检索。在这个案例中,每个节点持有一小块缓存,当前节点在去数据源查询之前,会先将请求发送到其他节点去检索数据。因此,分布式缓存的好处是只需要增加请求池的处理节点就可以扩展缓存容量。
  • 分布式缓存的一个缺点是需要补救丢失的节点。一些分布式缓存通过在不同节点上存有多份缓存拷贝来解决这个问题;然而,你可以预想一下逻辑变得有多复杂,尤其是当你需要在请求层增加或者移除节点的时候。即便有的节点会丢失导致部分缓存数据丢失,请求也会从数据源把数据加载回来 – 因此节点丢失并不是灾难性的。
  • 缓存的好处是它们通常可以提升某些操作的速度。然而缓存的成本在于需要管理额外的存储空间,通常是昂贵的内存资源。在提升资源访问速度上使用缓存是很理想的,而且缓存还能确保高负载下系统功能的正常运转,没有缓存可能系统服务只能降级处理。
  • 一个比较流行的开源缓存是**Memcached(可以同时作为本地缓存和分布式缓存使用),不过也有很多其他的选择。
  • Memcached 用于许多大型站点,而且虽然他只是简单的内存键值存储,但是优化了任意数据存储和快速查找,因此显得强有力。
  • 那现在来考虑一下,当数据不在缓存中时,应该怎么办…

代理

  • 作为一个基础设施级别的存在,代理服务器是软硬件之间的中间件,接受来自客户端的请求并将它们转发到后端源服务器。通常,代理用来过滤请求,记录请求日志,有时也会转换请求信息(通过修改请求头信息,编解码或压缩等)。
  • 在调度来自多个服务器的请求上代理也发挥了极大的作用,提供了能够在系统层面组织请求通信的机会。其中一个使用代理加速请求的方式是将相同或相似的请求当成一个请求处理,然后返回相同的数据结果。这被称为压缩转发。
  • 现在假设有一个来自不同节点的检索相同数据的请求,而且数据并不在缓存中。如果这个请求通过代理进行路由,那么所有的请求都可以压缩成一个请求进行处理,意味着我们只需要从磁盘上读取一次该数据即可(见下图)。这种设计也关联到一些其他的成本,因为每个请求可能会因此产生略高的延迟,而且请求也需要花费时间等待和类似的请求进行分组压缩。但是在高负载的情形下会明显提高系统性能,尤其是当相同的数据一次次地被请求。这和前面提到的缓存有点类似,但是和缓存将数据存储起来不一样,代理服务器是组织对这些数据的请求并且对于客户端而言呈现一种代理的角色。
  • 在局域网代理中,例如,客户端不需要自己的IP联网,局域网会将所有此客户端的请求压缩处理返回相同的内容。这么看可能会让人觉得疑惑,因为许多代理也会进行缓存(因为作用的原因代理是个十分合乎逻辑的进行缓存的地方),但并不是所有的缓存都具备像代理一样的行为。
  • 另一个利用好代理的方式是不仅仅对请求同一数据的请求进行压缩,也将对磁盘上存储位置相近的数据的请求进行压缩处理。采用这种方式最小化请求数据定位的时间,可以明显降低请求延迟。例如,假设一些节点请求B的部分数据:一些请求B1部分,一些请求B2部分等等。我们可以设置代理识别单个请求中具备空间局部性特点的数据,将这些请求压缩成单个请求并返回整个B数据集,极大地节约了从数据源读取数据的时间。当你对TB级别的数据进行随机访问的时候,这种方案在请求时间的表现上会产生明显的差异。在高负载的情况下代理尤其能发挥作用,或者在有限缓存容量的场景中,因为代理本质上会将多数请求压缩成一个请求。
  • 同时使用代理和缓存并没有特别的要求,但是通常都会将缓存放在代理之前,也是基于同样的道理,最好让速度更快的运动员在拥挤的马拉松比赛中先开始(强化优势)。因为缓存是利用内存提供数据服务的,非常快速而且并不需要担心多个请求对同一数据的访问。但是如果缓存置于代理服务器之后,会导致每个请求到达缓存的延迟增加,反而会阻碍系统性能。

索引

  • 通过索引来加速数据访问是一个通用的策略,在数据库方面尤其是这样。索引在增加存储成本和降低写入数据速度(因为需要同时写数据和索引)做出了权衡,而使得数据读取更快。

  • 就像传统的关系数据存储,你也可以将这个概念应用到更大的数据集中。使用索引的技巧在于你必须小心地考虑用户会以何种方式访问你的数据。对于大小为TB级别的数据,有效负载很少的情况下,索引就成为了优化数据访问的必要条件。在如此大的数据集中找到一小块有效负载的数据是十分具备挑战性的,因为你无法在合理的时间范围内对所有的数据进行遍历。此外,这么大的数据集可能会分散在不同的物理设备上-- 这意味着你还需要找到存储指定数据的物理设备。而索引是处理这种情况的最佳方式。

  • 一个索引就像是目录指引你需要查找的数据位于哪个位置。例如,假设你在查找B数据中的一小部分B1–你怎么知道去哪里查找这一部分数据。而如果你具备存储指定类型数据的索引–A,B,C类型–它就可以告诉你B类型的数据位于哪个位置。然后你只需要遍历B所在的数据区域,取出你需要的数据。如下:

  • 索引通常存储在内存中,或者十分接近传入的客户端请求的本地位置。Berkeley 数据库和类树的结构通常将数据按照顺序,对于索引方式的访问十分理想。

  • 索引经常也会具有分层结构就像一张地图指南一样提供服务,将你从一个地方指引到另一个地方,直到最后,你找到自己需要的数据。(例如B+树从内部节点一路索引直到到达指定的叶子节点)

  • 索引也可以用来为同一份数据创建不同的视图,对于较大的数据集来说,这是一种不需要创建多份数据拷贝再重排序就可以提供不同的过滤和排序条件的方式。

  • 例如,假设我们之前提到的图片托管系统是托管书页内容的,并且提供按照文字检索这些图片的服务,通过主题检索所有的书页内容,搜索引擎也允许你以同样的方式检索HTML内容。在这个场景中,使用非常非常多的文件服务器存储这些书页图片,找到其中的一页呈现给用户会比较麻烦。首先,任意文字和词组的反向索引需要易于查找;定位到具体的一页和它在书中的位置会是一个挑战,然后返回正确的需要检索的图片。所以在这个场景中,这个反向索引会匹配到一个位置(例如Book B),然后 B可能包含一个索引,索引内容是所有的单词,位置和它们的出现次数。

  • 反向索引类似于上图中的index1,看起来可能和下面的形式类似:

    单词书籍
    being awesomeBook B,Book C,Book D
    alwaysBook C,Book F
    believeBook B
  • 中间索引可能看起来类似上面的结构但是不会仅仅只包含这些信息。如果所有的信息都已经存储在一个大的反向索引中,这种嵌套的索引架构可以让每个索引都只占用比较小的一部分内存空间。

  • 在大规模系统中这也是关键的部分,因为即使是经过压缩,这些索引也会非常大而且存储成本很高。假设我们在这个系统中存储了1亿本书籍的信息,而且每一本只有十页,一页250个单词,意味着一共有2500亿个单词存在。我们假设一个单词5个字符,一个字符8位(或者说1字节,尽管有的是2字节),因此一个单词5字节,那么每个单词仅包含一次的索引就超过了TB级别。所以,你可以创建一些包含其他信息的索引,例如词组,数据位置,出现的次数,可以非常快速地添加这些索引。

  • 创建这些中间索引并且以较小的部分来代替整个数据集,使得大数据量下的问题得以分化解决。数据即便分散在不同的服务器仍然可以被快速访问。索引是数据检索的基石,也是现代搜索引擎的基础。当然,本章节仅仅涉及到了一些表面的知识,还有一些研究专注于如何使得索引更小,更快,包含更多的信息并且无缝更新(//TODO)。

  • 简单快速地找到你需要的数据十分重要,而索引就是达成此目标的高效,简单的工具。


负载均衡

  • 最后,分布式系统的一个关键点是负载均衡器。对于所有分布式系统来说,负载均衡器都是比较特殊的存在,它们负责将系统访问压力分散到不同的节点来响应用户请求,这样可以通过多个节点透明地服务于系统的相同功能(它们并不知晓负载均衡器的存在,就像代理对于用户而言也是透明的,如下图)。负载均衡的首要目标是捕获大量的并发连接并且将它们分别路由到某个请求节点上,允许系统通过增加节点进行扩展从而支撑更多的请求。
  • 有许多不同的负载均衡算法用来服务请求,包括随机节点,轮询,或者基于确切的标准选择节点,例如内存或CPU利用率。负载均衡器可以通过软硬件的方式实现。一个广泛应用的开源负载均衡软件是HAProxy(听起来像个代理)。
  • 在分布式系统中,负载均衡器通常位于系统中比较靠前的位置(接近用户请求),因此可以对所有进入的请求进行路由。在一个复杂的分布式系统中,将请求路由到多个负载均衡器的设计并不常见,如下图这种结构:
  • 和代理类似,一些负载均衡器也可以根据请求类型的不同将它路由到不同的节点。(从技术上讲称为反向代理)
  • 负载均衡器面临的挑战是管理用户会话信息。在电商网站中,如果是单机架构的设计能够允许用户将商品添加到购物车并且在多次访问页面之间持久化这些内容(这一点很重要,因为当用户返回查看购物车的时候是很可能下单购买的)。然而,当用户路由到一个节点获得了session信息,而他们需要访问的下一个页面在另一个节点,这就可能产生用户信息不一致,因为新的节点可能会丢失用户的购物车信息(因为没有session)。围绕这个问题的一个解决方案是让session变得有粘性,这样用户每次都会路由到同一个节点,但是这种方式很难从一些高可用性功能中获益,例如故障转移(因为session随着节点一起故障了)。在这个粘性session的场景下,用户的购物车信息可以一直保存,但特殊情况下如果粘性节点故障导致不可用,可以假设这些购物车的内容很快就会变得无效。当然,也可以通过其他策略或工具来解决这个问题,例如服务化或者其他未涉及的方面(例如浏览器缓存,cookies 和 URL重写)。
  • 如果系统只有几个节点,像轮询DNS这种机制会更加灵活一点因为负载均衡成本较高而且增加了不必要的系统层次到来了复杂性。 在大型系统里面会有各种各样的调度和负载均衡算法,包括一些比较简单的例如随机选择和轮询,而更加复杂的机制实现需要将资源利用率和容量也列入考虑。所有这些算法允许调度和请求分离,并且提供十分有帮助的高可用工具例如自动故障转移,或者自动移除故障节点(例如节点不可响应)。然而,这些便利的功能也会使得问题诊断变得麻烦。例如,在高负载的情况下,负载均衡器可能会移除响应过慢或超时的节点,但这样只会导致其他节点的压力增加。这种情况下全局监控就显得很重要,因为系统整体流量和吞吐量看起来可能下降了(因为每个节点处理的请求数很少),但是个别节点可能已经是满负载状态。
  • 负载均衡器是一种允许你对系统扩容的简单方式,而且就像本文中提到的其他技术一样,负载均衡本身也是分布式系统比较重要的一环。负载均衡本身也提供了一些附加性的功能来测试节点健康状况,因此如果节点过载或者无响应,就可以从请求处理池中将它移除,体现了系统冗余节点的优势。

队列

  • 目前为止我们已经讨论了很多加速数据读取的方式,但是扩展数据层的另一个重要部分是高效的写操作管理。当系统结构比较简单的时候,只有很小的运行负载和很少的数据,可以预见写入操作会很快;然而,在更加复杂的系统中写入操作所需的时间可能是不确定的。例如,数据需要写入不同服务器/索引的不同地方,或者系统此时正处于高负载状态。在这些场景下,写操作或者类似性质的任务,会耗费较长的时间,为了性能和可用性要求会在系统内构建异步处理模块,一个常用的方式就是队列
  • 假设有一个系统,所有访问他的客户端都发起了要求远程服务的请求任务。这些客户端将自身的请求发送到服务器,服务器尽可能快地处理完这些任务并将结果各自返回。在小型系统中例如单机部署模式,可以很好地响应这些进入的请求,但是当进入的请求大大超过了服务器能够处理的范围,所有的客户端都会被强制要求等待其他客户端的请求处理完成之后才能得到响应。下图是一个同步请求的例子:
  • 这种同步行为会严重降低客户端的处理性能;客户端会被强制等待,期间无法执行任何任务,直到请求被应答。就算通过添加额外的服务器降低系统负载也无法解决这个问题;即便是高效的负载均衡设施也很难确保公平公正地分配任务以最大化提高客户端性能。此外,如果处理请求的服务不可用或者失败,客户端的上游应用也会受到影响。有效地解决这个问题需要对客户端请求和该请求处理的实际工作进行抽象。
  • 队列就像字面意思那样:一个任务入队,然后工作者获取下一个需要处理的任务。这些任务可以持久化到数据库层面,或者其他更加复杂的类似于为文档生成预览图一样的操作。当客户端将任务提交到队列之后它们不再需要强制等待处理结果;只需要知道任务被正确地接收的反馈。这个反馈信息可以在客户端需要的时候作为任务处理结果的引用返回。
  • 队列使得客户端以异步管理的方式工作,对客户端请求和响应做了策略抽象。换句话说,在异步系统里面,请求和应答之间没有区别,因此它们不能被分开管理。在一个异步系统中,客户端发送任务请求,然后服务方返回一个任务已接收的反馈信息,然后客户端可以定期地检查任务状态,只在任务完成的时候请求处理结果。在客户端等待异步任务处理完成之前可以自由地处理其它任务,甚至是向其他的服务发起异步任务等。后者是如何在分布式系统中利用消息和队列的样例。
  • 队列也为服务故障提供了保护性措施。例如,可以简单地构建一个健壮队列,当服务从故障中恢复时可以重试服务故障期间无法处理的请求。当客户端遭遇请求不稳定,需要进行复杂和不一致性客户端错误处理时,使用队列保障服务质量是可取的。
  • 队列是大规模分布式系统模块之间进行分布式通讯管理的基础设施,有一些常见的实现方案。开源的队列方案例如 RabbitMQ , ActiveMQ,BeanstalkD,也有利用服务实现的方式例如Zookeeper , 甚至数据存储的方式实现例如 Redis

1.4 结论

  • 设计高效的分布式系统是很让人激动的,而且涉及到很多很棒的工具可以实践到不同的应用中。本文只是涉及到了其中很小的一方面,只停留在了表面层次,但是该领域还涉及更多内容–而且这些内容不断地进行着创新。