文章目录

  • 1.1 分布式Web系统设计原则
    • 可用性 Availability
    • 性能 Performance
    • 可靠性 Reliability
    • 可伸缩性 Scalability
    • 可管理性 Manageablility
    • 成本 Cost
    • 原则总结
  • 1.2 基础
    • 实例 图像托管应用程序
    • 服务
    • 冗余
    • 分区
      • 垂直伸缩
      • 水平伸缩
  • 1.3 快速、可伸缩的数据访问的构建块
    • 缓存(Caches)
    • 代理(Proxies)
    • 索引(Indexes)
    • 负载均衡(Load Balancers)
    • 队列(Queues)
  • 1.4 结论
  • 参考资料

陈老师推荐语:“这篇文章会给你一个大概的分布式架构是怎么解决系统扩展性问题的粗略方法。” 本文是直接翻译自文章 Scalable Web Architecture and Distributed Systems

对于一些非常大型的Web站点来说,开源软件已经成为了一个基础的构建块。随着那些Web系统逐渐增长,在他们的架构中的最佳实践和指导性原则逐渐出现。本文主要关注一些设计大型WebSites的时候需要考虑的关键点,以及部分用于达到这些目标的构建块

尽管这章中的部分材料对于其他分布式系统也是合适的,但这章重点在分布式Web系统上。

1.1 分布式Web系统设计原则

构建和操作一个可伸缩的web站点或者应用究竟意味着什么?本质上来讲,其实就是让用户通过Internet连接到远程的资源,可伸缩则意味着资源或者对这些资源的访问是分布在多台服务器之间的。

跟生活中很多事情一样,在构建一个Web服务之前花时间去规划有助于日后的长跑。在创建一些更小型的Web站点时,去了解大型Web站点背后的一些考虑点和权衡点(tradeoffs)可以催生出更加明智的设计。下面是一些影响设计大规模Web系统的关键原则:

可用性 Availability

站点的持续运行时间对于大多数公司的口碑和服务能力都至关重要。对于一些零售站点,即使几分钟的不可用都可能导致成千上百万美金的收入损失。因此,将系统设计成持续可用的,既是一个基本的业务需求也是一个技术需求。分布式系统中的高可用性需要对关键部件冗余的仔细考虑,在部分系统失败事件出现时的迅速恢复,以及当问题出现时的优雅降级。

性能 Performance

对于大部分站点来说,站点的性能已经成为一个非常重要的考虑点。网站的速度影响使用率和用户满意度,一个可以直接关联到收入和用户留存率的因子,也会影响到搜索引擎排名。因此,对于创建一个系统来说,优化的快速响应和低延迟是非常关键的。

可靠性 Reliability

系统必须是可靠的,只有这样对数据的一个请求才能一致地返回相同数据。在数据改变或者更新的事件中,相同的请求应该返回新数据。用户需要知道是否数据已经写入到系统中,或者存储起来了,将持续存在,并且对于后面的获取可以保证已经就位。

可伸缩性 Scalability

当考虑任何大型分布式系统的时候,大小是规模中需要考虑的一个属性。同样重要的是增加处理更大负载所需的努力,通常称为系统的可扩展性。可扩展性指系统的许多不同参数:它可以处理多少额外流量,添加更多存储容量有多简单,能够处理多少事务。

可管理性 Manageablility

设计一个易于操作的系统是另一个重要的考虑点。系统的可管理性等同于操作的可扩展性:维护和更新。可管理性的考虑因素是当问题出现的时候,易于诊断和理解问题,易于做更新和修改,以及系统操作的简单性。(即,系统是否经常运行而没有错误或者异常?)

成本 Cost

成本是另一个非常重要的因素。这个显然包括软件和硬件的成本,但是考虑部署和维护系统的其他方面也很重要。系统构建所需要的开发者时间,运行系统所需的操作工作量,甚至所需的培训工作量也要考虑进去。成本是总拥有成本。

原则总结

以上的每个原则提供了一个在设计一个分布式Web架构时的决策依据。然而,他们可能本身相互冲突,这样就会使得达到一个目标是以牺牲另一个目标为代价的。一个基本的例子:当选择简单地堆加服务器来解决容量(Scalability)问题的时候,代价是可管理性(Manageablility 必须在额外增加的Server上操作)和成本(Cost 服务器的价格)。

当设计任何种类的Web应用时,即使承认在一个设计中可能牺牲了其中的一个或者多个原则,考虑以上这些关键原则仍然是非常重要的。

1.2 基础

当考虑系统架构的时候,只有很少的东西需要考虑:正确的部分是什么?这些部分如何组织在一起?以及什么是正确的权衡?在真正需要之前在规模上进行投资并不是一个明智的商业决策。然而,一些前瞻性的设计可以在未来节约大量的时间和资源。

这个小节重点讲几乎所有的大型web应用都非常重要的部分核心因素:服务,冗余,分区,以及处理失败。这些因素中的每个都包含了选择和妥协,尤其是在上一节的原则的情境下。为了更详细地解释这些,最好的方式是用一个例子来开始。

实例 图像托管应用程序

可能你曾经上传了一张照片上线。对于托管和提供大量的图片的大站点来说,构建一个成本节约,高可用,以及有低延迟(快速获取)的架构有非常大的挑战。

假设一个系统,正如Flickr或Picasa这种,用户可以上传他们的图片到一个中央服务器并且图片可以通过web链接或者API访问到。为了简单起见,假定这个应用有两个关键的部分:上传(写)图片到服务器的能力 和 查询图片的能力。 虽然我们希望上传非常高效,但是其实我们更关注于查询图像的时候又多快能查到,例如通过web页面或者其他应用程序请求查询到。这个对于一个web服务器或者CDN边缘服务器能提供的功能非常相似。一个CDN服务器通常将内容存储在很多位置上,因此内容在物理上或者地理上更靠近用户,这样可以有更快的表现。

这个系统的其他重要方面:

  • 对于能存储的图片的数量没有限制,因此存储可伸缩性需要被考虑
  • 对于图像下载或请求访问需要有低延迟
  • 如果一个用户上传了图像,这个图像应该永远在那里(图像的数据可靠性)
  • 系统应该易于维护(可管理性)
  • 由于图像托管的利润率不高,因此系统需要有成本效益

图1是一个功能的简单框图。

在这个图像托管应用实例中,系统必须要可察觉的快,数据存储必须可靠并且所有的属性都可伸缩。构建一个小版本的应用没有什么挑战,并且可以简单地托管在一台服务器上;然而,对本文来说并不感兴趣。假设我们想要构建一个可以成长为跟Flickr一样大的系统。

服务

当考虑可伸缩性系统设计时,有助于解耦功能,并且将系统的各个部分当做自己的服务并定义清楚接口。实际上,这种方式设计的系统被称为SOA(面向服务的体系结构)。对于这种类型的系统,每个服务有独特的功能上下文。对于跟功能上下文之外的任何东西交互是通过一个抽象接口,通常就是另一个服务的对外的公共接口。

将系统拆解为一系列互补的服务将这些部分的操作彼此分离。这个抽取过程有助于在服务、其底层的运行环境,以及服务的使用者之间建立清晰的关系。创建这些清晰的描述可帮助隔离问题,但也允许每个部分彼此独立地扩展。这种系统的面向服务的设计很类似于程序中的面向对象的设计。

在我们的实例中,所有上传图片和获取图片的请求都是由相同的server处理的;然而,当系统需要扩展时,将这两个功能拆分成不同的服务很有意义。

假设服务正在大量使用,这种场景下将可以简单地看出来写的时间影响读图像所需要的时间(因为这两个功能竞争共享资源)。根据架构这种影响可能很大。即使上传和下载的速度一样(这个假设对于大多数IP网络不正确,因为大部分都是下载速度:上传速度=3:1来设计的),读文件通常从缓存中读取,写将最终写入到磁盘中(可能在最终一致性场景中需要写几次)。即使全都是内存中操作或者从ssd读取,数据库的写总是比读慢的。

这个设计存在的另一个潜在的问题是,像Apache或者lightppd这种web服务器通常对同时可以维持的连接数有一个上限(默认值在500左右,可以调更高),并且在高流量的情况下,写入可以快速消耗所有这些。因为读可以是异步的,或者利用其它新能优化手段(如gzip压缩或者分块传输编码),web服务器可以更快地切换服务读取,并在客户端之间切换,每秒快速提供比最大连接数更多的请求(在Apache最大连接设置为500的前提下,服务几千每秒的读请求是很常见的)。写另一方面在上传期间会维护一个打开的连接,因此,在大多数家庭网络中上传1MB文件可能消耗多于1秒,因此,web服务器仅仅能同时处理500个这种写。

规划这种瓶颈可以很好地拆分读和写图像到各自的服务,如图2所示。这允许我们独立地扩展他们中的每一个(因为比起写入我们总是做更多读操作),也有助于澄清在每个时刻发生的事情(排查的时候更加清楚问题)。最后,这将分离将来的关注点,也就是说更容易进行故障排除和扩容,例如读取慢这种问题。

这种方法的优势是我们能够独立于另一个问题来解决当前问题,也就是说不用在相同的上下文环境中同时考虑写和获取新图像。这两种服务仍然利用全局图像库,但他们可以使用适合服务的方法自由地优化自己的性能(例如,请求排队,或者缓存热点图片——更多内容如下所示)。从维护和成本的角度来看,每个服务可以根据需要独立扩展,这很好,因为如果它们被组合和混合,则可能无意中影响另一个服务的性能,就像上面讨论的场景一样。

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

例如,Flickr是通过分配用户到不同的分片来解决这个读写问题的,这样每个分片(shard)仅仅能处理一定数量的用户,并且随着用户增加,把更多的分片添加到集群中。在第一个例子中,基于实际的使用量(整个系统的读和写)去扩容硬件更简单,而Flickr根据用户群去扩容(强制假设每个用户的使用量是相等的,因此可以有额外的容量)。在前者中,其中一个服务的中断或者问题会降低整个系统的功能(例如,写服务挂了,没有一个人能写文件),而Flickr的一个分区中断了将仅仅影响那些用户。在第一个例子中,对跨整个数据集执行操作非常简单,例如,更新写服务以包含新的metadata或者搜索所有图片的metadata。而对于Flickr的架构,每个分区都需要更新或者搜索(或者需要创建一个搜索服务去采集metadata,实际上就是这么干的)。

当来到了这些系统中,根本没有正确的答案。但是回到本文开头的那些原则上去会很有帮助。确定系统的需求(大量的读或者写或者两者都有,并发程度,跨数据集的查询,范围,排序等),对不同的替代方案进行基准测试,了解系统将如何失败,并对失败发生时制定可靠的计划。

  • 小结:

    • 服务间按照功能进行拆分,如读写分离等,好处很多
    • 解决性能问题可以有不同的方法,如Flickr采用的是将用户分片来解决的,跟拆分服务相比,对用户分片各有利弊,需要权衡
    • 没有正确答案,需要回到前面的原则上考虑系统的实际需求来确定方案,并进行基准测试、预测可能的失败、准备失败方案

冗余

为了优雅地处理失败,一个web体系结构必须有服务和数据的冗余。例如,如果一个文件只在一个服务器上存了仅一份副本,那么那台服务器的数据丢失意味着那个文件也丢失了。丢数据是很严重的问题,通常处理的方式是创建多个或者冗余的备份。

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

在一个系统中创建冗余可以消除单点故障,并且在危机事件中根据需要提供一个备份或者备用功能。例如,生产中一个服务有两个实例在运行,其中一个故障或者降级了,系统可以故障转移(failover)到另一个正常的副本上。故障转移可以自动或者手工干预完成。

服务冗余的另一个关键部分是创建无共享架构(shared-nothing)。在这个架构下,每个节点可以独立于另一个来操作,并且没有什么中央大脑来管理状态或者协调其他节点的活动。因为新节点不需要特殊条件或者知识就可以直接加入进来,这大大提高了可伸缩性。但是,最重要的是,系统中没有了单点故障,因此对故障更有弹性。

例如,在我们的图像服务器应用中,所有图像数据在其他地方的某块硬件上有冗余备份(理想情况下,在一个像一个灾难性事件如数据中心的地震或者火灾中,在不同的地理位置下)。对于所有潜在的服务请求,访问图像的服务也是冗余的。如下图3所示。负载均衡是一个非常好的方式来实现这种冗余,但是我们后面还会有更好的方式。

分区

  • 可能存在无法放在单个服务器上的很大的数据集。
  • 也可能有这种情况:一个操作需要太多的计算资源,逐渐让性能变得很差到必须扩容的程度。

在以上任一种情况下,你有两个选择:垂直伸缩或者水平伸缩。

