1. 程式人生 > >大型網際網路分散式系統架構技術要點

大型網際網路分散式系統架構技術要點

1.1. web分散式系統的設計原則

搭建和運營一個可伸縮的web站點或者應用程式意味著什麼?在原始層面上這僅僅是使用者通過網際網路連線到遠端資源-使系統變得可伸縮的部分是將資源、或者訪問的資源,分佈於多個伺服器上。

像生活中大多數事情一樣,當構建一個web服務時花時間提前做好計劃從長遠看來還是很有幫助的;瞭解一些注意事項和大網站背後的權衡原則可以在建立小型網站時做出更明智的決定。以下是一些影響大規模web系統設計的關鍵原則:

  • 可用性:對於很多公司來說一個網站的正常執行時間是非常關鍵的聲譽和功能,像一些大型的線上零售系統,即使一分鐘的宕 機都有可能導致數千或者數百萬美元的損失,因此設計系統的時時可用性和彈性的錯誤處理機制既是一個基本業務也是一個技術要求。 高可用分散式系統需要仔細考慮關鍵元件的冗餘,分系統失敗後能快速修復,並且當問題出現時優雅型降級。
  • 效能:網站的效能正在變成大多數站點考慮的一個重要的方面,網站的速度影響正常使用和使用者的滿意度,同樣影響搜尋的排名,這也是影響網站收益和保留使用者的一個因素。因此,建立一個快速響應和低延遲的系統是非常關鍵的。
  • 可靠性:一個系統需要具備可靠性,比如同一個數據的請求始終返回同樣的資料響應 。如果資料改變或者被更新,那麼同樣的資料將返回一個新的資料。使用者需要知道一些東西被寫入系統或者被儲存到系統後,系統會保持不變並且可以在以後恢復到合適的位置。
  • 可伸縮性:當談到任何大型的分散式系統時,規模大小隻是考慮的其中一個方面,同樣重要的是增強處理較大規模的負載效能所做的努力,這通常稱為系統的可伸縮性。可伸縮性可以代表系統很多不同的引數:額外流量的處理量,新增儲存容量的便意性,甚至事務的處理量。
  • 可管理性: 設計一個系統可以方便操作是另一個重要的考慮方面,系統的可管理性等同於操作的可伸縮性:維護和升級。可管理性需要考慮的事情是當問題發生時方便診斷和了解問題,易於升級和修改,以及系統能簡單性的操作(即,例行的操作有沒有失敗和異常?)
  • 成本: 成本是一個重要的因素。很明顯這包含硬體和軟體成本,但同樣重要需要考慮的其他方面是部署和維護系統的成本。開發者構建系統花費的大量時間,運維部署時間,甚至培訓時間都需要考慮,成本是總體成本。

以上每個原則都為設計分散式web架構提供了基礎決策。然而,他們也能彼此互斥,例如要實現某個目標就要以另外的作為代價。一個基本的例子:選擇通過單純 增加更多的伺服器(可擴充套件性)來增加地址容量,是以可管理性(你必須操作增加的伺服器)和成本(伺服器的價格)為代價的。

當設計任何的web應用程式時,考慮這些關鍵原則都是很重要的,即使得承認一個設計可能要犧牲它們之中的一個或者多個。

1.2. 基礎

當設計一個系統架構時,有一些東西是要考慮的:正確的部分是什麼,怎樣讓這些部分很好地融合在一起,以及好的折中方法是什麼。通常在系統架構需要之前就為它的可擴充套件性投資不是一個聰明的商業抉擇;然而,在設計上的深謀遠慮能在未來節省大量的時間和資源。

這部分關注點是幾乎所有大型web應用程式中心的一些核心因素:服務、冗餘、劃分和錯誤處理。每一個因素都包含了選擇和妥協,特別是上部分提到的設計原則。為了詳細的解析這些,最好是用一個例子來開始。

例項:圖片託管應用

有時候你可能會在線上傳一張圖片。對於那些託管並負責分發大量圖片的網站來說,要搭建一個既節省成本又高效還能具備較低的延遲性(你能快速的獲圖片)的網站架構確實是一種挑戰。

