構建高可擴Web架構和分佈式系統實戰(下)(轉載自CSDN)

本文作者Kate Matsudaira是一位美麗的女工程副總裁,曾在Sun Microsystems、微軟、亞馬遜這些一流的IT公司任職。她有着非常豐富的工作經驗和團隊管理經驗,當過程序員、項目經理、產品經理以及人事經理。專注於構建和操作大型Web應用程序/網站,目前她的主要研究方向是SaaS(軟件即服務)應用程序和雲計算(如大家所說的大數據)。

本文是作者在AOSA一書介紹如何構建可擴展的分佈式系統裏的內容,我們進行了翻譯並分爲上下兩篇分佈分享給大家。在上一篇《構建高可擴Web架構和分佈式系統實戰》中,我們舉例討論了設計分佈式系統需要考慮的核心要素:可用性、性能、可靠性、可擴展、易管理、成本。而在這篇文章中,我們將深入介紹如何設計可擴展的數據訪問,包括負載均衡、代理、全局緩存、分佈式緩存等。

構建快速可伸縮的數據訪問塊

在討論完設計分佈式系統的核心考慮因素後,下面讓我們再一起討論難點部分:可擴展的數據訪問。

大多數簡單的Web應用程序,例如LAMP堆棧應用程序,看起來如圖5所示:

 圖5:簡單的Web應用程序

隨着系統漸漸擴大,他們將面臨兩大主要挑戰:構建可擴展的應用程序服務器和數據訪問機制。在一個高可擴的應用程序設計中,通常最小化的應用程序(或Web)服務往往能體現一種無共享的架構。這就使得應用程序服務器層要進行橫向擴展,這種設計的結果就是把繁重的工作轉移到堆棧下層的數據庫服務和配置服務上,這纔是這一層上真正的可擴展和性能挑戰。

本文的剩餘部分專門討論一些常見策略和方法來使這些服務可以快速和可擴展,提升數據的訪問速度。

圖6 過於簡化的的Web應用程序

大多數系統可能會簡化成圖6那樣,這是個非常不錯的起點。如果你有大量的數據,想要快速便捷地訪問,就好比在你書桌抽屜的最上面有一堆糖果。雖然過於簡化,但也暗示了兩個難題:可擴展存儲和快速的數據訪問。

爲了這個例子,讓我們假設有許多太字節(TB)數據,並且允許用戶隨機訪問一小部分數據(見圖7)。這與本文圖片應用程序裏的在文件服務器上定位一個圖片文件非常相似。

圖7 訪問特定數據

這也是個非常大的挑戰,把TB級數據加載到內存中的成本比較昂貴,這可以直接轉化到磁盤上進行IO。從磁盤上讀取要比內存慢的多——內存訪問和Chuck Norris一樣快,而磁盤的訪問速度要比在DMV上慢。這個速度不同於大數據集上的合計,實數內存訪問大概要比順序訪問快6倍,或者比隨機從磁盤上讀取快10萬倍(參考The Pathologies of Big Data)。此外,即使是unique ID,想要在較少的數據中查找確切的位置也是一項艱鉅的任務。

幸運的是,能找到許多方法讓這個問題變的簡單,這裏提供4個非常重要的方法:緩存、代理、索引和負載均衡器。在下面將會詳細討論這4個內容來提升數據訪問速度。

緩存

緩存就是利用本地參考原則:當CPU要讀取一個數據時,首先從緩存中查找,找到就立即讀取並送給CPU處理;沒有找到,就用相對慢的速率從內存中讀取並送給CPU處理,同時把這個數據所在的數據塊調入緩存中,可以使得以後對整塊數據的讀取都從緩存中進行,不必再調用內存。它們幾乎被用在每一個計算層上:硬件、操作系統、Web瀏覽器、Web應用程序等。一個緩存就相當於是一個臨時內存:它有一個有限的空間量,但訪問它比訪問原始數據速度要快。緩存也可以存在於各個層的架構中,但經常在離前端最近的那個層上發現,在那裏可以快速實現並返回數據,無需佔用下游層數據。

