【轉:分佈式存儲】-leveldb/rocksdb

本篇介紹典型的基於SStable的存儲。適用於與SSD一起使用。更多存儲相關見:https://segmentfault.com/a/11...。涉及到leveldb,rocksdb。基本上分佈式都要單獨做,重點是單機架構,數據寫入,合併,ACID等功能和性能相關的。
先對性能有個直觀認識:
mysql寫入千條/s,讀萬應該沒問題。redis 寫入 萬條/s 7M/s(k+v 700bytes,雙核)讀是寫入的1.4倍 mem 3gb 2核。這兩個網上搜的,不保證正確,就看個大概吧。
SSD上 rocksdb隨機和順序的性能差不多,寫要比讀性能稍好。隨機讀寫1.7萬條/s 14M/s (32核)。batch_write/read下SSD單線程會好8倍。普通write只快1.2倍。
沒有再一個機器上的對比。rocksdb在用SSD和batch-write/read下的讀寫性能還是可以的。



第一章 levelDb

架構圖

clipboard.png

讀取過程

數據的讀取是按照 MemTable、Immutable MemTable 以及不同層級的 SSTable 的順序進行的,前兩者都是在內存中,後面不同層級的 SSTable 都是以 *.ldb 文件的形式持久存儲在磁盤上

寫入過程

1.調用 MakeRoomForWrite 方法爲即將進行的寫入提供足夠的空間;
在這個過程中,由於 memtable 中空間的不足可能會凍結當前的 memtable,發生 Minor Compaction 並創建一個新的 MemTable 對象;不可變imm去minor C,新的memtable繼續接收寫
在某些條件滿足時,也可能發生 Major Compaction,對數據庫中的 SSTable 進行壓縮;
2.通過 AddRecord 方法向日志中追加一條寫操作的記錄;
3.再向日誌成功寫入記錄後,我們使用 InsertInto 直接插入 memtable 中,內存格式爲跳錶,完成整個寫操作的流程;



writebatch併發控制

全局的sequence(memcache中記錄,這裏指的就是內存)。讀寫事務都申請writebatch,過程如下程序。
雖然是批量,但是仍然串行,是選擇一個leader(cas+memory_order)將多個writebatch合併一起寫入WAL,再依次寫入memtable再提交。
每一批writebatch完成後才更新sequence

加鎖,獲取隊列信息,釋放鎖,此次隊列加入到weitebatch中處理,寫日誌成功後寫入mem,此時其他線程可以繼續加入隊列,結束後加鎖,更新seq,將處理過的隊列移除。
Status DBImpl::Write(const WriteOptions &options, WriteBatch *my_batch){
    Writer w(&mutex_);
    w.batch = my_batch;
    w.sync = options.sync;
    w.done = false;

    MutexLock l(&mutex_);
    writers_.push_back(&w);
    while (!w.done && &w != writers_.front())
    {
        w.cv.Wait();
    }
    if (w.done)
    {
        return w.status;
    }
    // May temporarily unlock and wait.
    Status status = MakeRoomForWrite(my_batch == nullptr);
    uint64_t last_sequence = versions_->LastSequence();
    Writer *last_writer = &w;
    if (status.ok() && my_batch != nullptr)
    { // nullptr batch is for compactions
        WriteBatch *updates = BuildBatchGroup(&last_writer);
        WriteBatchInternal::SetSequence(updates, last_sequence + 1);
        last_sequence += WriteBatchInternal::Count(updates);
        // Add to log and apply to memtable.  We can release the lock
        // during this phase since &w is currently responsible for logging
        // and protects against concurrent loggers and concurrent writes
        // into mem_.
        {
            mutex_.Unlock();
            status = log_->AddRecord(WriteBatchInternal::Contents(updates));
            bool sync_error = false;
            if (status.ok() && options.sync)
            {
                status = logfile_->Sync();
                if (!status.ok())
                {
                    sync_error = true;
                }
            }
            if (status.ok())
            {
                status = WriteBatchInternal::InsertInto(updates, mem_);
            }
            mutex_.Lock();
            if (sync_error)
            {
                // The state of the log file is indeterminate: the log record we
                // just added may or may not show up when the DB is re-opened.
                // So we force the DB into a mode where all future writes fail.
                RecordBackgroundError(status);
            }
        }
        if (updates == tmp_batch_)
            tmp_batch_->Clear();

        versions_->SetLastSequence(last_sequence);
    }
    while (true) {
        Writer* ready = writers_.front();
        writers_.pop_front();
        if (ready != &w) {
          ready->status = status;
          ready->done = true;
          ready->cv.Signal();
        }
        if (ready == last_writer) break;
  }
 
  // Notify new head of write queue
  if (!writers_.empty()) {
    writers_.front()->cv.Signal();
  }
 
  return status;
}

