淘寶分布式文件存儲系統:TFS


TFS ——分布式文件存儲系統

 

TFS(Taobao File System)是淘寶針對海量非結構化數據存儲設計的分布式系統,構築在普通的Linux機器集群上,可為外部提供高可靠和高並發的存儲訪問。

 

TFS架構

Tair類似,TFS也是由NameServer和DataServer組成:

為了容災,NameServer采用了HA結構,即兩台機器互為熱備,同時運行,一台為主,一台為備,主機綁定到對外vip,提供服務;當主機器宕機后,迅速將vip綁定至備份NameServer,將其切換為主機,對外提供服務;圖中的HeartAgent就完成了此功能。

 

TFS的設計初衷是為了解決淘寶海量圖片存儲的問題,所以主要是針對小文件存儲的特征作了很多優化。TFS會將大量的小文件(實際數據文件)合並成為一個大文件,這個大文件稱為塊(Block),每個Block擁有在集群內唯一的編號(Block Id),Block Id在NameServer在創建Block的時候分配,NameServer維護block與DataServer的關系。Block中的實際數據都存儲在DataServer上。而一台DataServer服務器一般會有多個獨立DataServer進程存在,每個進程負責管理一個掛載點,這個掛載點一般是一個獨立磁盤上的文件目錄,以降低單個磁盤損壞帶來的影響。

TFS架構中,NameServer主要功能是:管理維護Block和DataServer相關信息,包括DataServer加入、退出、心跳信息,block和DataServer的對應關系建立,解除。

正常情況下,一個塊會在DataServer上存在, 主NameServer負責Block的創建,刪除,復制,均衡,整理,NameServer不負責實際數據的讀寫,實際數據的讀寫由DataServer完成。

TFS的Block 大小可以通過配置項來決定,通常為64M。TFS的設計目標是海量小文件的存儲,所以每個塊中會存儲許多不同的小文件。DataServer進程會給Block中的每個文件分配一個File ID(該ID在每個Block中唯一),並將每個文件在Block中的信息存放在和Block對應的Index文件中。這個Index文件一般都會全部load在內存,除非出現DataServer服務器內存和集群中所存放文件平均大小不匹配的情況。

 

 

 

存儲機制
前面說到,TFS以Block的方式組織文件的存儲。每一個Block在整個集群內擁有唯一的編號,這個編號是由NameServer進行分配的,而DataServer上實際存儲了該Block。在NameServer節點中存儲了所有的Block的信息,一個Block存儲於多個DataServer中以保證數據的冗余。對於數據讀寫請求,均先由NameServer選擇合適的DataServer節點返回給客戶端,再在對應的DataServer節點上進行數據操作。NameServer需要維護Block信息列表,以及Block與DataServer之間的映射關系,其存儲的元數據結構如下:

在DataServer節點上,在掛載目錄上會有很多物理塊,物理塊以文件的形式存在磁盤上,並在DataServer部署前預先分配,以保證后續的訪問速度和減少碎片產生。為了滿足這個特性,DataServer一般在EXT4文件系統上運行。物理塊分為主塊和擴展塊,一般主塊的大小會遠大於擴展塊,使用擴展塊是為了滿足文件更新操作時文件大小的變化。每個Block在文件系統上以“主塊+擴展塊”的方式存儲。每一個Block可能對應於多個物理塊,其中包括一個主塊,多個擴展塊。
在DataServer端,每個Block可能會有多個實際的物理文件組成:一個主Physical Block文件,N個擴展Physical Block文件和一個與該Block對應的索引文件。Block中的每個小文件會用一個block內唯一的fileid來標識。DataServer會在啟動的時候把自身所擁有的Block和對應的Index加載進來。

TFS的文件名由塊號和文件號通過某種對應關系組成,最大長度為18字節。文件名固定以T開始,第二字節為該集群的編號(可以在配置項中指定,取值范圍 1~9)。余下的字節由Block ID和File ID通過一定的編碼方式得到。文件名由客戶端程序進行編碼和解碼,它映射方式如下圖:

 

