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。
文檔里面說了幾個點:
- checkpoint 是 rocksdb 提供的一個為運行中的 db 生成快照的特性;
- 用 checkpoint 生成的快照,可以以只讀方式或讀寫方式打開;
- checkpoint 機制可用于全量備份和增量備份;
- 需要指定一個目錄存放快照;
- 如果快照目錄與原始數(shù)據(jù)文件位于同一文件系統(tǒng)上,SST 文件將被硬鏈接,否則 SST 文件將被復制;
- manifest 和 CURRENT 文件會被復制;
- 如果有多個列族,則會復制 checkpoint 開始和結束期間的日志文件(應該是 wal)。
這里的一個關鍵點是:如果位于同一文件系統(tǒng)上,sst 文件將被硬鏈接,所以應該是一個非常快速的操作,為什么實際測起來那么慢呢?
從這里似乎看不出什么問題,因為我也提供了目錄,而且這個目錄的文件系統(tǒng)與原始數(shù)據(jù)的文件系統(tǒng)是一樣的。
看來得使用老套路——火焰圖幫忙分析了。
1.2 打火焰圖
為了更方便地打火焰圖,其實我改了一下代碼,為 Graph 增加了一個顯式生成快照的方法,這樣就不用非要等 raft 的快照機制觸發(fā)了,我可以自行控制生成快照的時機。當然,這只是輔助測試的修改,代碼就不貼了。
替換 jar 包,啟動服務(此前已經(jīng)保存了 150G 的數(shù)據(jù)了)后,請求 API 主動生成快照,然后用 arthas 工具生成了火焰圖:

從火焰圖中可以看到,有大量的entry_SYSCALL_64_fastpath、vfs_read和vfs_write系統(tǒng)調用。很明顯,這是我看不懂的東西,但是看名字大概能猜到:這是在讀寫虛擬文件系統(tǒng),而且能看到一個rocksdb::CopyFile的方法調用占了很大比例。按道理,創(chuàng)建硬連接應該跟這些read、write和Copy是無關的。
所以:猜測這應該不是生成硬鏈接,而是在復制文件。
雖然看不懂這些系統(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_cb和copy_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. 總結
其實回過頭來想想,其實好多地方都有跡象表明這個問題的原因是什么,比如:
- 直接查看 rocksdb 的 LOG 文件,能看到大段的 Copying 字樣;
- 查看原始文件和快照文件的詳細信息,對比 inode,會發(fā)現(xiàn)不一樣;
- 直接敲一下 ln 命令,就能看到異常提示信息;
未能快速發(fā)現(xiàn)問題,主要還是因為對 linux 系統(tǒng)不太熟悉所致,以后需要在這方面加強一下。