可伸縮Web架構(gòu)與分布式系統(tǒng)(2)- 構(gòu)建快速、可伸縮數(shù)據(jù)訪問的組件

開源軟件近年來已變?yōu)闃?gòu)建一些大型網(wǎng)站的基礎(chǔ)組件。并且伴隨著網(wǎng)站的成長,圍繞著它們架構(gòu)的最佳實踐和指導(dǎo)準(zhǔn)則已經(jīng)顯露。這篇文章旨在涉及一些在設(shè)計大型網(wǎng)站時需要考慮的關(guān)鍵問題和一些為達(dá)到這些目標(biāo)所使用的組件。上篇文章介紹了Web分布式系統(tǒng)設(shè)計準(zhǔn)則和基本原理,本文介紹構(gòu)建快速、可伸縮數(shù)據(jù)訪問的組件。

(上文)談及了在設(shè)計分布式系統(tǒng)中需要考慮的一些核心問題,現(xiàn)在讓我們來聊聊(比較)困難的部分:訪問數(shù)據(jù)的可伸縮性。大多數(shù)簡單的web應(yīng)用,例如LAMP棧應(yīng)用,看上去如圖1.5

隨著它們的成長,會有兩個主要的挑戰(zhàn):訪問應(yīng)用服務(wù)器和數(shù)據(jù)庫的可伸縮性。在一個高可伸縮的應(yīng)用設(shè)計中,應(yīng)用(或者web)服務(wù)器通常會最小化(minimized)并通常表現(xiàn)為一個非共享(無狀態(tài))架構(gòu)。這樣使得系統(tǒng)的應(yīng)用服務(wù)層能夠很好地進(jìn)行伸縮。這樣數(shù)據(jù)的結(jié)果是,壓力被向下推到了數(shù)據(jù)庫服務(wù)器和相關(guān)(底層)支持服務(wù);真正的伸縮和性能挑戰(zhàn)就在這一層起到作用。本章余下部分致力于(介紹)一些更加通用的策略和方法,通過更快的數(shù)據(jù)訪問使得這些類型的服務(wù)更加快速和可伸縮。

大多數(shù)系統(tǒng)可以極度簡化為像圖1.6這樣的。這是一個很好的開始。如果你有大量的數(shù)據(jù)且希望快速、簡單地訪問,就像你把糖果藏在你桌子第一個抽屜里。雖然被極度簡化,前面觀點仍暗示著兩個難題:存儲的可伸縮性和數(shù)據(jù)的快速訪問。
為了本節(jié),我們假設(shè)你有數(shù)以TB計的數(shù)據(jù)并且希望能讓用戶隨機(jī)訪問這些數(shù)據(jù)的一小部分。(見圖1.7)這就類似于在圖片應(yīng)用例子里定位文件服務(wù)器上一個圖片文件的位置。

由于很難將TB級的數(shù)據(jù)加載到內(nèi)存,所以這會使得事情變得非常有挑戰(zhàn)性;這(種訪問)將直接變?yōu)榇疟PIO操作。從磁盤讀取會比從內(nèi)存要慢得多——訪問內(nèi)存就像Chuck Norris一樣快,然而訪問磁盤比DMV線還要慢。這樣的速度差異對于大數(shù)據(jù)來說比較客觀(This speed difference really adds up for large data sets);順序讀方面訪問內(nèi)存的速度是訪問磁盤的6倍,而在隨機(jī)讀方面,前者是后者的十萬倍(參見”The Pathologies of Big Data”, http://queue.acm.org/detail.cfm?id=1563874)。而且,即使有唯一ID,從哪里能夠找到這樣一小塊數(shù)據(jù)仍然是一項艱巨的任務(wù)。這就好比從你藏糖果的地方不看一眼地想拿到最后一塊Jolly Rancher。

幸運的是,你有很多能把事情變得更加容易的選擇;其中重要的有如下4個:緩存、代理、索引、負(fù)載均衡。本節(jié)剩余部分將會討論每個用于加速數(shù)據(jù)訪問的概念。

緩存

緩存利用了本地引用原則的好處:最近訪問的數(shù)據(jù)可能被再次訪問。緩存幾乎被用在計算機(jī)運行的各層:硬件,操作系統(tǒng),web瀏覽器,web應(yīng)用等等。緩存就像短期的內(nèi)存:有著限定大小的空間,但通常比訪問原始數(shù)據(jù)源更快,并且包含有最近最多被訪問過的(數(shù)據(jù))項。緩存可以存在于架構(gòu)的各個層次,但會發(fā)現(xiàn)到經(jīng)常更靠近前端(非web前端界面,架構(gòu)上層),這樣就可盡快返回數(shù)據(jù)而不用經(jīng)過繁重的下層(處理)了。

在我們的API例子中,如何使用一個緩存來加速你的數(shù)據(jù)訪問速度呢?在這個場景下,你可以在很多地方插入一個緩存。選擇之一是在你的請求層節(jié)點中插入一個緩存,如圖1.8.

將緩存直接放置在請求層節(jié)點中讓本地存儲響應(yīng)數(shù)據(jù)變?yōu)榭赡?。每次對于一個服務(wù)的請求,節(jié)點將立即返回存在的本地、緩存的數(shù)據(jù)。如果(對應(yīng)的)緩存不存在,請求節(jié)點將會從磁盤中查詢數(shù)據(jù)。請求層節(jié)點的緩存既可以放置在內(nèi)存(更快)也可以在節(jié)點本地磁盤(比通過網(wǎng)絡(luò)快)上。

