0. 背景描述
测试 rocksdb 多副本版的导入时发现 rocksdb 生成快照非常慢,其实一开始还发现了一些别的影响导入的因素,但都不是最核心的问题,这里就不说了。
我们直奔主题,具体执行慢的代码位于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)
方法,这个方法是干什么的下面会说明。
测试发现,导入20分钟的数据后,进行一次写快照,需要两分半钟,在此期间,storeLock
的写锁一直未释放,导致上层所有的读写请求都被阻塞,服务处于不可用状态。
1. 定位问题
1.1 查看官方文档
首先肯定是看一下官方文档,看我使用得对不对。checkpoint 的链接: https://github.com/facebook/rocksdb/wiki/Checkpoints。
文档里面说了几个点:
- checkpoint 是 rocksdb 提供的一个为运行中的 db 生成快照的特性;
- 用 checkpoint 生成的快照,可以以只读方式或读写方式打开;
- checkpoint 机制可用于全量备份和增量备份;
- 需要指定一个目录存放快照;
- 如果快照目录与原始数据文件位于同一文件系统上,SST 文件将被硬链接,否则 SST 文件将被复制;
- manifest 和 CURRENT 文件会被复制;
- 如果有多个列族,则会复制 checkpoint 开始和结束期间的日志文件(应该是 wal)。
这里的一个关键点是:如果位于同一文件系统上,sst 文件将被硬链接,所以应该是一个非常快速的操作,为什么实际测起来那么慢呢?
从这里似乎看不出什么问题,因为我也提供了目录,而且这个目录的文件系统与原始数据的文件系统是一样的。
看来得使用老套路——火焰图帮忙分析了。
1.2 打火焰图
为了更方便地打火焰图,其实我改了一下代码,为 Graph 增加了一个显式生成快照的方法,这样就不用非要等 raft 的快照机制触发了,我可以自行控制生成快照的时机。当然,这只是辅助测试的修改,代码就不贴了。
替换 jar 包,启动服务(此前已经保存了 150G 的数据了)后,请求 API 主动生成快照,然后用 arthas 工具生成了火焰图:
从火焰图中可以看到,有大量的entry_SYSCALL_64_fastpath
、vfs_read
和vfs_write
系统调用。很明显,这是我看不懂的东西,但是看名字大概能猜到:这是在读写虚拟文件系统,而且能看到一个rocksdb::CopyFile
的方法调用占了很大比例。按道理,创建硬连接应该跟这些read
、write
和Copy
是无关的。
所以:猜测这应该不是生成硬链接,而是在复制文件。
虽然看不懂这些系统调用,好在火焰图也打印了 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 参数比较令人注意,分别表示链接文件、拷贝文件、创建文件的函数,所以可以大胆猜测,方法里面的逻辑是:先判断能不能链接文件,如果不行就拷贝文件,再不行就创建文件。
再继续往下看主逻辑,果然看到了符合猜想的代码:
// 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);
}
链接文件和拷贝文件的比较顺序是符合猜想的,不过创建文件只是跟 manifest 和 CURRENT 有关的。
上述代码的逻辑是:如果是相同的文件系统,就执行链接,链接的 function 如果返回“不支持”,就执行拷贝。
根据火焰图的方法占比推断,就是执行了上述的逻辑。那现在的问题是:为什么链接文件会报“不支持”。
继续回过头查看调用CreateCustomCheckpoint
的地方,看看link_file_cb
是怎么定义的:
s = CreateCustomCheckpoint(
db_options,
// 这就是 link_file_cb 函数的定义
[&](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
的定义,我们发现在函数内部,其实都有日志打印的。这个日志会打印到哪呢?rocksdb 数据目录下的 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
现在就确认了前面的猜想,确实是先尝试链接文件,然后失败,于是走了拷贝文件的分支。
但是疑惑点还是存在:明明是同一个机器,为什么链接会失败呢?要不直接用 linux 命令验证一下吧。
1.4 命令验证
在我当前的目录执行 ln 命令创建硬链接:
$ 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)和原始数据的目录(/home/disk1)属于不同磁盘,这就是硬链接失败的原因。
2. 修复问题
知道了问题原因是:快照目录和原始数据目录不在同一块盘,改就好改了,让快照目录与原始数据目录位于同一块盘即可。
TODO: 附上 pr 链接
3. 总结
其实回过头来想想,其实好多地方都有迹象表明这个问题的原因是什么,比如:
- 直接查看 rocksdb 的 LOG 文件,能看到大段的 Copying 字样;
- 查看原始文件和快照文件的详细信息,对比 inode,会发现不一样;
- 直接敲一下 ln 命令,就能看到异常提示信息;
未能快速发现问题,主要还是因为对 linux 系统不太熟悉所致,以后需要在这方面加强一下。