可伸縮Web體系結構和分佈式系統


陳老師推薦語:“這篇文章會給你一個大概的分佈式架構是怎麼解決系統擴展性問題的粗略方法。” 本文是直接翻譯自文章Scalable Web Architecture and Distributed Systems

對於一些非常大型的Web站點來說,開源軟件已經成爲了一個基礎的構建塊。隨着那些Web系統逐漸增長,在他們的架構中的最佳實踐和指導性原則逐漸出現。本文主要關注一些設計大型WebSites的時候需要考慮的關鍵點,以及部分用於達到這些目標的構建塊

儘管這章中的部分材料對於其他分佈式系統也是合適的,但這章重點在分佈式Web系統上。

1.1 分佈式Web系統設計原則

構建和操作一個可伸縮的web站點或者應用究竟意味着什麼?本質上來講,其實就是讓用戶通過Internet連接到遠程的資源,可伸縮則意味着資源或者對這些資源的訪問是分佈在多臺服務器之間的。

跟生活中很多事情一樣,在構建一個Web服務之前花時間去規劃有助於日後的長跑。在創建一些更小型的Web站點時,去了解大型Web站點背後的一些考慮點和權衡點(tradeoffs)可以催生出更加明智的設計。下面是一些影響設計大規模Web系統的關鍵原則:

可用性 Availability

站點的持續運行時間對於大多數公司的口碑和服務能力都至關重要。對於一些零售站點,即使幾分鐘的不可用都可能導致成千上百萬美金的收入損失。因此,將系統設計成持續可用的,既是一個基本的業務需求也是一個技術需求。分佈式系統中的高可用性需要對關鍵部件冗餘的仔細考慮,在部分系統失敗事件出現時的迅速恢復,以及當問題出現時的優雅降級。

性能 Performance

對於大部分站點來說,站點的性能已經成爲一個非常重要的考慮點。網站的速度影響使用率和用戶滿意度,一個可以直接關聯到收入和用戶留存率的因子,也會影響到搜索引擎排名。因此,對於創建一個系統來說,優化的快速響應和低延遲是非常關鍵的。

可靠性 Reliability

系統必須是可靠的,只有這樣對數據的一個請求才能一致地返回相同數據。在數據改變或者更新的事件中,相同的請求應該返回新數據。用戶需要知道是否數據已經寫入到系統中,或者存儲起來了,將持續存在,並且對於後面的獲取可以保證已經就位。

可伸縮性 Scalability

當考慮任何大型分佈式系統的時候,大小是規模中需要考慮的一個屬性。同樣重要的是增加處理更大負載所需的努力,通常稱爲系統的可擴展性。可擴展性指系統的許多不同參數:它可以處理多少額外流量,添加更多存儲容量有多簡單,能夠處理多少事務。

可管理性 Manageablility

設計一個易於操作的系統是另一個重要的考慮點。系統的可管理性等同於操作的可擴展性:維護和更新。可管理性的考慮因素是當問題出現的時候,易於診斷和理解問題,易於做更新和修改,以及系統操作的簡單性。(即,系統是否經常運行而沒有錯誤或者異常?)

成本 Cost

成本是另一個非常重要的因素。這個顯然包括軟件和硬件的成本,但是考慮部署和維護系統的其他方面也很重要。系統構建所需要的開發者時間,運行系統所需的操作工作量,甚至所需的培訓工作量也要考慮進去。成本是總擁有成本。

原則總結

以上的每個原則提供了一個在設計一個分佈式Web架構時的決策依據。然而,他們可能本身相互衝突,這樣就會使得達到一個目標是以犧牲另一個目標爲代價的。一個基本的例子:當選擇簡單地堆加服務器來解決容量(Scalability)問題的時候,代價是可管理性(Manageablility 必須在額外增加的Server上操作)和成本(Cost 服務器的價格)。

當設計任何種類的Web應用時,即使承認在一個設計中可能犧牲了其中的一個或者多個原則,考慮以上這些關鍵原則仍然是非常重要的。

1.2 基礎

當考慮系統架構的時候,只有很少的東西需要考慮:正確的部分是什麼?這些部分如何組織在一起?以及什麼是正確的權衡?在真正需要之前在規模上進行投資並不是一個明智的商業決策。然而,一些前瞻性的設計可以在未來節約大量的時間和資源。

這個小節重點講幾乎所有的大型web應用都非常重要的部分核心因素:服務,冗餘,分區,以及處理失敗。這些因素中的每個都包含了選擇和妥協,尤其是在上一節的原則的情境下。爲了更詳細地解釋這些,最好的方式是用一個例子來開始。

實例 圖像託管應用程序

可能你曾經上傳了一張照片上線。對於託管和提供大量的圖片的大站點來說,構建一個成本節約,高可用,以及有低延遲(快速獲取)的架構有非常大的挑戰。