當(dāng)你擴(kuò)展到多個節(jié)點時,會發(fā)生什么呢?正如你看到的圖1.9,如果請求曾擴(kuò)展到多個節(jié)點,那么每個節(jié)點都可以擁有它自身的緩存。但是,如果你的負(fù)載均衡器將請求隨機(jī)分發(fā)到這些節(jié)點上,同樣的請求會到達(dá)不同的節(jié)點,就會提高緩存miss率。兩種克服這種困難的方法是:全局緩存和分布式緩存。

全局緩存

正如聽起來的一樣,全局緩存是指:所有節(jié)點使用同一緩存空間。這包括增加一臺服務(wù)器或是某種類型的文件存儲,比從你原始存儲地方(訪問)更快,并且所有請求層的節(jié)點均可以訪問(全局緩存)。所有請求節(jié)點統(tǒng)一像訪問其本地緩存般訪問(全局)緩存。這種類型的緩存機(jī)制可能會變得比較復(fù)雜,因為隨著客戶端和請求數(shù)量的增加,單個緩存(服務(wù)器)很容易被壓垮,但是在一些架構(gòu)中非常有效(特別是有專門定制的硬件使得訪問全局緩存非??焖伲蛘咝枰彺娴臄?shù)據(jù)集是固定的)。

通常有兩種形式的全局緩存,如下圖。圖1.10中,如果緩存中找不到對應(yīng)的響應(yīng),那緩存自身會去從下層存儲中獲取丟失的數(shù)據(jù)。在圖1.11中,當(dāng)緩存中找不到相應(yīng)數(shù)據(jù)時,需要請求節(jié)點自己去獲取數(shù)據(jù)。

【譯者注】第一種方式相當(dāng)于是全局緩存將查詢緩存、底層獲取數(shù)據(jù)、填充緩存這些操作一并做掉,理想情況下對于上層應(yīng)用應(yīng)該只需要提供一個獲取數(shù)據(jù)的API,上層應(yīng)用無需關(guān)心所請求的數(shù)據(jù)是已存在于緩存中的還是從底層存儲中獲取的,能夠更專注于上層業(yè)務(wù)邏輯,但這就可能需要這種全局緩存設(shè)計成能夠根據(jù)傳入API接口的參數(shù)去獲取底層存儲的數(shù)據(jù),譯者認(rèn)為接口簽名可以簡化為Object getData(String uniqueId, DataRetrieveCallback callback),第一個參數(shù)代表與緩存約定的唯一標(biāo)示一個數(shù)據(jù)的ID,第二個是一個獲取數(shù)據(jù)回調(diào)接口,具體實現(xiàn)由調(diào)用該接口的業(yè)務(wù)端來實現(xiàn),即當(dāng)全局緩存中未找到uniqueId對應(yīng)的緩存數(shù)據(jù)時,那就會以該callback去獲取數(shù)據(jù),并以uniqueId為key、callback獲取數(shù)據(jù)為value放入全局緩存中。第二種方式相對來說自由一些。請求節(jié)點自行根據(jù)業(yè)務(wù)場景需求來決定查詢數(shù)據(jù)的方式,以及查數(shù)據(jù)后的處理(比如緩存回收策略),全局緩存只作為一個基礎(chǔ)組件讓請求節(jié)點能夠在其中存取數(shù)據(jù)。