ACID

  • 版本控制
    在每次讀的時候用userkey+sequence生成。保證整個讀過程都讀到當前版本的,在寫入時,寫入成功後才更新sequnce保證最新寫入的sequnce+1的內存不會被舊的讀取讀到。
    Compaction過程中:(更多見下面元數據的version)
    被引用的version不會刪除。被version引用的file也不會刪除
    每次獲取current versionn的內容。更新後纔會更改current的位置



memtable

頻繁插入查詢,沒有刪除。需要無寫狀態下的遍歷(dump過程)=》跳錶
默認4M

sstable

sstable(默認7個)【上層0,下層7】

  • 內存索引結構filemetadata
    refs文件被不同version引用次數,allowed_Seeks允許查找次數,number文件序號,file_size,smallest,largest
    除了level0是內存滿直接落盤key範圍會有重疊,下層都是經過合併的,沒重疊,可以直接通過filemetadata定位在一個文件後二分查找。level0會查找多個文件。
    上層到容量後觸發向下一層歸併,每一層數據量是比其上一層成倍增長


  • 物理
    sstable=>blocks=>entrys

clipboard.png

  • data index:每個datablock最後一個key+此地址.查找先在bloom(從內存的filemetadata只能判斷範圍,但是稀疏存儲,不知道是否有值)中判斷有再從data_index中二分查找(到重啓點比較)再從data_block中二分查找
  • meta index:目前只有bloom->meta_Data的地址
  • Meta Block:比較特殊的Block,用來存儲元信息,目前LevelDB使用的僅有對布隆過濾器的存儲。寫入Data Block的數據會同時更新對應Meta Block中的過濾器。讀取數據時也會首先經過布隆過濾器過濾.
  • bloom過濾器:key散列到hash%過濾器容量,1代表有0代表無,判斷key在容量範圍內是否存在。因爲hash衝突有一定存在但並不存在的錯誤率 http://www.eecs.harvard.edu/~...
    哈希函數的個數k;=>double-hashing i從0-k, gi(x) = h1(x) + ih2(x) + i^2 mod m,
    布隆過濾器位數組的容量m;布隆過濾器插入的數據數量n; 錯誤率e^(-kn)/m

  • datablock:
    Key := UserKey + SequenceNum + Type
    Type := kDelete or kValue
    clipboard.png
    有相同的Prefix的特點來減少存儲數據量,減少了數據存儲,但同時也引入一個風險,如果最開頭的Entry數據損壞,其後的所有Entry都將無法恢復。爲了降低這個風險,leveldb引入了重啓點,每隔固定條數Entry會強制加入一個重啓點,這個位置的Entry會完整的記錄自己的Key,並將其shared值設置爲0。同時,Block會將這些重啓點的偏移量及個數記錄在所有Entry後邊的Tailer中。



    • filemate的物理結構Manifest

每次進行更新操作就會把更新內容寫入 Manifest 文件,同時它會更新版本號。