我們來假設一個系統,使用者可以上傳他們的圖片到中心伺服器,這些圖片又能夠讓一些web連結或者API獲取這些圖片,就如同現在的Flickr或者 Picasa。為了簡化的需要,我們假設應用程式分為兩個主要的部分:一個是上傳圖片到伺服器的能力(通常說的寫操作),另一個是查詢一個圖片的能力。然 而,我們當然想上傳功能很高效,但是我們更關心的是能夠快速分發能力,也就是說當某個人請求一個圖片的時候(比如,一個web頁面或者其它應用程式請求圖 片)能夠快速的滿足。這種分發能力很像web伺服器或者CDN連線伺服器(CDN伺服器一般用來在多個位置儲存內容一邊這些內容能夠從地理位置或者物理上 更靠近訪問它的使用者,已達到高效訪問的目的)氣的作用。

系統其他重要方面:

  • 對圖片儲存的數量沒有限制,所以儲存需要可擴充套件,在影象數量方面需要考慮。
  • 圖片的下載和請求不需要低延遲。
  • 如果使用者上傳一個圖片,圖片應該都在那裡(圖片資料的可靠性)。
  • 系統應該容易管理(可管理性)。
  • 由於圖片主機不會有高利潤的空間,所以系統需要具有成本效益。

Figure 1.1是一個簡化的功能圖。

Figure 1.1: 圖片主機應用的簡化架構圖

在這個圖片主機的例子裡,可遇見系統必需快速,它的資料儲存要可靠以及這些所有的屬性都應該高度的可擴充套件。建立這個應用程式的一個小版本不是很重要而且很容易部署在單一的伺服器上;然而,這不是這節裡的感興趣部分。假設下我們想建一個會增長到和Flickr痛讓規模的東西。

服務

當要考慮設計一個可擴充套件的系統時,為功能解耦和考慮下系統每部分的服務都定義一個清晰的介面都是很有幫助的。在實際中,在這種方式下的系統設計被成為面向 服務架構(SOA)。對於這型別的系統,每個服務有自己獨立的方法上下文,以及使用抽象介面與上下文的外部任何東西進行互動,典型的是別的服務的公共 API。

把一個系統解構為一些列互補的服務,能夠為這些部分從別的部分的操作解耦。這樣的抽象幫助在這些服務服、它的基礎環境和服務的消費者之間建立清晰的關係。 建立這種清晰的輪廓能幫助隔離問題,但也允許各模組相對其它部分獨立擴充套件。這類面向服務設計系統是非常類似面向物件設計程式設計的。

在我們的例子中,上傳和檢索影象的請求都是由同一個伺服器處理的;然而,因為系統需要具有伸縮性,有理由要將這兩個功能分解為各由自己的服務進行處理。

