TFS(Taobao !FileSystem)是一個高可擴展、高可用、高性能、面向互聯網服務的分佈式文件系統,主要針對海量的非結構化數據,它構築在普通的Linux機器集羣上,可爲外部提供高可靠和高併發的存儲訪問。TFS爲淘寶提供海量小文件存儲,一般文件大小不超過1M,知足了淘寶對小文件存儲的需求,被普遍地應用在淘寶各項應用中。它採用了HA架構和平滑擴容,保證了整個文件系統的可用性和擴展性。同時扁平化的數據組織結構,可將文件名映射到文件的物理地址,簡化了文件的訪問流程,必定程度上爲TFS提供了良好的讀寫性能。mysql
一個TFS集羣由兩個!NameServer節點(一主一備)和多個!DataServer節點組成。這些服務程序都是做爲一個用戶級的程序運行在普通Linux機器上的。算法
在TFS中,將大量的小文件(實際數據文件)合併成爲一個大文件,這個大文件稱爲塊(Block), 每一個Block擁有在集羣內惟一的編號(Block Id), Block Id在!NameServer在建立Block的時候分配, !NameServer維護block與!DataServer的關係。Block中的實際數據都存儲在!DataServer上。而一臺!DataServer服務器通常會有多個獨立!DataServer進程存在,每一個進程負責管理一個掛載點,這個掛載點通常是一個獨立磁盤上的文件目錄,以下降單個磁盤損壞帶來的影響。sql
!NameServer主要功能是: 管理維護Block和!DataServer相關信息,包括!DataServer加入,退出, 心跳信息, block和!DataServer的對應關係創建,解除。正常狀況下,一個塊會在!DataServer上存在, 主!NameServer負責Block的建立,刪除,複製,均衡,整理, !NameServer不負責實際數據的讀寫,實際數據的讀寫由!DataServer完成。數據庫
!DataServer主要功能是: 負責實際數據的存儲和讀寫。後端
同時爲了考慮容災,!NameServer採用了HA結構,即兩臺機器互爲熱備,同時運行,一臺爲主,一臺爲備,主機綁定到對外vip,提供服務;當主機器宕機後,迅速將vip綁定至備份!NameServer,將其切換爲主機,對外提供服務。圖中的HeartAgent就完成了此功能。
緩存
TFS的塊大小能夠經過配置項來決定,一般使用的塊大小爲64M。TFS的設計目標是海量小文件的存儲,因此每一個塊中會存儲許多不一樣的小文件。!DataServer進程會給Block中的每一個文件分配一個ID(File ID,該ID在每一個Block中惟一),並將每一個文件在Block中的信息存放在和Block對應的Index文件中。這個Index文件通常都會所有load在內存,除非出現!DataServer服務器內存和集羣中所存放文件平均大小不匹配的狀況。服務器
另外,還能夠部署一個對等的TFS集羣,做爲當前集羣的輔集羣。輔集羣不提供來自應用的寫入,只接受來自主集羣的寫入。當前主集羣的每一個數據變動操做都會重放至輔集羣。輔集羣也能夠提供對外的讀,而且在主集羣出現故障的時候,能夠接管主集羣的工做。數據結構
原有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中,將大量的小文件(實際用戶文件)合併成爲一個大文件,這個大文件稱爲塊(Block)。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能夠配置主輔集羣,通常主輔集羣會存放在兩個不一樣的機房。主集羣提供全部功能,輔集羣只提供讀。主集羣會把全部操做重放到輔集羣。這樣既提供了負載均衡,又能夠在主集羣機房出現異常的狀況不會中斷服務或者丟失數據。
Namserver主要管理了!DataServer和Block之間的關係。如每一個!DataServer擁有哪些Block,每一個Block存放在哪些!DataServer上等。同時,!NameServer採用了HA結構,一主一備,主NameServer上的操做會重放至備NameServer。若是主NameServer出現問題,能夠實時切換到備NameServer。
另外!NameServer和!DataServer之間也會有定時的heartbeat,!DataServer會把本身擁有的Block發送給!NameServer。!NameServer會根據這些信息重建!DataServer和Block的關係。
TFS採用Block存儲多份的方式來實現!DataServer的容錯。每個Block會在TFS中存在多份,通常爲3份,而且分佈在不一樣網段的不一樣!DataServer上。對於每個寫入請求,必須在全部的Block寫入成功纔算成功。當出現磁盤損壞!DataServer宕機的時候,TFS啓動複製流程,把備份數未達到最小備份數的Block儘快複製到其餘DataServer上去。 TFS對每個文件會記錄校驗crc,當客戶端發現crc和文件內容不匹配時,會自動切換到一個好的block上讀取。此後客戶端將會實現自動修復單個文件損壞的狀況。
對於同一個文件來講,多個用戶能夠併發讀。
現有TFS並不支持併發寫一個文件。一個文件只會有一個用戶在寫。這在TFS的設計裏面對應着是一個block同時只能有一個寫或者更新操做。
TFS的文件名由塊號和文件號經過某種對應關係組成,最大長度爲18字節。文件名固定以T開始,第二字節爲該集羣的編號(能夠在配置項中指定,取值範圍 1~9)。餘下的字節由Block ID和File ID經過必定的編碼方式獲得。文件名由客戶端程序進行編碼和解碼,它映射方式以下圖:
TFS客戶程序在讀文件的時候經過將文件名轉換爲BlockID和FileID信息,而後能夠在!NameServer取得該塊所在!DataServer信息(若是客戶端有該Block與!DataServere的緩存,則直接從緩存中取),而後與!DataServer進行讀取操做。
【測試機軟件狀況描述】
(1) Red Hat Enterprise Linux AS release 4 (Nahant Update 8)
(2) gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-11)
(3) 部署了TFS客戶端程序
【服務器軟件狀況描述】
(1) Red Hat Enterprise Linux Server release 5.4 (Tikanga)
(2) gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-9)
(3) 部署了2臺!DataServer程序。
【服務器軟件狀況描述】
(1) Red Hat Enterprise Linux Server release 5.4 (Tikanga)
(2) gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-46)
(3) 部署了2臺!NameServer(HA)程序。
【測試機硬件狀況描述】
(1) 一枚八核Intel(R) Xeon(R) CPU E5520 @ 2.27GHz
(2) 內存總數8299424 kB
【服務器硬件狀況描述】cpu/memory等
(1) 一枚八核Intel(R) Xeon(R) CPU E5520 @ 2.27GHz
(2) 內存總數8165616 kB
Read的TPS隨着線程數的增長而增長,增加逐漸趨緩,到90線程的時候達到第一個高峯,此時再增長讀線程,則TPS再也不穩定增加。
Write的TPS在線程數60左右達到高峯,此時再增長寫入線程,TPS再也不穩定增加。
能夠看出隨着寫壓力的增長,讀文件的TPS會大幅下滑。當寫壓力達到必定程度時讀文件TPS趨緩。
同時,對平均大小爲20K的文件進行了測試,測試中讀:寫:更新:刪除操做的比率爲100:18:1:1時,在!DataServer服務器磁盤util訪問達到80%以上時,響應時間以下:
TYPE | SUCCCOUNT | FAILCOUNT | AVG(us) | MIN(us) | MAX(us) |
read | 100000 | 0 | 20886 | 925 | 1170418 |
write | 18000 | 0 | 17192 | 2495 | 1660686 |
update | 1000 | 0 | 48489 | 5755 | 1205119 |
delete | 1000 | 0 | 14221 | 382 | 591651 |
TYPE:操做類型
SUCCCOUNT:成功個數
FAILCOUNT:失敗個數
AVG:平均響應時間
MIN:最短響應時間
MAX: 最大響應時間
TFS系統中,nameserver會保證一個文件有多個副本存儲於不一樣的dataserver上以保證冗餘。當因爲dataserver服務器宕機或因爲其餘緣由退出系統致使某些文件副本數量降低時,nameserver將會調度新的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和客戶端返回操做成功的信息。
根據TFS文件名解析出Block ID和block中的File ID.
向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的偏移量,從而讀取到正確的文件內容.
1. Ns中的BlockManager用來管理全部來自Ds的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在不一樣機架)
1、 心跳線程
這裏的心跳是指Ds向Ns發的週期性統計信息。原先的作法是當Ds須要彙報block時會將blockInfo的信息經過心跳包的形式發給Ns。而如今的心跳只負責keepalive,彙報block的工做由專門的包進行發送。(因此以前的作法是Ns會在心跳的回覆包中帶上一個狀態(status),Ds在收到這個狀態包後,會根據狀態進行一些相應的操做(好比淘汰過時的Block以及新增Block操做等))。
2、 複製線程(replicate_block.cpp)
人工或者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的狀態。
3、 壓縮線程(compact_block.cpp)
真正的壓縮線程也從壓縮隊列中取出並進行執行(按文件進行,小文件合成一塊兒發送)。壓縮的過程其實和複製有點像,只是說不須要將刪除的文件數據以及index數據複製到新建立的壓縮塊中。要判斷某個文件是否被刪除,還須要拿index文件的offset去fileinfo裏面取刪除標記,若是標記不是刪除的,那麼就能夠進行write_raw_data的操做,不然則濾過。
4、 檢查線程
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 在2.0版本增長了一個server, 叫作 rcserver. 這個 server 主要是爲了淘寶內部管理使用 TFS 的各個應用. 咱們給每一個應用分配一個惟一的 AppKey. TFS 客戶端使用這個 AppKey 登陸到 rcserver, 取得本身應該訪問的 TFS 集羣信息. 客戶端還會按期把本身的一些統計值發送給 rcserver. 具體信息能夠參看源碼中 doc 目錄下的關於 rcserve 的文檔.
metaserver是咱們在2.0版本引進的一個服務. 用來存儲一些元數據信息, 這樣本來不支持自定義文件名的 TFS 就能夠在 metaserver 的幫助下, 支持自定義文件名了.
metaserver 由一個主控節點(rootserver), 若干服務節點(metaserver) 組成. rootserver 主要管理全部的 metaserver. 而metaserver 完成跟文件相關的操做. metaserver 緩存最近的被訪問到目錄和文件信息. 對於任何寫入, 除了更改本身的緩存外還要更改後端持久化存儲中的內容. 目前咱們暫時使用 mysql 數據庫提供後端持久化存儲, 未來會替換成淘寶本身的分佈式數據庫 oceanbase.
客戶端在作自定義文件名的讀操做的時候, 會先從 rootserver 獲得關於 metaserver 的信息, 並緩存在本身的內存中. 而後用自定義文件名去 metaserver 中查找 TFS 文件名信息, 再去 TFS 中訪問該文件. 客戶端在作自定義文件名的寫操做的時候, 會先寫入到 TFS 中, 再把 TFS 文件名和自定義文件的對應關係寫入metaserver中.
咱們目前要求使用自定義文件名的時候必須傳入一個app_id 一個 uid. 這兩個 id 成爲全部自定義文件名的固定前綴. mv 操做只能在相同的app_id, uid 之下進行. 這樣作是咱們爲了簡化實現複雜度. 咱們的應用都是要面向海量客戶, 每一個客戶自身的數據量有限. 有了上面的限制, 咱們能夠老是把對同一個app_id uid的請求用相同的 metaserver 來響應. 這樣使得咱們能夠很容易的處理數據一致性問題.
在使用自定義文件名寫文件的時候, 必須先調用 creat_file 接口創建文件. 文件創建以後, 能夠多進程併發的寫這個文件. 這樣是爲了便於大文件的分片上傳. 咱們仍是不支持對已有文件的修改, 因此分片上傳的時候各個分片是不能互相覆蓋的.
目前自定義文件名提供的功能還比較簡單初級, 咱們會根據應用的需求逐步完善功能, 提升性能. 咱們未來計劃在 oceanbase 團隊的幫助下, 把後端存儲替換成 oceanbase 數據庫. 另:編譯時候咱們設置了 mysql 的最低版本, 這個版本設置的比較高, 其實只要是5.0以上版本就能夠支持這個應用.