LevelDB原理探究與代碼分析

1. 概述

Level DB(http://code.google.com/p/leveldb/)是google開源的Key/Value存儲系統,它的committer陣容相當強大,基本上是bigtable的原班人馬,包括像jeff dean這樣的大牛,它的代碼合設計非常具有借鑑意義,是一種典型的LSM Tree的KV引擎的實現,從它的數據結構來看,基本就是sstable的開源實現,而且針對各種平臺作了port,目前被用在chrome等項目中。


2. LSM Tree

Level DB是典型的Log-Structured-Merge Tree的實現,它通過延遲寫入以及Write Log Ahead技術來加速數據的寫入並保障數據的安全。LevelDB的每個數據文件(sstable)中的記錄都是按照Key的順序進行排序的,但是隨機寫入時,key的到來是無序的,因此難以將記錄插入到其排序位置。於是需要它採取一種延遲寫入的方式,批量攢集一定量的數據,將它們在內存中排好序,一次性寫入到磁盤中。但是這期間一旦系統斷電或其他異常,則可能導致數據丟失,因此需要將數據先寫入到log的文件中,這樣便將隨機寫轉化爲追加寫入,對於磁盤性能會有很大提升,如果進程發生中斷,重啓後可以根據log恢復之前寫入的數據。

2.1 Write Batch

Level DB只支持兩種更新操作:
1. 插入一條記錄 
2. 刪除一條記錄
代碼如下:
std::string key1,key2,value;  
leveldb::Status s;
s = db->Put(leveldb::WriteOptions(), key1, value);  
s = db->Delete(leveldb::WriteOptions(), key2);  
同時還支持以一種批量的方式寫入數據:
std::string key1,key2,value; 
leveldb::WriteBatch batch; 
batch.Delete(key1); 
batch.Put(key2, value); 
leveldb::status s = db->Write(leveldb::WriteOptions(), &batch);


其實,在Level DB內部,單獨更新與批量更新的調用的接口是相同的,單獨更新也會被組織成爲包含一條記錄的Batch,然後寫入數據庫中。Write Batch的組織形式如下:

2.1 Log Format

每次更新操作都被組織成這樣一個數據包,並作爲一條日誌寫入到log文件中,同時也會被解析爲一條條內存記錄,按照key排序後插入到內存表中的相應位置。LevelDB使用Memory Mapping的方式對log數據進行訪問:如果前一次映射的空間已寫滿,則先將文件擴展一定的長度(每次擴展的長度按64KB,128KB,...的順序逐次翻倍,最大到1MB),然後映射到內存,對映射的內存再以32KB的Page進行切分,每次寫入的日誌填充到Page中,攢積一定量後Sync到磁盤上(也可以設置WriteOptions,每寫一條日誌就Sync一次,但是這樣效率很低),內存映射文件的代碼如下:
class PosixMmapFile : public WritableFile 
{
private:
  std::string filename_;  // 文件名稱
  int fd_;                // 文件句柄
  size_t page_size_;      // 
  size_t map_size_;       // 內存映射的區域大小
  char* base_;            // 內存映射區域的起始地址
  char* limit_;           // 內存映射區域的結束地址
  char* dst_;             // 最後一次佔用的內存的結束地址
  char* last_sync_;       // 最後一次同步到磁盤的結束地址
  uint64_t file_offset_;  // 當前文件的偏移值
  bool pending_sync_;     // 延遲同步的標誌
 
public:
  PosixMmapFile(const std::string& fname, int fd, size_t page_size)
      : filename_(fname),
        fd_(fd),
        page_size_(page_size),
        map_size_(Roundup(65536, page_size)),
        base_(NULL),
        limit_(NULL),
        dst_(NULL),
        last_sync_(NULL),
        file_offset_(0),
        pending_sync_(false) {
    assert((page_size & (page_size - 1)) == 0);
  }

  ~PosixMmapFile() {
    if (fd_ >= 0) {
      PosixMmapFile::Close();
    }
  }

  Status Append(const Slice& data) {
    const char* src = data.data();
    size_t left = data.size();
    while (left > 0) {
      // 計算上次最後一次申請的區域的剩餘容量,如果已完全耗盡,
      // 則卸載當前區域,申請一個新的區域
      size_t avail = limit_ - dst_;
      if (avail == 0) {
        if (!UnmapCurrentRegion() ||
            !MapNewRegion()) {
          return IOError(filename_, errno);
        }
      }
      // 填充當前區域的剩餘容量
      size_t n = (left <= avail) ? left : avail;
      memcpy(dst_, src, n);
      dst_ += n;
      src += n;
      left -= n;
    }
    return Status::OK();
  }

  Status PosixMmapFile::Close() {
    Status s;
    size_t unused = limit_ - dst_;
    if (!UnmapCurrentRegion()) {
      s = IOError(filename_, errno);
    } else if (unused > 0) {
      // 關閉時將文件沒有使用用的空間truncate掉
      if (ftruncate(fd_, file_offset_ - unused) < 0) {
        s = IOError(filename_, errno);
      }
    }

    if (close(fd_) < 0) {
      if (s.ok()) {
        s = IOError(filename_, errno);
      }
    }

    fd_ = -1;
    base_ = NULL;
    limit_ = NULL;
    return s;
  }
  
  virtual Status Sync() {
    Status s;
    if (pending_sync_) {
      // 上個區域也有數據未同步,則先同步數據
      pending_sync_ = false;
      if (fdatasync(fd_) < 0) {
        s = IOError(filename_, errno);
      }
    }

    if (dst_ > last_sync_) {
      // 計算未同步數據的起始與結束地址,同步時,起始地址按page_size_向下取整,
      // 結束地址向上取整,保證每次同步都是同步一個或多個page
      size_t p1 = TruncateToPageBoundary(last_sync_ - base_);
      size_t p2 = TruncateToPageBoundary(dst_ - base_ - 1); 
      // 如果剛好爲整數個page_size_,由於下面同步時必然會加一個page_size_,所以這裏可以減去1
      last_sync_ = dst_;
      if (msync(base_ + p1, p2 - p1 + page_size_, MS_SYNC) < 0) {
        s = IOError(filename_, errno);
      }
    }
    return s;
  }
private:
  // 將x按y向上對齊   
  static size_t Roundup(size_t x, size_t y) {
    return ((x + y - 1) / y) * y;
  }
  // 將s按page_size_向下對齊
  size_t TruncateToPageBoundary(size_t s) {
    s -= (s & (page_size_ - 1));
    assert((s % page_size_) == 0);
    return s;
  } 

  // 卸載當前映射的內存區域  
  bool UnmapCurrentRegion() {    
    bool result = true;
    if (base_ != NULL) {
      if (last_sync_ < limit_) {
        // 如果當前頁沒有完全被同步,則標明本文件需要被同步,下次調用Sync()方法時會將本頁中未同步的數據同步到磁盤
        pending_sync_ = true;
      }
      if (munmap(base_, limit_ - base_) != 0) {
        result = false;
      }
      file_offset_ += limit_ - base_;
      base_ = NULL;
      limit_ = NULL;
      last_sync_ = NULL;
      dst_ = NULL;      // 使用翻倍的策略增加下次申請區域的大小,最大到1MB
      if (map_size_ < (1<<20)) {
        map_size_ *= 2;
      }
    }
    return result;
  }  

  bool MapNewRegion() {
    assert(base_ == NULL); // 申請一個新的區域時,上一個申請的區域必須已經卸載 
    // 先將文件擴大    
    if (ftruncate(fd_, file_offset_ + map_size_) < 0) {
      return false;
    }
    // 將新區域映射到文件
    void* ptr = mmap(NULL, map_size_, PROT_READ | PROT_WRITE, MAP_SHARED,
                     fd_, file_offset_);
    if (ptr == MAP_FAILED) {
      return false;
    }
    base_ = reinterpret_cast<char*>(ptr);
    limit_ = base_ + map_size_;
    dst_ = base_;
    last_sync_ = base_;
    return true;
  }
};
但是,一個Batch的數據按上面的方式組織後,如果做爲一條日誌寫入Log,則很可能需要跨兩個或更多個Page;爲了更好地管理日誌以及保障數據安全,LevelDB對日誌記錄進行了更細的切分,如果一個Batch對應的數據需要跨頁,則會將其切分爲多條Entry,然後寫入到不同Page中,Entry不會跨越Page,我們通過對多個Entry進行解包,可以還原出的Batch數據。最終,LevelDB的log文件被組織爲下面的形式:


這裏,我們可以看一下log_writer的代碼:
Status Writer::AddRecord(const Slice& slice) {
  const char* ptr = slice.data();
  size_t left = slice.size();

  Status s;
  bool begin = true;
  do {
    const int leftover = kBlockSize - block_offset_;
    assert(leftover >= 0);
    if (leftover < kHeaderSize) {
      // 如果當前page的剩餘長度小於7字節且大於0,則都填充'\0',並新起一個page
      if (leftover > 0) {
        assert(kHeaderSize == 7);
        dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
      }
      block_offset_ = 0;
    }
  
    // 計算page能否容納整體日誌,如果不能,則將日誌切分爲多條entry,插入不同的page中,type中註明該entry是日誌的開頭部分,中間部分還是結尾部分。
    const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
    const size_t fragment_length = (left < avail) ? left : avail;

    RecordType type;
    const bool end = (left == fragment_length);
    if (begin && end) {
      type = kFullType;   // 本Entry保存完整的Batch
    } else if (begin) {
      type = kFirstType;  // 本Entry只保存起始部分
    } else if (end) {
      type = kLastType;   // 本Entry只保存結束部分
    } else {
      type = kMiddleType; // 本Entry保存Batch的中間部分,不含起始與結尾,有時可能需要保存多個middle
    }

    s = EmitPhysicalRecord(type, ptr, fragment_length);
    ptr += fragment_length;
    left -= fragment_length;
    begin = false;
  } while (s.ok() && left > 0);
  return s;
}

Status Writer::EmitPhysicalRecord(RecordType t, const char* ptr, size_t n) {
  assert(n <= 0xffff);  
  assert(block_offset_ + kHeaderSize + n <= kBlockSize);

  // 填充記錄頭
  char buf[kHeaderSize];
  buf[4] = static_cast<char>(n & 0xff);
  buf[5] = static_cast<char>(n >> 8);
  buf[6] = static_cast<char>(t);

  // 計算crc
  uint32_t crc = crc32c::Extend(type_crc_[t], ptr, n);
  crc = crc32c::Mask(crc); 
  EncodeFixed32(buf, crc);

  // 填充entry內容
  Status s = dest_->Append(Slice(buf, kHeaderSize));
  if (s.ok()) {
    s = dest_->Append(Slice(ptr, n));
    if (s.ok()) {
      s = dest_->Flush();
    }
  }
  block_offset_ += kHeaderSize + n;
  return s;
}

2.3 Write Log Ahead

Level DB在更新時,先寫log,然後更新memtable,每個memtable會設置一個最大容量,如果超過閾值,則採用雙buffer機制,關閉當前log文件並將當前memtable切換未從memtable,然後新建一個log文件以及memtable,將數據寫進新的log文件與memtable,並通知後臺線程對從memtable進行處理,及時將其dump到磁盤上,或者啓動compaction流程。Write的代碼分析如下:
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) 
{
  Status status;
  MutexLock l(&mutex_);  // 鎖定互斥體,同一時間只能有一個線程更新數據
  LoggerId self;   
  // 獲取Logger的使用權,如果有其他線程擁有所有權,則等待至其釋放所有權。
  AcquireLoggingResponsibility(&self);
  status = MakeRoomForWrite(false);  // May temporarily release lock and wait
  uint64_t last_sequence = versions_->LastSequence();  // 獲取當前的版本號
  if (status.ok()) {
    // 將當前版本號加1後作爲本次更新的日誌的版本,
    // 一次批量更新可能包含多個操作,這些操作都用一個版本有一個好處:
    // 本次更新的所有操作,要麼都可見,要麼都不可見,不存在一部分可見,另一部分不可見的情況。
    WriteBatchInternal::SetSequence(updates, last_sequence + 1);
    // 但是本次更新可能有多個操作,跳過與操作數相等的版本號,保證不被使用
    last_sequence += WriteBatchInternal::Count(updates);

    // 將batch寫入log,然後應用到memtable中
    {
      assert(logger_ == &self);
      mutex_.Unlock();
      // 這裏,可以解鎖,因爲在AcquireLoggingResponsibility()方法中已經獲取了Logger的擁有權,
      // 其他線程即使獲得了鎖,但是由於&self != logger,其會阻塞在AcquireLoggingResponsibility()方法中。
      // 將更新寫入log文件,如果設置了每次寫入進行sync,則將其同步到磁盤,這個操作可能比較長,
      // 防止了mutex_對象長期被佔用,因爲其還負責其他一些資源的同步
      status = log_->AddRecord(WriteBatchInternal::Contents(updates));
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
      }
      if (status.ok()) {
        // 成功寫入了log後,才寫入memtable
        status = WriteBatchInternal::InsertInto(updates, mem_);
      }
      // 重新鎖定mutex_
      mutex_.Lock();
      assert(logger_ == &self);
    }
    // 更新版本號
    versions_->SetLastSequence(last_sequence);
  }
  // 釋放對logger的所有權,並通知等待的線程,然後解鎖
  ReleaseLoggingResponsibility(&self);
  return status;
}