TFS客戶端在讀文件的時候通過將文件名轉換為BlockID和FileID信息,然后可以在NameServer取得該塊所在DataServer信息(如果客戶端有該Block與DataServere的緩存,則直接從緩存中取),然后與DataServer進行讀取操作。

 

 

並發機制

  • 對於同一個文件來說,多個用戶可以並發讀;
  • 現有TFS並不支持並發寫一個文件。一個文件只會有一個用戶在寫。這在TFS的設計里面對應着是一個block同時只能有一個寫或者更新操作。

 

容錯機制

  • 集群容錯,TFS可以配置主輔集群,一般主輔集群會存放在兩個不同的機房。主集群提供所有功能,輔集群只提供讀。主集群會把所有操作同步到輔集群。這樣既提供了負載均衡,又可以在主集群機房出現異常的情況不會中斷服務或者丟失數據。
  • NameServer容錯,NameServer采用了HA結構,一主一備,主NameServer上的操作會同步到備NameServer。如果主NameServer出現問題,可以實時切換到備NameServer。另外NameServer和DataServer之間也會有定時的heartbeat,DataServer會把自己擁有的Block發送給NameServer。NameServer會根據這些信息重建DataServer和Block的關系。
  • DataServer容錯,TFS采用Block存儲多份的方式來實現DataServer的容錯。每一個Block會在TFS中存在多份,一般為3份,並且分布在不同網段的不同DataServer上。對於每一個寫入請求,必須在所有的Block寫入成功才算成功。當出現磁盤損壞DataServer宕機的時候,TFS啟動復制流程,把備份數未達到最小備份數的Block盡快復制到其他DataServer上去。 TFS對每一個文件會記錄校驗crc,當客戶端發現crc和文件內容不匹配時,會自動切換到一個好的block上讀取。此后客戶端將會實現自動修復單個文件損壞的情況。

 

平滑擴容

原有TFS集群運行一定時間后,集群容量不足,此時需要對TFS集群擴容。由於DataServer與NameServer之間使用心跳機制通信,如果系統擴容,只需要將相應數量的新DataServer服務器部署好應用程序后啟動即可。這些DataServer服務器會向NameServer進行心跳匯報。NameServer會根據DataServer容量的比率和DataServer的負載決定新數據寫往哪台DataServer的服務器。根據寫入策略,容量較小,負載較輕的服務器新數據寫入的概率會比較高。同時,在集群負載比較輕的時候,NameServer會對DataServer上的Block進行均衡,使所有DataServer的容量盡早達到均衡。
進行均衡計划時,首先計算每台機器應擁有的blocks平均數量,然后將機器划分為兩堆,一堆是超過平均數量的,作為移動源;一類是低於平均數量的,作為移動目的。
移動目的的選擇:首先一個block的移動的源和目的,應該保持在同一網段內,也就是要與另外的block不同網段;另外,在作為目的的一定機器內,優先選擇同機器的源到目的之間移動,也就是同台DataServer服務器中的不同DataServer進程。
當有服務器故障或者下線退出時(單個集群內的不同網段機器不能同時退出),不影響TFS的服務。此時NameServer會檢測到備份數減少的Block,對這些Block重新進行數據復制。
在創建復制計划時,一次要復制多個block, 每個block的復制源和目的都要盡可能的不同,並且保證每個block在不同的子網段內。因此采用輪換選擇(roundrobin)算法,並結合加權平均。
由於DataServer之間的通信是主要發生在數據寫入轉發的時候和數據復制的時候,集群擴容基本沒有影響。假設一個Block為64M,數量級為1PB。那么NameServer上會有 1 * 1024 * 1024 * 1024 / 64 = 16.7M個block。假設每個Block的元數據大小為0.1K,則占用內存不到2G。

 

 


 

TFS讀寫流程

 

寫流程

TFS會為每一個文件保存多個副本(在不同的dataserver上),為了保證數據一致性,當寫入一個文件時,只有所有參與的dataserver均寫入成功時,該操作才算成功。

TFS的寫操作數據流圖如下所示:

 

