一. 背景
當下資訊社會每天都產生大量需要儲存的資料,這些資料在刺激海量儲存技術發展的同時也帶來了新的挑戰。比如,海量資料為儲存系統增加了大量的小檔案,這些小檔案的後設資料如何管理?如何控制定位某個檔案的時間和空間開銷?
隨著對資料實時性要求的提高, 檔案也越來越趨於碎片化,像短視訊、直播類業務, 往往一個視訊只有幾百KB, 甚至幾十KB大小。可以說, 一個成熟的物件儲存系統最後都會面臨巨量後設資料管理的挑戰, 如HDFS, openstack-swift等, 在軟體整體進入相對成熟的階段, 小檔案成為最頭疼的問題。
以100TB資料(大約是日常的單機容量)為例,若全部儲存10KB的檔案(檔名<=1KB),僅是管理這些檔案所需的索引資料就將需要約10,000GB的記憶體空間。這是任何成(sheng)熟(qian)的儲存系統都無法接受的巨大壓(cheng)力(ben)。
為了應對當前環境給儲存帶來的挑戰,經過不懈研究和探索,白山雲端儲存在兩個方面進行了優化:
- 整體上對後設資料管理採用無中心的設計,索引採用分層的思想,拋棄中心化後設資料管理的策略, 將後設資料分散到每個單機儲存伺服器。
- 單機上, 我們部署了一套全新的索引資料結構SlimTrie,對索引資料進行了裁剪、壓縮和聚合,將索引進行了極大的優化, 逼近空間利用率的理論極限。以單機100TB資料為例, 如果檔案都是10KB小檔案, 那麼就有100億個檔案,我們的SlimTrie演算法最終只需10GB記憶體空間。
今天我們就主要聊聊如何能在單機上實現百億檔案的索引。
二. 巨人的肩膀: 主流索引設計
儲存系統的架構主要由資料的儲存和資料的定位 兩方面構成。資料的儲存更多關注檔案佈局、複製、故障檢測、修復等環節,它主要決定系統的可靠性;而資料的定位是最具挑戰的, 尤其是面對海量資料時,一個儲存系統中索引的設計,直接決定了這個系統的讀寫效率、可擴充套件能力和成熟度。
然而,索引的設計面臨著各種挑戰和難題。比如,當儲存的資料量越來越大,如何權衡索引資料的格式、演算法、達到最高的空間利用率和查詢效率等問題, 就成為系統設計的關鍵。
在討論SlimTrie索引設計之前,我們來回顧一下已知的幾種索引設計。
1.儲存體系
在分散式領域,管理大量索引資料時,一般會採用分層的思路(非常類似於兩層b+tree的實現), 如果不是超大規模的系統, 兩層最為常見,上層索引主要負責sharding, 將查詢路由到一個獨立的伺服器,下層負責具體的查詢。
一般來說,單叢集規模可能有是幾百到幾千個伺服器組成, 這時上層sharding部分的資料可能只有幾千條(或上百萬條,如果使用虛擬bucket等策略, 虛擬節點可能是物理節點的幾百倍), 所以上層索引會很小,大部分問題集中在底層索引上。
在我們的設計中, 上層是一個百萬級別的sharding, 下層直接是儲存伺服器, 儲存伺服器負責索引整機的檔案。這樣, 上層sharding的量級不會很大, 整個系統設計的核心問題就落在了單機的檔案索引設計上。
Tips:
- 一般很少有千臺伺服器以上的叢集,不是受限於技術,而是為了簡化運維。 幾百到幾千個伺服器已經具備了不錯的容量、負載彈性和單點故障容錯能力, 而且幾百個伺服器的小叢集管理相對容易。
- 如果叢集規模大到需要3層索引, 多一次索引訪問, 效能也會降低。
- 類Haystack的設計在物件儲存中很常見: Haystack是一個關於單機儲存設計的實現, 為了提升IO效能, 降低檔案系統Inode的讀寫開銷, 將小檔案合併成一個大檔案儲存, 並在記憶體中儲存所有檔案的元資訊(meta), 這樣直接將每個檔案讀取的2次IO(inode+data)轉變成一次記憶體操作和一次IO操作。
剝去系統架構層面的元件, 剩下的就是單機上檔案定位的問題:
2.消滅問題,在URL中嵌入定位資訊
這一類方案可以稱之為伺服器端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沒有業務上的規律而變得複雜
3.解決“資料的定位”問題,客戶端指定URL
客戶端指定URL是比較通用的方式, 它允許使用者在上傳時指定下載的URL, 因此它不僅要管理”資料的儲存”問題, 同時也關心”資料的定位”問題。儲存系統負責記錄每個URL到檔案資料位置的資訊, 相當於一個分散式的key-value map。
類似aws S3和其他大部分公有云物件儲存服務, 都屬於第二類,是通用的儲存。
提到 key-value map, 分散式領域和單機領域有頗多相似, 分散式儲存系統的”資料的定位”問題, 也就是索引的構建, 基本上也分為兩個思路: 無序的hash map類結構, 和有序的tree 類結構。
接下來我們分別分析兩類索引的優劣。
3.1明確問題: 定義索引
提出一個好問題永遠比解決問題更重要:
索引可以被認為是一些”額外”的資料, 在這些額外資料的幫助下, 可以從大量資料中快速找到自己想要的內容。
就像一本書, 一般包括1個”索引”—— 目錄, 它讓讀者只翻閱幾頁的目錄後就可以定位到某個章節的頁碼。
儲存系統中的索引需要做到:
- 足夠小: 如果目錄過於詳細, 翻閱目錄的時間成本就會變高
- 縮小查詢範圍: 目錄的作用不是精確的定位到某一頁某一行某個字, 而是定位到一個足夠小的範圍(幾頁)
- 足夠準確: 對較小的檔案, 訪問一個檔案開銷為1次磁碟IO操作
- 全記憶體: 索引資訊必須全部在記憶體中, 訪問一個檔案分為2步——訪問索引、訪問磁碟。 訪問索引的過程中不能訪問磁碟, 否則延遲變得不可控(這也是為什麼leveldb或其他db在我們的設計中沒有作為索引的實現來考慮)
3.2基於Hash map的索引
Hash 類索引例圖
Hash map類索引首先利用hash函式的計算,將要儲存的key對映到一個新的hash值,然後再建立索引。查詢定位時也需要這一步來定位到真正資料儲存的位置。上面的例圖簡單展示了其結構和工作原理。
它的優點很明顯:
- 一次檢索定位資料. 即, 每個key都可以通過一步計算找到所需的值的位置.
- 查詢的時間複雜度是 O(k)(k是key的長度)。這個特點非常適合用來做單條資料的定位,然而它有一個前提是查詢的key必須是等值匹配的,不支援“>”、“<”的操作
範圍查詢在儲存系統中也是一個非常重要的特性, 在資料清理、合併等操作時, 是必須要支援的一個API
從圖中我們能明顯看到它的幾個天然缺陷: - 無序。當進行查詢操作時,如果不是等值的匹配而是範圍查詢,比如,想要順序列出索引中全部的key,最優時間複雜度也需要O(k * n * log(n)),這樣的操作消耗的空間和時間代價都是索引系統不可接受的。
- 記憶體開銷大。 Hash map 要求在記憶體中儲存完整的key, 也就是說記憶體開銷是O(k*n), 這對單機百億檔案級別的目標來說無疑是致命的缺陷。
有一種優化方式是: 使用MD5(key)的前8位元組作為索引的key, 可以將任意長度key縮減到8位元組, 並在一定範圍內把碰撞機率控制到很小。
但我們沒有選擇這種方案的原因還是因為hash的無序。
3.3 基於Tree 的索引
Tree 類索引例圖
Tree 類索引利用樹的中間節點和分支將全量的key分成一個個更小的部分。上圖是一個典型的B+Tree實現,其中間節點只儲存了key,資料部分全部儲存在葉子節點裡。這樣的結構在查詢時,通過樹的中間節點一步步縮小查詢範圍,從而找到要查詢的key。
Tree 類中代表性的資料結構有:
- B+tree、 RBTree、SkipList、LSM Tree : 一般以平衡性最優為特點, 適用於資料庫中實現索引等場合。
- 排序陣列: 也可以認為是Tree類的資料結構, 它的空間開銷、查詢效能都跟平衡樹相當。
Tree 類索引的特點也很明顯:
- 優勢: 對儲存的key是排序的。如例圖所示,通過一個順序訪問資料的指標,就能夠方便地順序列出全部資料,這彌補了Hash類索引不能夠範圍查詢的缺點。
此外,Tree類索引有許多成熟的實現,如B樹、B+樹的設計在查詢效能方面也有很好的表現,MySQL的預設索引型別就是B+樹。 - 劣勢: 跟Hash map一樣, 用Tree做索引的時候, map.set(key = key, value = (offset, size)) 記憶體中必須儲存完整的key, 記憶體開銷為O(k * n),也很大。
4.小結
以上是兩種經典的索引結構設計案例,它們都存在一個無法避免的問題:首先這兩種索引結構首先都會儲存全量的key資訊,當key的數量快速增長時,它們對記憶體空間的需求會變的非常巨大。
小檔案索引資料量大的困境,導致以上的經典索引結構無法支援在索引海量資料的同時,將索引快取在記憶體中。而一旦索引資料需要磁碟IO,時間消耗會增大幾個量級,儲存系統的效能將因索引效率低而大打折扣。
優化索引結構以提高儲存效能,才是解決這個問題的唯一出路。
對此,目前業界也有自己的一些方案,
比如LevelDB採用skiplist建立索引,但skiplist記憶體佔用太大,需要2n個指標的開銷,而且無法做字首壓縮。仔細研究過這些已有方案後,我們認為都不太理想。
是否有一種資料結構能夠索引海量資料,並且佔用空間不大,能夠快取在記憶體中呢?
三. 魚和熊掌我都要: 低記憶體, 高效能的 SlimTrie 索引
1.理論極限:
如果要索引n個key, 那至少需要log2(n) 個bit, 才能區分出n個不同的key。
如果一共有n個key, 因此理論上所需的記憶體空間最低是log2(n) * n, 這就是我們空間優化的目標。
在這個極限中, key的長度不會影響空間開銷, 而僅僅依賴於key的數量, 這也是我們要達到的一個目標——允許很長的key出現在索引中而不需要增加額外的記憶體。
實際上我們在實現時限制了n的大小, 將整個key的集合拆分成多個指定大小的子集, 這樣有2個好處:
- n 和 log2(n) 都比較確定, 容易進行優化
- 佔用空間更小, 因為: a * log(a) + b * log(b) < (a+b) * log(a+b)
最終達到每個檔案的索引均攤記憶體開銷與key的長度無關:
每條索引一共10 byte, 其中6 byte是key的資訊、4 byte是value: offset。
2.SlimTrie 的前輩: Trie
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的空間開銷降到足夠低, 它就是我們想要的東西!
3.SlimTrie的設計
- 靜態資料索引
資料生成之後在使用階段不修改。依賴於這個假設我們可以對索引進行更多的優化: 預先對所有的key進行掃描, 提取特徵, 大大降低索引資訊的量。
在儲存系統中, 需要被索引的資料大部分是靜態的,資料的更新是通過Append 和 Compact這2個操作完成的, 一般不需要隨機插入一條記錄。
- SlimTrie保證存在的key被正確定位, 但被索引到的key不一定存在
索引的目的在於快速定位一個物件所在的位置範圍, 但不保證定位到的物件一定存在,就像Btree的中間節點, 用來確定key的範圍, 但要查詢的key是否真的存在, 需要在Btree的葉子節點(真實資料)上來確定。
- SlimTrie支援順序查詢和遍歷key
索引很多情況下需要支援範圍查詢,SlimTrie 作為索引的資料結構,一定是支援順序遍歷的特點。SlimTrie 在結構上與樹形結構有相似點,順序遍歷的實現並不難。
- SlimTrie的記憶體開銷只與key的個數n相關,不依賴於key的長度k
- SlimTrie支援最大16KB的key
- SlimTrie查詢速度要非常快
假設n個key,每個key的長度為k,各資料結構的特性如下表:
4.生成SlimTrie的三個步驟
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 索引
使用 SlimTrie 資料結構的索引相比於使用其他類索引 ,在保證索引功能的情況下壓縮了索引中的 key 所佔用的空間。理論上講,使用 SlimTrie 做索引可以極大的節約記憶體佔用。
現在我們來看看實際測試的結果。
1.記憶體的低開銷
首先我們用一個基本的實驗來證明我們的實現和上文說到的理論是相符的。實驗選取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)索引記憶體佔用對比
可以得出明顯結論:
- SlimTrie 作為索引在記憶體的節省上碾壓 map 和 B-Tree。
- SlimTrie 作為索引其記憶體佔用的決定因素是 value 的大小,與key的大小無關。
在此實驗的基礎上我們再做一個理論上的計算: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語言環境下進行實驗。
2.查詢的高效能
1)測量查詢相同的、確定存在的 key 的查詢時間比較結果如下圖
存在的key的查詢耗時對比圖(越小越優)
2)查詢相同的、確定不存在的 key 的查詢時間比較結果如下圖
不存在的key的查詢耗時對比圖(越小越優)
SlimTrie的查詢效率遠好於Btree, 也非常接近Hash map的效能。
• 上面兩個圖中,前半段key的長度k=1000保持不變,隨著key的個數n的增長, SlimTrie 查詢耗時隨之上漲
• 後半段key的個數保持不變,長度減小,SlimTrie 的查詢耗時基本維持不變
從查詢效率上也反應了SlimTrie的內部結構只與n相關的特性.
另一方面,在上圖中,我們也能夠看到,SlimTrie 的實際查詢耗時在 150ns 左右,加上優秀的空間佔用優化,作為儲存系統的索引依然有非常強的競爭力。
3.小結
作為索引,SlimTrie 的優勢巨大,可以在1GB記憶體中建立100TB資料量的索引,空間節約驚人,令以往的索引結構望塵莫及;時間消耗上,SlimTrie 的查詢效能與 sorted Array 接近,超過經典的B-Tree。拋下索引這個身份,SlimTrie 在各項效能方面表現依舊不俗,作為一個通用 Key-Value 的資料結構,記憶體額外開銷仍遠遠小於經典的 map 和 Btree。
五. SlimTrie ,為未來而生
我們生在最好的時代, 科技爆炸和資訊指數級的增長, 對IT產業帶來了巨大的挑戰, 嚴酷的競爭才是誕生奇蹟的角鬥場, 沒有了平庸的溫床, 每個人都要嘗試把自己的身體打碎, 去涅槃重生, 才有機會給時間長河添一道驚豔的波浪。
當下資訊爆炸增長,陳舊的索引模式已無法適應海量資料新環境,儲存系統海量資料的元資訊管理面臨巨大挑戰,而SlimTrie 提供了一個全新的解決方法,為海量儲存系統帶來一絲曙光,為雲端儲存擁抱海量資料時代注入了強大動力,讓我們看到了未來的無限可能。
SlimTrie 是白山雲端儲存團隊經過長時間研究和探索的產物,在實際使用中的表現沒有辜負我們對它的深厚期望。它的成功不會停下我們開拓的腳步,這只是個開始,還遠沒有結束。
感興趣的朋友,可以掃描下方二維碼,加入“白山雲端儲存技術交流群”,一起碰撞,一起進步。