那麼如何在我們的API例子裏利用緩存使數據訪問更快呢?在這種情況下,有許多地方可以插入緩存。一種是在請求層節點上插入緩存,如圖8所示。

圖8 在請求層節點插入緩存

在請求層節點上放置一個緩存,即可響應本地的存儲數據。當對服務器發送一個請求時,如果本地存在所請求數據,那麼該節點即會快速返回本地緩存數據。如果本地不存在,那麼請求節點將會查詢磁盤上的數據。請求層節點緩存即可以存在於內存中(這個非常快速)也可以位於該節點的本地磁盤上(比訪問網絡存儲要快)。

 

圖9 多個緩存

當擴展到許多節點的時候,會發生什麼呢?如圖9所示,如果請求層被擴展爲多個節點,它仍然有可能訪問每個節點所在的主機緩存。然而,如果你的負載均衡器隨機分佈節點之間的請求,那麼請求將會訪問各個不同的節點,因此緩存遺漏將會增加。這裏有兩種方法可以克服這個問題:全局緩存和分佈式緩存。

全局緩存

顧名思義,全局緩存是指所有節點都使用同一個緩存空間。這包含添加一臺服務器或某種類型的文件存儲,所有請求層節點訪問該存儲要比原始存儲快。每個請求節點會以同種方式查詢緩存,這種緩存方案可能有點複雜,隨着客戶機和請求數量的增加,單個緩存(Cache)很容易溢出,但在某些結構中卻是非常有效的(特別是那些特定的硬件,專門用來提升全局緩存速度,或者是需要被緩存的特定數據集)。

在圖10中描述了全局緩存常見的兩種方式。當一個Cache響應在高速緩存中沒有發現時,Cache自己會從底層存儲中檢索缺少的那塊數據。如圖11所示,請求節點去檢索那些在高速緩存中沒有發現的數據。

圖10 負責檢索數據的全局緩存

圖11 全局緩存裏負責檢索的請求節點

大多使用全局緩存的應用程序都傾向於使用第一種類型,利用Cache本身來驅逐和獲取數據以防止來自客戶端的同一個數據區發出大量的請求。然而,在某些情況下,使用第二種實現反而更有意義。例如,如果該緩存用於存儲大量的文件,低緩存的命中率會導致高速緩衝區不堪重負和緩存遺漏,在這種情況下, it helps to have a large percentage of the total data set (or hot data set) in the cache.

分佈式緩存

分佈式緩存即緩存在分佈式系統各節點內存中的緩存數據。如圖12所示,每個節點都有自己的緩存數據,所以如果冰箱扮演着緩存雜貨店的角色,那麼分佈式緩存就是把食物放置在不同的地方——冰箱、櫥櫃和飯盒——當索取的時候,方便拿哪個就拿哪個,而無需特地往商店跑一趟。通常情況下,會使用一致性哈希函數對緩存進行劃分,例如,一個請求節點正在尋找一個特定塊的數據,在分佈式緩存中,它很快就會知道去哪裏找,並確保這些數據是可用的。這種情況下,每個節點都會有一小塊緩存,然後在向另一個節點發送數據請求。因此分佈式緩存的優點之一就是隻需向請求池添加節點即可增加緩存空間,減少對數據庫的訪問負載量。

當然,分佈式緩存也存在缺點,例如單點實效,當該節點出現故障或宕機,那麼該節點保存的數據就會丟失。

圖12 分佈式緩存

分佈式緩存的突出優點是提高運行速度(前提當然是正確實現)。選擇不同的方法也會有不一樣的效果,如果方法正確,即使請求數很多,也不會對速度有所影響。然而,緩存的維護需要額外的存儲空間,這些通常需要購買存儲器實現,但價格都很昂貴。

其中一個非常流行的開源緩存產品:Memcached(即可以在本地緩存上工作也可以在分佈式緩存上工作);然而,這裏還有許多其他選項(包括許多語言——或者是框架——特定選項)。

Memcached用於許多大型Web站點,其非常強大。Memcached基於一個存儲鍵/值對的hashmap,優化數據存儲和實現快速搜索(O(1))。