大多數(shù)應(yīng)用傾向于通過第一種方式使用全局緩存,由緩存自身來管理回收、獲取數(shù)據(jù),來應(yīng)對從客戶端發(fā)起的對同一數(shù)據(jù)的眾多請求。但是,對于一些場景來說,第二種實現(xiàn)就比較有意義。比如,如果是用來緩存大型文件,那緩存低命中率將會導(dǎo)致緩存緩沖區(qū)被緩存miss給壓垮;在這種情況下,緩存中緩存大部分?jǐn)?shù)據(jù)集(或熱門數(shù)據(jù))將會有助解決這個問題。另一個例子是,一個架構(gòu)中緩存的文件是靜態(tài)、不應(yīng)回收的。(這可能跟應(yīng)用對于數(shù)據(jù)延遲的需求有關(guān)——對于大數(shù)據(jù)集來說,某些數(shù)據(jù)段需要被快速訪問——這時應(yīng)用的業(yè)務(wù)邏輯會比緩存更懂得回收策略或熱點處理。)

分布式緩存

在一個分布式緩存中(如圖1.12),沒個節(jié)點擁有部分緩存的數(shù)據(jù),如果將雜貨店里的冰箱比作一個緩存,那么一個分布式緩存好比是將你的食物放在幾個不同的地方——你的冰箱、食物柜、午餐飯盒里——非常便于取到快餐的地方而無需跑一趟商店。通常這類緩存使用一致性Hash算法進(jìn)行切分,這樣一個請求節(jié)點在查詢指定數(shù)據(jù)時,可以很快知道去哪里查詢,并通過分布式緩存來判斷數(shù)據(jù)可用性。這種場景下,每個節(jié)點都會擁有一部分緩存,并且會將請求傳遞到其他節(jié)點來獲取數(shù)據(jù),最后才到原始地方查詢數(shù)據(jù)。因此,分布式緩存的一個優(yōu)勢就是通過往請求池里增加節(jié)點來擴(kuò)大緩存空間。

分布式緩存的一個缺點在于節(jié)點丟失糾正問題。一些分布式緩存通過將復(fù)制數(shù)據(jù)多份存放在不同的節(jié)點來解決這個問題;但是,你可以想象到這樣做會讓邏輯迅速變得復(fù)雜,特別是當(dāng)你向請求層增加或減少節(jié)點的時候。雖然一個節(jié)點丟失并且緩存失效,但請求仍然可以從源頭來獲?。〝?shù)據(jù))——所以這不一定是最悲劇的。

緩存的偉大之處在于它們讓事情進(jìn)行的更快(當(dāng)然需要執(zhí)行正確)。你所選擇的方法只是讓你能夠更快處理更多的請求。但是,這些緩存是以需要維護(hù)更多存儲空間為代價的,特別是昂貴的內(nèi)存方式;天下沒有免費的午餐。緩存讓事情變得更快,同時還保證了高負(fù)載條件下系統(tǒng)的功能,否則(系統(tǒng))服務(wù)可能早已降級。

