1. 大型网站分布式设计

http://blog.jobbole.com/58551/

1.1. Web 分布式系统设计准则

可用性

大型在线零售网站,甚至是几分钟的不可用都会导致数以千万计美元的收入损失,所以将他们的系统设计成 不间断可用和能够弹性恢复 既是一项基础业务也是一个技术需求。分布式系统的高可用性需要仔细考虑关键组件冗余,快速恢复部分有问题的系统,并且当出现问题时能够做到优雅降级。

性能

性能已成为大多数网站一个很重要的考量指标。一个网站的速度会影响到使用和用户满意度,同样也会影响到搜索引擎排名,将直接关系到网站收入和用户保持力(黏性)。因此,关键之处就在于创建一个为 快速响应、低延迟 的系统。

可靠性

一个系统需要做到可靠,这样才能使得对于 固定数据的请求始终会返回同样的数据。如果数据发生变化或者更新,那相同的请求应该返回新的数据。 用户需要知道,一旦一些数据被写入、存储到系统中,那么系统就会持久化(这些数据)并且能够让人信赖随时能够检索。

可伸缩性

对于任何大型分布式系统,系统规模只是可伸缩性需要考虑的一个方面。同样重要的是, 增加容量能够处理更大量的负载所需的工作 ,通常在系统可伸缩性方面被提及到。可伸缩性会涉及到系统的很多不同的因素:系统额外还能够处理多少流量,是否能够轻易增加存储容量,还能多处理多少事务。

可管理性

设计一个易于运维的系统是另一个重要考量点。系统的可管理型等同于操作的可伸缩性:维护和变更。可管理性需要考虑的有:当 问题发生时能够便于诊断和理解 ,便于进行变更和修改,并且系统易于操作。(比如系统是否能够进行例行操作而不带来失败或者异常?)

成本

包括硬件和软件成本,但同样还要考虑到一些其他方面来 部署、运维系统,比如构建系统所需的开发时间,运行系统所需的运维工作量,甚至所需的培训都要被考虑在内

小节

这些准则中的每一条都提供了在设计一个分布式 web 系统架构时作决定的基本原则。但是,他们也可能互相矛盾,比如达到某一目标是以牺牲另一个为代价的。一个典型的例子:专注于系统容量时,选择通过简单增加更多机器(可伸缩性)的代价是(增加了)可管理性(你需要运维更多的服务器)和成本(更多服务器价格)。当设计任何 web 应用时,这些关键准则都是需要考量的,即使不得不承认,一个设计可能会牺牲它们中的一个或更多。

1.2. 基本原理

对于系统架构来说,有一些事情需要考虑:什么是正确的组件,这些组件如何协作,需要做哪些正确的权衡。

大型 Web 应用都非常核心的因素:服务,冗余,分期和失败处理。每个因素均包含有选择和妥协。

1.2.1. 举例:图片托管应用

设想一个这样的系统:用户可以将他们的图片上传到一个中央服务器,并且图片可以通过一个 web 链接或者 API(应用程序接口)进行请求,就像 Flickr 或者 Picasa 一样。为了简单起见,我们假定这个应用有两个关键部分:能够上传(写入)一张图片到服务器,能够查询一张图片。虽然我们希望上传能够更快速,但我们最关心的是系统能够快速分发用户请求的图片(比如图片可以被请求用于一张网页或是其他应用)。这些跟一个 web 服务器或者 CDN(内容分发网络) edge server(CDN 所使用的服务器,用于在很多位置存放内容,这样内容在地理/物理上更接近用户,起到更高性能的作用)所提供的功能非常类似。

系统其他重要的方面

  • 对于存储的图片数量没有设限,所以就图片数量而言,需要考虑存储的可伸缩性。
  • 对于图片的下载/请求需要做到低延迟。
  • 如果一个用户上传了一张图片,那该图片应该总是存在的。(图片的数据可靠性)
  • 系统需要易于管理(可管理型)。
  • 由于图片托管不会带来很高的利润,所以系统需要做到有成本效益的。

