分布式存儲系統設計(3)—— 存儲結構


在NoSQL存儲系統中,一般都采用Key-Value的數據類型,Key-Value結構簡單,易於存儲,非常適合分布式NoSQL存儲系統。但簡單的數據類型對業務存儲的數據就有一定的局限性,比如需要存儲列表類型的數據。針對這個問題,系統對Key-Value類型的數據做了一些擴展,支持在一個Key下存儲多個字段和列表,擴大了數據存儲的業務場景。本文主要介紹這個分布式存儲系統所支持的數據類型,以及數據在內存中的存儲實現。

數據類型

  1. Key-Value

Key-Value是最簡單的數據類型,Key和Value都不支持結構化數據,業務進行讀寫的時候需要進行序列化和反序列化,系統並不理解數據結構,都當作二進制序列處理,具有通用性,這樣Key和Value就支持任意類型的數據,只要序列化后的數據長度不超過限制。

  1. Key-Fields

Key-Fields支持多個字段,使用整型的Tag進行字段標識,每個Tag對應的Field長度可以不一樣,Field也是序列化的數據,比較通用。對於需要在一個Key的數據中存儲多個字段的場景,Key-Fields相比Key-Value友好了很多。

  1. Key-Rows

Key-Rows支持多個行,每一行就是一個Key-Fields,支持多個字段,各行的Field數量、長度都可以不一樣,非常靈活。Key-Rows可以用來存儲列表,支持更復雜的業務場景,如存放一個用戶的購物訂單信息。

多階哈希表

由於數據是全內存存儲,為了保證進程重啟時不丟失數據,使用共享內存的方式,同時也方便了多個進程訪問數據。對於Key-Value類型的數據,最高效數據結構自然是哈希表。這里介紹一種多階哈希表的實現,具有簡單、高效、魯棒等特點,非常適合工程應用。

多階哈希表采用類似再哈希的思想來解決沖突。如下圖所示,把一個線性數組划分為N階,每一階的桶大小(元素個數)為Ni(1iN),插入數據時,通過Hash(Key)%Ni計算在每一階的位置,從第1階開始,如果出現沖突,則繼續在下一階查找,只到找到空位為止,如果在N階都發生了沖突,則插入失敗,查找的過程需要類似。需要注意的是,在插入一個新的數據時,需要先在所有階都查找一遍,對性能會有一定的影響,例如數據1第一次插入在第3階,隨后在其查找路徑上第2階的數據2被刪除,在數據1再次插入時,會在第2階找到空位,如果插入此位置,第3階的數據2會殘留而無法清理,造成空間的浪費。而對於NoSQL數據存儲系統來說,新增數據占比比較少,大部分操作是數據更新和讀取,所以多階哈希表在這種場景下可以保持比較好的性能。

衡量多階哈希表的好壞主要有2個指標,空間利用率和平均查找次數。通過實驗可知,階數越多,處理沖突的能力越強,空間的利用率也高,但平均查找次數越多,可見空間利用率和平均查找次數相互制約,需要根據實際情況進行權衡。

對於每一階的桶大小,需要選擇不同的質數,可以在數學上證明各階的桶大小互質時,數據在各階的分布會更加均勻,從而降低了沖突的概率。最簡單的方法是各階選擇連續的質數,那有沒有更好的選擇,可以在相同階數下,得到更高的空間利用率和更少的平均查找次數。在每階桶大小相近的情況下,低階會分布更多的數據,所以從低階到高階桶大小遞減的情況下,會提升空間利用率;在階數不變的條件下,為了降低平均查找次數,應該讓低階桶大小越大越好,同時為了保持高階對沖突的處理能力,桶大小不能太小。綜上所述,桶大小從低階到高階依次遞減,在低階遞減比較劇烈,在高階遞減比較平緩,而等比數列比較符合這個特征。桶大小采用系數為1.5的等比數列與桶大小平均分布的對比測試結果如下,使用隨機數進行插入,第一次出現插入失敗時的空間利用率作為最終的結果,可以看出在相同階數下,桶大小為等比數列時會有更高的空間利用率和更少的平均查找次數,階數為20階是比較好的選擇,此時多階哈希表的空間利用率達到93%,平均查找次數約為3次。

在前面所列舉的幾種數據類型,Key和數據都是變長的,而哈希表的節點是定長的,如果把數據直接存在哈希表節點,是非常浪費空間的。為了提高內存的利用率,這里采用一種哈希表和鏈表相結合的數據接口,如下圖所示,哈希表節點存的是數據索引,Key和數據都存放在一個定長塊鏈表中,這樣就可以根據數據大小來分配定長塊的數量,即高效又不浪費空間。定長塊的大小也需要仔細權衡,太大了會導致無效空洞比較大,太小了又會使得每個數據占用的定長塊數量比較多,對性能有一定的影響。

讀寫沖突

對於一塊存儲空間,單進程進行讀寫是最簡單的設計,沒有讀寫沖突的問題,但單進程在處理能力上就會成為瓶頸。為了提升處理能力,有必要使用多進程或多線程並發的方式,這樣就引入了讀寫沖突的問題。

如果多個寫進程並發操作一塊存儲空間,需要使用互斥鎖機制,由於存儲結構使用了鏈表,需要大粒度的鎖才能保證不出現錯鏈,並且鎖機制實現起來也比較復雜。在這一系列的上一篇文章中提高一個更好的解決方法,把單機的存儲空間划分為多個存儲單元,每個存儲單元由一個寫進程處理,這樣在並發的同時也保證了互斥,這種無鎖的實現性能也更好。

解決了寫進程互斥的問題,讀寫進程之間還是會存在沖突,如果某個數據在進行寫操作的過程中,正好讀進程在讀取這個數據,就有可能讀到的是不完整的數據。這個問題可以通過校驗來解決,在數據中記錄一個校驗值,每次寫入就更新校驗值,讀取數據的時候,對數據進行校驗,如果校驗失敗說明出現了讀寫沖突,則需要進行重試。為了降低讀寫沖突的概率,在更新數據的時候,按照下圖所示的流程,並不在原來的定長塊鏈上進行覆寫進行處理,而是分配新的定長塊鏈寫入數據。

大多數使用分布式存儲的業務,都是寫少讀多的場景,在上面的讀寫沖突處理機制下,一個存儲單元是可以同時支持一個寫進程和多個讀進程,使得讀操作有更高的並發能力,非常適合寫少讀多的情況。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM