facebook海量圖片存儲系統與淘寶TFS系統比較

經典論文翻譯導讀之《Finding a needle in Haystack: Facebook’s photo storage》

【譯者預讀】面對海量小文件的存儲和檢索,Google發表了GFS,淘寶開源了TFS,而Facebook又是如何應對千億級別的圖片存儲、每秒百萬級別的圖片查詢?Facebook與一樣提供了海量圖片服務的淘寶,解決方案有何異同?本篇文章,爲您揭曉。html

本篇論文的原文可謂通俗易懂、行雲流水、結構清晰、圖文並茂……正如做者所說的——"替換Facebook的圖片存儲系統就像高速公路上給汽車換輪子,咱們沒法去追求完美的設計……咱們花費了不少的注意力來保持它的簡單",本篇論文也是同樣,沒有牽扯空洞的龐大架構、也沒有晦澀零散的陳述,有的是對痛點的反思,對目標的分解,條理清晰,循序漸進。既描述了宏觀的總體流程,又推導了細節難點的技術突破過程。以致於譯者都不須要在文中插入過多備註和解讀了^_^。不過在文章末尾,譯者以淘寶的解決方案做爲對比,闡述了文章中的一些精髓的突破點,以供讀者參考。node

摘要

本篇論文描述了Haystack,一個爲Facebook的照片應用而專門優化定製的對象存儲系統。Facebook當前存儲了超過260 billion的圖片,至關於20PB的數據。用戶每一個星期還會上傳1 billion的新照片(60TB),Facebook在峯值時需提供每秒查詢超過1 million圖片的能力。相比咱們之前的方案(基於NAS和NFS),Haystack提供了一個成本更低的、性能更高的解決方案。咱們觀察到一個很是關鍵的問題:傳統的設計由於元數據查詢而致使了過多的磁盤操做。咱們不遺餘力的減小每一個圖片的元數據,讓Haystack能在內存中執行全部的元數據查詢。這個突破讓系統騰出了更多的性能來讀取真實的數據,增長了總體的吞吐量。web

 

1 介紹

分享照片是Facebook最受歡迎的功能之一。迄今爲止,用戶已經上傳了超過65 billion的圖片,使得Facebook成爲世界上最大的圖片分享網站。對每一個上傳的照片,Facebook生成和存儲4種不一樣大小的圖片(好比在某些場景下只需展現縮略圖),這就產生了超過260 billion張圖片、超過20PB的數據。用戶每一個星期還在上傳1 billion的新照片(60TB),Facebook峯值時須要提供每秒查詢1 million張圖片的能力。這些數字將來還會不斷增加,圖片存儲對Facebook的基礎設施提出了一個巨大的挑戰。sql

這篇論文介紹了Haystack的設計和實現,它已做爲Facebook的圖片存儲系統投入生產環境24個月了。Haystack是一個爲Facebook上分享照片而設計的對象存儲技術,在這個應用場景中,每一個數據只會寫入一次、讀操做頻繁、從不修改、不多刪除。在Facebook遭遇的負荷下,傳統的文件系統性能不好,優化定製出Haystack是大勢所趨。數據庫

根據咱們的經驗,傳統基於POSIX的文件系統的缺點主要是目錄和每一個文件的元數據。對於圖片應用,不少元數據(好比文件權限),是無用的並且浪費了不少存儲容量。並且更大的性能消耗在於文件的元數據必須從磁盤讀到內存來定位文件。文件規模較小時這些花費可有可無,然而面對幾百billion的圖片和PB級別的數據,訪問元數據就是吞吐量瓶頸所在。這是咱們從以前(NAS+NFS)方案中總結的血的教訓。一般狀況下,咱們讀取單個照片就須要好幾個磁盤操做:一個(有時候更多)轉換文件名爲inode number,另外一個從磁盤上讀取inode,最後一個讀取文件自己。簡單來講,爲了查詢元數據使用磁盤I/O是限制吞吐量的重要因素。在實際生產環境中,咱們必須依賴內容分發網絡(CDN,好比Akamai)來支撐主要的讀取流量,即便如此,文件元數據的大小和I/O一樣對總體系統有很大影響。瀏覽器

瞭解傳統途徑的缺點後,咱們設計了Haystack來達到4個主要目標:緩存

  • 高吞吐量和低延遲。咱們的圖片存儲系統必須跟得上海量用戶查詢請求。超過處理容量上限的請求,要麼被忽略(對用戶體驗是不可接受的),要麼被CDN處理(成本昂貴並且可能遭遇一個性價比轉折點)。想要用戶體驗好,圖片查詢必須快速。Haystack但願每一個讀操做至多須要一個磁盤操做,基於此才能達到高吞吐量和低延遲。爲了實現這個目標,咱們不遺餘力的減小每一個圖片的必需元數據,而後將全部的元數據保存在內存中。安全

  •  容錯。在大規模系統中,故障天天都會發生。儘管服務器崩潰和硬盤故障是不可避免的,也毫不能夠給用戶返回一個error,哪怕整個數據中心都停電,哪怕一個跨國網絡斷開。因此,Haystack複製每張圖片到地理隔離的多個地點,一臺機器倒下了,多臺機器會替補上來。服務器

  •  高性價比。Haystack比咱們以前(NAS+NFS)方案性能更好,並且更省錢。咱們按兩個維度來衡量:每TB可用存儲的花費、每TB可用存儲的讀取速度。相對NAS設備,Haystack每一個可用TB省了28%的成本,每秒支撐了超過4倍的讀請求。cookie

  •  簡單。替換Facebook的圖片存儲系統就像高速公路上給汽車換輪子,咱們沒法去追求完美的設計,這會致使實現和維護都很是耗時耗力。Haystack是一個新系統,缺少多年的生產環境級別的測試。咱們花費了不少的注意力來保持它的簡單,因此構建和部署一個可工做的Haystack只花了幾個月而不是好幾年。

本篇文章3個主要的貢獻是:

  • Haystack,一個爲高效存儲和檢索billion級別圖片而優化定製的對象存儲系統。

  • 構建和擴展一個低成本、高可靠、高可用圖片存儲系統中的經驗教訓。

  • 訪問Facebook照片分享應用的請求的特徵描述

文章剩餘部分結構以下。章節2闡述了背景、突出了以前架構遇到的挑戰。章節3描述了Haystack的設計和實現。章節4描述了各類圖片讀寫場景下的系統負載特徵,經過實驗數據證實Haystack達到了設計目標。章節5是對比和相關工做,以及章節6的總結。

 

