前言
HBase是當下流行的一款海量數據存儲的分布式資料庫。往往海量數據存儲會涉及到一個成本問題,如何降低成本。常見的方案就是通過冷熱分離來治理數據。冷數據可以用更高的壓縮比算法(ZSTD),更低副本數算法(Erasure Coding),更便宜存儲設備(HDD,高密集型存儲機型)。
HBase冷熱分離常見解決方案
1.主備集群
備(冷)集群用更廉價的硬體,主集群設置TTL,這樣當數據熱度退去,冷數據自然只在冷集群有。
優點:方案簡單,現成內核版本都能搞
缺點:維護開銷大,冷集群CPU存在浪費
1.x版本的HBase在不改內核情況下,基本只能有這種方案。
2.HDFS Archival Storage + HBase CF-level Storage Policy
需要在2.x之後的版本才能使用。結合HDFS分層存儲能力 + 在Table層面指定數據存儲策略,實現同集群下,不同表數據的冷熱分離。
優點:同一集群冷熱分離,維護開銷少,更靈活的配置不同業務表的策略
缺點:磁碟配比是個很大的問題,不同業務冷熱配比是不一樣的,比較難整合在一起,一旦業務變動,集群硬體配置是沒法跟著變的。
雲HBase冷熱分離解決方案
上述2套方案都不是最好的方案,對於雲上來說。第一套方案就不說了,客戶搞2個集群,對於數據量不大的客戶其實根本降不了成本。第二套方案,雲上客戶千千萬,業務各有各樣,磁碟配置是很難定製到合適的狀態。
雲上要做 cloud native 的方案,必須滿足同集群下,極致的彈性伸縮,才能真正意義上做到產品化。雲上低成本,彈性存儲,只有OSS了。所以很自然的想到如下架構:
實現這樣的架構,最直接的想法是直接改HBase內核:1)增加冷表數據標記 2)根據標記增加寫OSS的IO路徑。
這樣做的缺陷非常明顯,你的外部系統(如:備份恢復,數據導入導出)很難兼容這些改動,他們需要感知哪些是冷文件得去OSS哪個位置讀,哪些是熱文件得去部署在雲盤上的HDFS上讀。這些本質上都是一些重複的工作,所以從架構設計角度來看必須抽象出一層。這一層能讀寫HDFS文件,讀寫OSS文件,感知冷熱文件。這一層也就是我最後設計出的ApsaraDB FileSystem,實現了Hadoop FileSystem API。對於HBase,備份恢復,數據導入導出等系統只要替換原先FileSystem的實現即可獲得冷熱分離的功能。
下面將詳細闡述,這套FileSystem設計的細節與難點。
ApsaraDB FileSystem 設計
核心難點A
1.OSS並非文件系統
OSS並不是一個真正意義上的文件系統,它僅僅是兩級映射 bucket/object,所以它是對象存儲。你在OSS上看到類似這樣一個文件:
/root/user/gzh/file
。你會以為有3層目錄+1個文件。實際上只有一個對象,這個對象的key包含了
/
字符罷了。
這麼帶來的一個問題是,你要想在其上模擬出文件系統,你必須先能創建目錄。很自然想到的是用
/
結尾的特殊對象代表目錄對象。Hadoop社區開源的OssFileSystem就是這麼搞的。有了這個方法,就能判斷到底存不存在某個目錄,能不能創建文件,不然會憑空創建出一個文件,而這個文件沒有父目錄。
當然你這麼做依然會有問題。除了開銷比較大(創建深層目錄多次HTTP請求OSS),最嚴重的是正確性的問題。試想一下下面這個場景:
把目錄
/root/user/source
rename 成
/root/user/target
。這個過程除了該目錄,它底下的子目錄,子目錄裡的子文件都會跟著變。類似這樣:
/root/user/source/file => /root/user/target/file
。這很好理解,文件系統就是一顆樹,你rename目錄,實際是把某顆子樹移動到另一個節點下。這個在NameNode裡的實現也很簡單,改變下樹結構即可。
但是如果是在OSS上,你要做rename,那你不得不遞歸遍歷
/root/user/source
把其下所有目錄對象,文件對象都rename。因為你沒法通過移動子樹這樣一個簡單操作一步到位。這裡帶來的問題就是,假設你遞歸遍歷到一半,掛了。那麼就可能會出現一半目錄或文件到了目標位置,一半沒過去。這樣rename這個操作就不是原子的了,本來你要麼rename成功,整個目錄下的內容到新的地方,要麼沒成功就在原地。所以正確性會存在問題,像HBase這樣依賴rename操作將臨時數據目錄移動到正式目錄來做數據commit,就會面臨風險。
2.OSS rename實則是數據拷貝
前面我們提到了rename,在正常文件系統中應該是一個輕量級的,數據結構修改操作。但是OSS並沒有rename這個操作實際上,rename得通過
CopyObject
+
DeleteObject
兩個操作完成。首先是copy成目標名字,然後delete掉原先的Object。這裡有2個明顯的問題,一個是copy是深度拷貝開銷很大,直接會影響HBase的性能。另一個是rename拆分成2個操作,這2個操作是沒法在一個事物裡的,也就是說:可能存在copy成功,沒delete掉的情況,此時你需要回滾,你需要delete掉copy出來的對象,但是delete依然可能不成功。所以rename操作本身實現上,正確性就難以保證了。
解決核心難點A
解決上面2個問題,需要自己做元數據管理,即相當於自己維護一個文件系統樹,OSS上只放數據文件。而因為我們環境中仍然有HDFS存儲在(為了放熱數據),所以直接復用NameNode代碼,讓NodeNode幫助管理元數據。所以整體架構就出來了:
ApsaraDB FileSystem(以下簡稱ADB FS)將雲端存儲分為:主存(PrimaryStorageFileSystem)和 冷存(ColdStorageFileSystem)。由
ApsaraDistributedFileSystem
類(以下簡稱ADFS)負責管理這兩類存儲文件系統,並且由ADFS負責感知冷熱文件。
ApsaraDistributedFileSystem: 總入口,負責管理冷存和主存,數據該從哪裡讀,該寫入哪裡(ADFS)。
主存:PrimaryStorageFileSystem 默認實現是 DistributedFileSystem(HDFS)
冷存:ColdStorageFileSystem 默認實現是 HBaseOssFileSystem(HOFS) ,基於OSS實現的Hadoop API文件系統,可以模擬目錄對象單獨使用,也可以只作為冷存讀寫數據,相比社區版本有針對性優化,後面會講。
具體,NameNode如何幫助管理冷存上的元數據,很簡單。ADFS在主存上創建同名索引文件,文件內容是索引指向冷存中對應的文件。實際數據在冷存中,所以冷存中的文件有沒有目錄結構無所謂,只有一級文件就行。我們再看下一rename操作過程,就明白了:
rename目錄的場景也同理,直接在NameNode中rename就行。對於熱文件,相當於全部代理HDFS操作即可,冷文件要在HDFS上創建索引文件,然後寫數據文件到OSS,然後關聯起來。
核心難點B
引入元數據管理解決方案,又會遇到新的問題:是索引文件和冷存中數據文件一致性問題。
我們可能會遇到如下場景:
主存索引文件存在,冷存數據文件不存在冷存數據文件存在,主存索引文件不存住主存索引文件信息不完整,無法定位冷存數據文件先排除BUG或者人為刪除數據文件因素,上訴3種情況都會由於程序crash產生。也就是說我們要想把法,把生成索引文件,寫入並生成冷數據文件,關聯,這3個操作放在一個事物裡。這樣才能具備原子性,才能保證要麼創建冷文件成功,那麼索引信息是完整的,也指向一個存在的數據文件。要麼創建冷文件失敗(包括中途程序crash),永遠也見不到這個冷文件。
解決核心難點B
核心思想是利用主存的rename操作,因為主存的rename是具備原子性的。我們先在主存的臨時目錄中生產索引文件,此時索引文件內容已經指向冷存中的一個路徑(但是實際上這個路徑的數據文件還沒開始寫入)。在冷存完成寫入,正確close後,那麼此時我們已經有完整且正確的索引文件&數據文件。然後通過rename一把將索引文件改到用戶實際需要寫入到目標路徑,即可。
如果中途進程crash,索引文件要麼已經rename成功,要麼索引文件還在臨時目錄。在臨時目錄我們認為寫入沒有完成,是失敗的。然後我們通過清理線程,定期清理掉N天以前臨時目錄的文件即可。所以一旦rename成功,那目標路徑上的索引文件一定是完整的,一定會指向一個寫好的數據文件。
為什麼我們需要先寫好路徑信息在索引文件裡?因為如果先寫數據文件,在這個過程中crash了,那我們是沒有索引信息指向這個數據文件的,從而造成類似「內存洩漏」的問題。
冷熱文件標記
對於主存,需要實現給文件冷熱標記的功能,通過標記判斷要打開怎樣的數據讀寫流。這點NameNode可以通過給文件設置StoragePolicy實現。這個過程就很簡單了,不詳細贅述,下面說HBaseOssFileSystem寫入優化設計。
HBaseOssFileSystem 寫入優化
在說HOFS寫設計之前,我們先要理解Hadoop社區版本的OssFileSystem設計(這也是社區用戶能直接使用的版本)。
社區版本寫入設計
Write -> OutputStream -> disk buffer(128M) -> FileInputStream -> OSS
這個過程就是先寫入磁碟,磁碟滿128M後,將這128M的block包裝成FileInputStream再提交給OSS。這個設計主要是考慮了OSS請求成本,OSS每次請求都是要收費的,但是內網流量不計費。如果你1KB寫一次,費用就很高了,所以必須大塊寫。而且OSS大文件寫入,設計最多讓你提交10000個block(OSS中叫MultipartUpload),如果block太小,那麼你能支持的最大文件大小也是受限。
所以要攢大buffer,另外一個因素是Hadoop FS API提供的是OutputStream讓你不斷write。OSS提供的是InputStream,讓你提供你要寫入內容,它自己不斷讀取。這樣你必然要通過一個buffer去轉換。
這裡會有比較大的一個問題,就是性能慢。寫入磁碟,再讀取磁碟,多了這麼兩輪會比較慢,雖然有PageCache存在,讀取過程不一定有IO。那你肯定想,用內存當buffer不就好了。內存當buffer的問題就是前面說的,有費用,所以buffer不能太小。所以你每個文件要開128M內存,是不可能的。更何況當你提交給OSS的時候,你要保證能繼續寫入新數據,你得有2塊128M內存滾動,這個開銷幾乎不能接受。
HBaseOssFileSystem 寫入設計
我們既要解決費用問題,也要解決性能問題,同時要保證開銷很低,看似不可能,那麼怎麼做呢?
這裡要利用的就是這個InputStream,OSS讓你提供InputStream,並從中讀取你要寫入的內容。那麼我們可以設計一個流式寫入,當我傳入這個InputStream給OSS的時候,流中並不一定得有數據。此時OSS調read讀取數據會block在read調用上。等用戶真的寫入數據,InputStream中才會有數據,這時候OSS就能順利讀到數據。當OSS讀了超過128M數據時候,InputStream會自動截斷,返回EOF,這樣OSS會以為流已經結束了,那麼這塊數據就算提交完成。
所以我們本質只要開發這麼一個特殊的InputStream即可。用戶向Hadoop API提供的OutputStream中寫入數據,數據每填滿一個page(2M)就發給InputStream讓其可讀取。OuputStream相當於生產者,InputStream相當於消費者。這裡的內存開銷會非常低,因為生產者速度和消費者速度相近時,也就2個page的開銷。最後將這整套實現封裝成OSSOutputStream類,當用戶要寫入冷文件時,實際提供的是OSSOutputStream,這裡面就包含了這個特殊InputStream的控制過程。
當然實際生產中,我們會對page進行控制,每個文件設置最多4個page。並且這4個page循環利用,減少對GC對影響。所以最後我們得到下面一個環形緩衝的寫入模式:
性能對比1:社區版本 vs 雲HBase版
因為不用寫磁碟,所以寫入吞吐可以比社區的高很多,下圖為HBase1.0上測試結果。在一些大KV,寫入壓力更大的場景,實測可以接近1倍。這個比較是通過替換ADFS冷存的實現(用社區版本和雲HBase版本),都避免了rename深拷貝問題。如果直接裸用社區版本而不用ADFS那性能會差數倍。
性能對比2:熱表 vs 冷表
熱表數據在雲盤,冷表數據在OSS。
得益於上述優化,加上冷表WAL也是放HDFS的,並且OSS相對HBase來說是大集群(吞吐上限高),冷表的HDFS只用抗WAL寫入壓力。所以冷表吞吐反而會比熱表略高一點點。
不管怎麼說,冷表的寫入性能和熱表相當了,這樣的表現已經相當不錯了。基本是不會影響用戶灌數據,否則使用冷存後,吞吐掉很多,那意味著要更多機器,那這功能就沒什麼意義了。
總結
這大約是1年多前落地的項目,已經穩定運行很久。之前也有出去分享過,但是沒有今天這麼細緻。現在寫出來主要是自我總結下。當設計一個服務去解決某個問題的時候,上下遊的關係,可能存在的問題得考慮清楚。在做這個項目最初的時候,想把HBase直接架設在社區版本OssFileSytem上,發現性能不行外,正確性也存在很大風險。不斷思考,考慮各種情況後才有了今天這套方案。