【4.分佈式存儲】-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下的讀寫性能仍是能夠的。mysql

第一章 levelDb

架構圖git

clipboard.png

讀取過程

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

寫入過程

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

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過程)=》跳錶
默認4Msql

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中。

合併

  • 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(不須要修復).重作時日誌部份內容會嵌入到另外一個日誌文件中segmentfault

記錄
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...

相關文章
相關標籤/搜索