2 背景 & 個人前任

在本章節,咱們將描述Haystack以前的架構,突出其主要的經驗教訓。因爲文章大小限制,一些細節就不細述了。

 

2.1 背景

咱們先來看一個概覽圖,它描述了一般的設計方案,web服務器、CDN和存儲系統如何交互協做,來實現一個熱門站點的圖片服務。圖1描述了從用戶訪問包含某個圖片的頁面開始,直到她最終從磁盤的特定位置下載此圖片結束的全過程。訪問一個頁面時,用戶的瀏覽器首先發送HTTP請求到一個web服務器,它負責生成markup以供瀏覽器渲染。對每張圖片,web服務器爲其構造一個URL,引導瀏覽器在此位置下載圖片數據。對於熱門站點,這個URL一般指向一個CDN。若是CDN緩存了此圖片,那麼它會馬上將數據回覆給瀏覽器。不然,CDN檢查URL,URL中須要嵌入足夠的信息以供CDN從本站點的存儲系統中檢索圖片。拿到圖片後,CDN更新它的緩存數據、將圖片發送回用戶的瀏覽器。

 

2.2 基於NFS的設計

在咱們最初的設計中,咱們使用了一個基於NFS的方案。咱們吸收的主要教訓是,對於一個熱門的社交網絡站點,只有CDN不足覺得圖片服務提供一個實用的解決方案。對於熱門圖片,CDN確實很高效——好比我的信息圖片和最近上傳的照片——可是一個像Facebook的社交網絡站點,會產生大量的對不熱門(較老)內容的請求,咱們稱之爲long tail(長尾理論中的名詞)。long tail的請求也佔據了很大流量,它們都須要訪問更下游的圖片存儲主機,由於這些請求在CDN緩存裏基本上都會命中失敗。緩存全部的圖片是能夠解決此問題,但這麼作代價太大,須要極大容量的緩存。

基於NFS的設計中,圖片文件存儲在一組商用NAS設備上,NAS設備的卷被mount到Photo Store Server的NFS上。圖2展現了這個架構。Photo Store Server解析URL得出卷和完整的文件路徑,在NFS上讀取數據,而後返回結果到CDN。

咱們最初在NFS卷的每一個目錄下存儲幾千個文件,致使讀取文件時產生了過多的磁盤操做,哪怕只是讀單個圖片。因爲NAS設備管理目錄元數據的機制,放置幾千個文件在一個目錄是極其低效的,由於目錄的blockmap太大不能被設備有效的緩存。所以檢索單個圖片均可能須要超過10個磁盤操做。在減小到每一個目錄下幾百個圖片後,系統仍然大概須要3個磁盤操做來獲取一個圖片:一個讀取目錄元數據到內存、第二個裝載inode到內存、最後讀取文件內容。

爲了繼續減小磁盤操做,咱們讓圖片存儲服務器明確的緩存NAS設備返回的文件"句柄"。第一次讀取一個文件時,圖片存儲服務器正常打開一個文件,將文件名與文件"句柄"的映射緩存到memcache中。同時,咱們在os內核中添加了一個經過句柄打開文件的接口,當查詢被緩存的文件時,圖片存儲服務器直接用此接口和"句柄"參數打開文件。遺憾的是,文件"句柄"緩存改進不大,由於越冷門的圖片越難被緩存到(沒有解決long tail問題)。值得討論的是能夠將全部文件"句柄"緩存到memcache,不過這也須要NAS設備能緩存全部的inode信息,這麼作是很是昂貴的。總結一下,咱們從NAS方案吸收的主要教訓是,僅針對緩存——無論是NAS設備緩存仍是額外的像memcache緩存——對減小磁盤操做的改進是有限的。存儲系統終究是要處理long tail請求(不熱門圖片)。

 

2.3 討論

咱們很難提出一個指導方針關於什麼時候應該構建一個自定義的存儲系統。下面是咱們在最終決定搭建Haystack以前的一些思考,但願能給你們提供參考。

面對基於NFS設計的瓶頸,咱們探討了是否能夠構建一個相似GFS的系統。而咱們大部分用戶數據都存儲在Mysql數據庫,文件存儲主要用於開發工做、日誌數據以及圖片。NAS設備其實爲這些場景提供了性價比很好的方案。此外,咱們補充了hadoop以供海量日誌數據處理。面對圖片服務的long tail問題,Mysql、NAS、Hadoop都不太合適。

咱們面臨的困境可簡稱爲"已存在存儲系統缺少合適的RAM-to-disk比率"。然而,沒有什麼比率是絕對正確的。系統須要足夠的內存才能緩存全部的文件系統元數據。在咱們基於NAS的方案中,一個圖片對應到一個文件,每一個文件須要至少一個inode,這已經佔了幾百byte。提供足夠的內存太昂貴。因此咱們決定構建一個定製存儲系統,減小每一個圖片的元數據總量,以便能有足夠的內存。相對購買更多的NAS設備,這是更加可行的、性價比更好的方案。

 

3 設計和實現

Facebook使用CDN來支撐熱門圖片查詢,結合Haystack則解決了它的long tail問題。若是web站點在查詢靜態內容時遇到I/O瓶頸,傳統方案就是使用CDN,它爲下游的存儲系統擋住了絕大部分的查詢請求。在Facebook,爲了傳統的、廉價的的底層存儲不受I/O擺佈,CDN每每須要緩存難以置信的海量靜態內容。

上面已經論述過,在不久的未來,CDN也不能徹底的解決咱們的問題,因此咱們設計了Haystack來解決這個嚴重瓶頸:磁盤操做。咱們接受long tail請求必然致使磁盤操做的現實,可是會盡可能減小除了訪問真實圖片數據以外的其餘操做。Haystack有效的減小了文件系統元數據的空間,並在內存中保存全部元數據。

每一個圖片存儲爲一個文件將會致使元數據太多,難以被所有緩存。Haystack的對策是:將多個圖片存儲在單個文件中,控制文件個數,維護大型文件,咱們將論述此方案是很是有效的。另外,咱們強調了它設計的簡潔性,以促進快速的實現和部署。咱們將以此核心技術展開,結合它周邊的全部架構組件,描述Haystack是如何實現了一個高可靠、高可用的存儲系統。在下面對Haystack的介紹中,須要區分兩種元數據,不要混淆。一種是應用元數據,它是用來爲瀏覽器構造檢索圖片所需的URL;另外一種是文件系統元數據,用於在磁盤上檢索文件。

 

