LevelDB原理解析

来源:https://www.cnblogs.com/jiweilearn/archive/2018/11/06/9914161.html
-Advertisement-
Play Games

LevelDb有如下一些特點: 首先,LevelDb是一個持久化存儲的KV系統,和Redis這種記憶體型的KV系統不同,LevelDb不會像Redis一樣狂吃記憶體,而是將大部分數據存儲到磁碟上。 其次,LevleDb在存儲數據時,是根據記錄的key值有序存儲的,就是說相鄰的key值在存儲文件中是依次順 ...


LevelDb有如下一些特點:

首先,LevelDb是一個持久化存儲的KV系統,和Redis這種記憶體型的KV系統不同,LevelDb不會像Redis一樣狂吃記憶體,而是將大部分數據存儲到磁碟上。

其次,LevleDb在存儲數據時,是根據記錄的key值有序存儲的,就是說相鄰的key值在存儲文件中是依次順序存儲的,而應用可以自定義key大小比較函數,LevleDb會按照用戶定義的比較函數依序存儲這些記錄。

再次,像大多數KV系統一樣,LevelDb的操作介面很簡單,基本操作包括寫記錄,讀記錄以及刪除記錄。也支持針對多條操作的原子批量操作。

另外,LevelDb支持數據快照(snapshot)功能,使得讀取操作不受寫操作影響,可以在讀操作過程中始終看到一致的數據。

除此外,LevelDb還支持數據壓縮等操作,這對於減小存儲空間以及增快IO效率都有直接的幫助。

LevelDb性能非常突出,官方網站報道其隨機寫性能達到40萬條記錄每秒,而隨機讀性能達到6萬條記錄每秒。總體來說,LevelDb的寫操作要大大快於讀操作,而順序讀寫操作大大快於隨機讀寫操作(註:把隨機寫轉換為順序寫,所以犧牲了隨機讀性能,提升隨機寫)。至於為何是這樣,看了我們後續推出的LevelDb日知錄,估計您會瞭解其內在原因。

 

LevelDb之二:整體架構

LevelDb本質上是一套存儲系統以及在這套存儲系統上提供的一些操作介面。為了便於理解整個系統及其處理流程,我們可以從兩個不同的角度來看待LevleDb:靜態角度和動態角度。從靜態角度,可以假想整個系統正在運行過程中(不斷插入刪除讀取數據),此時我們給LevelDb照相,從照片可以看到之前系統的數據在記憶體和磁碟中是如何分佈的,處於什麼狀態等;從動態的角度,主要是瞭解系統是如何寫入一條記錄,讀出一條記錄,刪除一條記錄的,同時也包括除了這些介面操作外的內部操作比如compaction,系統運行時崩潰後如何恢復系統等等方面。

本節所講的整體架構主要從靜態角度來描述,之後接下來的幾節內容會詳述靜態結構涉及到的文件或者記憶體數據結構,LevelDb日誌記錄後半部分主要介紹動態視角下的LevelDb,就是說整個系統是怎麼運轉起來的。

LevelDb作為存儲系統,數據記錄的存儲介質包括記憶體以及磁碟文件,如果像上面說的,當LevelDb運行了一段時間,此時我們給LevelDb進行透視拍照,那麼您會看到如下一番景象:

 

 

LevelDb結構

從圖中可以看出,構成LevelDb靜態結構的包括六個主要部分:記憶體中MemTableImmutable MemTable以及磁碟上的幾種主要文件:Current文件,Manifest文件,log文件以及SSTable文件。當然,LevelDb除了這六個主要部分還有一些輔助的文件,但是以上六個文件和數據結構是LevelDb的主體構成元素。

寫Memtable之前有一個WAL操作

WAL: Write-Ahead Logging預寫日誌系統

