161104、NoSQL數據庫:key/value型之levelDB介紹及java實現


簡介:Leveldb是一個google實現的非常高效的kv數據庫,能夠支持billion級別的數據量了。 在這個數量級別下還有着非常高的性能,主要歸功於它的良好的設計。特別是LSM算法。LevelDB 是單進程的服務,性能非常之高,在一台4核Q6600的CPU機器上,每秒鍾寫數據超過40w,而隨機讀的性能每秒鍾超過10w。

原理(可以查看相關原理圖更容易理解,非常類似於hadoop的某些組件實現)


1、Files

    leveldb的實現類似於Bigtable中的一個tablet(Google),只不過底層的文件組織形式稍有不同。

    每個Database有一系列本地文件組成,這些文件有不同的類型:

    Log文件

    log文件存儲了一序列的最近更新操作,每個更新(update)都會append到當前log文件的尾部,當log文件的尺寸達到預設定的大小時,將會把此log文件轉換成一個sorted table(.sst)文件,然后滾動創建一個新的log文件來保存此后的updates操作。

 

    當前log文件的數據copy被保存在一個內存結構中,稱為memtable。每個read操作都會訪問memtable,因此這些update數據都可以在read操作中反應出來。

 

    Sorted tables(簡稱SST)

    Sorted table(.sst)文件存儲了一序列按照key排序的entries,每個entry可以是key-value,或者是一個key的刪除標記(marker)。(刪除標記可以屏蔽掉先前sst文件中保存的較舊的數據,即如果一個key被標記為刪除,那么先前的sst文件中關於此key的數據,將不會被read到)

 

    Sorted tables按照層次(level)進行組織,由log文件生成的SST將會放置在一個特殊的young level中--即level-0,當young level中SST文件的個數超過一個閥值(4個),這些young文件將會與level-1中的那些有數據重疊的文件合並,並生成一序列新的level-1文件(每個新文件大小位2M)。

 

    備注:“重疊”意義為key區間在兩個文件中都存在。keys在SST文件中保存是嚴格排序的。

 

    young level中的文件可能包含重疊的keys,不過其他level中的SST文件只會包含不同的“非重疊”的keys區間。假如level-L,其中L >= 1,當level-L中SST文件的總大小達到(10^L)MB時(例如level-1位10MB,level-2位100MB),那么level-L中的一個文件,將會和level-(L+1)中那些有keys重疊(覆蓋)的文件merged,並生成一組新的level-(L+1)文件。這寫merge,只通過批量的文件讀寫操作,即可將最新的updates數據從young level遷移到最高的level。

 

    Manifest(清單)

    manifest文件中列舉了構成每個level的SST文件列表,以及相應的key區間,還包括一些重要的metadata。當database被reopened時,都會創建一個新的manifest文件(文件命中包含一個新的number序列號)。manifest文件的格式像log,“serving data”的變更(比如SST文件的創建、刪除)操作都會被append到此log中。

 

    Current

    CURRENT文件是一個簡單的文本文件,保存了當前最新的manifest文件的名稱。

    其他:略

 

2、Level 0

    當log文件的尺寸增長到一定的大小(默認1M):

  • 創建一個新的memtable和log文件,用來保存此后的updates操作。

  • 在后台:將舊的memtable寫入到文件生成新的SST文件,然后銷毀此memtable。刪除舊的log文件,然后將此新的SST文件添加到young level組織中。