3.1 概覽

Haystack架構包含3個核心組件:Haytack Store、Haystack Directory和Haystack Cache(簡單起見咱們下面就不帶Haystack前綴了)。Store是持久化存儲系統,並負責管理圖片的文件系統元數據。Store將數據存儲在物理的捲上。好比,在一臺機器上提供100個物理卷,每一個提供100GB的存儲容量,整臺機器則能夠支撐10TB的存儲。更進一步,不一樣機器上的多個物理卷將對應一個邏輯卷。Haystack將一個圖片存儲到一個邏輯卷時,圖片被寫入到全部對應的物理卷。這個冗餘可避免因爲硬盤故障,磁盤控制器bug等致使的數據丟失。Directory維護了邏輯到物理卷的映射以及其餘應用元數據,好比某個圖片寄存在哪一個邏輯卷、某個邏輯卷的空閒空間等。Cache的功能相似咱們系統內部的CDN,它幫Store擋住熱門圖片的請求(能夠緩存的就毫不交給下游的持久化存儲)。在獨立設計Haystack時,咱們要設想它處於一個沒有CDN的大環境中,即便有CDN也要預防其節點故障致使大量請求直接進入存儲系統,因此Cache是十分必要的。

圖3說明了Store、Directory、Cache是如何協做的,以及如何與外部的瀏覽器、web服務器、CDN和存儲系統交互。在Haystack架構中,瀏覽器會被引導至CDN或者Cache上。須要注意的是Cache本質上也是一個CDN,爲了不困惑,咱們使用"CDN"表示外部的系統、使用"Cache"表示咱們內部的系統。有一個內部的緩存設施能減小對外部CDN的依賴。

當用戶訪問一個頁面,web服務器使用Directory爲每一個圖片來構建一個URL(Directory中有足夠的應用元數據來構造URL)。URL包含幾塊信息,每一塊內容能夠對應到從瀏覽器訪問CDN(或者Cache)直至最終在一臺Store機器上檢索到圖片的各個步驟。一個典型的URL以下:

http://<cdn>/<cache>/<machine id="">/<logical volume,="" photo="">

第一個部分<cdn>指明瞭從哪一個CDN查詢此圖片。到CDN後它使用最後部分的URL(邏輯卷和圖片ID)便可查找緩存的圖片。若是CDN未命中緩存,它從URL中刪除<cdn>相關信息,而後訪問Cache。Cache的查找過程與之相似,若是還沒命中,則去掉<cache>相關信息,請求被髮至指定的Store機器(<machine id="">)。若是請求不通過CDN直接發至Cache,其過程與上述相似,只是少了CDN這個環節。

圖4說明了在Haystack中的上傳流程。用戶上傳一個圖片時,她首先發送數據到web服務器。web服務器隨後從Directory中請求一個可寫邏輯卷。最後,web服務器爲圖片分配一個惟一的ID,而後將其上傳至邏輯卷對應的每一個物理卷。

 

3.2 Haystack Directory

Directory提供4個主要功能。首先,它提供一個從邏輯捲到物理卷的映射。web服務器上傳圖片和構建圖片URL時都須要使用這個映射。第二,Directory在分配寫請求到邏輯卷、分配讀請求到物理卷時需保證負載均衡。第三,Directory決定一個圖片請求應該被髮至CDN仍是Cache,這個功能可讓咱們動態調整是否依賴CDN。第四,Directory指明那些邏輯卷是隻讀的(只讀限制多是源自運維緣由、或者達到存儲容量上限;爲了運維方便,咱們以機器粒度來標記卷的只讀)。

當咱們增長新機器以增大Store的容量時,那些新機器是可寫的;僅僅可寫的機器會收到upload請求。隨時間流逝這些機器的可用容量會不斷減小。當一個機器達到容量上限,咱們標記它爲只讀,在下一個子章節咱們將討論如何這個特性如何影響Cache和Store。

Directory將應用元數據存儲在一個冗餘複製的數據庫,經過一個PHP接口訪問,也能夠換成memcache以減小延遲。當一個Store機器故障、數據丟失時,Directory在應用元數據中刪除對應的項,新Store機器上線後則接替此項。

 【譯者YY】3.2章節是整篇文章中惟一一處譯者認爲沒有解釋清楚的環節。結合3.1章節中的URL結構解析部分,讀者能夠發現Directory須要拿到圖片的"原始URL"(頁面html中link的URL),再結合應用元數據,就能夠構造出"引導URL"以供下游使用。從3.2中咱們知道Directory必然保存了邏輯捲到物理卷的映射,僅用此映射+原始URL足夠發掘其餘應用元數據嗎?原始URL中到底包含了什麼信息(論文中沒看到介紹)?咱們能夠作個假設,假如原始URL中僅僅包含圖片ID,那Directory如何得知它對應哪一個邏輯卷(必須先完成這一步映射,才能繼續挖掘更多應用元數據)?Directory是否在upload階段將圖片ID與邏輯卷的映射也保存了?若是是,那這個映射的數據量不能忽略不計,論文也不應一筆帶過。

從原文一些細枝末節的描述中,譯者認爲Directory確實保存了不少與圖片ID相關的元數據(存儲在哪一個邏輯卷、cookie等)。但整篇論文譯者也沒找到對策,總感受這樣性價比過低,不符合Haystack的做風。對於這個疑惑,文章末尾擴展閱讀部分將嘗試解答。讀者先認爲其具有此能力吧。

3.3 Haystack Cache

Cache會從CDN或者直接從用戶瀏覽器接收到圖片查詢請求。Cache的實現可理解爲一個分佈式Hash Table,使用圖片ID做爲key來定位緩存的數據。若是Cache未命中,Cache則根據URL從指定Store機器上獲取圖片,視狀況回覆給CDN或者用戶瀏覽器。

