SStable的结构:
data_block可以有多个,保存的是kv信息,是文件的数据部分
filter_block为过滤数据
metaindex_block为filter_block的index数据,结构同data block只记录了一条数据,key为过滤器名称格式:filter.xxx,value为filter block的size和offset
index_block为data_block的index数据,记录的data_block中每个block的size和offset
footer写在文件尾部,为索引数据的索引,以及文件类型标识
这部分代码实现集中在 table/ 目录
TableBuilder
主要函数实现BuildTable,根据memtable的迭代器,将对应memtable写入磁盘ldb文件
// 输入:iter:new出来的memtable迭代器
// meta:纪录文件最大key/最小key/文件size
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta) {
Status s;
meta->file_size = 0;
iter->SeekToFirst(); // skiplist : list_->head_->Next(0) // next_[0] 头节点指向的level0第一个节点
std::string fname = TableFileName(dbname, meta->number); // dbname/xxx.ldb
if (iter->Valid()) {
WritableFile* file;
s = env->NewWritableFile(fname, &file);
if (!s.ok()) {
return s;
}
TableBuilder* builder = new TableBuilder(options, file);
meta->smallest.DecodeFrom(iter->key());
Slice key;
// 这部分只有data_block直接写了文件
for (; iter->Valid(); iter->Next()) { // node_->Next(0), for循环遍历key,写入
key = iter->key();
builder->Add(key, iter->value());
}
if (!key.empty()) {
meta->largest.DecodeFrom(key);
}
// Finish and check for builder errors
// 把所有key都flush到ldb文件之后执行(最后一个block的flush在Finish方法里执行)
// 写入:执行r->filter_block->Finish()并写入过滤数据,不压缩
// "filter.xxx" 过滤器名称写入
// 写入index block
// 写footer:48字节=两个handle+两个magic number
s = builder->Finish();
if (s.ok()) {
meta->file_size = builder->FileSize();
assert(meta->file_size > 0);
}
delete builder;
// Finish and check for file errors
if (s.ok()) {
s = file->Sync();
}
if (s.ok()) {
s = file->Close();
}
delete file;
file = nullptr;
if (s.ok()) {
// Verify that the table is usable
// 在table_cache创建这个ldb文件对应的迭代器并确认可用
Iterator* it = table_cache->NewIterator(ReadOptions(), meta->number,
meta->file_size);
s = it->status();
delete it;
}
}
// Check for input iterator errors
if (!iter->status().ok()) {
s = iter->status();
}
if (s.ok() && meta->file_size > 0) {
// Keep it
} else {
env->RemoveFile(fname); // 有错误,删文件
}
return s;
}
TableBuilder::Rep
Rep是TableBuilder唯一的成员变量rep_的类型,保存了用于构建table的一些数据及上下文信息。定义:
// 通过options和WritableFile(ldb文件)构建
// TableBuilder构建时初始化
struct TableBuilder::Rep {
Rep(const Options& opt, WritableFile* f)
: options(opt),
index_block_options(opt),
file(f),
offset(0),
data_block(&options),
index_block(&index_block_options),
num_entries(0),
closed(false),
filter_block(opt.filter_policy == nullptr? nullptr : new FilterBlockBuilder(opt.filter_policy)),
pending_index_entry(false) {
index_block_options.block_restart_interval = 1;
}
Options options; // options用户定义
Options index_block_options; // options用户定义
WritableFile* file; // dbname/xxx.ldb,写磁盘
uint64_t offset; // 初始化为0
Status status;
BlockBuilder data_block; // 使用options用户定义来初始化,data_block大于block_size写入文件后clear
BlockBuilder index_block; // 使用options用户定义来初始化,一直在内存中?
std::string last_key; // 上一个key,用来校验是否排序好的,以及pending_index_entry相关用来获取上一个block的最后一个key
int64_t num_entries; // 初始化0,data_block中kv对的数量
bool closed; // Either Finish() or Abandon() has been called.
FilterBlockBuilder* filter_block; // filter写入
// We do not emit the index entry for a block until we have seen the
// first key for the next data block. This allows us to use shorter
// keys in the index block. For example, consider a block boundary
// between the keys "the quick brown fox" and "the who". We can use
// "the r" as the key for the index block entry since it is >= all
// entries in the first block and < all entries in subsequent
// blocks.
//
// Invariant: r->pending_index_entry is true only if data_block is empty.
bool pending_index_entry; // 初始化为false,只有在data_block是空的时候(Flush之后)才为true,写完文件之后改成true,用于标识是否写index_block
BlockHandle pending_handle; // Handle to add to index block
std::string compressed_output;// 临时保存data_block中数据的snappy压缩结果,也是防止频繁分配内存的复用
};
data_block写文件:
结构
文件中一个data block包含block数据部分、type压缩类型、crc校验三部分
其中数据部分 包括kv部分、多个重启点、以及一个重启点数目
每个重启点纪录的数据部分包含多个kvs,每个kv称之为一个entry,entry编码包含五个部分:key共享长度、key非shared长度、value长度、key非共享内容、value内容
其中,key共享长度为当前key与上一个key的前端相同部分长度;每次新的重启点会把这个长度重置
BlockBuilder
用于构建data_block和index_block的类。定义
class BlockBuilder {
...
const Options* options_;
std::string buffer_; // Destination buffer,写入key value的缓冲区
std::vector<uint32_t> restarts_; // Restart points,重启点offset集合
int counter_; // Number of entries emitted since restart, 重启点之后的写入entry个数,用来判断是否到了下一个重启点
bool finished_; // Has Finish() been called?
std::string last_key_; // 上一个key
};
主要函数实现:
Add:向BlockBuilder添加kv
// 功能:将新的key/value写入buffer_,记录重启点等
// 写入key数量到达options_->block_restart_interval之后,restart block,纪录restart时候buffer_的size(重启点位置),并将counter_清0
// buffer_=公共长度(varint32)+非公共长度+value长度 + key非公共部分字符串 + value字符串
// 更新last_key_
void BlockBuilder::Add(const Slice& key, const Slice& value) {
Slice last_key_piece(last_key_); // 拷贝了一下,有更改
assert(!finished_);
assert(counter_ <= options_->block_restart_interval);
assert(buffer_.empty() // No values yet?
|| options_->comparator->Compare(key, last_key_piece) > 0);
size_t shared = 0;
if (counter_ < options_->block_restart_interval) {
// See how much sharing to do with previous string
// 找last_key_piece和key的公共部分结束位置索引
const size_t min_length = std::min(last_key_piece.size(), key.size());
while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
shared++;
} // 找last_key_piece和key的公共部分结束位置索引
} else {
// Restart compression
// counter_ == options_->block_restart_interval, 重新计数,增加一个重启点,shared=0
restarts_.push_back(buffer_.size());
counter_ = 0;
}
const size_t non_shared = key.size() - shared; // 非公共长度
// Add "<shared><non_shared><value_size>" to buffer_
// key的公共长度、非公共长度,value长度编码写入buffer_
PutVarint32(&buffer_, shared);
PutVarint32(&buffer_, non_shared);
PutVarint32(&buffer_, value.size());
// Add string delta to buffer_ followed by value
buffer_.append(key.data() + shared, non_shared);
buffer_.append(value.data(), value.size());
// Update state,这一步操作没看懂为什么,是为了少拷贝shared部分的数据吗?
// 这个last_key_就是key
last_key_.resize(shared);
last_key_.append(key.data() + shared, non_shared); // 写入非公共部分
assert(Slice(last_key_) == key);
counter_++;
}
index_block写文件
每次新开启一个data block,需要向index block写一条数据,记录key、value(size和offset)其中
- key为上一个block的最后一个last_key与当前block第一个first_key之间的值(last_key, first_key),也有可能是last_key,将会用来查找key所在的block
- value为pending_handle的值,其中size为上一个data_block的数据部分字节数(不包括type和crc),offset为上一个data_block开始位置
filter_block写文件
每个ldb文件对应一个filter block,filter block文件结构
filter block的compress type为不压缩
每写一个data block会调用一次GenerateFilter函数,将(block filter data+参数k)写入FilterBlockBuilder的result_中,并记录其在result_中的开始位置到filter_offsets_
主要的类定义:
// 用于把一个table的所有filter构建成一个string,存到table的一个block里面
class FilterBlockBuilder {
public:
explicit FilterBlockBuilder(const FilterPolicy*);
FilterBlockBuilder(const FilterBlockBuilder&) = delete;
FilterBlockBuilder& operator=(const FilterBlockBuilder&) = delete;
void StartBlock(uint64_t block_offset);
void AddKey(const Slice& key);
Slice Finish();
private:
// 把所有key从keys_和starts_中解析出来,存到tmp_keys_里面
// 输入CreateFilter创建过滤器,记录block对应filter的offset
void GenerateFilter();
const FilterPolicy* policy_; // 用户在options里设置的
std::string keys_; // Flattened key contents,所有key拼接起来的字符串,写完一个block就clear
std::vector<size_t> start_; // Starting index in keys_ of each key, 每个key在keys_中的index
std::string result_; // Filter data computed so far
std::vector<Slice> tmp_keys_; // policy_->CreateFilter() argument,keys集合,每个block复用的结构
std::vector<uint32_t> filter_offsets_; // 每个block的filter对应的offset index
};
BloomFilterPolicy
位置:util/bloom.cc
每个data block对应一个filter entry的数据,格式:
| char* array | k |
|---|---|
| 数据 | 参数,1字节,表示使用的hash函数个数(每个key用的位数) |
CreateFilter函数实现
// 输入:keys key集合
// n key数量
// dst:输出结果
// 功能:通过hash生成keys对应的bloom过滤器结果dst
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
// Compute bloom filter size (in both bits and bytes)
// 总共需要的位数 = key个数*每个key的bit数
size_t bits = n * bits_per_key_;
// For small n, we can see a very high false positive rate. Fix it
// by enforcing a minimum bloom filter length.
// n很小时正确率很低,通过强制使用最小布隆过滤器长度来修复
if (bits < 64) bits = 64;
size_t bytes = (bits + 7) / 8; // 字节数,向上取整
bits = bytes * 8;
const size_t init_size = dst->size();
dst->resize(init_size + bytes, 0); // resize并初始化为0
dst->push_back(static_cast<char>(k_)); // Remember # of probes in filter,最后写入一个字节的k参数
char* array = &(*dst)[init_size];
for (int i = 0; i < n; i++) { // 遍历每个key
// Use double-hashing to generate a sequence of hash values.
// See analysis in [Kirsch,Mitzenmacher 2006].
uint32_t h = BloomHash(keys[i]);
const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits,旋转
for (size_t j = 0; j < k_; j++) { // 遍历每个hash函数h
const uint32_t bitpos = h % bits; // char array的index,第bitpos位
array[bitpos / 8] |= (1 << (bitpos % 8)); // set 位,如果是0的话设置成1
h += delta; // 计算第j个hash值
}
}
}
match函数
// 返回:false 不存在这个key
// true 可能存在这个key
bool KeyMayMatch(const Slice& key, const Slice& bloom_filter)
double-hashing论文:
Footer
footer是放到每个table文件尾部的固定格式
48个char
构成:
1.8字节magic number
2.20字节metaindex handle(uint64 offset + uint64 size)
3.20字节index handle
定义:
public:
// Footer编码固定长度=2个BlockHandle和一个magic number的长度
// magic number是一个固定的数字,用于判断文件类型为ldb(sst)
enum { kEncodedLength = 2 * BlockHandle::kMaxEncodedLength + 8 };
private:
BlockHandle metaindex_handle_;
BlockHandle index_handle_;
其中blockHandle是存储指向data block或者meta block在文件位置的指针
这个类的size是16,但是编码长度是20