前言
之所以想尝试写Rocksdb源码分析系列,主要的目的:一是对过去工作的总结;二是系统梳理下自己对Rocksdb的认识;三是因为自己在分析代码的过程中也参考学习了前人的相关资料,受益很多,而且深入分析Rocksdb代码的文章基本没有,所以想在分析过程中形成文字,能让更多的同学得到帮助。由于作者功力有限,分析不对的地方欢迎出指出,多多交流。
后面会在微信公众号中推送后续的章节,与TensorFlow的第一次接触系列已整理成pdf,关注公众号后回复:tensorflow即可下载~~公众号:源码之心
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的析构函数中统一释放内存。内部结构图如下:
Arena是对Allocator的继承实现,另外一个对Alloctar的实现为MemTableAllocator,我们会在Memtable一节中对MemTableAllocator进行详解,此处只分析Arena;下面是Alloctar类关系图,
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的实现需要线程安全,能在多线程环境中使用。
从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等组件。