咱們如今強調一下Cache的一個重要行爲概念。只有當符合兩種條件之一時它纔會緩存圖片:(a)請求直接來自用戶瀏覽器而不是CDN;(b)圖片獲取自一個可寫的Store機器。第一個條件的理由是一個請求若是在CDN中沒命中(非熱門圖片),那在咱們內部緩存也不太須要命中(即便此圖片開始逐漸活躍,那也能在CDN中命中緩存,這裏無需畫蛇添足;直接的瀏覽器請求說明是不通過CDN的,那就須要Cache代爲CDN,爲其緩存)。第二個條件的理由是間接的,有點經驗論,主要是爲了保護可寫Store機器;緣由挺有意思,大部分圖片在上傳以後很快會被頻繁訪問(好比某個美女新上傳了一張自拍),並且文件系統在只有讀或者只有寫的狀況下執行的更好,不太喜歡同時併發讀寫(章節4.1)。若是沒有Cache,可寫Store機器每每會遭遇頻繁的讀請求。所以,咱們甚至會主動的推送最近上傳的圖片到Cache。

 

3.4 Haystack Store

Store機器的接口設計的很簡約。讀操做只需提供一些很明確的元數據信息,包括圖片ID、哪一個邏輯卷、哪臺物理Store機器等。機器若是找到圖片則返回其真實數據,不然返回錯誤信息。

每一個Store機器管理多個物理卷。每一個物理卷存有百萬張圖片。讀者能夠將一個物理卷想象爲一個很是大的文件(100GB),保存爲'/hay/haystack<logical volume="" id="">'。Store機器僅須要邏輯卷ID和文件offset就能很是快的訪問一個圖片。這是Haystack設計的主旨:不須要磁盤操做就能夠檢索文件名、偏移量、文件大小等元數據。Store機器會將其下全部物理卷的文件描述符(open的文件"句柄",卷的數量很少,數據量不大)緩存在內存中。同時,圖片ID到文件系統元數據(文件、偏移量、大小等)的映射(後文簡稱爲"內存中映射")是檢索圖片的重要條件,也會所有緩存在內存中。

如今咱們描述一下物理卷和內存中映射的結構。一個物理卷能夠理解爲一個大型文件,其中包含一系列的needle。每一個needle就是一張圖片。圖5說明了卷文件和每一個needle的格式。Table1描述了needle中的字段。

爲了快速的檢索needle,Store機器須要爲每一個卷維護一個內存中的key-value映射。映射的Key就是(needle.key+needle.alternate_key)的組合,映射的Value就是needle的flag、size、卷offset(都以byte爲單位)。若是Store機器崩潰、重啓,它能夠直接分析卷文件來從新構建這個映射(構建完成以前不處理請求)。下面咱們介紹Store機器如何響應讀寫和刪除請求(Store僅支持這些操做)。

【譯者注】從Table1咱們看到needle.key就是圖片ID,爲什麼僅用圖片ID作內存中映射的Key還不夠,還須要一個alternate_key?這是由於一張照片會有4份副本,它們的圖片ID相同,只是類型不一樣(好比大圖、小圖、縮略圖等),因而將圖片ID做爲needle.key,將類型做爲needle.alternate_key。根據譯者的理解,內存中映射不是一個簡單的HashMap結構,而是相似一個兩層的嵌套HashMap,Map<long *needle.key*="" ,map<int="" *alternate_key*="" ,object="">>。這樣作可讓4個副本共用同一個needle.key,避免爲重複的內容浪費內存空間。

 

3.4.1 讀取圖片

Cache機器向Store機器請求一個圖片時,它須要提供邏輯卷id、key、alternate key,和cookie。cookie是個數字,嵌在URL中。當一張新圖片被上傳,Directory爲其隨機分配一個cookie值,並做爲應用元數據之一存儲在Directory。它就至關於一張圖片的"私人密碼",此密碼能夠保證全部發往Cache或CDN的請求都是通過Directory"批准"的(Cache和Store都持有圖片的cookie,若用戶本身在瀏覽器中僞造、猜想URL或發起攻擊,則會由於cookie不匹配而失敗,從而保證Cache、Store能放心處理合法的圖片請求)。

當Store機器接收到Cache機器發來的圖片查詢請求,它會利用內存中映射快速的查找相關的元數據。若是圖片沒有被刪除,Store則在卷文件中seek到相應的offset,從磁盤上讀取整個needle(needle的size能夠提早計算出來),而後檢查cookie和數據完整性,若所有合法則將圖片數據返回到Cache機器。

 

3.4.2 寫入圖片

上傳一個圖片到Haystack時,web服務器向Directory諮詢獲得一個可寫邏輯卷及其對應的多臺Store機器,隨後直接訪問這些Store機器,向其提供邏輯卷id、key、alternate key、cookie和真實數據。每一個Store機器爲圖片建立一個新needle,append到相應的物理卷文件,更新內存中映射。過程很簡單,可是append-only策略不能很好的支持修改性的操做,好比旋轉(圖片順時針旋轉90度之類的)。Haystack並不容許覆蓋needle,因此圖片的修改只能經過添加一個新needle,其擁有相同的key和alternate key。若是新needle被寫入到與老needle不一樣的邏輯卷,則只須要Directory更新它的應用元數據,將來的請求都路由到新邏輯卷,不會獲取老版本的數據。若是新needle寫入到相同的邏輯卷,Store機器也只是將其append到相同的物理卷中。Haystack利用一個十分簡單的手段來區分重複的needle,那就是判斷它們的offset(新版本的needle確定是offset最高的那個),在構造或更新內存中映射時若是遇到相同的needle,則用offset高的覆蓋低的。

 

3.4.3 圖片刪除

在刪除圖片時,Store機器將內存中映射和卷文件中相應的flag同步的設置爲已刪除(軟刪除機制,此刻不會刪除needle的磁盤數據)。當接收到已刪除圖片的查詢請求,Store會檢查內存中flag並返回錯誤信息。值得注意的是,已刪除needle依然佔用的空間是個問題,咱們稍後將討論如何經過壓縮技術來回收已刪除needle的空間。

 

3.4.4 索引文件

Store機器使用一個重要的優化——索引文件——來幫助重啓初始化。儘管理論上一個機器能經過讀取全部的物理捲來從新構建它的內存中映射,但大量數據(TB級別)須要從磁盤讀取,很是耗時。索引文件容許Store機器快速的構建內存中映射,減小重啓時間。

Store機器爲每一個卷維護一個索引文件。索引文件能夠想象爲內存中映射的一個"存檔"。索引文件的佈局和卷文件相似,一個超級塊包含了一系列索引記錄,每一個記錄對應到各個needle。索引文件中的記錄與卷文件中對應的needle必須保證相同的存儲順序。圖6描述了索引文件的佈局,Table2解釋了記錄中的不一樣的字段。

