在信息爆炸的大數據時代,如何以更低成原本解決海量數據的存儲問題,已成爲企業大數據業務中的重要一環。UCloud自研的新一代對象存儲服務US3,在過去一段時間,針對大數據業務場景推出了計算存儲分離和大數據備份解決方案。數據庫
這背後的主要緣由包括:緩存
一、因爲網絡技術的高速發展,使得網絡傳輸性能再也不是大數據場景下高吞吐業務需求的瓶頸;
二、Hadoop技術棧中的HDFS存儲解決方案運維複雜且成本高昂;
三、雲平臺基於海量存儲資源池構建的對象存儲服務US3具有按需使用、操做簡單、可靠穩定、價格便宜的優點,是替換HDFS的最佳存儲方案選擇。所以,爲了讓用戶可以更加方便的在Hadoop場景下,使用US3實現計算存儲分離和大數據備份解決方案,US3自研了US3Hadoop適配器、US3Vmds、US3Distcp三個組件。網絡
本文主要介紹US3Hadoop適配器在研發設計過程當中的一些思路和問題解決。架構
整體設計思路併發
Hadoop生態裏對存儲的操做基本上都是經過一個通用的文件系統基類FileSystem來進行的。US3Hadoop適配器(簡稱:適配器)是經過US3FileSystem實現該基類來操做US3。相似於HDFS實現的DistributedFileSystem和基於AWS S3協議實現的S3AFileSystem。適配器直接把IO和索引都請求發給US3,架構以下圖所示:app
這裏的索引操做主要是不涉及讀寫數據的API,如: HEADFile, ListObjects, Rename, DeleteFile, Copy(用於修改metadata);IO操做的API,如GetFile,PutFile(小於4M文件)已經分片上傳相關的4個API: InitiateMultipartUpload,UploadPart,FinishMultipartUpload,AbortMultipartUpload。US3有了這些API後,怎麼跟FileSystem的成員方法能對應起來,能夠看下FileSystem須要重寫哪些方法。結合實際需求和參考DistributedFileSystem、S3AFileSystem的實現,咱們肯定了須要重寫的主要方法:initialize、create、rename、getFileStatus、open、listStatus、mkdirs、setOwner、setPermission、setReplication、setWorkingDirectory、getWorkingDirectory、getSchem、getUri、getDefaultBlockSize、delete。同時對一些難以模擬的方法,重寫爲異常不支持,如Append成員方法。運維
其實從上面FileSystem的成員方法說明來看,其語義和單機文件系統引擎的接口語義相似,基本上也是以目錄樹結構來組織管理文件系統。US3提供的ListObjects API恰好也提供了目錄樹拉取的一種方式,當Hadoop調用listStatus方法時,就能夠經過ListObjects循環拉取到當前目錄(前綴)下全部子成員從而返回對應的結果。tcp
設置文件所屬用戶/組,操做權限等相關操做則利用了US3的元數據功能,把這些信息都映射到文件的KV元數據對上。寫入文件流則會優先緩存在內存中最多4MB數據,再根據後續的操做來決定採用PutFile仍是分片上傳的API來實現。oop
讀取文件流則經過GetFile返回流實例來讀取期待的數據。雖然這些方法實現看上去很直白,可是潛在着不少值得優化的地方。性能
getFileStatus的時空博弈
經過分析FileSystem的調用狀況,能夠知道索引操做在大數據場景中佔比達70%以上,而getFileStatus在索引操做重佔比最高,因此有必要對其進行優化。那優化點在哪裏呢?
首先由於US3中的「目錄」(對象存儲是KV存儲,所謂目錄只是模擬而已)是以‘/’結尾的Key,而FileSystem的對文件的操做是經過Path結構進行,該結構的路徑都不會以‘/’結尾,因此經過Path拿到的Key去US3中進行HeadFile時,有可能因爲該Key在US3中是目錄,HeadFile就會返回404, 必須經過第二次用「Key/」去US3中Head才能確認。若是這個Key目錄仍是不存在,就會致使getFileStatus時延大大增長了。
所以US3適配在建立目錄時作了如下兩件事:1.向US3寫入mime-type爲「file/path」, 文件名爲「Key」的空文件;2.向US3寫入mime-type爲「application/x-director」, 文件名爲「Key/」的空文件;
而普通文件mime-type爲「application/octet-stream」。這樣在getFileStatus中經過一次HeadFile API就判斷當前Key究竟是文件仍是目錄,同時當該目錄下爲空時,也能在US3控制檯展示出該目錄。並且因爲Hadoop場景寫入的主要是大文件,增長一次空文件索引的寫入耗時在ms級別,時延基本可忽略。
此外,getFileStatus在Hadoop的使用中具有明顯的「時空局部性」特徵,在具體的FileSystem實例中最近被getFileStatus操做的Key,在短期會被屢次操做。利用這個特色,US3FileSystem在實現過程當中,getFileStatus獲得對應的結果在FileStatus返回以前,會把有3s生命週期的FileStatus插入到Cache中。那後續3秒內對該Key的操做就會複用Cache中該Key的FileStatus信息,固然delete操做會在US3中刪除完Key後,直接把Cache中的有效FileStatus標記爲有3s生命週期的404 Cache,或者直接插入一個有3s生命週期的404 Cache。若是是rename,會複用源的Cache來構造目的Key的Cache,並刪除源,這樣就能減小大量跟US3的交互。Cache命中(us級別)會減小getFileStatus上百倍的時延。
固然這會引入必定的一致性問題,但僅限於在多個Job併發時至少有一個存在「寫」的狀況,如delete和rename的狀況下,若是僅僅只有讀,那麼無影響。不過大數據場景基本屬於後者。
ListObjects一致性問題
US3的ListObjects接口跟其餘對象存儲方案相似,目前都只能作到最終一致性(不過US3後續將推出強一致性保證的ListObjects接口),所以其餘對象存儲實現的適配器也都會存在寫入一個文件,而後當即調用listStatus時會偶爾出現這個文件不存在的狀況。其餘對象存儲方案有時會經過引入一箇中間件服務(通常是數據庫),當寫入一個文件會向這個中間件寫入這個文件索引,當listStatus時會跟中間件的索引信息進行合併,這樣確實緩解了這種狀況,進一步提升了一致性。
但還不夠,好比寫入對象存儲成功,但寫入中間件時程序奔潰了,這樣就致使不一致的問題,又回到了最終一致性的問題。
US3Hadoop適配器的實現相對更加簡單有效,不須要藉助額外的服務,能提供索引操做級別的Read-Your-Writes一致性,而該一致性級別在Hadoop大部分場景基本等同於強一致性。US3Hadoop適配器不像S3AFileSystem的實現,在create或者rename、delete後立馬返回,而是在內部調用ListObjects接口作了一次「對帳」,直到「對帳」結果符合預期則返回。
固然這裏也是有優化空間的,好比delete一個目錄時,對應會把這個目錄下全部文件先拉出來,而後依次調用DeleteFile API去刪除,若是每次DeleteFile API刪除都「對帳」一次,那麼整個時延會翻倍。US3Hadoop適配器的作法是隻對最後一次索引操做進行「對帳」,這是因爲索引的oplog是按時序同步到列表服務中,若是最後一條索引「對帳」成功,那麼前面的oplog必定在列表服務中寫入成功。
Rename的深度定製
前面提到的rename也是US3的一個重要優化點,其餘對象存儲方案的實現通常經過Copy的接口會先把文件複製一遍,而後再刪除源文件。能夠看出若是rename的文件很大,那麼rename的整個過程勢必致使時延很高。
US3根據該場景的需求,專門開發了Rename的API接口,所以US3Hadoop適配器實現rename的語義相對比較輕量,並且時延保持在ms級別。
保證read高效穩定
讀是大數據場景的高頻操做,因此US3Hadoop適配器的讀取流實現,不是對http響應的body簡單封裝,而是考慮了多方面的優化。例如,對讀取流的優化,經過加入預讀Buffer,減小網絡IO系統調用頻率,下降read操做的等待時延,特別是大批量順序讀的IO提高效果明顯。
另外,FileSystem的讀取流具備seek接口,也就是須要支持隨機讀,這裏又分兩種場景:
一、seek到已讀流位置的前置位置,那麼做爲Underlay Stream的Http響應的body流就要做廢關閉掉,須要從新發起一個從seek的位置開始分片下載的GetFile API,得到其Http響應的body流來做爲新的Underlay Stream。可是實際測試過程當中發現,不少seek操做事後不必定會進行read操做,有可能直接關閉,或者seek回到已讀取流位置的後置位置,因此在seek發生時,US3Hadoop適配器的實現是隻作seek位置標記,在read的時候根據實際狀況對Underlay Stream作延遲關閉打開處理。此外若是seek的位置還在Buffer中,也不會從新打開Underlay Stream,而是經過修改Buffer的消費偏移。
二、隨機讀的另外一種場景就是,seek到已讀流位置的後置位置。這裏一樣跟前面同樣採用延遲流打開,可是在肯定要作真實的seek操做時,不必定會經過關閉老的Underlay Stream,從新在目標位置打開新的Underlay Stream來實現。由於當前已讀的位置跟seek的後置位置可能距離很近,假設只有100KB距離,說不定這段距離徹底在預讀Buffer的範圍中,這時也能夠經過修改Buffer的消費偏移來實現。
事實上US3Hadoop適配器確實也是這麼作的,不過目前的規則是seek的後置位置到當前已讀流位置的距離小於等於預讀Buffer剩餘空間加上16K的和,則直接經過修改預讀Buffer的消費偏移和消費Underlay Stream中的數據來定位到seek的後置位置上。之因此還加了16K是考慮到TCP接收緩存中的數據。固然後續肯定從一個ready的Underlay Stream中消費N字節數據的時間成本大體等於從新發起一個GetFile API並在準備傳輸該Http響應body以前的時間成本,也會考慮把這N字節的因素計入偏移計算過程當中。
最後流的優化還要考慮到Underlay Stream異常的狀況,好比HBase場景長時間持有打開的流,卻因爲其餘操做致使長時間沒有操做該流,那麼US3可能會主動關閉釋放Underlay Stream對應的TCP鏈接,後續對在Underlay Stream上的操做就會報TCP RST的異常。爲了提供可用性,US3Hadoop適配器的實現是在已經讀取位置點上進行Underlay Stream的從新打開。
寫在最後
US3Hadoop適配器的實如今借鑑開源方案下,進一步優化了相關核心問題點,提高了Hadoop訪問US3的可靠性與穩定性,並在多個客戶案例中發揮着打通Hadoop與US3的重要橋樑做用,幫助用戶提高大數據場景下的存儲讀寫效率。
但US3Haoop適配器還存在不少可提高的空間,相比於HDFS,索引、IO的時延還有差距,原子性保障上也相對比較弱,這些也是咱們接下來要思考解決的問題。目前推出的US3Vmds解決了索引時延的大部分問題,使得經過US3Hadoop適配器操做US3的性能獲得大幅提高,並在部分場景接近原生HDFS的性能。具體數據能夠參考官方文檔(https://docs.ucloud.cn/ufile/...
將來,US3產品會不斷改進優化大數據場景下的存儲解決方案,在下降大數據存儲成本的同時,進一步提高用戶在大數據場景下的US3使用體驗。