// force參數表示強制新起一個memtable
Status DBImpl::MakeRoomForWrite(bool force) {
  mutex_.AssertHeld();
  assert(logger_ != NULL);
  bool allow_delay = !force;
  Status s;
  while (true) {
    if (!bg_error_.ok()) {
      // 後臺線程存在問題,則返回錯誤,不接受更新
      s = bg_error_;
      break;
    } else if (
        allow_delay &&
        versions_->NumLevelFiles(0) >= config::kL0_SlowdownWritesTrigger) {
      // 如果不是強制寫入,而且level 0的sstable超過8個,則本次更新阻塞1毫秒,
      // leveldb將sstable分爲多個等級,其中level 0中的不同表的key是可能重疊的,
      // 如果l0的sstable過多,會導致查詢性能下降,這時需要適當降低更新速度,讓
      // 後臺線程進行compaction操作,但是設計者不希望讓某次寫操作等待數秒,
      // 而是讓每次更新操作分擔延遲,即每次寫操作阻塞1毫秒,平衡讀寫速率;
      // 另外,理論上這也能讓compaction線程獲得更多的cpu時間(當然,
      // 這是假定compaction與更新操作共享一個CPU時纔有意義)
      mutex_.Unlock();
      env_->SleepForMicroseconds(1000);
      allow_delay = false;  // 最多延遲一次,下次不延遲
      mutex_.Lock();
    } else if (!force &&
               (mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
      // 如果當前memtable已使用的空間小於write_buffer_size,則跳出,更新到當前memtable即可。
      // 當force爲true時,第一次循環會走後面else邏輯,切換了memtable後force被置爲false,
      // 第二次循環時就可以在此跳出了
      break;
    } else if (imm_ != NULL) {
      // 如果當前memtable已經超過write_buffer_size,且備用的memtable也在被使用,則阻塞更新並等待
      bg_cv_.Wait();
    } else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
      // 如果當前memtable已使用的空間小於write_buffer_size,但是備用的memtable未被使用,
      // 則檢查level 0的sstable個數,如超過12個,則阻塞更新並等待
      Log(options_.info_log, "waiting...\n");
      bg_cv_.Wait();
    } else {
      // 否則,使用新的id新創建一個log文件,並將當前memtable切換爲備用的memtable,新建一個
      // memtable,然後將數據寫入當前的新memtable,即切換log文件與memtable,並告訴後臺線程
      // 可以進行compaction操作了
      assert(versions_->PrevLogNumber() == 0);
      uint64_t new_log_number = versions_->NewFileNumber();
      WritableFile* lfile = NULL;
      s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
      if (!s.ok()) {
        break;
      }
      delete log_;
      delete logfile_;
      logfile_ = lfile;
      logfile_number_ = new_log_number;
      log_ = new log::Writer(lfile);
      imm_ = mem_;
      has_imm_.Release_Store(imm_);
      mem_ = new MemTable(internal_comparator_);
      mem_->Ref();
      force = false;   // 下次判斷可以不新建memtable了
      MaybeScheduleCompaction();
    }
  }
  return s;
}
void DBImpl::AcquireLoggingResponsibility(LoggerId* self) {
  while (logger_ != NULL) {
    logger_cv_.Wait();
  }
  logger_ = self;
}

void DBImpl::ReleaseLoggingResponsibility(LoggerId* self) {
  assert(logger_ == self);
  logger_ = NULL;
  logger_cv_.SignalAll();
}

2.4 Skip List

Level DB內部採用跳錶結構來組織Memtable,每插入一條記錄,先根據跳錶通過多次key的比較,定位到記錄應該插入的位置,然後按照一定的概率確定該節點需要建立多少級的索引,跳錶結構如下:


Level DB的SkipList最高12層,最下面一層(level0)的鏈是全鏈,即每條記錄必須在此鏈中插入相應的索引節點;從level1到level11則是按概率決定是否需要建索引,概率按照1/4的因子等比遞減。下面舉個例子,說明一下這個流程:
1. 看上圖,假定我們鏈不存在record3,level0中,record2的下一條記錄是record4,level1中,record2的下一條記錄是record5。
2. 現在,我們插入一條記錄record3,通過key的比較,我們定位到它應該在record2與record4之間。
3. 然後,我們按照下面的代碼確定一條記錄需要在跳錶中建立幾重索引:

template<typename Key, class Comparator>
int SkipList<Key,Comparator>::RandomHeight() {
  // Increase height with probability 1 in kBranching
  static const unsigned int kBranching = 4;
  int height = 1;
  while (height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)) {
    height++;
  }
  return height;
}

按照上面的代碼,我們可以得出,建立x級索引的概率是0.25 ^(x - 1) * 0.75,所以,建立1級索引的概率爲75%,建立2級索引的概率爲25%*75%=18.75%,...(個人感覺,google把分支因子定爲4有點高了,這樣在絕大多數情況下,跳錶的高度都不大於3)。

4.  在level0 ~ level (x-1)中鏈表的合適位置插入record3,假定根據上面的公式,我們得到需要爲record3建立2級索引,即x=2,因此需要在level0與level1中的鏈中插入record3:在level 0的鏈中,record3插在record2與record4之間,在level 1的鏈中,record3插入在record2與record5之間,形成了現在的索引結構,在查詢一個記錄時,可以從最高一級索引向下查找,節約比較次數。

2.5 Record Format

Level DB將用戶的每個更新或刪除操作組合成一個Record,其格式如下:


從圖中可以看出,每個Record會在原用key的基礎上添加版本號以及key的類型(更新 or 刪除),組成internal key。插入跳錶時,是按照internal key進行排序,而非用戶key。這樣,我們只可能向跳錶中添加節點,而不可能刪除和替換節點。

Internal Key在比較時,按照下面的算法:

int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
  int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
  if (r == 0) {
    // 比較後面8個字節構造的整數,第一個字節的type爲Least Significant Byte
    const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
    const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
    if (anum > bnum)  // 注意:整數大反而key比較小
    {
      r = -1;
    } else if (anum < bnum) {
      r = +1;
    }
  }
  return r;
}

根據上面的算法,我們可以得知Internal Key的比較順序:

1. 如果User Key不相等,則User Key比較小的記錄的Internal Key也比較小,User Key默認採用字典序(lexicographic)進行比較,可以在建表參數中自定義comparator。
2. 如果type也相同,則比較Sequence Num,Sequence Num大的Internal Key比較小。
3. 如果Sequence Num相等,則比較Type,type爲更新(Key Type=1)的記錄比的type爲刪除(Key Type=0)的記錄的Internal Key小。

在插入到跳錶時,一般不會出現Internal Key相等的情況(除非在一個Batch中操作了同一條記錄兩次,這裏會出現一種bug:在一個Write Batch中,先插入一條記錄,然後刪除這條記錄,最後把這個Batch寫入DB,會發現DB中這條記錄存在。因此,不推薦在Batch中多次操作相同key的記錄),User Key相同的記錄插入跳錶時,Sequence Num大的記錄會排在前面。
設計Internal Key有個以下一些作用:
1. Level DB支持快照查詢,即查詢時指定快照的版本號,查詢出創建快照時某個User Key對應的Value,那麼可以組成這樣一個Internal Key:Sequence=快照版本號,Type=1,User Key爲用戶指定Key,然後查詢數據文件與內存,找到大於等於此Internal Key且User Key匹配的第一條記錄即可(即Sequence Num小於等於快照版本號的第一條記錄)。
2.如果查詢最新的記錄時,將Sequence Num設置爲0xFFFFFFFFFFFFFF即可。因爲我們更多的是查詢最新記錄,所以讓Sequence Num大的記錄排前面,可以在遍歷時遇見第一條匹配的記錄立即返回,減少往後遍歷的次數。


3.文件結構

3.1 文件組成

Level DB包含一下幾種文件:

文件類型 說明
dbname/MANIFEST-[0-9]+   清單文件            
dbname/[0-9]+.log db日誌文件
dbname/[0-9]+.sst dbtable文件
dbname/[0-9]+.dbtmp db臨時文件
dbname/CURRENT  記錄當前使用的清單文件名
dbname/LOCK   DB鎖文件
dbname/LOG info log日誌文件
dbname/LOG.old 舊的info log日誌文件

上面的log文件,sst文件,臨時文件,清單文件末尾都帶着序列號,序號是單調遞增的(隨着next_file_number從1開始遞增),以保證不會和之前的文件名重複。另外,注意區分db log與info log:前者是爲了防止保障數據安全而實現的二進制Log,後者是打印引擎中間運行狀態及警告等信息的文本log。
隨着更新與Compaction的進行,Level DB會不斷生成新文件,有時還會刪除老文件,所以需要一個文件來記錄文件列表,這個列表就是清單文件的作用,清單會不斷變化,DB需要知道最新的清單文件,必須將清單準備好後原子切換,這就是CURRENT文件的作用,Level DB的清單過程更新如下:
1. 遞增清單序號,生成一個新的清單文件。
2. 將此清單文件的名稱寫入到一個臨時文件中。
3. 將臨時文件rename爲CURRENT。
代碼如下:
Status SetCurrentFile(Env* env, const std::string& dbname,
                      uint64_t descriptor_number) {
  // 創建一個新的清單文件名
  std::string manifest = DescriptorFileName(dbname, descriptor_number);
  Slice contents = manifest;
  // 移除"dbname/"前綴
  assert(contents.starts_with(dbname + "/"));
  contents.remove_prefix(dbname.size() + 1);
  // 創建一個臨時文件
  std::string tmp = TempFileName(dbname, descriptor_number);
  // 寫入清單文件名
  Status s = WriteStringToFile(env, contents.ToString() + "\n", tmp);
  if (s.ok()) {
    // 將臨時文件改名爲CURRENT
    s = env->RenameFile(tmp, CurrentFileName(dbname));
  }
  if (!s.ok()) {
    env->DeleteFile(tmp);
  }
  return s;
}

3.2 Manifest

在介紹其他文件格式前,先了解清單文件,MANIFEST文件是Level DB的元信息文件,它主要包括下面一些信息:
1. Comparator的名稱
2. 
其的格式如下:

我們可以看看其序列化的代碼:
void VersionEdit::EncodeTo(std::string* dst) const {
  if (has_comparator_) {  // 記錄Comparator名稱
    PutVarint32(dst, kComdparator);
    PutLengthPrefixedSlice(dst, comparator_);
  }
  if (has_log_number_) {  // 記錄Log Numer
    PutVarint32(dst, kLogNumber);
    PutVarint64(dst, log_number_);
  }
  if (has_prev_log_number_) {  // 記錄Prev Log Number,現在已廢棄,一般爲0
    PutVarint32(dst, kPrevLogNumber);
    PutVarint64(dst, prev_log_number_);
  }
  if (has_next_file_number_) {  // 記錄下一個文件序號
    PutVarint32(dst, kNextFileNumber);
    PutVarint64(dst, next_file_number_);
  }
  if (has_last_sequence_) {  // 記錄最大的sequence num
    PutVarint32(dst, kLastSequence);
    PutVarint64(dst, last_sequence_);
  }
  // 記錄每一級Level下次compaction的起始Key
  for (size_t i = 0; i < compact_pointers_.size(); i++) {
    PutVarint32(dst, kCompactPointer);
    PutVarint32(dst, compact_pointers_[i].first);  // level
    PutLengthPrefixedSlice(dst, compact_pointers_[i].second.Encode());
  }
  // 記錄每一級需要刪除的文件
  for (DeletedFileSet::const_iterator iter = deleted_files_.begin();
       iter != deleted_files_.end();
       ++iter) {
    PutVarint32(dst, kDeletedFile);
    PutVarint32(dst, iter->first);   // level
    PutVarint64(dst, iter->second);  // file number
  }
  // 記錄每一級需要有效的sst以及其smallest與largest的key
  for (size_t i = 0; i < new_files_.size(); i++) {
    const FileMetaData& f = new_files_[i].second;
    PutVarint32(dst, kNewFile);
    PutVarint32(dst, new_files_[i].first);  // level
    PutVarint64(dst, f.number);
    PutVarint64(dst, f.file_size);
    PutLengthPrefixedSlice(dst, f.smallest.Encode());
    PutLengthPrefixedSlice(dst, f.largest.Encode());
  }
}

3.3 Sortedtable

Level DB間歇性地將內存中的SkipList對應的數據集合Dump到磁盤上,生成一個sst的文件,這個文件的格式如下:

按照SSTable的結構,可以正向遍歷,也可以逆向遍歷,但是逆向遍歷的代價要遠遠高於正向遍歷的代價,因爲每條record都是變長的,且其沒有記錄前一條記錄的偏移,因此逆向Group遍歷時,只能先回到group(代碼中稱爲一個restart,爲了便於理解,下面都稱爲group)開頭(一個Data Block的group一般爲16條記錄,每個Data Block的尾部有group起始位置偏移索引),然後從頭開始正向遍歷,直至找到其前一條記錄,如果當前位置爲group的第一條記錄,則需要回到上一個group的開頭,遍歷到其最後一條記錄。另外,內存中跳錶反向的遍歷效率也遠遠不如正向遍歷。

3.4 Sparse Index

一個sst文件內部除了Data Block,還有Index Block,Index Block的結構與Data Block一樣,只不過每個group只包含一條記錄,即Data Block的最大Key與偏移。其實這裏說最大Key並不是很準確,理論上,只要保存最大Key就可以實現二分查找,但是Level DB在這裏做了個優化,它並保存最大key,而是保存一個能分隔兩個Data Block的最短Key,如:假定Data Block1的最後一個Key爲“abcdefg”,Data Block2的第一個Key爲“abzxcv”,則index可以記錄Data Block1的索引key爲“abd”;這樣的分割串可以有很多,只要保證Data Block1中的所有Key都小於等於此索引,Data Block2中的所有Key都大於此索引即可。這種優化縮減了索引長度,查詢時可以有效減小比較次數。我們可以看看默認comparator如何實現這種分割的:
void BytewiseComparatorImpl::FindShortestSeparator(
      std::string* start,
      const Slice& limit) const {
    // 先比較獲得最大公共前綴
    size_t min_length = std::min(start->size(), limit.size());
    size_t diff_index = 0;
    while ((diff_index < min_length) &&
           ((*start)[diff_index] == limit[diff_index])) {
      diff_index++;
    }
    if (diff_index >= min_length) {
      // 如果start就是limit的前綴,則只能使用start本身作爲分割
    } else {
      uint8_t diff_byte = static_cast<uint8_t>((*start)[diff_index]);
      // 將第一個不同字符+1,並確保其不會溢出,同時比limit小
      if (diff_byte < static_cast<uint8_t>(0xff) &&
          diff_byte + 1 < static_cast<uint8_t>(limit[diff_index])) {
        (*start)[diff_index]++;
        start->resize(diff_index + 1);
        assert(Compare(*start, limit) < 0);
      }
    }
  }
從上面可以看出,FindShortestSeparator方法並不嚴格,有些時候沒有找出最短分割的key(比如第一個不等的字符已經爲0xFF時),它只是一種優化,我們自定義Comparator時,既可以實現,也可以不實現,如果不實現,將始終使用Data Block的最大Key作爲索引,並不影響功能正確性。

4. Operations

在介紹了數據結構後,我們看看Level DB一些基本操作的實現:

4.1 創建一個新表

創建一個新的表大概分爲幾步,包括建立各類文件以及內存中的數據結構,線程同步對象等,關鍵代碼如下:

// DBImpl在構造時會初始化互斥體與信號量,創建一個空的memtable,並根據配置設置Comparator及LRU緩衝
DBImpl::DBImpl(const Options& options, const std::string& dbname)
    : env_(options.env),
      internal_comparator_(options.comparator), // 初始化Comparator
      options_(SanitizeOptions(dbname, &internal_comparator_, options)),  // 檢查參數是否合法
      owns_info_log_(options_.info_log != options.info_log),  // 是擁有自己info log,還是使用用戶提供的
      owns_cache_(options_.block_cache != options.block_cache), // 是否擁有自己的LRU緩衝,或者使用用戶提供的
      dbname_(dbname),  // 數據表名稱
      db_lock_(NULL),  // 不創建也不鎖定文件鎖
      shutting_down_(NULL), 
      bg_cv_(&mutex_),  // 用於與後臺線程交互的條件信號
      mem_(new MemTable(internal_comparator_)), // 創建一個新的跳錶
      imm_(NULL),  // 用於雙緩衝的緩衝跳錶開始時爲NULL
      logfile_(NULL),  // log文件
      logfile_number_(0), // log文件的序號
      log_(NULL),  // log writer
      logger_(NULL),  // 用於在多線程環境中記錄Owner logger的一個指針
      logger_cv_(&mutex_), // 用於與Logger交互的條件信號
      bg_compaction_scheduled_(false), // 沒打開表時不起動後臺的compaction線程
      manual_compaction_(NULL) {
  // 增加memtable的引用計數
  mem_->Ref();
  has_imm_.Release_Store(NULL);

  // 根據Option創建一個LRU的緩衝對象,如果options中指定了Cache空間,則使用用戶
  // 提供的Cache空間,否則會在內部確實創建8MB的Cache,另外,LRU的Entry數目不能超過max_open_files-10
  const int table_cache_size = options.max_open_files - 10;
  table_cache_ = new TableCache(dbname_, &options_, table_cache_size);

  // 創建一個Version管理器
  versions_ = new VersionSet(dbname_, &options_, table_cache_,
                             &internal_comparator_);
}

Options SanitizeOptions(const std::string& dbname,
                        const InternalKeyComparator* icmp,
                        const Options& src) {
  Options result = src;
  result.comparator = icmp;
  ClipToRange(&result.max_open_files,           20,     50000);
  ClipToRange(&result.write_buffer_size,        64<<10, 1<<30);
  ClipToRange(&result.block_size,               1<<10,  4<<20);
  // 如果用戶未指定info log文件(用於打印狀態等文本信息的日誌文件),則由引擎自己創建一個info log文件。
  if (result.info_log == NULL) {
    // Open a log file in the same directory as the db
    src.env->CreateDir(dbname);  // 如果目錄不存在則創建
    // 如果已存在以前的info log文件,則將其改名爲LOG.old,然後創建新的log文件與日誌的writer
    src.env->RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname));
    Status s = src.env->NewLogger(InfoLogFileName(dbname), &result.info_log);
    if (!s.ok()) {
      result.info_log = NULL;
    }
  }
  // 如果用戶沒指定LRU緩衝,則創建8MB的LRU緩衝
  if (result.block_cache == NULL) {
    result.block_cache = NewLRUCache(8 << 20);
  }
  return result;
}