使用索引幫助重啓稍微增長了系統複雜度,由於索引文件都是異步更新的,這意味着當前索引文件中的"存檔"可能不是最新的。當咱們寫入一個新圖片時,Store機器同步append一個needle到卷文件末尾,並異步append一個記錄到索引文件。當咱們刪除圖片時,Store機器在對應needle上同步設置flag,而不會更新索引文件。這些設計決策是爲了讓寫和刪除操做更快返回,避免附加的同步磁盤寫。可是也致使了兩方面的影響:一個needle可能沒有對應的索引記錄、索引記錄中沒法得知圖片已刪除。

咱們將對應不到任何索引記錄的needle稱爲"孤兒"。在重啓時,Store機器順序的檢查每一個孤兒,從新建立匹配的索引記錄,append到索引文件。咱們能快速的識別孤兒是由於索引文件中最後的記錄能對應到卷文件中最後的非孤兒needle。處理完孤兒問題,Store機器則開始使用索引文件初始化它的內存中映射。

因爲索引記錄中沒法得知圖片已刪除,Store機器可能去檢索一個實際上已經被刪除的圖片。爲了解決這個問題,能夠在Store機器讀取整個needle後檢查其flag,若標記爲已刪除,則更新內存中映射的flag,並回復Cache此對象未找到。

 

3.4.5 文件系統

Haystack能夠理解爲基於通用的類Unix文件系統搭建的對象存儲,可是某些特殊文件系統能更好的適應Haystack。好比,Store機器的文件系統應該不須要太多內存就可以在一個大型文件上快速的執行隨機seek。當前咱們全部的Store機器都在使用的文件系統是XFS,一個基於"範圍(extent)"的文件系統。XFS有兩個優點:首先,XFS中鄰近的大型文件的"blockmap"很小,可放心使用內存存儲;第二,XFS提供高效文件預分配,減輕磁盤碎片等問題。

使用XFS,Haystack能夠在讀取一張圖片時徹底避免檢索文件系統元數據致使的磁盤操做。可是這並不意味着Haystack能保證讀取單張圖片絕對只須要一個磁盤操做。在一些極端狀況下會發生額外的磁盤操做,好比當圖片數據跨越XFS的"範圍(extent)"或者RAID邊界時。不過Haystack會預分配1GB的"範圍(extent)"、設置RAID stripe大小爲256KB,因此實際上咱們不多遭遇這些極端場景。

 

3.5 故障恢復

對於運行在普通硬件上的大規模系統,容忍各類類型的故障是必須的,包括硬盤驅動故障、RAID控制器錯誤、主板錯誤等,Haystack也不例外。咱們的對策由兩個部分組成——一個爲偵測、一個爲修復。

爲了主動找到有問題的Store機器,咱們維護了一個後臺任務,稱之爲pitchfork,它週期性的檢查每一個Store機器的健康度。pitchfork遠程的測試到每臺Store機器的鏈接,檢查其每一個卷文件的可用性,並嘗試讀取數據。若是pitchfork肯定某臺Store機器沒經過這些健康檢查,它會自動標記此臺機器涉及的全部邏輯卷爲只讀。咱們的工程師將在線下人工的檢查根本故障緣由。

一旦確診,咱們就能快速的解決問題。不過在少數狀況下,須要執行一個更加嚴厲的bulk同步操做,此操做須要使用複製品中的卷文件重置某個Store機器的全部數據。Bulk同步發生的概率很小(每月幾回),並且過程比較簡單,只是執行很慢。主要的瓶頸在於bulk同步的數據量常常會遠遠超過單臺Store機器NIC速度,致使好幾個小時才能恢復。咱們正積極解決這個問題。

3.6 優化

Haystack的成功還歸功於幾個很是重要的細節優化。

3.6.1 壓縮

壓縮操做是直接在線執行的,它能回收已刪除的、重複的needle所佔據的空間。Store機器壓縮卷文件的方式是,逐個複製needle到一個新的卷文件,並跳過任何重複項、已刪除項。在壓縮時若是接收到刪除操做,兩個卷文件都需處理。一旦複製過程執行到卷文件末尾,全部對此卷的修改操做將被阻塞,新卷文件和新內存中映射將對前任執行原子替換,隨後恢復正常工做。

 

3.6.2 節省更多內存

上面描述過,Store機器會在內存中映射中維護一個flag,可是目前它只會用來標記一個needle是否已刪除,有點浪費。因此咱們經過設置偏移量爲0來表示圖片已刪除,物理上消除了這個flag。另外,映射Value中不包含cookie,當needle從磁盤讀出以後Store纔會進行cookie檢查。經過這兩個技術減小了20%的內存佔用。

當前,Haystack平均爲每一個圖片使用10byte的內存。每一個上傳的圖片對應4張副本,它們共用同一個key(佔64bits),alternate keys不一樣(佔32bits),size不一樣(佔16bits),目前佔用(64+(32+16)*4)/8=32個bytes。另外,對於每一個副本,Haystack在用hash table等結構時須要消耗額外的2個bytes,最終總量爲一張圖片的4份副本共佔用40bytes。做爲對比,一個xfs_inode_t結構在Linux中需佔用536bytes。

 

3.6.3 批量上傳

磁盤在執行大型的、連續的寫時性能要優於大量小型的隨機寫,因此咱們儘可能將相關寫操做捆綁批量執行。幸運的是,不少用戶都會上傳整個相冊到Facebook,而不是頻繁上傳單個圖片。所以只需作一些巧妙的安排就能夠捆綁批量upload,實現大型、連續的寫操做。

章節四、五、6是實驗和總結等內容,這裏再也不贅述了。

 

【擴展閱讀】

提到CDN和分佈式文件存儲就不得不提到淘寶,它的商品圖片不會少於Facebook的我的照片。其著名的CDN+TFS的解決方案因爲爲公司節省了鉅額的預算開支而得到創新大獎,團隊成員也獲得不菲的獎金(羨慕嫉妒恨)。淘寶的CDN技術作了很是多的技術創新和突破,不過並不是本文範疇,接下來的討論主要是針對Haystack與TFS在存儲、檢索環節的對比,並嘗試提取出此類場景常見的技術難點。(譯者對TFS的理解僅限於介紹文檔,如有錯誤望讀者矯正)

淘寶CDN、TFS的介紹請移步

<http: www.infoq.com="" cn="" presentations="" zws-taobao-image-store-cdn=""> 

http://tfs.taobao.org/index.html 

注意:下文中不少術語(好比應用元數據、Store、文件系統元數據等,都是基於本篇論文的上下文,請勿混淆)

上圖是整個CDN+TFS解決方案的全貌,對應本文就是圖3。CDN在前三層上實現了各類創新和技術突破,不過並不是本文焦點,這裏主要針對第四層Storage(淘寶的分佈式文件系統TFS),對比Haystack,看其是否也解決了long tail問題。下面是TFS的架構概覽:

從粗粒度的宏觀視角來看,TFS與Haystack的最大區別就是: TFS只care存儲層面,它沒有Haystack Cache組件;Haystack指望提供的是從瀏覽器、到CDN、到最終存儲的一整套解決方案,架構定位稍有不一樣,Haystack也是專門爲這種場景下的圖片服務所定製的,作了不少精細的優化;TFS的目標是通用分佈式文件存儲,除了CDN還會支持其餘各類場景。

究竟是定製一整套優化的解決方案,仍是使用通用分佈式文件存儲平臺強強聯手?Facebook的工程師也曾糾結過(章節2.3),這個沒有標準答案,各有所長,視狀況去選擇最合適的方案吧。

下面咱們以本文中關注的一些難點來對比一下雙方的實現:

1 存儲機器上的文件結構、文件系統元數據對策

Haystack的機器上維護了少許的大型物理卷文件,其中包含一系列needle來存儲小文件,同時needle的文件系統元數據被全量緩存、持久化"存檔"。

在TFS中(後文爲清晰起見,引用TFS文獻的內容都用淘寶最愛的橙色展現):

"……在TFS中,將大量的小文件(實際用戶文件)合併成爲一個大文件,這個大文件稱爲塊(Block)。TFS以Block的方式組織文件的存儲……"

"……!DataServer進程會給Block中的每一個文件分配一個ID(File ID,該ID在每一個Block中惟一),並將每一個文件在Block中的信息存放在和Block對應的Index文件中。這個Index文件通常都會所有load在內存……"

看來面對可憐的操做系統,你們都不忍心把海量的小文件直接放到它的文件系統上,合併成super block,維護super block中各entry的元數據和索引信息(並全量緩存),纔是王道。這裏TFS的Block應該對應到Haystack中的一個物理卷。

 

2 分佈式協調調度、應用元數據策略

Haystack在接收到讀寫請求時,依靠Directory分析應用元數據,再結合必定策略(如負載均衡、容量、運維、只讀、可寫等),決定請求被髮送到哪臺Store機器,並向Store提供足夠的存儲或檢索信息。Directory負責了總體分佈式環境的協調調度、應用元數據管理職能,並基於此幫助實現了系統的可擴展性、容錯性。

在TFS中:

"……!NameServer主要功能是: 管理維護Block和!DataServer相關信息,包括!DataServer加入,退出, 心跳信息, block和!DataServer的對應關係創建,解除。正常狀況下,一個塊會在!DataServer上存在, 主!NameServer負責Block的建立,刪除,複製,均衡,整理……"

"……每個Block在整個集羣內擁有惟一的編號,這個編號是由NameServer進行分配的,而DataServer上實際存儲了該Block。在!NameServer節點中存儲了全部的Block的信息……"

TFS中與Directory對應的就是NameServer了,職責大同小異,就是分佈式協調調度和應用元數據分配管理,並基於此實現系統的平滑擴容、故障容忍。下面專門討論一下這兩個重要特性。

3 擴展性

Haystack和TFS都基於(分佈式協調調度+元數據分配管理)實現了很是優雅的可擴展方案。咱們先回顧一下傳統擴展性方案中的那些簡單粗暴的方法。

最簡單最粗暴的場景:

如今有海量的數據,好比data [key : value],有100臺機器,經過一種策略讓這些數據能負載均衡的發給各臺機器。策略能夠是這樣,int index=Math.abs(key.hashCode)%100,這就獲得了一個惟一的、肯定的、[0,99]的序號,按此序號發給對應的某臺機器,最終能達到負載均衡的效果。此方案的粗暴顯而易見,當咱們新增機器後(好比100變成130),大部分老數據的key執行此策略後獲得的index會發生變化,這也就意味着對它們的檢索都會發往錯誤的機器,找不到數據。

稍微改進的場景是:

如今有海量的數據,好比data [key : value],我假想本身是高富帥,有一萬臺機器,一樣按照上述的策略進行路由。可是我只有100臺機器,這一萬臺是假想的,怎麼辦?先給它們一個稱號,叫虛擬節點(簡稱vnode,vnode的序號簡稱爲vnodeId),而後想辦法將vnode與真實機器創建多對一映射關係(每一個真實機器上100個vnode),這個辦法能夠是某種策略,好比故技重施對vnodeId%100獲得[0,99]的機器序號,或者在數據庫中建幾張表維護一下這個多對一的映射關係。在路由時,先按老辦法獲得vnodeId,再執行一次映射,找到真實機器。這個方案還須要一個架構假設:個人系統規模在5年內都不須要上漲到一萬臺機器(5年差很少了,像我等碼農估計一生也玩不了一萬臺機器的集羣吧),所以10000這個數字"永遠"不會變,這就保證了一個key永遠對應某個vnodeId,不會發生改變。而後在擴容時,咱們改變的是vnode與真實機器的映射關係,可是此映射關係一改,也會不可避免的致使數據命中失敗,由於必然會產生這樣的現象:某個vnodeId(v1)原先是對應機器A的,如今變成了機器B。可是相比以前的方案,如今已經好不少了,咱們能夠經過運維手段先阻塞住對v1的讀寫請求,而後執行數據遷移(以已知的vnode爲粒度,而不是千千萬萬個未知的data,這種遷移操做仍是能夠接受的),遷移完畢後新機器開始接收請求。作的更好一點,能夠不阻塞請求,想辦法作點容錯處理和寫同步之類的,能夠在線無痛的完成遷移。

上面兩個老方案還能夠加上一致性Hash等策略來儘可能避免數據命中失敗和數據遷移。可是始終逃避不了這樣一個公式:

int machine_id=function(data.key , x)

