當下信息社會天天都產生大量須要保存的數據,這些數據在刺激海量存儲技術發展的同時也帶來了新的挑戰。好比,海量數據爲存儲系統增長了大量的小文件,這些小文件的元數據如何管理?如何控制定位某個文件的時間和空間開銷?node
隨着對數據實時性要求的提升, 文件也愈來愈趨於碎片化,像短視頻、直播類業務, 每每一個視頻只有幾百KB, 甚至幾十KB大小。能夠說, 一個成熟的對象存儲系統最後都會面臨巨量元數據管理的挑戰, 如HDFS, openstack-swift等, 在軟件總體進入相對成熟的階段, 小文件成爲最頭疼的問題。 以100TB數據(大約是平常的單機容量)爲例,若所有存儲10KB的文件(文件名<=1KB),僅是管理這些文件所需的索引數據就將須要約10,000GB的內存空間。這是任何成(sheng)熟(qian)的存儲系統都沒法接受的巨大壓(cheng)力(ben)。git
爲了應對當前環境給存儲帶來的挑戰,通過不懈研究和探索,白山雲存儲在兩個方面進行了優化:github
今天咱們就主要聊聊如何能在單機上實現百億文件的索引。golang
存儲系統的架構主要由數據的存儲和數據的定位 兩方面構成。數據的存儲更多關注文件佈局、複製、故障檢測、修復等環節,它主要決定系統的可靠性;而數據的定位是最具挑戰的, 尤爲是面對海量數據時,一個存儲系統中索引的設計,直接決定了這個系統的讀寫效率、可擴展能力和成熟度。算法
然而,索引的設計面臨着各類挑戰和難題。好比,當存儲的數據量愈來愈大,如何權衡索引數據的格式、算法、達到最高的空間利用率和查詢效率等問題, 就成爲系統設計的關鍵。 在討論SlimTrie索引設計以前,咱們來回顧一下已知的幾種索引設計。數據庫
在分佈式領域,管理大量索引數據時,通常會採用分層的思路(很是相似於兩層b+tree的實現), 若是不是超大規模的系統, 兩層最爲常見,上層索引主要負責sharding, 將查詢路由到一個獨立的服務器,下層負責具體的查詢。swift
通常來講,單集羣規模可能有是幾百到幾千個服務器組成, 這時上層sharding部分的數據可能只有幾千條(或上百萬條,若是使用虛擬bucket等策略, 虛擬節點多是物理節點的幾百倍), 因此上層索引會很小,大部分問題集中在底層索引上。 在咱們的設計中, 上層是一個百萬級別的sharding, 下層直接是存儲服務器, 存儲服務器負責索引整機的文件。這樣, 上層sharding的量級不會很大, 整個系統設計的核心問題就落在了單機的文件索引設計上。數組
Tips:緩存
剝去系統架構層面的組件, 剩下的就是單機上文件定位的問題:服務器
這一類方案能夠稱之爲服務器端URL生成。 每次上傳時, 存儲服務器負責生成一個用於下載的URL,如FastDFS的實現:
http://192.168.101.5/group1/M00/00/00/wKhlBVVY2M-AM_9DAAAT7-0xdqM485_big.png
其中, group1, M00, 00, 00是分組和定位信息。
當服務器接到一個URL時,直接從其中解析出文件位置, 而後定位到文件所在的服務器、 磁盤、目錄和文件名,再也不須要額外的索引數據。 這種方案其實是將」數據的定位」繞開了, 交給外層邏輯, 也就是存儲的使用方來處理, 而本身只處理」數據的存儲」。
優點:
客戶端指定URL是比較通用的方式, 它容許用戶在上傳時指定下載的URL, 所以它不只要管理」數據的存儲」問題, 同時也關心」數據的定位」問題。存儲系統負責記錄每一個URL到文件數據位置的信息, 至關於一個分佈式的key-value map。
相似aws S3和其餘大部分公有云對象存儲服務, 都屬於第二類,是通用的存儲。
提到 key-value map, 分佈式領域和單機領域有頗多類似, 分佈式存儲系統的」數據的定位」問題, 也就是索引的構建, 基本上也分爲兩個思路: 無序的hash map類結構, 和有序的tree 類結構。 接下來咱們分別分析兩類索引的優劣。
提出一個好問題永遠比解決問題更重要:
索引能夠被認爲是一些"額外"的數據, 在這些額外數據的幫助下, 能夠從大量數據中快速找到本身想要的內容。 就像一本書, 通常包括1個"索引"—— 目錄, 它讓讀者只翻閱幾頁的目錄後就能夠定位到某個章節的頁碼。 存儲系統中的索引須要作到:
Hash 類索引例圖
Hash map類索引首先利用hash函數的計算,將要存儲的key映射到一個新的hash值,而後再創建索引。查找定位時也須要這一步來定位到真正數據存儲的位置。上面的例圖簡單展現了其結構和工做原理。
它的優勢很明顯:
Tree 類索引例圖
Tree 類索引利用樹的中間節點和分支將全量的key分紅一個個更小的部分。上圖是一個典型的B+Tree實現,其中間節點只保存了key,數據部分所有保存在葉子節點裏。這樣的結構在查詢時,經過樹的中間節點一步步縮小查找範圍,從而找到要查找的key。
Tree 類中表明性的數據結構有:
Tree 類索引的特色也很明顯:
以上是兩種經典的索引結構設計案例,它們都存在一個沒法避免的問題:首先這兩種索引結構首先都會存儲全量的key信息,當key的數量快速增加時,它們對內存空間的需求會變的很是巨大。
小文件索引數據量大的困境,致使以上的經典索引結構沒法支持在索引海量數據的同時,將索引緩存在內存中。而一旦索引數據須要磁盤IO,時間消耗會增大幾個量級,存儲系統的性能將因索引效率低而大打折扣。 優化索引結構以提升存儲性能,纔是解決這個問題的惟一出路。 對此,目前業界也有本身的一些方案,
好比LevelDB採用skiplist創建索引,但skiplist內存佔用太大,須要2n個指針的開銷,並且沒法作前綴壓縮。仔細研究過這些已有方案後,咱們認爲都不太理想。
是否有一種數據結構可以索引海量數據,而且佔用空間不大,可以緩存在內存中呢?
若是要索引n個key, 那至少須要log2(n) 個bit, 才能區分出n個不一樣的key。
若是一共有n個key, 所以理論上所需的內存空間最低是log2(n) * n, 這就是咱們空間優化的目標。
在這個極限中, key的長度不會影響空間開銷, 而僅僅依賴於key的數量, 這也是咱們要達到的一個目標——容許很長的key出如今索引中而不須要增長額外的內存。
實際上咱們在實現時限制了n的大小, 將整個key的集合拆分紅多個指定大小的子集, 這樣有2個好處:
最終達到每一個文件的索引均攤內存開銷與key的長度無關: 每條索引一共10 byte, 其中6 byte是key的信息、4 byte是value: offset。
Tree的順序性、查詢效率均可以知足預期, 但空間開銷仍然很大。 在以字符串爲key的索引結構中, Trie的特性恰好能夠優化key存儲的問題。 Trie 是一個前綴樹, 例如:
保存了8個key的trie結構 "A", "to", "tea", "ted", "ten", "i", "in", and "inn"
Trie的特色在於原生的前綴壓縮, 而Trie上的節點數最少爲O(n), 但Trie的空間開銷比較大, 由於每一個節點都要保存若干個指針(指針單獨要佔8字節), 致使它的空間複雜度雖然是O(n), 但實際內存開銷很大。
若是能將Trie的空間開銷降到足夠低, 它就是咱們想要的東西!
數據生成以後在使用階段不修改。依賴於這個假設咱們能夠對索引進行更多的優化: 預先對全部的key進行掃描, 提取特徵, 大大下降索引信息的量。 在存儲系統中, 須要被索引的數據大部分是靜態的,數據的更新是經過Append 和 Compact這2個操做完成的, 通常不須要隨機插入一條記錄。
索引的目的在於快速定位一個對象所在的位置範圍, 但不保證定位到的對象必定存在,就像Btree的中間節點, 用來肯定key的範圍, 但要查找的key是否真的存在, 須要在Btree的葉子節點(真實數據)上來肯定。
索引不少狀況下須要支持範圍查詢,SlimTrie 做爲索引的數據結構,必定是支持順序遍歷的特色。SlimTrie 在結構上與樹形結構有類似點,順序遍歷的實現並不難。
假設n個key,每一個key的長度爲k,各數據結構的特性以下表:
1)用全部的key建立一個標準的Trie樹, 而後在標準Trie樹基礎上作裁剪。 裁剪掉標準Trie中無效的節點,即Trie樹中的單分支節點,對索引key沒有任何的幫助,將索引數據的量級從O(n * k)下降到O(n)。
2)Trie的壓縮, 經過一個compacted array來存儲整個Trie的數據結構, 在實現上將內存開銷下降。 接下來還要在實現上壓縮Trie實際的內存開銷。樹形結構在內存中多以指針的形式來實現, 但指針在64位系統上佔用8個字節, 至關於最差狀況下, 內存開銷至少爲 8*n,這樣的內存開銷仍是太大了,因此咱們使用compacted array來壓縮內存開銷。
3)對小文件進行優化, 將多個相鄰的小文件用1條索引來標識, 平衡IO開銷和內存開銷。 索引的設計以下降IO和下降內存開銷爲目的,這兩方面有矛盾的地方, 若是要下降IO就須要索引儘量準確, 這將帶來索引的容量增長;若是要減少索引的內存開銷, 則可能帶來對磁盤上文件定位的不許確而致使額外的IO。在作這個設計時, 有一個假設是, 磁盤的一次IO, 開銷是差很少的, 跟此次IO的讀取的數據量大小關係不大,因此能夠在一次IO中讀取更多的數據來有效利用IO。
使用 SlimTrie 數據結構的索引相比於使用其餘類索引 ,在保證索引功能的狀況下壓縮了索引中的 key 所佔用的空間。理論上講,使用 SlimTrie 作索引能夠極大的節約內存佔用。 如今咱們來看看實際測試的結果。
首先咱們用一個基本的實驗來證實咱們的實現和上文說到的理論是相符的。實驗選取Hash 類數據結構的 map 和 Tree 類數據結構的B-Tree 與 SlimTrie 作對比,計算在同等條件下,各個數據結構創建索引所耗費的內存空間。 實驗在go語言環境下進行,map 使用 golang 的 map 實現,B-Tree使用Google的BTree implementation for Go (github.com/google/btre… 。 key 和 value 都是 string 類型(咱們更多關心它的大小)。
實驗的結果數據以下:
1)索引內存佔用對比
能夠得出明顯結論:
在此實驗的基礎上咱們再作一個理論上的計算:1PB 的數據量,使用 SlimTrie 作索引,小文件合併到 1MB,索引的 value 是每個 1MB 數據塊的起始位置,4 byte 的 int 足夠,根據測試,索引的 key 在 SlimTrie 中佔的空間不會超過 6 Byte。
那麼,1GB內存即可創建 100TB 數據量的索引: 100TB / 1M * (4+6) = 1GB
2)SlimTrie 在通用場景中的表現 由於此次測試全部的數據結構都保存了完整的key和value信息,因此咱們只看memory overhead便可比較出誰的空間佔用小。測試獲得的數據,見下面的圖表:
二者進行對比,能夠明顯看出,SlimTrie 所佔用的空間額外開銷仍然遠遠小於 map 和 B-Tree 所佔的內存,每一個 key 可以節省大約 50 Byte。 內存佔用空間大獲全勝以後,咱們還對 SlimTrie 的查詢進行了測試,同時和 map 、Btree 進行了比較,在與內存測試相同的go語言環境下進行實驗。
1)測量查詢相同的、肯定存在的 key 的查詢時間比較結果以下圖
存在的key的查找耗時對比圖(越小越優)
2)查詢相同的、肯定不存在的 key 的查詢時間比較結果以下圖
不存在的key的查找耗時對比圖(越小越優)
SlimTrie的查詢效率遠好於Btree, 也很是接近Hash map的性能。 • 上面兩個圖中,前半段key的長度k=1000保持不變,隨着key的個數n的增加, SlimTrie 查詢耗時隨之上漲 • 後半段key的個數保持不變,長度減少,SlimTrie 的查詢耗時基本維持不變
從查詢效率上也反應了SlimTrie的內部結構只與n相關的特性. 另外一方面,在上圖中,咱們也可以看到,SlimTrie 的實際查詢耗時在 150ns 左右,加上優秀的空間佔用優化,做爲存儲系統的索引依然有很是強的競爭力。
做爲索引,SlimTrie 的優點巨大,能夠在1GB內存中創建100TB數據量的索引,空間節約驚人,令以往的索引結構可望不可即;時間消耗上,SlimTrie 的查找性能與 sorted Array 接近,超過經典的B-Tree。拋下索引這個身份,SlimTrie 在各項性能方面表現依舊不俗,做爲一個通用 Key-Value 的數據結構,內存額外開銷仍遠遠小於經典的 map 和 Btree。
咱們生在最好的時代, 科技爆炸和信息指數級的增加, 對IT產業帶來了巨大的挑戰, 嚴酷的競爭纔是誕生奇蹟的角鬥場, 沒有了平庸的溫牀, 每一個人都要嘗試把本身的身體打碎, 去涅槃重生, 纔有機會給時間長河添一道驚豔的波浪。
當下信息爆炸增加,陳舊的索引模式已沒法適應海量數據新環境,存儲系統海量數據的元信息管理面臨巨大挑戰,而SlimTrie 提供了一個全新的解決方法,爲海量存儲系統帶來一絲曙光,爲雲存儲擁抱海量數據時代注入了強大動力,讓咱們看到了將來的無限可能。
SlimTrie 是白山雲存儲團隊通過長時間研究和探索的產物,在實際使用中的表現沒有辜負咱們對它的深厚指望。它的成功不會停下咱們開拓的腳步,這只是個開始,還遠沒有結束。
感興趣的朋友,能夠掃描下方二維碼,加入「白山雲存儲技術交流羣」,一塊兒碰撞,一塊兒進步。