一個非常受歡迎的開源緩存叫做Memcached(http://memcached.org/)(既可以是本地又可以是分布式緩存);但是,還有很多其他選擇(包括許多語言/框架特定選擇)。Memcached被應(yīng)用于許多大型web網(wǎng)站,縱然它功能強(qiáng)大,但它簡單來說就是一個內(nèi)存key-value存儲,對任意數(shù)據(jù)存儲和快速查找做了優(yōu)化(時間復(fù)雜度O(1))。

Facebook使用了若干種不同類型的緩存以達(dá)到他們網(wǎng)站的性能(要求,參加see “Facebook caching and performance“)。他們在語言層面使用$GLOBALS和APC緩存(在PHP中提供的函數(shù)調(diào)用)使得中間功能調(diào)用和(得到)結(jié)果更加快速。(大多數(shù)語言都有這種類型的類庫來提高web性能,應(yīng)該經(jīng)常去使用。)Facebook使用一種全局緩存,分布在多臺服務(wù)器上(參見”Scaling memcached at Facebook“),這樣一個訪問緩存的函數(shù)調(diào)用就會產(chǎn)生很多并行請求來從Memcached服務(wù)器(集群)獲取數(shù)據(jù)。這使得他們能夠在用戶概況數(shù)據(jù)上獲得更高的性能和吞吐量,并且有一個集中的地方去更新數(shù)據(jù)(當(dāng)你運行著數(shù)以千計的服務(wù)器時,緩存失效、管理一致性都將變得很有挑戰(zhàn),所以這是很重要的)。
現(xiàn)在讓我們來聊聊當(dāng)數(shù)據(jù)不存在于緩存的時候應(yīng)該做什么。

代理

從基本層面來看,代理服務(wù)器是硬件/軟件的一個中間層,用于接收從客戶端發(fā)起的請求并傳遞到后端服務(wù)器。通常來說,代理是用來過濾請求、記錄請求日志或者有時對請求進(jìn)行轉(zhuǎn)換(增加/去除頭文件,加密/解密或者進(jìn)行壓縮)。

代理同樣能夠極大幫助協(xié)調(diào)多個服務(wù)器的請求,有機(jī)會從系統(tǒng)的角度來優(yōu)化請求流量。使用代理來加快數(shù)據(jù)訪問速度的方式之一是將多個同種請求集中放到一個請求中,然后將單個結(jié)果返回到請求客戶端。這就叫做壓縮轉(zhuǎn)發(fā)(原文叫做collapsed forwarding)。

假設(shè)在幾個節(jié)點上存在對同樣數(shù)據(jù)的請求(我們叫它littleB),并且這份數(shù)據(jù)不在緩存里。如果請求通過代理路由,那么這些請求可以被壓縮為一個,就意味著我們只需要從磁盤讀取一次littleB即可。(見圖1.14)這種設(shè)計是會帶來一定的開銷,因為每個請求都會產(chǎn)生更高的延遲(跟不用代理相比),并且一些請求會因為要與相同請求合并而產(chǎn)生一些延遲。但這種做法在高負(fù)載的情況下提高系統(tǒng)性能,特別是當(dāng)相同的數(shù)據(jù)重復(fù)被請求。這很像緩存,但不用像緩存那樣存儲數(shù)據(jù)/文件,而是優(yōu)化了對那些文件的請求或調(diào)用,并且充當(dāng)那些客戶端的代理。
例如,在局域網(wǎng)(LAN)代理中,客戶端不需有自己的IP來連接互聯(lián)網(wǎng),而局域網(wǎng)會將對同樣內(nèi)容的客戶端請求進(jìn)行壓縮。這里可能很容易產(chǎn)生困惑,因為許多代理同樣也是緩存(因為在這里放一個緩存很合理),但不是所有緩存都能充當(dāng)代理。

另一個使用代理的好方法是,不單把代理用來壓縮對同樣數(shù)據(jù)的請求,還可以用來壓縮對那些在原始存儲中空間上緊密聯(lián)系的數(shù)據(jù)(磁盤連續(xù)塊)的請求。使用這一策略最大化(利用)所請求數(shù)據(jù)的本地性,可以減少請求延遲。例如,我們假設(shè)一群節(jié)點請求B的部分(數(shù)據(jù)):B1, B2,等。我們可以對代理進(jìn)行設(shè)置使其能夠識別出不同請求的空間局部性,將它們壓縮為單個請求并且只返回bigB,最小化對原始數(shù)據(jù)的讀取操作。(見圖1.15)當(dāng)你隨機(jī)訪問TB級的數(shù)據(jù)時,這樣會大幅改變(降低)請求時間。在高負(fù)載情況下或者當(dāng)你只有有限的緩存,代理是非常有幫助的,因為代理可以從根本上將若干個請求合并為一個。

你完全可以一并使用代理和緩存,但通常最好將緩存放在代理之前使用,正如在馬拉松賽跑中最好讓跑得快的選手跑在前面。這是因為緩存通過內(nèi)存來提供數(shù)據(jù)非??焖?,并且它也不關(guān)心多個對同樣結(jié)果的請求。但如果緩存被放在代理服務(wù)器的另一邊(后面),那在每個請求訪問緩存前就會有額外的延遲,這會阻礙系統(tǒng)性能。

如果你在尋找一款代理想要加入到你的系統(tǒng)中,那有很多選擇可供考慮;Squid和Varnish都是經(jīng)過路演并廣泛應(yīng)用于很多網(wǎng)站的生產(chǎn)環(huán)境中。這些代理方案做了很多優(yōu)化來充分使用客戶端與服務(wù)端的通信。安裝其中之一并在web服務(wù)器層將其作為一個反向代理(將在下面的負(fù)載均衡小節(jié)解釋)可以提高web服務(wù)器相當(dāng)大的性能,降低處理來自客戶端的請求所消耗的工作量。

索引

使用索引來加快訪問數(shù)據(jù)已經(jīng)是優(yōu)化數(shù)據(jù)訪問性能眾所周知的策略;可能更多來自數(shù)據(jù)庫。索引是以增加存儲開銷和減慢寫入速度(因為你必須同時寫入數(shù)據(jù)并更新索引)的代價來得到更快讀取的好處。

就像對于傳統(tǒng)的關(guān)系數(shù)據(jù)庫,你同樣可以將這種概念應(yīng)用到大數(shù)據(jù)集上。索引的訣竅在于你必須仔細(xì)考慮你的用戶會如何使用你的數(shù)據(jù)。對于TB級但單項數(shù)據(jù)比較?。ū热?KB,原文這里寫的是small payload)的數(shù)據(jù)集,索引是優(yōu)化數(shù)據(jù)訪問非常必要的方式。在一個大數(shù)據(jù)集中尋找一個小單元是非常困難的,因為你不可能在一個可接受的時間里遍歷這么大的數(shù)據(jù)。并且,像這么一個大數(shù)據(jù)集很有可能是分布在幾個(或更多)物理設(shè)備上——這就意味著你需要有方法能夠找到所要數(shù)據(jù)正確的物理位置。索引是達(dá)到這個的最好方法。

