可扩展Web架构与分布式系统文档格式.docx
《可扩展Web架构与分布式系统文档格式.docx》由会员分享,可在线阅读,更多相关《可扩展Web架构与分布式系统文档格式.docx(21页珍藏版)》请在冰豆网上搜索。
设计一个系统可以方便操作是另一个重要的考虑方面,系统的可管理性等同于操作的可伸缩性:
维护和升级。
可管理性需要考虑的事情是当问题发生时方便诊断和了解问题,易于升级和修改,以及系统能简单性的操作(即,例行的操作有没有失败和异常?
)
∙成本:
成本是一个重要的因素。
很明显这包含硬件和软件成本,但同样重要需要考虑的其他方面是部署和维护系统的成本。
开发者构建系统花费的大量时间,运维部署时间,甚至培训时间都需要考虑,成本是总体成本。
以上每个原则都为设计分布式web架构提供了基础决策。
然而,他们也能彼此互斥,例如要实现某个目标就要以另外的作为代价。
一个基本的例子:
选择通过单纯增加更多的服务器(可扩展性)来增加地址容量,是以可管理性(你必须操作增加的服务器)和成本(服务器的价格)为代价的。
当设计任何的web应用程序时,考虑这些关键原则都是很重要的,即使得承认一个设计可能要牺牲它们之中的一个或者多个。
1.2.基础
当设计一个系统架构时,有一些东西是要考虑的:
正确的部分是什么,怎样让这些部分很好地融合在一起,以及好的折中方法是什么。
通常在系统架构需要之前就为它的可扩展性投资不是一个聪明的商业抉择;
然而,在设计上的深谋远虑能在未来节省大量的时间和资源。
这部分关注点是几乎所有大型web应用程序中心的一些核心因素:
服务、冗余、划分和错误处理。
每一个因素都包含了选择和妥协,特别是上部分提到的设计原则。
为了详细的解析这些,最好是用一个例子来开始。
实例:
图片托管应用
有时候你可能会在线上传一张图片。
对于那些托管并负责分发大量图片的网站来说,要搭建一个既节省成本又高效还能具备较低的延迟性(你能快速的获图片)的网站架构确实是一种挑战。
我们来假设一个系统,用户可以上传他们的图片到中心服务器,这些图片又能够让一些web链接或者API获取这些图片,就如同现在的Flickr或者Picasa。
为了简化的需要,我们假设应用程序分为两个主要的部分:
一个是上传图片到服务器的能力(通常说的写操作),另一个是查询一个图片的能力。
然而,我们当然想上传功能很高效,但是我们更关心的是能够快速分发能力,也就是说当某个人请求一个图片的时候(比如,一个web页面或者其它应用程序请求图片)能够快速的满足。
这种分发能力很像web服务器或者CDN连接服务器(CDN服务器一般用来在多个位置存储内容一边这些内容能够从地理位置或者物理上更靠近访问它的用户,已达到高效访问的目的)气的作用。
系统其他重要方面:
∙对图片存储的数量没有限制,所以存储需要可扩展,在图像数量方面需要考虑。
∙图片的下载和请求不需要低延迟。
∙如果用户上传一个图片,图片应该都在那里(图片数据的可靠性)。
∙系统应该容易管理(可管理性)。
∙由于图片主机不会有高利润的空间,所以系统需要具有成本效益。
Figure1.1是一个简化的功能图。
Figure1.1:
图片主机应用的简化架构图
在这个图片主机的例子里,可遇见系统必需快速,它的数据存储要可靠以及这些所有的属性都应该高度的可扩展。
建立这个应用程序的一个小版本不是很重要而且很容易部署在单一的服务器上;
然而,这不是这节里的感兴趣部分。
假设下我们想建一个会增长到和Flickr痛让规模的东西。
服务
当要考虑设计一个可扩展的系统时,为功能解耦和考虑下系统每部分的服务都定义一个清晰的接口都是很有帮助的。
在实际中,在这种方式下的系统设计被成为面向服务架构(SOA)。
对于这类型的系统,每个服务有自己独立的方法上下文,以及使用抽象接口与上下文的外部任何东西进行交互,典型的是别的服务的公共API。
把一个系统解构为一些列互补的服务,能够为这些部分从别的部分的操作解耦。
这样的抽象帮助在这些服务服、它的基础环境和服务的消费者之间建立清晰的关系。
建立这种清晰的轮廓能帮助隔离问题,但也允许各模块相对其它部分独立扩展。
这类面向服务设计系统是非常类似面向对象设计编程的。
在我们的例子中,上传和检索图像的请求都是由同一个服务器处理的;
然而,因为系统需要具有伸缩性,有理由要将这两个功能分解为各由自己的服务进行处理。
快速转发(Fast-forward)假定服务处于大量使用中;
在这种情况下就很容易看到,读取图像所花的时间中有多少是由于受到了写入操作的影响(因为这两个功能将竞争使用它们共享的资源)。
取决于所采用的体系结构,这种影响可能是巨大的。
即使上传和下载的速度完全相同(在绝大多数IP网络中都不是这样的情况,大部分下载速度和上传速度之比都至少设计为3:
1),文件读取操作一般都是从高速缓存中进行的,而写操作却不得不进行最终的磁盘操作(而且可能要写几次才能达成最后的一致状态)。
即使所有内容都已在内存中,或者从磁盘(比如SSD磁盘)中进行读取,数据库写入操作几乎往往都要慢于读取操作。
(PolePosition是一个开源的DB基准测试工具,http:
//polepos.org/,测试结果参见
这种设计另一个潜在的问题出在web服务器上,像Apache或者lighttpd通常都有一个能够维持的并发连接数上限(默认情况下在500左右,不过可以更高)和最高流量数,它们会很快被写操作消耗掉。
因为读操作可以异步进行,或者采用其它一些像gizp压缩的性能优化或者块传输编码方式,web服务器可以通过在多个请求服务之间切换来满足比最大连接数更多的请求(一台Apache的最大连接数设置为500,它每秒钟提供近千次读请求服务也是正常的)。
写操作则不同,它需要在上传过程中保持连接,所以大多数家庭网络环境下,上传一个1MB的文件可能需要超过1秒的时间,所以web服务器只能处理500个这样并发写操作请求。
对于这种瓶颈,一个好的规划案例是将读取和写入图片分离为两个独立的服务,如图Figure1.2.所示。
这让我们可以单独的扩展其中任意一个(因为有可能我们读操作比写操作要频繁很多),同时也有助于我们理清每个节点在做什么。
最后,这也避免了未来的忧虑,这使得故障诊断和查找问题更简单,像慢读问题。
这种方法的优点是我们能够单独的解决各个模块的问题-我们不用担心写入和检索新图片在同一个上下文环境中。
这两种服务仍然使用全球资料库的图片,但是它们可通过适当的服务接口自由优化它们自己的性能(比如,请求队列,或者缓存热点图片-在这之上的优化)。
从维护和成本角度来看,每个服务按需进行独立规模的规划,这点非常有用,试想如果它们都组合混杂在一起,其中一个无意间影响到了性能,另外的也会受影响。
当然,上面的例子在你使用两个不同端点时可以很好的工作(事实上,这非常类似于云存储和内容分发网络)。
虽然有很多方式来解决这样的瓶颈,但每个都有各自的取舍。
比如,Flickr通过分配用户访问不同的分片解决这类读/写问题,每一个分片只可以处理一定数量的用户,随着用户的增加更多的分片被添加到集群上(参看“Flickr缩影”的描述
冗余(Redundancy)
为了优雅的处理故障,web架构必须冗余它的服务和数据。
例如,单服务器只拥有单文件的话,文件丢失就意味这永远丢失了。
丢失数据是个很糟糕的事情,常见的方法是创建多个或者冗余备份。
同样的原则也适用于服务。
如果应用有一个核心功能,确保它同时运行多个备份或者版本可以安全的应对单点故障。
在系统中创建冗余可以消除单点故障,可以在紧急时刻提供备用功能。
例如,如果在一个产品中同时运行服务的两个实例,当其中一个发生故障或者降级(degrade),系统可以转移(failover)到好的那个备份上。
故障转移(Failover)可以自动执行或者人工手动干预。
服务冗余的另一个关键部分是创建无共享(shared-nothing)架构。
采用这种架构,每个接点都可以独立的运作,没有中心”大脑”管理状态或者协调活动。
这可以大大提高可伸缩性(scalability)因为新的接点可以随时加入而不需要特殊的条件或者知识。
而且更重要的是,系统没有单点故障。
所以可以更好的应对故障。
例如,在我们的图片服务应用,所有的图片应该都冗余备份在另外的一个硬件上(理想的情况下,在不同的地理位置,以防数据中心发生大灾难,例如地震,火灾),而且访问图片的服务(见Figure1.3.)-包括所有潜在的服务请求-也应该冗余。
(负载均衡器是个很好的方法冗余服务,但是下面的方法不仅仅是负载均衡)
Figure1.3:
使用冗余的图片存储
分区
我们可能遇见单一服务器无法存放的庞大数据集。
也可能遇到一个需要过多计算资源的操作,导致性能下降,急需增添容量。
这些情况下,你都有两种选择:
横向或纵向扩展。
纵向扩展意味着对单一服务器增添更多资源。
对于一个非常庞大的数据集,这可能意味着为单一服务器增加更多(或更大)的硬盘以存放整个数据集。
而对于计算操作,这可能意味着将操作移到一个拥有更快的CPU或更大的内存的服务器中。
无论哪种情况,纵向扩展都是为了使单个服务器能够自己处理更多的方法。
另一方面,对于横向扩展,则是增加更多的节点。
例如庞大的数据集,你可以用第二个服务器来存放部分数据;
而对于计算操作,你可以切割计算,或是通过额外的节点加载。
想要充分的利用横向扩展的优势,你应该以内在的系统构架设计原则来实现,否则的话,实现的方法将会变成繁琐的修改和切分操作。
说道横向分区,更常见的技术是将你的服务分区,或分片。
分区可以通过对每个功能逻辑集的分割分配而来;
可以通过地域划分,也可以通过类似付费vs.未付费用户来区分。
这种方式的优势是可以通过增添容量来运行服务或实现数据存储。
以我们的图像服务器为例,将曾经储存在单一的文件服务器的图片重新保存到多个文件服务器中是可以实现的,每个文件服务器都有自己惟一的图片集。
(见图表1.4。
)这种构架允许系统将图片保存到某个文件服务器中,在服务器都即将存满时,像增加硬盘一样增加额外的服务器。
这种设计需要一种能够将文件名和存放服务器绑定的命名规则。
一个图像的名称可能是映射全部服务器的完整散列方案的形式。
或者可选的,每个图像都被分配给一个递增的ID,当用户请求图像时,图像检索服务只需要保存映射到每个服务器的ID范围(类似索引)就可以了。
图1.4:
使用冗余和分区实现的图片存储服务
当然,为多个服务器分配数据或功能是充满挑战的。
一个关键的问题就是数据局部性;
对于分布式系统,计算或操作的数据越相近,系统的性能越佳。
因此,一个潜在的问题就是数据的存放遍布多个服务器,当需要一个数据时,它们并不在一起,迫使服务器不得不为从网络中获取数据而付出昂贵的性能代价。
另一个潜在的问题是不一致性。
当多个不同的服务读取和写入同一共享资源时,有可能会遭遇竞争状态——某些数据应当被更新,但读取操作恰好发生在更新之前——这种情形下,数据就是不一致的。
例如图像托管方案中可能出现的竞争状态,一个客户端发送请求,将其某标题为“狗”的图像改名为”小家伙“。
而同时另一个客户端发送读取此图像的请求。
第二个客户端中显示的标题是“狗”还是“小家伙”是不能明确的。
当然,对于分区还有一些障碍存在,但分区允许将问题——数据、负载、使用模式等——切割成可以管理的数据块。
这将极大的提高可扩展性和可管理性,但并非没有风险。
有很多可以降低风险,处理故障的方法;
不过篇幅有限,不再赘述。
若有兴趣,可见于此文,获取更多容错和检测的信息。
1.3.构建高效和可伸缩的数据访问模块
在设计分布式系统时一些核心问题已经考虑到,现在让我们来讨论下比较困难的一部分:
可伸缩的数据访问。
对于大多数简单的web应用程序,比如LAMP系统,类似于图
Figure1.5.
Figure1.5:
简单web应用程序
随着它们的成长,主要发生了两方面的变化:
应用服务器和数据库的扩展。
在一个高度可伸缩的应用程序中,应用服务器通常最小化并且一般是shared-nothing架构(译注:
sharednothingarchitecture是一种分布式计算架构,这种架构中不存在集中存储的状态,整个系统中没有资源竞争,这种架构具有非常强的扩张性,在web应用中广泛使用)方式的体现,这使得系统的应用服务器层水平可伸缩。
由于这种设计,数据库服务器可以支持更多的负载和服务;
在这一层真正的扩展和性能改变开始发挥作用了。
剩下的章节主要集中于通过一些更常用的策略和方法提供快速的数据访问来使这些类型服务变得更加迅捷。
Figure1.6:
Oversimplifiedwebapplication
大多数系统简化为如图
Figure1.6所示,这是一个良好的开始。
如果你有大量的数据,你想快捷的访问,就像一堆糖果摆放在你办公室抽屉的最上方。
虽然过于简化,前面的声明暗示了两个困难的问题:
存储的可伸缩性和数据的快速访问。
为了这一节内容,我们假设你有很大的数据存储空间(TB),并且你想让用户随机访问一小部分数据(查看Figure1.7)。
这类似于在图像应用的例子里在文件服务器定位一个图片文件。
Figure1.7:
Accessingspecificdata
这非常具有挑战性,因为它需要把数TB的数据加载到内存中;
并且直接转化为磁盘的IO。
要知道从磁盘读取比从内存读取慢很多倍-内存的访问速度如同敏捷的查克·
诺里斯(译注:
空手道冠军),而磁盘的访问速度就像笨重的卡车一样。
这个速度差异在大数据集上会增加更多;
在实数顺序读取上内存访问速度至少是磁盘的6倍,随机读取速度比磁盘快100,000倍(参考“大数据之殇”http:
//queue.acm.org/detail.cfm?
id=1563874)。
另外,即使使用唯一的ID,解决获取少量数据存放位置的问题也是个艰巨的任务。
这就如同不用眼睛看在你的糖果存放点取出最后一块JollyRancher口味的糖果一样。
谢天谢地,有很多方式你可以让这样的操作更简单些;
其中四个比较重要的是缓存,代理,索引和负载均衡。
本章的剩余部分将讨论下如何使用每一个概念来使数据访问加快。
缓存
缓存利用局部访问原则:
最近请求的数据可能会再次被请求。
它们几乎被用于计算机的每一层:
硬件,操作系统,web浏览器,web应用程序等等。
缓存就像短期存储的内存:
它有空间的限制,但是通常访问速度比源数据源快并且包含了大多数最近访问的条目。
缓存可以在架构的各个层级存在,但是常常在前端比较常见,在这里通常需要在没有下游层级的负担下快速返回数据。
在我们的API例子中如何使用缓存来快速访问数据?
在这种情况下,有两个地方你可以插入缓存。
一个操作是在你的请求层节点添加一个缓存,如图
Figure1.8.
Figure1.8:
Insertingacacheonyourrequestlayernode
直接在一个请求层节点配置一个缓存可以在本地存储相应数据。
每次发送一个请求到服务,如果数据存在节点会快速的返回本地缓存的数据。
如果数据不在缓存中,请求节点将在磁盘查找数据。
请求层节点缓存可以存放在内存和节点本地磁盘中(比网络存储快些)。
Figure1.9:
Multiplecaches
当你扩展这些节点后会发生什么呢?
如图Figure1.9所示,如果请求层扩展为多个节点,每个主机仍然可能有自己的缓存。
然而,如果你的负载均衡器随机分配请求到节点,同样的请求将指向不同的节点,从而增加了缓存的命中缺失率。
有两种选择可以解决这个问题:
全局缓存和分布式缓存。
全局缓存
全局缓存顾名思义:
所有的节点使用同一个缓存空间,这涉及到添加一个服务器,或者某种文件存储系统,速度比访问源存储和通过所有节点访问要快些。
每个请求节点以同样的方式查询本地的一个缓存,这种缓存方案可能有点复杂,因为在客户端和请求数量增加时它很容易被压倒,但是在有些架构里它还是很有用的(尤其是那些专门的硬件来使全局缓存变得非常快,或者是固定数据集需要被缓存的)。
在描述图中有两种常见形式的缓存。
在图Figure1.10中,当一个缓存响应没有在缓存中找到时,缓存自身从底层存储中查找出数据。
在
Figure1.11中,当在缓存中招不到数据时,请求节点会向底层去检索数据。
Figure1.10:
Globalcachewherecacheisresponsibleforretrieval
Figure1.11:
Globalcachewhererequestnodesareresponsibleforretrieval
大多数使用全局缓存的应用程序趋向于第一类,这类缓存可以管理数据的读取,防止客户端大量的请求同样的数据。
然而,一些情况下,第二类实现方式似乎更有意义。
比如,如果一个缓存被用于非常大的文件,一个低命中比的缓存将会导致缓冲区来填满未命中的缓存;
在这种情况下,将使缓存中有一个大比例的总数据集。
另一个例子是架构设计中文件在缓存中存储是静态的并且不会被排除。
(这可能是因为应用程序要求周围数据的延迟-某些片段的数据可能需要在大数据集中非常快-在有些地方应用程序逻辑理清排除策略或者热点
比缓存方案好使些)
分布式缓存
在分布式缓存(图1.12)中,每个节点都会缓存一部分数据。
如果把冰箱看作食杂店的缓存的话,那么分布式缓存就象是把你的食物分别放到多个地方——你的冰箱、柜橱以及便当盒——放到这些便于随时取用的地方就无需一趟趟跑去食杂店了。
缓存一般使用一个具有一致性的哈希函数进行分割,如此便可在某请求节点寻找某数据时,能够迅速知道要到分布式缓存中的哪个地方去找它,以确定改数据是否从缓存中可得。
在这种情况下,每个节点都有一个小型缓存,在直接到原数据所作处找数据之前就可以向别的节点发出寻找数据的请求。
由此可得,分布式缓存的一个优势就是,仅仅通过向请求池中添加新的节点便可以拥有更多的缓存空间。
分布式缓存的一个缺点是修复缺失的节点。
一些分布式缓存系统通过在不同节点做多个备份绕过了这个问题;
然而,你可以想象这个逻辑迅速变复杂了,尤其是当你在请求层添加或者删除节点时。
即便是一个节点消失和部分缓存数据丢失了,我们还可以在源数据存储地址获取-因此这不一定是灾难性的!
Figure1.12:
Distributedcache
缓存的伟大之处在于它们使我们的访问速度更快了(当然前提是正确使用),你选择的方法要在更多请求下更快才行。
然而,所有这些缓存的代价是必须有额外的存储空间,通常在放在昂贵的内存中;
从来没有嗟来之食。
缓存让事情处理起来更快,而且在高负载情况下提供系统功能,否则将会使服务器出现降级。
有一个很流行的开源缓存项目Memcached
(http:
//memcached.org/)(它可以当做一个本地缓存,也可以用作分布式缓存);
当然,还有一些其他操作的支持(包括语言包和框架的一些特有设置)。
Memcached被用作很多大型的web站点,尽管他很强大,但也只是简单的内存key-value存储方式,它优化了任意数据存储和快速检索(o
(1))。
Facebook使用了多种不同的缓存来提高他们站点的性能(查看”Facebookcachingandperformance”)。
在语言层面上(使用PHP内置函数调用)他们使用$GLOBALSandAPC缓存,这有助于使中间函数调用和结果返回更快(大多数语言都有这样的类库用来提高web页面的性能)。
Facebook使用的全局缓存分布在多个服务器上(查看
”ScalingmemcachedatFacebook”),这样一个访问缓存的函数调用可以使用很多并行的请求在不同的Memcached服务器上获取存储的数据。
这使得他们在为用户分配数据空间时有了更高的性能和吞吐量,同时有一个中央服务器做更新(这非常重要,因为当你运行上千服务器时,缓存失效和一致性将是一个大挑战)。
现在让我们讨论下当数据不在缓存中时该如何处理·
·
代理
简单来说,代理服务器是一种处于客户端和服务器中间的硬件或软件,它从客户端接收请求,并将它们转交给服务器。
代理一般用于过滤请求、记录日志或对请求进行转换(增加/删除头部、加密/解密、压缩,等等)。
图1.13:
代理服务器
当需要协调来自多个服务器的请求时,代理服务器也十分有用,它允许我们从整个系统的角度出发、对请求流量执行优化。
压缩转发(collapsedforwarding)是利用代理加快访问的其中一种方法,将多个相同或相似的请求压缩在同一个请求中,然后将单个结果发送给各个客户端。
假设,有几个节点都希望请求同一份数据,而且它并不在缓存中。
在这些请求经过代理时,代理可以通过压缩转发技术将它们合并成为一个请求,这样一来,数据只需要从磁盘上读取一次即可(见图1.14)。
这种技术也有一些缺点,由于每个请求都会有一些时延,有些请求