資料庫中一種高效的日誌演算法,對於非記憶體資料庫而言,磁碟I/O操作是資料庫效率的一大瓶頸。在相同的數據量下,採用WAL日誌的資料庫系統在事務提交時,磁碟寫操作只有傳統的回滾日誌的一半左右,大大提高了資料庫磁碟I/O操作的效率,從而提高了資料庫的性能。

 

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所存儲內容的示意:

 

 

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日誌記錄接下來的內容中,我們會首先介紹重要文件或者記憶體數據的具體數據佈局與結構。

LevelDb之三:log文件

     上節內容講到log文件在LevelDb中的主要作用是系統故障恢復時,能夠保證不會丟失數據。因為在將記錄寫入記憶體的Memtable之前,會先寫入Log文件,這樣即使系統發生故障,Memtable中的數據沒有來得及Dump到磁碟的SSTable文件,LevelDB也可以根據log文件恢復記憶體的Memtable數據結構內容,不會造成系統丟失數據,在這點上LevelDb和Bigtable是一致的。

因為一次寫入操作只涉及一次磁碟順序寫和一次記憶體寫入,所以這是為何說LevelDb寫入速度極快的主要原因。

下麵我們帶大家看看log文件的具體物理和邏輯佈局是怎樣的,LevelDb對於一個log文件,會把它切割成32K為單位的物理Block,每次讀取的單位以一個Block作為基本讀取單位,下圖展示的log文件由3個Block構成,所以從物理佈局來講,一個log文件就是由連續的32K大小Block構成的。這種安排使得發生數據錯誤時,最多只需丟棄一個Block大小的內容。顯而易見地,不同的Record可能共存於一個Block,同時,一個Record也可能橫跨幾個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,然後根據類型情況拼接出邏輯記錄,供後續流程處理。

LevelDb之四:SSTable文件

SSTable是Bigtable中至關重要的一塊,對於LevelDb來說也是如此,對LevelDb的SSTable實現細節的瞭解也有助於瞭解Bigtable中一些實現細節。

SSTable就是由記憶體中的數據不斷導出併進行Compaction操作(壓縮操作,下文會講到)後形成的,而且SSTable的所有文件是一種層級結構,第一層為Level 0,第二層為Level 1,依次類推,層級逐漸增高,這也是為何稱之為LevelDb的原因。

SSTable中的文件是Key有序的,就是說在文件中小key記錄排在大Key記錄之前,各個Level的SSTable都是如此,但是這裡需要註意的一點是:Level 0SSTable文件(尾碼為.sst)和其它Level的文件相比有特殊性:這個層級內的.sst文件,兩個文件可能存在key重疊,比如有兩個level 0的sst文件,文件A和文件B,文件A的key範圍是:{bar, car},文件B的Key範圍是{blue,samecity}:

本節內容主要講述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作為一個獨立的寫入和解析單位,每個Block分為三個部分,紅色部分是數據存儲區, 藍色的Type(一個位元組)區用於標識數據存儲區是否採用了數據壓縮演算法(Snappy壓縮或者無壓縮兩種),CRC4個位元組)部分則是數據校驗碼,用於判別數據是否在生成和傳輸中出錯。

以上是.sst的物理佈局,下麵介紹.sst文件的邏輯佈局,所謂邏輯佈局,就是說儘管大家都是物理塊,但是每一塊存儲什麼內容,內部又有什麼結構等。圖4.2展示了.sst文件的內部邏輯解釋

 

 

圖4.2 邏輯佈局

 

從圖4.2可以看出,從大的方面,可以將.sst文件劃分為數據存儲區和數據管理區,數據存儲區存放實際的Key:Value數據,數據管理區則提供一些索引指針等管理數據,目的是更快速便捷的查找相應的記錄。兩個區域都是在上述的分塊基礎上的,就是說文-件的前面若幹塊存儲實際KV數據,後面數據管理區存儲管理數據。管理數據又分為四種不同類型:紫色的Meta Block,紅色的Metablock  Index索引和藍色的Index block數據索引塊以及一個Footer文件尾部塊