索引可以像一張可以引導(dǎo)你至所要數(shù)據(jù)位置的表格來使用。例如,我們假設(shè)你在尋找B的part2數(shù)據(jù)——你將如何知道到哪去找到它?如果你有一個按照數(shù)據(jù)類型(如A,B,C)排序好的索引,它會告訴你數(shù)據(jù)B在哪里。然后你查找到位置,然后讀取你所要的部分。(見圖1.16)這些索引通常存放在內(nèi)存中,或者在更靠近客戶端請求的地方。伯克利數(shù)據(jù)庫(BDBs)和樹形數(shù)據(jù)結(jié)構(gòu)經(jīng)常用來有序地存儲數(shù)據(jù),非常適合通過索引來訪問。

索引經(jīng)常會有很多層,類似一個map,將你從一個地方引導(dǎo)至另一個,以此類推,直到你獲取到你所要的那份數(shù)據(jù)。(見圖1.17)

索引也可以用來對同樣的數(shù)據(jù)創(chuàng)建出一些不同的視圖。對于大數(shù)據(jù)集來說,通過定義不同的過濾器和排序是一個很好的方式,而不需要創(chuàng)建很多額外數(shù)據(jù)拷貝。

例如,假設(shè)之前的圖片托管系統(tǒng)就是在管理書頁上的圖片,并且服務(wù)能夠允許客戶端查詢圖片中的文字,按照標(biāo)題搜索整本書的內(nèi)容,就像搜索引擎允許你搜索HTML內(nèi)容一樣。這種場景下,所有書中的圖片需要很多很多的服務(wù)器去存儲文件,查找到其中一頁渲染給用戶將會是比較復(fù)雜的。首先,對需要易于查詢的任意單詞、詞組進(jìn)行倒排索引;然后挑戰(zhàn)在于導(dǎo)航至那本書具體的頁面、位置并獲取到正確的圖片。所以,在這一場景,倒排索引將會映射到一個位置(比如B書),然后B可能會包含每個部分的所有單詞、位置、出現(xiàn)次數(shù)的索引。倒排索引可能如同下圖——每個單詞或詞組會提供一個哪些書包含它的索引。


這種中間索引看上去都類似,僅會包含單詞、位置和B的一些信息。這種嵌套索引的架構(gòu)允許每個索引占用更少的空間而非將所有的信息存放在一個巨大的倒排索引中。

在大型可伸縮的系統(tǒng)中,即使索引已被壓縮但仍會變得很大,不易存儲。在這個系統(tǒng)里,我們假設(shè)世界上有很多書——100,000,000本——并且每本書僅有10頁(為了便于計算),每頁有250個單詞,這就意味著一共有2500億個單詞。如果我們假設(shè)平均每個單詞有5個字符,每個字符占用8個比特,每個單詞5個字節(jié),那么對于僅包含每個單詞的索引的大小就達(dá)到TB級。所以你會發(fā)現(xiàn)創(chuàng)建像一些如詞組、數(shù)據(jù)位置、出現(xiàn)次數(shù)之類的其他信息的索引將會增長得更快。

