可伸缩Web架构与分布式系统
本文由 伯乐在线 - narutoying 翻译自 Aosabook。欢迎加入技术翻译小组。转载请参见文章末尾处的要求。
开源软件近年来已变为构建一些大型网站的基础组件。并且伴随着网站的成长,围绕着它们架构的最佳实践和指导准则已经显露。这篇文章旨在涉及一些在设计大型网站时需要考虑的关键问题和一些为达到这些目标所使用的组件。本文主要关注于Web系统,然而其中的一些内容同样适用于其他分布式系统。
1、Web分布式系统设计准则
构建和运行一个可伸缩的网站或应用来说究竟意味着什么?从一个基本的层面来看,就是将用户和远程资源通过互联网连接起来——将其变得可伸缩的部分,是指这些资源或者访问这些资源是分布式的、贯穿于多个服务器。像很多生活中的事情一样,在构建一个web服务时花费时间提前做好计划将会帮助服务能够长久运行;理解一些大型网站背后的考量和权衡会在创建小型网站时帮助做出更明智的决定。下面是一些会影响到设计大型可伸缩性Web系统的关键准则:
可用性
一个网站的正常运行时间,对于很多公司的信誉和功能来说都是至关重要的。对于一些大型在线零售网站,甚至是几分钟的不可用都会导致数以千万计美元的收入损失,所以将他们的系统设计成不间断可用和能够弹性恢复既是一项基础业务也是一个技术需求。分布式系统的高可用性需要仔细考虑关键组件冗余,快速恢复部分有问题的系统,并且当出现问题时能够做到优雅降级。
性能
性能已成为大多数网站一个很重要的考量指标。一个网站的速度会影响到使用和用户满意度,同样也会影响到搜索引擎排名,将直接关系到网站收入和用户保持力(黏性)。因此,关键之处就在于创建一个为快速响应、低延迟的系统。
可靠性
一个系统需要做到可靠,这样才能使得对于(固定)数据的请求始终会返回同样的数据。如果数据发生变化或者更新,那相同的请求应该返回新的数据。用户需要知道,一旦一些数据被写入、存储到系统中,那么系统就会持久化(这些数据)并且能够让人信赖随时能够检索。
可伸缩性
对于任何大型分布式系统,系统规模只是可伸缩性需要考虑的一个方面。同样重要的是,增加容量能够处理更大量的负载所需的工作,通常在系统可伸缩性方面被提及到。可伸缩性会涉及到系统的很多不同的因素:系统额外还能够处理多少流量,是否能够轻易增加存储容量,还能多处理多少事务。
可管理性
设计一个易于运维的系统是另一个重要考量点。系统的可管理型等同于操作的可伸缩性:维护和变更。可管理性需要考虑的有:当问题发生时能够便于诊断和理解,便于进行变更和修改,并且系统易于操作。(比如系统是否能够进行例行操作而不带来失败或者异常?)
成本
成本是一个重要因素。这明显会包括硬件和软件成本,但同样还要考虑到一些其他方面来部署、运维系统,比如构建系统所需的开发时间,运行系统所需的运维工作量,甚至所需的培训都要被考虑在内。成本是指拥有系统的总成本(原句是Cost is the total cost of ownership.)。
这些准则中的每一条都提供了在设计一个分布式web系统架构时作决定的基本原则。但是,他们也可能互相矛盾,比如达到某一目标是以牺牲另一个为代价的。一个典型的例子:专注于系统容量时,选择通过简单增加更多机器(可伸缩性)的代价是(增加了)可管理性(你需要运维更多的服务器)和成本(更多服务器价格)。当设计任何web应用时,这些关键准则都是需要考量的,即使不得不承认,一个设计可能会牺牲它们中的一个或更多。
2、基本原理
对于系统架构来说,有一些事情需要考虑:什么是正确的组件,这些组件如何协作,需要做哪些正确的权衡。在真正需要可伸缩性之前的投资通常并不是一个明智的商业提议,然而,一些设计中的远见卓识将会在未来节省真正的时间和资源。
本节着眼于一些对于几乎所有大型Web应用都非常核心的因素:服务,冗余,分期和失败处理。每个因素均包含有选择和妥协,特别是在上一节提到的准则上下文中。为了详细解释这些,最好的方式就是从一个例子开始。
举例:图片托管应用
你很可能已经在网上上传过一张图片。对于托管和传送大量图片的大型网站来说,将会在架构建设上遇到很多挑战,比如成本效益、高可用性和低延迟性(能够快速检索)。
设想一个这样的系统:用户可以将他们的图片上传到一个中央服务器,并且图片可以通过一个web链接或者API(应用程序接口)进行请求,就像Flickr或者Picasa一样。为了简单起见,我们假定这个应用有两个关键部分:能够上传(写入)一张图片到服务器,能够查询一张图片。虽然我们希望上传能够更快速,但我们最关心的是系统能够快速分发用户请求的图片(比如图片可以被请求用于一张网页或是其他应用)。这些跟一个web服务器或者CDN(内容分发网络) edge server(CDN所使用的服务器,用于在很多位置存放内容,这样内容在地理/物理上更接近用户,起到更高性能的作用)所提供的功能非常类似。
系统其他重要的方面
- 对于存储的图片数量没有设限,所以就图片数量而言,需要考虑存储的可伸缩性。
- 对于图片的下载/请求需要做到低延迟。
- 如果一个用户上传了一张图片,那该图片应该总是存在的。(图片的数据可靠性)
- 系统需要易于管理(可管理型)。
- 由于图片托管不会带来很高的利润,所以系统需要做到有成本效益的。
图片1.1:图片托管应用的简化架构图
在这个图片托管例子中,系统必须做到(让用户)可感知到快速,存储数据的可靠性和那些所有高可伸缩的特征。构建一个小型的托管于单台服务器上的应用过于简单,也没有意义,对于本章来说也没有乐趣所在。来假设下,我们想要构建出可以成长为像Flickr一样的庞然大物。
服务
当考虑可伸缩系统的设计时,(服务)有助于各功能去耦并且通过一个清晰定义的接口思考系统的每个部分。在实践中,这种方式的系统设计表明其拥有一个面向服务的架构(SOA)。对于这些类型的系统,每个服务都有它们各自确切的功能上下文,并且和该上下文以外的任何交互均是与一个抽象的接口进行的,特别是另一个服务的公有接口。
将一个系统拆解为一个互补的服务集合解耦了那些相互间的操作。这种抽象有助于建立服务间明确的关系、潜在的(运行)环境、服务的消费者。通过这些清晰的描绘有助于隔离问题,并且允许每个部分能够相互独立地进行扩展。这种面向服务的系统设计有点类似与面向对象编程。
在我们的例子中,所有上传和获取图片的请求都是在同一服务器上处理,但是,如果系统想要达到可伸缩,那么将这两个功能拆分成各自的服务是非常明智的。
快进下,假设这些服务被大量使用;这样的场景将非常易于看到更久(原文此处是how longer writes will impact the time it takes to read the images)的写操作会如何影响(系统)读取图片的时间(因为这两个功能会竞争共享资源)。在这样的架构下,这种影响是真实存在的。即使上传和下载速度是一样的(对于大多数IP网络来说不一定是,因为大多数都是设计成下载速度与上传速度3:1的比例),文件通常直接从缓存中读取,而写入则最终必须到达磁盘(在最终一致的场景中可能会被写入多次)。即使所有东西都是从内存或者磁盘(比如SSD固态硬盘)读取,数据库的写入操作总还是比读取要慢(Pole Position, 一个开源的数据库评测工具
http://polepos.org/ http://polepos.sourceforge.net/results/PolePositionClientServer.pdf)。
另一个潜在的设计问题是,一个像Apache或者lighttpd的web服务器,通常有一个它可以维持并发连接数的上线(默认大约在500左右,但可以调得更高),并且在高流量下,写操作将很快消耗完所有(连接资源)。由于读操作可以异步进行,或者利用其它性能调优如gzip压缩或者chunked transfer encoding,web服务器可以转换为更快服务读操作、更快切换客户端,从而比最大连接数每秒服务更多的请求(Apache最大连接数设置为500,但一般都能每秒服务数千个请求)。写操作,在另一方面,倾向于在上传过程中维护一个打开状态的连接,所有上传一个1M大小的文件在大多数家庭网络上将花费超过1秒的视角,所以web服务器只能同时处理500个写操作。
图 1.2: 读写分离
将图片的读、写操作拆分成各自的服务是一个应对这种瓶颈很好的解决方案,如图1.2。这样允许我们能够独立的扩展它们(我们通常会读大于写),而且有助于将每一点的进展情况看得更加清晰。最后,这样可以分离未来的担心,可以更简单地解决像读操作缓慢的问题,并做到可伸缩。
这种方法的好处在于我们能够独立(不影响其他)解决问题——我们不用担心在同一上下文中写入、读取新的图片。这两者(服务)仍然影响着全部的图片,但均能通过service-appropriate方法优化它们的性能(比如让请求排队,或者缓存受欢迎的图片——更多种方式请见下文)。从一个维护和成本的视角出发,每个服务均能独立、按需伸缩是非常好的,因为如果它们被组合、混合在一起,在上面讨论的场景下,可能某一(服务)不经意间就会影响到其他(服务)的性能。
当然,当你考虑着两个不同点时,上面的例子能够工作得很好(事实上,这跟一些云存储提供商的实现方案和CDN很类似)。尽管还有很多方法来处理这些类型的瓶颈,但每个都有不同方面的权衡。
例如,Flickr通过将用户分布在不同区域的方法来解决读/写问题,比如每个分区只处理一定数量的用户,随着用户的增加,集群会更多的分区(参考Flickr可伸缩报告,, http://mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html)。在第一个例子中,基于实际使用(整个系统的读写操作数量)可以更容易地伸缩硬件,然而Flickr是基于它的用户(但强制假设用户的使用率均等,所以仍有额外的容量)。对于前者来说,停电或者一个服务的问题就会降低整个系统的功能性(比如没人可以写入文件),然而Flickr的一个分区停电仅会影响到这个分区相应的用户。第一个例子易于操作整个数据集,比如升级写入服务来包含新的元数据或者搜索所有的图片元数据,然而在Flickr的架构下,每个分区均需要被更新或搜索(或者一个搜索服务需要能够整理相关元数据——事实上他们确实这么做)。
对于这些系统来说没有孰对孰错,而是帮助我们回到本章开头所说的准则,判断系统需求(读多还是写多还是两者都多,并发程度,跨数据集查询,搜索,排序等),检测不同的取舍,理解系统为什么会失败并且有可靠的计划来应对失败的发生。
冗余
为了能够优雅地处理失败问题,Web架构必须做到服务和数据的冗余。比如,如果在单台服务器上仅有一份文件,那么失去那台服务器就意味着丢失那份文件。丢失数据很少是件好事情,而通常的解决方案是创建多个、冗余的备份。
该准则同样适用于服务。如果应用有一个核心功能,那么通过确保多个拷贝(多个同类服务实例)或者版本同时运行能够免于单点失败的情况。
在一个系统中创建冗余能够去除单点失败,并提供一个备份或在必要的紧急时刻替换功能。例如,如果在生产环境有同一服务的两个实例在运行,其中一个失败或者降级了,系统可以(启动)failover到那个健康状态的服务。Failover可以自动发生或者需要人工干预。
服务冗余的另一个关键点在于创建一个非共享的架构(译者理解为无状态架构)。通过这种架构,每个节点都能够独立操作,并且没有中央“大脑”来管理状态或者协调其他节点的活动。这对于可伸缩性非常有帮助,因为新的节点不需要特殊的条件或知识就能加入(到集群)。但是,最重要的是在这些系统中不会存在单点失败问题,所以它们能够更加弹性地面对失败。
例如,在我们的图片服务应用,所有的图片会在另一个地方的硬件中有冗余的备份(理想情况是在一个不同的地理位置,以防地震或者数据中心火灾这类的灾难发生),而访问图片的服务同样是冗余的,所有(服务)都可能会服务请求。(见图1.3)(负载均衡器可以将其变为现实,详情请见下文。
图1.3:图片托管应用,带有冗余特性
分区
单台服务器可能没法放下海量数据集。也可能是一个操作需要太多计算资源,消耗性能,使得有必要增加(系统)容量。无论是哪种情况,你都有两种选择:垂直扩展(scale vertically)或者水平扩展(scale horizontally)。
垂直扩展意味着在单台服务器上增加更多的资源。所以对于大数据来说,这意味着增加更多(更大容量)的硬盘以便让单台服务器能够容纳整个数据集。对于计算操作的场景,这意味着将计算任务交给一台拥有更快CPU或者更多内存的大型服务器。对于每种场景,垂直扩展是通过自身(个体)能够处理更多的方式来达到目标的。
另一方面,对于水平扩展来说,就是增加更多的节点。对于大数据集,可能是用另一台服务器来存储部分数据集;而对于计算资源来说,则意味着将操作进行分解或者加载在一些额外的节点上。为了充分利用水平扩展的优势,这(译者认为此处指代的是系统支持水平扩展。垂直扩展对于应用来说无需修改,通常升级机器即可达到目的。而水平扩展就要求应用架构能够支持这种方式的扩展,因为数据、服务都是分布式的,需要从软件层面来支持这一特性,从而做到数据、服务的水平可扩展。)应该被天然地包含在系统架构设计准则里,否则想要通过修改、隔离上下文来达到这一点将会相当麻烦。
对于水平扩展来说,通常方法之一就是将你的服务打散、分区。分区可以是分布式的,这样每个逻辑功能集都是分离的;分区可通过地理边界来划分,或者其他标准如付费/未付费用户。这些设计的好处在于它们能够使得服务或数据存储易于增加容量。
在我们的图片服务器例子中,可以将单台存储图片的服务器替换为多台文件服务器,每台保存各自单独的图片集。(见图1.4)这样的架构使得系统能够往各台文件服务器中存入图片,当磁盘快满时再增加额外的服务器。这种设计将需要一种命名机制,将图片的文件名与所在服务器关联起来。一个图片的名字可以通过服务器间一致性Hash机制来生成。或者另一种选择是,可以分配给每张图片一个增量ID,当一个客户端请求一张图片时,图片检索服务只需要维护每台服务器对应的ID区间即可(类似索引)。
图1.4:图片托管应用,加入冗余和分区特性
当然,将数据或功能分布在多台服务器上会带来很多挑战。关键问题之一是数据局部性(data locality);在分布式系统里,数据离操作或者计算点越近,系统性能就越高。因此将数据分布在多台服务器可能是有问题的,任何需要(数据)的时候都可能不在本地,使得服务器必须通过网络来获取所需的信息。
另一个潜在问题是不一致性。当不同的服务在对同一块共享资源进行读、写时,可能是另一个服务或者数据,就会存在竞争条件的机会——当一些数据将被更新,但读操作先于更新发生——这类场景下数据就会发生不一致。例如,在图片托管这个场景下,竞争条件会发生在一个客户端发出将小狗图片标题由“Dog”更新为“Gizmo”的请求,但同时另一个客户端正在读取该图片这样的情况下。在这样的情况下,第二个客户端就不清楚接收到的标题会是“Dog”还是“Gizmo”。(此端译者并不理解作者原意,因为无论是在分布式还是在单机环境下,都可能出现同时读、写的操作,返回结果取决于底层存储对同时接收到的请求处理的调度,可能读先于写,反之亦然,故此处的例子用来解释不一致是否并不恰当?)
诚然,关于数据分区还存在很多阻碍,但分区通过数据、负载、用户使用模式等使得每个问题分解成易处理的部分。这样有助于可伸缩性和可管理型,但也不是没有风险的。有很多方法能够用来降低风险、处理失败问题;但为了简化篇幅,本章就不覆盖(这些方法)了。如果你有兴趣想了解更多,可以看下我博客上发表的关于容错性和监控的相关文章。
开源软件近年来已变为构建一些大型网站的基础组件。并且伴随着网站的成长,围绕着它们架构的最佳实践和指导准则已经显露。这篇文章旨在涉及一些在设计大型网站时需要考虑的关键问题和一些为达到这些目标所使用的组件。上篇文章介绍了Web分布式系统设计准则和基本原理,本文介绍构建快速、可伸缩数据访问的组件。
(上文)谈及了在设计分布式系统中需要考虑的一些核心问题,现在让我们来聊聊(比较)困难的部分:访问数据的可伸缩性。
大多数简单的web应用,例如LAMP栈应用,看上去如图1.5
图1.5:简单的web应用
随着它们的成长,会有两个主要的挑战:访问应用服务器和数据库的可伸缩性。在一个高可伸缩的应用设计中,应用(或者web)服务器通常会最小化(minimized)并通常表现为一个非共享(无状态)架构。这样使得系统的应用服务层能够很好地进行伸缩。这样数据的结果是,压力被向下推到了数据库服务器和相关(底层)支持服务;真正的伸缩和性能挑战就在这一层起到作用。
本章余下部分致力于(介绍)一些更加通用的策略和方法,通过更快的数据访问使得这些类型的服务更加快速和可伸缩。
图1.6:极简的web应用
大多数系统可以极度简化为像图1.6这样的。这是一个很好的开始。如果你有大量的数据且希望快速、简单地访问,就像你把糖果藏在你桌子第一个抽屉里。虽然被极度简化,前面观点仍暗示着两个难题:存储的可伸缩性和数据的快速访问。
为了本节,我们假设你有数以TB计的数据并且希望能让用户随机访问这些数据的一小部分。(见图1.7)这就类似于在图片应用例子里定位文件服务器上一个图片文件的位置。
图1.7:访问特定的数据
由于很难将TB级的数据加载到内存,所以这会使得事情变得非常有挑战性;这(种访问)将直接变为磁盘IO操作。从磁盘读取会比从内存要慢得多——访问内存就像Chuck Norris一样快,然而访问磁盘比DMV线还要慢。这样的速度差异对于大数据来说比较客观(This speed difference really adds up for large data sets);顺序读方面访问内存的速度是访问磁盘的6倍,而在随机读方面,前者是后者的十万倍(参见”The Pathologies of Big Data”, http://queue.acm.org/detail.cfm?id=1563874)。而且,即使有唯一ID,从哪里能够找到这样一小块数据仍然是一项艰巨的任务。这就好比从你藏糖果的地方不看一眼地想拿到最后一块Jolly Rancher。
幸运的是,你有很多能把事情变得更加容易的选择;其中重要的有如下4个:缓存、代理、索引、负载均衡。本节剩余部分将会讨论每个用于加速数据访问的概念。
缓存
缓存利用了本地引用原则的好处:最近访问的数据可能被再次访问。缓存几乎被用在计算机运行的各层:硬件,操作系统,web浏览器,web应用等等。缓存就像短期的内存:有着限定大小的空间,但通常比访问原始数据源更快,并且包含有最近最多被访问过的(数据)项。缓存可以存在于架构的各个层次,但会发现到经常更靠近前端(非web前端界面,架构上层),这样就可尽快返回数据而不用经过繁重的下层(处理)了。
在我们的API例子中,如何使用一个缓存来加速你的数据访问速度呢?在这个场景下,你可以在很多地方插入一个缓存。选择之一是在你的请求层节点中插入一个缓存,如图1.8.
图1.8:在你的请求层节点中插入缓存
将缓存直接放置在请求层节点中让本地存储响应数据变为可能。每次对于一个服务的请求,节点将立即返回存在的本地、缓存的数据。如果(对应的)缓存不存在,请求节点将会从磁盘中查询数据。请求层节点的缓存既可以放置在内存(更快)也可以在节点本地磁盘(比通过网络快)上。
图1.9:多个缓存
当你扩展到多个节点时,会发生什么呢?正如你看到的图1.9,如果请求曾扩展到多个节点,那么每个节点都可以拥有它自身的缓存。但是,如果你的负载均衡器将请求随机分发到这些节点上,同样的请求会到达不同的节点,就会提高缓存miss率。两种克服这种困难的方法是:全局缓存和分布式缓存。
全局缓存
正如听起来的一样,全局缓存是指:所有节点使用同一缓存空间。这包括增加一台服务器或是某种类型的文件存储,比从你原始存储地方(访问)更快,并且所有请求层的节点均可以访问(全局缓存)。所有请求节点统一像访问其本地缓存般访问(全局)缓存。这种类型的缓存机制可能会变得比较复杂,因为随着客户端和请求数量的增加,单个缓存(服务器)很容易被压垮,但是在一些架构中非常有效(特别是有专门定制的硬件使得访问全局缓存非常快速,或者需要缓存的数据集是固定的)。
通常有两种形式的全局缓存,如下图。图1.10中,如果缓存中找不到对应的响应,那缓存自身会去从下层存储中获取丢失的数据。在图1.11中,当缓存中找不到相应数据时,需要请求节点自己去获取数据。
图1.10:全局缓存自身负责存取