【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...

相关文章
相关标签/搜索