Facebook採用不同類型的緩存技術來提升他們的網站性能(參考“Facebook caching and performance”)。在語言層面上使用$GLOBALS和APC(在PHP裏提供函數調用),這有助於使中間函數調用更快(大多數語言都使用這些類型庫來提升網站頁面性能)。Facebook使用全局緩存並且通過多臺服務器對緩存進行分佈(參考“Scaling memcached at Facebook”),這就允許他們通過配置用戶文件數據來獲得更好的性能和吞吐量,並且還可以有一箇中心位置更新數據(這是非常重要的,當運行成千上萬臺服務器時,緩存實效和一致性維護都是非常大的挑戰)。

下面讓我們談談當數據不在緩存中時,我們該做什麼……

代理

簡單點講,代理服務器就是硬件/軟件的中間件,接受客戶端請求並且將他們轉發到後端的源服務器上。通常,代理服務器用於過濾請求、記錄請求日誌或有時轉換請求(通過添加/刪除頭結點、加密/解密或壓縮)。

圖13 代理服務器

代理可以優化請求,並且從整個系統的角度來優化請求通信量。一方面,使用代理可以加速數據訪問,可以把相同(或相似的)請求重疊壓縮成一個請求,然後返回單個結果到請求客戶端,這就是壓縮轉發(collapsed forwarding)。

在一個局域網代理中,例如,客戶端無需使用它們自己的IP去連接互聯網,對於相同的內容,局域網將壓縮來自客戶端的請求。它很容易造成混淆,因爲很多代理同樣也是緩存(它是一個非常合乎邏輯放置緩存的地方),但並非所有緩存都扮演代理這一角色。

圖14 使用一個代理服務器壓縮請求

使用代理服務器的另一偉大方式是通過壓縮請求對空間數據進行加密。採用這種策略最大化數據本地化的請求會導致減少請求的延遲。例如這裏有一大串的節點請求B:partB1、partB2等等。我們可以設置代理來識別個人空間的位置請求,把它們壓縮成單一的請求並只返回bigB,大大減少了從數據源處讀取數據次數(如圖15所示)。在高負載的情況下,代理也特別有用,或者當你具有有限的緩存時,它們基本上可以把多個請求批處理成一個。

圖15 使用代理對空間數據請求進行壓縮

如果你正在爲系統尋找代理,這裏提供幾個供你選擇:SquidVarnish,它們都做過非常全面的測試並且被廣泛用在許多大型網站上。這些代理解決方案對客戶端——服務器端通信提供了許多優化方案。在Web服務器層作爲反向代理安裝可以大大提高Web服務性能,減少處理傳入客戶機請求所需的工作量。

索引

使用索引來快速訪問和優化數據是一個衆所周知的策略,最有名的莫過於數據庫索引。

圖16 索引

一個索引就是數據庫表的目錄,表中數據和相應的存儲位置的列表。好比是一篇文章的目錄,可以加快數據表的。例如讓我們來查找一塊數據,B中的第二部分——如何發現它的位置?如果你通過數據類型存儲了一個索引——例如數據A、B、C——它將告訴你數據B的原始位置。然後你只需去查看B並且根據需要閱讀B的數據即可(參考圖16)。

這些索引通常存儲在內存或者是傳入客戶端請求的本地中。Berkeley DBs(BDBs)和樹數據結構常常被用在有序列表中存儲數據,這是訪問索引的理想選擇。

通常,會把許多層索引作爲一個映射,從一個位置移到下一個,以此類推,直到你得到想要的特定塊數據(參照圖17)。

圖17 多層索引

索引也可以對相同的數據創建多個不同的視圖。對大型數據集來說,這種方法是非常好的,無需創建多個額外的數據副本就可以定義不同的過濾和排序,

例如,早期的圖像託管系統實際上是託管圖像書本內容,允許客戶端查詢這些圖像中的內容,輸入一個主題,就可以把所有相關的內容搜索出來。此外,採用同樣的方式,搜索引擎還允許你搜索出HTML內容。在這種情況下,需要很多的服務器來存儲這些文件,查找其中一個頁面可能會很麻煩。首先,反向索引查詢任意個單詞或字元祖都需要可以輕鬆地訪問;再有就是導航到正確的頁面和位置,檢索到正確的圖像結果也是項挑戰。因此,在這種情況下,反向索引會映射到一個位置(例如書B),然後書B可能會有一個包含所有內容、位置和各個部分出現次數的索引。

