標籤:
Key-value儲存簡介
具備高可靠性及可擴充性的海量資料存放區對互連網公司來說是一個巨大的挑戰,傳統的資料庫往往很難滿足該需求,並且很多時候對於特定的系統絕大部分的檢索都是基於主鍵的的查詢,在這種情況下使用關係型資料庫將使得效率低下,並且擴充也將成為未來很大的難題。在這樣的情況下,使用Key-value儲存將會是一個很好的選擇。
它被廣泛應用於緩衝,搜尋引擎等等領域。
根據以上的描述,一個好的key-value儲存需要滿足哪些條件呢?
l Availability可用性
l Scalability可擴充性
l Failover故障恢複
l Performance高效能
簡單來說,就是資料不能丟失,服務不能中斷,能對故障進行感知並能自動回復,讀寫效能極高。
檔案儲存體
這一部分比較大,以後會另開主題寫
單檔案還是多檔案
不少nosql的產品採用的是單檔案儲存體的,資料量大以後肯定會遇到效能瓶頸,這一點無需多說,我想強調的是,採用多檔案儲存體資料優點還是非常多的,不過也需要注意,作業系統對於能夠開啟的檔案數目是由限制的,貌似Linux好像是1024(待確認),
Only Append
為了支援更快的寫操作,資料檔案的寫操作只支援append,這個就不多說了,相信大部分的海量儲存設計都是這樣的。因此,更新操作等價於寫操作,不過在寫的時候第一步判斷寫到樹的哪個位置時肯定會定位到樹已有的節點上,這樣可以使得這次寫失效或者直接覆蓋。
這樣存在一個問題,就是對於失效的資料(比如更新過的資料)如何處理,比較好的辦法是啟動獨立線程定時或手動進行清理,請注意,這是一個非常巨大的過程,它將耗光你的CPU和I/O,因為要進行頻繁計算和資料移轉。
資料結構
B Tree家族這一資料結構被廣泛的運用於資料庫索引,如Mssql的B+tree,oracle的B-tree,熟悉索引的朋友一定很清楚,這種資料結構非常適合作為我們的Key-value儲存的資料結構.關於B+tree,可以參見:它是一個多路搜尋樹, 資料存放區在葉子節點上,非葉子節點作為葉子節點的索引,加速資料的尋找,而葉子節點是一個有序的鏈表,每次搜尋都會到達葉子節點才會結束,插入新資料可能會引起節點的分裂。
在本篇文章中,你需要知道,上層的節點成為IN(Internal Node),它持有其他節點的引用,葉子節點的上層是(Bottom Internal Node),而葉子節點則是儲存資料的節點。
圖片來自:http://blog.csdn.net/manesking/archive/2007/02/09/1505979.aspx
這部分是純粹的資料結構,就不多說了,如果想深入瞭解的話可以看看這篇論文《The Ubiquitous B-Tree》
設計要點Partition
因為系統要具備高擴充性,因此,增加刪除機器是頻繁的操作,如何將資料均勻分散到叢集中呢?比較常用的辦法是hash模數的辦法,但是這樣一來,增加機器的瞬間,按照之前的hash模數方式,資料無法讀取,這意味著需要對資料進行遷移,等待機器預熱,這是很不好的辦法。
目前比較公認的解決辦法就是一致性雜湊(consistent hashing)
首先按照機器的hash進行順時針分布,,目前有5台機器,如果有一個讀寫請求,那麼hash該key值得的一個hash值,定位到環上,如果沒有定位到具體的機器,那麼按照順時針尋找,找到的第一個機器就是目標節點。
如果需要新增機器,增加過程為,首先hash新機器得到其位置,加入新機器F,這時存取原則不變,那麼按照之前的策略,如果hash到C-F之間的資料就無法找到了,但是這樣一來影響就局限於C-F之間,不象之前需要整體遷移了。
最後,為了降低增加機器所帶來的影響,我們可以為其增加虛擬節點(virtual nodes)。這樣的話伺服器在環上的分布就比較均勻,這樣多個虛擬節點將對應一個我們的物理節點,增加機器所受到的影響也會變得最小。
Replication
為了達到高可用性和資料不丟失,我們需要通過複製將資料備份到多台機器,replication的實現機制一般是通過Master與replica之間的TCP/IP串連,然後根據相應的一致性策略將資料分發到replica上,這裡的一致性策略主要包括兩項:
- replica能夠延遲master的時間,這個的意思就是說,在這個時間內更新的資料,replica可能是看不到的。例如你設定的一致性時間是3s,那麼在某個特定的時刻,replica上的資料實際上可能是master3s以前的snapshot。
- master事務提交返回之前是否需要得到replica的確認。為了盡量保證資料不丟失,master需要得到一定數量的replica確認資料更新成功之後才能提交事務。
關於資料可靠性和效能之間,是需要進行折衷的,很顯然,越是高的資料保障,那麼效能肯定會受到影響。在這樣的情況下,需要對上層的應用進行分析,看是否允許丟失一部分資料。
另外,還有一個問題就是,資料的同步是採用master分發還是replica定時請求的問題,兩者各有優缺點,前者會在replica較多的情況下遇到瓶頸,而後者可能會有一些延遲。多級同步的方式能在一定程度上解決這個問題,即master向某些機器同步,而這些機器向其他機器同步。
當然,master管理寫請求而replica管理讀請求,至於如何決定讀寫請求的分發,我們可以使用monitor節點,由它來作為讀寫的入口, 如,然後Monitor管理叢集的狀態和生命週期,例如Master fail後,monitor將收到事件,它將發起一次選舉選出新的Master,一般的選舉演算法就是在叢集中尋找最後一次更新的節點,因為往往它的資料是最新。還有就是在有新的機器加入叢集的情況下,Monitor會告訴新機器叢集內的master是誰,replica機器才能與master取得串連同步資料。
<!--[if gte mso 9]><xml> <o:OLEObject Type="Embed" ProgID="Visio.Drawing.11" ShapeID="_x0000_i1025" DrawAspect="Content" ObjectID="_1344072934"> </o:OLEObject> </xml><![endif]-->
使用Monitor的Master-replica叢集
當然,自從有了ZooKeeper,這種監控和協同的髒活累活就可以都交給它了,利用ZooKeeper管理叢集內節點的健康情況具備很大的便利,畢竟,上面這個辦法的架構存在單點問題,最後肯定Monitor會拖叢集的後腿。所以用ZooKeeper管理叢集並且針對不同的事件作出響應。下面是一個:
<!--[if gte mso 9]><xml> <o:OLEObject Type="Embed" ProgID="Visio.Drawing.11" ShapeID="_x0000_i1026" DrawAspect="Content" ObjectID="_1344072935"> </o:OLEObject> </xml><![endif]-->
用ZooKeeper管理叢集
最後,關於事務提交時的處理策略也值得注意,你需要為master和replica都指定.一般情況下,我們需要在減少頻繁的I/O操作和資料保障性方面進行折衷,以下提交策略可選:
- 在事務提交時將資料由記憶體刷到硬碟,這樣資料具有最高的保障,但是你需要等待昂貴的I/O操作。
- 在事務提交時將資料刷到作業系統buffer中,然後由作業系統自己決定何時刷到硬碟。這樣可以在虛擬機器掛了的情況下保證資料不丟失,但是不能hardware failture
- 在事務提交時資料仍然維持在記憶體中,而在儲存系統比較閑的時候進行持久到硬碟的操作,或在記憶體不夠用的情況下進行寫磁碟操作。
Get和Put操作
這兩個動作是key-value儲存系統最核心的兩個操作,因此在設計上需要做更多的考慮。
下面是一種寫操作的過程:
- 執行tree搜尋以定位插入資料所在的位置
- 鎖定該位置的父節點
- 建立新的葉子節點
- 將葉子節點寫入。這個寫操作發生在記憶體中,並且返回一個number它決定了葉子節點寫到硬碟的位置
- 修改父節點指向該葉子節點的引用。該父節點既持有指向記憶體的葉子節點的引用又持有磁碟的位置的number。
- 標記該父節點為dirty(意味著記憶體中的版本沒有出現在磁碟中)
- 解鎖該父節點
請注意,很明顯,這個寫操作完全發生在記憶體中,那麼何時將資料同步到磁碟呢?這就是後面要講的checkpoint。通過它定時的將記憶體中的髒資料寫到硬碟。
讀操作就簡單很多了,搜尋tree,定位到葉子節點,取出資料就行了,當然還有一些包括為快取服務的操作就不細講了(比如,為每個節點計數,每次對該節點的訪問都使得其+1,這樣在緩衝evict的時候就很有用了)
上面所說的寫操作在記憶體中並不會影響到讀操作,因為,為了加快讀操作,我們會在啟動時預先載入硬碟資料檔案的內容到記憶體,由於只有葉子節點儲存資料,因此我們需要根據載入的葉子節點還原整棵B+tree,毫無疑問這是一個耗時的操作,但是卻是值得的。
資料模型
這塊比較大,這裡只重點講一下以什麼資料結構存取的問題。
首先需要解決的是,儲存物件的問題,很顯然,我們都有存取對象的需求,那麼如何將對象轉換為我們的底層儲存格式呢?一般的辦法有序列化,Json,XML之類,下面依次講一下優缺點:
- 序列化。可能是比較簡單易實現的辦法,但是空間佔用過大
- Json和XML都差不多,儲存格式比較可讀一點,解析和轉換比較方便,不過對於資料量大的情況還是不推薦。
- 字串或者位元組數組。我們按照一定的約定將對象拼成字串,或者一次將對象的屬性寫入到位元組數組,讀取時按照相同的順序解析即可,比較好的辦法是定義一個介面,然後由用戶端去實現對象字串之間轉換順序的方法。這個比較推薦。
還有一些序列化的工具值得推薦,比如hadoop下的avro。
Checkpoint
和普通的關係型資料庫一樣,key-value也可以有自己的checkpoint,一般情況,checkpoint是為了減少資料恢複所需要的時間.在檢查點到來時,按照之前的設計,它會將所有的dirty Internal Node寫入log,這樣會存在一個問題,大多數情況下,checkpoint會把整棵樹寫到log,解決問題的辦法是我們採用增量的辦法進行log,例如,如果有a和b加入到某個父節點。那麼此時如果進行checkpoint時我們需要首先寫一個完整的IN引用,並且記錄對其進行的操作,add a,add b,這些操作記錄在一個list中,當list變得過大以後我們又重新寫一個完整的IN節點。
到期資料清理
這一部分只針對按照順序寫並且僅append的情況,為了減少I/O操作,無效資料僅僅被標記為delete且刪除記憶體中對應的樹的分葉節點而不進行物理的刪除,那麼長期下去,失效資料會很多,這時候需要進行清理,一般的策略就是,當失效資料在檔案中所佔比例達到一定程度以後,執行清除操作。
- 首先根據預先儲存的記錄資訊判斷哪些檔案需要進行清理操作。
- 掃描檔案,找到仍然active的資料,拷貝到新的檔案,掃描完成後刪除此檔案。
請注意,在寫操作密集的情況下,這會造成競爭,因此盡量在訪問量少的情況下執行此操作。
另外,可以使用多線程來進行清理操作。當然還有很多策略可以在清理的時候使用,比如,緩衝一個葉子節點的父節點,在鎖住該父節點執行遷移操作的時候可以順便掃描該父節點下的其他葉子節點。
Need More?複雜查詢
人的慾望是無止境的…….除了基於主鍵的檢索你可能還需要基於某個屬性的檢索,最好還能在多個屬性上查詢完了以後來個取交集,這個時候怎麼辦呢?
首先,我們有一個基於主鍵的key-value資料庫,key儲存的主鍵,而value儲存的對象(實體儲存體為byte數組),那麼假如我們想對對象的某個屬性如name進行查詢的話,那麼我們可以再建一個資料庫,key是待查詢的欄位,value是主鍵資料庫的id。
Primary Database
|
Key(ID) |
Value(Object) |
|
1 |
Byte[] |
|
2 |
Byte[] |
Secondary Database
|
Foreign Key(Name) |
Value(ID) |
|
dahuang |
2 |
|
tugou |
1 |
這樣一來按照name查詢只需要查詢一次secondary database取出主鍵id,再查一查primary database即可,這有點像正排索引和倒排索引的機率,如果對於多個欄位的組合查詢,只需要對其進行一次join即可,在join的時候可以先對待join的結果按照結果集大小進行排序,這樣可以省下不少時間消耗。
雖然key-value儲存按照上面的描述也可以支援多條件查詢,但是不建議這樣做,一是建立索引(二級資料庫)需要額外的空間,二是這樣需要多次查詢影響效能,不管怎麼樣適度的折衷吧。
最後不得不提一下,由於B+tree的資料結構,它很好地支援範圍查詢(查詢可以不下到葉子節點)可以極大的彌補搜尋引擎中倒排索引進行範圍查詢需要全部掃描的缺陷。這也是其應用情境之一。
總結
Key-value在海量資料存放區中佔據很重要的地位,對於它的深入研究能帶給我們很多啟發,而它在某些局部問題上所表現的優秀的能力也值得我們關注。本文大致總結了一下目前所瞭解的一些問題,沒有提到的東西還有很多(檔案系統設計,事務,緩衝等等),接下來如果有空會對檔案系統設計進行詳細講解。
聲明
首先,本文只是代表本人的一些淺見,不代表任何官方意見。其次,由於作者水平的原因,肯定會出現錯誤,歡迎指正(最好站內,給我留一點臉面-_-)
參考文獻:
Dynamo: Amazon’s Highly Available Key-value Store
http://blog.csdn.net/manesking/archive/2007/02/09/1505979.aspx (BTree,B-Tree,B+Tree,B*Tree都是什麼)
海量資料存放區之Key-Value儲存簡介