垂直伸缩

垂直伸缩意味着向单个服务器增加更多的资源。

  • 对于无法存放的大数据集的场景,意味着添加更多或者更大的磁盘来使得单个服务器可以容纳整个数据集。
  • 在高计算资源的操作的场景中,意味着将计算移动到一个有更快的CPU或者更多内存的更大的服务器上。

在任意一种情况中,垂直伸缩是通过靠自己让单个资源足够处理更多来完成的。

水平伸缩

水平伸缩是增加更多的节点。

  • 对于大数据集场景中,这意味额外增加一台server去存放这个数据集。
  • 对于高计算资源的操作的场景中,意味着拆分操作或者加载到一些额外的节点上。

为了最大限度利用水平伸缩,应该让其成为系统架构设计中的一个固有的设计原则,否则,后面去修改和分离上下文来做水平伸缩是非常麻烦的。在涉及水平伸缩时,一种比较常见的技术是将服务分解为分区(partitions)或分片(shards)。分区可以是分布式的,这样每个功能逻辑集合是隔离的。这可以通过地理边界或者其他标准来完成,如非付费用户用户和付费用户。这些方案的优点是他们给服务或者数据存储扩容。


在我们的图像服务器的例子中,可以直接将用于存储图像的单个文件服务器用多个文件服务器替换掉,每个新的文件服务器都包含其自己唯一的图像集合,如图4所示。这样的架构允许系统将图片写入到每个服务器中,并且当磁盘变满时,添加额外的服务器。这个设计需要一个可以将图片的文件名关联到包含它的服务器的命名方案。一个图片的名字可以由跨所有服务器映射的一致性hash方案形成。或者作为一个可选方案,每个图片可以被分配一个增量ID,这样当一个客户端请求一个图片的时候,获取图片的服务仅仅需要维护映射到每个服务器上的ID的范围,正如索引一样。

当然,跨多个服务器分布数据或者功能存在挑战。其中一个关键问题就是数据局部性;在分布式系统中,数据离操作或者计算点越近,系统的性能越好。因此,数据跨多个服务器存放是一个潜在的问题。因为在需要数据的任何时候,他可能不是本地的,这种情况下强制server去执行一个高成本的跨网络获取所需要的信息。

另一个潜在的问题就是不一致性。当从共享资源(可能是另一个服务或数据存储)读取和写入不同的服务时,存在竞争条件的可能性 - 其中一些数据应该被更新,但是在更新之前发生读取 - 并且在那些情况下数据不一致。例如,在图片托管服务场景中,在下述场景中就可能会出现竞态:当一个客户发送一个请求去更新狗子的图片文件的标题,从Dog改成Gizmo,但是此时另一个客户端正在读取这个图片文件。在这种情况下,不知道到底第二个client读取到的是Dog还是Gizmo标题。

将数据分区当然有很多障碍,但分区允许将每个问题按数据,负载,使用模式等分成可管理的块。这有助于提高可扩展性和可管理性,但并非没有风险。有很多方法可以降低风险并处理故障;但是,为了简洁起见,本章不涉及它们。

1.3 快速、可伸缩的数据访问的构建块

在介绍了设计分布式系统时的一些核心考虑因素之后,我们现在谈谈困难的部分:扩展对数据的访问

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

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

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


大多数系统都可以简化为图6。 这是一个很好的起点。 如果您有大量数据,需要快速方便地访问,就好像把糖果放在桌面的顶部抽屉中那么好拿。虽然过于简化但前文已述及的两个问题依然存在:存储的可扩展性和数据的快速访问。

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

这特别具有挑战性,因为将TB的数据加载到存储器中开销非常大,这直接转换为磁盘IO。从磁盘读取速度比从存储器读取速度慢几倍。这种速度差异实际上增加了大数据集;实际上,对于顺序读取,内存访问速度比从磁盘读取速度快6倍,对于随机读取速度内存访问速度要快10万倍。而且,即使有唯一的ID,解决知道在哪里找到这么少的数据的问题,这也还是一项艰巨的任务。这就等于你不能看但是要从你的糖果储存中获取最后一个Jolly Rancher(一个特定的糖果品牌)。

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

缓存(Caches)

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

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

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

