提及LevelDb也許您不清楚,可是若是做爲IT工程師,不知道下面兩位大神級別的工程師,那您的領導估計會Hold不住了:Jeff Dean和Sanjay Ghemawat。這兩位是Google公司重量級的工程師,爲數甚少的Google Fellow之二。html
Jeff Dean其人:http://research.google.com/people/jeff/index.html,Google大規模分佈式平臺Bigtable和MapReduce主要設計和實現者。算法
Sanjay Ghemawat其人:http://research.google.com/people/sanjay/index.html,Google大規模分佈式平臺GFS,Bigtable和MapReduce主要設計和實現工程師。編程
LevelDb就是這兩位大神級別的工程師發起的開源項目,簡而言之,LevelDb是可以處理十億級別規模Key-Value型數據持久性存儲的C++ 程序庫。正像上面介紹的,這二位是Bigtable的設計和實現者,若是瞭解Bigtable的話,應該知道在這個影響深遠的分佈式存儲系統中有兩個核心的部分:Master Server和Tablet Server。其中Master Server作一些管理數據的存儲以及分佈式調度工做,實際的分佈式數據存儲以及讀寫操做是由Tablet Server完成的,而LevelDb則能夠理解爲一個簡化版的Tablet Server。緩存
LevelDb有以下一些特色:服務器
首先,LevelDb是一個持久化存儲的KV系統,和Redis這種內存型的KV系統不一樣,LevelDb不會像Redis同樣狂吃內存,而是將大部分數據存儲到磁盤上。數據結構
其次,LevleDb在存儲數據時,是根據記錄的key值有序存儲的,就是說相鄰的key值在存儲文件中是依次順序存儲的,而應用能夠自定義key大小比較函數,LevleDb會按照用戶定義的比較函數依序存儲這些記錄。架構
再次,像大多數KV系統同樣,LevelDb的操做接口很簡單,基本操做包括寫記錄,讀記錄以及刪除記錄。也支持針對多條操做的原子批量操做。app
另外,LevelDb支持數據快照(snapshot)功能,使得讀取操做不受寫操做影響,能夠在讀操做過程當中始終看到一致的數據。分佈式
除此外,LevelDb還支持數據壓縮等操做,這對於減少存儲空間以及增快IO效率都有直接的幫助。函數
LevelDb性能很是突出,官方網站報道其隨機寫性能達到40萬條記錄每秒,而隨機讀性能達到6萬條記錄每秒。整體來講,LevelDb的寫操做要大大快於讀操做,而順序讀寫操做則大大快於隨機讀寫操做。至於爲什麼是這樣,看了咱們後續推出的LevelDb日知錄,估計您會了解其內在緣由。
LevelDb本質上是一套存儲系統以及在這套存儲系統上提供的一些操做接口。爲了便於理解整個系統及其處理流程,咱們能夠從兩個不一樣的角度來看待LevleDb:靜態角度和動態角度。從靜態角度,能夠假想整個系統正在運行過程當中(不斷插入刪除讀取數據),此時咱們給LevelDb照相,從照片能夠看到以前系統的數據在內存和磁盤中是如何分佈的,處於什麼狀態等;從動態的角度,主要是瞭解系統是如何寫入一條記錄,讀出一條記錄,刪除一條記錄的,同時也包括除了這些接口操做外的內部操做好比compaction,系統運行時崩潰後如何恢復系統等等方面。
本節所講的總體架構主要從靜態角度來描述,以後接下來的幾節內容會詳述靜態結構涉及到的文件或者內存數據結構,LevelDb日知錄後半部分主要介紹動態視角下的LevelDb,就是說整個系統是怎麼運轉起來的。
LevelDb做爲存儲系統,數據記錄的存儲介質包括內存以及磁盤文件,若是像上面說的,當LevelDb運行了一段時間,此時咱們給LevelDb進行透視拍照,那麼您會看到以下一番景象:
圖1.1:LevelDb結構
從圖中能夠看出,構成LevelDb靜態結構的包括六個主要部分:內存中的MemTable和Immutable MemTable以及磁盤上的幾種主要文件:Current文件,Manifest文件,log文件以及SSTable文件。固然,LevelDb除了這六個主要部分還有一些輔助的文件,可是以上六個文件和數據結構是LevelDb的主體構成元素。
LevelDb的Log文件和Memtable與Bigtable論文中介紹的是一致的,當應用寫入一條Key:Value記錄的時候,LevelDb會先往log文件裏寫入,成功後將記錄插進Memtable中,這樣基本就算完成了寫入操做,由於一次寫入操做只涉及一次磁盤順序寫和一次內存寫入,因此這是爲什麼說LevelDb寫入速度極快的主要緣由。
Log文件在系統中的做用主要是用於系統崩潰恢復而不丟失數據,假如沒有Log文件,由於寫入的記錄剛開始是保存在內存中的,此時若是系統崩潰,內存中的數據尚未來得及Dump到磁盤,因此會丟失數據(Redis就存在這個問題)。爲了不這種狀況,LevelDb在寫入內存前先將操做記錄到Log文件中,而後再記入內存中,這樣即便系統崩潰,也能夠從Log文件中恢復內存中的Memtable,不會形成數據的丟失。
當Memtable插入的數據佔用內存到了一個界限後,須要將內存的記錄導出到外存文件中,LevleDb會生成新的Log文件和Memtable,原先的Memtable就成爲Immutable Memtable,顧名思義,就是說這個Memtable的內容是不可更改的,只能讀不能寫入或者刪除。新到來的數據被記入新的Log文件和Memtable,LevelDb後臺調度會將Immutable Memtable的數據導出到磁盤,造成一個新的SSTable文件。SSTable就是由內存中的數據不斷導出並進行Compaction操做後造成的,並且SSTable的全部文件是一種層級結構,第一層爲Level 0,第二層爲Level 1,依次類推,層級逐漸增高,這也是爲什麼稱之爲LevelDb的緣由。
SSTable中的文件是Key有序的,就是說在文件中小key記錄排在大Key記錄以前,各個Level的SSTable都是如此,可是這裏須要注意的一點是:Level 0的SSTable文件(後綴爲.sst)和其它Level的文件相比有特殊性:這個層級內的.sst文件,兩個文件可能存在key重疊,好比有兩個level 0的sst文件,文件A和文件B,文件A的key範圍是:{bar, car},文件B的Key範圍是{blue,samecity},那麼極可能兩個文件都存在key=」blood」的記錄。對於其它Level的SSTable文件來講,則不會出現同一層級內.sst文件的key重疊現象,就是說Level L中任意兩個.sst文件,那麼能夠保證它們的key值是不會重疊的。這點須要特別注意,後面您會看到不少操做的差別都是因爲這個緣由形成的。
SSTable中的某個文件屬於特定層級,並且其存儲的記錄是key有序的,那麼必然有文件中的最小key和最大key,這是很是重要的信息,LevelDb應該記下這些信息。Manifest就是幹這個的,它記載了SSTable各個文件的管理信息,好比屬於哪一個Level,文件名稱叫啥,最小key和最大key各自是多少。下圖是Manifest所存儲內容的示意:
圖2.1:Manifest存儲示意圖
圖中只顯示了兩個文件(manifest會記載全部SSTable文件的這些信息),即Level 0的test.sst1和test.sst2文件,同時記載了這些文件各自對應的key範圍,好比test.sstt1的key範圍是「an」到 「banana」,而文件test.sst2的key範圍是「baby」到「samecity」,能夠看出二者的key範圍是有重疊的。
Current文件是幹什麼的呢?這個文件的內容只有一個信息,就是記載當前的manifest文件名。由於在LevleDb的運行過程當中,隨着Compaction的進行,SSTable文件會發生變化,會有新的文件產生,老的文件被廢棄,Manifest也會跟着反映這種變化,此時每每會新生成Manifest文件來記載這種變化,而Current則用來指出哪一個Manifest文件纔是咱們關心的那個Manifest文件。
以上介紹的內容就構成了LevelDb的總體靜態結構,在LevelDb日知錄接下來的內容中,咱們會首先介紹重要文件或者內存數據的具體數據佈局與結構。
上節內容講到log文件在LevelDb中的主要做用是系統故障恢復時,可以保證不會丟失數據。由於在將記錄寫入內存的Memtable以前,會先寫入Log文件,這樣即便系統發生故障,Memtable中的數據沒有來得及Dump到磁盤的SSTable文件,LevelDB也能夠根據log文件恢復內存的Memtable數據結構內容,不會形成系統丟失數據,在這點上LevelDb和Bigtable是一致的。
下面咱們帶你們看看log文件的具體物理和邏輯佈局是怎樣的,LevelDb對於一個log文件,會把它切割成以32K爲單位的物理Block,每次讀取的單位以一個Block做爲基本讀取單位,下圖展現的log文件由3個Block構成,因此從物理佈局來說,一個log文件就是由連續的32K大小Block構成的。
圖3.1 log文件佈局
在應用的視野裏是看不到這些Block的,應用看到的是一系列的Key:Value對,在LevelDb內部,會將一個Key:Value對看作一條記錄的數據,另外在這個數據前增長一個記錄頭,用來記載一些管理信息,以方便內部處理,圖3.2顯示了一個記錄在LevelDb內部是如何表示的。
圖3.2 記錄結構
記錄頭包含三個字段,ChechSum是對「類型」和「數據」字段的校驗碼,爲了不處理不完整或者是被破壞的數據,當LevelDb讀取記錄數據時候會對數據進行校驗,若是發現和存儲的CheckSum相同,說明數據完整無破壞,能夠繼續後續流程。「記錄長度」記載了數據的大小,「數據」則是上面講的Key:Value數值對,「類型」字段則指出了每條記錄的邏輯結構和log文件物理分塊結構之間的關係,具體而言,主要有如下四種類型:FULL/FIRST/MIDDLE/LAST。
若是記錄類型是FULL,表明了當前記錄內容完整地存儲在一個物理Block裏,沒有被不一樣的物理Block切割開;若是記錄被相鄰的物理Block切割開,則類型會是其餘三種類型中的一種。咱們以圖3.1所示的例子來具體說明。
假設目前存在三條記錄,Record A,Record B和Record C,其中Record A大小爲10K,Record B 大小爲80K,Record C大小爲12K,那麼其在log文件中的邏輯佈局會如圖3.1所示。Record A是圖中藍色區域所示,由於大小爲10K<32K,可以放在一個物理Block中,因此其類型爲FULL;Record B 大小爲80K,而Block 1由於放入了Record A,因此還剩下22K,不足以放下Record B,因此在Block 1的剩餘部分放入Record B的開頭一部分,類型標識爲FIRST,表明了是一個記錄的起始部分;Record B還有58K沒有存儲,這些只能依次放在後續的物理Block裏面,由於Block 2大小隻有32K,仍然放不下Record B的剩餘部分,因此Block 2所有用來放Record B,且標識類型爲MIDDLE,意思是這是Record B中間一段數據;Record B剩下的部分能夠徹底放在Block 3中,類型標識爲LAST,表明了這是Record B的末尾數據;圖中黃色的Record C由於大小爲12K,Block 3剩下的空間足以所有放下它,因此其類型標識爲FULL。
從這個小例子能夠看出邏輯記錄和物理Block之間的關係,LevelDb一次物理讀取爲一個Block,而後根據類型狀況拼接出邏輯記錄,供後續流程處理。
SSTable是Bigtable中相當重要的一塊,對於LevelDb來講也是如此,對LevelDb的SSTable實現細節的瞭解也有助於瞭解Bigtable中一些實現細節。
本節內容主要講述SSTable的靜態佈局結構,咱們曾在「LevelDb日知錄之二:總體架構」中說過,SSTable文件造成了不一樣Level的層級結構,至於這個層級結構是如何造成的咱們放在後面Compaction一節細說。本節主要介紹SSTable某個文件的物理佈局和邏輯佈局結構,這對了解LevelDb的運行過程頗有幫助。
LevelDb不一樣層級有不少SSTable文件(之後綴.sst爲特徵),全部.sst文件內部佈局都是同樣的。上節介紹Log文件是物理分塊的,SSTable也同樣會將文件劃分爲固定大小的物理存儲塊,可是二者邏輯佈局大不相同,根本緣由是:Log文件中的記錄是Key無序的,即前後記錄的key大小沒有明確大小關係,而.sst文件內部則是根據記錄的Key由小到大排列的,從下面介紹的SSTable佈局能夠體會到Key有序是爲什麼如此設計.sst文件結構的關鍵。
圖4.1 .sst文件的分塊結構
圖4.1展現了一個.sst文件的物理劃分結構,同Log文件同樣,也是劃分爲固定大小的存儲塊,每一個Block分爲三個部分,紅色部分是數據存儲區, 藍色的Type區用於標識數據存儲區是否採用了數據壓縮算法(Snappy壓縮或者無壓縮兩種),CRC部分則是數據校驗碼,用於判別數據是否在生成和傳輸中出錯。
以上是.sst的物理佈局,下面介紹.sst文件的邏輯佈局,所謂邏輯佈局,就是說盡管你們都是物理塊,可是每一塊存儲什麼內容,內部又有什麼結構等。圖4.2展現了.sst文件的內部邏輯解釋。
圖4.2 邏輯佈局
從圖4.2能夠看出,從大的方面,能夠將.sst文件劃分爲數據存儲區和數據管理區,數據存儲區存放實際的Key:Value數據,數據管理區則提供一些索引指針等管理數據,目的是更快速便捷的查找相應的記錄。兩個區域都是在上述的分塊基礎上的,就是說文-件的前面若干塊實際存儲KV數據,後面數據管理區存儲管理數據。管理數據又分爲四種不一樣類型:紫色的Meta Block,紅色的MetaBlock 索引和藍色的數據索引塊以及一個文件尾部塊。
LevelDb 1.2版對於Meta Block尚無實際使用,只是保留了一個接口,估計會在後續版本中加入內容,下面咱們看看數據索引區和文件尾部Footer的內部結構。
圖4.3 數據索引
圖4.3是數據索引的內部結構示意圖。再次強調一下,Data Block內的KV記錄是按照Key由小到大排列的,數據索引區的每條記錄是對某個Data Block創建的索引信息,每條索引信息包含三個內容,以圖4.3所示的數據塊i的索引Index i來講:紅色部分的第一個字段記載大於等於數據塊i中最大的Key值的那個Key,第二個字段指出數據塊i在.sst文件中的起始位置,第三個字段指出Data Block i的大小(有時候是有數據壓縮的)。後面兩個字段好理解,是用於定位數據塊在文件中的位置的,第一個字段須要詳細解釋一下,在索引裏保存的這個Key值未必必定是某條記錄的Key,以圖4.3的例子來講,假設數據塊i 的最小Key=「samecity」,最大Key=「the best」;數據塊i+1的最小Key=「the fox」,最大Key=「zoo」,那麼對於數據塊i的索引Index i來講,其第一個字段記載大於等於數據塊i的最大Key(「the best」)同時要小於數據塊i+1的最小Key(「the fox」),因此例子中Index i的第一個字段是:「the c」,這個是知足要求的;而Index i+1的第一個字段則是「zoo」,即數據塊i+1的最大Key。
文件末尾Footer塊的內部結構見圖4.4,metaindex_handle指出了metaindex block的起始位置和大小;inex_handle指出了index Block的起始地址和大小;這兩個字段能夠理解爲索引的索引,是爲了正確讀出索引值而設立的,後面跟着一個填充區和魔數。
圖4.4 Footer
上面主要介紹的是數據管理區的內部結構,下面咱們看看數據區的一個Block的數據部分內部是如何佈局的(圖4.1中的紅色部分),圖4.5是其內部佈局示意圖。
圖4.5 數據Block內部結構
從圖中能夠看出,其內部也分爲兩個部分,前面是一個個KV記錄,其順序是根據Key值由小到大排列的,在Block尾部則是一些「重啓點」(Restart Point),實際上是一些指針,指出Block內容中的一些記錄位置。
「重啓點」是幹什麼的呢?咱們一再強調,Block內容裏的KV記錄是按照Key大小有序的,這樣的話,相鄰的兩條記錄極可能Key部分存在重疊,好比key i=「the Car」,Key i+1=「the color」,那麼二者存在重疊部分「the c」,爲了減小Key的存儲量,Key i+1能夠只存儲和上一條Key不一樣的部分「olor」,二者的共同部分從Key i中能夠得到。記錄的Key在Block內容部分就是這麼存儲的,主要目的是減小存儲開銷。「重啓點」的意思是:在這條記錄開始,再也不採起只記載不一樣的Key部分,而是從新記錄全部的Key值,假設Key i+1是一個重啓點,那麼Key裏面會完整存儲「the color」,而不是採用簡略的「olor」方式。Block尾部就是指出哪些記錄是這些重啓點圖4.6 記錄格式
在Block內容區,每一個KV記錄的內部結構是怎樣的?圖4.6給出了其詳細結構,每一個記錄包含5個字段:key共享長度,好比上面的「olor」記錄, 其key和上一條記錄共享的Key部分長度是「the c」的長度,即5;key非共享長度,對於「olor」來講,是4;value長度指出Key:Value中Value的長度,在後面的Value內容字段中存儲實際的Value值;而key非共享內容則實際存儲「olor」這個Key字符串。
上面講的這些就是.sst文件的所有內部奧祕。
LevelDb日知錄前述小節大體講述了磁盤文件相關的重要靜態結構,本小節講述內存中的數據結構Memtable,Memtable在整個體系中的重要地位也不言而喻。整體而言,全部KV數據都是存儲在Memtable,Immutable Memtable和SSTable中的,Immutable Memtable從結構上講和Memtable是徹底同樣的,區別僅僅在於其是隻讀的,不容許寫入操做,而Memtable則是容許寫入和讀取的。當Memtable寫入的數據佔用內存到達指定數量,則自動轉換爲Immutable Memtable,等待Dump到磁盤中,系統會自動生成新的Memtable供寫操做寫入新數據,理解了Memtable,那麼Immutable Memtable天然不在話下。
LevelDb的MemTable提供了將KV數據寫入,刪除以及讀取KV記錄的操做接口,可是事實上Memtable並不存在真正的刪除操做,刪除某個Key的Value在Memtable內是做爲插入一條記錄實施的,可是會打上一個Key的刪除標記,真正的刪除操做是Lazy的,會在之後的Compaction過程當中去掉這個KV。
須要注意的是,LevelDb的Memtable中KV對是根據Key大小有序存儲的,在系統插入新的KV時,LevelDb要把這個KV插到合適的位置上以保持這種Key有序性。其實,LevelDb的Memtable類只是一個接口類,真正的操做是經過背後的SkipList來作的,包括插入操做和讀取操做等,因此Memtable的核心數據結構是一個SkipList。
SkipList是由William Pugh發明。他在Communications of the ACM June 1990, 33(6) 668-676 發表了Skip lists: a probabilistic alternative to balanced trees,在該論文中詳細解釋了SkipList的數據結構和插入刪除操做。
SkipList是平衡樹的一種替代數據結構,可是和紅黑樹不相同的是,SkipList對於樹的平衡的實現是基於一種隨機化的算法的,這樣也就是說SkipList的插入和刪除的工做是比較簡單的。
關於SkipList的詳細介紹能夠參考這篇文章:http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html,講述的很清楚,LevelDb的SkipList基本上是一個具體實現,並沒有特殊之處。
SkipList不只是維護有序數據的一個簡單實現,並且相比較平衡樹來講,在插入數據的時候能夠避免頻繁的樹節點調整操做,因此寫入效率是很高的,LevelDb總體而言是個高寫入系統,SkipList在其中應該也起到了很重要的做用。Redis爲了加快插入操做,也使用了SkipList來做爲內部實現數據結構。
在以前的五節LevelDb日知錄中,咱們介紹了LevelDb的一些靜態文件及其詳細布局,從本節開始,咱們看看LevelDb的一些動態操做,好比讀寫記錄,Compaction,錯誤恢復等操做。
本節介紹levelDb的記錄更新操做,即插入一條KV記錄或者刪除一條KV記錄。levelDb的更新操做速度是很是快的,源於其內部機制決定了這種更新操做的簡單性。
圖6.1 LevelDb寫入記錄
圖6.1是levelDb如何更新KV數據的示意圖,從圖中能夠看出,對於一個插入操做Put(Key,Value)來講,完成插入操做包含兩個具體步驟:首先是將這條KV記錄以順序寫的方式追加到以前介紹過的log文件末尾,由於儘管這是一個磁盤讀寫操做,可是文件的順序追加寫入效率是很高的,因此並不會致使寫入速度的下降;第二個步驟是:若是寫入log文件成功,那麼將這條KV記錄插入內存中的Memtable中,前面介紹過,Memtable只是一層封裝,其內部實際上是一個Key有序的SkipList列表,插入一條新記錄的過程也很簡單,即先查找合適的插入位置,而後修改相應的連接指針將新記錄插入便可。完成這一步,寫入記錄就算完成了,因此一個插入記錄操做涉及一次磁盤文件追加寫和內存SkipList插入操做,這是爲什麼levelDb寫入速度如此高效的根本緣由。
從上面的介紹過程當中也能夠看出:log文件內是key無序的,而Memtable中是key有序的。那麼若是是刪除一條KV記錄呢?對於levelDb來講,並不存在當即刪除的操做,而是與插入操做相同的,區別是,插入操做插入的是Key:Value 值,而刪除操做插入的是「Key:刪除標記」,並不真正去刪除記錄,而是後臺Compaction的時候纔去作真正的刪除操做。
levelDb的寫入操做就是如此簡單。真正的麻煩在後面將要介紹的讀取操做中。
LevelDb是針對大規模Key/Value數據的單機存儲庫,從應用的角度來看,LevelDb就是一個存儲工具。而做爲稱職的存儲工具,常見的調用接口無非是新增KV,刪除KV,讀取KV,更新Key對應的Value值這麼幾種操做。LevelDb的接口沒有直接支持更新操做的接口,若是須要更新某個Key的Value,你能夠選擇直接生猛地插入新的KV,保持Key相同,這樣系統內的key對應的value就會被更新;或者你能夠先刪除舊的KV, 以後再插入新的KV,這樣比較委婉地完成KV的更新操做。
假設應用提交一個Key值,下面咱們看看LevelDb是如何從存儲的數據中讀出其對應的Value值的。圖7-1是LevelDb讀取過程的總體示意圖。
圖7-1 LevelDb讀取記錄流程
LevelDb首先會去查看內存中的Memtable,若是Memtable中包含key及其對應的value,則返回value值便可;若是在Memtable沒有讀到key,則接下來到一樣處於內存中的Immutable Memtable中去讀取,相似地,若是讀到就返回,如果沒有讀到,那麼只能萬般無奈下從磁盤中的大量SSTable文件中查找。由於SSTable數量較多,並且分紅多個Level,因此在SSTable中讀數據是至關蜿蜒曲折的一段旅程。總的讀取原則是這樣的:首先從屬於level 0的文件中查找,若是找到則返回對應的value值,若是沒有找到那麼到level 1中的文件中去找,如此循環往復,直到在某層SSTable文件中找到這個key對應的value爲止(或者查到最高level,查找失敗,說明整個系統中不存在這個Key)。
那麼爲何是從Memtable到Immutable Memtable,再從Immutable Memtable到文件,而文件中爲什麼是從低level到高level這麼一個查詢路徑呢?道理何在?之因此選擇這麼個查詢路徑,是由於從信息的更新時間來講,很明顯Memtable存儲的是最新鮮的KV對;Immutable Memtable中存儲的KV數據對的新鮮程度次之;而全部SSTable文件中的KV數據新鮮程度必定不如內存中的Memtable和Immutable Memtable的。對於SSTable文件來講,若是同時在level L和Level L+1找到同一個key,level L的信息必定比level L+1的要新。也就是說,上面列出的查找路徑就是按照數據新鮮程度排列出來的,越新鮮的越先查找。
爲啥要優先查找新鮮的數據呢?這個道理不言而喻,舉個例子。好比咱們先往levelDb裏面插入一條數據 {key="www.samecity.com" value="咱們"},過了幾天,samecity網站更名爲:69同城,此時咱們插入數據{key="www.samecity.com" value="69同城"},一樣的key,不一樣的value;邏輯上理解好像levelDb中只有一個存儲記錄,即第二個記錄,可是在levelDb中極可能存在兩條記錄,即上面的兩個記錄都在levelDb中存儲了,此時若是用戶查詢key="www.samecity.com",咱們固然但願找到最新的更新記錄,也就是第二個記錄返回,這就是爲什麼要優先查找新鮮數據的緣由。
前文有講:對於SSTable文件來講,若是同時在level L和Level L+1找到同一個key,level L的信息必定比level L+1的要新。這是一個結論,理論上須要一個證實過程,不然會招致以下的問題:爲神馬呢?從道理上講呢,很明白:由於Level L+1的數據不是從石頭縫裏蹦出來的,也不是作夢夢到的,那它是從哪裏來的?Level L+1的數據是從Level L 通過Compaction後獲得的(若是您不知道什麼是Compaction,那麼........也許之後會知道的),也就是說,您看到的如今的Level L+1層的SSTable數據是從原來的Level L中來的,如今的Level L比原來的Level L數據要新鮮,因此可證,如今的Level L比如今的Level L+1的數據要新鮮。
SSTable文件不少,如何快速地找到key對應的value值?在LevelDb中,level 0一直都愛搞特殊化,在level 0和其它level中查找某個key的過程是不同的。由於level 0下的不一樣文件可能key的範圍有重疊,某個要查詢的key有可能多個文件都包含,這樣的話LevelDb的策略是先找出level 0中哪些文件包含這個key(manifest文件中記載了level和對應的文件及文件裏key的範圍信息,LevelDb在內存中保留這種映射表), 以後按照文件的新鮮程度排序,新的文件排在前面,以後依次查找,讀出key對應的value。而若是是非level 0的話,由於這個level的文件之間key是不重疊的,因此只從一個文件就能夠找到key對應的value。
最後一個問題,若是給定一個要查詢的key和某個key range包含這個key的SSTable文件,那麼levelDb是如何進行具體查找過程的呢?levelDb通常會先在內存中的Cache中查找是否包含這個文件的緩存記錄,若是包含,則從緩存中讀取;若是不包含,則打開SSTable文件,同時將這個文件的索引部分加載到內存中並放入Cache中。 這樣Cache裏面就有了這個SSTable的緩存項,可是隻有索引部分在內存中,以後levelDb根據索引能夠定位到哪一個內容Block會包含這條key,從文件中讀出這個Block的內容,在根據記錄一一比較,若是找到則返回結果,若是沒有找到,那麼說明這個level的SSTable文件並不包含這個key,因此到下一級別的SSTable中去查找。
從以前介紹的LevelDb的寫操做和這裏介紹的讀操做能夠看出,相對寫操做,讀操做處理起來要複雜不少,因此寫的速度必然要遠遠高於讀數據的速度,也就是說,LevelDb比較適合寫操做多於讀操做的應用場合。而若是應用是不少讀操做類型的,那麼順序讀取效率會比較高,由於這樣大部份內容都會在緩存中找到,儘量避免大量的隨機讀取操做。
前文有述,對於LevelDb來講,寫入記錄操做很簡單,刪除記錄僅僅寫入一個刪除標記就算完事,可是讀取記錄比較複雜,須要在內存以及各個層級文件中依照新鮮程度依次查找,代價很高。爲了加快讀取速度,levelDb採起了compaction的方式來對已有的記錄進行整理壓縮,經過這種方式,來刪除掉一些再也不有效的KV數據,減少數據規模,減小文件數量等。
levelDb的compaction機制和過程與Bigtable所講述的是基本一致的,Bigtable中講到三種類型的compaction: minor ,major和full。所謂minor Compaction,就是把memtable中的數據導出到SSTable文件中;major compaction就是合併不一樣層級的SSTable文件,而full compaction就是將全部SSTable進行合併。
LevelDb包含其中兩種,minor和major。
咱們將爲你們詳細敘述其機理。
先來看看minor Compaction的過程。Minor compaction 的目的是當內存中的memtable大小到了必定值時,將內容保存到磁盤文件中,圖8.1是其機理示意圖。
圖8.1 minor compaction
從8.1能夠看出,當memtable數量到了必定程度會轉換爲immutable memtable,此時不能往其中寫入記錄,只能從中讀取KV內容。以前介紹過,immutable memtable實際上是一個多層級隊列SkipList,其中的記錄是根據key有序排列的。因此這個minor compaction實現起來也很簡單,就是按照immutable memtable中記錄由小到大遍歷,並依次寫入一個level 0 的新建SSTable文件中,寫完後創建文件的index 數據,這樣就完成了一次minor compaction。從圖中也能夠看出,對於被刪除的記錄,在minor compaction過程當中並不真正刪除這個記錄,緣由也很簡單,這裏只知道要刪掉key記錄,可是這個KV數據在哪裏?那須要複雜的查找,因此在minor compaction的時候並不作刪除,只是將這個key做爲一個記錄寫入文件中,至於真正的刪除操做,在之後更高層級的compaction中會去作。
當某個level下的SSTable文件數目超過必定設置值後,levelDb會從這個level的SSTable中選擇一個文件(level>0),將其和高一層級的level+1的SSTable文件合併,這就是major compaction。
咱們知道在大於0的層級中,每一個SSTable文件內的Key都是由小到大有序存儲的,並且不一樣文件之間的key範圍(文件內最小key和最大key之間)不會有任何重疊。Level 0的SSTable文件有些特殊,儘管每一個文件也是根據Key由小到大排列,可是由於level 0的文件是經過minor compaction直接生成的,因此任意兩個level 0下的兩個sstable文件可能再key範圍上有重疊。因此在作major compaction的時候,對於大於level 0的層級,選擇其中一個文件就行,可是對於level 0來講,指定某個文件後,本level中極可能有其餘SSTable文件的key範圍和這個文件有重疊,這種狀況下,要找出全部有重疊的文件和level 1的文件進行合併,即level 0在進行文件選擇的時候,可能會有多個文件參與major compaction。
levelDb在選定某個level進行compaction後,還要選擇是具體哪一個文件要進行compaction,levelDb在這裏有個小技巧, 就是說輪流來,好比此次是文件A進行compaction,那麼下次就是在key range上緊挨着文件A的文件B進行compaction,這樣每一個文件都會有機會輪流和高層的level 文件進行合併。
若是選好了level L的文件A和level L+1層的文件進行合併,那麼問題又來了,應該選擇level L+1哪些文件進行合併?levelDb選擇L+1層中和文件A在key range上有重疊的全部文件來和文件A進行合併。
也就是說,選定了level L的文件A,以後在level L+1中找到了全部須要合併的文件B,C,D…..等等。剩下的問題就是具體是如何進行major 合併的?就是說給定了一系列文件,每一個文件內部是key有序的,如何對這些文件進行合併,使得新生成的文件仍然Key有序,同時拋掉哪些再也不有價值的KV 數據。
圖8.2說明了這一過程。
圖8.2 SSTable Compaction
Major compaction的過程以下:對多個文件採用多路歸併排序的方式,依次找出其中最小的Key記錄,也就是對多個文件中的全部記錄從新進行排序。以後採起必定的標準判斷這個Key是否還須要保存,若是判斷沒有保存價值,那麼直接拋掉,若是以爲還須要繼續保存,那麼就將其寫入level L+1層中新生成的一個SSTable文件中。就這樣對KV數據一一處理,造成了一系列新的L+1層數據文件,以前的L層文件和L+1層參與compaction 的文件數據此時已經沒有意義了,因此所有刪除。這樣就完成了L層和L+1層文件記錄的合併過程。
那麼在major compaction過程當中,判斷一個KV記錄是否拋棄的標準是什麼呢?其中一個標準是:對於某個key來講,若是在小於L層中存在這個Key,那麼這個KV在major compaction過程當中能夠拋掉。由於咱們前面分析過,對於層級低於L的文件中若是存在同一Key的記錄,那麼說明對於Key來講,有更新鮮的Value存在,那麼過去的Value就等於沒有意義了,因此能夠刪除。
書接前文,前面講過對於levelDb來講,讀取操做若是沒有在內存的memtable中找到記錄,要屢次進行磁盤訪問操做。假設最優狀況,即第一次就在level 0中最新的文件中找到了這個key,那麼也須要讀取2次磁盤,一次是將SSTable的文件中的index部分讀入內存,這樣根據這個index能夠肯定key是在哪一個block中存儲;第二次是讀入這個block的內容,而後在內存中查找key對應的value。
levelDb中引入了兩個不一樣的Cache:Table Cache和Block Cache。其中Block Cache是配置可選的,即在配置文件中指定是否打開這個功能。
圖9.1 table cache
圖9.1是table cache的結構。在Cache中,key值是SSTable的文件名稱,Value部分包含兩部分,一個是指向磁盤打開的SSTable文件的文件指針,這是爲了方便讀取內容;另一個是指向內存中這個SSTable文件對應的Table結構指針,table結構在內存中,保存了SSTable的index內容以及用來指示block cache用的cache_id ,固然除此外還有其它一些內容。
好比在get(key)讀取操做中,若是levelDb肯定了key在某個level下某個文件A的key range範圍內,那麼須要判斷是否是文件A真的包含這個KV。此時,levelDb會首先查找Table Cache,看這個文件是否在緩存裏,若是找到了,那麼根據index部分就能夠查找是哪一個block包含這個key。若是沒有在緩存中找到文件,那麼打開SSTable文件,將其index部分讀入內存,而後插入Cache裏面,去index裏面定位哪一個block包含這個Key 。若是肯定了文件哪一個block包含這個key,那麼須要讀入block內容,這是第二次讀取。
圖9.2 block cache
Block Cache是爲了加快這個過程的,圖9.2是其結構示意圖。其中的key是文件的cache_id加上這個block在文件中的起始位置block_offset。而value則是這個Block的內容。
若是levelDb發現這個block在block cache中,那麼能夠避免讀取數據,直接在cache裏的block內容裏面查找key的value就行,若是沒找到呢?那麼讀入block內容並把它插入block cache中。levelDb就是這樣經過兩個cache來加快讀取速度的。從這裏能夠看出,若是讀取的數據局部性比較好,也就是說要讀的數據大部分在cache裏面都能讀到,那麼讀取效率應該仍是很高的,而若是是對key進行順序讀取效率也應該不錯,由於一次讀入後能夠屢次被複用。可是若是是隨機讀取,您能夠推斷下其效率如何。
Version 保存了當前磁盤以及內存中全部的文件信息,通常只有一個Version叫作"current" version(當前版本)。Leveldb還保存了一系列的歷史版本,這些歷史版本有什麼做用呢?
當一個Iterator建立後,Iterator就引用到了current version(當前版本),只要這個Iterator不被delete那麼被Iterator引用的版本就會一直存活。這就意味着當你用完一個Iterator後,須要及時刪除它。
當一次Compaction結束後(會生成新的文件,合併前的文件須要刪除),Leveldb會建立一個新的版本做爲當前版本,原先的當前版本就會變爲歷史版本。
VersionSet 是全部Version的集合,管理着全部存活的Version。
VersionEdit 表示Version之間的變化,至關於delta 增量,表示有增長了多少文件,刪除了文件。下圖表示他們之間的關係。
Version0 +VersionEdit-->Version1
VersionEdit會保存到MANIFEST文件中,當作數據恢復時就會從MANIFEST文件中讀出來重建數據。
leveldb的這種版本的控制,讓我想到了雙buffer切換,雙buffer切換來自於圖形學中,用於解決屏幕繪製時的閃屏問題,在服務器編程中也有用處。
好比咱們的服務器上有一個字典庫,天天咱們須要更新這個字典庫,咱們能夠新開一個buffer,將新的字典庫加載到這個新buffer中,等到加載完畢,將字典的指針指向新的字典庫。
leveldb的version管理和雙buffer切換相似,可是若是原version被某個iterator引用,那麼這個version會一直保持,直到沒有被任何一個iterator引用,此時就能夠刪除這個version。