machine_id指最終路由到哪臺機器,function表明咱們的路由策略函數,data.key就是數據的key(數據ID之類的),x在第一個方案裏就是機器數量100,在第二個方案裏就是vnode數量+(vnode與機器的映射關係)。在這個公式裏,永遠存在了x這個未知數,一旦它風吹草動,function的執行結果就可能改變,因此它逃避不了命中失敗。

只有當公式變成下面這個,才能絕對避免:

 Map<data.key,final machine_id=""> map = xxx; 

int machine_id=map.get(data.key);

注意map只是個理論上的結構,這裏只是簡單的僞代碼,並不強制它是個簡單的<key-value>結構,它的結構可能會更復雜,可是不管怎麼複雜,此map都真實的、明確的存在,其效果都是——用data.key就能映射到machine_id,找到目標機器,無論是直接,仍是間接,反正不是用一個function去動態計算獲得。map裏的final不符合語法,加在這裏是想強調,此map一旦爲某個data.key設置了machine_id,就永不改變(起碼不會由於平常擴容而改變)。當增長機器時,此map的已有值也不會受到影響。這樣一個沒有未知數x的公式,才能保證新老數據來了都能根據key拿到一個永遠不變的machine_id,永遠命中成功。

所以咱們得出這樣一個結論,只要擁有這樣一個map,系統就能擁有很是優雅平滑的可擴展潛力。當系統擴容時,老的數據不會命中失敗,在分佈式協調調度的保證下,新的增量數據會更傾向於寫入新機器,整個集羣的負載會逐漸均衡。

很顯然Haystack和TFS都作到了,下面忽略其餘細節問題,着重討論一下它們是如何裝備上這個map的。

讀者回顧一下3.2章節留下的那個疑惑——原始URL中到底包含什麼信息,是否是隻有圖片ID?Directory到底需不須要維護圖片ID到邏輯卷的映射?

這個"圖片ID到邏輯卷的映射",就是咱們須要的map,用圖片ID(data.key)能get到邏輯卷ID(此值是upload時就明確分配的,不會改變),再間接從"邏輯捲到物理卷映射"中就能get到目標Store機器;不管是新增邏輯卷仍是新增物理卷,"圖片ID到邏輯卷的映射"中的已有值均可以不受影響。這些都符合map的行爲定義。

Haystack也所以,具有了十分優雅平滑的可擴展能力。可是譯者提到的疑惑並無解答——"這個映射(圖片ID到邏輯卷的映射)的數據量不能忽略不計,論文也不應一筆帶過"

做者提到過memcache,也許這就是相關的解決方案,此數據雖然不小,可是也沒大到望而生畏的地步。不過咱們依然能夠發散一下,假如Haystack沒保存這個映射呢?

這就意味着原始URL不僅包含圖片ID,還包含邏輯卷ID等必要信息。這樣也是遵循map的行爲定義的,即便map的信息沒有集中存儲在系統內,可是卻分散在各個原始URL中,依然存在。不可避免的,這些信息就要在upload階段返回給業務系統(好比Facebook的照片分享應用系統),業務系統須要理解、存儲和處理它們(隨後再利用它們組裝爲原始URL去查詢圖片)。這樣至關於把map的維護工做分擔給了各個用戶系統,這也是讓人十分痛苦的,致使了不可接受的耦合。

咱們能夠看看TFS的解決方案:

"……TFS的文件名由塊號和文件號經過某種對應關係組成,最大長度爲18字節。文件名固定以T開始,第二字節爲該集羣的編號(能夠在配置項中指定,取值範圍 1~9)。餘下的字節由Block ID和File ID經過必定的編碼方式獲得。文件名由客戶端程序進行編碼和解碼,它映射方式以下圖……"

"……根據TFS文件名解析出Block ID和block中的File ID.……dataserver會根據本地記錄的信息來獲得File ID所在block的偏移量,從而讀取到正確的文件內容……"

 一切,迎刃而解了…… 這個方案能夠稱之爲"結構化ID"、"聚合ID",或者是"命名規則大於配置"。當咱們糾結於僅僅有圖片ID不夠時,能夠給ID簡單的動動手腳,好比ID是long類型,8個byte,左邊給點byte用於存儲邏輯卷ID,剩下的用於存儲真實的圖片ID(某些場景下還能夠多截幾段給更多的元數據),因而既避免了保存大量的映射數據,又避免了增長系統間的耦合,魚和熊掌兼得。不過這個方案對圖片ID有所約束,也不支持自定義的圖片名稱,針對這個問題,TFS在新版本中:

 "……metaserver是咱們在2.0版本引進的一個服務. 用來存儲一些元數據信息, 這樣本來不支持自定義文件名的 TFS 就能夠在 metaserver 的幫助下, 支持自定義文件名了.……"

此metaserver的做用無疑就和Directory中部分應用元數據相關的職責相似了。我的認爲能夠二者結合左右開弓,畢竟自定義文件名這種需求應該不是主流。

值得商榷的是,全量保存這部分應用元數據其實仍是有不少好處的,最典型的就是順帶保存的cookie,有效的幫助Haystack不受僞造URL攻擊的困擾,這個問題不知道TFS是如何解決的(大量的文件檢索異常勢必會影響系統性能)。若是Haystack的做者能和TFS的同窗們作個交流,說不定你們都能少走點彎路吧(這都是後話了~)

小結一下,針對第三個可擴展性痛點,譯者描述了傳統方案的缺陷,以及Haystack和TFS是如何彌補了這些缺陷,實現了平滑優雅的可擴展能力。此小節的最後再補充一個TFS的特性:

"……同時,在集羣負載比較輕的時候,!NameServer會對!DataServer上的Block進行均衡,使全部!DataServer的容量儘早達到均衡。進行均衡計劃時,首先計算每臺機器應擁有的blocks平均數量,而後將機器劃分爲兩堆,一堆是超過平均數量的,做爲移動源;一類是低於平均數量的,做爲移動目的……"

均衡計劃的職責是在負載較低的時候(深夜),按計劃執行Block數據的遷移,促進總體負載更加均衡。根據譯者的理解,此計劃會改變公式中的map,由於根據文件名拿到的BlockId對應的機器可能發生變化,這也是它爲什麼要在深夜負載較低時按計劃縝密執行的緣由。其效果是避免了由於運維操做等緣由致使的數據分佈不均。

 

4 容錯性

