美文网首页LevelDB数据存储大数据
8. LevelDB源码剖析之日志文件

8. LevelDB源码剖析之日志文件

作者: 随安居士 | 来源:发表于2017-08-13 21:11 被阅读46次

    8.1 基本原理

    "LOG文件在LevelDb中的主要作用是系统故障恢复时,能够保证不会丢失数据。因为在将记录写入内存的Memtable之前,会先写入Log文件,这样即使系统发生故障,Memtable中的数据没有来得及Dump到磁盘的SSTable文件,LevelDB也可以根据log文件恢复内存的Memtable数据结构内容,不会造成系统丢失数据,在这点上LevelDb和Bigtable是一致的。" --- 数据分析与处理之二(Leveldb 实现原理)

    8.2. 日志文件

    8.2.1 数据结构

    日志中的每条记录由Record Header + Record Content组成,其中Header大小为kHeaderSize(7字节),由CRC(4字节) + Size(2字节) + Type(1字节)三部分组成。除此之外才是content的真正内容:

    日志记录

    日志文件的基础部件很简单,只需要能够创建文件、追加操作、实时刷新数据即可。为了做到跨平台、解耦,LevelDB还是对此做了封装。Leveldb命名空间下,有一个名为log的子命名空间,其下有Writer、Reader两个实现类。按前几节的命名规则,Writer其实是一个Builder,它对外提供了唯一的AddRecord方法用于追加操作记录。

    8.2.2 Writer

    在log命名空间中,包含一个Writer用于日志操作,其只有一个Append方法,这和日志的定位相同,定义如下:

    class Writer
    {
      explicit Writer(WritableFile *dest);
      Writer(WritableFile *dest, uint64_t dest_length);
      ...
      Status AddRecord(const Slice &slice);
      ...
    };
    

    外部创建一个WritableFile,通过构造函数传递给Writer。AddRecord按上述的结构完善record并添加到日志文件中。

    Status Writer::AddRecord(const Slice& slice) {
               const char* ptr = slice.data();
               size_t left = slice.size();
    
               // Fragment the record if necessary and emit it.  Note that if slice
               // is empty, we still want to iterate once to emit a single
               // zero-length record
               Status s;
               bool begin = true;
               do {
                   //1. 当前块剩余大小
                   const int leftover = kBlockSize - block_offset_;    
                   assert(leftover >= 0);
                   //2. 剩余大小不足,占位
                   if (leftover < kHeaderSize)                        
                   {
                       // Switch to a new block
                       if (leftover > 0) 
                       {
                           // Fill the trailer (literal below relies on kHeaderSize being 7)
                           assert(kHeaderSize == 7);
                           dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
                       }
                       block_offset_ = 0;
                   }
    
                   // Invariant: we never leave < kHeaderSize bytes in a block.
                   assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
    
                   const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
                   //3. 当前块存储的空间大小
                   const size_t fragment_length = (left < avail) ? left : avail;    
    
                   //4. Record Type
                   RecordType type;                                                
                   const bool end = (left == fragment_length);                        
                   if (begin && end) {
                       type = kFullType;
                   }
                   else if (begin) {
                       type = kFirstType;
                   }
                   else if (end) {
                       type = kLastType;
                   }
                   else {
                       type = kMiddleType;
                   }
                   //5. 写入文件
                   s = EmitPhysicalRecord(type, ptr, fragment_length);            
                   ptr += fragment_length;
                   left -= fragment_length;
                   begin = false;
               } while (s.ok() && left > 0);
               return s;
           }
    
    • 当前Block剩余大小不足以填充Record Header时,以"\x00\x00\x00\x00\x00\x00"占位。
    • 当Block无法完整记录一条Record时,通过type信息标识该record在当前block中的区块信息,以便读取时可根据type拼接出完整的record。
    • EmitPhysicalRecord向Block中插入Record数据,每条记录append之后会执行一次flush。

    8.3 创建日志的时机

    在LevelDB中,日志文件和memtable是配对的,在任何数据写入Memtable之前都会先写入日志文件。除此之外,日志文件别无它用。

    因此,日志文件的创建时和Memtable的创建时机也必然一致,这点对于我们理解日志文件至关重要。那么,Memtable在何时会创建呢?

    8.3.1 数据库启动

    如果我们创建了一个新数据库,或者数据库上次运行的所有日志都已经归档到Level0状态。此时,需要为本次数据库进程创建新的Memtable以及日志文件,代码逻辑如下:

    Status DB::Open(const Options &options, const std::string &dbname,
                    DB **dbptr)
    {
      *dbptr = NULL;
    
      //创建新的数据库实例
      DBImpl *impl = new DBImpl(options, dbname);
      impl->mutex_.Lock();
      VersionEdit edit;
    
      //恢复到上一次关闭时的状态
      // Recover handles create_if_missing, error_if_exists
      bool save_manifest = false;
      Status s = impl->Recover(&edit, &save_manifest);
      if (s.ok() && impl->mem_ == NULL)
      {
        //创建新的memtable及日志文件
        // Create new log and a corresponding memtable.
        
        //分配日志文件编号及创建日志文件
        uint64_t new_log_number = impl->versions_->NewFileNumber();
        WritableFile *lfile;
        s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
                                         &lfile);
        if (s.ok())
        {
          edit.SetLogNumber(new_log_number);
          impl->logfile_ = lfile;
          impl->logfile_number_ = new_log_number;
          //文件交由log::Writer做追加操作
          impl->log_ = new log::Writer(lfile);  
          //创建MemTable
          impl->mem_ = new MemTable(impl->internal_comparator_);
          impl->mem_->Ref();
        }
      }
      ......
    }
    

    创建日志文件前,需要先给日志文件起一个名字,此处使用日志编号及数据库名称拼接而成,例如:

    数据库名称为AiDb,编号为324时,日志文件名称为AiDb000324.log

    8.3.2 插入数据

    如果插入数据时,当前的memtable容量达到设定的options_.write_buffer_size,此时触发新的memtable创建,并将之前的memtable转为imm,同时构建新的日志文件。

          uint64_t new_log_number = versions_->NewFileNumber();
          WritableFile *lfile = NULL;
          s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
          if (!s.ok())
          {
            // Avoid chewing through file number space in a tight loop.
            versions_->ReuseFileNumber(new_log_number);
            break;
          }
          delete log_;
          delete logfile_;
          logfile_ = lfile;
          logfile_number_ = new_log_number;
    
          //创建日志文件
          log_ = new log::Writer(lfile);
          imm_ = mem_;
          has_imm_.Release_Store(imm_);
    
          //创建memtable
          mem_ = new MemTable(internal_comparator_);
          mem_->Ref();
          force = false; // Do not force another compaction if have room
          MaybeScheduleCompaction();
    

    8.3.3 数据库恢复

    数据库启动时首先完成数据库状态恢复,日志恢复过程中,如果为最后一个日志文件,且配置为日志重用模式(options_.reuse_logs=true)时,创建新的日志文件。但和其他场景不同的是,这里的日志文件是“继承性”的,也就是说部分内容是上次遗留下来的。来看实现:

      // See if we should keep reusing the last log file.
      if (status.ok() && options_.reuse_logs && last_log && compactions == 0)
      {
        assert(logfile_ == NULL);
        assert(log_ == NULL);
        assert(mem_ == NULL);
        uint64_t lfile_size;
        if (env_->GetFileSize(fname, &lfile_size).ok() &&
            env_->NewAppendableFile(fname, &logfile_).ok())
        {
          Log(options_.info_log, "Reusing old log %s \n", fname.c_str());
          log_ = new log::Writer(logfile_, lfile_size);
          logfile_number_ = log_number;
          if (mem != NULL)
          {
            mem_ = mem;
            mem = NULL;
          }
          else
          {
            // mem can be NULL if lognum exists but was empty.
            mem_ = new MemTable(internal_comparator_);
            mem_->Ref();
          }
        }
      }
    

    8.5 总结

    日志文件本身的定位是清晰的,实现也不复杂。原本Current、Manifest与Log打算一起备注,但要搞清楚Manifest,LevelDB的版本机制必定要搞清楚,而这本身又是很丰富的内容。


    转载请注明;【随安居士】http://www.jianshu.com/p/d1bb2e2ceb4c

    相关文章

      网友评论

        本文标题:8. LevelDB源码剖析之日志文件

        本文链接:https://www.haomeiwen.com/subject/ezhtkxtx.html