假設一個系統,正如Flickr或Picasa這種,用戶可以上傳他們的圖片到一箇中央服務器並且圖片可以通過web鏈接或者API訪問到。爲了簡單起見,假定這個應用有兩個關鍵的部分:上傳(寫)圖片到服務器的能力 和 查詢圖片的能力。 雖然我們希望上傳非常高效,但是其實我們更關注於查詢圖像的時候又多快能查到,例如通過web頁面或者其他應用程序請求查詢到。這個對於一個web服務器或者CDN邊緣服務器能提供的功能非常相似。一個CDN服務器通常將內容存儲在很多位置上,因此內容在物理上或者地理上更靠近用戶,這樣可以有更快的表現。

這個系統的其他重要方面:

  • 對於能存儲的圖片的數量沒有限制,因此存儲可伸縮性需要被考慮
  • 對於圖像下載或請求訪問需要有低延遲
  • 如果一個用戶上傳了圖像,這個圖像應該永遠在那裏(圖像的數據可靠性)
  • 系統應該易於維護(可管理性)
  • 由於圖像託管的利潤率不高,因此係統需要有成本效益

圖1是一個功能的簡單框圖。
簡單的功能框圖

在這個圖像託管應用實例中,系統必須要可察覺的快,數據存儲必須可靠並且所有的屬性都可伸縮。構建一個小版本的應用沒有什麼挑戰,並且可以簡單地託管在一臺服務器上;然而,對本文來說並不感興趣。假設我們想要構建一個可以成長爲跟Flickr一樣大的系統。

服務

當考慮可伸縮性系統設計時,有助於解耦功能,並且將系統的各個部分當做自己的服務並定義清楚接口。實際上,這種方式設計的系統被稱爲SOA(面向服務的體系結構)。對於這種類型的系統,每個服務有獨特的功能上下文。對於跟功能上下文之外的任何東西交互是通過一個抽象接口,通常就是另一個服務的對外的公共接口。

將系統拆解爲一系列互補的服務將這些部分的操作彼此分離。這個抽取過程有助於在服務、其底層的運行環境,以及服務的使用者之間建立清晰的關係。創建這些清晰的描述可幫助隔離問題,但也允許每個部分彼此獨立地擴展。這種系統的面向服務的設計很類似於程序中的面向對象的設計。

在我們的實例中,所有上傳圖片和獲取圖片的請求都是由相同的server處理的;然而,當系統需要擴展時,將這兩個功能拆分成不同的服務很有意義。

假設服務正在大量使用,這種場景下將可以簡單地看出來寫的時間影響讀圖像所需要的時間(因爲這兩個功能競爭共享資源)。根據架構這種影響可能很大。即使上傳和下載的速度一樣(這個假設對於大多數IP網絡不正確,因爲大部分都是下載速度:上傳速度=3:1來設計的),讀文件通常從緩存中讀取,寫將最終寫入到磁盤中(可能在最終一致性場景中需要寫幾次)。即使全都是內存中操作或者從ssd讀取,數據庫的寫總是比讀慢的。

這個設計存在的另一個潛在的問題是,像Apache或者lightppd這種web服務器通常對同時可以維持的連接數有一個上限(默認值在500左右,可以調更高),並且在高流量的情況下,寫入可以快速消耗所有這些。因爲讀可以是異步的,或者利用其它新能優化手段(如gzip壓縮或者分塊傳輸編碼),web服務器可以更快地切換服務讀取,並在客戶端之間切換,每秒快速提供比最大連接數更多的請求(在Apache最大連接設置爲500的前提下,服務幾千每秒的讀請求是很常見的)。寫另一方面在上傳期間會維護一個打開的連接,因此,在大多數家庭網絡中上傳1MB文件可能消耗多於1秒,因此,web服務器僅僅能同時處理500個這種寫。
圖2讀寫服務分離
規劃這種瓶頸可以很好地拆分讀和寫圖像到各自的服務,如圖2所示。這允許我們獨立地擴展他們中的每一個(因爲比起寫入我們總是做更多讀操作),也有助於澄清在每個時刻發生的事情(排查的時候更加清楚問題)。最後,這將分離將來的關注點,也就是說更容易進行故障排除和擴容,例如讀取慢這種問題。

這種方法的優勢是我們能夠獨立於另一個問題來解決當前問題,也就是說不用在相同的上下文環境中同時考慮寫和獲取新圖像。這兩種服務仍然利用全局圖像庫,但他們可以使用適合服務的方法自由地優化自己的性能(例如,請求排隊,或者緩存熱點圖片——更多內容如下所示)。從維護和成本的角度來看,每個服務可以根據需要獨立擴展,這很好,因爲如果它們被組合和混合,則可能無意中影響另一個服務的性能,就像上面討論的場景一樣。

當然,當您有兩個不同的端點時,上面的示例可以很好地工作(實際上這與幾個雲存儲提供商的實現和CDN非常相似)。有很多方法可以解決這類型的瓶頸,每種方法都有不同的權衡。

