搭建和运营一个可伸缩的web站点或者应用程序意味着什么?在原始层面上这仅仅是用户经过互联网链接到远程资源-使系统变得可伸缩的部分是将资源、或者访问的资源,分布于多个服务器上。php
像生活中大多数事情同样,当构建一个web服务时花时间提早作好计划从长远看来仍是颇有帮助的;了解一些注意事项和大网站背后的权衡原则能够在建立小型网站时作出更明智的决定。如下是一些影响大规模web系统设计的关键原则:html
以上每一个原则都为设计分布式web架构提供了基础决策。然而,他们也能彼此互斥,例如要实现某个目标就要以另外的做为代价。一个基本的例子:选择经过单纯增长更多的服务器(可扩展性)来增长地址容量,是以可管理性(你必须操做增长的服务器)和成本(服务器的价格)为代价的。前端
当设计任何的web应用程序时,考虑这些关键原则都是很重要的,即便得认可一个设计可能要牺牲它们之中的一个或者多个。node
当设计一个系统架构时,有一些东西是要考虑的:正确的部分是什么,怎样让这些部分很好地融合在一块儿,以及好的折中方法是什么。一般在系统架构须要以前就为它的可扩展性投资不是一个聪明的商业抉择;然而,在设计上的深谋远虑能在将来节省大量的时间和资源。mysql
这部分关注点是几乎全部大型web应用程序中心的一些核心因素:服务、冗余、划分和错误处理。每个因素都包含了选择和妥协,特别是上部分提到的设计原则。为了详细的解析这些,最好是用一个例子来开始。git
有时候你可能会在线上传一张图片。对于那些托管并负责分发大量图片的网站来讲,要搭建一个既节省成本又高效还能具有较低的延迟性(你能快速的获图片)的网站架构确实是一种挑战。github
咱们来假设一个系统,用户能够上传他们的图片到中心服务器,这些图片又可以让一些web连接或者API获取这些图片,就如同如今的Flickr或者Picasa。为了简化的须要,咱们假设应用程序分为两个主要的部分:一个是上传图片到服务器的能力(一般说的写操做),另外一个是查询一个图片的能力。然而,咱们固然想上传功能很高效,可是咱们更关心的是可以快速分发能力,也就是说当某我的请求一个图片的时候(好比,一个web页面或者其它应用程序请求图片)可以快速的知足。这种分发能力很像web服务器或者CDN链接服务器(CDN服务器通常用来在多个位置存储内容一边这些内容可以从地理位置或者物理上更靠近访问它的用户,已达到高效访问的目的)气的做用。web
系统其余重要方面:redis
Figure 1.1是一个简化的功能图。算法
Figure 1.1: 图片主机应用的简化架构图
在这个图片主机的例子里,可碰见系统必需快速,它的数据存储要可靠以及这些全部的属性都应该高度的可扩展。创建这个应用程序的一个小版本不是很重要并且很容易部署在单一的服务器上;然而,这不是这节里的感兴趣部分。假设下咱们想建一个会增加到和Flickr痛让规模的东西。
当要考虑设计一个可扩展的系统时,为功能解耦和考虑下系统每部分的服务都定义一个清晰的接口都是颇有帮助的。在实际中,在这种方式下的系统设计被成为面向服务架构(SOA)。对于这类型的系统,每一个服务有本身独立的方法上下文,以及使用抽象接口与上下文的外部任何东西进行交互,典型的是别的服务的公共API。
把一个系统解构为一些列互补的服务,可以为这些部分从别的部分的操做解耦。这样的抽象帮助在这些服务服、它的基础环境和服务的消费者之间创建清晰的关系。创建这种清晰的轮廓能帮助隔离问题,但也容许各模块相对其它部分独立扩展。这类面向服务设计系统是很是相似面向对象设计编程的。
在咱们的例子中,上传和检索图像的请求都是由同一个服务器处理的;然而,由于系统须要具备伸缩性,有理由要将这两个功能分解为各由本身的服务进行处理。
快速转发(Fast-forward)假定服务处于大量使用中;在这种状况下就很容易看到,读取图像所花的时间中有多少是因为受到了写入操做的影响(由于这两个功能将竞争使用它们共享的资源)。取决于所采用的体系结构,这种影响多是巨大的。即便上传和下载的速度彻底相同(在绝大多数IP网络中都不是这样的状况,大部分下载速度和上传速度之比都至少设计为3:1),文件读取操做通常都是从高速缓存中进行的,而写操做却不得不进行最终的磁盘操做(并且可能要写几回才能达成最后的一致状态)。即便全部内容都已在内存中,或者从磁盘(好比SSD磁盘)中进行读取,数据库写入操做几乎每每都要慢于读取操做。(Pole Position是一个开源的DB基准测试工具,http://polepos.org/,测试结果参见 http://polepos.sourceforge.net/results/PolePositionClientServer.pdf)
这种设计另外一个潜在的问题出在web服务器上,像Apache或者lighttpd一般都有一个可以维持的并发链接数上限(默认状况下在500左右,不过能够更高)和最高流量数,它们会很快被写操做消耗掉。由于读操做能够异步进行,或者采用其它一些像gizp压缩的性能优化或者块传输编码方式,web服务器能够经过在多个请求服务之间切换来知足比最大链接数更多的请求(一台Apache的最大链接数设置为500,它每秒钟提供近千次读请求服务也是正常的)。写操做则不一样,它须要在上传过程当中保持链接,因此大多数家庭网络环境下,上传一个1MB的文件可能须要超过1秒的时间,因此web服务器只能处理500个这样并发写操做请求。
对于这种瓶颈,一个好的规划案例是将读取和写入图片分离为两个独立的服务,如图Figure 1.2.所示。这让咱们能够单独的扩展其中任意一个(由于有可能咱们读操做比写操做要频繁不少),同时也有助于咱们理清每一个节点在作什么。最后,这也避免了将来的忧虑,这使得故障诊断和查找问题更简单,像慢读问题。
这种方法的优势是咱们可以单独的解决各个模块的问题-咱们不用担忧写入和检索新图片在同一个上下文环境中。这两种服务仍然使用全球资料库的图片,可是它们可经过适当的服务接口自由优化它们本身的性能(好比,请求队列,或者缓存热点图片-在这之上的优化)。从维护和成本角度来看,每一个服务按需进行独立规模的规划,这点很是有用,试想若是它们都组合混杂在一块儿,其中一个无心间影响到了性能,另外的也会受影响。
固然,上面的例子在你使用两个不一样端点时能够很好的工做(事实上,这很是相似于云存储和内容分发网络)。虽然有不少方式来解决这样的瓶颈,但每一个都有各自的取舍。
好比,Flickr经过分配用户访问不一样的分片解决这类读/写问题,每个分片只能够处理必定数量的用户,随着用户的增长更多的分片被添加到集群上(参看“Flickr缩影”的描述http://mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html)。在第一个例子中,能够根据实际用途更简单的规划硬件资源(在整个系统中读和写的比例),然而,Flickr规划是根据用户基数(假定每一个用户拥有相同的资源空间)。在前者中一个故障或者问题会致使整个系统功能的降低(好比,所有不能写入文件了),然而Flickr一个分片的故障只会影响到相关的那部分用户。在第一个例子中,更容易操做整个数据集-好比,在全部的图像元数据上更新写入服务用来包含新的元数据或者检索-然而在Flickr架构上每个分片都须要执行更新或者检索(或者须要建立个索引服务来核对元数据-找出哪个才是实际结果)。
为了优雅的处理故障,web架构必须冗余它的服务和数据。例如,单服务器只拥有单文件的话,文件丢失就意味这永远丢失了。丢失数据是个很糟糕的事情,常见的方法是建立多个或者冗余备份。
一样的原则也适用于服务。若是应用有一个核心功能,确保它同时运行多个备份或者版本能够安全的应对单点故障。
在系统中建立冗余能够消除单点故障,能够在紧急时刻提供备用功能。例如,若是在一个产品中同时运行服务的两个实例,当其中一个发生故障或者降级(degrade),系统能够转移(failover)到好的那个备份上。故障转移(Failover)能够自动执行或者人工手动干预。
服务冗余的另外一个关键部分是建立无共享(shared-nothing)架构。采用这种架构,每一个接点均可以独立的运做,没有中心"大脑"管理状态或者协调活动。这能够大大提升可伸缩性(scalability)由于新的接点能够随时加入而不须要特殊的条件或者知识。并且更重要的是,系统没有单点故障。因此能够更好的应对故障。
例如,在咱们的图片服务应用,全部的图片应该都冗余备份在另外的一个硬件上(理想的状况下,在不一样的地理位置,以防数据中心发生大灾难,例如地震,火灾),并且访问图片的服务(见Figure 1.3.)-包括全部潜在的服务请求-也应该冗余。(负载均衡器是个很好的方法冗余服务,可是下面的方法不只仅是负载均衡)
Figure 1.3: 使用冗余的图片存储
咱们可能碰见单一服务器没法存放的庞大数据集。也可能遇到一个须要过多计算资源的操做,致使性能降低,急需增添容量。这些状况下,你都有两种选择:横向或纵向扩展。
纵向扩展意味着对单一服务器增添更多资源。对于一个很是庞大的数据集,这可能意味着为单一服务器增长更多(或更大)的硬盘以存放整个数据集。而对于计算操做,这可能意味着将操做移到一个拥有更快的 CPU 或 更大的内存的服务器中。不管哪一种状况,纵向扩展都是为了使单个服务器可以本身处理更多的方法。
另外一方面,对于横向扩展,则是增长更多的节点。例如庞大的数据集,你能够用第二个服务器来存放部分数据;而对于计算操做,你能够切割计算,或是经过额外的节点加载。想要充分的利用横向扩展的优点,你应该之内在的系统构架设计原则来实现,不然的话,实现的方法将会变成繁琐的修改和切分操做。
说道横向分区,更常见的技术是将你的服务分区,或分片。分区能够经过对每一个功能逻辑集的分割分配而来;能够经过地域划分,也能够经过相似付费 vs. 未付费用户来区分。这种方式的优点是能够经过增添容量来运行服务或实现数据存储。
以咱们的图像服务器为例,将曾经储存在单一的文件服务器的图片从新保存到多个文件服务器中是能够实现的,每一个文件服务器都有本身唯一的图片集。(见图表1.4。)这种构架容许系统将图片保存到某个文件服务器中,在服务器都即将存满时,像增长硬盘同样增长额外的服务器。这种设计须要一种可以将文件名和存放服务器绑定的命名规则。一个图像的名称多是映射所有服务器的完整散列方案的形式。或者可选的,每一个图像都被分配给一个递增的 ID,当用户请求图像时,图像检索服务只须要保存映射到每一个服务器的 ID 范围(相似索引)就能够了。
图表 1.4: 使用冗余和分区实现的图片存储服务
固然,为多个服务器分配数据或功能是充满挑战的。一个关键的问题就是数据局部性;对于分布式系统,计算或操做的数据越相近,系统的性能越佳。所以,一个潜在的问题就是数据的存放遍及多个服务器,当须要一个数据时,它们并不在一块儿,迫使服务器不得不为从网络中获取数据而付出提交翻译昂贵的性能代价。
另外一个潜在的问题是不一致性。当多个不一样的服务读取和写入同一共享资源时,有可能会遭遇竞争状态——某些数据应当被更新,但读取操做刚好发生在更新以前——这种情形下,数据就是不一致的。例如图像托管方案中可能出现的竞争状态,一个客户端发送请求,将其某标题为“狗"的图像更名为”小家伙“。而同时另外一个客户端发送读取此图像的请求。第二个客户端中显示的标题是“狗”仍是“小家伙”是不能明确的。
固然,对于分区还有一些障碍存在,但分区容许将问题——数据、负载、使用模式等——切割成能够管理的数据块。这将极大的提升可扩展性和可管理性,但并不是没有风险。有不少能够下降风险,处理故障的方法;不过篇幅有限,再也不赘述。如有兴趣,可见于此文,获取更多容错和检测的信息。
在设计分布式系统时一些核心问题已经考虑到,如今让咱们来讨论下比较困难的一部分:可伸缩的数据访问。
对于大多数简单的web应用程序,好比LAMP系统,相似于图 Figure 1.5.
Figure 1.5: 简单web应用程序
随着它们的成长,主要发生了两方面的变化:应用服务器和数据库的扩展。在一个高度可伸缩的应用程序中,应用服务器一般最小化而且通常是shared-nothing架构(译注:shared nothing architecture是一 种分布式计算架构,这种架构中不存在集中存储的状态,整个系统中没有资源竞争,这种架构具备很是强的扩张性,在web应用中普遍使用)方式的体现,这使得系统的应用服务器层水平可伸缩。因为这种设计,数据库服务器能够支持更多的负载和服务;在这一层真正的扩展和性能改变开始发挥做用了。
剩下的章节主要集中于经过一些更经常使用的策略和方法提供快速的数据访问来使这些类型服务变得更加迅捷。
Figure 1.6: Oversimplified web application
大多数系统简化为如图 Figure 1.6所示,这是一个良好的开始。若是你有大量的数据,你想快捷的访问,就像一堆糖果摆放在你办公室抽屉的最上方。虽然过于简化,前面的声明暗示了两个困难的问题:存储的可伸缩性和数据的快速访问。
为了这一节内容,咱们假设你有很大的数据存储空间(TB),而且你想让用户随机访问一小部分数据(查看Figure 1.7)。这相似于在图像应用的例子里在文件服务器定位一个图片文件。
Figure 1.7:
Accessing specific data
这很是具备挑战性,由于它须要把数TB的数据加载到内存中;而且直接转化为磁盘的IO。要知道从磁盘读取比从内存读取慢不少倍-内存的访问速度如同敏捷的查克·诺里斯(译注:空手道冠军),而磁盘的访问速度就像笨重的卡车同样。这个速度差别在大数据集上会增长更多;在实数顺序读取上内存访问速度至少是磁盘的6倍,随机读取速度比磁盘快100,000倍(参考“大数据之殇”http://queue.acm.org/detail.cfm?id=1563874)。另外,即便使用惟一的ID,解决获取少许数据存放位置的问题也是个艰巨的任务。这就如同不用眼睛看在你的糖果存放点取出最后一块Jolly Rancher口味的糖果同样。
谢天谢地,有不少方式你可让这样的操做更简单些;其中四个比较重要的是缓存,代理,索引和负载均衡。本章的剩余部分将讨论下如何使用每个概念来使数据访问加快。
缓存利用局部访问原则:最近请求的数据可能会再次被请求。它们几乎被用于计算机的每一层:硬件,操做系统,web浏览器,web应用程序等等。缓存就像短时间存储的内存:它有空间的限制,可是一般访问速度比源数据源快而且包含了大多数最近访问的条目。缓存能够在架构的各个层级存在,可是经常在前端比较常见,在这里一般须要在没有下游层级的负担下快速返回数据。
在咱们的API例子中如何使用缓存来快速访问数据?在这种状况下,有两个地方你能够插入缓存。一个操做是在你的请求层节点添加一个缓存,如图 Figure 1.8.
Figure 1.8: Inserting a cache on your request layer node
直接在一个请求层节点配置一个缓存能够在本地存储相应数据。每次发送一个请求到服务,若是数据存在节点会快速的返回本地缓存的数据。若是数据不在缓存中,请求节点将在磁盘查找数据。请求层节点缓存能够存放在内存和节点本地磁盘中(比网络存储快些)。
Figure 1.9: Multiple caches
当你扩展这些节点后会发生什么呢?如图Figure 1.9所示,若是请求层扩展为多个节点,每一个主机仍然可能有本身的缓存。然而,若是你的负载均衡器随机分配请求到节点,一样的请求将指向不一样的节点,从而增长了缓存的命中缺失率。有两种选择能够解决这个问题:全局缓存和分布式缓存。
全局缓存顾名思义:全部的节点使用同一个缓存空间,这涉及到添加一个服务器,或者某种文件存储系统,速度比访问源存储和经过全部节点访问要快些。每一个请求节点以一样的方式查询本地的一个缓存,这种缓存方案可能有点复杂,由于在客户端和请求数量增长时它很容易被压倒,可是在有些架构里它仍是颇有用的(尤为是那些专门的硬件来使全局缓存变得很是快,或者是固定数据集须要被缓存的)。
在描述图中有两种常见形式的缓存。在图Figure 1.10中,当一个缓存响应没有在缓存中找到时,缓存自身从底层存储中查找出数据。在 Figure 1.11中,当在缓存中招不到数据时,请求节点会向底层去检索数据。
Figure 1.10: Global cache where cache is responsible for retrieval
Figure 1.11: Global cache where request nodes are responsible for retrieval
大多数使用全局缓存的应用程序趋向于第一类,这类缓存能够管理数据的读取,防止客户端大量的请求一样的数据。然而,一些状况下,第二类实现方式彷佛更有意义。好比,若是一个缓存被用于很是大的文件,一个低命中比的缓存将会致使缓冲区来填满未命中的缓存;在这种状况下,将使缓存中有一个大比例的总数据集。另外一个例子是架构设计中文件在缓存中存储是静态的而且不会被排除。(这多是由于应用程序要求周围数据的延迟-某些片断的数据可能须要在大数据集中很是快-在有些地方应用程序逻辑理清排除策略或者热点 比缓存方案好使些)
在分布式缓存(图1.12)中,每一个节点都会缓存一部分数据。若是把冰箱看做食杂店的缓存的话,那么分布式缓存就象是把你的食物分别放到多个地方 —— 你的冰箱、柜橱以及便当盒 ——放到这些便于随时取用的地方就无需一趟趟跑去食杂店了。缓存通常使用一个具备一致性的哈希函数进行分割,如此即可在某请求节点寻找某数据时,可以迅速知道要到分布式缓存中的哪一个地方去找它,以肯定改数据是否从缓存中可得。在这种状况下,每一个节点都有一个小型缓存,在直接到原数据所做处找数据以前就能够向别的节点发出寻找数据的请求。由此可得,分布式缓存的一个优点就是,仅仅经过向请求池中添加新的节点即可以拥有更多的缓存空间。
分布式缓存的一个缺点是修复缺失的节点。一些分布式缓存系统经过在不一样节点作多个备份绕过了这个问题;然而,你能够想象这个逻辑迅速变复杂了,尤为是当你在请求层添加或者删除节点时。即使是一个节点消失和部分缓存数据丢失了,咱们还能够在源数据存储地址获取-所以这不必定是灾难性的!
Figure 1.12: Distributed cache
缓存的伟大之处在于它们使咱们的访问速度更快了(固然前提是正确使用),你选择的方法要在更多请求下更快才行。然而,全部这些缓存的代价是必须有额外的存储空间,一般在放在昂贵的内存中;历来没有嗟来之食。缓存让事情处理起来更快,并且在高负载状况下提供系统功能,不然将会使服务器出现降级。
有一个很流行的开源缓存项目Memcached (http://memcached.org/)(它能够当作一个本地缓存,也能够用做分布式缓存);固然,还有一些其余操做的支持(包括语言包和框架的一些特有设置)。
Memcached 被用做不少大型的web站点,尽管他很强大,但也只是简单的内存key-value存储方式,它优化了任意数据存储和快速检索(o(1))。
Facebook使用了多种不一样的缓存来提升他们站点的性能(查看"Facebook caching and performance")。在语言层面上(使用PHP内置函数调用)他们使用$GLOBALSand APC缓存,这有助于使中间函数调用和结果返回更快(大多数语言都有这样的类库用来提升web页面的性能)。Facebook使用的全局缓存分布在多个服务器上(查看 "Scaling memcached at Facebook"),这样一个访问缓存的函数调用能够使用不少并行的请求在不一样的Memcached 服务器上获取存储的数据。这使得他们在为用户分配数据空间时有了更高的性能和吞吐量,同时有一个中央服务器作更新(这很是重要,由于当你运行上千服务器时,缓存失效和一致性将是一个大挑战)。
如今让咱们讨论下当数据不在缓存中时该如何处理···
简单来讲,代理服务器是一种处于客户端和服务器中间的硬件或软件,它从客户端接收请求,并将它们转交给服务器。代理通常用于过滤请求、记录日志或对请求进行转换(增长/删除头部、加密/解密、压缩,等等)。
图1.13: 代理服务器
当须要协调来自多个服务器的请求时,代理服务器也十分有用,它容许咱们从整个系统的角度出发、对请求流量执行优化。压缩转发(collapsed forwarding)是利用代理加快访问的其中一种方法,将多个相同或类似的请求压缩在同一个请求中,而后将单个结果发送给各个客户端。
假设,有几个节点都但愿请求同一份数据,并且它并不在缓存中。在这些请求通过代理时,代理能够经过压缩转发技术将它们合并成为一个请求,这样一来,数据只须要从磁盘上读取一次便可(见图1.14)。这种技术也有一些缺点,因为每一个请求都会有一些时延,有些请求会因为等待与其它请求合并而有所延迟。无论怎么样,这种技术在高负载环境中是能够帮助提高性能的,特别是在同一份数据被反复访问的状况下。压缩转发有点相似缓存技术,只不过它并不对数据进行存储,而是充当客户端的代理人,对它们的请求进行某种程度的优化。
在一个LAN代理服务器中,客户端不须要经过本身的IP链接到Internet,而代理会将请求相同内容的请求合并起来。这里比较容易搞混,由于许多代理同时也充当缓存(这里也确实是一个很适合放缓存的地方),但缓存却不必定能当代理。
图1.14: 经过代理来合并请求
另外一个使用代理的方式不仅是合并相同数据的请求,同时也能够用来合并靠近存储源(通常是磁盘)的数据请求。采用这种策略可让请求最大化使用本地数据,这样能够减小请求的数据延迟。好比,一群节点请求B部分信息:partB1,partB2等,咱们能够设置代理来识别各个请求的空间区域,而后把它们合并为一个请求并返回一个bigB,大大减小了读取的数据来源(查看图Figure 1.15)。当你随机访问上TB数据时这个请求时间上的差别就很是明显了!代理在高负载状况下,或者限制使用缓存时特别有用,由于它基本上能够批量的把多个请求合并为一个。
Figure 1.15: Using a proxy to collapse requests for data that is spatially close together
值得注意的是,代理和缓存能够放到一块儿使用,但一般最好把缓存放到代理的前面,放到前面的缘由和在参加者众多的马拉松比赛中最好让跑得较快的选手在队首起跑同样。由于缓存从内存中提取数据,速度飞快,它并不介意存在对同一结果的多个请求。可是若是缓存位于代理服务器的另外一边,那么在每一个请求到达cache以前都会增长一段额外的时延,这就会影响性能。
若是你正想在系统中添加代理,那你能够考虑的选项有不少;Squid和Varnish都通过了实践检验,普遍用于不少实际的web站点中。这些代理解决方案针对大部分client-server通讯提供了大量的优化措施。将两者之中的某一个安装为web服务器层的反向代理(reverse proxy,下面负载均衡器一节中解释)能够大大提升web服务器的性能,减小处理来自客户端的请求所需的工做量。
使用索引快速访问数据是个优化数据访问性能公认的策略;可能咱们大多数人都是从数据库了解到的索引。索引用增加的存储空间占用和更慢的写(由于你必须写和更新索引)来换取更快的读取。
你能够把这个概念应用到大数据集中就像应用在传统的关系数据存储。索引要关注的技巧是你必须仔细考虑用户会怎样访问你的数据。若是数据集有不少TBs,可是每一个数据包(payload)很小(可能只有1KB),这时就必须用索引来优化数据访问。在这么大的数据集找到小的数据包是个颇有挑战性的工做由于你不可能在合理的时间內遍历全部数据。甚至,更有可能的是这么大的数据集分布在几个(甚至不少个)物理设备上-这意味着你要用些方法找到指望数据的正确物理位置。索引是最适合的方法作这种事情。
Figure 1.16: Indexes
索引能够做为内容的一个表格-表格的每一项指明你的数据存储的位置。例如,若是你正在查找B的第二部分数据-你如何知道去哪里找?若是你有个根据数据类型(数据A,B,C)排序的索引,索引会告诉你数据B的起点位置。而后你就能够跳转(seek)到那个位置,读取你想要的数据B的第二部分。 (See Figure 1.16.)
这些索引经常存储在内存中,或者存储在对于客户端请求来讲很是快速的本地位置(somewhere very local)。Berkeley DBs (BDBs)和树状数据结构经常按顺序存储数据,很是理想用来存储索引。
经常索引有不少层,看成数据地图,把你从一个地方指向另一个地方,一直到你的获得你想要的那块数据。(SeeFigure 1.17.)
Figure 1.17: Many layers of indexes
索引也能够用来建立一样数据的多个不一样视图(views)。对于大数据集来讲,这是个很棒的方法来定义不一样的过滤器(filter)和类别(sort),而不用建立多个额外的数据拷贝。
例如,想象一下,图片存储系统开始实际上存储的是书的每一页的图像,并且服务容许客户查询这些图片中的文字,搜索每一个主题的全部书的内容,就像搜索引擎容许你搜索HTML内容同样。在这种状况下,全部的书的图片占用了不少不少服务器存储,查找其中的一页给用户显示有点难度。首先,用来查询任意词或者词数组(tuples)的倒排索引(inverse indexes)须要很容易的访问到;而后,导航到那本书的确切页面和位置并获取准确的图片做为返回结果,也有点挑战性。因此,这种境况下,倒排索引应该映射到每一个位置(例如书B),而后B要包含一个索引每一个部分全部单词,位置和出现次数的索引。
能够表示上图Index1的一个倒排索引,可能看起来像下面的样子-每一个词或者词数组对应一个包含他们的书。
Word(s) | Book(s) |
---|---|
being awesome | Book B, Book C, Book D |
always | Book C, Book F |
believe | Book B |
这个中间索引可能看起来像上面的样子,可是可能只包含词,位置和书B的信息。这种嵌套的索引架构要使每一个子索引占用足够小的空间,以防全部的这些信息必须保存在一个大的倒排索引中。
这是大型系统的关键点,由于即便压缩,这些索引也太大,太昂贵(expensive)而难以存储。在这个系统,若是咱们假设咱们世界上的不少书-100,000,000 (see Inside Google Books blog post)-每一个书只有10页(只是为了下面好计算),每页有250个词,那就是2500亿(250 billion)个词。若是咱们假设每一个词有5个字符,每一个字符占用8位(或者1个字节,即便某些字符要用2个字节),因此每一个词占用5个字节,那么每一个词即便只包含一次,这个索引也要占用超过1000GB存储空间。那么,你能够明白建立包含不少其余信息-词组,数据位置和出现次数-的索引,存储空间增加多快了吧。
建立这些中间索引和用更小分段表示数据,使的大数据问题能够获得解决。数据能够分散到多个服务器,访问仍然很快。索引是信息检索(information retrieval)的奠定石,是现代搜索引擎的基础。固然,咱们这段只是浅显的介绍,还有其余不少深刻研究没有涉及-例如如何使索引更快,更小,包含更多信息(例如关联(relevancy)),和无缝的更新(在竞争条件下(race conditions),有一些管理性难题;在海量添加或者修改数据的更新中,尤为还涉及到关联(relevancy)和得分(scoring),也有一些难题)。
快速简便的查找到数据是很重要的;索引是能够达到这个目的有效简单工具。
最后还要讲讲全部分布式系统中另外一个比较关键的部分,负载均衡器。负载均衡器是各类体系结构中一个不可或缺的部分,由于它们担负着将负载在处理服务请求的一组节点中进行分配的任务。这样就可让系统中的多个节点透明地服务于同一个功能(参见图1.18)。它的主要目的就是要处理大量并发的链接并将这些链接分配给某个请求处理节点,从而可以使系统具备伸缩性,仅仅经过添加新节点便能处理更多的请求。
图1.18: 负载均衡器
用于处理这些请求的算法有不少种,包括随机选取节点、循环式选取,甚至能够按照内存或CPU的利用率等等这样特定的条件进行节点选取。负载均衡器能够用软件或硬件设备来实现。近来获得普遍应用的一个开源的软件负载均衡器叫作 HAProxy)。
在分布式系统中,负载均衡器每每处于系统的最前端,这样全部发来的请求才能进行相应的分发。在一些比较复杂的分布式系统中,将一个请求分发给多个负载均衡器也是常事,如图1.19所示。
图1.19: 多重负载均衡器
和代理相似,有些负载均衡器还能够基于请求的类型对不一样的请求进行不一样的处理(技术上讲,这样的叫作反向代理)。
负载均衡器面临的一个难题是怎么管理同用户的session相关的数据。在电子商务网站中,若是你只有一个客户端,那么很容易就能够把用户放入购物车里的东西保存起来,等他下次访问访问时购物车里仍能看到那些东西(这很重要,由于当用户回来发现仍然呆在购物车里的产品时颇有可能就会买它)。然而,若是在一个session中将用户分发到了某个节点,但该用户下次访问时却分发到了另一个节点,这里就有可能产生不一致性,由于新的节点可能就没有保留下用户购物车里的东西。(要是你把6盒子子农夫山泉放到购物车里了,可下次回来一看购物车空了,难道你不会发火吗?) 解决该问题的一个方法是能够使session具备保持性,让同一用户老是分发到同一个节点之上,但这样一来就很难利用相似failover这样的可靠性措施了。若是这样的话,用户的购物车里的东西不会丢,但若是用户保持的那个节点失效,就会出现一种特殊的状况,购物车里的东西不会丢这个假设不再成立了(虽然希望不要把这个假设写到程序里)。固然,这个问题还能够用本章中讲到的其它策略和工具来解决,好比服务以及许多并无讲到的方法(象服务器缓存、cookie以及URL重写)。
若是系统中只有不太多的节点,循环式(round robin)DNS系统这样的方案也许更有意义,由于负载均衡器可能比较贵,并且还额外增长了一层不必的复杂性。固然,在比较大的系统中会有各类各样的调度以及负载均衡算法,简单点的有随机选取或循环式选取,复杂点的能够考虑上利用率以及处理能力这些因素。全部这些算法都是对浏览和请求进行分发,并能提供颇有用的可靠性工具,好比自动failover或者自动提出失效节点(好比节点失去响应)。然而,这些高级特性会让问题诊断难以进行。例如,当系统载荷较大时,负载均衡器可能会移除慢速或者超时的节点(因为节点要处理大量请求),但对其它节点而言,这么作其实是加重了状况的恶化程度。在这时进行大量的监测很是重要,由于系统整体流量和吞吐率可能看上去是在降低(由于节点处理的请求变少了),但个别节点却愈来愈忙得不可开交。
负载均衡器是一种能让你扩展系统能力的简单易行的方式,和本文中所讲的其它技术同样,它在分布式系统架构中起着基础性的做用。负载均衡器还要提供一个比较关键的功能,它必需可以探测出节点的运行情况,好比,若是一个节点失去响应或处于过载状态,负载均衡器能够将其总处理请求的节点池中移除出去,还接着使用系统中冗余的其它不一样节点。
目前为止咱们已经介绍了许多更快读取数据的方法,但另外一个使数据层具伸缩性的重要部分是对写的有效管理。当系统简单的时候,只有最小的处理负载和很小的数据库,写的有多快能够预知;然而,在更复杂的系统,写可能须要几乎没法决定的长久时间。例如,数据可能必须写到不一样数据库或索引中的几个地方,或者系统可能正好处于高负载。这些状况下,写或者任何那一类任务,有可能须要很长的时间,追求性能和可用性须要在系统中建立异步;一个一般的作到那一点的办法是经过队列。
Figure 1.20: Synchronous request
设想一个系统,每一个客户端都在发起一个远程服务的任务请求。每个客户端都向服务器发送它们的请求,服务器尽量快的完成这些任务,并分别返回结果给各个客户端。在一个小型系统,一个服务器(或逻辑服务)能够给传入的客户端请求提供迅速服务,就像它们来的同样快,这种情形应该工做的很好。然而,当服务器收到了超过它所能处理数量的请求时,每一个客户端在产生一个响应前,将被迫等待其余客户端的请求结束。这是一个同步请求的例子,示意在图1.20。
这种同步的行为会严重的下降客户端性能;客户端被迫等待,有效的执行零工做,直到它的请求被应答。添加额外的服务器承担系统负载也不会解决这个问题;即便是有效的负载均衡,为了最大化客户端性能,保证平等的公平的分发工做也是极其困难的。并且,若是服务器处理请求不可及,或者失败了,客户端上行也会失败。有效解决这个问题在于,须要在客户端请求与实际的提供服务的被执行工做之间创建抽象。 图 1.21:用队列管理请求
进入队列。一个队列就像它听起来那么简单:一个任务进入,被加入队列而后工人们只要有能力去处理就会拿起下一个任务。(看图1.21)这些任务多是表明了简单的写数据库,或者一些复杂的事情,像为一个文档生成一个缩略预览图一类的。当一个客户端提交一个任务请求到一个队列,它们不再会被迫等待结果;它们只须要确认请求被正确的接收了。这个确认以后可能在客户端请求的时候,做为一个工做结果的参考。
队列使客户端能以异步的方式工做,提供了一个客户端请求与其响应的战略抽象。换句话说,在一个同步系统,没有请求与响应的区别,所以它们不能被单独的管理。在一个异步的系统,客户端请求一个任务,服务端响应一个任务已收到的确认,而后客户端能够周期性的检查任务的状态,一旦它结束就请求结果。当客户端等待一个异步的请求完成,它能够自由执行其它工做,甚至异步请求其它的服务。后者是队列与消息在分布式系统如何成为杠杆的例子。
队列也对服务中断和失败提供了防御。例如,建立一个高度强健的队列,这个队列可以从新尝试因为瞬间服务器故障而失败的服务请求,是很是容易的事。相比直接暴露客户端于间歇性服务中断,须要复杂的并且常常矛盾的客户端错误处理程序,用一个队列去增强服务质量的担保更为可取。
队列对管理任何大规模分布式系统不一样部分之间的分布式通讯,是一个基础,并且实现它们有许多的方法。有很多开源的队列如 RabbitMQ, ActiveMQ, BeanstalkD,可是有些也用像 Zookeeper的服务,或者甚至像Redis的数据存储。
设计有效的系统来进行快速的大数据访问是有趣的,同时有大量的好工具来帮助各类各样的应用程序进行设计。 这文章只覆盖了一些例子,仅仅是一些表面的东西,但将会愈来愈多--同时在这个领域里必定会继续有更多创新东西。
转自 http://www.oschina.net/translate/scalable-web-architecture-and-distributed-systems