Meta Block:比較特殊的Block,用來存儲元信息,目前LevelDB使用的僅有對布隆過濾器的存儲。寫入Data Block的數據會同時更新對應Meta Block中的過濾器。讀取數據時也會首先經過布隆過濾器過濾。Meta Block的物理結構也與其他Block有所不同:

Metablock index:與Index Block類似,由一組Handle組成,不同的是這裡的Handle指向的Meta Block。

Index Block:記錄Data Block位置信息的Block,其中的每一條Entry指向一個Data Block,其Key值為所指向的Data Block最後一條數據的Key,Value為指向該Data Block位置的Handle。

Footer:為於Table尾部,記錄指向Metaindex Block的Handle和指向Index Block的Handle。需要說明的是Table中所有的Handle是通過偏移量Offset以及Size一同來表示的,用來指明所指向的Block位置。Footer是SST文件解析開始的地方,通過Footer中記錄的這兩個關鍵元信息Block的位置,可以方便的開啟之後的解析工作。另外Footer中還記錄了用於驗證文件是否為合法SST文件的常數值Magic num。

LevelDb 1.2版對於Meta Block尚無實際使用,只是保留了一個介面,估計會在後續版本中加入內容,下麵我們看看數據索引區和文件尾部Footer的內部結構。

 

圖4.3 數據索引

 

圖4.3是數據索引的內部結構示意圖。再次強調一下,Data Block內的KV記錄是按照Key由小到大排列的,數據索引區的每條記錄是對某個Data Block建立的索引信息,每條索引信息包含三個內容,以圖4.3所示的數據塊i(不是每個KV記錄)的索引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內容部分就是這麼存儲的,主要目的是減少存儲開銷。這種方式非常好的減少了數據存儲,但同時也引入一個風險,如果最開頭的記錄數據損壞,其後的所有記錄都將無法恢復。為了降低這個風險,leveldb引入了重啟點,每隔固定條數記錄會強制加入一個重啟點,這個位置的記錄會完整的記錄自己的Key,並將其shared值設置為0。同時,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詳解

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並不存在真正的刪除操作,刪除某個KeyValueMemtable內是作為插入一條記錄實施的,但是會打上一個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://kenby.iteye.com/blog/1187303,講述的很清楚,LevelDb的SkipList基本上是一個具體實現,並無特殊之處。

SkipList不僅是維護有序數據的一個簡單實現,而且相比較平衡樹來說,在插入數據的時候可以避免頻繁的樹節點調整操作,所以寫入效率是很高的,LevelDb整體而言是個高寫入系統,SkipList在其中應該也起到了很重要的作用。Redis為了加快插入操作,也使用了SkipList來作為內部實現數據結構。

LevelDb之六 寫入與刪除記錄

在之前的五節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之七:讀取記錄

LevelDb是針對大規模Key/Value數據的單機存儲庫,從應用的角度來看,LevelDb就是一個存儲工具。而作為稱職的存儲工具,常見的調用介面無非是新增KV,刪除KV,讀取KV,更新Key對應的Value值這麼幾種操作。LevelDb的介面沒有直接支持更新操作的介面,如果需要更新某個KeyValue,你可以選擇直接生猛地插入新的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之八:Compaction操作

前文有述,對於LevelDb來說,寫入記錄操作很簡單,刪除記錄僅僅寫入一個刪除標記就算完事,但是讀取記錄比較複雜,需要在記憶體以及各個層級文件中依照新鮮程度依次查找,代價很高。為了加快讀取速度,levelDb採取了compaction的方式來對已有的記錄進行整理壓縮,通過這種方式,來刪除掉一些不再有效的KV數據,減小數據規模,減少文件數量等。

levelDb的compaction機制和過程與Bigtable所講述的是基本一致的,Bigtable中講到三種類型的compaction: minor majorfull。所謂minor Compaction,就是把memtable中的數據導出到SSTable文件中;major compaction就是合併不同層級的SSTable文件,而full compaction就是將所有SSTable進行合併。