1 图片 1.1:图片托管应用的简化架构图

服务

可伸缩系统的设计有助于各功能解耦并且通过一个清晰定义的接口思考系统的每个部分。在实践中,这种方式的系统拥有一个面向服务的架构 SOA。对于这些类型的系统,每个服务都有它们各自确切的功能上下文,并且和该上下文以外的任何交互均是与一个抽象的接口进行的,特别是另一个服务的公有接口。

将一个系统拆解为一个互补的服务集合解耦了那些相互间的操作。这种抽象有助于建立服务间明确的关系、潜在的运行环境、服务的消费者。通过这些清晰的描绘有助于隔离问题,并且允许每个部分能够相互独立地进行扩展。这种面向服务的系统设计有点类似与面向对象编程。

在我们的例子中,所有上传和获取图片的请求都是在同一服务器上处理,但是,如果系统想要达到可伸缩,那么将这两个功能拆分成各自的服务是非常明智的。

1.2.2. 读写分离

假设这些服务被大量使用,这样的场景将非常易于看到写操作会如何影响读取图片的时间(因为这两个功能会竞争共享资源)。即使上传和下载速度是一样的(对于大多数 IP 网络来说不一定是,因为大多数都是设计成下载速度与上传速度 3:1 的比例),文件通常直接从缓存中读取,而写入则最终必须到达磁盘(在最终一致的场景中可能会被写入多次)。即使所有东西都是从内存或者磁盘(比如 SSD 固态硬盘)读取,数据库的写入操作总还是比读取要慢。

另一个潜在的设计问题是,一个像 Apache 或者 lighttpd 的 web 服务器,通常有一个它可以维持并发连接数的上线(默认大约在 500 左右,但可以调得更高),并且在高流量下,写操作将很快消耗完所有连接资源。由于读操作可以异步进行,或者利用其它性能调优如 gzip 压缩或者 chunked transfer encoding,web 服务器可以转换为更快服务读操作、更快切换客户端,从而比最大连接数每秒服务更多的请求(Apache 最大连接数设置为 500,但一般都能每秒服务数千个请求)。写操作,在另一方面,倾向于在上传过程中维护一个打开状态的连接,所有上传一个 1M 大小的文件在大多数家庭网络上将花费超过 1 秒的视角,所以 web 服务器只能同时处理 500 个写操作。

2

图 1.2: 读写分离

将图片的读、写操作拆分成各自的服务是一个应对这种瓶颈很好的解决方案,如图 1.2。这样允许我们能够独立的扩展它们(通常读大于写)。这样可以分离未来的担心,可以更简单地解决像读操作缓慢的问题,并做到可伸缩。

这种方法的好处在于我们能够独立解决问题——不用担心在同一上下文中写入、读取新的图片。这两种服务仍然影响着全部的图片,但均能通过 service-appropriate 方法优化它们的性能,比如让请求排队,或者缓存受欢迎的图片从一个维护和成本的视角出发,每个服务均能独立、按需伸缩是非常好的,因为如果它们被组合、混合在一起,在上面讨论的场景下,可能某一服务不经意间就会影响到其他服务的性能。

当然,当你考虑着两个不同点时,上面的例子能够工作得很好(事实上,这跟一些云存储提供商的实现方案和 CDN 很类似)。尽管还有很多方法来处理这些类型的瓶颈,但每个都有不同方面的权衡。

1.2.3. 分区处理

例如, Flickr 通过将用户分布在不同区域的方法来解决读/写问题,比如每个分区只处理一定数量的用户,随着用户的增加,集群会更多的分区 。在第一个例子中,基于实际使用(整个系统的读写操作数量)可以更容易地伸缩硬件,然而 Flickr 是基于它的用户(但强制假设用户的使用率均等,所以仍有额外的容量)。对于前者来说,停电或者一个服务的问题就会降低整个系统的功能性(比如没人可以写入文件),然而 Flickr 的一个分区停电仅会影响到这个分区相应的用户。第一个例子易于操作整个数据集,比如升级写入服务来包含新的元数据或者搜索所有的图片元数据,然而在 Flickr 的架构下,每个分区均需要被更新或搜索(或者一个搜索服务需要能够整理相关元数据——事实上他们确实这么做)。