客戶端首先向nameserver發起寫請求,nameserver需要根據dataserver上的可寫塊,容量和負載加權平均來選擇一個可寫的block。並且在該block所在的多個dataserver中選擇一個作為寫入的master,這個選擇過程也需要根據dataserver的負載以及當前作為master的次數來計算,使得每個dataserver作為master的機會均等。master一段選定,除非master宕機,不會更換,一旦master宕機,需要在剩余的dataserver中選擇新的master。返回一個dataserver列表。
客戶端向master dataserver開始數據寫入操作。master server將數據傳輸為其他的dataserver節點,只有當所有dataserver節點寫入均成功時,master server才會向nameserver和客戶端返回操作成功的信息。

 

 

讀流程

獲得Block ID和File ID
根據TFS文件名解析出Block ID和block中的File ID.
獲取dataserver地址
向nameserver發送查詢請求得到Block ID所在的dataserver地址。
由於nameserver中維護了block和dataserver的對應關系,所以nameserver能夠提供相應的信息。
Note: 由於TFS是把大量小文件放在一個block里面,
所以TFS的文件復制是基於block的,而且復制出來的block的block id應該是一致的
請求文件
通過發送Block_ID、File_ID和offset為參數的讀請求到對應的dataserver,得到文件內容。
dataserver會根據本地記錄的信息來得到File ID所在block的偏移量,從而讀取到正確的文件內容.

 

 

 


 

 Nameserver

1、Ns(Nameserver)中的BlockManager用來管理所有來自Ds(Dataserver)的Block信息。因為Block的數量比較多,因此,BlockManager將Block組織成HashMap的數據結構,Hash的桶的個數由MAX_BLOCK_CHUNK_NUMS決定。另外,為了組織方便,定義了一個雙向隊列std::deque<std::pair<uint32, uint64> > delete_block_queue_,用來對刪除的Block(以及Block所在的Server)進行一個管理並且將最近(在規定時間內)寫過的Block也組織成一個HashMap便於管理(難道這個和延遲刪有關,就是最近時間內有寫入的不馬上刪除?)。通過這些數據結構,BlockManager可以實現insert一個Block,remove一個Block,將刪除的Block以及對應的Server加入和從刪除隊列中移除,dump所有的Block信息以及最近寫入比較頻繁的Block信息。 此外,BlockManager還可以判斷某個Block是否存在(通過BlockId)以及先通過BlockId獲得BlockCollect結構,進而獲取到該Block所對應的Ds的信息(這里提供多個重載)。在與Ds的關系方便,BlockManager提供了建立、解除以及更新具體Block與Ds關系接口,以及判斷某個Block是否需要復制、壓縮、遷移的接口。最后,BlockManager還會根據時間在last_write_blocks_[i]中插入和刪除最近寫入的Block。
2、Ns中的ServerManager用來管理所有的Server信息。為了管理好活動的和不可服務的DS,ServerManager定義了兩個Server列表servers_和dead_servers_。針對具體的DS的操作大致包括加入Server到活動列表(分為是新加入的還是暫時不可服務又好了的),從活動列表中移除Server到不可服務列表(這種情況可能發生在Ds某種原因退出)。當Server在不可服務列表中超過一定時間后,就會將它從不可服務列表中移除(這種情況可能是磁盤壞掉了,所以等換好新盤啟動需要一定的時間)。另外,通過ServerManager可以得到活動Server列表以及不可服務Server列表以及某一個范圍內的Server列表。 與BlockManager類似,ServerManager也提供了建立和解除具體Ds與Block的關系接口,但這些過程是以各個Server為中心來完成的。此外,ServerManager還負責挑選可寫主塊,先由ServerManager挑一個Server,再由Server挑一個Block。當BlockManager中發現某些Block需要復制時,由於每個Block對應多個Server,ServerManager負責挑選出要復制的源Server和目標Server。當ServerManager發現某個Server不滿足均衡(目前是將活動列表中的前32個server根據容量百分比,特別小的作為目標Server,特別大的作為源Server)時,針對該Server(作為Source Server)里面的具體Block,ServerManager負責挑選出可做為目標的Server。當某種原因導致Block的副本數大於最大副本數時,ServerManager會根據包含該Block的Server的容量進行排序並在滿足一定條件下選擇一個Server將多余的Block進行刪除。(在選擇復制、遷移目標Server時需要考慮Server是否不在任務隊列里,是否有空間,以及是否和已經挑選的Server在不同機架)

 