3、Compactions

    當level-L的尺寸達到了它的限制,我們將使用一個后台線程對它進行Compaction。壓縮時,將會從level-L中選擇一個文件,同時選擇level-(L+1)中所有與此文件key有重疊的文件。如果level-L中一個文件只與level-(L+1)中某個文件的一部分重疊,那么level-(L+1)中的此文件作為壓縮時的輸入,在壓縮結束后,此文件將被拋棄。不過,level-0比較特殊(文件中的keys可能互相重疊),對於level-0到level-1的壓縮我們需要特殊處理:level-0中文件中互相重疊的話,那么將可能一次選擇多個level-0的文件作為輸入。

 

    壓縮將選擇的文件內容重新輸出到一序列新的level-(L+1)文件中(多路合並),當每個輸出文件達到2M時將會切換一個新的文件,或者當新輸出的文件中key區間覆蓋了level-(L+2)中多於10個文件時,也會切換生成新文件;第二個規則保證此后level-(L+1)的壓縮時無需選擇太多的文件。

 

    當level-(L+1)中的新文件加入到“serving state”時,那么舊的文件將會被刪除(包括level-L和level-(L+1))。

 

    壓縮時,將會拋棄那些“overwritten”的值;如果遇到刪除標記,且對應的key在更高的level中不存在,也會直接拋棄。

 

    Timing

    level-0將會讀取4個1M的文件(每個1M,level-0最多4個文件),最壞的情況是讀取level-1的所有文件(10M),即我們讀寫各10MB。

    和level-0不同,對於其他level-L,我們將讀取2M的一個文件,最壞的情況是它與level-(L+1)中12文件有重疊(10個文件,同時還有2個處於邊界的文件);那么一次壓縮將讀寫26MB數據。假定磁盤IO速率位100M/S,那么一次壓縮耗時大約0.5秒。

 

    如果我們對磁盤速率受限,比如10M/S,那么壓縮可能耗時達到5秒。

 

    文件個數

    每個SST文件的大小為2M,事實上我們可以通過增大此值,來減少文件的總數,不過這會導致壓縮更加耗時(讀取的文件尺寸更大,磁盤密集操作);另外,我們可以將不同的文件放在多個目錄中。

 

4、數據恢復

    1)從CURRENT中讀取最新的manifest文件的名字。

    2)讀取manifest文件。

    3)清理那么過期的文件。

    4)我們可以打開所有的SST文件,不過通常lazy更好。

    5)將log存留文件轉存成新的level-0中的SST文件。

    6)引導write操作到新的log文件中。

    7)回收垃圾文件。

 

    每次壓縮和recovery操作后,將會調用DeleteObsoleteFiles():從database中查詢出所有的file的名字,然后將當前log文件之外的其他log文件全出刪除,刪除那些所有level中都不包含的、以及壓縮操作沒有引用的SST文件。

 

使用

    leveldb為一個本地化的K-V存儲數據庫,設計思想類似於Bigtable,將key按照順序在底層文件中存儲,同時為了加快讀取操作,內存中有一個memtable來緩存數據。

    根據leveldb官網的性能基准測試,我們大概得出其特性:

    1)leveldb的順序讀(遍歷)的效率極高,幾乎接近文件系統的文件順序讀。比BTree數據庫要快多倍。

    2)其隨機讀性能較高,但和順序讀仍有幾個量級上的差距。leveldb的隨機讀,和基於BTree的數據庫仍有較大差距。(個人親測,其隨機讀的效率並不像官網所說的如此之高,可能與cache的配置有關)隨機讀,要比BTree慢上一倍左右。

    3)順序寫,性能極高(無強制sync),受限於磁盤速率;隨機寫,性能稍差,不過性能相對於其他DB而言,仍有極大的優勢。無論是順序寫還是隨機寫,性能都比BTree要快多倍。

    4)leveldb為K-V存儲結構,字節存儲。屬於NoSql數據庫的一種,不支持事務,只能通過KEY查詢數據;支持批量讀寫操作。

    5)leveldb中key和value數據尺寸不能太大,在KB級別,如果存儲較大的key或者value,將對leveld的讀寫性能都有較大的影響。

 

    因為leveldb本身尚不具備“分布式”集群架構能力,所以,我們將有限的數據基於leveldb存儲(受限於本地磁盤)。

    

    案例推演:

    1)leveldb具備“cache + 磁盤持久存儲”特性,且不支持RPC調用,那么leveldb需要和application部署在同一宿主機器上。類似於“嵌入式”K-V存儲系統。

    2)如果存儲數據較少,3~5G,且“讀寫比”(R:W)較高,我們可以讓leveldb作為本地cache來使用,比如Guava cache + leveldb,這種結合,可以實現類似於輕量級redis。即作為本地緩存使用。

    3)如果數據較多,通常為“順序讀”或者“順序寫”,我們可以將leveldb作為Hadoop HDFS的“微縮版”,可以用來緩存高峰期的消息、日志存儲的緩沖區。比如我們將用戶操作日志暫且存儲在leveldb中,而不是直接將日志發送給remote端的Hadoop(因為每次都直接調用RPC,將會對系統的吞吐能力帶來極大的影響),而是將這些頻繁寫入的日志數據存儲在本地的leveldb中,然后使用后台線程以“均衡”的速度發送出去。起到了“Flow Control”(流量控制)的作用。

 

    其中ActiveMQ即采用leveldb作為底層的消息數據存儲,性能和容錯能力很強。

 