对于这些系统来说没有孰对孰错,而是帮助我们回到本章开头所说的准则,判断系统需求(读多还是写多还是两者都多,并发程度,跨数据集查询,搜索,排序等),检测不同的取舍,理解系统为什么会失败并且有可靠的计划来应对失败的发生。

1.2.4. 冗余

为了能够优雅地处理失败问题,Web 架构必须做到服务和数据的冗余。比如,如果在单台服务器上仅有一份文件,那么失去那台服务器就意味着丢失那份文件。通常的解决方案是创建多个、冗余的备份。

该准则同样适用于服务。如果应用有一个核心功能,那么通过确保多个拷贝(多个同类服务实例)或者版本同时运行能够免于单点失败的情况。

在一个系统中创建冗余能够去除单点失败,并提供一个备份或在必要的紧急时刻替换功能。例如,如果在生产环境有同一服务的两个实例在运行,其中一个失败或者降级了,系统可以(启动)failover 到那个健康状态的服务。Failover 可以自动发生或者需要人工干预。

服务冗余的另一个关键点在于创建一个非共享的架构。通过这种架构,每个节点都能够独立操作,并且没有中央“大脑”来管理状态或者协调其他节点的活动。这对于可伸缩性非常有帮助,因为新的节点不需要特殊的条件或知识就能加入到集群。但是,最重要的是在这些系统中不会存在单点失败问题,所以它们能够更加弹性地面对失败。

例如,在我们的图片服务应用,所有的图片会在另一个地方的硬件中有冗余的备份,理想情况是在一个不同的地理位置,以防地震或者数据中心火灾这类的灾难发生,而访问图片的服务同样是冗余的,见图 1.3(负载均衡器可以将其变为现实,详情请见下文)

3

图 1.3:图片托管应用,带有冗余特性

1.2.5. 扩容之水平扩展分区和垂直扩展

单台服务器可能没法放下海量数据集。也可能是一个操作需要太多计算资源,消耗性能,使得有必要增加系统容量。无论是哪种情况,你都有两种选择:垂直扩展(scale vertically)或者水平扩展(scale horizontally)。

垂直扩展意味着在单台服务器上增加更多的资源。所以对于大数据来说,这意味着增加更多更大容量的硬盘以便让单台服务器能够容纳整个数据集。对于计算操作的场景,这意味着将计算任务交给一台拥有更快 CPU 或者更多内存的大型服务器。对于每种场景,垂直扩展是通过自身能够处理更多的方式来达到目标的。 垂直扩展对于应用来说无需修改,通常升级机器即可达到目的

水平扩展,就是增加更多的节点。对于大数据集,可能是用另一台服务器来存储部分数据集;而对于计算资源来说,则意味着将操作进行分解或者加载在一些额外的节点上。水平扩展要求应用架构能够支持这种方式的扩展,因为数据、服务都是分布式的,需要从软件层面来支持这一特性,从而做到数据、服务的水平可扩展。水平扩展应该被天然地包含在系统架构设计准则里,否则想要通过修改、隔离上下文来达到这一点将会相当麻烦。

对于水平扩展来说,通常方法之一就是 将你的服务打散、分区。分区可以是分布式的,这样每个逻辑功能集都是分离的;分区可通过地理边界来划分,或者其他标准如付费/未付费用户 。这些设计的好处在于它们能够使得服务或数据存储易于增加容量。

在我们的图片服务器例子中,可以将单台存储图片的服务器替换为多台文件服务器,每台保存各自单独的图片集。(见图 1.4)这样的架构使得系统能够往各台文件服务器中存入图片,当磁盘快满时再增加额外的服务器。这种设计将需要一种命名机制,将图片的文件名与所在服务器关联起来。一个图片的名字可以通过服务器间一致性 Hash 机制来生成。或者另一种选择是,可以分配给每张图片一个增量 ID,当一个客户端请求一张图片时,图片检索服务只需要维护每台服务器对应的 ID 区间即可(类似索引)。

4

图 1.4:图片托管应用,加入冗余和分区特性

当然,将数据或功能分布在多台服务器上会带来很多挑战。关键问题之一是数据局部性(data locality)。在分布式系统里,数据离操作或者计算点越近,系统性能就越高。因此将数据分布在多台服务器可能是有问题的,任何需要数据的时候都可能不在本地,使得服务器必须通过网络来获取所需的信息。

1.3. 建立大型 web

大多数简单的 web 应用随着它们的成长,会有两个主要的挑战:访问应用服务器和数据库的可伸缩性。在一个高可伸缩的应用设计中,web 服务器通常会最小化并通常表现为一个非共享(无状态)架构。这样使得系统的应用服务层能够很好地进行伸缩。这样数据的结果是, 压力被向下推到了数据库服务器和相关(底层)支持服务;真正的伸缩和性能挑战就在这一层起到作用

假设你有数以 TB 计的数据并且希望能让用户随机访问这些数据的一小部分。由于很难将 TB 级的数据加载到内存,所以这会使得事情变得非常有挑战性。这种访问将直接变为磁盘 IO 操作。从磁盘读取会比从内存要慢得多。顺序访问内存的速度是访问磁盘的 6 倍,而在随机读方面,前者是后者的十万倍。而且,即使有唯一 ID,从哪里能够找到这样一小块数据仍然是一项艰巨的任务。

幸运的是,可以通过 缓存、代理、索引、负载均衡 来解决。

1.3.1. 缓存

缓存几乎被用在计算机运行的各层:硬件,操作系统,web 浏览器,web 应用等等。缓存就像短期的内存:有着限定大小的空间,但通常比访问原始数据源更快,并且包含有最近最多被访问过的(数据)项。缓存可以存在于架构的各个层次,但会发现到经常更靠近前端(非 web 前端界面,架构上层),这样就可尽快返回数据而不用经过繁重的下层处理了。

4-c

图 1.5:在请求层节点中插入缓存

每次对于一个服务的请求,节点将立即返回存在的本地、缓存的数据。如果对应的缓存不存在,请求节点将会从磁盘中查询数据。请求层节点的缓存既可以放置在内存(更快)也可以在节点本地磁盘(比通过网络快)上。

5

图 1.6:多个缓存

当你扩展到多个节点时,会发生什么呢?正如你看到的图 1.6,如果请求曾扩展到多个节点,那么每个节点都可以拥有它自身的缓存。但是,如果你的负载均衡器将请求随机分发到这些节点上,同样的请求会到达不同的节点,就会提高缓存 miss 率。两种克服这种困难的方法是:全局缓存和分布式缓存。

全局缓存

所有节点使用同一缓存空间。这包括增加一台服务器或是某种类型的文件存储,并且所有请求层的节点均可以访问全局缓存。这种类型的缓存机制可能会变得比较复杂,因为随着客户端和请求数量的增加,单个缓存服务器很容易被压垮,但是在一些架构中非常有效(特别是有专门定制的硬件使得访问全局缓存非常快速,或者需要缓存的数据集是固定的)。

通常有两种形式的全局缓存。

  1. 如果缓存中找不到对应的响应,那缓存自身会去从下层存储中获取丢失的数据。
  2. 当缓存中找不到相应数据时,需要请求节点自己去获取数据。

第一种方式相当于是全局缓存将查询缓存、底层获取数据、填充缓存这些操作一并做掉,理想情况下对于上层应用应该只需要提供一个获取数据的 API,上层应用无需关心所请求的数据是已存在于缓存中的还是从底层存储中获取的,能够更专注于上层业务逻辑,但这就可能需要这种全局缓存设计成能够根据传入 API 接口的参数去获取底层存储的数据,接口签名可以简化为 Object getData(String uniqueId, DataRetrieveCallback callback),第一个参数代表与缓存约定的唯一标示一个数据的 ID,第二个是一个获取数据回调接口,具体实现由调用该接口的业务端来实现,即当全局缓存中未找到 uniqueId 对应的缓存数据时,那就会以该 callback 去获取数据,并以 uniqueId 为 key、callback 获取数据为 value 放入全局缓存中。

6

图 1.7:全局缓存自身负责存取

第二种方式相对来说自由一些。请求节点自行根据业务场景需求来决定查询数据的方式,以及查数据后的处理(比如缓存回收策略),全局缓存只作为一个基础组件让请求节点能够在其中存取数据。

7

图 1.8 全局缓存,请求节点负责存取

大多数应用倾向于通过第一种方式使用全局缓存,由缓存自身来管理回收、获取数据,来应对从客户端发起的对同一数据的众多请求。但是,对于一些场景来说,第二种实现就比较有意义。比如,如果是用来缓存大型文件,那缓存低命中率将会导致缓存缓冲区被缓存 miss 给压垮;在这种情况下,缓存中缓存大部分数据集(或热门数据)将会有助解决这个问题。另一个例子是,一个架构中缓存的文件是静态、不应回收的。(这可能跟应用对于数据延迟的需求有关——对于大数据集来说,某些数据段需要被快速访问——这时应用的业务逻辑会比缓存更懂得回收策略或热点处理。)

分布式缓存

在一个分布式缓存中,每个节点拥有部分缓存的数据,如果将杂货店里的冰箱比作一个缓存,那么一个分布式缓存好比是将你的食物放在几个不同的地方——你的冰箱、食物柜、午餐饭盒里——非常便于取到快餐的地方而无需跑一趟商店。通常这类缓存使用 一致性 Hash 算法进行切分,这样一个请求节点在查询指定数据时,可以很快知道去哪里查询 ,并通过分布式缓存来判断数据可用性。这种场景下,每个节点都会拥有一部分缓存,并且会将请求传递到其他节点来获取数据,最后才到原始地方查询数据。因此,分布式缓存的一个优势就是通过往请求池里增加节点来扩大缓存空间。

分布式缓存的一个缺点在于节点丢失纠正问题。一些分布式缓存通过将复制数据多份存放在不同的节点来解决这个问题;但是,你可以想象到这样做会让逻辑迅速变得复杂,特别是当你向请求层增加或减少节点的时候。虽然一个节点丢失并且缓存失效,但请求仍然可以从源头来获取(数据)——所以这不一定是最悲剧的。

8

图 1.8 分布式缓存

缓存是以需要维护更多存储空间为代价的,特别是昂贵的内存方式;天下没有免费的午餐。缓存让事情变得更快,同时还保证了高负载条件下系统的功能,否则系统服务可能早已降级。

一个非常受欢迎的开源缓存叫做Memcached(既可以是本地又可以是分布式缓存)。它简单来说就是一个内存 key-value 存储,对任意数据存储和快速查找做了优化(时间复杂度 O(1))。

Facebook 使用了若干种不同类型的缓存以达到他们网站的性能要求(Facebook caching and performance)。他们在语言层面使用$GLOBALS 和 APC 缓存(在 PHP 中提供的函数调用)使得中间功能调用和结果更快。Facebook 使用一种全局缓存,分布在多台服务器上(Scaling memcached at Facebook),这样一个访问缓存的函数调用就会产生很多并行请求来从 Memcached 服务器集群获取数据。这使得他们能够在用户概况数据上获得更高的性能和吞吐量,并且有一个集中的地方去更新数据。

现在让我们来聊聊当数据不存在于缓存的时候应该做什么。

1.3.2. 代理

从基本层面来看,代理服务器是硬件/软件的一个中间层,用于接收从客户端发起的请求并传递到后端服务器。通常来说,代理是用来 过滤请求、记录请求日志或者有时对请求进行转换(增加/去除头文件,加密/解密或者进行压缩)

代理同样能够极大帮助协调多个服务器的请求,有机会从系统的角度来优化请求流量。使用代理来加快数据访问速度的方式之一是 将多个同种请求集中放到一个请求中,然后将单个结果返回到请求客户端。这就叫做压缩转发(collapsed forwarding)

假设在几个节点上存在对同样数据的请求(我们叫它 littleB),并且这份数据不在缓存里。如果请求通过代理路由,那么这些请求可以被压缩为一个,就意味着我们只需要从磁盘读取一次 littleB 即可。这种设计是会带来一定的开销,因为跟不用代理相比每个请求都会产生更高的延迟,并且一些请求会因为要与相同请求合并而产生一些延迟。但这种做法在高负载的情况下提高系统性能,特别是当相同的数据重复被请求。这很像缓存,但不用像缓存那样存储数据/文件,而是优化了对那些文件的请求或调用,并且充当那些客户端的代理。

例如,在局域网(LAN)代理中,客户端不需有自己的 IP 来连接互联网,而局域网会将对同样内容的客户端请求进行压缩。这里可能很容易产生困惑,因为许多代理同样也是缓存(因为在这里放一个缓存很合理),但不是所有缓存都能充当代理。

10

图 1.14:使用一个代理服务器来压缩请求

另一个使用代理的好方法是,可以用来压缩对那些在原始存储中空间上紧密联系的数据(磁盘连续块)的请求。例如,我们假设一群节点请求 B 的部分数据:B1、B2。我们可以对代理进行设置使其能够识别出不同请求的空间局部性,将它们压缩为单个请求并且只返回 bigB,最小化对原始数据的读取操作。当你随机访问 TB 级的数据时,这样会大幅降低请求时间。在高负载情况下或者当你只有有限的缓存,代理是非常有帮助的,因为代理可以从根本上将若干个请求合并为一个。

11

图 1.11:使用代理压缩空间上邻近的数据请求

你完全可以一并使用代理和缓存,但通常 最好将缓存放在代理之前使用 。这是因为缓存通过内存来提供数据非常快速,并且它也不关心多个对同样结果的请求。但如果缓存被放在代理服务器的后面,那在每个请求访问缓存前就会有额外的延迟,这会阻碍系统性能。

如果你在寻找一款代理想要加入到你的系统中,那有很多选择可供考虑;Squid 和 Varnish 都是经过路演并广泛应用于很多网站的生产环境中。这些代理方案做了很多优化来充分使用客户端与服务端的通信。安装其中之一并在 web 服务器层将其作为一个反向代理(将在下面的负载均衡小节解释)可以提高 web 服务器性能,降低处理来自客户端的请求所消耗的工作量。

1.3.3. 索引

使用索引来加快访问数据已经是优化数据访问性能众所周知的策略。索引是以增加存储开销和减慢写入速度(因为你必须同时写入数据并更新索引)的代价来得到更快读取的好处。

就像对于传统的关系数据库,你同样可以将这种概念应用到大数据集上。索引的诀窍在于你必须仔细考虑你的用户会如何使用你的数据。对于 TB 级但单项数据比较小比如 1KB 的数据集,索引是优化数据访问非常必要的方式。在一个大数据集中寻找一个小单元是非常困难的,因为你不可能在一个可接受的时间里遍历这么大的数据。并且,像这么一个大数据集很有可能是分布在几个物理设备上——这就意味着你需要有方法能够找到所要数据正确的物理位置。索引是达到这个的最好方法。

12

图 1.12:索引

假设你在寻找 B 的 part2 数据——你将如何知道到哪去找到它?如果你有一个按照数据类型(如 A,B,C)排序好的索引,它会告诉你数据 B 在哪里。然后你查找到位置,然后读取你所要的部分。

这些索引通常存放在内存中,或者在更靠近客户端请求的地方。伯克利数据库(BDBs)和树形数据结构经常用来有序地存储数据,非常适合通过索引来访问。

