leveldb内存、磁盘文件格式

本文详细介绍了LevelDB数据库中从内存到磁盘的数据结构,包括memtable的SkipList实现、并发控制、arena内存池、keycompare、Immutablememtable转换、manifest文件、version和versionedit、current文件、log文件(WAL)以及sstable文件的footer和block结构。
摘要由CSDN通过智能技术生成

内容简介

主要介绍leveldb从内存到磁盘的基本的数据结构,主要包括memtable,immutable memtable,manifest文件,current 文件,log文件,sstable文件。

memtable

在这里插入图片描述

以上Table定义为SkipList<const char*, KeyComparator> Table
从提供的功能来看,主要支持get和add;从结构来看,其包含了skiplist,area,keycompare,即可以理解对skiplist和内存分配和内部编码做了个组合形成了memtable。下面进行分别描述:

skiplist

跳表基础知识可见:https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf

对leveldb中跳表做以下分析:

1.本身的概率均衡和内存使用考虑
根据 RandomHeight 的实现,leveldb 将每个元素高度增长的概率设置为 1/4

template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
  // Increase height with probability 1 in kBranching
  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxHeight && rnd_.OneIn(kBranching)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxHeight);
  return height;
}

2.并发访问考虑

leveldb的skiplist支持一写多读的访问,对于写来说,存在写入队列,保证同时只有一个线程在写,获取到锁,写入队列,然后根据条件变量等待。对于读写并发,基于以下假设:
1.除非跳表被销毁,跳表节点只会增加而不会被删除,因为跳表对外根本不提供删除接口。
2.被插入到跳表中的节点,除了 next 指针其他域都是不可变的,并且只有插入操作会改变跳表。
其内部节点Node利用atomic来保证原子性,还利用了内存屏障,防止指令重排,代码见:

Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return next_[n].load(std::memory_order_acquire);
  }
  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].store(x, std::memory_order_release);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_relaxed);
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_relaxed);
  }

arena

arena在leveldb中分析过,是一个内存池,支持普通和对齐方式分配

keycompare

负责从 memkey 中提取出 internalkey,最终排序逻辑是使用 InternalKeyComparator 进行比较,排序规则如下:
1.优先按照 user key 进行排序。
2.User key 相同的按照 seq 降序排序。
3.User key 和 seq 相同的按照 type 降序排序
所以,在一个 MemTable 中,相同的 user key 的多个版本,新的排在前面,旧的排在后面。

下面来看一下各种key
在这里插入图片描述

组织形式可以参见(部分):

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,const Slice& value) {
  // Format of an entry is concatenation of:
  //  key_size     : varint32 of internal_key.size()
  //  key bytes    : char[internal_key.size()]
  //  tag          : uint64((sequence << 8) | type)
  //  value_size   : varint32 of value.size()
  //  value bytes  : char[value.size()]
  size_t key_size = key.size();
  size_t val_size = value.size();
  size_t internal_key_size = key_size + 8;
  const size_t encoded_len = VarintLength(internal_key_size) +
                             internal_key_size + VarintLength(val_size) +
                             val_size;
  char* buf = arena_.Allocate(encoded_len);
  char* p = EncodeVarint32(buf, internal_key_size);
  std::memcpy(p, key.data(), key_size);
  p += key_size;
  EncodeFixed64(p, (s << 8) | type);
  p += 8;
  p = EncodeVarint32(p, val_size);
  std::memcpy(p, value.data(), val_size);
  assert(p + val_size == buf + encoded_len);
  table_.Insert(buf);
}

Immutable memtable

一个只读memtable,数据达到一定数量write_buffer_size,memtable就会转换为immutable memtable,同时新建一个memtable,此时后台线程会将immutable memtable组织为sstable格式写入磁盘。

manifest文件

Manifest 文件存储的信息是leveldb的元数据信息,其中存放着Version信息和versionedit信息,前面存放version信息,后面存放versionedit信息,这样方便恢复,比如已经到了version10,然后修改过程宕机,不能从version1开始恢复,要从version10开始。

versionedit

versionedit记录元数据变更,其内部数据如下:
在这里插入图片描述

versionedit就可以看作元数据的变更日志

version

Version 是 数据库某一次应用versionedit后的元数据状态

versionset

VersionSet 是一个 Version 的集合。

leveldb内部数据库状态变化会产生VersionEdit,然后产生新的version,此时,旧version可能仍然在使用,所以会存在多个version,versionset就是一个链表将这些version维护起来,为了避免宕机丢失数据,需要持久化,就是manifest文件。
在这里插入图片描述

这样恢复时我们就能根据versionedit来获取下一个版本

current 文件

保存最新的manifest文件名称,从中读取后,读取manifest文件恢复

log文件-WAL

目的:防止突然断电、宕机等各种异常场景导致的数据丢失

基本组成

wal日志按照block为单位进行存储的,每个block大小为32k。而每个block是由一系列record组成的,每个预写日志大小为4M,和memtable默认大小相同,格式如下:
在这里插入图片描述

我们可以看到有个recordtype的字段,其值为以下:

enum RecordType {
  kZeroType = 0,        /* 预留给预分配文件 */
  kFullType = 1,        /* 当前 Record 包含完整的日志记录 */
  kFirstType = 2,       /* 当前 Record 包含日志记录的起始部分 */
  kMiddleType = 3,      /* 当前 Record 包含日志记录的中间部分 */
  kLastType = 4         /* 当前 Record 包含日志记录的结束部分 */
};

用来处理超出record大小超出32k场景。这么做一个明显的好处就是,当日志文件发生数据损坏的时候,这种定长块的模式可以很简单地跳过有问题的块,而不会导致局部的错误影响到整个文件。

清理时机

1.opendb时会清理不需要的文件
2.Compaction 后清理不需要的文件

关于存储空间的考虑:InnoDB 存储的 redo log 就是一种典型的 WAL 实现,由于预写日志不可能任其无限增长,所以 InnoDB 使用了“环形数组”的方式进行覆盖写入,单个 redo log 的默认大小为 48MB。leveldb 并没有采用循环写入的方式实现,而是使用创建新的日志文件并删除旧有的日志文件实现。

sstable文件

整体结构:
在这里插入图片描述

footer

footer包含magic number,padding(补齐48字节),index offset,index size,metadata offset,metadata size。这样通过footer就能找到其他的元数据块。

block

Block
Index block、meta index block、data block 都是用 BlockBuilder 创建,使用 Block 读取,为了节省空间,LevelDB 在 block 的内部实现了前缀压缩,后续流程里介绍。

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值