創(chuàng)建這些中間索引并且以更小的方式表達(dá)數(shù)據(jù),將大數(shù)據(jù)的問題變得易于處理。數(shù)據(jù)可以分布在多臺服務(wù)器但仍可以快速訪問。索引是信息獲取的基石,也是當(dāng)今現(xiàn)代搜索引擎的基礎(chǔ)。當(dāng)然,這一小節(jié)僅僅是揭開表面,為了把索引變得更小、更快、包含更多信息(比如關(guān)聯(lián))、無縫更新,還有大量的研究工作要做。(還有一些可管理性方面的挑戰(zhàn),比如競爭條件、增加或修改數(shù)據(jù)所帶來的更新操作,特別是再加上關(guān)聯(lián)、scoring)
能夠快速、簡單地找到你的數(shù)據(jù)非常重要;索引是達(dá)到這一目標(biāo)非常有效、簡單的工具。

負(fù)載均衡

另一個任何分布式系統(tǒng)的關(guān)鍵組件是負(fù)載均衡器。負(fù)載均衡器是任何架構(gòu)的關(guān)鍵部分,用于將負(fù)載分?jǐn)傇谝恍┝胸?fù)責(zé)服務(wù)請求的節(jié)點上。這使得一個系統(tǒng)的多個節(jié)點能夠為相同功能提供服務(wù)。(見圖1.18)它們主要目的是處理許多同時進(jìn)行的連接并將這些連接路由到其中的一個請求節(jié)點上,使得系統(tǒng)能夠可伸縮地通過增加節(jié)點來服務(wù)更多請求。

有很多不同的用于服務(wù)請求的算法,包括隨機(jī)挑選一個節(jié)點、循環(huán)(round robin)或給予某些標(biāo)準(zhǔn)如內(nèi)存/CPU使用率選取節(jié)點。一個廣泛使用的開源軟件級負(fù)載均衡器是HAProxy。

在一個分布式系統(tǒng)中,負(fù)責(zé)均衡器通常是放置在系統(tǒng)很前端的地方,這樣就能路由所有進(jìn)入(系統(tǒng))的請求。在一個復(fù)雜的分布式系統(tǒng)中,一個請求被多個負(fù)載均衡器路由也不是不可能。(見圖1.19)

如同代理一般,一些負(fù)載均衡器也能根據(jù)不同類型的請求進(jìn)行路由。(從技術(shù)上來說,就是所謂的反向代理。)
負(fù)載均衡器的挑戰(zhàn)之一在于(如何)管理用戶session數(shù)據(jù)。在一個電子商務(wù)網(wǎng)站,當(dāng)你只有一個客戶端時很容易讓用戶把東西放到他們的購物車并且在不同的訪問間保存(這是很重要的,因為當(dāng)用戶回來時很有可能買放在購物車?yán)锏漠a(chǎn)品)。但是,如果一個用戶先被路由到一個session節(jié)點,然后在他們下次訪問時路由到另一個不同的節(jié)點,那將會因為新節(jié)點可能丟失用戶購物車?yán)锏臇|西而產(chǎn)生不一致。(如果你精心挑選了6包Mountain Dew放到購物車,但當(dāng)你回來的時候發(fā)現(xiàn)購物車清空了,你會不會很沮喪?)解決辦法之一通過粘性session機(jī)制總是將用戶路由到同一節(jié)點,但這樣既很難享受到一些像自動failover的可靠機(jī)制了。在這一場景下,用戶的購物車總是會有東西的,如果他們所對應(yīng)的粘性節(jié)點不可用了,那么就會是一個特殊情況對于(保存)在那里的東西的假設(shè)就無效了(當(dāng)然我們希望這種假設(shè)不會出現(xiàn)在應(yīng)用里)。當(dāng)然,這個問題可以通過本章中的一些其他策略或者工具來解決,比如服務(wù),還有一些沒有提到的(如瀏覽器緩存、cookie、URL地址重寫)。

