rocksdb生成快照慢問題定位

0. 背景描述

測試 rocksdb 多副本版的導入時發(fā)現(xiàn) rocksdb 生成快照非常慢,其實一開始還發(fā)現(xiàn)了一些別的影響導入的因素,但都不是最核心的問題,這里就不說了。

我們直奔主題,具體執(zhí)行慢的代碼位于RocksDBStore類的writeSnapshot方法(下稱寫快照或生成快照):

@Override
public void writeSnapshot(String parentPath) {
    Lock writeLock = this.storeLock.writeLock();
    writeLock.lock();
    try {
        LOG.info("Store {} prepare write snapshot", this.store);
        // Every rocksdb instance should create an snapshot
        for (RocksDBSessions sessions : this.sessions()) {
            sessions.createSnapshot(parentPath);
        }
        LOG.info("Store {} write snapshot succeed, store lock will release",
                 this.store);
    } finally {
        writeLock.unlock();
    }
}

sessions.createSnapshot方法里最主要的代碼是調 rocksdb 提供的checkpoint.createCheckpoint(tempPath)方法,這個方法是干什么的下面會說明。

測試發(fā)現(xiàn),導入20分鐘的數(shù)據(jù)后,進行一次寫快照,需要兩分半鐘,在此期間,storeLock的寫鎖一直未釋放,導致上層所有的讀寫請求都被阻塞,服務處于不可用狀態(tài)。

1. 定位問題

1.1 查看官方文檔

首先肯定是看一下官方文檔,看我使用得對不對。checkpoint 的鏈接: https://github.com/facebook/rocksdb/wiki/Checkpoints

文檔里面說了幾個點:

  1. checkpoint 是 rocksdb 提供的一個為運行中的 db 生成快照的特性;
  2. 用 checkpoint 生成的快照,可以以只讀方式或讀寫方式打開;
  3. checkpoint 機制可用于全量備份和增量備份;
  4. 需要指定一個目錄存放快照;
  5. 如果快照目錄與原始數(shù)據(jù)文件位于同一文件系統(tǒng)上,SST 文件將被硬鏈接,否則 SST 文件將被復制;
  6. manifest 和 CURRENT 文件會被復制;
  7. 如果有多個列族,則會復制 checkpoint 開始和結束期間的日志文件(應該是 wal)。

這里的一個關鍵點是:如果位于同一文件系統(tǒng)上,sst 文件將被硬鏈接,所以應該是一個非常快速的操作,為什么實際測起來那么慢呢?

從這里似乎看不出什么問題,因為我也提供了目錄,而且這個目錄的文件系統(tǒng)與原始數(shù)據(jù)的文件系統(tǒng)是一樣的。

看來得使用老套路——火焰圖幫忙分析了。

1.2 打火焰圖

為了更方便地打火焰圖,其實我改了一下代碼,為 Graph 增加了一個顯式生成快照的方法,這樣就不用非要等 raft 的快照機制觸發(fā)了,我可以自行控制生成快照的時機。當然,這只是輔助測試的修改,代碼就不貼了。

替換 jar 包,啟動服務(此前已經(jīng)保存了 150G 的數(shù)據(jù)了)后,請求 API 主動生成快照,然后用 arthas 工具生成了火焰圖:

生成快照.png

從火焰圖中可以看到,有大量的entry_SYSCALL_64_fastpath、vfs_readvfs_write系統(tǒng)調用。很明顯,這是我看不懂的東西,但是看名字大概能猜到:這是在讀寫虛擬文件系統(tǒng),而且能看到一個rocksdb::CopyFile的方法調用占了很大比例。按道理,創(chuàng)建硬連接應該跟這些read、writeCopy是無關的。

所以:猜測這應該不是生成硬鏈接,而是在復制文件。

雖然看不懂這些系統(tǒng)調用,好在火焰圖也打印了 rocksdb 的方法調用:CreateCustomCheckpoint,所以跟蹤一下 rocksdb 的源碼吧。

1.3 查看 rocksdb 源碼

rocksdb/utilities/checkpoint/checkpoint_impl.cc中搜到了CreateCustomCheckpoint方法,方法簽名如下:

// 方法簽名
Status CheckpointImpl::CreateCustomCheckpoint(
    const DBOptions& db_options,
    std::function<Status(const std::string& src_dirname,
                         const std::string& src_fname, FileType type)>
        link_file_cb,
    std::function<Status(
        const std::string& src_dirname, const std::string& src_fname,
        uint64_t size_limit_bytes, FileType type,
        const std::string& checksum_func_name, const std::string& checksum_val)>
        copy_file_cb,
    std::function<Status(const std::string& fname, const std::string& contents,
                         FileType type)>
        create_file_cb,
    uint64_t* sequence_number, uint64_t log_size_for_flush,
    bool get_live_table_checksum) {

其接受了 3 個 function 參數(shù)比較令人注意,分別表示鏈接文件、拷貝文件創(chuàng)建文件的函數(shù),所以可以大膽猜測,方法里面的邏輯是:先判斷能不能鏈接文件,如果不行就拷貝文件,再不行就創(chuàng)建文件。

再繼續(xù)往下看主邏輯,果然看到了符合猜想的代碼:

// rules:
// * for kTableFile, attempt hard link instead of copy.
// * but can't hard link across filesystems.
if (same_fs) {
  s = link_file_cb(db_->GetName(), src_fname, kTableFile);
  if (s.IsNotSupported()) {
    same_fs = false;
    s = Status::OK();
  }
}
if (!same_fs) {
  // 省略
  s = copy_file_cb(db_->GetName(), src_fname, 0, kTableFile, checksum_name,
                   checksum_value);
}

鏈接文件和拷貝文件的比較順序是符合猜想的,不過創(chuàng)建文件只是跟 manifest 和 CURRENT 有關的。

上述代碼的邏輯是:如果是相同的文件系統(tǒng),就執(zhí)行鏈接,鏈接的 function 如果返回“不支持”,就執(zhí)行拷貝。

根據(jù)火焰圖的方法占比推斷,就是執(zhí)行了上述的邏輯。那現(xiàn)在的問題是:為什么鏈接文件會報“不支持”。

繼續(xù)回過頭查看調用CreateCustomCheckpoint的地方,看看link_file_cb是怎么定義的:

s = CreateCustomCheckpoint(
    db_options,
    // 這就是 link_file_cb 函數(shù)的定義
    [&](const std::string& src_dirname, const std::string& fname, FileType) {
        ROCKS_LOG_INFO(db_options.info_log, "Hard Linking %s", fname.c_str());
        return db_->GetFileSystem()->LinkFile(src_dirname + fname,
                                              full_private_path + fname,
                                              IOOptions(), nullptr);
    } /* link_file_cb */,
    [&](const std::string& src_dirname, const std::string& fname,
        uint64_t size_limit_bytes, FileType,
        const std::string& /* checksum_func_name */,
        const std::string& /* checksum_val */) {
        ROCKS_LOG_INFO(db_options.info_log, "Copying %s", fname.c_str());
        return CopyFile(db_->GetFileSystem(), src_dirname + fname,
                        full_private_path + fname, size_limit_bytes,
                        db_options.use_fsync);
    } /* copy_file_cb */,
    [&](const std::string& fname, const std::string& contents, FileType) {
        ROCKS_LOG_INFO(db_options.info_log, "Creating %s", fname.c_str());
        return CreateFile(db_->GetFileSystem(), full_private_path + fname,
                          contents, db_options.use_fsync);
    } /* create_file_cb */,
    &sequence_number, 
    log_size_for_flush);

這里看到了link_file_cbcopy_file_cb的定義,我們發(fā)現(xiàn)在函數(shù)內部,其實都有日志打印的。這個日志會打印到哪呢?rocksdb 數(shù)據(jù)目錄下的 LOG 文件。查看 LOG 文件,果然搜索到了如下內容:

2021/02/04-15:32:37.140621 7fd652cec700 [/db_filesnapshot.cc:30] File Deletions Disabled
2021/02/04-15:32:37.146337 7fd652cec700 [ilities/checkpoint/checkpoint_impl.cc:115] Hard Linking /009366.sst
2021/02/04-15:32:37.146354 7fd652cec700 [ilities/checkpoint/checkpoint_impl.cc:122] Copying /009366.sst
2021/02/04-15:32:39.840758 7fd652cec700 [ilities/checkpoint/checkpoint_impl.cc:122] Copying /009362.sst
2021/02/04-15:32:40.257284 7fd652cec700 [ilities/checkpoint/checkpoint_impl.cc:122] Copying /009376.sst
2021/02/04-15:32:42.986539 7fd652cec700 [ilities/checkpoint/checkpoint_impl.cc:122] Copying /009380.sst

現(xiàn)在就確認了前面的猜想,確實是先嘗試鏈接文件,然后失敗,于是走了拷貝文件的分支。

但是疑惑點還是存在:明明是同一個機器,為什么鏈接會失敗呢?要不直接用 linux 命令驗證一下吧。

1.4 命令驗證

在我當前的目錄執(zhí)行 ln 命令創(chuàng)建硬鏈接:

$ ln /home/disk1/rocksdb-vertex/g/009366.sst hard-link.sst
ln: creating hard link `hard-link.sst' => `/home/disk1/rocksdb-vertex/g/009366.sst': Invalid cross-device link

居然報了Invalid cross-device link,不能跨設備鏈接。

是用命令查看磁盤和目錄的對應關系:

$ df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda2              19G  4.8G   14G  27% /
tmpfs                  63G   11M   63G   1% /dev/shm
/dev/sda3              14G  457M   14G   4% /var
/dev/sda4              14G  939M   13G   7% /noah
/dev/sda5             4.6G  509M  4.1G  12% /matrix
/dev/sda6             1.9G  294M  1.6G  16% /has
/dev/sda7             6.4G  2.1G  4.3G  33% /tmp
/dev/sda8             2.6T  1.8T  852G  68% /home
/dev/sdb1             2.7T  622G  2.0T  25% /home/disk1
/dev/sdc1             2.7T  787G  1.8T  31% /home/disk2
/dev/sdd1             2.7T  500G  2.2T  19% /home/disk3

猛然想起來,當前所在的目錄(/home/xxx)和原始數(shù)據(jù)的目錄(/home/disk1)屬于不同磁盤,這就是硬鏈接失敗的原因。

2. 修復問題

知道了問題原因是:快照目錄和原始數(shù)據(jù)目錄不在同一塊盤,改就好改了,讓快照目錄與原始數(shù)據(jù)目錄位于同一塊盤即可。

TODO: 附上 pr 鏈接

3. 總結

其實回過頭來想想,其實好多地方都有跡象表明這個問題的原因是什么,比如:

  1. 直接查看 rocksdb 的 LOG 文件,能看到大段的 Copying 字樣;
  2. 查看原始文件和快照文件的詳細信息,對比 inode,會發(fā)現(xiàn)不一樣;
  3. 直接敲一下 ln 命令,就能看到異常提示信息;

未能快速發(fā)現(xiàn)問題,主要還是因為對 linux 系統(tǒng)不太熟悉所致,以后需要在這方面加強一下。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容