例如,Flickr是通過分配用戶到不同的分片來解決這個讀寫問題的,這樣每個分片(shard)僅僅能處理一定數量的用戶,並且隨着用戶增加,把更多的分片添加到集羣中。在第一個例子中,基於實際的使用量(整個系統的讀和寫)去擴容硬件更簡單,而Flickr根據用戶羣去擴容(強制假設每個用戶的使用量是相等的,因此可以有額外的容量)。在前者中,其中一個服務的中斷或者問題會降低整個系統的功能(例如,寫服務掛了,沒有一個人能寫文件),而Flickr的一個分區中斷了將僅僅影響那些用戶。在第一個例子中,對跨整個數據集執行操作非常簡單,例如,更新寫服務以包含新的metadata或者搜索所有圖片的metadata。而對於Flickr的架構,每個分區都需要更新或者搜索(或者需要創建一個搜索服務去採集metadata,實際上就是這麼幹的)。

當來到了這些系統中,根本沒有正確的答案。但是回到本文開頭的那些原則上去會很有幫助。確定系統的需求(大量的讀或者寫或者兩者都有,併發程度,跨數據集的查詢,範圍,排序等),對不同的替代方案進行基準測試,瞭解系統將如何失敗,並對失敗發生時制定可靠的計劃。

  • 小結:
    • 服務間按照功能進行拆分,如讀寫分離等,好處很多
    • 解決性能問題可以有不同的方法,如Flickr採用的是將用戶分片來解決的,跟拆分服務相比,對用戶分片各有利弊,需要權衡
    • 沒有正確答案,需要回到前面的原則上考慮系統的實際需求來確定方案,並進行基準測試、預測可能的失敗、準備失敗方案

冗餘

爲了優雅地處理失敗,一個web體系結構必須有服務和數據的冗餘。例如,如果一個文件只在一個服務器上存了僅一份副本,那麼那臺服務器的數據丟失意味着那個文件也丟失了。丟數據是很嚴重的問題,通常處理的方式是創建多個或者冗餘的備份。

這個原則同樣也適用於服務。如果一個應用有一個核心的功能,確保多個備份或者多個版本同時運行可以在防止單個節點發生故障。

在一個系統中創建冗餘可以消除單點故障,並且在危機事件中根據需要提供一個備份或者備用功能。例如,生產中一個服務有兩個實例在運行,其中一個故障或者降級了,系統可以故障轉移(failover)到另一個正常的副本上。故障轉移可以自動或者手工干預完成。

服務冗餘的另一個關鍵部分是創建無共享架構(shared-nothing)。在這個架構下,每個節點可以獨立於另一個來操作,並且沒有什麼中央大腦來管理狀態或者協調其他節點的活動。因爲新節點不需要特殊條件或者知識就可以直接加入進來,這大大提高了可伸縮性。但是,最重要的是,系統中沒有了單點故障,因此對故障更有彈性。

例如,在我們的圖像服務器應用中,所有圖像數據在其他地方的某塊硬件上有冗餘備份(理想情況下,在一個像一個災難性事件如數據中心的地震或者火災中,在不同的地理位置下)。對於所有潛在的服務請求,訪問圖像的服務也是冗餘的。如下圖3所示。負載均衡是一個非常好的方式來實現這種冗餘,但是我們後面還會有更好的方式。
圖3 帶有服務冗餘和數據冗餘的設計

分區

  • 可能存在無法放在單個服務器上的很大的數據集。
  • 也可能有這種情況:一個操作需要太多的計算資源,逐漸讓性能變得很差到必須擴容的程度。

在以上任一種情況下,你有兩個選擇:垂直伸縮或者水平伸縮。

垂直伸縮

垂直伸縮意味着向單個服務器增加更多的資源。

  • 對於無法存放的大數據集的場景,意味着添加更多或者更大的磁盤來使得單個服務器可以容納整個數據集。
  • 在高計算資源的操作的場景中,意味着將計算移動到一個有更快的CPU或者更多內存的更大的服務器上。

在任意一種情況中,垂直伸縮是通過靠自己讓單個資源足夠處理更多來完成的。

水平伸縮

水平伸縮是增加更多的節點。

  • 對於大數據集場景中,這意味額外增加一臺server去存放這個數據集。
  • 對於高計算資源的操作的場景中,意味着拆分操作或者加載到一些額外的節點上。

爲了最大限度利用水平伸縮,應該讓其成爲系統架構設計中的一個固有的設計原則,否則,後面去修改和分離上下文來做水平伸縮是非常麻煩的。在涉及水平伸縮時,一種比較常見的技術是將服務分解爲分區(partitions)或分片(shards)。分區可以是分佈式的,這樣每個功能邏輯集合是隔離的。這可以通過地理邊界或者其他標準來完成,如非付費用戶用戶和付費用戶。這些方案的優點是他們給服務或者數據存儲擴容。

圖4
在我們的圖像服務器的例子中,可以直接將用於存儲圖像的單個文件服務器用多個文件服務器替換掉,每個新的文件服務器都包含其自己唯一的圖像集合,如圖4所示。這樣的架構允許系統將圖片寫入到每個服務器中,並且當磁盤變滿時,添加額外的服務器。這個設計需要一個可以將圖片的文件名關聯到包含它的服務器的命名方案。一個圖片的名字可以由跨所有服務器映射的一致性hash方案形成。或者作爲一個可選方案,每個圖片可以被分配一個增量ID,這樣當一個客戶端請求一個圖片的時候,獲取圖片的服務僅僅需要維護映射到每個服務器上的ID的範圍,正如索引一樣。