【譯者注】上段中提到的用戶session問題,實際上在很多大型網(wǎng)站如淘寶、支付寶,都是通過一個分布式session的中間件來解決的。原理其實很簡單,比如用戶登錄了支付寶,那么系統(tǒng)會給當(dāng)前用戶分配一個全局唯一的sessionId并寫入到瀏覽器的cookie中,在后臺服務(wù)端也會有專門的一個分布式存儲以sessionId為key開辟一個空間存放該用戶session數(shù)據(jù)。雖然應(yīng)用都是集群部署方式,但每個無狀態(tài)應(yīng)用節(jié)點都會統(tǒng)一連接到該分布式存儲。由于用戶session數(shù)據(jù)是統(tǒng)一保存在分布式存儲上,即對session數(shù)據(jù)的存取都是發(fā)生在同一個地方,而非各個節(jié)點內(nèi)部,所以不會因為不同的請求路由到不同的應(yīng)用節(jié)點上導(dǎo)致session數(shù)據(jù)不一致的情況。同時,這一方法不會像sticky session機(jī)制那樣限制了系統(tǒng)的可伸縮性。如果出現(xiàn)session存取的性能問題,那只需通過擴(kuò)展后端分布式存儲即可解決。如果系統(tǒng)只是由少數(shù)節(jié)點構(gòu)成的,那么像Round Robin DNS那樣的系統(tǒng)就更加明智,因為負(fù)責(zé)均衡器很貴而且增加了一層不必要的復(fù)雜度。當(dāng)然在大型系統(tǒng)里有各種各樣的調(diào)度和負(fù)載均衡算法,包括簡單的像隨機(jī)選擇或循環(huán)方式,還有更加復(fù)雜的機(jī)制如考慮(系統(tǒng))使用率和容量的。所有這些算法都分布化了流量和請求,并且提供像自動failover或者自動去除壞節(jié)點(當(dāng)該節(jié)點失去響應(yīng)后)這類對可靠性非常有幫助的工具。但是,這些先進(jìn)特性也會使得問題診斷變得復(fù)雜化。比如,在一個高負(fù)載情況下,負(fù)載均衡器會去除掉那些變慢或者超時(由于請求過多)的節(jié)點,但這樣反而加重了其他節(jié)點的(惡劣)處境。在這些情況下,全面監(jiān)控變得很重要,因為從全局來看系統(tǒng)的流量和吞吐量正在下降(由于各節(jié)點服務(wù)請求越來越少),但從節(jié)點個體來看正在達(dá)到極限。

負(fù)載均衡器是一個非常簡單能讓你提高系統(tǒng)容量的方法,并且像本文其他的技術(shù)一樣,在分布式系統(tǒng)架構(gòu)中扮演者重要角色。負(fù)載均衡器還能用來判斷一個節(jié)點的健康度,這樣當(dāng)一個節(jié)點失去響應(yīng)或者過載時,得益于系統(tǒng)不同節(jié)點的冗余性,可以將其從請求處理池中去除。

至此,我們已經(jīng)覆蓋了很多用于加快數(shù)據(jù)讀取的方法,另一個擴(kuò)展數(shù)據(jù)層的重要部分是有效管理寫入操作。當(dāng)系統(tǒng)比較簡單,系統(tǒng)處理負(fù)載很低,數(shù)據(jù)庫也很小,可以預(yù)見寫入操作是很快的;但是,在更加復(fù)雜的系統(tǒng)中,寫入操作的時間可能無法確定。例如,數(shù)據(jù)需要被寫入到不同服務(wù)器或索引的多個地方,或者系統(tǒng)負(fù)載很高。這些情況下,由于上面的原因,寫操作或者任何任務(wù)都會花費很長的時間,這時需要異步化系統(tǒng)才能提高系統(tǒng)的性能和可靠性;通常的方法之一是使用隊列。

假設(shè)在一個系統(tǒng)中,每個客戶端在請求遠(yuǎn)程服務(wù)來處理任務(wù)。每個客戶端將其請求送至服務(wù)器,服務(wù)器盡可能快地完成這些任務(wù)并返回結(jié)果給相應(yīng)的客戶端。在小型系統(tǒng)中,當(dāng)一臺服務(wù)器(或者邏輯上的一個服務(wù))可以盡快地服務(wù)到來的客戶端(請求),這種情況下(系統(tǒng))工作會比較好。但是,當(dāng)服務(wù)器接收到超過其處理能力的請求時,那每個客戶端都只能被迫等待其他客戶端請求完成才能得到響應(yīng)。圖1.20描繪的就是一個同步請求的例子。