将此扩展到多个节点时会发生什么? 如图9所示,请求层扩展成多个节点,每个节点仍然有自己的缓存。 但是,如果位于节点前面的负载均衡器在节点之间随机分配请求,则相同的请求将转到不同的节点,从而增加了缓存未命中。解决这个问题的两个方法:全局缓存和分布式缓存。

  • 全局缓存(Global Cache)
    全局缓存就像它听起来一样:所有节点都使用相同的单个缓存空间。 这涉及添加某种服务器或某种文件存储,比原始存储更快,并且可由所有请求层节点访问。 每个请求节点以与本地节点相同的方式查询缓存。 这种缓存方案可能会变得有点复杂,因为随着客户端和请求数量的增加,很容易压倒单个缓存,但在某些体系结构中非常有效(特别是那些有使这种全局缓存非常快的专用硬件上, 或者有一个需要缓存的固定数据集)。
    图中描述了两种常见的全局缓存形式。 在图10中,当在缓存没命中时,缓存本身负责从底层存储中检索没命中的数据。 在图11中,请求节点负责去从底层的存储中找到缓存中找不到的任何数据。这里只是一个简单的描述,涉及到缓存的读取和更新策略其实有多种方式,可参考本博客中的另一篇文章专门描述。


    利用全局缓存的大多数应用程序倾向于使用第一种类型,其中缓存本身管理淘汰和获取数据以防止来自客户端的相同数据的大量请求。但是,在某些情况下,第二种实现更有意义。例如,如果缓存用于非常大的文件,则低缓存命中百分比将导致缓存缓冲区因缓存未命中而变得不堪重负; 在这种情况下,它有助于在缓存中拥有大部分的总数据集(或热数据集)。 另一个例子是一种架构,其中存储在缓存中的文件是静态的,不应该被淘汰。 (这可能是因为围绕数据延迟的应用程序要求 - 对于大型数据集,某些数据片段可能需要非常快 - 应用程序逻辑比缓存更好地理解淘汰策略或热点。)

  • 分布式缓存(Distributed Cache)
    在分布式缓存(图12)中,每个节点都拥有缓存数据的一部分,因此如果把冰箱比作杂货店的缓存,分布式缓存就像将食物放在几个位置如冰箱,橱柜, 和午餐盒等这些方便的地方以方便取吃的,但是又不用跑到杂货店去。通常,使用一致性hash函数来划分cache,使得如果请求节点正在寻找某个数据片段,则它可以快速知道在分布式cache中的哪个位置以确定该数据是否可用。在这种情况下,每个节点都有一小部分缓存,然后在转到源数据去访问之前向另一个节点发送数据请求。因此,分布式Cache的一个优点是只需将请求节点添加到请求池就可以增加Cache空间。

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

关于缓存的好处在于它们通常会使事情变得更快(当然,正确实现的前提下)。缓存的方法可以让系统更快地处理更多请求。 然而,所有这些缓存都需要以维持额外的昂贵的内存存储空间为代价; 毕竟没有什么是免费的。Cache非常适合于使事情变得更快,更重要的是在高负载条件下提供完整的系统功能,否则会出现服务降级。

redis和memcached都是非常流行的开源cache,可以作为本地cache或者分布式cache都可以。当然也可以有很多其他的选择方案。
Memcached用于许多大型网站,非常强大。它是一个内存中的KV存储,针对任意数据存储和快速查找进行了优化(O(1))。

Facebook使用几种不同类型的缓存来获取其网站性能。 他们在语言级别使用$ GLOBALS和APC缓存(以函数调用为代价在PHP中提供),这有助于使中间函数调用和结果更快。 (大多数语言都有这些类型的库来提高网页性能,它们几乎总是被使用。)Facebook然后使用分布在许多服务器上的全局缓存(参见“在Facebook上扩展memcached”),这样一个函数调用访问 缓存可以为存储在不同Memcached服务器上的数据并行发出许多请求。 这使他们能够为其用户配置文件数据获得更高的性能和吞吐量,并且有一个更新数据的中心位置(这很重要,因为当您运行数千台服务器时,缓存失效和维护一致性可能具有挑战性)。

