现在开始觉得看过的东西如果记下来总觉得像没看一样,网上源码分析贴很多,文章质量各有所长。本文并不打算如对代码进行一一分析,决定只做资料的搬运工,完成对所看过的一些比较好的资源的一些索引,方便日后回头查看,就酱。
LevelDB组件源码
基础类
Slice
其实就是对字符串的封装,Slice是贯穿整个源码的类,只有两个私有的成员变量,如下:123456class Slice {...private:const char* data_;size_t size_;};VarInt
作者为了将整数存储的空间进一步压缩,想法子采用变长整数编码的技术,因为如果不这样,一个int整数无论大小如何都会占用4B的空间,采用变长编码之后会根据实际数字的大小采用不同的字节数。
参考:讨论coding.h/coding.cc- Arena
在理解下面的mentable的实现时,必须要先知道memtable是如何管理内存,因为数据首先会以一定的格式存到mentable中,那么每次add(k,v)的过程中,是必然要分配内存空间的,那么Arena其实就相当于memtable的内存大管家,来管理内存的分配和释放。Arena的几个私有变量如下:12345678// 当前指针指向的位置char* alloc_ptr_;//分配的blocks里面还有多少空间没用完size_t alloc_bytes_remaining_;// 用来存放blocks的指针std::vector<char*> blocks_;// 用了多少内存空间port::AtomicPointer memory_usage_;
详情参考:Arena内存管理
- Skiplist
关于跳表,网上的资源很多,跳跃表(skiplist)是一种随机化的数据,由 William Pugh 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出,效率和平衡树媲美——查找、删除、添加等操作都可以在对数期望时间(O(lgn))下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多。
memtable
先看看memtable.h中的几个比较重要的私有成员变量
再来看看Add方法的源码是如何实现的
其他详情参考:MemTable
sstable
逻辑结构
如果你想看原汁原味的作者写的文档,可以打开/doc/table_format.txt,作者在该文档中大致描述了sstable的轮廓,不过看完总是还有些许地方比较疑惑,感觉描述的不是那么清晰,比如block data中的restartpoint(重启点)概念等等,从找的资料来看,下面这篇参考博文对sstable的逻辑结构解释比较清楚易懂:
也许你会以为在划分好block的数据存储区域以后那么就是一个一个的KV对(如图中的Record)了,但是其实不是,leveldb为了降低数据的存储量和快速的查找引入了一个重启点(restartpoint)的概念。出自参考1:SSTable之逻辑结构
参考2:SSTable的文件组织
table_builder解析
知道了sstable文件的逻辑结构,才能更好的理解sstable的创建过程,在源码中主要是由table_builder.cc这个文件来帮助完成这些功能的(注意table_builder并不是sstable文件的格式,它只是完成了文件内容的组装和写入)。刚开始看可能会云里雾里,但是在阅读代码之前脑子中尤其应该有data block、index block这些逻辑结构的印象,这是理解下面代码的基本。
- table_builder.h中的结构体和方法123456789private:bool ok() const { return status().ok(); }//写文件之前将数据序列化为slicevoid WriteBlock(BlockBuilder* block, BlockHandle* handle);//将内容写进文件中void WriteRawBlock(const Slice& data, CompressionType, BlockHandle* handle);struct Rep;//其实是封装了table_builder的成员变量,隐藏细节Rep* rep_;
下面会进入table_builder.cc文件中。
结构体Rep的成员变量:
12345678910111213141516171819Options options;Options index_block_options;WritableFile* file; //!!!sstable文件!!!uint64_t offset; // data block在sstable文件中的偏移,初始0Status status;BlockBuilder data_block; //!!!当前操作的data block!!!BlockBuilder index_block; // sstable的index blockstd::string last_key; //当前data block最后的一条k/v对的keyint64_t num_entries; //当前k/v个数,初始0,add方法中可知bool closed; //调用了Finish() or Abandon(),初始falseFilterBlockBuilder* filter_block;//根据这个块可以快速定位key在不在bool pending_index_entry;BlockHandle pending_handle;//属于index block中的每个k/v中的v,指向data block,可以从字面理解std::string compressed_output;//压缩后的data block,临时存储,写入后即被清空Add方法添加每条kv
该方法的主要作用就是给当前的data block添加k/v对,这中间还要完成一系列其他的操作,比如:在index block中添加data block的索引键值对(key<->blockhandle)、在filter block中添加key。123456789101112131415161718192021222324252627282930313233343536373839404142434445void TableBuilder::Add(const Slice& key, const Slice& value) {Rep* r = rep_;assert(!r->closed);if (!ok()) return;if (r->num_entries > 0) {assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);}//r->pending_index_entry标记当前block是否为空,为null才允许进入if (r->pending_index_entry) {assert(r->data_block.empty());r->options.comparator->FindShortestSeparator(&r->last_key, key);std::string handle_encoding;r->pending_handle.EncodeTo(&handle_encoding);//所以只能每一个新的data block,在index block中添加一条索引r->index_block.Add(r->last_key, Slice(handle_encoding));r->pending_index_entry = false;}//filter_block不空的时候,添加keyif (r->filter_block != NULL) {r->filter_block->AddKey(key);}r->last_key.assign(key.data(), key.size());//总记录数自增r->num_entries++;//当前data block添加记录r->data_block.Add(key, value);const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();//当前data block大小达到一定阀值,将缓存kv写入文件。if (estimated_block_size >= r->options.block_size) {Flush();}}void TableBuilder::Flush() {Rep* r = rep_;assert(!r->closed);if (!ok()) return;if (r->data_block.empty()) return;assert(!r->pending_index_entry);/////////////////////////////////////////////WriteBlock(&r->data_block, &r->pending_handle);/////////////////////////////////////////////...}WriteBlock方法完成写block的准备工作
这里的block不仅仅是data block,而是所有的block,从最下面的Finish方法就可以看出。包括data block、filter block在内的所有block都会通过该方法完成block写入文件。123456789101112131415161718192021222324252627void TableBuilder::WriteBlock(BlockBuilder* block, BlockHandle* handle) {// File format contains a sequence of blocks where each block has:// block_data: uint8[n]// type: uint8// crc: uint32assert(ok());Rep* r = rep_;Slice raw = block->Finish();//获得data block的序列化字符串,注意这里不要看错是Finish,不是Flush//完成压缩方式的选择Slice block_contents;CompressionType type = r->options.compression;switch (type) {case kNoCompression://不压缩block_contents = raw;break;case kSnappyCompression: {//压缩...break;}}///////////////开始真正写文件方法///////////////WriteRawBlock(block_contents, type, handle);//写完之后的一些清理工作r->compressed_output.clear();block->Reset();}WriteRawBlock真正写入block数据到文件
12345678910111213141516171819void TableBuilder::WriteRawBlock(const Slice& block_contents,CompressionType type,BlockHandle* handle) {Rep* r = rep_;handle->set_offset(r->offset);//在handle中设置当前block在文件中的偏移handle->set_size(block_contents.size());//设置block大小r->status = r->file->Append(block_contents);//将当前block内容写入file中if (r->status.ok()) {// 写入1byte的type和4bytes的crc32char trailer[kBlockTrailerSize];trailer[0] = type;uint32_t crc = crc32c::Value(block_contents.data(), block_contents.size());crc = crc32c::Extend(crc, trailer, 1);EncodeFixed32(trailer+1, crc32c::Mask(crc));r->status = r->file->Append(Slice(trailer, kBlockTrailerSize));if (r->status.ok()) {r->offset += block_contents.size() + kBlockTrailerSize;// 写入成功更新offset-下一个data block的写入偏移}}}Finish()方法完成添加完所有KV之后的工作
基本就是按照sstable文件格式的顺序来完成的,Write filter block、Write metaindex block、Write index block、 Write footer。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556Status TableBuilder::Finish() {Rep* r = rep_;Flush();assert(!r->closed);r->closed = true;BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;// Write filter blockif (ok() && r->filter_block != NULL) {WriteRawBlock(r->filter_block->Finish(), kNoCompression,&filter_block_handle);}// Write metaindex blockif (ok()) {BlockBuilder meta_index_block(&r->options);if (r->filter_block != NULL) {// Add mapping from "filter.Name" to location of filter datastd::string key = "filter.";key.append(r->options.filter_policy->Name());std::string handle_encoding;filter_block_handle.EncodeTo(&handle_encoding);meta_index_block.Add(key, handle_encoding);}// TODO(postrelease): Add stats and other meta blocksWriteBlock(&meta_index_block, &metaindex_block_handle);}// Write index blockif (ok()) {if (r->pending_index_entry) {r->options.comparator->FindShortSuccessor(&r->last_key);std::string handle_encoding;r->pending_handle.EncodeTo(&handle_encoding);r->index_block.Add(r->last_key, Slice(handle_encoding));r->pending_index_entry = false;}WriteBlock(&r->index_block, &index_block_handle);}// Write footerif (ok()) {Footer footer;footer.set_metaindex_handle(metaindex_block_handle);footer.set_index_handle(index_block_handle);std::string footer_encoding;footer.EncodeTo(&footer_encoding);r->status = r->file->Append(footer_encoding);if (r->status.ok()) {r->offset += footer_encoding.size();}}return r->status;}
到这基本sstable文件的创建过程就结束了,一刚开始看基本是懵懵懂懂,到最后仔细看完,发现是大快人心啊,逻辑顺畅太棒了!下面是参考文章,不过感觉排版有点乱,看起来有点累。
参考:创建sstable文件
minor compaction
结束了memtable和sstable这两个在leveldb中举足轻重的两个组件,我们尝试来了解compaction这个过程,也就是immutable memtable是在什么时候dump为sstable的,即为minor compaction这个过程。
在看网上的一些参考文章时候,在db_impl.cc文件中的CompactMemTable()方法似乎发现这部分代码:
进入BuildTable,发现其实就是build.h/cc这个文件,整个文件就是定义了一个方法,其他什么都没做,BuildTable方法,代码如下:
这就是一个minor compaction的过程,很简单。