這種同步的方式將會嚴(yán)重降低客戶端性能;客戶端被強(qiáng)制等待,在請求被響應(yīng)前什么都做不了。增加額外的服務(wù)器并不能解決這個問題;即使通過有效的負(fù)載均衡,依然難以保證最大化客戶端性能所需做的公平分配的工作。更進(jìn)一步來說,當(dāng)處理請求的服務(wù)器不可用或掛掉了,那么上游的客戶端同樣也會失敗。有效解決這個問題需要抽象化客戶端的請求和真正服務(wù)它所做的工作。

現(xiàn)在進(jìn)入隊列環(huán)節(jié)。一個隊列,正如聽上去的,簡單來說就是當(dāng)一個任務(wù)過來時,會被加入到隊列中,然后會有當(dāng)前有能力處理(任務(wù))的worker去取下一個任務(wù)來做。(見圖1.21。)這些任務(wù)可以是對數(shù)據(jù)庫的寫入操作,或是復(fù)雜一些的如生成文件的小型預(yù)覽圖。當(dāng)一個客戶端將任務(wù)的請求提交到隊列后,它們不再需要被迫等待結(jié)果;取而代之的是,它們只需要確認(rèn)請求被得到正確接收。當(dāng)客戶端需要的時候,這個確認(rèn)此后可以當(dāng)做是任務(wù)結(jié)果的引用。

隊列使得客戶端能夠以異步的方式進(jìn)行工作,至關(guān)重要地抽象了一個客戶端請求及其響應(yīng)。另一方面,一個同步化系統(tǒng)不會區(qū)分請求和響應(yīng),因此就無法分開管理。在一個異步化系統(tǒng)里,客戶端提交任務(wù)請求,后端服務(wù)反饋一個收到任務(wù)的確認(rèn)信息,并且客戶端可以定期地查看任務(wù)的狀態(tài),一旦完成即可取得任務(wù)結(jié)果。在客戶端等待一個異步請求完成時,它可以自由地處理其他的工作,即使是發(fā)起對其他服務(wù)的異步請求。上面第二個就是分布式系統(tǒng)中采用隊列和消息的例子。

隊列還能提供對服務(wù)斷供/失敗的保護(hù)措施。比如,很容易創(chuàng)建一個健壯的隊列來重試那些由于服務(wù)器短暫失敗的服務(wù)請求。更好的是通過使用隊列來確保服務(wù)品質(zhì),而非將客戶端直接面對斷斷續(xù)續(xù)的服務(wù),因為那樣會需要客戶端復(fù)雜且經(jīng)常不一致的錯誤處理。

隊列是管理大型可伸縮分布式應(yīng)用不同部分間通信的基礎(chǔ),可以通過很多方式來實現(xiàn)。有一些開源的隊列如RabbitMQ, ActiveMQ, BeanstalkD,也有一些使用像Zookeeper的服務(wù),還有像Redis那樣的數(shù)據(jù)存儲。

【譯者注】隊列是分布式系統(tǒng)異步化的一個關(guān)鍵基礎(chǔ)組件。在淘寶、支付寶這類大型分布式網(wǎng)站中應(yīng)用廣泛。正如大家所知的雙十一、雙十二,這兩天用戶的請求可謂超級海量。拿支付寶來說,核心系統(tǒng)如支付、賬務(wù),即使使用了很多技術(shù)方案來確保高性能、高可用,但面對數(shù)倍、數(shù)十倍于平時的請求量,依然捉急。在開發(fā)了一套分布式隊列基礎(chǔ)中間件后,網(wǎng)站的吞吐量、可用性得到了很大的提高。同時,對于隊列來說,除了將客戶端請求與服務(wù)端處理分離外,通過對隊列加上額外的一些特性,能夠起到非常大的作用。比如,在隊列上加入限流特性,當(dāng)請求量大大超過后端服務(wù)處理能力時,可以采取丟棄請求的方式來保證系統(tǒng)、隊列不至于被海量請求壓垮;當(dāng)請求量回到一定水平,再將限流放開。這種做法,正好滿足了系統(tǒng)對可用性、性能、可伸縮性、可管理性的要求。

總結(jié)

設(shè)計出能夠快速訪問大量數(shù)據(jù)的高效系統(tǒng)(的方法)是存在的,并且又很多非常棒的工具來幫助各種各樣的新應(yīng)用來達(dá)到這一點。本章只覆蓋了少量例子,僅僅是掀開了面紗,但其實還有更多,并將繼續(xù)保持創(chuàng)新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容