當然,跨多個服務器分佈數據或者功能存在挑戰。其中一個關鍵問題就是數據局部性;在分佈式系統中,數據離操作或者計算點越近,系統的性能越好。因此,數據跨多個服務器存放是一個潛在的問題。因爲在需要數據的任何時候,他可能不是本地的,這種情況下強制server去執行一個高成本的跨網絡獲取所需要的信息。

另一個潛在的問題就是不一致性。當從共享資源(可能是另一個服務或數據存儲)讀取和寫入不同的服務時,存在競爭條件的可能性 - 其中一些數據應該被更新,但是在更新之前發生讀取 - 並且在那些情況下數據不一致。例如,在圖片託管服務場景中,在下述場景中就可能會出現競態:當一個客戶發送一個請求去更新狗子的圖片文件的標題,從Dog改成Gizmo,但是此時另一個客戶端正在讀取這個圖片文件。在這種情況下,不知道到底第二個client讀取到的是Dog還是Gizmo標題。

將數據分區當然有很多障礙,但分區允許將每個問題按數據,負載,使用模式等分成可管理的塊。這有助於提高可擴展性和可管理性,但並非沒有風險。有很多方法可以降低風險並處理故障;但是,爲了簡潔起見,本章不涉及它們。

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

在介紹了設計分佈式系統時的一些核心考慮因素之後,我們現在談談困難的部分:擴展對數據的訪問

大多數簡單的Web應用程序,例如LAMP堆棧應用程序,如圖5所示。
圖5
隨着它們的發展,存在兩個主要挑戰:擴展對應用服務器和數據庫的訪問。在高度可擴展的應用程序設計中,應用程序(或Web)服務器通常被最小化,並且通常體現爲無共享體系結構。這使得系統的app服務器層可以水平擴展。由於這種設計,繁重的工作被壓縮到數據庫服務器和支持服務; 在這一層,真正的擴展和性能挑戰發揮作用。

本章的其餘部分致力於通過提供對數據的快速訪問,使這些類型的服務快速且可擴展的一些更常見的策略和方法。

圖6過渡簡化的Web應用
大多數系統都可以簡化爲圖6。 這是一個很好的起點。 如果您有大量數據,需要快速方便地訪問,就好像把糖果放在桌面的頂部抽屜中那麼好拿。雖然過於簡化但前文已述及的兩個問題依然存在:存儲的可擴展性和數據的快速訪問。

爲了本節要講的內容,假設有幾TB的數據,並且希望允許用戶隨機訪問該數據的一小部分。(參見圖7)這類似於在圖像應用程序示例中在文件服務器上的某處定位圖像文件。
圖7
這特別具有挑戰性,因爲將TB的數據加載到存儲器中開銷非常大,這直接轉換爲磁盤IO。從磁盤讀取速度比從存儲器讀取速度慢幾倍。這種速度差異實際上增加了大數據集;實際上,對於順序讀取,內存訪問速度比從磁盤讀取速度快6倍,對於隨機讀取速度內存訪問速度要快10萬倍。而且,即使有唯一的ID,解決知道在哪裏找到這麼少的數據的問題,這也還是一項艱鉅的任務。這就等於你不能看但是要從你的糖果儲存中獲取最後一個Jolly Rancher(一個特定的糖果品牌)。

值得慶幸的是,您可以使用許多選項來簡化這一過程;其中四個比較重要的是緩存,代理,索引和負載均衡器。本節的其餘部分討論瞭如何使用這些概念中的每一個來更快地進行數據訪問。

緩存(Caches)

高速緩存利用了局部性的原理:最近請求的數據很可能被再次請求。它們幾乎用於每個計算層:硬件,操作系統,Web瀏覽器,Web應用程序等。 緩存就像短期內存:它具有有限的空間,但通常比原始數據源更快,幷包含最近訪問的項目。 高速緩存可以存在於體系結構的所有級別,但通常位於最靠近前端的級別,在那裏它們被實現爲快速返回數據而不會對下游級別產生負擔。