合併

  • Minor C 內存超過限制 單獨後臺線 入level0
  • Major C level0的SST個數超過限制,其他層SST文件總大小/allowed_Seeks次數。單獨後臺線程 (文件合併後還是大是否會拆分)
    當級別L的大小超過其限制時,我們在後臺線程中壓縮它。壓縮從級別L中拾取文件,從下一級別L + 1中選擇所有重疊文件。請注意,如果level-L文件僅與level-(L + 1)文件的一部分重疊,則level-(L + 1)處的整個文件將用作壓縮的輸入,並在壓縮後將被丟棄。除此之外:因爲level-0是特殊的(其中的文件可能相互重疊),我們特別處理從0級到1級的壓縮:0級壓縮可能會選擇多個0級文件,以防其中一些文件相互重疊。
    壓縮合並拾取文件的內容以生成一系列級別(L + 1)文件。在當前輸出文件達到目標文件大小(2MB)後,我們切換到生成新的級別(L + 1)文件。噹噹前輸出文件的鍵範圍增長到足以重疊超過十個級別(L + 2)文件時,我們也會切換到新的輸出文件。最後一條規則確保稍後壓縮級別(L + 1)文件不會從級別(L + 2)中獲取太多數據。
    舊文件將被丟棄,新文件將添加到服務狀態。
    特定級別的壓縮在密鑰空間中循環。更詳細地說,對於每個級別L,我們記住級別L處的最後一次壓縮的結束鍵。級別L的下一個壓縮將選擇在該鍵之後開始的第一個文件(如果存在則包圍到密鑰空間的開頭)沒有這樣的文件)。



wal

32K。內存寫入完成時,直接將緩衝區fflush到磁盤
日誌的類型 first full, middle,last 若發現損壞的塊直接跳過直到下一個first或者full(不需要修復).重做時日誌部分內容會嵌入到另一個日誌文件中

記錄
keysize | key | sequnce_number | type |value_size |value
type爲插入或刪除。排序按照key+sequence_number作爲新的key

其他元信息文件

記錄LogNumber,Sequence,下一個SST文件編號等狀態信息;
維護SST文件索引信息及層次信息,爲整個LevelDB的讀、寫、Compaction提供數據結構支持;
記錄Compaction相關信息,使得Compaction過程能在需要的時候被觸發;配置大小
以版本的方式維護元信息,使得Leveldb內部或外部用戶可以以快照的方式使用文件和數據。
負責元信息數據的持久化,使得整個庫可以從進程重啓或機器宕機中恢復到正確的狀態;
versionset鏈表
每個version引用的file(指向filemetadata的二維指針(每層包含哪些file)),如LogNumber,Sequence,下一個SST文件編號的狀態信息
clipboard.png






每個version之間的差異versionedit。每次計算versionedit,落盤Manifest文件(會存version0和每次變更),用versionedit構建新的version。manifest文件會有多個,current文件記錄當前manifest文件,使啓動變快
Manifest文件是versionset的物理結構。中記錄SST文件在不同Level的分佈,單個SST文件的最大最小key,以及其他一些LevelDB需要的元信息。
每當調用LogAndApply(compact)的時候,都會將VersionEdit作爲一筆記錄,追加寫入到MANIFEST文件。並且生成新version加入到版本鏈表。
MANIFEST文件和LOG文件一樣,只要DB不關閉,這個文件一直在增長。
早期的版本是沒有意義的,我們沒必要還原所有的版本的情況,我們只需要還原還活着的版本的信息。MANIFEST只有一個機會變小,拋棄早期過時的VersionEdit,給當前的VersionSet來個快照,然後從新的起點開始累加VerisonEdit。這個機會就是重新開啓DB。
LevelDB的早期,只要Open DB必然會重新生成MANIFEST,哪怕MANIFEST文件大小比較小,這會給打開DB帶來較大的延遲。後面判斷小的manifest繼續沿用。
如果不延用老的MANIFEST文件,會生成一個空的MANIFEST文件,同時調用WriteSnapShot將當前版本情況作爲起點記錄到MANIFEST文件。
dB打開的恢復用MANIFEST生成所有LIVE-version和當前version






分佈式實現

google的bigtable是chubby(分佈式鎖)+單機lebeldb

第二章 rocksdb

https://github.com/facebook/r...

增加功能

range
merge(就是爲了add這種多個rocksdb操作)
工具解析sst
壓縮算法除了level的snappy還有zlib,bzip2(同時支持多文件)
支持增量備份和全量備份
支持單進程中啓動多個實例
可以有多個memtable,解決put和compact的速度差異瓶頸。數據結構:跳錶(只有這個支持併發)\hash+skiplist\hash+list等結構





這裏講了memtable併發寫入的過程,利用了InlineSkipList,它是支持多讀多寫的,節點插入的時候會使用 每層CAS 判斷節點的 next域是否發生了改變,這個 CAS 操作使用默認的memory_order_seq_cst。
http://mysql.taobao.org/monthly/2017/07/05/
源碼分析
https://youjiali1995.github.io/rocksdb/inlineskiplist/

合併

通用合併(有時亦稱作tiered)與leveled合併(rocksdb的默認方式)。它們的最主要區別在於頻度,後者會更積極的合併小的sorted run到大的,而前者更傾向於等到兩者大小相當後再合併。遵循的一個規則是“合併結果放到可能最高的level”。是否觸發合併是依據設置的空間比例參數。
size amplification ratio = (size(R1) + size(R2) + ... size(Rn-1)) / size(Rn)
低寫入放大(合併次數少),高讀放個大(掃描文件多),高臨時空間佔用(合併文件多)

壓縮算法

RocksDB典型的做法是Level 0-2不壓縮,最後一層使用zlib(慢,壓縮比很高),而其它各層採用snappy

副本

  • 備份
    相關接口:CreateNewBackup(增量),GetBackupInfo獲取備份ID,VerifyBackup(ID),恢復:BackupEngineReadOnly::RestoreDBFromBackup(備份ID,目標數據庫,目標位置)。備份引擎open時會掃描所有備份耗時間,常開啓或刪除文件。
    步驟:

    禁用文件刪除
    獲取實時文件(包括表文件,當前,選項和清單文件)。
    將實時文件複製到備份目錄。由於表文件是不可變的並且文件名是唯一的,因此我們不會複製備份目錄中已存在的表文件。例如,如果00050.sst已備份並GetLiveFiles()返回文件00050.sst,則不會將該文件複製到備份目錄。但是,無論是否需要複製文件,都會計算所有文件的校驗和。如果文件已經存在,則將計算的校驗和與先前計算的校驗和進行比較,以確保備份之間沒有發生任何瘋狂。如果檢測到不匹配,則中止備份並將系統恢復到之前的狀態BackupEngine::CreateNewBackup()叫做。需要注意的一點是,備份中止可能意味着來自備份目錄中的文件或當前數據庫中相應的實時文件的損壞。選項,清單和當前文件始終複製到專用目錄,因爲它們不是不可變的。
    如果flush_before_backup設置爲false,我們還需要將日誌文件複製到備份目錄。我們GetSortedWalFiles()將所有實時文件調用並複製到備份目錄。
    重新啓用文件刪除
  • 複製:
    1.1checkpoint
    1.2DisableFileDeletion,然後從中檢索文件列表GetLiveFiles(),複製它們,最後調用2.EnableFileDeletion()。
    RocksDB支持一個API PutLogData,應用程序可以使用該API 來爲每個Put添加元數據。此元數據僅存儲在事務日誌中,不存儲在數據文件中。PutLogData可以通過GetUpdatesSinceAPI 檢索插入的元數據。
    日誌文件時,它將移動到存檔目錄。存檔目錄存在的原因是因爲落後的複製流可能需要從過去的日誌文件中檢索事務
    或者調checkpoint保存




iter

clipboard.png

clipboard.png
第一個圖中的置換LRU,CLOCK。CLOCK介於FIFO和LRU之間,首次裝入主存時和隨後再被訪問到時,該幀的使用位設置爲1。循環,每當遇到一個使用位爲1的幀時,操作系統就將該位重新置爲0,遇到第一個0替換。concurrent_hash_map是CAS等併發安全

更多:
SST大時頂級索引:https://github.com/facebook/r...
兩階段提交:https://github.com/facebook/r...

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章