Status DBImpl::NewDB() {
  // 創建version管理器
  VersionEdit new_db;
  // 設置Comparator
  new_db.SetComparatorName(user_comparator()->Name());
  new_db.SetLogNumber(0);
  // 下一個序號從2開始,1留給清單文件
  new_db.SetNextFile(2);
  new_db.SetLastSequence(0);
  // 創建一個清單文件,MANIFEST-1
  const std::string manifest = DescriptorFileName(dbname_, 1);
  WritableFile* file;
  Status s = env_->NewWritableFile(manifest, &file);
  if (!s.ok()) {
    return s;
  }
  {
    // 寫入清單文件頭
    log::Writer log(file);
    std::string record;
    new_db.EncodeTo(&record);
    s = log.AddRecord(record);
    if (s.ok()) {
      s = file->Close();
    }
  }
  delete file;
  if (s.ok()) {
    // 設置CURRENT文件,使其指向清單文件
    s = SetCurrentFile(env_, dbname_, 1);
  } else {
    env_->DeleteFile(manifest);
  }
  return s;

4.2 打開一個已存在的表

上面的步驟中,其實還遺漏了一個的重要流程,那就是DB的Open方法。Level DB無論是創建表,還是打開現有的表,都是使用Open方法。代碼如下:
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;
  // 如果存在表數據,則Load表數據,並對日誌進行恢復,否則,創建新表
  Status s = impl->Recover(&edit);
  if (s.ok()) {
    // 從VersionEdit獲取一個新的文件序號,所以如果是新建數據表,則第一個LOG的序號爲2(1已經被MANIFEST佔用)
    uint64_t new_log_number = impl->versions_->NewFileNumber();
    // 記錄日誌文件號,創建新的log文件及Writer對象
    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;
      impl->log_ = new log::Writer(lfile);
      // 如果存在原來的log,則回放log
      s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
    }
    if (s.ok()) {
      // 刪除廢棄的文件(如果存在)
      impl->DeleteObsoleteFiles();
      // 檢查是否需要Compaction,如果需要,則讓後臺啓動Compaction線程
      impl->MaybeScheduleCompaction();
    }
  }
  impl->mutex_.Unlock();
  if (s.ok()) {
    *dbptr = impl;
  } else {
    delete impl;
  }
  return s;
}
從上面可以看出,其實到底是新建表還是打開表都是取決與DBImpl::Recover()這個方法的行爲,它的流程如下:
Status DBImpl::Recover(VersionEdit* edit) {
  mutex_.AssertHeld();

  // 創建DB目錄,不關注錯誤
  env_->CreateDir(dbname_);
  // 在DB目錄下打開或創建(如果不存在)LOCK文件並鎖定它,防止其他進程打開此表
  Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);
  if (!s.ok()) {
    return s;
  }
  
  if (!env_->FileExists(CurrentFileName(dbname_))) {
    // 如果DB目錄下不存在CURRENT文件且允許在表不存在時創建表,則新建一個表返回
    if (options_.create_if_missing) {
      s = NewDB();
      if (!s.ok()) {
        return s;
      }
    } else {
      return Status::InvalidArgument(
          dbname_, "does not exist (create_if_missing is false)");
    }
  } else {
    if (options_.error_if_exists) {
      return Status::InvalidArgument(
          dbname_, "exists (error_if_exists is true)");
    }
  }
  // 如果運行到此,表明表已經存在,需要load,第一步是從MANIFEST文件中恢復VersionSet
  s = versions_->Recover();
  if (s.ok()) {
    SequenceNumber max_sequence(0);
    // 獲取MANIFEST中獲取最後一次持久化清單時在使用LOG文件序號,注意:這個LOG當時正在使用,
    // 表明數據還在memtable中,沒有形成sst文件,所以數據恢復需要從這個LOG文件開始(包含這個LOG)。
    // 另外,prev_log是早前版本level_db使用的機制,現在以及不再使用,這裏只是爲了兼容
    const uint64_t min_log = versions_->LogNumber();
    const uint64_t prev_log = versions_->PrevLogNumber();
    // 掃描DB目錄,記錄下所有比MANIFEST中記錄的LOG更加新的LOG文件
    std::vector<std::string> filenames;
    s = env_->GetChildren(dbname_, &filenames);
    if (!s.ok()) {
      return s;
    }
    uint64_t number;
    FileType type;
    std::vector<uint64_t> logs;
    for (size_t i = 0; i < filenames.size(); i++) {
      if (ParseFileName(filenames[i], &number, &type)
          && type == kLogFile
          && ((number >= min_log) || (number == prev_log))) {
        logs.push_back(number);
      }
    }
    // 將LOG文件安裝從小到大排序
    std::sort(logs.begin(), logs.end());
    // 逐個LOG文件回放    for (size_t i = 0; i < logs.size(); i++) {
      // 回放LOG時,記錄被插入到memtable,如果超過write buffer,則還會dump出level 0的sst文件,
      // 此方法會將日誌種每條記錄的sequence num與max_sequence進行比較,以記錄下最大的sequence num。
      s = RecoverLogFile(logs[i], edit, &max_sequence);
      // 更新最大的文件序號,因爲MANIFEST文件中沒有記錄這些LOG文件佔用的序號;
      // 當然,也可能LOG的序號小於MANIFEST中記錄的最大文件序號,這時不需要更新。
      versions_->MarkFileNumberUsed(logs[i]);
    }
    if (s.ok()) {
      // 比較日誌回放前後的最大sequence num,如果回放記錄中有超過LastSequence()的記錄,則替換
      if (versions_->LastSequence() < max_sequence) {
        versions_->SetLastSequence(max_sequence);
      }
    }
  }
  return s;
}	

4.3 關閉一個已打開的表

Level DB設計成只要刪除DB對象就可以關閉表,其關鍵流程如下:
DBImpl::~DBImpl() {
  // 通知後臺線程,DB即將關閉
  mutex_.Lock();
  // 後臺線程會間歇性地檢查shutting_down_對象的指針,一旦不爲NULL就會退出
  shutting_down_.Release_Store(this);
  // 注意:這裏必須循環通知,直至compaction線程獲得信號並設置了bg_compaction_scheduled_爲false  
  while (bg_compaction_scheduled_) {
    bg_cv_.Wait();
  }
  mutex_.Unlock();

  // 如果鎖定了文件鎖,則釋放文件鎖
  if (db_lock_ != NULL) {
    env_->UnlockFile(db_lock_);
  }

  delete versions_;
  // 減去memtable的引用計數
  if (mem_ != NULL) mem_->Unref();
  if (imm_ != NULL) imm_->Unref();
  // 銷燬db log相關對象以及表緩衝對象
  delete log_;
  delete logfile_;
  delete table_cache_;

  // 如果info log與cache是引擎自己構建,則需要銷燬它們
  if (owns_info_log_) {
    delete options_.info_log;
  }
  if (owns_cache_) {
    delete options_.block_cache;
  }
}
由上可見,delete一個db對象可能會阻塞調用線程一段時間,必須讓其完成一些必須完成的工作,才能進一步保障數據的安全。

4.4 隨機查詢

Level DB可能dump多個sst文件,這些文件的key範圍可能重疊。按照Level DB的設計,其會將sst分爲7個等級,可以視爲代齡,其中,只有Level 0中的sst可能存在key的區間重疊的情況,而level1 - level6中,同一level中的sst可以保證不重疊,但不同level之間的sst依然可能key重疊。因此,如果查詢一個key,其最多可能在6+n個sst中同時存在,n爲level0中sst的個數;同時,由於這些文件的生成有先後關係,查詢時還需要注意順序,Get一個key的流程如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章