Scaling Web Applications
构建Web应用程序可能是相当容易的,但要构建可扩展的Web应用程序却比较困难。在小规模时可以正常工作的技术和技巧,在规模增大后却可能会失败。要避免将来浪费大量的时间和精力,应该预先考虑好扩展的问题,这样能确保构建小规模程序时工作良好,而且也能够进一步处理大流量的应用程序,不必将体系结构推倒重来。
一个中等大小的Web应用程序,每天要服务于一千万个页面,还需要处理百万级的用于Ajax交互的请求(假定应用程序需要某种API,这个我们待会再谈)。一天一千万个页面,即需要每秒支持116个页面,根据具体的流量情况,在高峰期还可能达到它的两倍或者三倍。如果平均下来每个页面有10个数据查询,那就是每秒超过1000个查询(QPS),高峰期是3 000QPS。对于一台机器而言,这可是很多数据库流量,那么,应该怎么设计系统来达到这个速度,还要能进一步扩展它,并且提供可靠性和冗余?
设计可扩展的系统要基于一些我们将要讨论的核心原则,我们还要研究用于扩展应用程序的各个部分的具体技术。
扩展之谜
The Scaling Myth
扩展和可扩展性一直都是Web应用程序开发人员的热门话题。每当需要构建服务于大量人群的Web应用程序时,扩展就成了问题:怎样才能支持一百个用户?一千个?一百万个?一亿个?
虽然扩展很久以来就是个热门话题,但对它的深入理解却很缺乏。在开始讨论如何设计可扩展的应用程序之前,需要明确定义所谓的“可扩展”。
什么是可扩展性
What Is Scalability
可扩展性有时被定义成“具备可扩展性的系统或组件,可以很容易被修改以适用于问题域”。这个定义非常模糊,让人困惑。
其实可扩展性很容易定义。一个可扩展的系统有三个简单特性:
l 系统能够容纳使用率的增加
l 系统能够容纳数据集的增加
l 系统可维护
在深入这些具体条目之前,先谈谈可扩展性不是什么,消除在当前Web开发中常见的一些谬误。
可扩展性不是指原始速度。性能和可扩展性是不同的问题。你很容易就可以有一个不可扩展的高性能系统,虽然反过来的情况并不常见——一个系统可以扩展,那么你总能通过扩展它来提高性能。一个系统在1000个用户和1 GB数据的情况下非常快,并不意味着它是可扩展的,除非它能够在10倍当前用户和10倍当前数据的情况下仍然保持现在的速度。
可扩展性跟用不用Java无关。用任何语言任何工具构建的系统都能变得可扩展,虽然不同语言和工具造成的相对难度肯定会是个问题。Java近来和可扩展性走得如此之近,以至于很多人把两者当成是相同的。再重复一次:不用Java就可以构建一个可扩展的应用程序。类似的,编译语言不是设计可扩展系统的必要条件。本地代码,即时编译,和编译过的字节码虚拟机能做到又好又快,但那是性能问题,而不是可扩展性的问题。不管怎么说,这一段可不是专门为了打压Java的。使用Java来创建可扩展的应用程序完全是可能的,只是说具体实施语言和系统的可扩展性之间的关系很小。
实际上,可扩展性跟使用特定的技术完全无关。另一个常见的误解是XML是可扩展性的核心——这完全是胡说。XML对互操作性很有用,但互操作性和可扩展性两者不是一回事。你可以拥有既不读也不写XML的可扩展的程序。在James Clark想出这个特别的缩写之前,可扩展的系统早就存在很久了。
可扩展性有时被描述成是页面逻辑和业务逻辑的分离,就像我们在第2章中讨论的那样。虽然这是一个很崇高的目标,而且有助于系统的维护,但它也不是必需的。我们能创建这样的可扩展系统——使用PHP,只包含一个层次,没有XML,没有Java,甚至速度也不快:
<?php
sleep(1);
echo "Hello world!";
?>
我们提供的示例“系统”速度不快——它总需要花一秒钟来响应。然而,它很好的满足了三条可扩展性标准。可以通过增加更多Web服务器来容纳更多流量增长,而代码无需任何改动。能够容纳数据集增长,因为根本就没有任何存储的数据。代码也是非常易于维护的,没有哪个训练过的程序员是不知道如何维护它的——例如,让它改说“Hello there”。
扩展硬件平台
Scaling a Hardware Platform
说到扩展体系结构的两种主要策略时,是从硬件的角度讨论提高性能的方法的。在项目初期,硬件看起来总是很昂贵,但随着时间地推移,软件成本变得昂贵得多(到一定程度,对于超大的应用程序而言,两者角色又会交换)。因此,构建应用程序时,应该想法让它能够不需要或者很少需要软件就能扩展;买更多的硬件,使用更多硬件来扩展要更好一些。
接着的问题就是“应该构建可纵向扩展还是可横向扩展的应用程序?”,这两个术语的含义有时可能互换,所以先定义一下。简单的说,可以纵向扩展的系统需要相同硬件的一个更为强大的版本来扩展(抛弃原来的硬件),而可以横向扩展的系统需要当前硬件的复制品来进行扩展。实际上,对于大型应用程序而言,两者中只有一个是有实际意义并且有成本效益的。
垂直扩展
Vertical Scaling
垂直扩展的原则很简单。开始的设置非常基础——可能就是一台Web服务器和一台数据库服务器。当机器性能不足时,用更加强大的机器替换它。新机器能力不足时,用另外一台机器替换它。这台机器能力也不足时,就买一台更加强大的机器。如此反复。
这种模型的问题在于成本不是线性增长的。选一家供货商,一种方式是买越来越强大的机器,另一种方式是购买多台小机器,然后把增加系统中处理器数目、RAM大小和磁盘容量所需要的价格做个比较,如图9-1所示。
采用这种增长模型,最终会受到限制,垂直扩展的价格是呈指数级增长的,并且最终会远远超出我们所拥有的资金。即使有一大堆钱可以烧,在某点还是会到达一个极限,这时不容易买到可用的商业用服务器。Sun公司当前最大的装备Sun Fire E25k,支持达到72个处理器,而且能够处理576GB的内存,不过一旦你达到这个极限,那么好运气也就到头