索引经常会有很多层,类似一个 map,将你从一个地方引导至另一个,以此类推,直到你获取到你所要的那份数据。

13

图 1.13:多层索引

索引也可以用来对同样的数据创建出一些不同的视图。对于大数据集来说,通过定义不同的过滤器和排序是一个很好的方式,而不需要创建很多额外数据拷贝。

在大型可伸缩的系统中,即使索引已被压缩但仍会变得很大,不易存储。在这个系统里,我们假设世界上有很多书——100,000,000 本——并且每本书仅有 10 页(为了便于计算),每页有 250 个单词,这就意味着一共有 2500 亿个单词。如果我们假设平均每个单词有 5 个字符,每个字符占用 8 个比特,每个单词 5 个字节,那么对于仅包含每个单词的索引的大小就达到 TB 级。所以你会发现创建像一些如词组、数据位置、出现次数之类的其他信息的索引将会增长得更快。

创建这些中间索引并且以更小的方式表达数据,将大数据的问题变得易于处理。数据可以分布在多台服务器但仍可以快速访问。索引是信息获取的基石,也是当今现代搜索引擎的基础。

1.3.4. 负载均衡

用于将负载分摊在一些列负责服务请求的节点上。这使得一个系统的多个节点能够为相同功能提供服务。它们主要目的是处理许多同时进行的连接并将这些连接路由到其中的一个请求节点上,使得系统能够可伸缩地通过增加节点来服务更多请求。

14

图 1.14 负载均衡器

有很多不同的用于服务请求的算法,包括随机挑选一个节点、循环或给予某些标准如内存/CPU 使用率选取节点。一个广泛使用的开源软件级负载均衡器是 HAProxy。

在一个分布式系统中,负责均衡器通常是放置在系统很前端的地方,这样就能路由所有进入系统的请求。在一个复杂的分布式系统中,一个请求被多个负载均衡器路由也是可能的。

15

图 1.15:多重负责均衡器

如同代理一般,一些负载均衡器也能根据不同类型的请求进行路由。(从技术上来说,就是所谓的反向代理。)

负载均衡器的挑战之一在于如何管理用户 session 数据。在一个电子商务网站,当你只有一个客户端时很容易让用户把东西放到他们的购物车并且在不同的访问间保存(这是很重要的,因为当用户回来时很有可能买放在购物车里的产品)。但是,如果一个用户先被路由到一个 session 节点,然后在他们下次访问时路由到另一个不同的节点,那将会因为新节点可能丢失用户购物车里的东西而产生不一致。解决办法之一通过粘性 session机制总是将用户路由到同一节点,但这样既很难享受到一些像自动 failover 的可靠机制了。

上段中提到的用户 session 问题,实际上在很多大型网站如淘宝、支付宝,都是通过一个分布式 session 的中间件来解决的。原理其实很简单,比如用户登录了支付宝,那么系统会给当前用户分配一个全局唯一的 sessionId 并写入到浏览器的 cookie 中,在后台服务端也会有专门的一个分布式存储以 sessionId 为 key 开辟一个空间存放该用户 session 数据。虽然应用都是集群部署方式,但每个无状态应用节点都会统一连接到该分布式存储。由于用户 session 数据是统一保存在分布式存储上,即对 session 数据的存取都是发生在同一个地方,而非各个节点内部,所以不会因为不同的请求路由到不同的应用节点上导致 session 数据不一致的情况。同时,这一方法不会像 sticky session 机制那样限制了系统的可伸缩性。如果出现 session 存取的性能问题,那只需通过扩展后端分布式存储即可解决。 如果系统只是由少数节点构成的,那么像 Round Robin DNS 那样的系统就更加明智,因为负责均衡器很贵而且增加了一层不必要的复杂度。当然在大型系统里有各种各样的调度和负载均衡算法,包括简单的像随机选择或循环方式,还有更加复杂的机制如考虑系统使用率和容量的。所有这些算法都分布化了流量和请求,并且提供像自动 failover 或者自动去除坏节点,这类对可靠性非常有帮助的工具。但是,这些先进特性也会使得问题诊断变得复杂化。比如,在一个高负载情况下,负载均衡器会去除掉那些变慢或者超时(由于请求过多)的节点,但这样反而加重了其他节点的恶劣处境。在这些情况下,全面监控变得很重要,因为从全局来看系统的流量和吞吐量正在下降(由于各节点服务请求越来越少),但从节点个体来看正在达到极限。

负载均衡器是一个非常简单能让你提高系统容量的方法,还能用来判断一个节点的健康度,这样当一个节点失去响应或者过载时,得益于系统不同节点的冗余性,可以将其从请求处理池中去除。

1.3.5. 队列

另一个扩展数据层的重要部分是有效管理写入操作。当系统比较简单,系统处理负载很低,数据库也很小,可以预见写入操作是很快的;但是,在更加复杂的系统中,写入操作的时间可能无法确定。例如,数据需要被写入到不同服务器或索引的多个地方,或者系统负载很高。这些情况下,写操作或者任何任务都会花费很长的时间,这时需要异步化系统才能提高系统的性能和可靠性;通常的方法之一是使用队列。

在小型系统中,一台服务器可以尽快地处理客户端请求。但是,当服务器接收到超过其处理能力的请求时,那每个客户端都只能被迫等待其他客户端请求完成才能得到响应。这种同步的方式将会严重降低客户端性能,客户端被强制等待。增加额外的服务器并不能解决这个问题;即使通过有效的负载均衡,依然难以保证最大化客户端性能所需做的公平分配的工作。

16

图 1.16:同步化请求

使用队列,当前有能力处理任务的 worker 去取下一个任务来做。这些任务可以是对数据库的写入操作,或是复杂一些的如生成文件的小型预览图。当一个客户端将任务的请求提交到队列后,它们不再需要被迫等待结果;取而代之的是,它们只需要确认请求被得到正确接收。

17

图 1.17:使用队列来管理请求

队列使得客户端能够以异步的方式进行工作。另一方面,一个同步化系统不会区分请求和响应,因此就无法分开管理。在一个异步化系统里,客户端提交任务请求,后端服务反馈一个收到任务的确认信息,并且客户端可以定期地查看任务的状态,一旦完成即可取得任务结果。在客户端等待一个异步请求完成时,它可以自由地处理其他的工作,即使是发起对其他服务的异步请求。

队列还能提供对服务断供/失败的保护措施。比如,很容易创建一个健壮的队列来重试那些由于服务器短暂失败的服务请求。更好的是通过使用队列来确保服务品质,而非将客户端直接面对断断续续的服务,因为那样会需要客户端复杂且经常不一致的错误处理。

队列是管理大型可伸缩分布式应用不同部分间通信的基础,可以通过很多方式来实现。有一些开源的队列如RabbitMQ, ActiveMQ, BeanstalkD,也有一些使用像 Zookeeper 的服务,还有像 Redis 那样的数据存储。

正如大家所知的双十一、双十二,这两天用户的请求可谓超级海量。拿支付宝来说,核心系统如支付、账务,即使使用了很多技术方案来确保高性能、高可用,但面对数倍于平时的请求量依然捉急。在开发了一套分布式队列基础中间件后,网站的吞吐量、可用性得到了很大的提高。同时,对于队列来说,除了将客户端请求与服务端处理分离外,通过对队列加上额外的一些特性,能够起到非常大的作用。比如,在队列上加入限流特性,当请求量大大超过后端服务处理能力时,可以采取丢弃请求的方式来保证系统、队列不至于被海量请求压垮;当请求量回到一定水平,再将限流放开。这种做法,正好满足了系统对可用性、性能、可伸缩性、可管理性的要求。

Copyright © Guanghui Wang all right reserved,powered by GitbookFile Modified: 2019-08-25 13:56:34

results matching ""

    No results matching ""