Dataserver 后台線程

心跳線程

即指Ds向Ns發的周期性統計信息。原先的做法是當Ds需要匯報block時會將blockInfo的信息通過心跳包的形式發給Ns。而現在的心跳只負責keepalive,匯報block的工作由專門的包進行發送。(所以之前的做法是Ns會在心跳的回復包中帶上一個狀態(status),Ds在收到這個狀態包后,會根據狀態進行一些相應的操作(比如淘汰過期的Block以及新增Block操作等))。

 

復制線程

人工或者Ns可以添加復制Block任務至復制隊列中,復制線程會從復制隊列中取出並執行。結合Ns,整個復制的大致過程是ns向復制的源Ds發起復制任務,源Ds將復制任務所需要的信息結構(ReplBlockExt)加入復制隊列中。復制線程取出一個復制任務后,會先通過ReadRawData接口將源Ds的Block數據讀出,然后向目標Ds發WriteRawData消息,目標ds在接到writeRawData消息后復制數據,然后通過batch_write_info進行index的復制。然后源Ds將復制是否成功的狀態向Ns進行回復,Ns在收到復制成功的消息后會進行Block與Ds關系的更新。當從ns中收到move的操作后,還會將源ds上的Block刪除掉。在管理復制的過程中,還用到兩個重要的數據結構ReplicateBlockMap_和ClonedBlockMap_,前者用來記錄源中將要進行復制的Block,后者用來記錄目標中正在復制Block的狀態。

 


壓縮線程

真正的壓縮線程也從壓縮隊列中取出並進行執行(按文件進行,小文件合成一起發送)。壓縮的過程其實和復制有點像,只是說不需要將刪除的文件數據以及index數據復制到新創建的壓縮塊中。要判斷某個文件是否被刪除,還需要拿index文件的offset去fileinfo里面取刪除標記,如果標記不是刪除的,那么就可以進行write_raw_data的操作,否則則濾過。

 

 

檢查線程

a 清理過期的Datafile;

b 修復check_file_queue_中的邏輯塊(block_checker.cpp);

c 清理過期的復制塊(由於復制過程中出錯導致的錯誤復制塊,復制目標的ds做);

d 清理過期的壓縮塊(由於壓縮過程中出錯導致的錯誤壓縮塊,壓縮在同一個ds上做);

e 每天rotate讀寫日志,清理過期的錯誤邏輯塊;

f 讀日志累積后刷磁盤;


b的詳細過程: 每次對文件進行讀寫刪操作失敗的時候,會try_add_repair_task(blockid, ret)來將ret錯誤的block加入check_file_queue_中,正常情況下加入的為-EIO(I/O錯誤)的錯誤Block,那什么時候加入的是CRC的錯誤呢?人工進行修復的時候發該類型的CRC_ERROR_MESSAGE消息,然后也會加入check_file_queue_中.也就是說人工修復是認為CRC錯誤的。然后在check的時候會根據類型進行do_repair_crc還是do_repair_eio操作,對各自類型進行錯誤統計,其中check_block的過程就是通過crc_error和eio_error數量來判斷該Block是否過期(對於過期的邏輯塊,在錯誤位圖上進行相應物理塊的設置),如果是,則請求Ns進行update_block_info, 如果不是,對於eio請求,則無法修復,設置Block不正常(abnormal)的最新時間,對於Crc的則嘗試修復,修復過程中會從其他Ds上讀副本來進行修復,若出錯則會請求Ns進行update_block_info,否則設置Block不正常的最新時間。

 

 


 

客戶端

TFS提供了Nginx模塊,通過Nginx作為HTTP客戶端可以實現對TFS的訪問。

另外,TFS還提供了C++Java庫,允許程序員利用這些庫開發自己的TFS客戶端。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM