天天看點

Rocksdb 利用recycle_log_file_num 重用wal-log檔案

​recycle_log_file_num​

​ 複用wal檔案資訊, 優化wal檔案的空間配置設定,減少pagecache中檔案元資訊的更新開銷。

為同僚提供了一組rocksdb寫優化參數之後有一個疑惑的現象被問到,發現之前的一些代碼細節有遺忘情況,同時也發現了這個參數的一些小優化,這裡做個總結。

在參數:

opts.recycle_log_file_num = 10;
opts.max_write_buffer_number = 16;
opts.write_buffer_size = 128 << 20;      

大壓力寫下 出現了多個.log檔案同時存在的現象,想要看看為什麼會有這個現象。

在描述這個現象産生的原因之前我們先看看Rocksdb的wal建立以及清理過程,其中​

​recycle_log_file_num​

​是如何reuse log的

Rocksdb的WAL建立

如果不設定​

​disable_memtable=true​

​​,不設定​

​enable_pipelined_write=1​

​​,不​

​disableWAL=1​

​的話,基本的寫入調用棧如下:

JoinBatchGroup的邏輯這裡暫不提及,從大體的寫入過程中各個檔案的建立過程如下:

DBImpl::PreprocessWrite
  DBImpl::SwitchWAL
    DBImpl::SwitchMemtable
      DBImpl::CreateWAL // 建立wal檔案      

在CreateWal檔案的時候,Rocksdb會為這個wal建立一個​

​PosixEnv​

​下的檔案句柄,以及檔案名,并建立一個檔案writer,用來後續的資料寫入。

如下代碼:

// 其中recycle_log_number 為目前想要複用的wal log檔案名
// new_log 為需要在該函數中建立的writer,最後傳出來
// log_file_num 是建立的新的檔案名,如果可回收的檔案為0,則直接用新的就可以了。
IOStatus DBImpl::CreateWAL(uint64_t log_file_num, uint64_t recycle_log_number,
                           size_t preallocate_block_size,
                           log::Writer** new_log) {
  ......
  // 根據檔案num,建立新的檔案名
  std::string log_fname =
      LogFileName(immutable_db_options_.wal_dir, log_file_num);

  // 如果有可回收的numer,則reuse
  if (recycle_log_number) {
    ROCKS_LOG_INFO(immutable_db_options_.info_log,
                   "reusing log %" PRIu64 " from recycle list\n",
                   recycle_log_number);
    std::string old_log_fname =
        LogFileName(immutable_db_options_.wal_dir, recycle_log_number);
    TEST_SYNC_POINT("DBImpl::CreateWAL:BeforeReuseWritableFile1");
    TEST_SYNC_POINT("DBImpl::CreateWAL:BeforeReuseWritableFile2");
    io_s = fs_->ReuseWritableFile(log_fname, old_log_fname, opt_file_options,
                                  &lfile, /*dbg=*/nullptr);
  } else {
    io_s = NewWritableFile(fs_.get(), log_fname, &lfile, opt_file_options);
  }

  if (io_s.ok()) {
    ......
    // 建立一個file writer
    std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
        std::move(lfile), log_fname, opt_file_options,
        immutable_db_options_.clock, io_tracer_, nullptr /* stats */, listeners,
        nullptr, tmp_set.Contains(FileType::kWalFile)));
    *new_log = new log::Writer(std::move(file_writer), log_file_num,
                               immutable_db_options_.recycle_log_file_num > 0,
                               immutable_db_options_.manual_wal_flush);
  }
  return io_s;
}      

其中​

​ReuseWritableFile​

​​函數和​

​NewWritableFile​

​​函數内部分别打開的是一個存在和不存在的檔案,如果我們能夠reuse log檔案名,則在​

​ReuseWritableFile​

​函數中通過open系統調用打開已存在檔案的時候不需要建立新的dentry和inode,且不需要将這一些中繼資料更新到各自dcache/inode-cache中的相應hash表中,是以重用檔案名這裡的優化就展現在核心對檔案的一些操作邏輯上。

關于open系統調用的核心邏輯,可以參考​​從unlink系統調用來看作業系統檔案系統原理​​。

​CreateWAL​

​這個函數僅僅是用到了recycle_log_number,什麼時候給recycle_log_number 指派呢,可以由下向上遞推。

recycle_log_number 如何複用log

向上遞推 ,可以看到​

​CreateWAL​

​​函數是在​

​SwitchMemtable​

​​中被調用,​

​recycle_log_number​

​​數值是從一個​

​log_recycle_files_​

​的deque中取出來的。

Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
  ......
  // 從log_recycle_files_ 的頭端取出一個元素作為目前可回收的log number
  uint64_t recycle_log_number = 0;
  if (creating_new_log && immutable_db_options_.recycle_log_file_num &&
      !log_recycle_files_.empty()) {
    recycle_log_number = log_recycle_files_.front();
  }
 
  ......
  if (creating_new_log) {
    // TODO: Write buffer size passed in should be max of all CF's instead
    // of mutable_cf_options.write_buffer_size.
    io_s = CreateWAL(new_log_number, recycle_log_number, preallocate_block_size,
                     &new_log);
    if (s.ok()) {
      s = io_s;
    }
  }      

而​

​log_recycle_files_​

​​ 這個deque則是在從活躍log deque ​

​alive_log_files_​

​ 中取的。

在​

​FindOnsoleteFiles​

​函數中需要清理一些過期檔案(log, sst, blob等),針對一些過期的log進行回收,并添加到log_recycle_files_ 雙端隊列中。

其中recycle_log_file_num 表示能夠回收的log個數

if (!alive_log_files_.empty() && !logs_.empty()) {
    uint64_t min_log_number = job_context->log_number;
    size_t num_alive_log_files = alive_log_files_.size();
    // find newly obsoleted log files
    // 從活躍log中取出沒有接受寫入資料的log,将這一部分log進行重用
    // min_log_number表示目前這個log 還在被持續更新。
    while (alive_log_files_.begin()->number < min_log_number) {
      auto& earliest = *alive_log_files_.begin();
      if (immutable_db_options_.recycle_log_file_num >
          log_recycle_files_.size()) {
        ROCKS_LOG_INFO(immutable_db_options_.info_log,
                       "adding log %" PRIu64 " to recycle list\n",
                       earliest.number);
        log_recycle_files_.push_back(earliest.number);
      } else {
        job_context->log_delete_files.push_back(earliest.number);      

關于​

​alive_log_files_​

​這個變量的元素更新是在 打開db 和 SwitchMemtable 過程中進行更新的,這兩個部分會建立wal

Rocksdb 利用recycle_log_file_num 重用wal-log檔案

在FindOnsoleteFiles函數中,構造好的​

​job_context​

​傳出即可。

清理WAL的調用棧如下(如果目前log被reuse,那就不會被清理了)

DBImpl::MaybeScheduleFlushOrCompaction // 排程compaction/flush
  DBImpl::BGWorkFlush // 從線程池排程flush
    DBImpl::BackgroundCallFlush 
      DBImpl::FindObsoleteFiles // 構造好我們的job_context,其中包括需要清理的sst/blob/log
        DBImpl::PurgeObsoleteFiles // 執行清理      

在清理函數中​

​PurgeObsoleteFiles​

​會決定是否需要keep log

bool keep = true;
    switch (type) {
      case kWalFile:
        keep = ((number >= state.log_number) ||
                (number == state.prev_log_number) ||
                (log_recycle_files_set.find(number) !=
                 log_recycle_files_set.end()));
        break;      

如果發現log在log_recycle_files_set 我們之前回收的log清單中,則需要keep,也就不會執行後續的log檔案删除了。

總結

到此,我們就知道參數​

​opts.recycle_log_file_num​

​的完整作用了,回到開頭提到的現象,在開頭的配置下大并發寫rocksdb 會發現部分log檔案可能存在的時間較長,且同時存在多個log 數目。

對于第一個問題 log存在的時間較長,即是由​

​recycle_log_file_num​

​​ 參數控制,它會不斷得複用一些過期(不接受寫入)的log,并且這一些log不會被回收。這個參數能夠提升log檔案的複用,減少對檔案中繼資料的操作,加速​

​SwitchMemtable​

​的過程。