在我們的API示例中,如何使用緩存來加快數據訪問速度? 在這種情況下,有幾個地方可以插入緩存。 一種選擇是在請求層節點上插入緩存,如圖8所示。
圖8
直接在請求層節點上放置緩存可以本地存儲響應數據。 每次向服務發出請求時,節點將快速返回本地緩存數據(如果存在)。 如果它不在緩存中,請求節點將從磁盤查詢數據。 一個請求層節點上的緩存也可以位於內存(非常快)和節點的本地磁盤上(比進入網絡存儲更快)。
圖9
將此擴展到多個節點時會發生什麼? 如圖9所示,請求層擴展成多個節點,每個節點仍然有自己的緩存。 但是,如果位於節點前面的負載均衡器在節點之間隨機分配請求,則相同的請求將轉到不同的節點,從而增加了緩存未命中。解決這個問題的兩個方法:全局緩存和分佈式緩存。

  • 全局緩存(Global Cache)
    全局緩存就像它聽起來一樣:所有節點都使用相同的單個緩存空間。 這涉及添加某種服務器或某種文件存儲,比原始存儲更快,並且可由所有請求層節點訪問。 每個請求節點以與本地節點相同的方式查詢緩存。 這種緩存方案可能會變得有點複雜,因爲隨着客戶端和請求數量的增加,很容易壓倒單個緩存,但在某些體系結構中非常有效(特別是那些有使這種全局緩存非常快的專用硬件上, 或者有一個需要緩存的固定數據集)。
    圖中描述了兩種常見的全局緩存形式。 在圖10中,當在緩存沒命中時,緩存本身負責從底層存儲中檢索沒命中的數據。 在圖11中,請求節點負責去從底層的存儲中找到緩存中找不到的任何數據。這裏只是一個簡單的描述,涉及到緩存的讀取和更新策略其實有多種方式,可參考本博客中的另一篇文章專門描述。
    圖10
    圖11
    利用全局緩存的大多數應用程序傾向於使用第一種類型,其中緩存本身管理淘汰和獲取數據以防止來自客戶端的相同數據的大量請求。但是,在某些情況下,第二種實現更有意義。例如,如果緩存用於非常大的文件,則低緩存命中百分比將導致緩存緩衝區因緩存未命中而變得不堪重負; 在這種情況下,它有助於在緩存中擁有大部分的總數據集(或熱數據集)。 另一個例子是一種架構,其中存儲在緩存中的文件是靜態的,不應該被淘汰。 (這可能是因爲圍繞數據延遲的應用程序要求 - 對於大型數據集,某些數據片段可能需要非常快 - 應用程序邏輯比緩存更好地理解淘汰策略或熱點。)

  • 分佈式緩存(Distributed Cache)
    在分佈式緩存(圖12)中,每個節點都擁有緩存數據的一部分,因此如果把冰箱比作雜貨店的緩存,分佈式緩存就像將食物放在幾個位置如冰箱,櫥櫃, 和午餐盒等這些方便的地方以方便取吃的,但是又不用跑到雜貨店去。通常,使用一致性hash函數來劃分cache,使得如果請求節點正在尋找某個數據片段,則它可以快速知道在分佈式cache中的哪個位置以確定該數據是否可用。在這種情況下,每個節點都有一小部分緩存,然後在轉到源數據去訪問之前向另一個節點發送數據請求。因此,分佈式Cache的一個優點是隻需將請求節點添加到請求池就可以增加Cache空間。
    圖12
    分佈式緩存的缺點是修復丟失的節點。 一些分佈式緩存通過在不同節點上存儲多個數據副本來解決這個問題。 但是,在從請求層添加或刪除節點時,可以想象這種邏輯如何快速複雜化。 雖然即使節點消失並且部分緩存丟失,但請求還是會去讀取源數據,因此它不一定是災難性的!

關於緩存的好處在於它們通常會使事情變得更快(當然,正確實現的前提下)。緩存的方法可以讓系統更快地處理更多請求。 然而,所有這些緩存都需要以維持額外的昂貴的內存存儲空間爲代價; 畢竟沒有什麼是免費的。Cache非常適合於使事情變得更快,更重要的是在高負載條件下提供完整的系統功能,否則會出現服務降級。

redis和memcached都是非常流行的開源cache,可以作爲本地cache或者分佈式cache都可以。當然也可以有很多其他的選擇方案。
Memcached用於許多大型網站,非常強大。它是一個內存中的KV存儲,針對任意數據存儲和快速查找進行了優化(O(1))。

Facebook使用幾種不同類型的緩存來獲取其網站性能。 他們在語言級別使用$ GLOBALS和APC緩存(以函數調用爲代價在PHP中提供),這有助於使中間函數調用和結果更快。 (大多數語言都有這些類型的庫來提高網頁性能,它們幾乎總是被使用。)Facebook然後使用分佈在許多服務器上的全局緩存(參見“在Facebook上擴展memcached”),這樣一個函數調用訪問 緩存可以爲存儲在不同Memcached服務器上的數據並行發出許多請求。 這使他們能夠爲其用戶配置文件數據獲得更高的性能和吞吐量,並且有一個更新數據的中心位置(這很重要,因爲當您運行數千臺服務器時,緩存失效和維護一致性可能具有挑戰性)。

小結:

  • 緩存解決的問題是?或者緩存的作用到底是什麼? 就是加快數據的訪問速率,從而實現更大的吞吐和更快的訪問
  • 緩存分爲全局緩存和分佈式緩存
    • 分佈式緩存不同於最開始提到的那種本地緩存
      • 分佈式緩存雖然每個節點上都有緩存,但是並不是獨立的,而是所有的節點上的cache組成了一個大家共同使用的cache池,每個節點如何找到自己需要的cache是通過一致性hash算法計算得到的
      • 最開始的那種本地緩存是每個節點都有自己獨立的緩存,兩者區別很大
    • 全局緩存按照緩存未命中時更新cache的主體不同而分兩類,實際根據更新緩存主體和寫入策略不同有更多的分類,可以參考另一篇介紹緩存的博客;
  • facebook用多級緩存提高性能,編程語言級別用了緩存(暫存下一些中間結果或者部分結果),然後用多臺memcache服務器組成了一個全局cache集羣
    • 實際讀了文中facebook的應用memcache的文章,發現2008年左右的memcache問題很多,諸如每連接都分配了固定的內存等
    • 加上當前主流cache基於redis,因此,此處不再深究;

