本文翻譯自:http://www.aosabook.org/en/index.html (卷2第1章)
中文版參考了這裡的翻譯:http://www.oschina.net/translate/scalable-web-architecture-and-distributed-systems
開源軟體已成為一些超大型網站的基礎組件。並且隨著那些網站的發展,圍繞它們的架構出現了一些最佳實務與指導性原則。本章嘗試闡述設計大型網站需要考慮的一些關鍵問題,以及一些實現這些目標的組件。
本章主要側重於Web系統,雖然其中一些內容也適用於其它分布式系統。
1.1 Web分布式系統設計原則
構建和營運一個可擴充Web網站或者應用到底意味著什嗎?說到底這種系統只不過是通過互連網將使用者與遠端資源相串連—使其可擴充的是分佈於多個伺服器的資源,或者對這些資源的訪問。
類似於生活中的大多數東西,從長遠來說,構建一個web服務之前花些時間提前規劃是很有協助的。理解大型網站背後一些需要考慮的因素與權衡取捨,在建立小一些的web網站時能讓你作出更明智的決策。以下是影響大規模web系統設計的一些核心原則:
* 可用性: 一個網站的正常已耗用時間對於許多公司的聲譽與運作都是至關重要的。對於一些更大的線上零售網站,幾分鐘的不可用都會造成數千或數百萬美元的營收損失,因此系統設計得能夠持續服務,並且能迅速從故障中恢複是技術和業務的最基本要求。分布式系統中的高可用性需要仔細考慮關鍵組件的冗餘,從部分系統故障中迅速恢複,以及問題發生時優雅降級。
* 效能: 對於多數網站而言,網站的效能已成為一個重要的考慮因素。網站的速度影響著使用和使用者滿意度,以及搜尋引擎排名,與營收和是否能留住使用者直接相關。因此,建立一個針對快速響應與低延遲進行最佳化的系統非常重要。
* 可靠性: 系統必須是可靠的,這樣相同資料請求才會始終返回相同的資料。資料變換或更新之後,同樣的請求則應該返回新的資料。使用者應該知道一點:如果東西寫入了系統,或者得到儲存,那麼它會持久化並且肯定保持不變以便將來進行檢索。
* 可擴充性: 對於任何大型分布式系統而言,大小(size)只是需要考慮的規模(scale)問題的一個方面。同樣重要的是努力去提高處理更大負載的能力,這通常被稱為系統的可擴充性。可擴充性以系統的許多不同參數為參考:能夠處理多少額外流量?增加儲存容量有多容易?能夠處理多少更多的事務?
* 可管理性:系統設計得易於營運是另一個重要的考慮因素。系統的可管理性等價於營運(維護和更新)的可擴充性。對於可管理性需要考慮的是:問題發生時易於診斷與理解,便於更新或修改,系統營運起來如何簡單(例如:常規營運是否不會引發失敗或異常?)
* 成本: 成本是一個重要因素。很明顯這包括硬體和軟體成本,但也要考慮系統部署和維護這一方面。系統構建所花費的開發人員時間,系統運行所需要的營運工作量,以及培訓工作都應該考慮進去。成本是擁有系統的總成本。
這些原則中的每一個都為設計分布式web架構提供了決策依據。然而,它們之間也會相互不一致,這樣實現一個目標的代價是犧牲另一個目標。一個基本的例子:簡單地通過增加更多的伺服器(可擴充性)來解決容量問題是以可管理性(你需要營運額外的一台伺服器)和成本(伺服器的價錢)為代價的。
設計任何一種web應用,考慮這些核心原則都是非常重要的,即使明知某個設計也許會犧牲其中的一個或多個原則。
1.2 基礎概念
說到系統架構,需要考慮幾個事情:什麼是合適的組件,這些組件如何組合在一起,以及什麼是正確的權衡取捨。在需要之前擴大投資通常不是一種明智的商業主張。然而,在設計上的一些遠見在將來能夠節省大量的時間和資源。
本節主要闡述對於幾乎所有大型web應用來說都是非常重要的一些核心因素:服務,冗餘,分區,以及故障處理。這些因素中的每一個都涉及選擇與折中,特別是在上一節所描述的那些原則的上下文中。為了詳細地解釋這些東西,最好是從一個例子開始。
例子:圖片託管應用
可能在以前的某個時候,你在網上張貼過圖片。對於託管和提供大量圖片的大網站來說,構建一個性價比高、高可用、以及低延遲(快速檢索)的架構是存在諸多挑戰的。
想象存在這樣一個系統,使用者可以上傳圖片到中央伺服器,也可以通過web連結或者API請求圖片,就像Flickr或Picasa一樣。為了簡單起見,我們假設這個應用有兩個關鍵區段:上傳(寫)圖片到伺服器和查詢圖片。當然我們希望圖片上傳很高效,同時我們非常關注當有人請求一張圖片時(例如,網頁或者其他應用請求圖片),系統能夠快速地交付。這非常類似於web伺服器或內容分髮網絡(CDN)Edge Server(CDN將這種伺服器用於在多個地方儲存內容,這樣內容就在地理/物理距離上更接近使用者,從而更加快速)提供的功能。
系統的另一些重要方面有:
* 對於將要儲存的圖片數量沒有限制,因此需要考慮儲存的可擴充性。
* 圖片下載/請求的延遲要低。
* 如果使用者上傳了某張圖片,那麼這張圖片就得一直存在(圖片資料的可靠性)。
* 系統應該易於維護(可管理性)。
* 由於圖片託管的利潤空間不大,所以系統應有較高的性價比。
圖1.1是系統的一張功能簡化圖。
圖1.1:圖片託管應用的簡化架構圖
在這個圖片託管例子中,系統必須明顯地快速,資料存放區可靠,並且所有這些屬性高度可擴充。構建該應用的一個小型版本輕而易舉,也很容易搭載在單個伺服器上;然而,那樣本章就沒多大意思了。假設我們想構建一個能夠發展得和Flickr一樣龐大的應用。
服務(Services)
考慮可擴充的系統設計時,對功能進行解耦,然後將系統的每一部分看作能夠自己提供服務,並具備明確定義的介面。實踐中,人們評價以這種方式設計的系統具備面向服務的架構(SOA)。對這類系統來說,每個服務都有自己截然不同的功能上下文,並通過一個抽象介面與該上下文之外的一切(通常是另一個服務公開的API)進行互動。
將系統解構為一組相互補充的服務也就將不同組件的操作進行解耦。這種抽象有助於在服務、底層環境以及服務的消費者之間建立清晰的關係。這樣明確的劃分有助於隔離問題,也允許每個組件獨立於其他組件進行擴充。這類面向服務的系統設計非常類似於程式設計的物件導向設計。
在我們的例子中,所有上傳和檢索圖片的請求都是在同一個伺服器上處理的;然而,將這兩個功能分割成兩個獨立的服務在系統需要擴充時非常有意義。
現在假設該服務被大量使用;這種情況下很容易看到寫操作對讀取圖片所花時間的影響有多大(因為這兩個功能將競爭共用資源)。依賴於這種架構,這個影響會很大。即使上傳和下載速度相同(多數IP網路不是這樣的,而是以下載速度:上傳速度為3:1的比例進行設計),檔案讀取操作通常是從緩衝中讀,而寫操作最終是要寫到磁碟(在最終一致的情況下,也許還要多次寫)。即使所有東西都在記憶體中或者都從磁碟上讀取(如SSD),資料庫寫操作幾乎總是比讀操作慢。(Pole Position,一個資料庫基準測試的開源工具,http://polepos.org/,測試結果見http://polepos.sourceforge.net/results/PolePositionClientServer.pdf)。
該設計的另一個潛在問題是像Apache或lighttpd這樣的web伺服器通常有可以維持的並發串連數量的上限(預設值為500左右,但可以更高)。在高流量下,寫操作會迅速消耗完允許的並發串連數。由於讀操作可以是非同步,或藉助於其他效能最佳化方法,如gzip壓縮或分塊傳輸編碼,web伺服器可以在讀操作之間更快速地切換服務,以及在用戶端之間快速切換從而能夠在每秒內服務於比串連最大值(使用Apache,將最大串連數設定為500,每秒服務數千個讀操作請求並不罕見)更多的請求。另一方面,寫操作傾向於在圖片上傳期間維持一個開啟的串連,在多數家用網路中,上傳一個1MB的檔案需要花費多於1秒的時間,這樣web伺服器僅可以處理500個這樣的並發寫操作。
圖1.2:切分讀寫操作
為這類瓶頸做規劃是將圖片的讀寫操作切分成獨立服務的一個很好的案例。1.2所示。這就允許我們單獨地對兩者中任意一個做擴充(因為通常讀操作總是比寫操作多),也有助於理清每個點上正在發生的事情。最後,這也分離了未來的憂患,從而更易於排解故障和對讀操作較慢這類問題進行擴充。
這種方法的優勢在於我們能夠將問題獨立於其他問題地進行解決—我們無需擔心相同上下文中新圖片的寫操作和檢索。這兩個服務仍然基於全域圖片語料,但可以通過與服務相適應的方法(例如:排隊請求,或緩衝常用圖片—更多相關內容見下文)隨意地最佳化它們的效能。從維護與成本的角度來看,每個服務都可以按需獨立地擴充,這一點非常重要,因為如果服務是摻雜混合的,在上述情境中,一個服務會無意地影響另一個服務的效能。
當然,若你有兩個不同的端點,那麼上述例子能夠工作得很好(事實上這非常類似於多個雲端儲存體提供者的實現和內容分髮網絡)。雖然有很多方法可以解決這類瓶頸,但每個都有不同的權衡折中。
例如,Flickr通過將使用者分散到不同的資料庫分區上來解決這個讀/寫問題,這樣每個資料庫分區僅能夠處理一定數量的使用者,並且隨著使用者的增加,可以添加更多的資料庫分區到伺服器叢集中(見關於Flickr擴充工作的簡報:http://mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html(注意在牆外))。在第一個例子中,基於實際使用請求,擴充硬體更容易,Flickr則是隨著使用者群的變化進行擴充(但要求假設在使用者之間的使用方式均衡,從而可以添加額外的容量)。對於前者,如果一個服務存在故障或問題,就會削弱整個系統的功能(例如,沒人可以寫檔案),但若Flickr的一個資料庫分區存在故障則僅影響使用該分區的使用者。第一個例子中,對整個資料集執行操作更方便。例如,更新寫操作服務以包含新的中繼資料或在所有圖片中繼資料上搜尋。對於Flickr的架構,需要更新或搜尋每個資料庫分區(或者需要建立一個搜尋服務來整理中繼資料—事實上它們也這麼做了)。
對於這些系統的討論並沒有正確的答案,但迴歸到本章開頭敘述的原則,確定系統的需求(頻繁讀或寫或兩者皆如此,並發層級,查詢整個資料集,範圍,排序,等等),基準測試不同的方案選擇,理解系統如何會失效,以及準備一個可靠的計劃以應對故障的發生是很有用的。
冗餘(Redundancy)
為了優雅地處理故障,web架構必須具備冗餘的服務和資料。例如,若某檔案僅有一個拷貝儲存在單個伺服器上,那麼失去該伺服器即意味著失去了該檔案。遺失資料很少是件好事,處理該問題的常見方法是建立多個或者說冗餘的資料拷貝。
同樣的原則也可應用於服務。如果應用程式的功能有個核心組件,那麼確保同時運行多個拷貝或版本能夠使系統免於單點故障。
在系統中建立冗餘能夠消除可能發生故障的單點,為了災難恢複提供備份或備用的功能。例如,如果生產中運行著兩個相同服務的執行個體,當其中一個發生故障或功能退化時,系統能夠失效轉移到健全副本。失效備援可以自動發生或者手動介入。
服務冗餘的另一關鍵區段是建立一個無共用(shared-nothing)的架構。使用這種架構,每個節點的營運工作都能獨立於其它節點,也沒有中心“大腦”來管理狀態或協調節點的行為。這有助於提高可擴充性,因為不需要特殊的條件或瞭解就能添加新的節點。然而,最重要的是這種系統不會有單點故障,因此對於故障更有彈性。
例如,在我們的圖片伺服器應用中,所有的圖片都在另一處(理想情況是在不同的地理位置,從而能夠應對地震或資料中心發生火災一類的災難)的硬體上存放著冗餘的拷貝,提供圖片訪問的服務也是冗餘的,均潛在地服務於請求(見圖1.3)(負載平衡器是使其成為可能的一種絕佳方法,將在下文詳述)。
圖1.3:具備冗餘的圖片託管應用
分區(Partitions)
可能會存在非常大的資料集無法存放在單個伺服器上。也可能某個操作需要非常多的計算資源,導致效能降低,需要增強計算能力。對於任一情形,你都有兩種選擇:縱向或橫向擴充。
縱向擴充即對單個伺服器添加更多的資源。因此對於一個非常龐大的資料集來說,這意味著增加更多(或更大的)硬碟,從而單個伺服器能夠容納下整個資料集。對於計算操作而言,這意味著將計算遷移到具備更快速的CPU或更大的記憶體空間的伺服器上。任一情況,都是使得單個伺服器的資源能夠自己解決對於更多資源的需求問題,實現縱向擴充。
另一方面,橫向擴充則是添加更多的節點。針對大資料集的情況,這就是使用第二個伺服器來儲存資料集的一部分,對於計算資源而言,這意味著將操作或負載分割到額外的節點。為了充分利用橫向擴充,應將其作為系統架構的一種本質的設計原則,否則為實現橫向擴充而修改系統或分割上下文會相當麻煩。
說到橫向擴充,一種更常見的技術是對服務進行分區,或分塊。分區可以是分布式的,這樣邏輯功能集之間是相互獨立的;可以通過地理邊界,或其他標準(如非付費使用者 VS. 付費使用者)來實現。這些方案的優勢是可以提供更強的服務或資料存放區能力。
在我們的圖片伺服器例子中,將曾經儲存在單一的檔案伺服器的圖片重新儲存到多個檔案伺服器中是可以實現的,每個檔案伺服器都有自己惟一的圖片集(見圖1.4)。這種架構允許系統將圖片儲存到某個檔案伺服器中,在伺服器都即將存滿時,像增加硬碟一樣增加額外的伺服器。這種設計需要一種能夠將檔案名稱和存放伺服器綁定的命名規則。一個映像的名稱可能是映射全部伺服器的完整散列方案的形式。或者可選的,每個映像都被分配給一個遞增的 ID,當使用者請求映像時,映像檢索服務只需要儲存映射到每個伺服器的 ID 範圍(類似索引)就可以了。
圖1.4:使用冗餘和分區實現的圖片儲存服務
當然,為多個伺服器分配資料或功能是充滿挑戰的。一個關鍵的問題就是資料局部性;對於分布式系統,計算或操作的資料越相近,系統的效能越佳。因此,一個潛在的問題就是資料的存放遍布多個伺服器,當需要一個資料時,它們並不在一起,迫使伺服器不得不為從網路中擷取資料而付出昂貴的效能代價。
另一個潛在的問題是不一致性。當多個不同的服務讀取和寫入同一共用資源時,有可能會遭遇競爭狀態——某些資料應當被更新,但讀取操作恰好發生在更新之前——這種情形下,資料就是不一致的。例像託管方案中可能出現的競爭狀態,一個用戶端發送請求,將其某標題為“狗"的映像改名為”小傢伙“。而同時另一個用戶端發送讀取此映像的請求。第二個用戶端中顯示的標題是“狗”還是“小傢伙”是不能明確的。
當然,對於分區還有一些障礙存在,但分區允許將問題——資料、負載、使用模式等——切割成可以管理的資料區塊。這將極大的提高可擴充性和可管理性,當然並非沒有風險。有很多可以降低風險和處理故障的方法;不過篇幅有限,不再贅述。若有興趣,可見於此文,擷取更多容錯和檢測的資訊。
1.3 構建高效和可伸縮的資料訪問模組
在設計分布式系統時一些核心問題已經考慮到,現在讓我們來討論下比較困難的一部分:可伸縮的資料訪問。
對於大多數簡單的web應用程式,比如LAMP系統,類似於圖1.5。
圖1.5:簡單web應用程式
隨著它們的成長,主要發生了兩方面的變化:應用伺服器和資料庫的擴充。在一個高度可伸縮的應用程式中,應用伺服器通常最小化並且一般是shared-nothing架構(譯註:shared nothing architecture是一 種分散式運算架構,這種架構中不存在集中儲存的狀態,整個系統中沒有資源競爭,這種架構具有非常強的擴充性,在web應用中廣泛使用)方式的體現,這使得系統的應用伺服器層水平可伸縮。由於這種設計,資料庫伺服器可以支援更多的負載和服務;在這一層真正的擴充和效能改變開始發揮作用了。
剩下的章節主要集中於通過一些更常用的策略和方法提供快速的資料訪問來使這些類型服務變得更加迅捷。
圖1.6:最簡單的web應用程式
多數系統簡化為 Figure 1.6所示,這是一個良好的開始。如果你有大量的資料,你想快捷的訪問,就像一堆糖果擺放在你辦公室抽屜的最上方。雖然過於簡化,前面的聲明暗示了兩個困難的問題:儲存的延展性和資料的快速存取。
為了這一節內容,我們假設你有很大的資料存放區空間(TB),並且你想讓使用者隨機訪問一小部分資料(查看Figure 1.7)。這類似於在映像應用的例子裡在檔案伺服器定位一個圖片檔案。
圖1.7:訪問特定資料
這非常具有挑戰性,因為它需要把數TB的資料載入到記憶體中;並且直接轉化為磁碟的IO。要知道從磁碟讀取比從記憶體讀取慢很多倍-記憶體的訪問速度如同敏捷的查克·諾裡斯(譯註:空手道冠軍),而磁碟的訪問速度就像笨重的卡車一樣。這個速度差異在大資料集上會增加更多;在實數順序讀取上記憶體訪問速度至少是磁碟的6倍,隨機讀取速度比磁碟快100,000倍(參考“大資料之殤”http://queue.acm.org/detail.cfm?id=1563874)。另外,即使使用唯一的ID,解決擷取少量資料存放位置的問題也是個艱巨的任務。這就如同不用眼睛看,在你的糖果存放點取出最後一塊Jolly
Rancher口味的糖果一樣。
謝天謝地,有很多方式你可以讓這樣的操作更簡單些;其中四個比較重要的是緩衝,代理,索引和負載平衡。本章的剩餘部分將討論下如何使用每一個概念來使資料訪問加快。
緩衝
緩衝利用局部訪問原則:最近請求的資料可能會再次被請求。它們幾乎被用於電腦的每一層:硬體,作業系統,web瀏覽器,web應用程式等等。緩衝就像短期儲存的記憶體:它有空間的限制,但是通常訪問速度比來源資料源快並且包含了大多數最近訪問的條目。緩衝可以在架構的各個層級存在,但是常常在前端比較常見,在這裡通常需要在沒有下遊層級的負擔下快速返回資料。
在我們的API例子中如何使用緩衝來快速存取資料?在這種情況下,有兩個地方你可以插入緩衝。一個操作是在你的請求層節點添加一個緩衝, Figure 1.8。
圖1.8:在請求層節點插入一個緩衝
直接在一個請求層節點配置一個緩衝可以在本機存放區相應資料。每次發送一個請求到服務,如果資料存在節點會快速的返回本機快取的資料。如果資料不在緩衝中,請求節點將在磁碟尋找資料。請求層節點緩衝可以存放在記憶體和節點本地磁碟中(比網路儲存快些)。
圖1.9:多個緩衝
當你擴充這些節點後會發生什麼呢?Figure 1.9所示,如果請求層擴充為多個節點,每個主機仍然可能有自己的緩衝。然而,如果你的負載平衡器隨機分配請求到節點,同樣的請求將指向不同的節點,從而增加了緩衝的命中缺失率。有兩種選擇可以解決這個問題:全域緩衝和分布式緩衝。
全域緩衝
全域緩衝顧名思義:所有的節點使用同一個緩衝空間,這涉及到添加一個伺服器,或者某種檔案儲存體系統,速度比訪問源儲存和通過所有節點訪問要快些。每個請求節點以同樣的方式查詢本地的一個緩衝,這種緩衝方案可能有點複雜,因為在用戶端和請求數量增加時它很容易被壓倒,但是在有些架構裡它還是很有用的(尤其是那些專門的硬體來使全域緩衝變得非常快,或者是固定資料集需要被緩衝的)。
在描述圖中有兩種常見形式的緩衝。在圖Figure 1.10中,當一個緩衝響應沒有在緩衝中找到時,緩衝自身從底層儲存中尋找出資料。在 Figure 1.11中,當在緩衝中招不到資料時,請求節點會向底層去檢索資料。
圖1.10:全域緩衝(緩衝自己負責尋找資料)
圖1.11:全域緩衝(請求節點負責尋找資料)
大多數使用全域緩衝的應用程式趨向於第一類,這類緩衝可以管理資料的讀取,防止用戶端大量的請求同樣的資料。然而,一些情況下,第二類實現方式似乎更有意義。比如,如果一個緩衝被用於非常大的檔案,一個低命中比的緩衝將會導致緩衝區來填滿未命中的緩衝;在這種情況下,將使緩衝中有一個大比例的總資料集。另一個例子是架構設計中檔案在緩衝中儲存是靜態並且不會被排除。(這可能是因為應用程式要求周圍資料的延遲---某些片段的資料可能需要在大資料集中非常快---在有些地方應用程式邏輯知道,排除策略或者熱點會比緩衝方案好使些)
分布式緩衝
在分布式緩衝(圖1.12)中,每個節點都會緩衝一部分資料。如果把冰箱看作食雜店的緩衝的話,那麼分布式緩衝就象是把你的食物分別放到多個地方 —— 你的冰箱、櫃櫥以及便當盒 ——放到這些便於隨時取用的地方就無需一趟趟跑去食雜店了。緩衝一般使用一個具有一致性的雜湊函數進行分割,如此便可在某請求節點尋找某資料時,能夠迅速知道要到分布式緩衝中的哪個地方去找它,以確定改資料是否從緩衝中可得。在這種情況下,每個節點都有一個小型緩衝,在直接到資料原始存放處找資料之前就可以向別的節點發出尋找資料的請求。由此可得,分布式緩衝的一個優勢就是,僅僅通過向請求池中添加新的節點便可以擁有更多的緩衝空間。
分布式緩衝的一個缺點是修複缺失的節點。一些分布式緩衝系統通過在不同節點做多個備份繞過了這個問題;然而,你可以想象這個邏輯迅速變複雜了,尤其是當你在請求層添加或者刪除節點時。即便是一個節點消失和部分快取資料丟失了,我們還可以在來源資料儲存地址擷取-因此這不一定是災難性的!
圖1.12:分布式緩衝
緩衝的偉大之處在於它們使我們的訪問速度更快了(當然前提是正確使用),你選擇的方法要在更多請求下更快才行。然而,所有這些緩衝的代價是必須有額外的儲存空間,通常在放在昂貴的記憶體中;從來沒有嗟來之食。緩衝讓事情處理起來更快,而且在高負載情況下提供系統功能,否則將會使伺服器出現降級。
有一個很流行的開源快取項目Memcached (http://memcached.org/)(它可以當做一個本機快取,也可以用作分布式緩衝);當然,還有一些其他動作的支援(包括語言套件和架構的一些特有設定)。
Memcached被用作很多大型的web網站,儘管他很強大,但也只是簡單的記憶體key-value儲存方式,它最佳化了任意資料存放區和快速檢索(o(1))。
Facebook使用了多種不同的緩衝來提高他們網站的效能(查看"Facebook caching and performance")。在語言層面上(使用PHP內建函數調用)他們使用$GLOBALSand APC緩衝,這有助於使中間函數調用和結果返回更快(大多數語言都有這樣的類庫用來提高web頁面的效能)。Facebook使用的全域緩衝分布在多個伺服器上(查看 "Scaling
memcached at Facebook"),這樣一個訪問緩衝的函數調用可以使用很多並行的請求在不同的Memcached 伺服器上擷取儲存的資料。這使得他們在為使用者指派資料空間時有了更高的效能和輸送量,同時有一個中央伺服器做更新(這非常重要,因為當你運行上千伺服器時,緩衝失效和一致性將是一個大挑戰)。
現在讓我們討論下當資料不在緩衝中時該如何處理···
代理
簡單來說,Proxy 伺服器是一種處於用戶端和伺服器中間的硬體或軟體,它從用戶端接收請求,並將它們轉交給伺服器。代理一般用於過濾請求、記錄日誌或對請求進行轉換(增加/刪除頭部、加密/解密、壓縮,等等)。
圖1.13:Proxy 伺服器
當需要協調來自多個伺服器的請求時,Proxy 伺服器也十分有用,它允許我們從整個系統的角度出發、對請求流量執行最佳化。壓縮轉寄(collapsed forwarding)是利用代理加快訪問的其中一種方法,將多個相同或相似的請求壓縮在同一個請求中,然後將單個結果發送給各個用戶端。
假設,有幾個節點都希望請求同一份資料,而且它並不在緩衝中。在這些請求經過代理時,代理可以通過壓縮轉寄技術將它們合并成為一個請求,這樣一來,資料只需要從磁碟上讀取一次即可(見圖1.14)。這種技術也有一些缺點,由於每個請求都會有一些時延,有些請求會由於等待與其它請求合并而有所延遲。不管怎麼樣,這種技術在高負載環境中是可以協助提升效能的,特別是在同一份資料被反覆訪問的情況下。壓縮轉寄有點類似緩衝技術,只不過它並不對資料進行儲存,而是充當用戶端的代理人,對它們的請求進行某種程度的最佳化。
在一個LANProxy 伺服器中,用戶端不需要通過自己的IP串連到Internet,而代理會將請求相同內容的請求合并起來。這裡比較容易搞混,因為許多代理同時也充當緩衝(這裡也確實是一個很適合放緩衝的地方),但緩衝卻不一定能當代理。
圖1.14: 使用代理來合并請求
另一個使用代理的方式不只是合并相同資料的請求,同時也可以用來合并靠近儲存源(一般是磁碟)的資料請求。採用這種策略可以讓請求最大化使用本機資料,這樣可以減少請求的資料延遲。比如,一群節點請求B的部分資訊:partB1,partB2等,我們可以設定代理來識別各個請求的空間地區,然後把它們合并為一個請求並返回一個bigB,大大減少了讀取的資料來源(查看圖Figure 1.15)。當你隨機訪問上TB資料時這個請求時間上的差異就非常明顯了!代理在高負載情況下,或者限制使用緩衝時特別有用,因為它基本上可以批量的把多個請求合并為一個。
圖1.15:使用代理來合并空間緊密的資料請求
值得注意的是,代理和緩衝可以放到一起使用,但通常最好把緩衝放到代理的前面,放到前面的原因和在參與者眾多的馬拉松比賽中最好讓跑得較快的選手在隊首起跑一樣。因為緩衝從記憶體中提取資料,速度飛快,它並不介意存在對同一結果的多個請求。但是如果緩衝位於Proxy 伺服器的另一邊,那麼在每個請求到達cache之前都會增加一段額外的時延,這就會影響效能。
如果你正想在系統中添加代理,那你可以考慮的選項有很多;Squid和Varnish都經過了實踐檢驗,廣泛用於很多實際的web網站中。這些代理解決方案針對大部分client-server通訊提供了大量的最佳化措施。將二者之中的某一個安裝為web伺服器層的反向 Proxy(reverse
proxy,下面負載平衡器一節中解釋)可以大大提高web伺服器的效能,減少處理來自用戶端的請求所需的工作量。
索引
使用索引快速存取資料是個最佳化資料訪問效能公認的策略;可能我們大多數人都是從資料庫瞭解到的索引。索引用增長的儲存空間佔用和更慢的寫(因為你必須寫和更新索引)來換取更快的讀取。
你可以把這個概念應用到大資料集中就像應用在傳統的關係資料存放區。索引要關注的技巧是你必須仔細考慮使用者會怎樣訪問你的資料。如果資料集有很多TBs,但是每個資料包(payload)很小(可能只有1KB),這時就必須用索引來最佳化資料訪問。在這麼大的資料集找到小的資料包是個很有挑戰性的工作,因為你不可能在合理的時間內遍曆所有資料。甚至,更有可能的是這麼大的資料集分布在幾個(甚至很多個)物理裝置上-這意味著你要用些方法找到期望資料的正確物理位置。索引是最適合的方法做這種事情。
圖1.16:索引
索引可以作為內容的一個表格-表格的每一項指明你的資料存放區的位置。例如,如果你正在尋找B的第二部分資料-你如何知道去哪裡找?如果你有個根據資料類型(資料A,B,C)排序的索引,索引會告訴你資料B的起點位置。然後你就可以跳轉(seek)到那個位置,讀取你想要的資料B的第二部分。 (參看Figure 1.16)
這些索引常常儲存在記憶體中,或者儲存在對於用戶端請求來說非常快速的本地位置(somewhere very local)。Berkeley DBs (BDBs)和樹狀資料結構常常按順序儲存資料,非常理想用來儲存索引。
常常索引有很多層,當作資料地圖,把你從一個地方指向另外一個地方,一直到你的得到你想要的那塊資料。(See Figure 1.17.)
圖1.17:多層索引
索引也可以用來建立同樣資料的多個不同視圖(views)。對於大資料集來說,這是個很棒的方法來定義不同的過濾器(filter)和類別(sort),而不用建立多個額外的資料拷貝。
例如,想象一下,圖片儲存系統開始實際上儲存的是書的每一頁的映像,而且服務允許客戶查詢這些圖片中的文字,搜尋每個主題的所有書的內容,就像搜尋引擎允許你搜尋HTML內容一樣。在這種情況下,所有的書的圖片佔用了很多很多伺服器儲存,尋找其中的一頁給使用者顯示有點難度。首先,用來查詢任意詞或者詞數組(tuples)的倒排索引(inverse indexes)需要很容易的訪問到;然後,導航到那本書的確切頁面和位置並擷取準確的圖片作為返回結果,也有點挑戰性。所以,這種境況下,倒排索引應該映射到每個位置(例如書B),然後B要包含一個索引每個部分所有單詞,位置和出現次數的索引。
可以表示Index1的一個倒排索引,可能看起來像下面的樣子-每個詞或者詞數組對應一個包含他們的書。
Word(s) |
Book(s) |
being awesome |
Book B, Book C, Book D |
always |
Book C, Book F |
believe |
Book B |
這個中間索引可能看起來像上面的樣子,但是可能只包含詞,位置和書B的資訊。這種嵌套的索引架構要使每個子索引佔用足夠小的空間,以防所有的這些資訊必須儲存在一個大的倒排索引中。
這是大型系統的關鍵點,因為即使壓縮,這些索引也太大,太昂貴(expensive)而難以儲存。在這個系統,如果我們假設我們世界上的很多書--100,000,000(seeInside Google Books blog post)-每個書只有10頁(只是為了下面好計算),每頁有250個詞,那就是2500億(250
billion)個詞。如果我們假設每個詞有5個字元,每個字元佔用8位(或者1個位元組,即使某些字元要用2個位元組),所以每個詞佔用5個位元組,那麼每個詞即使只包含一次,這個索引也要佔用超過1000GB儲存空間。那麼,你可以明白建立包含很多其他資訊-片語,資料位元置和出現次數-的索引,儲存空間增長多快了吧。
建立這些中間索引和用更小分段表示資料,使的大資料問題可以得到解決。資料可以分散到多個伺服器,訪問仍然很快。索引是資訊檢索(information retrieval)的奠基石,是現代搜尋引擎的基礎。當然,我們這段只是淺顯的介紹,還有其他很多深入研究沒有涉及-例如如何使索引更快,更小,包含更多資訊(例如關聯(relevancy)),和無縫的更新(在競爭條件下(race conditions),有一些管理性難題;在海量添加或者修改資料的更新中,尤其還涉及到關聯(relevancy)和得分(scoring),也有一些難題)。
快速簡便的尋找到資料是很重要的;索引是可以達到這個目的有效簡單工具。
負載平衡器
最後還要講講所有分布式系統中另一個比較關鍵的部分,負載平衡器。負載平衡器是各種體繫結構中一個不可或缺的部分,因為它們擔負著將負載在處理服務要求的一組節點中進行分配的任務。這樣就可以讓系統中的多個節點透明地服務於同一個功能(參見圖1.18)。它的主要目的就是要處理大量並發的串連並將這些串連分配給某個請求處理節點,從而可使系統具有伸縮性,僅僅通過添加新節點便能處理更多的請求。
圖1.18:負載平衡器
用於處理這些請求的演算法有很多種,包括隨機選取節點、迴圈式選取,甚至可以按照記憶體或CPU的利用率等等這樣特定的條件進行節點選取。負載平衡器可以用軟體或硬體裝置來實現。近來得到廣泛應用的一個開源的軟體負載平衡器叫做HAProxy。
在分布式系統中,負載平衡器往往處於系統的最前端,這樣所有發來的請求才能進行相應的分發。在一些比較複雜的分布式系統中,將一個請求分發給多個負載平衡器也是常事,1.19所示。
圖1.19:多重負載平衡器
負載平衡器面臨的一個難題是怎麼管理同使用者的session相關的資料。在電子商務網站中,如果你只有一個用戶端,那麼很容易就可以把使用者放入購物車裡的東西儲存起來,等他下次訪問訪問時購物車裡仍能看到那些東西(這很重要,因為當使用者回來發現仍然呆在購物車裡的產品時很有可能就會買它)。然而,如果在一個session中將使用者分發到了某個節點,但該使用者下次訪問時卻分發到了另外一個節點,這裡就有可能產生不一致性,因為新的節點可能就沒有保留下使用者購物車裡的東西。(要是你把6盒子子農夫山泉放到購物車裡了,可下次回來一看購物車空了,難道你不會發火嗎?)
解決該問題的一個方法是使session具有粘性,讓同一使用者總是分發到同一個節點之上,但這樣一來就很難利用類似失效備援(failover)這樣的可靠性措施了。如果這樣的話,使用者的購物車裡的東西不會丟,但如果使用者保持的那個節點失效,就會出現一種特殊的情況,購物車裡的東西不會丟這個假設再也不成立了(雖然但願不要把這個假設寫到程式裡)。當然,這個問題還可以用本章中講到的其它策略和工具來解決,比如服務以及許多並沒有講到的方法(像伺服器緩衝、cookie以及URL重寫)。
如果系統中只有不太多的節點,迴圈式(round robin)DNS系統這樣的方案也許更有意義,因為負載平衡器可能比較貴,而且還額外增加了一層沒必要的複雜性。當然,在比較大的系統中會有各種各樣的調度以及負載平衡演算法,簡單點的有隨機選取或迴圈式選取,複雜點的可以考慮上利用率以及處理能力這些因素。所有這些演算法都是對瀏覽和請求進行分發,並能提供很有用的可靠性工具,比如自動failover或者自動提出失效節點(比如節點失去響應)。然而,這些進階特性會讓問題診斷難以進行。例如,當系統載荷較大時,負載平衡器可能會移除慢速或者逾時的節點(由於節點要處理大量請求),但對其它節點而言,這麼做實際上是加劇了情況的惡化程度。在這時進行大量的監測非常重要,因為系統總體流量和吞吐率可能看上去是在下降(因為節點處理的請求變少了),但個別節點卻越來越忙得不可開交。
負載平衡器是一種能讓你擴充系統能力的簡單易行的方式,和本文中所講的其它技術一樣,它在分布式系統架構中起著基礎性的作用。負載平衡器還要提供一個比較關鍵的功能,它必需能夠探測出節點的健全狀態,比如,如果一個節點失去響應或處於過載狀態,負載平衡器可以將其從處理請求的節點池中移除出去,還接著使用系統中冗餘的其它不同節點。
隊列
目前為止我們已經介紹了許多更快讀取資料的方法,但另一個使資料層具伸縮性的重要部分是對寫的有效管理。當系統簡單的時候,只有最小的處理負載和很小的資料庫,寫的有多快可以預知;然而,在更複雜的系統,寫可能需要幾乎無法決定的長久時間。例如,資料可能必須寫到不同資料庫或索引中的幾個地方,或者系統可能正好處於高負載。這些情況下,寫或者任何那一類任務,有可能需要很長的時間,追求效能和可用性需要在系統中建立非同步;一個通常的做到那一點的辦法是通過隊列。
圖1.20:同步請求
設想一個系統,每個用戶端都在發起一個遠程服務的工作要求。每一個用戶端都向伺服器發送它們的請求,伺服器儘可能快的完成這些任務,並分別返回結果給各個用戶端。在一個小型系統,一個伺服器(或邏輯服務)可以給傳入的用戶端請求提供迅速服務,就像它們來的一樣快,這種情形應該工作的很好。然而,當伺服器收到了超過它所能處理數量的請求時,每個用戶端在產生一個響應前,將被迫等待其他用戶端的請求結束。這是一個同步請求的例子,示意在圖1.20。
這種同步的行為會嚴重的降低用戶端效能;用戶端被迫等待,有效執行零工作,直到它的請求被應答。添加額外的伺服器承擔系統負載也不會解決這個問題;即使是有效負載平衡,為了最大化用戶端效能,保證平等的公平的分發工作也是極其困難的。而且,如果伺服器處理請求不可及,或者失敗了,用戶端上行也會失敗。有效解決這個問題在於,需要在用戶端請求與實際的提供服務的被執行工作之間建立抽象。
圖1.21:用隊列管理請求
進入隊列。一個隊列就像它聽起來那麼簡單:一個任務進入,被排入佇列然後工人們只要有能力去處理就會拿起下一個任務。(看圖1.21)這些任務可能是代表了簡單的寫資料庫,或者一些複雜的事情,像為一個文檔產生一個縮減預覽圖一類的。當一個用戶端提交一個工作要求到一個隊列,它們再也不會被迫等待結果;它們只需要確認請求被正確的接收了。這個確認之後可能在用戶端請求的時候,作為一個工作結果的參考。
隊列使用戶端能以非同步方式工作,提供了一個用戶端請求與其響應的戰略抽象。換句話說,在一個同步系統,沒有請求與響應的區別,因此它們不能被單獨的管理。在一個非同步系統,用戶端請求一個任務,服務端響應一個任務已收到的確認,然後用戶端可以周期性的檢查任務的狀態,一旦它結束就請求結果。當用戶端等待一個非同步請求完成,它可以自由執行其它工作,甚至非同步請求其它的服務。後者是隊列與訊息在分布式系統如何成為槓桿的例子(譯註:用一個或多個、一層或多層的訊息佇列來撬動海量的並發串連)。
隊列也對服務中斷和失敗提供了防護。例如,建立一個高度強健的隊列,這個隊列能夠重新嘗試由於瞬間伺服器故障而失敗的服務要求,是非常容易的事。相比直接暴露用戶端於間歇性服務中斷下--這需要複雜的而且經常不一致的用戶端錯誤處理,用一個隊列去加強服務品質的擔保更為可取。
隊列在管理任何大規模分布式系統不同部分之間的分布式通訊方面是一個基礎,而且實現它們有許多的方法。有不少開源的隊列如RabbitMQ,ActiveMQ,
BeanstalkD,但是有些也用像Zookeeper的服務,或者甚至像Redis的資料存放區。
1.4 結論
設計有效系統來進行快速的大資料訪問是有趣的,同時有大量的好工具來協助各種各樣的應用程式進行設計。 這文章只覆蓋了一些例子,僅僅是一些表面的東西,但將會越來越多--同時在這個領域裡一定會繼續有更多創新東西。