Rocksdb源码剖析(2)--基础数据结构(组件)1


前言

之所以想尝试写Rocksdb源码分析系列,主要的目的:一是对过去工作的总结;二是系统梳理下自己对Rocksdb的认识;三是因为自己在分析代码的过程中也参考学习了前人的相关资料,受益很多,而且深入分析Rocksdb代码的文章基本没有,所以想在分析过程中形成文字,能让更多的同学得到帮助。由于作者功力有限,分析不对的地方欢迎出指出,多多交流。

 后面会在微信公众号中推送后续的章节,与TensorFlow的第一次接触系列已整理成pdf,关注公众号后回复:tensorflow即可下载~~公众号:源码之心

Rocksdb源码剖析(2)--基础数据结构(组件)1_第1张图片
源码之心


Rocksdb源码分析--基础数据结构(组件)

Rockcsdb作为一个可插拔的存储引擎,其内部各个模块基本也是以可插拔为原则,基本的设计套路为:虚基类定义接口,内部继承虚基类提供默认实现版本,同时提供工厂类来负责创建对象;另外,用户可继承虚基类自定义实现接口,将将自定义实现的版本注册进系统内部,这样很方便实现定制化功能。在后续整个Rocksdb源码分析过程,基本也是按照这个模式来,读者可自行体会下。

一.Staus

对于大部分工程系统来说,函数都会有返回值,返回值类型一般为bool或int类型,很多系统中将这两种类型的返回值混杂在一起,一是代码风格不统一,二是这样的返回值表达能力也有限,很多时间需要根据返回值再填充返回给上层的错误信息。Rocksdb中将返回值统一抽象为status类,status类中包括了返回值状态码,子状态码,详细错误信息等,该类的设计基本是照搬了Leveldb中Status类,

class Status {

 public: 

 // Create a success status. 

 Status() : code_(kOk), subcode_(kNone), state_(nullptr) {}

  ~Status() { delete[] state_; } 

 enum Code {  

  kOk = 0,    

 kNotFound = 1,  

  kCorruption = 2,   

 kNotSupported = 3,   

 kInvalidArgument = 4,  

 kIOError = 5,   

 kMergeInProgress = 6,  

 kIncomplete = 7,   

 kShutdownInProgress = 8,  

  kTimedOut = 9, 

  kAborted = 10,    

 kBusy = 11,

 kExpired = 12,    

 kTryAgain = 13  };  

 Code code() const { return code_; } 

 enum SubCode {    

 kNone = 0,    

 kMutexTimeout = 1,    

 kLockTimeout = 2,    

 kLockLimit = 3,    

 kMaxSubCode  }; 

private: 

 // A nullptr state_ (which is always the case for OK) means the message is empty.  of the following form:  state_[0..3] == length of message    state_[4..]  == message   

Code code_;  

SubCode subcode_;  

const char* state_; 

static const char* msgs[static_cast(kMaxSubCode)];

explicit Status(Code _code, SubCode _subcode = kNone): code_(_code), subcode_(_subcode), state_(nullptr) {}

Status(Code _code, const Slice& msg, const Slice& msg2);

}

二. Slice

C++中经常会以string作为参数传递,在string作为参数时,很难避免拷贝操作的发生。Rocksdb中提供了Slice数据结构,该类可以避免出现内存拷贝,同样该类与Leveldb中的定义实现一样。

class Slice {

public:

// Create an empty slice.

Slice() : data_(""), size_(0) { }

// Create a slice that refers to d[0,n-1].

Slice(const char* d, size_t n) : data_(d), size_(n) { }

// Return true iff "x" is a prefix of "*this"

bool starts_with(const Slice& x) const {

return ((size_ >= x.size_) &&

(memcmp(data_, x.data_, x.size_) == 0));

}

// private: make these public for rocksdbjni access

const char* data_;

size_t size_;

// Intentionally copyable

};

Slice类的成员中主要是data_/size_,其中data_为指针,size_为指针指向的内存多大,通过指针来避免大量的内存拷贝。

三.Arena

Rocksdb内部实现了一个简单的内存池:arena,可将arena作为各种数据结构如memtable/sstable的成员,负责内存管理分配,其实现简洁明了,在申请内存时,将申请到的内存保存至blocks_中,在arena的析构函数中统一释放内存。内部结构图如下:


Rocksdb源码剖析(2)--基础数据结构(组件)1_第2张图片
图1-1 Arena结构图

Arena是对Allocator的继承实现,另外一个对Alloctar的实现为MemTableAllocator,我们会在Memtable一节中对MemTableAllocator进行详解,此处只分析Arena;下面是Alloctar类关系图,


