【轉自】http://alinuxer.sinaapp.com/?p=400
LDB
首先,我們先總結下google的LevelDB相關內容:
影響磁盤性能的因素:
主要受限于磁盤的尋道時間,優化磁盤數據訪問的方法是盡量減少磁盤的IO次數。磁盤數據訪問效率取決于磁盤IO次數,而磁盤IO次數又取決于數據在磁盤上的組織方式。磁盤數據存儲大多采用B+樹類型數據結構,這種數據結構針對磁盤數據的存儲和訪問進行了優化,減少訪問數據時磁盤IO次數。
常用的數據結構
B+樹是一種專門針對磁盤存儲而優化的N叉排序樹,以樹節點為單位存儲在磁盤中,從根開始查找所需數據所在的節點編號和磁盤位置,將其加載到內存中然后繼續查找,直到找到所需的數據。
目前數據庫多采用兩級索引的B+樹,樹的層次最多三層。因此可能需要5次磁盤訪問才能更新一條記錄(三次磁盤訪問獲得數據索引及行ID,然后再進行一次數據文件讀操作及一次數據文件寫操作)。
但是由于每次磁盤訪問都是隨機的,而傳統機械硬盤在數據隨機訪問時性能較差,每次數據訪問都需要多次訪問磁盤影響數據訪問性能。
LSM樹可以看作是一個N階合并樹。LSM樹本質上就是在讀寫之間取得平衡,和B+樹相比,它犧牲了部分讀性能,用來大幅提高寫性能。它的原理是把一顆大樹拆分成N棵小樹, 它首先寫入到內存中(內存沒有尋道速度的問題,隨機寫的性能得到大幅提升),在內存中構建一顆有序小樹,隨著小樹越來越大,內存的小樹會flush到磁盤上。當讀時,由于不知道數據在哪棵小樹上,因此必須遍歷所有的小樹,但在每顆小樹內部數據是有序的。數據寫操作(包括插入、修改、刪除)都在內存中進行,并且都會創建一個新記錄(修改會記錄新的數據值,而刪除會記錄一個刪除標志),這些數據在內存中仍然還是一棵排序樹,當數據量超過設定的內存閾值后,會將這棵排序樹和磁盤上最新的排序樹合并。當這棵排序樹的數據量也超過設定閾值后,和磁盤上下一級的排序樹合并。合并過程中,會用最新更新的數據覆蓋舊的數據(或者記錄為不同版本)。在需要進行讀操作時,總是從內存中的排序樹開始搜索,如果沒有找到,就從磁盤上的排序樹順序查找。在LSM樹上進行一次數據更新不需要磁盤訪問,在內存即可完成,速度遠快于B+樹。當數據訪問以寫操作為主,而讀操作則集中在最近寫入的數據上時,使用LSM樹可以極大程度地減少磁盤的訪問次數,加快訪問速度。
Leveldb是Google開源的一個快速,輕量級的key-value存儲引擎,以庫的形式提供,沒有提供上層c/s架構和網絡通信的功能。Leveldb存儲引擎的功能如下:
- 提供key-value存儲,key和value是任意的二進制數據。數據是按照key有序存儲的,可以修改排序規則。
- 只提供了Put/Get/Delete等基本操作接口,支持批量原子操作。
- 支持快照功能。
- 支持數據的正向和反向遍歷訪問。
- 支持數據壓縮功能(Snappy壓縮)。
- 支持多線程同步,但不支持多進程同時訪問。
LevelDB的組成:
構成LevelDb靜態結構的包括六個主要部分:內存中的MemTable和Immutable MemTable以及磁盤上的幾種主要文件:Current文件,Manifest文件,log文件以及SSTable文件。如下圖所示
寫操作:
我們通過寫操作來綜合了解下這幾個文件:
當應用寫入一條Key:Value記錄的時候,LevelDb會先往log文件里追加寫入,成功后將記錄插進Memtable中,這樣基本就算完成了寫入操作,因為一次寫入操作只涉及一次磁盤順序寫和一次內存寫入,這是LevelDb寫入速度極快的主要原因。Log文件在系統中的作用主要是用于系統崩潰恢復而不丟失數據,假如沒有Log文件,因為寫入的記錄剛開始是保存在內存中的,此時如果系統崩潰,內存中的數據還沒有來得及Dump到磁盤,所以會丟失數據。為了避免這種情況,LevelDb在寫入內存前先將操作記錄到Log文件中,然后再記入內存中,這樣即使系統崩潰,也可以從Log文件中恢復內存中的Memtable,不會造成數據的丟失。
當Memtable插入的數據占用內存到了一個界限后,需要將內存的記錄導出到外存文件中,LevleDb會生成新的Log文件和Memtable,原先的Memtable就成為Immutable Memtable,顧名思義,就是說這個文件的內容是不可更改的。新到來的數據被記入新的Log文件和Memtable,LevelDb后臺調度會將Immutable Memtable的數據導出到磁盤,形成一個新的SSTable文件。SSTable就是由內存中的數據不斷導出并進行Compaction操作后形成的,而且SSTable的所有文件是一種層級結構,第一層為Level 0,第二層為Level 1,依次類推,層級逐漸增高,這也是為何稱為LevelDb的原因。
SSTable中的文件是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,這是非常重要的信息。Manifest就是記載SSTable各個文件的管理信息,比如屬于哪個Level,文件名稱叫什么,最小key和最大key各自是多少。
Current文件是記載當前的manifest文件名。因為在LevleDb的運行過程中,隨著Compaction的進行,SSTable文件會發生變化,會有新的文件產生,老的文件被廢棄,Manifest也會跟著反映這種變化,此時往往會新生成Manifest文件來記載這種變化,而Current則用來指出哪個Manifest文件才是我們關心的那個Manifest文件。
當某個level下的SSTable文件數目超過一定設置值后,levelDb會從這個level的SSTable中選擇n個文件(level>0時,n=1;level=0時因key有重疊,n>=1),將其和高一層級的level+1的SSTable文件合并,這就是major compaction。 對于 compaction,下文具體分析。
另外,LevelDB的寫操作是線程安全的:LevelDB Write 的線程安全是通過將所有的寫入插入到一個隊列中, 然后有且僅有一個線程去消費這個隊列來做到的. 消費這個隊列的線程會把隊列頭部的幾個Batch合并成一個大的Batch 一起消費掉, 也是為了提高效率.
log文件內是key無序的,而Memtable中是key有序的,如何實現LevelDB的刪除操作?對于levelDb來說,并不存在立即刪除的操作,而是與插入操作相同的,區別是,插入操作插入的是Key:Value 值,而刪除操作插入的是“Key:刪除標記”,并不真正去刪除記錄,而是后臺Compaction的時候才去做真正的刪除操作。
讀取記錄
LevelDB首先會去查看內存中的Memtable,如果Memtable中包含key及其對應的value,則返回value值即可;如果在Memtable沒有讀到key,則接下來到同樣處于內存中的Immutable Memtable中去讀取,類似地,如果讀到就返回,若是沒有讀到,那么只能萬般無奈下從磁盤中的大量SSTable文件中查找。因為SSTable數量較多,而且分成多個Level,首先從屬于level 0的文件中查找,如果找到則返回對應的value值,如果沒有找到那么到level 1中的文件中去找,如此循環往復,直到在某層SSTable文件中找到這個key對應的value為止(或者查到最高level,查找失敗,說明整個系統中不存在這個Key)。
為何選擇是從低level到高level的查詢路徑
從信息的更新時間來說,很明顯Memtable存儲的是最新鮮的KV對;Immutable Memtable中存儲的KV數據對的新鮮程度次之;而所有SSTable文件中的KV數據新鮮程度一定不如內存中的Memtable和Immutable Memtable的。對于SSTable文件來說,如果同時在level L和Level L+1找到同一個key,level L的信息一定比level L+1的要新。也就是說,上面列出的查找路徑就是按照數據新鮮程度排列出來的,越新鮮的越先查找。同樣的key,不同的value;邏輯上理解好像levelDb中只有一個存儲記錄,即最新的記錄,但是在levelDb中很可能存在兩條記錄,比如記錄分別存在Level L和Level L+1中。如果同時在level L和Level L+1找到同一個key,level L的信息一定比level L+1的要新。從道理上講呢,Level L+1的數據是從Level L 經過Compaction后得到的,Level L比Level L+1的數據要新鮮。
SSTable文件很多,如何快速地找到key對應的value值
在LevelDb中,在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一般會先在內存中的Cache中查找是否包含這個文件的緩存記錄,如果包含,則從緩存中讀取;如果不包含,則打開SSTable文件,同時將這個文件的索引部分加載到內存中并放入Cache中。 這樣Cache里面就有了這個SSTable的緩存項,但是只有索引部分在內存中,之后levelDb根據索引可以定位到哪個內容Block會包含這條key,從文件中讀出這個Block的內容,在根據記錄一一比較,如果找到則返回結果,如果沒有找到,那么說明這個level的SSTable文件并不包含這個key,所以到下一級別的SSTable中去查找。
Compaction操作
LevelDB的compaction操作包含minor compaction和major compaction兩種
Minor compaction 的目的是當內存中的memtable大小到了一定值時,將內容保存到磁盤文件中,如下圖所示
從上圖可以看出,當memtable數量到了一定程度會轉換為immutable memtable,此時不能往其中寫入記錄,只能從中讀取KV內容。immutable memtable其實是一個多層級隊列SkipList,其中的記錄是根據key有序排列的。按照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的文件是通過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 文件進行合并。
接著levelDb選擇L+1層中和文件A在key range上有重疊的所有文件來和文件A進行合并。也就是說,選定了level L的文件A,之后在level L+1中找到了所有需要合并的文件B,C,D等。合并的過程如下:
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就等于沒有意義了,所以可以刪除。
大體了解了Level DB后,讓我們來學習下Tair中的LDB。
ldb是基于leveldb開發的一款持久化產品,作為Tair的持久化存儲引擎,適用于確實有持久化需求,richldb引擎內嵌Mdb作為內存KV級別cache,讀寫qps較高(萬級別)的應用場景。ldb目前線上使用的ssd機型(SSD單機讀10w qps 寫2W tps (700B測試)),因此在成本上要比緩存型產品高出幾倍。在支持的數據結構上和mdb相同,功能上ldb還支持范圍查找,但基于性能考慮,需特別謹慎使用。
讀流程
首先在MemTable文件中尋找,查看它的開始和結束是哪一部分,找到負責這個key的文件,從里面把這個數字給拿出來。對每一個LO都會去找,當然如果有一步能夠找到就會提前返回。對于LeveIDB,會做一些聚合,盡量讀一次盤能讀少一些。
寫流程
找到數據分區,存入操作指引,同時把結果插入到Mdb cache里。這樣的話,持久化存儲引擎的效率跟緩存性效率是一樣的,如果數據有訪問熱點的話,訪問的IP可以得到很好的保證。當memtable寫滿后dump成Level 0上的sstable,無數據合并。Level 會循環compact range。
Ldb的新功能
加入過期時間,嵌入一個datafilter邏輯,得到這個數據的時候會先過濾一下,比如設置一些過期數據,這個數據如果已經過期,它就會跳過去,就認為是不存在的。雖然持久化,但是就想把之前數據全部清掉,然后再寫入新的,這樣是有異步清理。它本身的compact是自己做的,有它內部自己的過程在level-n上做,這個過程當中現在主要做的工作就是把一些垃圾數據給清理了,在做機群變動的時候,因為Tair宕掉一臺機器會做數據的重分布,導致數據備份的完整性和前端服務的可用性問題,這個過程可以把垃圾數據可以清理掉。如果按照自己的過程它不是很敏感,我們就要做高Levelcompact,我們會主動出發,加速一些range合并。使用binlog做異步跨集群數據同步。(這里還不是很清楚)
典型應用場景
1.存儲黑白單數據,讀qps很高,db無法承載。
2.計數器功能,更新非常頻繁,且數據不可丟失。
RDB
名詞解釋:
Restful協議:(Representational State Transfer,資源是由URI來指定。對資源的操作包括獲取、創建、修改和刪除資源,這些操作正好對應HTTP協議提供的GET、POST、PUT和DELETE方法。通過操作資源的表現形式來操作資源。資源的表現形式則是XML或者HTML,取決于讀者是機器還是人,是消費web服務的客戶軟件還是web瀏覽器。當然也可以是任何其他的格式。)
rdb是基于redis開發另一款內存型產品,tair抽取了redis內部存儲引擎部分,支持redis所有數據結構,故rdb不僅支持key對應一個value的結構,同時也支持key對應多個value的結構,結構可以是list/map/set/zset。(注:很多同學會把value是一個map/list之類的對象和rdb支持的list/hash結構混為一談。這里再強調一下,tair的key和value均為可序列化對象,因此如果value是map或者list之類的序列化之后的對象,這個還是屬于key-value結構,而rdb支持的list/hash/set/zset,是指一個key對應多個value。比如:list就是key-{value1, value2, value3}。)設置限制內存quota,去除aof/vm,增加logiclock的概念,lazy清理db數據(比如想把area數據都清理掉,可能對當時運行會有影響,當后臺再做的時候,可以根據前面訪問的時候來比較一下logiclock,來確定這個數據是不是在它的生存周期里面,如果不在生存周期里面可以做清理,就是因為這個數據是不存在的。),特別適用容量小(一般在M級別,50G之內),讀寫qps高(萬級別)的應用場景。由于是內存型產品,因此無法保證數據的安全性,對數據安全有要求的應用建議在后端加持久化數據源或使用ldb作為rdb的持久化。實現了Restful協議。
典型應用場景:
1.用list結構來顯示最新的項目列表;
2.用sortedset來做排行榜,取Top N;
3.用set來做uniq操作,如頁面訪問者排重;
4.使用hset來做單key下多屬性的項目,例如商品的基本信息,庫存,價格等設置成多屬性。
典型用法
- rdb集群應用,有復雜數據結構的需求。rdb不僅支持key對應一個value的結構,同時也支持key對應多個value的結構,結構可以是list/map/set/zset。
MDB
mdb是Tair最早的一款內存型產品,也是在公司內部應用最廣泛的集中式緩存。特別適用容量小(一般在M級別,50G之內),讀寫QPS高(萬級別)的應用場景。由于是內存型產品,因此無法保證數據的安全性,對數據安全有要求的應用建議在后端加持久化數據源(例如MySQL)
典型應用場景:
- 用于緩存,降低對后端數據庫的訪問壓力。
- 臨時數據存儲,分鐘級別后失效,偶爾部分數據丟失不會對業務產生較大影響。
- 讀多寫少,讀qps達到萬級別以上。
mdb是memche系統,在內存里面,真正內存管理使用page和slab。抽樣出一個area的概念,有很多應用會共用,一個應用給第一個area,做一個邏輯分區,可以針對area獲得配額,進行數據清除,不會影響其他的應用,做到邏輯分區。有些應用有些情況下,一些數據這段時間不想用,可以直接清掉,是可以支持的。
支持數據過期的應用,會有后臺的一些邏輯來把它清理掉。它的內存使用率會是一個問題,所以會在一定時間去均衡slab,達到內存使用率優化。還對它各種操作、統計都做了很詳細的監控,可以實時監控到這個集群的情況。
還有后臺進程,后臺進程能做兩個事情,會定期對數據進行清理。另外一個為了內存利用率,會做一些均衡slab的管理,達到內存使用率的均衡。
典型用法
- session場景。這種場景一般訪問量非常高,且對響應時間有很高的要求,對數據有一定的一致性要求。后端無數據源,數據丟失對應用影響不大。