Haystack的容錯是依靠:一個邏輯卷對應多個物理卷(不一樣機器上);"客戶端"向一個邏輯卷的寫操做會翻譯爲對多個物理卷的寫,達到冗餘備份;機器故障時Directory優雅的修改應用元數據(在牽涉到的邏輯卷映射中刪除此機器的物理卷項)、或者標記只讀,繼而指導路由過程(分佈式協調調度)將請求發送到後備的節點,避免請求錯誤;經過bulk複製重置來安全的恢復數據。等等。

在TFS中:

"……TFS能夠配置主輔集羣,通常主輔集羣會存放在兩個不一樣的機房。主集羣提供全部功能,輔集羣只提供讀。主集羣會把全部操做重放到輔集羣。這樣既提供了負載均衡,又能夠在主集羣機房出現異常的狀況不會中斷服務或者丟失數據。……"

"……每個Block會在TFS中存在多份,通常爲3份,而且分佈在不一樣網段的不一樣!DataServer上……"

"……客戶端向master dataserver開始數據寫入操做。master server將數據傳輸爲其餘的dataserver節點,只有當全部dataserver節點寫入均成功時,master server纔會向nameserver和客戶端返回操做成功的信息。……"

能夠看出冗餘備份+協調調度是解決這類問題的慣用範式,在大概思路上二者差很少,可是有幾個技術方案卻差異很大:

第一,冗餘寫機制。Haystack Store是將冗餘寫的責任交給"客戶端"(發起寫操做的客戶端,就是圖3中的web server),"客戶端"須要發起屢次寫操做到不一樣的Store機器上;而TFS是依靠自身的master-slave機制,由master向slave複製。

第二,機房容錯機制。TFS依然是遵循master-slave機制,集羣也分主輔,主輔集羣分佈在不一樣機房,主集羣負責重放數據操做到輔集羣。而Haystack在這方面沒有詳細介紹,只是略微提到"……Haystack複製每張圖片到地理隔離的多個地點……"

針對上面兩點,按譯者的理解,Haystack可能更偏向於對等結構的設計,也就是說沒有master、slave之分,各個Store是對等的節點,沒有誰負責給誰複製數據,"客戶端"向各個Store寫入數據,一視同仁。

不考慮webserver、Directory等角色,只考慮Store,來分析一下它的容錯機制:若是單臺Store掛了,Directory在應用元數據的相關邏輯卷映射中刪除此臺機器的物理卷(此過程簡稱爲"調整邏輯物理映射"),其餘"對等"的物理卷能繼續服務,沒有問題;一整個機房掛了,Directory處理過程和單臺故障相同,只是會對此機房中每臺機器都執行一遍"調整邏輯物理映射",因爲邏輯捲到物理卷的映射是在Directory中明確維護的,因此只要在維護和管理過程當中確保一個邏輯卷下不一樣的物理卷分佈在不一樣的機房,哪怕在映射中刪除一整個機房全部機器對應的物理卷,各個邏輯卷下依然持有到其餘機房可用物理卷的映射,依然有對等Store的物理卷作後備,沒有問題。

主從結構和對等結構各有所長,視狀況選擇。對等結構看似簡潔美好,也有不少細節上的妥協;主從結構增長了複雜度,好比嚴格角色分配、約定角色行爲等等(TFS的輔集羣爲什麼只讀?在主集羣掛掉時是否依然只讀?這些比較棘手也是由於此複雜度吧)

第三,修復機制。Haystack的修復機制依靠週期性後臺任務pitchfork和離線bulk重置等。在TFS中:

"……Dataserver後臺線程介紹……"

"……心跳線程……這裏的心跳是指Ds向Ns發的週期性統計信息……負責keepalive……彙報block的工做……"

"……檢查線程……修復checkfile_queue中的邏輯塊……每次對文件進行讀寫刪操做失敗的時候,會tryadd_repair_task(blockid, ret)來將ret錯誤的block加入check_file_queue中……若出錯則會請求Ns進行update_block_info……"

除了相似的遠程心跳機制,TFS還多了在DataServer上對自身的錯誤統計和自行恢復,必要時還會請求上級(NameServer)幫助恢復。

 

5 文件系統

 Haystack提到了預分配、磁盤碎片、XFS等方案,TFS中也有所涉及:

"……在!DataServer節點上,在掛載目錄上會有不少物理塊,物理塊以文件的形式存在磁盤上,並在!DataServer部署前預先分配,以保證後續的訪問速度和減小碎片產生。爲了知足這個特性,!DataServer現通常在EXT4文件系統上運行。物理塊分爲主塊和擴展塊,通常主塊的大小會遠大於擴展塊,使用擴展塊是爲了知足文件更新操做時文件大小的變化。每一個Block在文件系統上以"主塊+擴展塊"的方式存儲。每個Block可能對應於多個物理塊,其中包括一個主塊,多個擴展塊。在DataServer端,每一個Block可能會有多個實際的物理文件組成:一個主Physical Block文件,N個擴展Physical Block文件和一個與該Block對應的索引文件……"

各有各的考究吧,比較瞭解底層的讀者能夠深刻研究下。 

6 刪除和壓縮

Haystack使用軟刪除(設置flag)、壓縮回收來支持delete操做,在TFS中:

"……壓縮線程(compact_block.cpp)……真正的壓縮線程也從壓縮隊列中取出並進行執行(按文件進行,小文件合成一塊兒發送)。壓縮的過程其實和複製有點像,只是說不須要將刪除的文件數據以及index數據複製到新建立的壓縮塊中。要判斷某個文件是否被刪除,還須要拿index文件的offset去fileinfo裏面取刪除標記,若是標記不是刪除的,那麼就能夠進行write_raw_data的操做,不然則濾過……"

可見二者大同小異,這也是此類場景中經常使用的解決機制。

 

總結

本篇論文以long tail沒法避免出發,探究了文件元數據致使的I/O瓶頸,推導了海量小文件的存儲和檢索方案,以及如何與CDN等外部系統配合搭建出整套海量圖片服務。其在各個痛點的解決方案以及簡約而不簡單的設計值得咱們學習。文章末尾將這些痛點列出並與淘寶的解決方案逐一對比,以供讀者發散。
英文原文:Facebook Haystack,編譯:ImportNew - 儲曉穎  新浪微博:@瘋狂編碼中的xiaoY

相關文章
相關標籤/搜索