API簡析(JAVA版,基於maven)

    原生leveldb是基於C++開發,java語言無法直接使用;iq80對leveldb使用JAVA語言進行了“逐句”重開發,經過很多大型項目的驗證(比如ActiveMQ),iq80開發的JAVA版leveldb在性能上損失極少(10%)。對於JAVA開發人員來說,我們直接使用即可,無需額外的安裝其他lib。

    1、pom.xml

<dependency>
	<groupId>org.iq80.leveldb</groupId>
	<artifactId>leveldb</artifactId>
	<version>0.7</version>
</dependency>
<dependency>
	<groupId>org.iq80.leveldb</groupId>
	<artifactId>leveldb-api</artifactId>
	<version>0.7</version>
</dependency>

 

    2、代碼樣例

        boolean cleanup = true;
        Charset charset = Charset.forName("utf-8");
        String path = "/data/leveldb";

        //init        DBFactory factory = Iq80DBFactory.factory;
        File dir = new File(path);
        //如果數據不需要reload,則每次重啟,嘗試清理磁盤中path下的舊數據。
        if(cleanup) {
            factory.destroy(dir,null);//清除文件夾內的所有文件。
        }
        Options options = new Options().createIfMissing(true);
        //重新open新的db        DB db = factory.open(dir,options);

        //write
        db.put("key-01".getBytes(charset),"value-01".getBytes(charset));

        //write后立即進行磁盤同步寫
        WriteOptions writeOptions = new WriteOptions().sync(true);//線程安全
        db.put("key-02".getBytes(charset),"value-02".getBytes(charset),writeOptions);


        //batch write;
        WriteBatch writeBatch = db.createWriteBatch();
        writeBatch.put("key-03".getBytes(charset),"value-03".getBytes(charset));
        writeBatch.put("key-04".getBytes(charset),"value-04".getBytes(charset));
        writeBatch.delete("key-01".getBytes(charset));
        db.write(writeBatch);
        writeBatch.close();

        //read
        byte[] bv = db.get("key-02".getBytes(charset));
        if(bv != null && bv.length > 0) {
            String value = new String(bv,charset);
            System.out.println(value);
        }

        //iterator,遍歷,順序讀

        //讀取當前snapshot,快照,讀取期間數據的變更,不會反應出來        Snapshot snapshot = db.getSnapshot();
        //讀選項        ReadOptions readOptions = new ReadOptions();
        readOptions.fillCache(false);//遍歷中swap出來的數據,不應該保存在memtable中。        readOptions.snapshot(snapshot);//默認snapshot為當前。        DBIterator iterator = db.iterator(readOptions);
        while (iterator.hasNext()) {
            Map.Entry<byte[],byte[]> item = iterator.next();
            String key = new String(item.getKey(),charset);
            String value = new String(item.getValue(),charset);//null,check.
            System.out.println(key + ":" + value);
        }
        iterator.close();//must be

        //delete
        db.delete("key-01".getBytes(charset));

        //compaction,手動

        db.compactRange("key-".getBytes(charset),null);

        //
        db.close();


免責聲明!

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



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