本篇博客是自己学习 Leveldb 实现原理,学习时主要参考leveldb源码分析,内容和图片很多也基本来自这系列文章,但也做了一些补充。
一、leveldb概述
leveldb的KV存储系统涉及skip list、内存KV table、LRU cache管理、table文件存储、operation log系统等。Leveldb的基本框架,几大关键组件,如图所示。
leveldb是一种基于operation log的文件系统,是Log-Structured-Merge Tree的典型实现。LSM源自Ousterhout和Rosenblum在1991年发表的经典论文<<The Design and Implementation of a Log-Structured File System >>。
由于采用了op log,它就可以把随机的磁盘写操作,变成了对op log的append操作,因此提高了IO效率,最新的数据则存储在内存memtable中。
当op log文件大小超过限定值时,就定时做check point。Leveldb会生成新的Log文件和Memtable,后台调度会将Immutable Memtable的数据导出到磁盘,形成一个新的SSTable文件。SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的,而且SSTable的所有文件是一种层级结构,第一层为Level 0,第二层为Level 1,依次类推,层级逐渐增高,这也是为何称之为LevelDb的原因。
一些约定
先说下代码中的一些约定:
1.1 字节序
Leveldb对于数字的存储是little-endian的,在把int32或者int64转换为char*的函数中,是按照先低位再高位的顺序存放的,也就是little-endian的。
1.2 VarInt
把一个int32或者int64格式化到字符串中,除了上面说的little-endian字节序外,大部分还是变长存储的,也就是VarInt。对于VarInt,每byte的有效存储是7bit的,用最高的8bit位来表示是否结束,如果是1就表示后面还有一个byte的数字,否则表示结束。直接见Encode和Decode函数。
在操作log中使用的是Fixed存储格式。
1.3 字符比较
是基于unsigned char的,而非char。
二、基本数据结构
基本数据结构有:
2.1 Slice
Leveldb中的基本数据结构(这个数据结构可以看成像智能指针一样的东西,是对原始char指针的一个扩充感觉和string_view差不多):
数据类型
class LEVELDB_EXPORT Slice {
...
private:
const char* data_;
size_t size_;
};
包括一个const指针和一个记录数据长度的大小。和string一样,允许字符串中包含’\0’。
基本接口
构造函数和运算符的重载
class LEVELDB_EXPORT 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) {}
// Create a slice that refers to the contents of "s"
Slice(const std::string& s) : data_(s.data()), size_(s.size()) {}
// Create a slice that refers to s[0,strlen(s)-1]
Slice(const char* s) : data_(s), size_(strlen(s)) {}
// Intentionally copyable.
Slice(const Slice&) = default;
Slice& operator=(const Slice&) = default;
char operator[](size_t n) const;
...
};
访问和操作内部数据接口
class LEVELDB_EXPORT Slice {
public:
// Return a pointer to the beginning of the referenced data
const char* data() const { return data_; }
// Return the length (in bytes) of the referenced data
size_t size() const { return size_; }
// Return true iff the length of the referenced data is zero
bool empty() const { return size_ == 0; }
// Change this slice to refer to an empty array
void clear();
// Drop the first "n" bytes from this slice.
void remove_prefix(size_t n)
};
其余有关字符串的操作
class LEVELDB_EXPORT Slice {
public:
// Return a string that contains the copy of the referenced data.
std::string ToString() const { return std::string(data_, size_); }
// Three-way comparison. Returns value:
// < 0 iff "*this" < "b",
// == 0 iff "*this" == "b",
// > 0 iff "*this" > "b"
int compare(const Slice& b) const;
// 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));
}
};
逻辑比较函数的重载
inline bool operator==(const Slice& x, const Slice& y);
inline bool operator!=(const Slice& x, const Slice& y)
2.2 Status
Leveldb 中的返回状态,将错误号和错误信息封装成Status类,统一进行处理。并定义了几种具体的返回状态,如成功或者文件不存在等。
数据类型
class LEVELDB_EXPORT Status {
private:
enum Code {
kOk = 0,
kNotFound = 1,
kCorruption = 2,
kNotSupported = 3,
kInvalidArgument = 4,
kIOError = 5
};
// OK status has a null state_. Otherwise, state_ is a new[] array
// of the following form:
// state_[0..3] == length of message
// state_[4] == code
// state_[5..] == message
const char* state_;
};
也就是说status要么是一个ok的状态,此时state_是nullptr,要么是一个异常的status,那么此时state_指向一个char*表示错误信息,消息格式u大概如下:
<------4字节------> <-1-> <---- length ----->
[length of message] [code] [ message \0]
基本接口
构造函数
class LEVELDB_EXPORT Status {
// Create a success status.
Status() noexcept : state_(nullptr) {}
Status(const Status& rhs);
Status& operator=(const Status& rhs);
Status(Status&& rhs) noexcept : state_(rhs.state_) { rhs.state_ = nullptr; }
Status& operator=(Status&& rhs) noexcept;
~Status() { delete[] state_; }
private:
// 将msg和msg2合并成 msg: msg2形式
Status(Code code, const Slice& msg, const Slice& msg2);
};
静态成员函数
class LEVELDB_EXPORT Status {
// Return a success status.
static Status OK() { return Status(); }
// Return error status of an appropriate type.
static Status NotFound(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kNotFound, msg, msg2);
}
static Status Corruption(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kCorruption, msg, msg2);
}
static Status NotSupported(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kNotSupported, msg, msg2);
}
static Status InvalidArgument(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kInvalidArgument, msg, msg2);
}
static Status IOError(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kIOError, msg, msg2);
}
};
状态判断
class LEVELDB_EXPORT Status {
// Returns true iff the status indicates success.
bool ok() const { return (state_ == nullptr); }
// Returns true iff the status indicates a NotFound error.
bool IsNotFound() const { return code() == kNotFound; }
// Returns true iff the status indicates a Corruption error.
bool IsCorruption() const { return code() == kCorruption; }
// Returns true iff the status indicates an IOError.
bool IsIOError() const { return code() == kIOError; }
// Returns true iff the status indicates a NotSupportedError.
bool IsNotSupportedError() const { return code() == kNotSupported; }
// Returns true iff the status indicates an InvalidArgument.
bool IsInvalidArgument() const { return code() == kInvalidArgument; }
};
其他函数
class LEVELDB_EXPORT Status {
public:
// Return a string representation of this status suitable for printing.
// Returns the string "OK" for success.
std::string ToString() const;
private:
static const char* CopyState(const char* s);
Code code() const {
return (state_ == nullptr) ? kOk : static_cast<Code>(state_[4]);
}
};
2.3 Arena
Leveldb的简单的内存池,它所作的工作十分简单,申请内存时,将申请到的内存块放入std::vector blocks_中,在Arena的生命周期结束后,统一释放掉所有申请到的内存,内部结构如图所示。
Arena主要提供了两个申请函数:其中一个直接分配内存,另一个可以申请对齐的内存空间。
class Arena {
// Return a pointer to a newly allocated memory block of "bytes" bytes.
char* Allocate(size_t bytes);
// Allocate memory with the normal alignment guarantees provided by malloc.
char* AllocateAligned(size_t bytes);
....
};
具体原理见源码,内存池不是很复杂,比stl的内存池还简单。
2.4 Skip list
*Skip list(跳跃表)是一种可以代替平衡树的数据结构。**Skip lists应用概率保证平衡,平衡树采用严格的旋转(比如平衡二叉树有左旋右旋)来保证平衡,因此Skip list比较容易实现,而且相比平衡树有着较高的运行效率。
从概率上保持数据结构的平衡比显式的保持数据结构平衡要简单的多。对于大多数应用,用skip list要比用树更自然,算法也会相对简单。由于skip list比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是skip list的常数项相对小很多。skip list在空间上也比较节省。一个节点平均只需要1.333个指针(甚至更少),并且不需要存储保持平衡的变量。
在Leveldb中,skip list是实现memtable的核心数据结构,memtable的KV数据都存储在skip list中。
具体结构如上图,skiplist类中包括一个迭代器和很多节点。
2.5 Cache
Leveldb内部通过双向链表实现了一个标准版的LRUCache,如图
基本单元
struct LRUHandle {
void* value;
void (*deleter)(const Slice&, void* value);
LRUHandle* next_hash;
LRUHandle* next;
LRUHandle* prev;
size_t charge; // TODO(opt): Only allow uint32_t?
size_t key_length;
bool in_cache; // Whether entry is in the cache.
uint32_t refs; // References, including cache reference, if present.
uint32_t hash; // Hash of key(); used for fast sharding and comparisons
char key_data[1]; // Beginning of key
Slice key() const {
// next is only equal to this if the LRU handle is the list head of an
// empty list. List heads never have meaningful keys.
assert(next != this);
return Slice(key_data, key_length);
}
};
LRU的基本单元。
整个LRU Cache组件有HandleTable,hash映射用于管理key,实现key的快速查找。LRUCache,包括lru_和in_use_,表示热点数据和新数据,并内置了mutex保护锁,是线程安全的。ShardedLRUCache内部有16个LRUCache,查找Key时首先计算key属于哪一个分片,分片的计算方法是取32位hash值的高4位,然后在相应的LRUCache中进行查找,这样就大大减少了多线程的访问锁的开销。
具体还是得看代码。
2.6 其他
此外还有其它几个Random、Hash、CRC32、Histogram等,都在util文件夹下,很简单,不过,那个Histogram没仔细看,不知道是什么。