【轉】Rocksdb實現(xiàn)分析及優(yōu)化-Write Ahead Log刷盤策略及實現(xiàn)

rocksdb在寫memtable之前,會先寫WAL,所以WAL的刷盤策略很重要,事關機器宕機后數(shù)據(jù)是否丟失的問題,看了下最新的v5.8版本的代碼,這里簡單總結下這里吧

1. 相關配置

options中和WAL刷盤策略相關的配置只有一個:

  uint64_t wal_bytes_per_sync = 0;

注意:direct_io對WAL是不生效的。

  1. WAL文件操作封裝
    WAL操作類從上到下的封裝如下:
log::Write

WritableFileWriter

PosixWritableFile
fd

另外,在創(chuàng)建WAL之前,首先會有一個調用如下:


  EnvOptions OptimizeForLogWrite(const EnvOptions& env_options,
                                 const DBOptions& db_options) 
                                 const override {
    EnvOptions optimized = env_options;
    optimized.use_mmap_writes = false;
    optimized.use_direct_writes = false;
    optimized.bytes_per_sync = db_options.wal_bytes_per_sync;
    optimized.fallocate_with_keep_size = true;
    return optimized;
  }

可以看到,這里不會用direct_io和mmap打開文件,然后將用戶配置的wal_bytes_per_sync賦值給EnvOptions,然后調用

s = NewWritableFile(
            env_, LogFileName(immutable_db_options_.wal_dir, new_log_number),
            &lfile, opt_env_opt);

使用上面optimized EnvOptions打開PosixWritableFile(即lfile),它里面會真正open一個fd。接著使用lfile構造一個WritableFileWrite對象file_writer,最后使用file_writer構造最終的log::Writer對象。

這里有個細節(jié),上面的optimized.fallocate_with_keep_size = true是干什么用的呢,等會說。在構造好lfile后,會根據(jù)write_buffer_manager、db_write_buffer_size和max_total_wal_size的配置大小算出一個合理值,將它賦值給lfile的preallocation_block_size_。這個又是干什么用的呢,原來在調用file_writer->Append時,會調用lfile->PrepareWrite,它里面會根據(jù)preallocation_block_size_的算出offset和len,傳入Allocate,Allocate實現(xiàn)如下:

#ifdef ROCKSDB_FALLOCATE_PRESENT
Status PosixWritableFile::Allocate(uint64_t offset, uint64_t len) {
  assert(offset <= std::numeric_limits<off_t>::max());
  assert(len <= std::numeric_limits<off_t>::max());
  TEST_KILL_RANDOM("PosixWritableFile::Allocate:0", rocksdb_kill_odds);
  IOSTATS_TIMER_GUARD(allocate_nanos);
  int alloc_status = 0;
  if (allow_fallocate_) {
    alloc_status = fallocate(
        fd_, fallocate_with_keep_size_ ? FALLOC_FL_KEEP_SIZE : 0,
        static_cast<off_t>(offset), static_cast<off_t>(len));
  }
  if (alloc_status == 0) {
    return Status::OK();
  } else {
    return IOError(
        "While fallocate offset " + ToString(offset) + " len " +
         ToString(len),
        filename_, errno);
  }
}
#endif

man 2一下fallocate

fallocate() allows the caller to directly manipulate the allocated disk space

for the file referred to by fd for the byte range starting at offset and

continuing for len bytes.

FALLOC_FL_KEEP_SIZE

This flag allocates and initializes to zero the disk space within the range specified by offset and len. After a successful call, subsequent writes into this range are guaranteed not to fail because of lack of disk space. Preallocating zeroed blocks beyond the end of the file is useful for optimizing append workloads.
?
Preallocating blocks does not change the file size (as reported by stat(2)) even if it is less than offset+len.

這里就明了了,其實就給預分配空間(比如分配write_buffer_size大小的空間),確保對fd的寫入不會因為磁盤空間不足而失敗,所以剛才提問的optimized.fallocate_with_keep_size = true其實就是此處的FALLOC_FL_KEEP_SIZE,有了它,fallocate即使offset大于當前文件大小,也不會改變文件大小。

扯了這么多,其實就是縷了一下rocksdb到底對WAL最低層的fd怎么封裝的,以及相關配置到底怎么用。

2. 三種策略

  1. 每條都刷盤
    如果對數(shù)據(jù)安全性要求特別高,可以在Put或者Write是,配置WriteOptions::sync = true,這樣在寫完日志后會立刻刷盤,實現(xiàn)如下:
Status DBImpl::WriteToWAL(const WriteThread::WriteGroup& write_group,
                          log::Writer* log_writer, uint64_t* log_used,
                          bool need_log_sync, bool need_log_dir_sync,
                          SequenceNumber sequence) {
  ......
  
  if (status.ok() && need_log_sync) {
    StopWatch sw(env_, stats_, WAL_FILE_SYNC_MICROS);
    // It's safe to access logs_ with unlocked mutex_ here because:
    //  - we've set getting_synced=true for all logs,
    //    so other threads won't pop from logs_ while we're here,
    //  - only writer thread can push to logs_, and we're in
    //    writer thread, so no one will push to logs_,
    //  - as long as other threads don't modify it, it's safe to read
    //    from std::deque from multiple threads concurrently.
    for (auto& log : logs_) {
      status = log.writer->file()->Sync(immutable_db_options_.use_fsync);
      if (!status.ok()) {
        break;
      }
    }
    if (status.ok() && need_log_dir_sync) {
      // We only sync WAL directory the first time WAL syncing is
      // requested, so that in case users never turn on WAL sync,
      // we can avoid the disk I/O in the write code path.
      status = directories_.GetWalDir()->Fsync();
    }
  }
  ......
}