快速轉發(Fast-forward)假定服務處於大量使用中;在這種情況下就很容易看到,讀取影象所花的時間中有多少是由於受到了寫入操作的影響(因為 這兩個功能將競爭使用它們共享的資源)。取決於所採用的體系結構,這種影響可能是巨大的。即使上傳和下載的速度完全相同(在絕大多數IP網路中都不是這樣 的情況,大部分下載速度和上傳速度之比都至少設計為3:1),檔案讀取操作一般都是從快取記憶體中進行的,而寫操作卻不得不進行最終的磁碟操作(而且可能要 寫幾次才能達成最後的一致狀態)。即使所有內容都已在記憶體中,或者從磁碟(比如SSD磁碟)中進行讀取,資料庫寫入操作幾乎往往都要慢於讀取操作。 (Pole Position是一個開源的DB基準測試工具,http://polepos.org/,測試結果參見 http://polepos.sourceforge.net/results/PolePositionClientServer.pdf

這種設計另一個潛在的問題出在web伺服器上,像Apache或者lighttpd通常都有一個能夠維持的併發連線數上限(預設情況下在500左右,不過 可以更高)和最高流量數,它們會很快被寫操作消耗掉。因為讀操作可以非同步進行,或者採用其它一些像gizp壓縮的效能優化或者塊傳輸編碼方式,web服務 器可以通過在多個請求服務之間切換來滿足比最大連線數更多的請求(一臺Apache的最大連線數設定為500,它每秒鐘提供近千次讀請求服務也是正常 的)。寫操作則不同,它需要在上傳過程中保持連線,所以大多數家庭網路環境下,上傳一個1MB的檔案可能需要超過1秒的時間,所以web伺服器只能處理 500個這樣併發寫操作請求。

對於這種瓶頸,一個好的規劃案例是將讀取和寫入圖片分離為兩個獨立的服務,如圖Figure 1.2.所示。這讓我們可以單獨的擴充套件其中任意一個(因為有可能我們讀操作比寫操作要頻繁很多),同時也有助於我們理清每個節點在做什麼。最後,這也避免 了未來的憂慮,這使得故障診斷和查詢問題更簡單,像慢讀問題。

這種方法的優點是我們能夠單獨的解決各個模組的問題-我們不用擔心寫入和檢索新圖片在同一個上下文環境中。這兩種服務仍然使用全球資料庫的圖片,但是它們 可通過適當的服務介面自由優化它們自己的效能(比如,請求佇列,或者快取熱點圖片-在這之上的優化)。從維護和成本角度來看,每個服務按需進行獨立規模的 規劃,這點非常有用,試想如果它們都組合混雜在一起,其中一個無意間影響到了效能,另外的也會受影響。

當然,上面的例子在你使用兩個不同端點時可以很好的工作(事實上,這非常類似於雲端儲存和內容分發網路)。雖然有很多方式來解決這樣的瓶頸,但每個都有各自的取捨。

比如,Flickr通過分配使用者訪問不同的分片解決這類讀/寫問題,每一個分片只可以處理一定數量的使用者,隨著使用者的增加更多的分片被新增到叢集上(參看“Flickr縮影”的描述http://mysqldba.blogspot.com/2008/04/mysql-uc-2007-presentation-file.html)。 在第一個例子中,可以根據實際用途更簡單的規劃硬體資源(在整個系統中讀和寫的比例),然而,Flickr規劃是根據使用者基數(假定每個使用者擁有相同的資 源空間)。在前者中一個故障或者問題會導致整個系統功能的下降(比如,全部不能寫入檔案了),然而Flickr一個分片的故障只會影響到相關的那部分用 戶。在第一個例子中,更容易操作整個資料集-比如,在所有的影象元資料上更新寫入服務用來包含新的元資料或者檢索-然而在Flickr架構上每一個分片都 需要執行更新或者檢索(或者需要建立個索引服務來核對元資料-找出哪一個才是實際結果)。

冗餘(Redundancy)

為了優雅的處理故障,web架構必須冗餘它的服務和資料。例如,單伺服器只擁有單檔案的話,檔案丟失就意味這永遠丟失了。丟失資料是個很糟糕的事情,常見的方法是建立多個或者冗餘備份。

同樣的原則也適用於服務。如果應用有一個核心功能,確保它同時執行多個備份或者版本可以安全的應對單點故障。

在系統中建立冗餘可以消除單點故障,可以在緊急時刻提供備用功能。例如,如果在一個產品中同時執行服務的兩個例項,當其中一個發生故障或者降級(degrade),系統可以轉移(failover)到好的那個備份上。故障轉移(Failover)可以自動執行或者人工手動干預。

服務冗餘的另一個關鍵部分是建立無共享(shared-nothing)架構。採用這種架構,每個接點都可以獨立的運作,沒有中心”大腦”管理狀態或者協調活動。這可以大大提高可伸縮性(scalability)因為新的接點可以隨時加入而不需要特殊的條件或者知識。而且更重要的是,系統沒有單點故障。所以可以更好的應對故障。

例如,在我們的圖片服務應用,所有的圖片應該都冗餘備份在另外的一個硬體上(理想的情況下,在不同的地理位置,以防資料中心發生大災難,例如地震,火 災),而且訪問圖片的服務(見Figure 1.3.)-包括所有潛在的服務請求-也應該冗餘。(負載均衡器是個很好的方法冗餘服務,但是下面的方法不僅僅是負載均衡)

Figure 1.3: 使用冗餘的圖片儲存

分割槽

我們可能遇見單一伺服器無法存放的龐大資料集。也可能遇到一個需要過多計算資源的操作,導致效能下降,急需增添容量。這些情況下,你都有兩種選擇:橫向或縱向擴充套件。

縱向擴充套件意味著對單一伺服器增添更多資源。對於一個非常龐大的資料集,這可能意味著為單一伺服器增加更多(或更大)的硬碟以存放整個資料集。而對於計算操 作,這可能意味著將操作移到一個擁有更快的 CPU 或 更大的記憶體的伺服器中。無論哪種情況,縱向擴充套件都是為了使單個伺服器能夠自己處理更多的方法。

另一方面,對於橫向擴充套件,則是增加更多的節點。例如龐大的資料集,你可以用第二個伺服器來存放部分資料;而對於計算操作,你可以切割計算,或是通過額外的 節點載入。想要充分的利用橫向擴充套件的優勢,你應該以內在的系統構架設計原則來實現,否則的話,實現的方法將會變成繁瑣的修改和切分操作。

說道橫向分割槽,更常見的技術是將你的服務分割槽,或分片。分割槽可以通過對每個功能邏輯集的分割分配而來;可以通過地域劃分,也可以通過類似付費 vs. 未付費使用者來區分。這種方式的優勢是可以通過增添容量來執行服務或實現資料儲存。

以我們的影象伺服器為例,將曾經儲存在單一的檔案伺服器的圖片重新儲存到多個檔案伺服器中是可以實現的,每個檔案伺服器都有自己惟一的圖片集。(見圖表 1.4。)這種構架允許系統將圖片儲存到某個檔案伺服器中,在伺服器都即將存滿時,像增加硬碟一樣增加額外的伺服器。這種設計需要一種能夠將檔名和存放 伺服器繫結的命名規則。一個影象的名稱可能是對映全部伺服器的完整雜湊方案的形式。或者可選的,每個影象都被分配給一個遞增的 ID,當用戶請求影象時,影象檢索服務只需要儲存對映到每個伺服器的 ID 範圍(類似索引)就可以了。

圖 1.4: 使用冗餘和分割槽實現的圖片儲存服務

當然,為多個伺服器分配資料或功能是充滿挑戰的。一個關鍵的問題就是資料區域性性;對於分散式系統,計算或操作的資料越相近,系統的效能越佳。因此,一個潛在的問題就是資料的存放遍佈多個伺服器,當需要一個數據時,它們並不在一起,迫使伺服器不得不為從網路中獲取資料而付出昂貴的效能代價。

另一個潛在的問題是不一致性。當多個不同的服務讀取和寫入同一共享資源時,有可能會遭遇競爭狀態——某些資料應當被更新,但讀取操作恰 好發生在更新之前——這種情形下,資料就是不一致的。例如影象託管方案中可能出現的競爭狀態,一個客戶端傳送請求,將其某標題為“狗”的影象改名為”小家 夥“。而同時另一個客戶端傳送讀取此影象的請求。第二個客戶端中顯示的標題是“狗”還是“小傢伙”是不能明確的。

當然,對於分割槽還有一些障礙存在,但分割槽允許將問題——資料、負載、使用模式等——切割成可以管理的資料塊。這將極大的提高可擴充套件性和可管理性,但並非沒有風險。有很多可以降低風險,處理故障的方法;不過篇幅有限,不再贅述。若有興趣,可見於此文,獲取更多容錯和檢測的資訊。

1.3. 構建高效和可伸縮的資料訪問模組

在設計分散式系統時一些核心問題已經考慮到,現在讓我們來討論下比較困難的一部分:可伸縮的資料訪問。

對於大多數簡單的web應用程式,比如LAMP系統,類似於圖 Figure 1.5.

Figure 1.5: 簡單web應用程式

隨著它們的成長,主要發生了兩方面的變化:應用伺服器和資料庫的擴充套件。在一個高度可伸縮的應用程式中,應用伺服器通常最小化並且一般是 shared-nothing架構(譯註:shared nothing architecture是一 種分散式計算架構,這種架構中不存在集中儲存的狀態,整個系統中沒有資源競爭,這種架構具有非常強的擴張性,在web應用中廣泛使用)方式的體現,這使得 系統的應用伺服器層水平可伸縮。由於這種設計,資料庫伺服器可以支援更多的負載和服務;在這一層真正的擴充套件和效能改變開始發揮作用了。

剩下的章節主要集中於通過一些更常用的策略和方法提供快速的資料訪問來使這些型別服務變得更加迅捷。

Figure 1.6: Oversimplified web application

大多數系統簡化為如圖 Figure 1.6所示,這是一個良好的開始。如果你有大量的資料,你想快捷的訪問,就像一堆糖果擺放在你辦公室抽屜的最上方。雖然過於簡化,前面的聲明暗示了兩個困難的問題:儲存的可伸縮性和資料的快速訪問。

為了這一節內容,我們假設你有很大的資料儲存空間(TB),並且你想讓使用者隨機訪問一小部分資料(檢視Figure 1.7)。這類似於在影象應用的例子裡在檔案伺服器定位一個圖片檔案。

Figure 1.7: Accessing specific data

這非常具有挑戰性,因為它需要把數TB的資料載入到記憶體中;並且直接轉化為磁碟的IO。要知道從磁碟讀取比從記憶體讀取慢很多倍-記憶體的訪問速度如同敏捷的 查克·諾里斯(譯註:空手道冠軍),而磁碟的訪問速度就像笨重的卡車一樣。這個速度差異在大資料集上會增加更多;在實數順序讀取上記憶體訪問速度至少是磁碟 的6倍,隨機讀取速度比磁碟快100,000倍(參考“大資料之殤”http://queue.acm.org/detail.cfm?id=1563874)。另外,即使使用唯一的ID,解決獲取少量資料存放位置的問題也是個艱鉅的任務。這就如同不用眼睛看在你的糖果存放點取出最後一塊Jolly Rancher口味的糖果一樣。

謝天謝地,有很多方式你可以讓這樣的操作更簡單些;其中四個比較重要的是快取,代理,索引和負載均衡。本章的剩餘部分將討論下如何使用每一個概念來使資料訪問加快。

快取

快取利用區域性訪問原則:最近請求的資料可能會再次被請求。它們幾乎被用於計算機的每一層:硬體,作業系統,web瀏覽器,web應用程式等等。快取就像短期儲存的記憶體:它有空間的限制,但是通常訪問速度比源資料來源快並且包含了大多數最近訪問的條目。快取可以在架構的各個層級存在,但是常常在前端比較常見,在這裡通常需要在沒有下游層級的負擔下快速返回資料。

在我們的API例子中如何使用快取來快速訪問資料?在這種情況下,有兩個地方你可以插入快取。一個操作是在你的請求層節點新增一個快取,如圖 Figure 1.8.

Figure 1.8: Inserting a cache on your request layer node

直接在一個請求層節點配置一個快取可以在本地儲存相應資料。每次傳送一個請求到服務,如果資料存在節點會快速的返回本地快取的資料。如果資料不在快取中,請求節點將在磁碟查詢資料。請求層節點快取可以存放在記憶體和節點本地磁碟中(比網路儲存快些)。

Figure 1.9: Multiple caches

當你擴充套件這些節點後會發生什麼呢?如圖Figure 1.9所示,如果請求層擴充套件為多個節點,每個主機仍然可能有自己的快取。然而,如果你的負載均衡器隨機分配請求到節點,同樣的請求將指向不同的節點,從而 增加了快取的命中缺失率。有兩種選擇可以解決這個問題:全域性快取和分散式快取。

全域性快取

全域性快取顧名思義:所有的節點使用同一個快取空間,這涉及到新增一個伺服器,或者某種檔案儲存系統,速度比訪問源儲存和通過所有節點訪問要快些。每個請求 節點以同樣的方式查詢本地的一個快取,這種快取方案可能有點複雜,因為在客戶端和請求數量增加時它很容易被壓倒,但是在有些架構裡它還是很有用的(尤其是 那些專門的硬體來使全域性快取變得非常快,或者是固定資料集需要被快取的)。

在描述圖中有兩種常見形式的快取。在圖Figure 1.10中,當一個快取響應沒有在快取中找到時,快取自身從底層儲存中查找出資料。在 Figure 1.11中,當在快取中招不到資料時,請求節點會向底層去檢索資料。

Figure 1.10: Global cache where cache is responsible for retrieval

Figure 1.11: Global cache where request nodes are responsible for retrieval

大多數使用全域性快取的應用程式趨向於第一類,這類快取可以管理資料的讀取,防止客戶端大量的請求同樣的資料。然而,一些情況下,第二類實現方式似乎更有意 義。比如,如果一個快取被用於非常大的檔案,一個低命中比的快取將會導致緩衝區來填滿未命中的快取;在這種情況下,將使快取中有一個大比例的總資料集。另 一個例子是架構設計中檔案在快取中儲存是靜態的並且不會被排除。(這可能是因為應用程式要求周圍資料的延遲-某些片段的資料可能需要在大資料集中非常快- 在有些地方應用程式邏輯理清排除策略或者熱點 比快取方案好使些)

分散式快取

在分散式快取(圖1.12)中,每個節點都會快取一部分資料。如果把冰箱看作食雜店的快取的話,那麼分散式快取就象是把你的食物分別放到多個地方 —— 你的冰箱、櫃櫥以及便 當盒 ——放到這些便於隨時取用的地方就無需一趟趟跑去食雜店了。快取一般使用一個具有一致性的雜湊函式進行分割,如此便可在某請求節點尋找某資料時,能夠迅速 知道要到分散式快取中的哪個地方去找它,以確定改資料是否從快取中可得。在這種情況下,每個節點都有一個小型快取,在直接到原資料所作處找資料之前就可以 向別的節點發出尋找資料的請求。由此可得,分散式快取的一個優勢就是,僅僅通過向請求池中新增新的節點便可以擁有更多的快取空間。

分散式快取的一個缺點是修復缺失的節點。一些分散式快取系統通過在不同節點做多個備份繞過了這個問題;然而,你可以想象這個邏輯迅速變複雜了,尤其是當你 在請求層新增或者刪除節點時。即便是一個節點消失和部分快取資料丟失了,我們還可以在源資料儲存地址獲取-因此這不一定是災難性的!

Figure 1.12: Distributed cache

快取的偉大之處在於它們使我們的訪問速度更快了(當然前提是正確使用),你選擇的方法要在更多請求下更快才行。然而,所有這些快取的代價是必須有額外的存 儲空間,通常在放在昂貴的記憶體中;從來沒有嗟來之食。快取讓事情處理起來更快,而且在高負載情況下提供系統功能,否則將會使伺服器出現降級。

有一個很流行的開源快取專案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 伺服器上獲取儲存的資料。這使得他們在為使用者分配資料空間時有了更高的效能和吞吐量,同時有一箇中央伺服器做更新(這非常重要,因為當你執行上千伺服器 時,快取失效和一致性將是一個大挑戰)。

現在讓我們討論下當資料不在快取中時該如何處理···

代理

簡單來說,代理伺服器是一種處於客戶端和伺服器中間的硬體或軟體,它從客戶端接收請求,並將它們轉交給伺服器。代理一般用於過濾請求、記錄日誌或對請求進行轉換(增加/刪除頭部、加密/解密、壓縮,等等)。

圖1.13: 代理伺服器

當需要協調來自多個伺服器的請求時,代理伺服器也十分有用,它允許我們從整個系統的角度出發、對請求流量執行優化。壓縮轉發(collapsed forwarding)是利用代理加快訪問的其中一種方法,將多個相同或相似的請求壓縮在同一個請求中,然後將單個結果傳送給各個客戶端。

假設,有幾個節點都希望請求同一份資料,而且它並不在快取中。在這些請求經過代理時,代理可以通過壓縮轉發技術將它們合併成為一個請求,這樣一來,資料只 需要從磁碟上讀取一次即可(見圖1.14)。這種技術也有一些缺點,由於每個請求都會有一些時延,有些請求會由於等待與其它請求合併而有所延遲。不管怎麼 樣,這種技術在高負載環境中是可以幫助提升效能的,特別是在同一份資料被反覆訪問的情況下。壓縮轉發有點類似快取技術,只不過它並不對資料進行儲存,而是 充當客戶端的代理人,對它們的請求進行某種程度的優化。

在一個LAN代理伺服器中,客戶端不需要通過自己的IP連線到Internet,而代理會將請求相同內容的請求合併起來。這裡比較容易搞混,因為許多代理同時也充當快取(這裡也確實是一個很適合放快取的地方),但快取卻不一定能當代理。

圖1.14: 通過代理來合併請求

另一個使用代理的方式不只是合併相同資料的請求,同時也可以用來合併靠近儲存源(一般是磁碟)的資料請求。採用這種策略可以讓請求最大化使用本地資料,這 樣可以減少請求的資料延遲。比如,一群節點請求B部分資訊:partB1,partB2等,我們可以設定代理來識別各個請求的空間區域,然後把它們合併為 一個請求並返回一個bigB,大大減少了讀取的資料來源(檢視圖Figure 1.15)。當你隨機訪問上TB資料時這個請求時間上的差異就非常明顯了!代理在高負載情況下,或者限制使用快取時特別有用,因為它基本上可以批量的把多 個請求合併為一個。

Figure 1.15: Using a proxy to collapse requests for data that is spatially close together

值得注意的是,代理和快取可以放到一起使用,但通常最好把快取放到代理的前面,放到前面的原因和在參加者眾多的馬拉松比賽中最好讓跑得較快的選手在隊首起 跑一樣。因為快取從記憶體中提取資料,速度飛快,它並不介意存在對同一結果的多個請求。但是如果快取位於代理伺服器的另一邊,那麼在每個請求到達 cache之前都會增加一段額外的時延,這就會影響效能。

如果你正想在系統中新增代理,那你可以考慮的選項有很多;SquidVarnish都經過了實踐檢驗,廣泛用於很多實際的web站點中。這些代理解決方案針對大部分client-server通訊提供了大量的優化措施。將二者之中的某一個安裝為web伺服器層的反向代理(reverse proxy,下面負載均衡器一節中解釋)可以大大提高web伺服器的效能,減少處理來自客戶端的請求所需的工作量。

索引

使用索引快速訪問資料是個優化資料訪問效能公認的策略;可能我們大多數人都是從資料庫瞭解到的索引。索引用增長的儲存空間佔用和更慢的寫(因為你必須寫和更新索引)來換取更快的讀取。

你可以把這個概念應用到大資料集中就像應用在傳統的關係資料儲存。索引要關注的技巧是你必須仔細考慮使用者會怎樣訪問你的資料。如果資料集有很多 TBs,但是每個資料包(payload)很小(可能只有1KB),這時就必須用索引來優化資料訪問。在這麼大的資料集找到小的資料包是個很有挑戰性的工 作因為你不可能在合理的時間內遍歷所有資料。甚至,更有可能的是這麼大的資料集分佈在幾個(甚至很多個)物理裝置上-這意味著你要用些方法找到期望資料的 正確物理位置。索引是最適合的方法做這種事情。

Figure 1.16: Indexes

索引可以作為內容的一個表格-表格的每一項指明你的資料儲存的位置。例如,如果你正在查詢B的第二部分資料-你如何知道去哪裡找?如果你有個根據資料型別 (資料A,B,C)排序的索引,索引會告訴你資料B的起點位置。然後你就可以跳轉(seek)到那個位置,讀取你想要的資料B的第二部分。 (See Figure 1.16.)

這些索引常常儲存在記憶體中,或者儲存在對於客戶端請求來說非常快速的本地位置(somewhere very local)。Berkeley DBs (BDBs)和樹狀資料結構常常按順序儲存資料,非常理想用來儲存索引。

常常索引有很多層,當作資料地圖,把你從一個地方指向另外一個地方,一直到你的得到你想要的那塊資料。(See Figure 1.17.)

Figure 1.17: Many layers of indexes

索引也可以用來建立同樣資料的多個不同檢視(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 (see Inside 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或者自動提出失效節點(比如節點失去響應)。然而,這些高階特性會讓問題診斷難以進行。例如,當系統載荷 較大時,負載均衡器可能會移除慢速或者超時的節點(由於節點要處理大量請求),但對其它節點而言,這麼做實際上是加劇了情況的惡化程度。在這時進行大量的 監測非常重要,因為系統總體流量和吞吐率可能看上去是在下降(因為節點處理的請求變少了),但個別節點卻越來越忙得不可開交。

負載均衡器是一種能讓你擴充套件系統能力的簡單易行的方式,和本文中所講的其它技術一樣,它在分散式系統架構中起著基礎性的作用。負載均衡器還要提供一個比較 關鍵的功能,它必需能夠探測出節點的執行狀況,比如,如果一個節點失去響應或處於過載狀態,負載均衡器可以將其總處理請求的節點池中移除出去,還接著使用 系統中冗餘的其它不同節點。

佇列

目前為止我們已經介紹了許多更快讀取資料的方法,但另一個使資料層具伸縮性的重要部分是對寫的有效管理。當系統簡單的時候,只有最小的處理負載和很小的數 據庫,寫的有多快可以預知;然而,在更復雜的系統,寫可能需要幾乎無法決定的長久時間。例如,資料可能必須寫到不同資料庫或索引中的幾個地方,或者系統可 能正好處於高負載。這些情況下,寫或者任何那一類任務,有可能需要很長的時間,追求效能和可用性需要在系統中建立非同步;一個通常的做到那一點的辦法是通過 佇列。

Figure 1.20: Synchronous request

設想一個系統,每個客戶端都在發起一個遠端服務的任務請求。每一個客戶端都向伺服器傳送它們的請求,伺服器儘可能快的完成這些任務,並分別返回結果給各個 客戶端。在一個小型系統,一個伺服器(或邏輯服務)可以給傳入的客戶端請求提供迅速服務,就像它們來的一樣快,這種情形應該工作的很好。然而,當伺服器收 到了超過它所能處理數量的請求時,每個客戶端在產生一個響應前,將被迫等待其他客戶端的請求結束。這是一個同步請求的例子,示意在圖1.20。

這種同步的行為會嚴重的降低客戶端效能;客戶端被迫等待,有效的執行零工作,直到它的請求被應答。新增額外的伺服器承擔系統負載也不會解決這個問題;即使 是有效的負載均衡,為了最大化客戶端效能,保證平等的公平的分發工作也是極其困難的。而且,如果伺服器處理請求不可及,或者失敗了,客戶端上行也會失敗。 有效解決這個問題在於,需要在客戶端請求與實際的提供服務的被執行工作之間建立抽象。

圖 1.21:用佇列管理請求

進入佇列。一個佇列就像它聽起來那麼簡單:一個任務進入,被加入佇列然後工人們只要有能力去處理就會拿起下一個任務。(看圖1.21)這些任務可能是代表 了簡單的寫資料庫,或者一些複雜的事情,像為一個文件生成一個縮略預覽圖一類的。當一個客戶端提交一個任務請求到一個佇列,它們再也不會被迫等待結果;它 們只需要確認請求被正確的接收了。這個確認之後可能在客戶端請求的時候,作為一個工作結果的參考。

佇列使客戶端能以非同步的方式工作,提供了一個客戶端請求與其響應的戰略抽象。換句話說,在一個同步系統,沒有請求與響應的區別,因此它們不能被單獨的管 理。在一個非同步的系統,客戶端請求一個任務,服務端響應一個任務已收到的確認,然後客戶端可以週期性的檢查任務的狀態,一旦它結束就請求結果。當客戶端等 待一個非同步的請求完成,它可以自由執行其它工作,甚至非同步請求其它的服務。後者是佇列與訊息在分散式系統如何成為槓桿的例子。

佇列也對服務中斷和失敗提供了防護。例如,建立一個高度強健的佇列,這個佇列能夠重新嘗試由於瞬間伺服器故障而失敗的服務請求,是非常容易的事。相比直接暴露客戶端於間歇性服務中斷,需要複雜的而且經常矛盾的客戶端錯誤處理程式,用一個佇列去加強服務質量的擔保更為可取。

佇列對管理任何大規模分散式系統不同部分之間的分散式通訊,是一個基礎,而且實現它們有許多的方法。有不少開源的佇列如 RabbitMQActiveMQBeanstalkD,但是有些也用像 Zookeeper的服務,或者甚至像Redis的資料儲存。

1.4. 結論

設計有效的系統來進行快速的大資料訪問是有趣的,同時有大量的好工具來幫助各種各樣的應用程式進行設計。 這文章只覆蓋了一些例子,僅僅是一些表面的東西,但將會越來越多–同時在這個領域裡一定會繼續有更多創新東西。

以上根據網路內容整理而成!