小结:

  • 缓存解决的问题是?或者缓存的作用到底是什么? 就是加快数据的访问速率,从而实现更大的吞吐和更快的访问
  • 缓存分为全局缓存和分布式缓存
    • 分布式缓存不同于最开始提到的那种本地缓存

      • 分布式缓存虽然每个节点上都有缓存,但是并不是独立的,而是所有的节点上的cache组成了一个大家共同使用的cache池,每个节点如何找到自己需要的cache是通过一致性hash算法计算得到的
      • 最开始的那种本地缓存是每个节点都有自己独立的缓存,两者区别很大
    • 全局缓存按照缓存未命中时更新cache的主体不同而分两类,实际根据更新缓存主体和写入策略不同有更多的分类,可以参考另一篇介绍缓存的博客;
  • facebook用多级缓存提高性能,编程语言级别用了缓存(暂存下一些中间结果或者部分结果),然后用多台memcache服务器组成了一个全局cache集群
    • 实际读了文中facebook的应用memcache的文章,发现2008年左右的memcache问题很多,诸如每连接都分配了固定的内存等
    • 加上当前主流cache基于redis,因此,此处不再深究;

现在让我们谈谈cache以外的事情…

代理(Proxies)

一般地,代理服务器就是一个中间件硬件/软件,它接收来自客户端的请求并将它们中继到后端源服务器。 通常,代理用于过滤请求,记录请求或转换请求(通过添加/删除标头,加密/解密或压缩)。

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

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

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

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

值得注意的是,您可以将代理和缓存一起使用,但通常最好将缓存放在代理服务器前面,原因与最好让速度较快的跑步者在拥挤的马拉松比赛中首先启动一样。这是因为缓存是从内存中提供数据来服务的,速度非常快,并且不关心对同一结果的多个请求。但是如果缓存位于代理服务器的另一端,那么缓存之前的每个请求都会有额外的延迟,这可能会影响性能。(意思是缓存可以直接返回,你加了一个proxy在前面反倒阻碍了原本非常快的cache的作用。总的来说就是cache应该尽可能靠近前端)

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

索引(Indexes)

使用索引快速访问数据是优化数据访问性能的众所周知的策略; 可能最知名是运用在数据库中。索引使得更高层的存储开销增加和写入速度变慢(因为您必须同时写入数据并更新索引),换来的是获得更快的读取。运用索引的技巧是您必须仔细考虑用户将如何访问您的数据。

就传统的关系数据存储而言,您也可以将此概念应用于更大的数据集。在数据集大小为TB但具有非常小的有效载荷(例如,1KB)的情况下,索引是优化数据访问的必要条件。在这样大的一个数据集中找到那么小的一个载荷是一个挑战,因为你不可能在合理的时间内迭代那么多的数据。更重要的是,很可能这样大的一个数据集可能分布很多物理设备,这意味着你需要一些方法来找到要找的数据的正确物理位置。索引就是做这个的最好的方式。

索引像一个内容表一样,引导你到数据所在的位置。例如,假定你在找一块数据,B的part2,你怎么知道去哪里可以找到呢?如果你有一个按照数据类型排序的索引,正如A,B,C这种顺序,索引将告诉你数据B在数据源中的位置。那么你仅仅只需要定位到那个位置并且读取你想要的B的那一部分。(如图16所示)。

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

通常有多层索引,这些索引像一个地图一样引导你从一个地方到另一个地方,直到你获取到你想要找的数据。图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的单词,位置和信息。这种嵌套索引架构允许每个索引占用的空间比所有信息都必须存储到一个大的倒排索引中的空间要小。

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

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

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

负载均衡(Load Balancers)

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

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

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

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

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

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

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

队列(Queues)

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

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

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

输入队列。 队列就像听起来一样简单:任务进来,被添加到队列中,然后当工作进程有容量能处理下一个任务的时候,从中取走下一个任务。 (参见图21。)这些任务可能是对数据库的简单写入,或者像为文档生成缩略图预览图像那样复杂的任务。当客户端向队列提交任务请求时,他们不再强制等待结果; 相反,他们只需要确认请求已被正确接收。此确认后续作为客户端对计算任务的结果的引用。

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

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

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

1.4 结论

设计可快速访问海量数据的高效系统是令人兴奋的,还有很多为各种新应用赋能的伟大的新工具。这章仅仅覆盖了很少的一些例子,但是还有更多,并且这个领域在持续创新。

参考资料

  1. Scalable Web Architecture and Distributed Systems