現在讓我們談談cache以外的事情…

代理(Proxies)

一般地,代理服務器就是一箇中間件硬件/軟件,它接收來自客戶端的請求並將它們中繼到後端源服務器。 通常,代理用於過濾請求,記錄請求或轉換請求(通過添加/刪除標頭,加密/解密或壓縮)。
圖13 代理服務器
在協調來自多個服務器的請求時,代理也非常有用,提供了從整個系統的角度優化請求流量的機會。 一種可以讓代理加速數據訪問的方法是將相同(或類似)請求一起摺疊爲一個請求,然後將單個結果返回給請求客戶端。這稱爲摺疊轉發。

想象一下,在幾個節點上存在對相同數據的請求(讓我們稱之爲littleB),並且該數據不在緩存中。 如果該請求是路由通過proxy的,那麼所有這些請求都可以摺疊成一個,這意味着我們只讀一次磁盤上的littleB。 (參見圖14)。此設計存在一些成本,因爲每個請求的延遲會略增高,而某些請求爲了與類似的請求分組可能會被稍微延遲。但它會提高高負載情況下的性能,特別是在反覆請求相同數據時。這類似於緩存,但它不是像緩存那樣存儲數據/文檔,而是優化對這些文檔的請求或調用,並充當這些客戶端的代理。(一句話解釋區別就是,cache緩存數據,代理合並相同的請求)
圖14

例如,在LAN代理中,客戶端不需要自己的IP來連接到Internet,LAN將摺疊來自客戶端的相同內容的呼叫。這裏很容易混淆,因爲許多代理也是緩存(因爲它是放置緩存的一個非常合理的位置),但並非所有緩存都充當代理。

使用代理的另一個好方法是不僅要摺疊對相同數據的請求,還要摺疊對原始存儲中空間上靠近的數據的請求(連續地在磁盤上)。採用這種策略可以最大化請求的數據局部性,從而減少請求延遲。例如,假設一堆節點請求B的部分:partB1,partB2等。我們可以設置我們的代理來識別各個請求的空間局部性,將它們摺疊成單個請求並僅返回bigB,從而大大減少了 從數據源讀取。 (參見圖15。)當您隨機訪問TB數據時,這會對請求時間產生很大影響! 代理在高負載情況下或緩存有限時特別有用,因爲它們實際上可以將多個請求合併爲一個。
圖15
值得注意的是,您可以將代理和緩存一起使用,但通常最好將緩存放在代理服務器前面,原因與最好讓速度較快的跑步者在擁擠的馬拉松比賽中首先啓動一樣。這是因爲緩存是從內存中提供數據來服務的,速度非常快,並且不關心對同一結果的多個請求。但是如果緩存位於代理服務器的另一端,那麼緩存之前的每個請求都會有額外的延遲,這可能會影響性能。(意思是緩存可以直接返回,你加了一個proxy在前面反倒阻礙了原本非常快的cache的作用。總的來說就是cache應該儘可能靠近前端)

如果您正在考慮爲系統添加代理,可以考慮許多選項:Squid和Varnish都經過實戰檢驗,並廣泛用於許多生產網站。這些代理解決方案提供了許多優化,以充分利用客戶端-服務器通信。在Web服務器層安裝其中一個作爲反向代理(在下面的負載平衡器部分中解釋)可以顯著提高Web服務器性能,減少處理傳入客戶端請求所需的工作量。

索引(Indexes)

使用索引快速訪問數據是優化數據訪問性能的衆所周知的策略; 可能最知名是運用在數據庫中。索引使得更高層的存儲開銷增加和寫入速度變慢(因爲您必須同時寫入數據並更新索引),換來的是獲得更快的讀取。運用索引的技巧是您必須仔細考慮用戶將如何訪問您的數據。

就傳統的關係數據存儲而言,您也可以將此概念應用於更大的數據集。在數據集大小爲TB但具有非常小的有效載荷(例如,1KB)的情況下,索引是優化數據訪問的必要條件。在這樣大的一個數據集中找到那麼小的一個載荷是一個挑戰,因爲你不可能在合理的時間內迭代那麼多的數據。更重要的是,很可能這樣大的一個數據集可能分佈很多物理設備,這意味着你需要一些方法來找到要找的數據的正確物理位置。索引就是做這個的最好的方式。

索引像一個內容表一樣,引導你到數據所在的位置。例如,假定你在找一塊數據,B的part2,你怎麼知道去哪裏可以找到呢?如果你有一個按照數據類型排序的索引,正如A,B,C這種順序,索引將告訴你數據B在數據源中的位置。那麼你僅僅只需要定位到那個位置並且讀取你想要的B的那一部分。(如圖16所示)。
圖16
這些索引通常存儲在內存中,或者存儲在傳入客戶端請求的本地。Berkeley DB(BDB)和樹狀數據結構通常用於存儲有序列表中的數據,非常適合使用索引進行訪問。

通常有多層索引,這些索引像一個地圖一樣引導你從一個地方到另一個地方,直到你獲取到你想要找的數據。圖17所示。
圖17
索引也能用於創建相同數據的多個不同的視圖。對於大數據集,這是一個定義不同過濾器和排序的很好的方法,並且不用重新排序來創建一些數據的額外的副本。

例如,想象一下,之前的圖像託管系統實際上是託管圖書頁面的圖像,正如搜索引擎允許你找html內容一樣,服務要允許客戶端查詢圖書頁面圖像上的文字,在所有的書本內容中搜索一個主題。在這種情況下,所有的那些書本圖像需要很多很多的服務器來存儲文件,找到一頁來渲染給用戶有點牽扯。首先,需要易於訪問查詢任意單詞和單詞元組的反向索引; 然後有一個挑戰是導航到該書中的確切頁面和位置,並檢索結果的正確圖像。在這個例子中,倒排索引將映射到一個位置(例如B書),因此,B書中包含一個涉及所有單詞,位置以及在每個部分出現的次數的索引。

表示上圖中的Index1的一個倒排索引可能用如下的方式查找——每個單詞或單詞組提供一個哪些書包含他們的索引。

Word(s) Book(s)
being awesome Book B, Book C, Book D
always Book C, Book F
believe Book B

中間索引看起來很相似,但只包含書B的單詞,位置和信息。這種嵌套索引架構允許每個索引佔用的空間比所有信息都必須存儲到一個大的倒排索引中的空間要小。

這對於大規模系統來說至關重要,因爲即使是壓縮,這些索引也會變得非常龐大且存儲成本也很高。在這個系統中,如果我們假設我們擁有世界上很多書籍 - 1億本(參見Inside Google Books博客文章) - 並且每本書只有10頁長(爲了使數學更容易),每頁250字, 這意味着有2500億字。 如果我們假設每個字平均有5個字符,並且每個字符佔用8位(或1個字節,即使某些字符是2個字節),那麼每個字5個字節,那麼只包含每個字一次的索引超過1TB 存儲。 因此,您可以看到創建具有許多其他信息的索引,例如單詞元組,數據位置和出現次數,其容量增長的速度非常快。

創建這些中間索引並以較小的部分表示數據可以使大數據問題易於處理。數據可以分佈在許多服務器上,並且仍然可以快速訪問。索引是信息檢索的基石,也是當今現代搜索引擎的基礎。 當然,本節僅涉及表面,並且正在進行大量研究,以便如何使索引更小,更快,包含更多信息(如相關性),並無縫更新。 (在添加新數據或更改現有數據所需的大量更新,特別是在涉及相關性或評分的情況下,以及競態方面都存在着很大的挑戰)。

能夠快速,輕鬆地找到您的數據非常重要; 索引是實現這一目標的有效而簡單的工具。

負載均衡(Load Balancers)

最後,任何分佈式系統的另一個關鍵部分是負載均衡器。 負載均衡器是任何架構的主要部分,因爲它們的作用是在負責服務請求的一組節點之間分配負載。 這允許多個節點透明地爲系統中的相同功能提供服務。 (參見圖18。)它們的主要目的是處理大量併發連接並將這些連接路由到其中一個請求節點,從而允許系統通過僅添加節點來實現擴展,以服務更多的請求。
圖18
有許多不同的算法可用於服務請求,包括選擇隨機節點,循環,甚至根據某些標準選擇節點,例如內存或CPU利用率。負載平衡器可以實現爲軟件或硬件設備。 一個廣泛採用的開源軟件負載均衡器是HAProxy)。

在分佈式系統中,負載平衡器通常位於系統的最前端,以便相應地路由所有傳入請求。 在複雜的分佈式系統中,將請求路由到多個負載均衡器的情況並不少見(就是類似於多級的負載均衡器嵌套),如圖19所示。
圖19
與代理一樣,某些負載均衡器也可以根據請求的類型不同地路由請求。 (從技術上講,這些也稱爲反向代理。)

負載平衡器面臨的挑戰之一是管理特定於用戶會話的數據。 在電子商務網站中,當您只有一個客戶時,很容易讓用戶將內容放入購物車並在訪問之間保留這些內容(這很重要,因爲如果當他們返回的時候仍然在用戶的購物車中,那麼這個商品更加可能被賣出去)。但是,如果用戶被路由到一個節點進行會話,然後在下次訪問時被路由到另一個節點,則可能存在不一致,因爲新節點可能缺少該用戶的購物車內容。(如果你把6包Mountain Dew放在你的購物車然後返回之後發現它是空的,你不會感到沮喪嗎?)解決這個問題的方法之一就是讓會話變得粘(也就是會話和服務的節點之間有聯繫),這樣用戶總是被路由到同一個節點。但是,很難利用自動故障轉移等可靠性功能。 在這種情況下,用戶的購物車將始終具有內容,但是如果他們的粘性節點變得不可用,則需要當成特殊場景來處理並且這個原來假定的節點上的內容不再有效(希望這個假設不會被內置到應用程序中)。當然,這個問題可以通過本章中的其他策略和工具來解決,例如服務,以及許多本章未涵蓋的技術(如瀏覽器緩存,cookie和URL重寫)。