LevelDb包含其中兩種,minormajor

我們將為大家詳細敘述其機理。

先來看看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層中和文件Akey 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之九 levelDb中的Cache

書接前文,前面講過對於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 ,當然除此外還有其它一些內容。table cache是緩存了SSTable文件中的block index內容,是為了加速查找某個鍵值是否在某個SSTable中,每一個SSTable文件對應table cache中一個Key。

比如在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進行順序讀取效率也應該不錯,因為一次讀入後可以多次被覆用。但是如果是隨機讀取,您可以推斷下其效率如何。

LevelDb之十 Version、VersionEdit、VersionSet

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。

 

 

以上內容來自:http://www.cnblogs.com/zhihaowu/p/7884424.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 Lepus(天兔)資料庫企業監控系統是一套由專業DBA針對互聯網企業開發的一款專業、強大的企業資料庫監控管理系統,企業通過Lepus可以對資料庫的實時健康和各種性能指標進行全方位的監控。目前已經支持MySQL、Oracle、MongoDB、Redis資料庫的全面監控。 Lepus可以在資料庫出 ...
  • 第一次寫博客,各位湊合著看吧(假裝有人看)。 我這裡使用的是centos7。 1、首先打開終端,查看有沒有安裝過MySQL: 若為空則說明沒有安裝過,若要刪除可用yum remove mysql命令。 2、下載mysql的repo源: 安裝mysql-community-release-el7-5. ...
  • 作者:天山老妖S 鏈接:http://blog.51cto.com/9291927 一、存儲過程簡介 1、存儲過程簡介 存儲過程是一組具有特定功能的SQl語句集組成的可編程的函數,經編譯創建並保存在資料庫中,用戶可通過指定存儲過程的名字並給定參數來調用執行。 存儲過程是資料庫管理中常用的技術之一,可 ...
  • 事務定義 事務是單個的工作單元。事務是在資料庫上按照一定的邏輯順序執行的任務序列,既可以由用戶手動執行,也可以由某種資料庫程式自動執行。 事務分類 自動提交事務 每條單獨的語句都是一個事務。 顯式事務 每個事務均以 BEGIN TRANSACTION 語句顯式開始,以 COMMIT 或 ROLLBA ...
  • 如果我們用成語來形容近幾年的大數據產業,也許最合適的就是:如火如荼! 從大量融資、大數據從業者薪資上漲、從研發到商業應用的技術,到2017年的大數據產業可以說已經贏得了全世界的關註。然而,當涉及到大數據時,很多人認為普通人根本無法進去。真的是這樣嗎?普通人只看招聘人員的巨額薪水嗎? 事實上,只要找到 ...
  • 歡迎大家前往 "騰訊雲+社區" ,獲取更多騰訊海量技術實踐乾貨哦~ 本文由 "騰訊雲資料庫 TencentDB" 發表於 "雲+社區專欄" 鄒鵬 ,騰訊高級工程師,騰訊雲資料庫Redis負責人,多年資料庫、網路安全研發經驗。在網路、計算、存儲、安全等領域有深入的研究和豐富的產品化經驗。 在Redis ...
  • 本文系列文章: ​ 使用Shell 操作 MongoDB的技巧 ​ MongoTemplate的使用技巧及其註意事項 敬請期待。 前言 最近公司想要做一個用戶行為數據的收集,最開始想用mysql來存儲後來發現這種方式對於不固定數據格式的保存存在局限性,也不利於查詢統計操作。所以衍生了使用mongod ...
  • Oracle視圖詳解 一. 視圖的定義 視圖(view),也稱虛表, 不占用物理空間,這個也是相對概念,因為視圖本身的定義語句還是要存儲在數據字典里的。視圖只有邏輯定義。每次使用的時候,只是重新執行SQL。 視圖是從一個或多個實際表中獲得的,這些表的數據存放在資料庫中。那些用於產生視圖的表叫做該視圖 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...