可伸缩Web体系结构和分布式系统相关推荐

  1. 使用LVS构建可伸缩WEB集群

    原文:http://kb.linuxvirtualserver.org/wiki/Building_Scalable_Web_Cluster_using_LVS Building Scalable W ...

  2. web 体系结构_Web服务体系结构概述

    web 体系结构 Web服务是独立的,模块化的应用程序,可以通过通常是万维网的网络进行描述,发布,定位和调用. Web服务体系结构描述了三个角色:服务提供者,服务请求者和服务代理. 和三个基本操作:发 ...

  3. 可扩展Web架构与分布式系统

    1.1. web分布式系统的设计原则 搭建和运营一个可伸缩的web站点或者应用程序意味着什么?在原始层面上这仅仅是用户通过互联网连接到远程资源-使系统变得可伸缩的部分是将资源.或者访问的资源,分布于多 ...

  4. 可扩展的Web架构和分布式系统

    原文链接:http://www.aosabook.org/en/distsys.html 开源软件已经成为一些大型网站的基石.随着这些网站的发展,围绕其架构的最佳实践和指导原则应运而生.本章旨在讨论设 ...

  5. app访问java web_Java Web App体系结构

    app访问java web 我曾经利用Servlet,JSP,JAX-RS,Spring框架,Play框架,带有Facelets的JSF和一些Spark框架. 以我的拙见,所有这些解决方案都远非面向对 ...

  6. Java Web App体系结构

    我曾经利用Servlet,JSP,JAX-RS,Spring框架,Play框架,带有Facelets的JSF和一些Spark框架. 以我的拙见,所有这些解决方案都远非面向对象和优雅的. 它们都充满了静 ...

  7. Linux服务器集群系统(二)——LVS集群的体系结构

    原文地址:  http://www.linuxvirtualserver.org/zh/lvs2.html 本文主要介绍了LVS集群的体系结构.先给出LVS集群的通用体系结构,并讨论了其的设计原则 ...

  8. 从 SPA 到 PWA:Web App的下一站在哪?

    从AJAX(Asynchronous JavaScript + XML,异步JavaScript和XML)开始, 尤其是 AngularJS 推出之后,SPA(Single Page App,单页应用 ...

  9. 《分布式系统:概念与设计》一1.6 实例研究:万维网

    1.6 实例研究:万维网 万维网[www.w3.org I,Berners-Lee 1991]是一个不断发展的系统,用于发布和访问互联网上的资源和服务.通过常用的Web浏览器,用户可以检索和查看多种类 ...

最新文章

  1. 如何取消button的点击特效_如何衡量一个人的 JavaScript 水平?
  2. Linux-进程内存占用情况
  3. hive 创建访问用户_hive创建角色并赋权
  4. 基于SVM的python简单实现验证码识别
  5. failed to load kernel library!处理办法
  6. linux命令:ln
  7. FlashDevelop 3.0.0 Beta2 released
  8. 【Linux】tee命令
  9. CSS3之firefoxsafari背景渐变之争 - [前端技术][转]
  10. Swagger注解说明
  11. 联创宽带上网助手协议的简单分析(三):密码包的构造过程
  12. 微信小程序商城API文档
  13. android语言包,安卓系统添加多国语言包
  14. C语言 线程 进程 优先级,C++线程优先级SetThreadPriority的使用实例
  15. jvm调优转载自http://www.cnblogs.com/xingzc/p/5756119.html
  16. 计算机设计大赛应用软件组,组一览表(计算机设计大赛).pdf
  17. 散列函数和数字签名概念
  18. 可控硅为啥不能用万用表触发?可控硅四种工作象限分析
  19. 歌谣学前端之箭头函数1
  20. java netbeans 控制台乱码_netbeans 中文乱码问题

热门文章

  1. 泰山OFFICE技术讲座:字体属性的上标研究1:上标是什么
  2. STM32学习笔记整理之(0)——新建工程
  3. 《枪炮、病菌与钢铁》之一
  4. STL string迭代器
  5. 【教学类-29-02】20230402《门牌号-黏贴版打印数量调查教学实践(6层*5间)》-(中班《我爱我家》偏数学)
  6. [Err] 1813 - Tablespace ‘`XX`.`XX`‘ exists.
  7. Python 之 异常值/离群值的处理
  8. python-matplotlib-箱线图为不同的箱体设置不同颜色
  9. LaTeX中的中文处理方法
  10. 网桥15式:无线网桥用得好,成本的降低少不了