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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容