在看源代码以前, 先了解设计结构是必须的, 这就绕不开著名的LSM Tree了. 我在阅读了原做者论文和BigTable论文以后, 一开始最惊奇的是"伪代码"呢? 没有. 其实LSM Tree与其说是某种数据结构/算法, 倒不如说是一种设计思路, 用日志和批量写入来替代索引更新, 达到经过牺牲随机查询速度换取更迅捷地写入的效果. 动机是机械硬盘时代, seek操做是昂贵的, 由于须要马达转动磁盘. 固态硬盘时代, 状况极大地好转了. 但仍然不可避免的是顺序写入一样快过随机的.程序员
强烈建议阅读LSM Tree相关文章, 太基本的我就不重复介绍了, 下面讲点思考.算法
全部数据直接写入memtable并打log, 当memtable足够大的时候, 变为immemtable, 开始往硬盘挪, 成为SSTable. 这就是LSM Tree仅有的所有. 你能够用任何有道理的数据结构来表示memtable, immemtable和SSTable. Google选择用跳表实现memtable和immemtable, 用有序行组来实现SSTable.数据库
一点也不惊喜吧~ 原论文1996年发表, 过了好多年才被Google工程师发掘. 问题太严重了. 首先, 搜索key最差时要发疯同样从memtable读到immemtable, 再到全部SSTable. 其次, SSTable要怎么有效merge(Google称之为"major compaction")呢? 数据库写啊写, 有10G了, 新来了一个immemtable要归并, 一言不合重写10个多G? 对此, 原论文描述了一种多组件版本, 下降了瞬时IO压力, 但总IO却更高了, 没解决什么大问题.缓存
Google打了两个加强补丁.数据结构
1. 添加BloomFilter, 这样能够提高全库扫描的速度, 确定没这个key的SSTable直接跳过.多线程
2. leveled compaction, 把SSTable分红不一样的等级. 除等级0之外, 其他各等级的SSTable不会有重复的key.app
这能够说是最重要最有用的改动(否则为啥叫LevelDB?). 想象一下, 若是永远只有一个SSTable, 我要把新immemtable归并进去, 就要重写这个SSTable. 数据有多大, 这个SSTable也会有多大, 那还怎么合并?函数
聪明的童鞋能够说那把SSTable分红若干份, 每份2MB. 但wrost case同样悲剧. 好比, 当前这个immemtable刚好永远有一个key与任意SSTable中至少一个key重复. Ops! 又回到了刚刚重写全库的case了.oop
Google的作法则让每次compaction波及到的范围是可预期的. 官方文档摘抄: "The compaction picks a file from level L and all overlapping files from the next level L+1". 这就很是优雅了! 数据库一个老大难题就是怎么释放被删除记录的空间? LevelDB这种不当即释放只按等级延迟合并的方法是很高明的, 没有任何随机读写操做, 机制上又很简单, 还不须要bookkeeping.学习
在第一部分的最后纠正下网上不少博文都有错的点(源代码证明). compaction不必定会清空全部deletion maker. 这个思考下就明白了. 若是下级还有相同key的数据, 就把deletion maker清了, 应该删除的数据不是又莫名其妙恢复了么?
http://db_impl.cc 958-967行,
} else if (ikey.type == kTypeDeletion && ikey.sequence <= compact->smallest_snapshot && compact->compaction->IsBaseLevelForKey(ikey.user_key)) { // For this user key: // (1) there is no data in higher levels // (2) data in lower levels will have larger sequence numbers // (3) data in layers that are being compacted here and have // smaller sequence numbers will be dropped in the next // few iterations of this loop (by rule (A) above). // Therefore this deletion marker is obsolete and can be dropped.
------
理解了大致设计, 啃代码的时间到了. 跟我一块儿看看leveldb::Status status = leveldb::DB::Open(options, "testdb", &db);会触发什么模块吧.
leveldb::DB::Open来自http://db_impl.cc 1490行,
Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) { // static工厂函数 *dbptr = NULL; DBImpl* impl = new DBImpl(options, dbname);
源代码有几点习惯挺好的, 值得学习.
接上, new而后跳到117行的构造函数,
1 DBImpl::DBImpl(const Options& raw_options, const std::string& dbname) 2 : env_(raw_options.env), // Env* const 3 internal_comparator_(raw_options.comparator), // const InternalKeyComparator 4 internal_filter_policy_(raw_options.filter_policy), // const InternalFilterPolicy 5 options_(SanitizeOptions(dbname, &internal_comparator_, // const Options 6 &internal_filter_policy_, raw_options)), 7 owns_info_log_(options_.info_log != raw_options.info_log), // bool 8 owns_cache_(options_.block_cache != raw_options.block_cache), // bool 9 dbname_(dbname), // const std::string 10 db_lock_(NULL), // FileLock* 11 shutting_down_(NULL), // port::AtomicPointer 12 bg_cv_(&mutex_), // port::CondVar 13 mem_(NULL), // MemTable* 14 imm_(NULL), // MemTable* 15 logfile_(NULL), // WritableFile* 16 logfile_number_(0), // uint64_t 17 log_(NULL), // log::Writer* 18 seed_(0), // uint32_t 19 tmp_batch_(new WriteBatch), // WriteBatch* 20 bg_compaction_scheduled_(false), // bool 21 manual_compaction_(NULL) { // ManualCompaction* 22 has_imm_.Release_Store(NULL); 23 24 // Reserve ten files or so for other uses and give the rest to TableCache. 25 const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles; 26 table_cache_ = new TableCache(dbname_, &options_, table_cache_size); 27 28 versions_ = new VersionSet(dbname_, &options_, table_cache_, 29 &internal_comparator_); 30 }
Google C++ Style虽然禁止函数默认参数, 但容许你扔个Options.
解释下成员变量的含义,
我决定先搞懂memory barrier的原子指针再继续分析, 就先到这了.
我之前历来没有C++多线程的经验, 借着看源码的机会, 才有机会了解. 曾今工做时, 我写Python爬虫就用thread-safe队列, 觉得原子性全是靠锁实现的. 所谓的无锁就是先修改再检查要不要反悔的乐观锁. 我错了, X86 CPU的赋值(Store)和读取(Load)操做自然能够作到无锁.
相关问题: C++的6种memory order
那memory barrier这个名词是哪里蹦出来的呢? Load是原子性操做, CPU不会Load流程走到一半, 就切换到另外一个线程去了, 也就是Load自己是不会在多线程环境下产生问题的. 真正致使问题的是作这个操做的时机不肯定!
1. 编译器有可能让指令乱序, 好比, int a=b; long c=b; 编译器一旦断定a和c没有依赖性, 就有权力让这两个取值操做以任意顺序执行. 由于有可能有CPU指令能够一下取4个int, 乱序能够凑个整.
2. CPU会让指令乱序, 缘由同上, 但额外还有个缘由是分支预测. AB线程都读写一个中间量c, B在处理c, 你预期B好了, A才会取. 但万一A分支预测成功, B在处理的时候, A已经提早Load c进寄存器, 这就没得玩了...
因此, 必需要有指令告诉CPU和编译器, 不要改变这个变量的存取顺序. 这就是Memory Barrier了. call MemoryBarrier保证先后一行是严格按照代码顺序的.
atomic_pointer.h 126-143行, 注意MemoryBarrier()的摆放,
1 class AtomicPointer { 2 private: 3 void* rep_; 4 public: 5 AtomicPointer() { } 6 explicit AtomicPointer(void* p) : rep_(p) {} 7 inline void* NoBarrier_Load() const { return rep_; } 8 inline void NoBarrier_Store(void* v) { rep_ = v; } 9 inline void* Acquire_Load() const { 10 void* result = rep_; 11 MemoryBarrier(); 12 return result; 13 } 14 inline void Release_Store(void* v) { 15 MemoryBarrier(); 16 rep_ = v; 17 } 18 };
大公司的开源项目真的是一个宝库! 就算用不到, 各类踩了无数坑的库, 编码规则和跨平台代码都是通常人没机会完善的.
另外, 有菊苣在问题leveldb中atomic_pointer里面memory barrier的几点疑问?提到MemoryBarrier不保证CPU不乱序. 我以为这个应该不用担忧. 由于MemoryBarrier的counterpart是std::atomic, 确定严格保证语义相同啊. 实在不放心用std::atomic是坠吼的.
------
继续上次没读完的Open部分代码.
http://db_impl.cc 139-146行,
has_imm_.Release_Store(NULL); // atomic pointer // Reserve ten files or so for other uses and give the rest to TableCache. const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles; table_cache_ = new TableCache(dbname_, &options_, table_cache_size); versions_ = new VersionSet(dbname_, &options_, table_cache_, &internal_comparator_);
has_imm_就是我上面描述的atomic pointer, 我推测这里大几率Google程序员雇了一个临时工(233), 把能够列表构造的has_imm_放到了函数部分, 由于这里不存在任何race的可能性. db new完了. 说下一个很重要的原则, 构造函数究竟要作什么? 阿里和Google共同的观点: 轻且无反作用(基本就是赋值). 业务有需求的话, 两步构造或者工厂函数, 二选一.
回到最先的工厂函数, 一个靠谱数据库的Open操做, 用脚趾头也能想到要从日志恢复数据,
1 DB::Open(const Options& options, const std::string& dbname, 2 DB** dbptr) { // 工厂函数 3 *dbptr = NULL; // 设置结果默认值, 指针传值 4 5 DBImpl* impl = new DBImpl(options, dbname); 6 impl->mutex_.Lock(); // 数据恢复时上锁, 禁止全部可能的后台任务 7 VersionEdit edit; 8 // Recover handles create_if_missing, error_if_exists 9 bool save_manifest = false; 10 Status s = impl->Recover(&edit, &save_manifest); // 读log恢复状态 11 if (s.ok() && impl->mem_ == NULL) { 12 // Create new log and a corresponding memtable. 复位 13 uint64_t new_log_number = impl->versions_->NewFileNumber(); 14 WritableFile* lfile; 15 s = options.env->NewWritableFile(LogFileName(dbname, new_log_number), 16 &lfile); 17 if (s.ok()) { 18 edit.SetLogNumber(new_log_number); 19 impl->logfile_ = lfile; 20 impl->logfile_number_ = new_log_number; 21 impl->log_ = new log::Writer(lfile); 22 impl->mem_ = new MemTable(impl->internal_comparator_); 23 impl->mem_->Ref(); 24 } 25 } 26 if (s.ok() && save_manifest) { 27 edit.SetPrevLogNumber(0); // No older logs needed after recovery. 28 edit.SetLogNumber(impl->logfile_number_); 29 s = impl->versions_->LogAndApply(&edit, &impl->mutex_); // 同步VersionEdit到MANIFEST文件 30 } 31 if (s.ok()) { 32 impl->DeleteObsoleteFiles(); // 清理无用文件 33 impl->MaybeScheduleCompaction(); // 有写入就有可能要compact 34 } 35 impl->mutex_.Unlock(); // 初始化完毕 36 if (s.ok()) { 37 assert(impl->mem_ != NULL); 38 *dbptr = impl; 39 } else { 40 delete impl; 41 } 42 return s; 43 }
------
就这样, Open操做的脉络大概应该是有了