如果系統只有幾個節點,那麼像時間片輪轉(round-robin)DNS這樣的系統可能會更有意義,因爲負載均衡器可能很昂貴,並且增加一層不是必要的複雜度。當然,在較大的系統中,存在各種不同的調度和負載平衡算法,包括諸如隨機選擇或時間片輪轉的簡單算法,以及考慮利用率和容量之類的更復雜的機制。所有這些算法都允許分發流量和請求,並且可以提供有用的可靠性工具,如自動故障轉移或自動刪除壞節點(例如,當它變得無響應時)。但是,這些高級功能可能會使問題診斷變得更麻煩。 例如,當涉及高負載情況時,負載平衡器將刪除可能很慢或超時的節點(因爲請求太多),但這隻會加劇其他節點的情況。 在這些情況下,廣泛的監控很重要,因爲整體系統流量和吞吐量可能看起來正在下降(因爲節點服務的請求較少),但各個節點的流量和吞吐量正在變得最大化。

負載平衡器是一種允許您擴展系統容量的簡單方法,與本文中的其他技術一樣,在分佈式系統架構中發揮着至關重要的作用。負載平衡器還提供了能夠測試節點運行狀況的關鍵功能,這樣,如果節點沒有響應或過載,可以從處理請求的池中刪除它,利用系統中不同節點的冗餘。

隊列(Queues)

到目前爲止,我們已經介紹了很多快速讀取數據的方法,但擴展數據層的另一個重要部分是有效的寫入管理。 當系統簡單,處理負載最小且數據庫較小時,寫入速度可以預測很快; 但是,在更復雜的系統中,寫入可能需要幾乎不確定的長時間。例如,可能必須將數據寫入不同服務器或索引上的多個位置,或者系統可能處於高負載狀態。 在執行寫入或任何相關任務可能需要很長時間的情況下,要實現高性能和可用性的目標,需要在系統中建立異步; 一種常見的方法是使用隊列。

想象一個系統,每個客戶端都要求遠程服務任務。 這些客戶端中的每一個都將其請求發送到服務器,服務器儘快完成任務並將結果返回給各自的客戶端。 在小型系統中,一臺服務器(或邏輯服務)可以像客戶端到達一樣快地爲傳入的客戶端迅速提供服務,這種情況可以正常工作。 但是,當服務器接收到的請求數超出其處理能力時,則會強制每個客戶端等待其他客戶端的請求完成,然後才能生成響應。這是同步請求的示例,如圖1.20所示。
圖20
這種同步行爲會嚴重降低客戶端性能; 客戶端被迫等待,一直在空轉,直到其請求得到響應。添加額外的服務器以解決系統負載也無法解決問題; 即使有效的負載平衡到位,也很難達到爲了實現客戶端性能最大化所需要的任務的均勻和公平分配。更重要的是,如果處理請求的服務器不可用或失敗,則上游客戶端也將失敗。有效地解決這個問題需要在客戶端的請求爲服務它而執行的實際工作之間進行抽象。

輸入隊列。 隊列就像聽起來一樣簡單:任務進來,被添加到隊列中,然後當工作進程有容量能處理下一個任務的時候,從中取走下一個任務。 (參見圖21。)這些任務可能是對數據庫的簡單寫入,或者像爲文檔生成縮略圖預覽圖像那樣複雜的任務。當客戶端向隊列提交任務請求時,他們不再強制等待結果; 相反,他們只需要確認請求已被正確接收。此確認後續作爲客戶端對計算任務的結果的引用。
圖21

隊列使客戶端能夠以異步方式工作,提供的是一種客戶端請求及其響應的抽象。另一方面,在同步系統中,請求和回覆之間沒有區別,因此它們不能單獨管理。在異步系統中,客戶端請求任務,服務以確認收到任務的消息進行響應,然後客戶端可以定期檢查任務的狀態,僅在結果完成後請求結果。 當客戶端等待異步請求完成時,它可以自由地執行其他工作,甚至可以對其他服務進行異步請求。 後面是如何在分佈式系統中利用隊列和消息的示例。

隊列還提供一些對於服務中斷和故障的保護。例如,創建一個高度健壯的隊列非常容易,該隊列可以重試因短暫的服務器故障而失敗的服務請求。最好使用隊列來保證服務質量(QoS)。而不是直接將客戶端直接暴露給存在間歇性故障中斷的服務,這需要客戶端能對複雜且經常不一致的錯誤進行處理。

隊列是管理任何大型分佈式系統的不同部分之間的分佈式通信的基礎,並且有許多方法來實現它們。有很多開源隊列,如RabbitMQ,ActiveMQ,BeanstalkD,但有些也使用像Zookeeper這樣的服務,甚至像Redis這樣的數據存儲。

1.4 結論

設計可快速訪問海量數據的高效系統是令人興奮的,還有很多爲各種新應用賦能的偉大的新工具。這章僅僅覆蓋了很少的一些例子,但是還有更多,並且這個領域在持續創新。

參考資料

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