zoukankan      html  css  js  c++  java
  • [leveldb] 2.open操作介绍

    在看源代码之前, 先了解设计结构是必须的, 这就绕不开著名的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.

    这可以说是最重要最有用的改动(不然为啥叫LevelDB?). 想象一下, 如果永远只有一个SSTable, 我要把新immemtable归并进去, 就要重写这个SSTable. 数据有多大, 这个SSTable也会有多大, 那还怎么合并?

    聪明的童鞋可以说那把SSTable分成若干份, 每份2MB. 但wrost case一样悲剧. 比如, 当前这个immemtable恰好永远有一个key与任意SSTable中至少一个key重复. Ops! 又回到了刚刚重写全库的case了.

    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清了, 应该删除的数据不是又莫名其妙恢复了么?

     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来自 1490行,

    Status DB::Open(const Options& options, const std::string& dbname,
                    DB** dbptr) { // static工厂函数
      *dbptr = NULL;
    
      DBImpl* impl = new DBImpl(options, dbname);
    

    源代码有几点习惯挺好的, 值得学习.

    • 提供给外部的接口一般都要做成工厂函数, 避免我觉得有点蠢萌的两步构造.
    • literal type(int, float, void*...)不允许传引用, int a=1; F(a) vs F(&a), 后者更清晰.
    • 总是考虑下是不是要禁止复制, 是的话写上private: A(const A&); void operator=(const A&);
    • 单参构造函数加explicit.

    接上, 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.

    解释下成员变量的含义,

    • env_, 负责所有IO, 比如建立文件
    • internal_comparator_, 用来比较不同key的大小
    • internal_filter_policy_, 可自定义BloomFilter
    • options_, 将调用者传入的options再用一个函数调整下, 可见Google程序员也不是尽善尽美的... 库的作者要帮忙去除错误参数和优化...
    • db_lock_, 文件锁
    • shutting_down_, 基于memory barrier的原子指针
    • bg_cv_, 多线程的条件
    • mem_ = memtable, imm = immemtable
    • tmp_batch_, 所有Put都是以batch写入, 这里建立个临时的
    • manual_compaction_, 内部开发者调用时的魔法参数, 可以不用理会

    我决定先搞懂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部分代码.

     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_, 用于判断是否有等待或者正在写入硬盘的immemtable
    • table_cache_, SSTable查询缓存
    • versions_, 数据库MVCC

    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操作的脉络大概应该是有了

  • 相关阅读:
    8 shell五大运算
    android闹钟——原代码【转】
    draw9patch超详细教程【转】
    史上最全的动画效果 Android Animation 总汇 【转】
    Android 中的BroadCastReceiver【转】
    android屏幕适配【转】
    [Android实例] ViewPager多页面滑动切换以及动画效果【转】
    人脸数据库汇总 【转】
    Android闹钟程序周期循环提醒源码(AlarmManager)【转】
    android背景图片更换——经典例子【转】
  • 原文地址:https://www.cnblogs.com/ym65536/p/7751067.html
Copyright © 2011-2022 走看看