Rocksdb Code Analysis Get Performance
Previous On Rocksdb
之前的博客讨论了DBImpl::Get接口实现,Rocksdb Get 接口会依次在memtable, immutable memtable,和version 中查找当前key。如果每次都是内存查找命中例如在memtable 中命中,例如Put之后马上Get的场景,Get的性能当然非常好。但是考虑如果每次都查盘的场景,例如在rocksdb中读到很久之前的数据,这样的场景下rockdb的性能如何呢?下面让我们通过实际测试结合代码进行分析。
Prepare
1,Physical Machine:40 core cpu,3T nvme ssd(4k fio 测试平均性能延迟100us左右)。
2,实验数据量10亿kv。kv大200bytes左右。由于有snappy压缩,最终落盘数据量220G左右。按照rocksdb数据分配的策略level1默认大小max_bytes_for_level_base 256MB,默认max_bytes_for_level_multiplier 10,这样level2最大容量2.56G,level3最大容量 25.6G,level4最大容量 256G。这样整个220G数据最终分布在level0-level4当中。
4, 关闭filter block和 index block cache。使其读盘获得filter block 和index block。
3,为了让Rocksdb读盘,我们需要在Get 实验之前将page cache清除。
Result Analysis
期间我们发现多线程调用Get接口的性能大概在1.4Wqps左右,此时40个CPU使用率已经超过90%。这显然不符合我们对于Rocksdb这种高速kv引擎的期望。近一步我们通过pstack看看Rocksdb在做什么。
观察pstack 结果如下:
#2 GetLogicalBufferSize at env/io_posix.cc:70
#3 PosixRandomAccessFile::PosixRandomAccessFile at env/io_posix.cc:303
#4 PosixEnv::NewRandomAccessFile at env/env_posix.cc:252
#5 TableCache::GetTableReader at db/table_cache.cc:97
#6 TableCache::FindTable at db/table_cache.cc:150
#7 TableCache::Get at db/table_cache.cc:376
#8 Version::Get at db/version_set.cc:1002
#9 DBImpl::GetImpl at db/db_impl.cc:1024
#10 DBImpl::Get at db/db_impl.cc:941
#11 DB::Get at ./include/rocksdb/db.h:317
可以看出 #5 中TableCache::FindTable中由于待查找sst对应的TableReader没有被创建,需要调用TableCache::GetTableReader创建TableReader,并缓存到table_cache中。所以,怀疑是缓存TableReader对应的参数max_open_files 配置不够高,大多数请求需要重新创建TableReader。
再次测试时,配置max_open_files 为-1,缓存所有创建过的TableReader后,Get QPS 达到了30W左右,disk iops 也是30W左右。由于测试的是Rocksdb每次请求都读盘的场景,所以结果还比较符合预期。
Code Analysis
然而,创建TableReader 过程到底发生了什么,导致这么消耗资源呢?由于有大量的CPU使用,所以怀疑是有大量的系统调用造成的。下面结合代码具体分析TableReader 的创建过程,重点关注其中涉及到的耗时操作。
创建TableReader主要系统调用代码如下:
Status TableCache::GetTableReader() {
// invoke GetLogicalBufferSize
Status s = ioptions_.env->NewRandomAccessFile(fname, &file, env_options);
file->Hint(RandomAccessFile::RANDOM);
std::unique_ptr<RandomAccessFileReader> file_reader(
new RandomAccessFileReader(std::move(file), fname,...));
s = ioptions_.table_factory->NewTableReader(file_reader);
}
1,创建NewRandomAccessFile。
2,调用Hint修改文件POSIX_FADV_RANDOM属性(”The specified data will be accessed in random order” from posix_fadvise man page)。
3,用NewRandomAccessFile的结果创建RandomAccessFileReader。
4,用RandomAccessFileReader的结果创建TableReader。
virtual Status NewRandomAccessFile(const std::string& fname,
unique_ptr<RandomAccessFile>* result,
const EnvOptions& options) override {
fd = open(fname.c_str(), flags, 0644);
SetFD_CLOEXEC(fd, &options);
// stat(fname.c_str(), &sbuf)
s = GetFileSize(fname, &size);
if (options.use_mmap_reads) {
void* base = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
result->reset(new PosixMmapReadableFile(fd, fname, base,
size, options));
} else {
result->reset(new PosixRandomAccessFile(fname, fd, options));
}
}
1,调用open,SetFD_CLOEXEC,GetFileSize(即stat()) 等系统调用。
2,创建PosixRandomAccessFile。
3,调用BlockBasedTableFactory::NewTableReader创建TableReader。
PosixRandomAccessFile::PosixRandomAccessFile(const std::string& fname,
int fd,
const EnvOptions& options)
: filename_(fname),
fd_(fd),
use_direct_io_(options.use_direct_reads),
logical_sector_size_(GetLogicalBufferSize(fd_)) {
}
// get device page cache
size_t GetLogicalBufferSize(int __attribute__((__unused__)) fd) {
int result = fstat(fd, &buf);
// get device dir
if (realpath(path, real_path) == nullptr) {
return kDefaultPageSize;
}
std::string fname = device_dir + "/queue/logical_block_size";
fp = fopen(fname.c_str(), "r");
getline(&line, &len, fp);
return size;
}
GetLogicalBufferSize 函数为了查找device使用的默认page cache大小,需要从一个设备文件目录读出来。期间调用realpath 利用lstat反复调用文件目录信息,获得文件完整路径(非常耗时 具体见strace结果)。之后调用fopen获得logical buffer cache。
BlockBasedTableFactory::NewTableReader {
return BlockBasedTable::Open();
}
Status BlockBasedTable::Open() {
s = prefetch_buffer->Prefetch(file.get(), prefetch_off, prefetch_len);
// 53 bytes footer
s = ReadFooterFromFile(file.get(), ..., file_size, &footer,
kBlockBasedTableMagicNumber);
}
最后,调用BlockBasedTableFactory::NewTableReader 进而调用 BlockBasedTable::Open,其中也会有一些系统调用,其中最主要的是ReadFooterFromFile会调用open读取 53bytes的footer。
以上就是创建TableReader的主要流程以及其需要的系统调用。
Strace SystemCall
为验证其正确性,打印了进程的strace 消息。其中打印了每个系统调用的调用时间。例如
4850 17:28:10.168420 futex(0x12cc9b0, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
4850 17:28:10.168520 <... futex resumed> ) = 1 <0.000096>
为线程4850 调用futex 所用时间0.000096s(96us)
具体的strace数据在strace raw result。
Conclusion
结合上述strace结果分析,创建TableReader 花费1.2ms时间,没有filter block和 index block cache的情况下,Get 实际读取meta block 读取 data block 花费 0.3~0.5ms。从这里可以看出,TableReader创建是一个非常费CPU的事情,如果max_open_files参数设置过小,会导致在LRU cache中不停的刷出旧的TableReader,并且创建新的TableReader。另外,上述的strace结果能看出的有意思的东西非常多,有兴趣的同学可以自行研究,这里就不一一解释了。