Rocksdb源码剖析(2)--基础数据结构(组件)1_第3张图片
图1-2 Arena类图

Arena类定义如下:

class Arena : public Allocator {

public:

// No copying allowed

Arena(const Arena&) = delete;

void operator=(const Arena&) = delete;

static const size_t kInlineSize = 2048;

static const size_t kMinBlockSize;

static const size_t kMaxBlockSize;

// huge_page_size: if 0, don't use huge page TLB. If > 0 (should set to the

// supported hugepage size of the system), block allocation will try huge

// page TLB first. If allocation fails, will fall back to normal case.

explicit Arena(size_t block_size = kMinBlockSize, size_t huge_page_size = 0);

~Arena();

char* Allocate(size_t bytes) override;

char* AllocateAligned(size_t bytes, size_t huge_page_size = 0,

Logger* logger = nullptr) override;

size_t ApproximateMemoryUsage() const {

return blocks_memory_ + blocks_.capacity() * sizeof(char*) -

alloc_bytes_remaining_;

}

size_t MemoryAllocatedBytes() const { return blocks_memory_; }

size_t AllocatedAndUnused() const { return alloc_bytes_remaining_; }

// If an allocation is too big, we'll allocate an irregular block with the

// same size of that allocation.

size_t IrregularBlockNum() const { return irregular_block_num; }

size_t BlockSize() const override { return kBlockSize; }

private:

char inline_block_[kInlineSize];

// Number of bytes allocated in one block

const size_t kBlockSize;

// Array of new[] allocated memory blocks

typedef std::vector Blocks;

Blocks blocks_;

struct MmapInfo {

void* addr_;

size_t length_;

MmapInfo(void* addr, size_t length) : addr_(addr), length_(length) {}

};

std::vector huge_blocks_;

size_t irregular_block_num = 0;

char* unaligned_alloc_ptr_ = nullptr;

char* aligned_alloc_ptr_ = nullptr;

size_t alloc_bytes_remaining_ = 0;

size_t hugetlb_size_ = 0;

char* AllocateFromHugePage(size_t bytes);

char* AllocateFallback(size_t bytes, bool aligned);

char* AllocateNewBlock(size_t block_bytes);

// Bytes of memory in blocks allocated so far

size_t blocks_memory_ = 0;

};

Allocator是一个虚基类,定义了分配器要实现的接口,arena继承Allocator并实现了两个接口:Allocate/AllocateAligned,一个是直接分配内存,一个可分配对齐的内存,同时arena中还支持huge_page概念。由于arena同时能直接分配内存与分配对齐的内存,在blocks_正在使用的block中,分别从两端开始,一端负责直接分配内存,一端负责分配对齐的内存,这种设计可以减少因内存对齐浪费掉的空间。在arena的析构函数中,会统一一次性释放掉所有内存,huge_page需要系统内核层面的支持,此处暂不谈论。Arena的具体实现可参考util/arena.cc。

四.Env

Rocksdb作为一个开源项目,想成为跨平台的存储引擎,一定会面对各种平台系统环境,如linux/mac/win等,所以,rocksdb在存储与系统之间抽象了Env类,其包含了需要访问操作系统的接口,其内部默认实现了env_hdfs/env_posix两种平台,用户完全可根据实际情况自定义实现Env接口,如访问本地文件系统的流量控制接口,同时Env的实现需要线程安全,能在多线程环境中使用。


Rocksdb源码剖析(2)--基础数据结构(组件)1_第4张图片
图1-3 Env类关系图

从Env定义的接口来看,主要分为:文件与目录操作相关(创建/删除/修改),文件锁,获取系统时间戳,不同优先级的后台线程池调度管理等,Rocksdb内部提供了一个默认实现,这里面最重要的一类接口是与后台线程池有关的,如Schedule/StartThread,它们与后面要分析的flush/compaction紧密相关,而且rocksdb与leveldb相比,一个优化的地方就是:Rocksdb支持多线程compaction;Rocksdb内部中与多线程相关的操作,基本全通过env中的接口来调度,这种设计方式也挺值的借鉴。

Rocksdb内部继承Env并提供了两种类型的实现,分别为env_posix与env_hdfs,根据名字就可以推断出这分别是在posix与hdfs平台上提供的具体实现。由于env_posix的具体实现十分繁琐复杂,且有很强的平台相关性,我们只需要知道接口功能即可,如对具体实现感兴趣,读者可自行分析util/env_xxx.cc文件。另外,Env变量是能够在多个Rocksdb实例间共享。


下一节中我们会继续分析Rocksdb中字符串与整型变长编码及comparator等组件。

你可能感兴趣的:(Rocksdb源码剖析(2)--基础数据结构(组件)1)