這種中間級索引只包含了Words、位置和書B的信息。與所有的信息不得不存儲到一個大的反向索引中相比,這種嵌套的索引架構允許每個索引佔用較少的空間。在大型系統中,這是非常關鍵的,即使採用壓縮,這些索引也需要佔用相當昂貴的存儲空間。

例如,讓我們假設這個世界上有——100,000,000本書(參考Inside Google Books官方博客)——每本書只有10頁,每頁只有250個單詞,這也就意味着有2500億個單詞。如果每個單詞只有5個字節,每個字節佔用8 bits(或1個byte,甚至有些字符佔用2 bytes),所以5 bytes/單詞,那麼一個索引所包含的單詞就有可能超過一個TB的存儲。此外,索引還有可能包含其他信息,例如元祖單詞、數據位置等。

能夠快速、輕鬆地找到數據是非常重要的,而使用索引就可以簡單高效的實現。

負載均衡器

分佈式系統的另一個關鍵部分是負載均衡。負載均衡器幾乎是每個架構的主要組成部分,他們的角色是負責把網絡請求分散到一個服務器集羣中的可用服務器上去,通過管理進入的Web數據流量和增加有效的網絡帶寬,從而使網絡訪問者獲得儘可能最佳的聯網體驗的硬件設備。

圖18 負載均衡器

這裏有許多種算法可用於爲請求提供服務,包括隨機選擇一個節點、循環或者甚至是基於某個特定的標準來選擇節點,例如內存或CPU利用率。負載均衡器即可以以硬件的方式表現出來,也可以以軟件的方式。HAProxy是一個開源的負載均衡器,並且得到了非常廣泛的使用。

在一個分佈式系統中,負載均衡器通常處於系統的前端位置,所有傳入的請求會相應地被路由。在一個複雜的分佈式系統中,一個請求被路由到多個負載均衡器上並不常見,如圖19所示:

圖19 多個負載平衡器

和代理一樣,有些負載均衡器也可以基於請求的類型路由到不同的服務器集羣上。(技術上來講,這也被稱爲反向代理。)

負載均衡器所面臨的挑戰之一是管理用戶特有的會話(user-session-specific)數據。在一個電子商務網站上,當你只有一個客戶端時,是很容易讓用戶把商品放入購物車並且繼續訪問(這是非常重要的,因爲商品很有可能在繼續出售,而用戶退出時,商品仍然留在購物車裏)。然而,如果用戶本次會話路由了一個節點,那麼當他下次訪問的時候會路由一個不同的節點,這樣,就很有可能使購物車裏的商品不一致,因爲新的節點有可能會丟失該用戶購物車裏原先的商品(當你先放6包Mountain Dew 在購物車裏,等到再次登錄後發現購物車爲空了)。解決這個問題的方法之一是使用sticky sessions,來使用戶一直被路由到相同的節點,但它很難利用到可靠性功能,像自動故障轉移(automatic failover)。這種情況下,用戶的購物車裏將會一直有商品,但如果sticky node變的不可用,這就需要特殊情況來處理並且假設購物車裏的商品將不再有效(儘管希望這種假設不會被內置於應用程序裏)。當然解決這個問題還有許多其他方法,例如本文提到的服務以及不包括(瀏覽器緩存、cookies和URL重寫)。

在一個大型系統裏會有各種不同類型的調度和負載均衡算法,包括簡單點的像隨機選擇或循環以及更復雜的機制,例如利用率和容量。所有的這些算法都可以分佈流量和請求,並且提供有用的可靠性工具,像自動故障轉移或者自動清除一個壞的節點(例如當它無法響應時)。然而,這種高級功能會把問題診斷的複雜。 例如,當遇到高負載情況時,負載均衡器將會移除變慢或超時的節點(因爲請求太多,刪除節點後會把請求分配到其他節點上),這無疑會加劇其他節點的工作量,即負載加重。這種情況下,大量的監測變的非常重要,因爲整個系統流量和吞吐量看起來可能會減少(因爲節點服務更少的請求),但可能會累壞個別節點(處理更多的請求)。