可以看到如果配置了sync=true,則need_log_sync=true,然后會執(zhí)行WritableFileWriter::Sync,先不管里面細節(jié),總之這里面最終會執(zhí)行fsync確保刷盤。
所以這種方式是最安全的,只要寫入成功即使立刻宕機,數(shù)據(jù)也不會丟,不過性能最差

  1. 配置了wal_bytes_per_sync
    如果配置了wal_bytes_per_sync不為0,假如1M,則WAL按照寫入順序,每寫入1M就將其前面1M的內容刷盤,可以理解成1M 1M的刷,這樣相對每一條都sync的策略來說,sync頻率變低,性能會高,但會有丟數(shù)據(jù)的風險,最多丟wal_bytes_per_sync字節(jié)

wal_bytes_per_sync到底在底下怎么做的呢,有必要看一下。

它會在構造WritableFileWriter對象(即上面說的file_writer)時傳入構造函數(shù),賦值給lfile的bytes_per_sync_成員變量,然后在file_writer->Append中,并不是直接寫文件,而是放到其緩沖區(qū)buf_中,然后調用file_writer->Flush,在Flush里面才會對其成員PosixWritableFile(即上面說的lfile)調用lfile->Append,真正寫到PageCache中,至于這里為什么file_writer->Append會先把數(shù)據(jù)放到buf_中,我覺的應該是如果配置了rate_limiter,把數(shù)據(jù)緩存在內存里然后按照限速策略來一點一點寫,最終達到一個限速的目的吧。

接著來,寫如PageCache后,file_writer->Flush還會做如下邏輯:

Status WritableFileWriter::Flush() {
  ......
  
  if (!use_direct_io() && bytes_per_sync_) {
    const uint64_t kBytesNotSyncRange = 1024 * 1024;  // recent 1MB
                                                      // is not synced.
    const uint64_t kBytesAlignWhenSync = 4 * 1024;    // Align 4KB.
    if (filesize_ > kBytesNotSyncRange) {
      uint64_t offset_sync_to = filesize_ - kBytesNotSyncRange;
      offset_sync_to -= offset_sync_to % kBytesAlignWhenSync;
      assert(offset_sync_to >= last_sync_size_);
      if (offset_sync_to > 0 &&
          offset_sync_to - last_sync_size_ >= bytes_per_sync_) {
        s = RangeSync(last_sync_size_, offset_sync_to - last_sync_size_);
        last_sync_size_ = offset_sync_to;
      }
    }
  }
  
  return s;
}

邏輯就是如果距上次sync的位置last_sync_size_,文件新的大小filesize_ 減去kBytesNotSyncRange(意思是最新寫入的1M數(shù)據(jù)不做sync)之后的大小如果大于bytes_per_sync_,則對這部分數(shù)據(jù)進行RangeSync,RangeSync最終調用sync_file_range來完成這部分數(shù)據(jù)的sync,調用之后,除了文件最后的1M數(shù)據(jù)之外,其他的內容都已經(jīng)刷盤成功。

  1. 完全交給操作系統(tǒng)
    這種性能最高,但一旦宕機,丟失的數(shù)據(jù)相對前面兩種策略也是最多的。rocksdb默認用的是這個策略。

有一點需要注意,這種策略下,rocksdb并不是完全不主動sync,當有多個columnfamily并且需要切換WAL文件時(Flush memtable前),rocksdb會強制把之前的WAL都刷盤,否則之前對多個columnfamily寫入成功的batch操作會丟失部分數(shù)據(jù)變的不一致。舉個例子:

假如有A、B兩個columnfamily,他們共享一個WAL,某一時刻A需要Flush memtable,此時它會切換到新的WAL,并且將memtable里的內容寫到Level 0的sst文件并且刷盤,注意,sst文件刷盤了!假設在A Flush之前有一個batch操作分別對A、B寫入一條數(shù)據(jù),F(xiàn)lush之后對A寫入的數(shù)據(jù)已經(jīng)存在于sst中并刷盤,不會丟,而對B寫入的數(shù)據(jù)還在B的memtable以及上一個WAL中,如果此時不對WAL刷盤,發(fā)生宕機并重啟,上一個WAL對B寫入的那條數(shù)據(jù)記錄丟失,不能recover,這時候就發(fā)生batch不一致了。

總結

清楚rocksdb WAL的刷盤策略還是很有必要的,這樣才能根據(jù)自己數(shù)據(jù)的重要程度來選擇合適的策略
原文地址:https://kernelmaker.github.io/Rocksdb_WAL

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • # rocksdb engine 寫邏輯 ## 執(zhí)行路徑 DB::Put(key, value)是一個寫操作簡單封...
    kenry閱讀 2,677評論 0 1
  • 對RocksDB的每一次update都會寫入兩個位置:1) 內存表(內存數(shù)據(jù)結構,后續(xù)會flush到SST fil...
    薛少佳閱讀 8,786評論 0 1
  • 原來我已經(jīng)27歲了。 我,93年,來廣州四年了。他,00年,還在漳州上學。我在最美麗的年紀遇到他,他在最懵懂的時候...
    被迫成長的18歲閱讀 245評論 2 1
  • 世人大都會在對方勢力弱小的時候,小瞧、鄙視別人,正如俗語“狗眼看人低”,在對方某些方面勝出一籌的時候會心存嫉妒甚至...
    洼泥閱讀 782評論 0 1
  • 現(xiàn)階段移動互聯(lián)網(wǎng)營銷發(fā)展的如火如荼,尤其是微博營銷、微信營銷更是備受關注。隨著智能手機的廣泛普及,微博、微信這種“...
    Cook閱讀 923評論 0 2

友情鏈接更多精彩內容