本文记录一些 RocksDB 中基本的数据结构、文件类型,以便之后阅读源代码时进行查阅以及理解。
接口
table cache有以下接口:
- GetTableReader
- FindTable
- NewIterator
- Get
- GetRangeTombstoneIterator
- MultiGetFilter
GetTableReader
GetReader函数是TableCache中的核心函数,负责创建TableReader对象来读取SST文件。
参数
Status GetTableReader(
const ReadOptions& ro, // 读取选项
const FileOptions& file_options, // 文件选项
const InternalKeyComparator& internal_comparator, // 内部键比较器
const FileMetaData& file_meta, // 文件元数据
bool sequential_mode, // 是否顺序读取模式
HistogramImpl* file_read_hist, // 文件读取直方图统计
std::unique_ptr<TableReader>* table_reader, // 输出参数:表读取器
const MutableCFOptions& mutable_cf_options, // 可变列族选项
bool skip_filters, // 是否跳过过滤器
int level, // SST文件所在层级
bool prefetch_index_and_filter_in_cache, // 是否预取索引和过滤器到缓存
size_t max_file_size_for_l0_meta_pin, // L0文件元数据固定的最大文件大小
Temperature file_temperature) // 文件温度(热、冷数据区分)流程
首先获取当前SST的名字,名字的格式是{ColumnFamily_Path}/{number}_{sst}。
std::string fname = TableFileName(
ioptions_.cf_paths, file_meta.fd.GetNumber(), file_meta.fd.GetPathId());然后读入文件I/O配置(哪些配置?)
FileOptions fopts = file_options;
fopts.temperature = file_temperature;
Status s = PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);再下来尝试打开文件,如果以上命名方式方式无法查到,则换另一种方式打开。打开后记录文件统计信息
if (s.ok()) {
s = ioptions_.fs->NewRandomAccessFile(fname, fopts, &file, nullptr);
}
if (s.ok()) {
RecordTick(ioptions_.stats, NO_FILE_OPENS);
} else if (s.IsPathNotFound()) {
// 尝试使用另一种命名格式查找文件
fname = Rocks2LevelTableFileName(fname);
// ... 重试打开文件的代码 ...
}如果成功打开文件,则进行对于文件读取的设置,目的在于增强文件读取的性能
if (s.ok()) {
if (!sequential_mode && ioptions_.advise_random_on_open) {
file->Hint(FSRandomAccessFile::kRandom);
}
if (ioptions_.default_temperature != Temperature::kUnknown &&
file_temperature == Temperature::kUnknown) {
file_temperature = ioptions_.default_temperature;
}以上代码的Hint很有意思,其实底层调用了一个Linux的系统调用posix_fadvise,用处是调整预读策略或清理缓存。
rocksdb中一共配置了以下几种:POSIX_FADV_NORMAL,POSIX_FADV_RANDOM,POSIX_FADV_SEQUENTIAL,POSIX_FADV_WILLNEED,POSIX_FADV_DONTNEED。
Table Cache中用到了Random类型,用处是随机访问模式,禁止了prefetch,避免缓存无用数据。如果没有设置sequential_mode顺序读且设置了随机访问的advice,则开启RANDOM。
然后是创建文件读取器和性能监控,记录打开文件所需要的微秒数,同时具有统计、跟踪功能
StopWatch sw(ioptions_.clock, ioptions_.stats, TABLE_OPEN_IO_MICROS);
std::unique_ptr<RandomAccessFileReader> file_reader(
new RandomAccessFileReader(std::move(file), fname, ioptions_.clock,
io_tracer_, ioptions_.stats, SST_READ_MICROS,
file_read_hist, ioptions_.rate_limiter.get(),
ioptions_.listeners, file_temperature,
level == ioptions_.num_levels - 1));然后将file_reader传入工厂类,开始构建
s = mutable_cf_options.table_factory->NewTableReader(
ro,
TableReaderOptions(
// 大量参数设置...
),
std::move(file_reader), file_meta.fd.GetFileSize(), table_reader,
prefetch_index_and_filter_in_cache);Table Factory
在TableCache中,Status TableCache::GetTableReader函数需要获取一个TableReader。这里使用了一个工厂模式的Table Factory实现。
调用最上层的基类为class TableFactory,继承自public Customizable,里面的方法都是纯虚函数。可以发现一共有三种创建方式:BlockBasedTable,PlainTable,CuckooTable。三种创建方式由class AdaptiveTableFactory这个类中的函数选定,也是继承自TableFactory,代码如下:
Status AdaptiveTableFactory::NewTableReader(
const ReadOptions& ro, const TableReaderOptions& table_reader_options,
std::unique_ptr<RandomAccessFileReader>&& file, uint64_t file_size,
std::unique_ptr<TableReader>* table,
bool prefetch_index_and_filter_in_cache) const {
Footer footer;
IOOptions opts;
auto s =
ReadFooterFromFile(opts, file.get(), *table_reader_options.ioptions.fs,
nullptr /* prefetch_buffer */, file_size, &footer);
if (!s.ok()) {
return s;
}
if (footer.table_magic_number() == kPlainTableMagicNumber ||
footer.table_magic_number() == kLegacyPlainTableMagicNumber) {
return plain_table_factory_->NewTableReader(
table_reader_options, std::move(file), file_size, table);
} else if (footer.table_magic_number() == kBlockBasedTableMagicNumber ||
footer.table_magic_number() == kLegacyBlockBasedTableMagicNumber) {
return block_based_table_factory_->NewTableReader(
ro, table_reader_options, std::move(file), file_size, table,
prefetch_index_and_filter_in_cache);
} else if (footer.table_magic_number() == kCuckooTableMagicNumber) {
return cuckoo_table_factory_->NewTableReader(
table_reader_options, std::move(file), file_size, table);
} else {
return Status::NotSupported("Unidentified table format");
}
}可以发现,这里的判断是基于footer.table_magic_number的。
然后是构建过程,这里直接new一个PlainTableBuilder进行构建 构造函数接收大量参数,主要包括:
- 配置选项:ioptions和moptions(不可变和可变的列族选项)
- 文件信息:file(可写文件)、file_number(文件编号)
- 编码相关:user_key_len(用户键长度)、encoding_type(编码类型)
- 索引相关:index_sparseness(索引稀疏度)、store_index_in_file(是否将索引存储在文件中)
- 布隆过滤器相关:bloom_bits_per_key(每个键的位数)、num_probes(探测次数)
- 哈希表相关:hash_table_ratio(哈希表比率)、huge_page_tlb_size(大页TLB大小)
- 标识信息:column_family_id、column_family_name、db_id、db_session_id
Table Builder
以下翻译自Table Builder上所加的注释,可以大致知道Table Builder使用的位置:
返回一个表构建器(table builder),用于将此表类型的数据写入文件。该函数在以下场景被调用:
(1) 当将内存表(memtable)刷新(flush)到 Level-0 层的输出文件时,
通过调用 BuildTable() 创建表构建器(见 DBImpl::WriteLevel0Table())。
(2) 在压缩(compaction)过程中,通过 DBImpl::OpenCompactionOutputFile()
获取构建器以写入压缩输出文件。
(3) 从事务日志(transaction logs)恢复数据时,通过调用 BuildTable()
创建表构建器以写入 Level-0 层的输出文件(见 DBImpl::WriteLevel0TableForRecovery)。
(4) 在运行修复工具(Repairer)时,通过调用 BuildTable() 创建表构建器,将日志转换为 SST 文件(见 Repairer::ConvertLogToTable())。
可通过此函数访问多项配置参数,包括但不限于压缩选项(compression options)。
参数 `file` 是一个可写文件(writable file)的句柄。
调用方需负责保持文件处于打开状态,并在表构建器关闭后关闭该文件。
参数 `compression_type` 表示此表中使用的压缩类型。
Table Builder的逻辑仅仅是调用一下::Open函数,这里以BlockBasedTable::Open作为例子。
Open
const bool prefetch_all = prefetch_index_and_filter_in_cache || level == 0;
const bool preload_all = !table_options.cache_index_and_filter_blocks;
if (!ioptions.allow_mmap_reads && !env_options.use_mmap_reads) {
s = PrefetchTail(/* 参数... */);
// 检查错误
} else {
// 内存映射模式不需要预取
prefetch_buffer.reset(new FilePrefetchBuffer(/* 参数... */));
}首先判断是否需要prefetch所有索引和过滤器,结果丢到下面的PrefetchTail使用。
- 根据传入的参数决定是否要prefetch,如果是L0层的文件则必须prefetch。
然后是有关mmap的设置,众所周知mmap是用来直接映射内存的,这里对于是否使用mmap对预取有不同的影响:
- 如果不用mmap,则PrefetchTail。
- 如果用了,则建立一个Prefetch buffer。
接下来是读取Table里的内容,注释中给出了顺序:
1. Footer
2. [metaindex block]
3. [meta block: properties]
4. [meta block: range deletion tombstone]
5. [meta block: compression dictionary]
6. [meta block: index]
7. [meta block: filter]
由于Footer是索引的索引,因此显然我们需要先取出Footer,再取出二级索引metaindex,进而取出meta block。