負載均衡器也是擴展系統容量的一種簡單方式,像文中提到的其他技術,在分佈式系統架構中發揮着非常重要的作用。負載均衡器也提供一些重要功能來測試節點的健康狀況,例如,如果該節點響應遲鈍或過載,它可能就會被刪除,然後利用系統中不同的節點冗餘。

隊列

到目前爲止,我們已經討論了許多方法來加快數據讀取速度,但擴展數據層的另一個重要組成部分是如何高效的寫入數據。在簡單的系統中,進程負載等都比較少,並且數據庫比較小,毋庸置疑,寫的速度肯定不會慢。然而,在大型複雜的系統裏,這個速度就很難把握了,可能會花費很長的時間。例如,數據有可能要寫到幾個不同的地方,不同的服務器或索引、或者系統正處於高負載情況下。在這種情況,該在哪裏進行寫?或者其他任何任務都有可能花費很長時間,要想在系統實現性能和可用性需要構建異步。處理這種異步的一種常見的方式就是採用隊列。

圖20 同步請求

想象在一個系統裏,每個客戶機都要把請求發送至遠程服務器,那麼服務器應該儘可能快的接收並完成任務,然後把結果返回到相應的客戶端。在小型系統中,一臺服務器(或邏輯服務器)傳入客戶端數據會與客戶端發出時一樣快,這樣就比較完美了。然而,當服務器接收到的請求多餘它的處理能力時,那麼每個客戶端必須排隊等待服務器處理其他客戶端請求,直到輪到你了,服務器纔會處理你的請求,直到最終完成。這就是一個同步請求的例子,如圖20所示。

這種同步行爲會嚴重降低客戶端性能,客戶端被迫等待,而通過添加額外的服務器來滿足負載並不能解決問題,即使採用最有效的負載均衡也很難保證分配公平,在大客戶端下。進一步講,如果處理請求的服務器不可用或者癱瘓,那麼客戶端上游也將失敗。有效的解決這個問題需要抽象客戶端請求以及服務請求的實際工作。

圖21 使用隊列來管理請求

隊列就像聽起來那樣簡單,一個任務進來,就添加到隊列裏去,然後the workers挑選有能力處理的下一個任務。(參考圖21)這些任務有可能僅是簡單的寫入,也有可能是複雜的,如把文檔生成圖像預覽。當一個客戶端把任務請求提交的隊列中時,他們不需要被迫等待結果,相反,他們只需確認請求是否被正確接收。

隊列使客戶端能夠以異步的方式工作,對客戶端請求和響應提供戰略抽象。另一方面,在一個同步系統中,請求和迴應是沒有分化的,因此他們不能被分開管理。在異步系統中,客戶端發出請求任務,服務器對收到的消息進行響應並確認任務被接收,然後客戶端可以定期檢查任務狀態,一旦任務完成,即可看到結果。當客戶端在等待異步請求是否完成時,它還可以自由執行其他任務,甚至是向其他服務器發出異步請求。下面要介紹的是消息和隊列在分佈式系統中的槓桿作用。

隊列也對服務中斷或失敗提供一種保護機制。例如,它很容易創建一個高度健壯的隊列,當服務器瞬間失敗時,該隊列可以把剛剛失敗的請求重新發送至服務器。相比直接暴露客戶端來間斷服務供應,使用隊列來保證服務質量更可取,要求必須有複雜且矛盾性的客戶端差錯處理。

隊列是管理分佈式通信與任何大規模分佈式系統中各個部分之間的基礎,並且有許多實現方式。這裏有許多開源的隊列,如RabbitMQActiveMQBeanstalkD,但也有一些當做服務使用,如Zookeeper,甚至是用來數據存儲,像Redis

總結

設計出一個高效的分佈式系統是令人興奮的事情,尤其是擁有快速的數據訪問速度。本文只是討論了幾個實際的例子,希望對你能有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章