图9-1:使用多台小服务器扩展与使用更大的服务器扩展
头了。值得注意的是,没有哪家现存的Web应用程序提供商在运行大型的SGI Altix集群,这种成本模型没有任何意义。
纵向扩展比较吸引人的地方是,设计很容易。我们可以在本地机器上构建软件,并且让它具备所需功能,能够运行。一旦做到这一点,只要换一台更大的机器,并且发布它即可。每次需要更多容量和性能,都只需要更新硬件。软件工程师(也就是我们)不需要再次去接触和考虑硬件问题。程序还没有大到一定程度之前,这种做法还是很有吸引力的。如果你能确认应用程序最终会达到的大小,那么垂直扩展能成为不错的构建一个真正的可扩展系统之外的选择。
水平扩展
Horizontal Scaling
水平扩展内在原则上和垂直扩展相似——不断添加更多的硬件。不同于垂直扩展模型的地方在于,增长时不需要超级强劲的机器,而只需要很多常规的机器。从一台常规的机器开始,其能力不足后添加第二台。然后添加第三台、第四台,等等。直到有上万台服务器(就像当前最大的应用程序那样)。
购置硬件用于水平扩展系统时,其中一个窍门是决定好买什么样的硬件。我们可以买数千台Mac minis——它们相当廉价,并且有CPU和RAM。购买最小最廉价的硬件这种方式的问题在于:需要考虑每台机器的TCO。每台机器都有相当固定的维护成本——不管是Mac mini或者简单的Dell的适用于机架的机器,都需要为它们提供机架还有电缆、安装操作系统、执行基本的设置。机器会占用空间和能源,而且根据不同的物理设计,还需
要额外的空间用于散热。你可以把整个42 1U Dell机架式服务器放在一个机架中,Mac mini的设计却无法保证它在这种环境下的散热良好。
在TCO和计算能力之间需要找到一个平衡点——它能够让我们的投资获得最大的性能——然后才购买那些服务器。随着时间推移,这个平衡点也在漂移(基本上总是要求变得越来越强大),所以理想的硬件也随时间的推移而变化。一开始就要记住这一点,而且在构建平台时予以考虑。如果应用程序需要所有的服务器都完全相同,那么成本会很昂贵,而且时间久了之后就没有办法扩展了,而能够混用不同服务器并且能配备最便宜服务器的应用程序,将一直保持成本上的优势。同时,Google那种购买可能存在的最便宜的硬件的方式,对于其他公司是很危险的,当然那些超大的公司(Google、Yahoo!和Microsoft) 除外。那些失效硬件的替换成本会消耗节省的费用并且浪费大量时间。根据系统的设计,添加和减少机器可能比较容易,但不会过于简单。如果每天得花几个小时处理硬件的增加和故障,你很快就会觉得无聊,而且成本也很昂贵。
这样我们已经看到了横向扩展诸多大问题中的一个:增加了管理成本。10台单处理器的机器和1台有10个处理器的机器相比,在管理上要花费10倍的时间。幸运的是,系统管理所用成本不是呈线性增长的,因为10台相同的机器(相同指的是软件,而不是硬件)能够同步管理。随着添加的机器越来越多,安装和设置将变成机械劳动,因为同样的问题会一次又一次地出现,而对于一台独立的大型机器,基本上是不会反复执行同样的操作的。
以上推论的前提是机器上安装的软件是相同的。如果不能保持集群中的每个节点(比如Web服务器集群)都相同,最终将得到一个非常难以管理的系统。而在一个所有节点都相同的系统中,如果有需要,我们可以简单地复制一台新的机器。每个节点都没有特殊的需要完成的任务,而对任何一个节点进行的工作都可以直接应用到另外一个节点上去。使用诸如System Imager和System Configurator(http://www.systemimager.org/)或者Red Hat的KickStart这样的程序,在需要增加硬件的时候,我们能快速地设置好新的机器。还有一点常常被忽略的,就是当遇到任何问题的时候,都可以快速“重配置”机器。如果10台Web服务器中有一台出了问题,可以直接把它上面的软件清除,然后在很短时间内重新安装,而不用浪费时间去解决那些混乱的配置问题。
机器数目的增加会带来基本的硬件成本和管理时间之外的附加成本。每台机器都有自己的电力供应,需要固定大小的电流。对于一整个机架的机器,总流量很容易就会达到100安培,而电力可从来都不便宜。一整个机架的机器也相应地需要一整个机架的空间——你拥有的机器越多,就需要租用越多的机架空间。如果是纵向扩展成更大的机器,单个机器的体积是很大,但因为它们共享很多组件,会节省很多空间。毕竟一台40U服务器不需要
40个电力供应。增加到机架上的每台机器都需要连接到网络。除了网络电缆的那点费用(除非你在使用光纤,光纤既昂贵又比较脆弱),每台机器还需要有个交换机端口(或者两个)和IP地址。对于每个机架上的机器,往往需要一到两个机架单元的网络设备。所有这一切都应该纳入到每台机器的总成本中。
除了管理成本之外,横向扩展时另一个比较容易出现的大问题是硬件利用不足。当缺少磁盘I/O或者内存时,我们添加另一台机器。这样重复几次后,就有很多CPU得不到充分使用的机器。解决这个问题的关键在于,首先确保购买的是具有合适的性能特征的机器。如果知道需要很多磁盘I/O,那么可以购买3个单元的带有6个内部磁盘的机器。如果知道需要很多RAM,那么就可以购买1个单元的带有16GB内存和一个普通处理器的Opteron机器。虽然买的是很多的小机器,但还是要注意它们的规格描述,力求做到各个特性之间的平衡,一定要充分认识到这一点的重要性。
前面遇到过垂直扩展模型在线性扩展方面的问题。对于横向扩展的系统,虽然硬件是线性扩展的,但运行于其上的软件却未必是这样。需要从集群中所有节点汇总结果的软件,或者在同伴之间交换消息的软件,若它们都做不到线性扩展,反而会有相反的效果;每添加一台服务器,可用的容量反而减少了,其性能也可能会降低。对于这样的系统,重要的是去理解,从什么时候开始向它添加更多硬件时,其代价会过于昂贵。我们总归会在某个时候遇到阻碍,这时添加硬件无法增加性能或容量——最坏的可能是它反而会降低总体性能。理想状态下能够设计出总是可以线性扩展的软件,但这并不符合实际情况。当做不到线性增长时,要确认当时的情况,并仔细辨识从什么时候开始,添加更多硬件会让成本/效益变得过于昂贵。横向扩展和纵向扩展的结合能提供双方的优点:软件设计简单得多,也不用购买现有的最昂贵的服务器。
正在进行中的工作
Ongoing Work
有了横向扩展的体系结构之后,仅剩的基础框架方面的工作是相当机械和系统性的。即根据预期的活动和数据集的增长,在性能不足前添加更多的生产用硬件,从而持续增加系统的性能和容量。容量规划是相当精确的一门学科,可以写好几本相当厚的书来讲述。《Performance by Design: Computer Capacity Planning by Example》(Prentice Hall)这本书中的开创性工作能为你提供很好的起点。除了容量规划,还需要处理不断发生的“事件”,
比如组件和机器级别的故障(甚至数据中心级别故障)。随着问题处理次数增多,它们越来越成为一种机械性的劳动。体系结构设计周期结束时,一个很好的成果就是整套的危机管理文档。
优秀的危机管理和故障场景文档应该包括一个简单的指南,提供从任何可以想象到的故障中恢复的步骤——当一个磁盘故障时应该怎么办?当一个磁盘没有空间了该怎么办?机器发生故障时怎么办?数据库索引失效时怎么办?当机器达到I/O吞吐量的限制时怎么办,如此等等。一组优秀文档的目标是使所有常见的问题易于处理,并且使复杂的问题变得可以管理。在设计程序时考虑和编写这份文档,能帮助确定哪些部分会出问题,以及系统可以怎样处理它。
冗余
Redundancy
不管是纵向扩展还是横向扩展,机器都会发生故障。各类Google展示都表明,在10000台机器中,可以预计每天都将报废一台。这里说的不是磁盘错误,而是整个系统的故障。无论什么硬件,只要时间足够长,它都可能而且终将损坏。系统设计需要能够处理每个组件故障,甚至可能是所有组件同时故障。
不管使用什么硬件,唯一的保证故障状况下正常服务的办法就是有多个硬件备份。根据所用硬件和你的设计,所拥有的备份种类可能是冷备份、热备份或完全热备。冷备份可能用在网络交换器之类的东西上。在交换器故障时,需要拿一个备用交换机插入所有接头,复制配置,然后启动它。这种属于冷备份,因为它需要安装和配置(可能是物理上的或者软件上的,这个例子里面两种情况都有),然后才能取代故障的组件。
热备份是指硬件设备一切就绪,只需要启动(同样的,物理上的或者软件意义上的)就可以开始使用。主从结构的MySQL系统可能算一个典型的例子,主系统用于生产,从属系统是备份。在生产中并不使用从属数据库,但是如果主数据库不可用了,那么就可以把所有的流量导向从属数据库。从属数据库已经配置好,随时可以使用;它一直从主数据库复制数据,并一直准备着接手。
第三个也是最为推荐的冗余模式是完全热备组件。当一个组件故障时,其他组件自动接手它的工作。失效的组件是被检测到的,而且转换过程无需用户干预。这显然是更好的方式,因为用户看到服务一直正常——一切都持续工作,你只需要在空闲的时候修复损坏了的组件。例如,两个负载均衡器被配置为活跃/非活跃对,活跃的均衡器接手所有的流量并通过一个监测协议和被动的均衡器交流。如果活跃的负载均衡器出现问题,而一直处于非活跃状态的负载均衡器发现一直没有从它那里接收到心跳信息,非活跃状态的均衡器立刻就知道需要接手,并且开始接收和处理所有流量。
完全热备份存在的一个问题是所谓的“振荡”。在某些配置下,组件可能由于某些软件原因看起来像是失效了,备份就取而代之。但当流量从失效的组件移走后,这个组件看起来又开始工作了,所以流量再度回到它这边。然后流量导致它再次失效(或显得失效),备份组件再次上场。因为流量在两个组件之间移来移去,这个过程就称为“振荡”。这种情况在互联网上的采用边界网关协议的路由中很常见。错误配置的路由器可能和它的邻居不一致,它的邻居会把它从路由表中删除,将另一个路由提拔到它之上。将“损坏”的路由器上的流量移走,导致它恢复可用,这样其他路由器的路由表又会更新,新建立一个具有较低量度的链接。这样流量又将按照原来的路由走,整个过程不断重复。对于第二个路由器,路由表不断更新的过程大大占用了CPU使用率,所有流经这个路由器的流量,可能会出现请求和响应不能按顺序抵达的情况,因为其中一些选择了第一种路由,而其他的则选择了另外一种。为了避免在BGP中的振荡,出现了各种不同的抑制算法来延缓第二个路由变动。
在我们的系统中有多种方式避免振荡,因为组件不一定要像BGP路由一样根据量度工作。当我们有一对活跃/非活跃的组件时,活跃组件不是由于它有最短量度才被选中的,而是进行任意选择——两个组件中的任何一个都能工作得像另一个一样好。因此,一旦出现故障,转移到非活跃节点后,可以接下去一直使用它。原来的那个活跃节点重新上线时,可以让它担当非活跃节点。只有当集群中的两个(或者全部)节点都存在同样的问题,并且在流量流向它们的时候它们都发生了故障时,才会发生振荡。对这种情况,可能需要抑制振荡,或者只是监测这种情况,并且手工修复。抑制在很多情况下都是危险的,因为如果组件是真的坏了,我们不希望阻碍备份切换的过程。
活跃/非活跃冗余对、活跃/活跃对和集群相互之间需要区分清楚。在活跃/非活跃对中,有一个在线的生产用设备和一个没有在使用的完全热备。在活跃/活跃对中,是同时使用两个设备的,在一个设备失败的时候把所有流量移到一台设备上。对于两台设备的情况,这种分别不是那么重要,但一旦超过两台设备,并且是处于集群中,事情就变得有趣了。
例如,假设有一个集群,包括10台从属数据库。那么可以同时从10台服务器中读数据,将十分之一的读流量分担到每台机器上。当一台机器失效后,就简单地将读流量移到剩余的9台机器上。这时,需要根据机器的故障率,替换失效机器所花的时间还有服务用户所需的机器数目来做个计算。可能结果会发现我们不仅需要一个备份,因为替换一台机器还要花一些天时间,所以一直备有两台备用机。应用程序至少需要5台机器来提供可接收的性能。
那么就需要分配7台机器给集群。当所有7台机器都在使用时,有一些空闲的性能,并且所有的机器都能够比较好的得以利用。万一有一台或两台机器坏了,还有足够的性能服务用户。
通过使用活跃/活跃的集群,避免了硬件闲置,由于一些原因,硬件闲置是不可取的。首先,这是个观感方面的问题——机器占有机架空间,使用能源,却没有为应用程序的运行做任何共享,这看起来就是在浪费。如果机器一直闲置,要使用它们的时候就更容易出故障。一台机器非常频繁地读和写,而它的磁盘却又不好的时候,它很快就会出现故障。在一批的100个磁盘中,总有一个或者两个会在开头几天的密集使用中出问题,而还有一些则会因为不断发生软件错误而不能发挥正常性能。在早期有完全热备运行时,就应该发现需要新的磁盘,而不是等到其他机器发生了故障,因而开始使用一直被闲置的备份的时候才知道。
计算出需要多少容量,在这个基础上再加上故障转移需要添加的容量,是很重要的,值得一再提醒。例如有两个磁盘处理所有的读写操作。我们同时对它们执行写操作,确保总能有两份相同的数据副本(待会解释为什么和如何去做这件事)。现在在一个活跃/活跃对中使用它们,从它们当中读取数据——而且所做的读操作要比写操作多得多,那么读取能力会很快达到极限,要比写操作早得多。如果两者之一失效,所有读操作都将移到第二个设备。在这种情况下,需要一个设备能独自处理所有的生产流量,这是基本要求。如果需要1.5个设备才能支持所有读操作,那么就需要至少配备三个镜像的设备。这样就能容忍其中一个失效,而其他两个应对生产负载。而如果想在同一时间能处理多于一个失效的情况,就需要更多的设备。
这个模型可以从更小的服务器中获益。如果存在成本是一半,容量或性能也是一半的服务器,那么花费就更少。现在只需要三台这样的服务器来处理生产负载,用另外一台作为备份(假定它们有相同的故障几率和置换服务等级协议(SLA))。这样只需要原有成本的三分之二即可(但TCO上有些增加)。成本节省来源于两处——粒度和冗余性的降低。因为每台机器的容量降低了,就能更加精确地知道需要多少机器来支持生产流量,最后一台机器需要支持的负载也减少了。这是一部分原因,但真正的原因是减少了所需的备份容量。如果我们有成本是十分之一,容量也是十分之一的机器,那么就只要花原来十分之一的成本在完全热备上即可(因为还是只需要一台机器作为备份)。
要达到最低冗余要求,在任何可能失败的地方都必须至少有一个冷备份。这在一开始可能会相当昂贵(尤其是当你使用昂贵的负载均衡设备时),所以在分配有限的预算时,你要确保最常见的易出故障的组件能够先恰当地做好冗余准备。对于没有完全热备、热备或者
冷备份的设备,你应该确认替换它们需要花费的时间,是否需要快速地获取另外一台。保有一份记载系统中每个组件的供货商和供货时间的文档,是个很不错的主意。
磁盘备份并不遵守上述规则。磁盘故障如此频繁,因此一直需要有库存备份。需要的备份数目取决于同时处于运转状态的磁盘数目、所用磁盘的MTBF和这些磁盘所处的环境(温度高的磁盘容易更快地出现故障)。如果能够避免使用太多种不同的磁盘,那么可以减少需要保存在手头的磁盘数目。使用相似的磁盘替换损坏的磁盘,在受限于I/O的RAID配置中极为重要,因为磁盘吞吐量受制于RAID中最慢的那个磁盘。
现在已经涉及了冗余和故障转移的一些领域,但讨论的问题还是局限在一个数据中心。根本没有讨论过跨数据中心的冗余性和故障转移。在讨论负载均衡时,会简单介绍故障转移语义,但对于冗余性,所有适用于数据中心内的规则在跨数据中心情况下仍然适用。我们必须具备足够的能力,在丢失了一个数据中心的情况下还能继续运作,否则就是没有做好冗余工作。对于简单的两个数据中心的设置,这意味着两者都需要有完全的能力,都有完整的镜像数据。就和其他组件一样,当有三个数据中心时,成本会有所下降——如果只需要为某个时刻有一个数据中心故障的情况做准备,每个数据中心都只需要一半的处理能力。任何数据都只需要复制到三个数据中心中的两个当中。这把备用硬件的负载降低到了一半,并且随着更多数据中心的添加,这个负载会持续降低。这也会赋予我们更快的反映时间,稍候就会讨论到这一点。






