ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RocksDB 异步 IO(async_io)深度解析:Iterator 扫描与 MultiGet 的延迟优化

RocksDB 异步 IO(async_io)深度解析:Iterator 扫描与 MultiGet 的延迟优化 RocksDB 异步 IOasync_io深度解析Iterator 扫描与 MultiGet 的延迟优化【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读本文基于 RocksDB 官方技术博客 Asynchronous IO in RocksDB 展开系统讲解 RocksDB 如何在迭代器Iterator扫描与 MultiGet 批量查询两条路径上引入异步 IO用并发读取 后台预取的方式隐藏慢速存储如远端闪存带来的访问延迟。读完本文你将掌握ReadOptions::async_io与ReadOptions::optimize_multiget_for_io两个核心开关的作用与适用场景、Seek/Next 两阶段异步化的内部原理、基于 C 协程的 MultiGet 实现以及异步 IO 的已知限制与适用前提并了解与之对应的核心源码实现。背景存储延迟为何成为读路径的瓶颈RocksDB 提供了多套 KV 读取 APIGet与MultiGet用于点查Iterator用于顺序扫描。这些 API 在读取时会从 SST 文件解析所需的 block数据块、索引块、布隆过滤器块等。实际从存储介质读取哪些 block、读取频率如何完全取决于工作负载特征工作集较小的负载大部分数据都能命中 block cache几乎不触碰磁盘工作集较大的负载频繁从磁盘读 block延迟显著升高、吞吐显著下降存储介质迁移困难从本地闪存迁移到分离式disaggregated闪存等新介质时读路径延迟特性完全不同应用很难无痛迁移。缓解存储延迟影响的通用思路是尽可能多地异步、并行地发起读取把 IO 延迟隐藏起来。RocksDB 在 Iterator 与 MultiGet 两条路径上落实了这一思路Iterator对每个被迭代的文件在后台异步预取数据取代以往阻塞迭代线程的同步预取MultiGet确定一批 key 与哪些 SST 文件重叠然后借助异步文件系统 API 并行读取这些文件中的数据块。需要强调的是这些优化全部位于 Iterator 与 MultiGet 的内部实现用户 API 依旧是同步语义现有代码无需任何改动即可受益。文档原文也预告了未来可能考虑异步用户 API。设计总览从 ReadOptions 到 FileSystem 层两个关键开关async_io 与 optimize_multiget_for_ioReadOptions中新增的async_io标志控制异步 IO 的启用其声明位于 include/rocksdb/options.h// If async_io is enabled, RocksDB will prefetch some of data asynchronously. // RocksDB apply it if reads are sequential and its internal automatic // prefetching. bool async_io false;要点async_io开启后同时作用于 Iterator 与 MultiGet两条路径源码注释明确了适用前提只有当读取是顺序的且走的是 RocksDB内部自动预取逻辑时才生效默认值为false。针对 MultiGet还有一个附加开关optimize_multiget_for_io默认true标记为 Experimental声明在 include/rocksdb/options.h// Experimental // // If async_io is set, then this flag controls whether we read SST files // in multiple levels asynchronously. Enabling this flag can help reduce // MultiGet latency by maximizing the number of SST files read in // parallel if the keys in the MultiGet batch are in different levels. It // comes at the expense of slightly higher CPU overhead. bool optimize_multiget_for_io true;该开关控制异步 IO 的激进程度设置行为代价optimize_multiget_for_io false同一 LSM level 内的多个文件并行读取但不同 level 之间串行single-level 并行CPU 开销较低optimize_multiget_for_io true默认解除 level 限制尽可能多的文件跨 level并行读取multi-level 并行CPU 开销依工作负载可能更高两个开关在 C API 中也暴露了 setter/getter见 include/rocksdb/c.h例如rocksdb_readoptions_set_async_io与rocksdb_readoptions_set_optimize_multiget_for_io。文件系统层的异步原语ReadAsync在 FileSystem 抽象层异步读取通过FSRandomAccessFile::ReadAsync发起调用方提供完成回调completion callback读操作在后台执行完成后回调被触发。接口声明位于 include/rocksdb/file_system.h。同时FSRandomAccessFile::Poll接口用于轮询异步 IO 的完成状态SupportedOps中默认包含 async_io 与 prefetch 操作include/rocksdb/file_system.h。从源码看ReadAsync的实际落地与io_uring强相关在 env/io_posix.cc 中PosixRandomAccessFile::ReadAsync尝试向 io_uring 提交队列获取 SQEsubmission queue entry失败时返回IOStatus::NotSupported(ReadAsync: failed to init io_uring)并在编译期依赖宏ROCKSDB_IOURING_PRESENT见 env/io_posix.cc。这正是文档已知限制一节所述目前仅 PosixFileSystem 支持的底层原因。扫描路径Scan的异步化一次扫描涉及哪些 IO一次典型的 RocksDB 扫描由三步组成分配新迭代器 → 用目标 key 执行Seek定位 → 连续执行多次Next顺序遍历。Seek与Next都存在异步读取的机会。扫描会依次遍历多个实体中的 key活跃 memtable已密封但未刷新的 memtable每个 L0 文件每个非空且非零的 level。前两类完全在内存中不受 IO 延迟影响后两类需要读取 SST 文件。这意味着 IO 延迟的上升存在放大效应多个 L0 文件、多个 level 都必须被迭代。block cache、前缀布隆过滤器等机制虽然能减少需要迭代的文件数与文件读取次数但只要仍有少量磁盘读就可能主导整体延迟。RocksDB 在Seek与Next两个操作上都使用异步 IO 来缓解延迟。Seek 的两阶段异步化迭代器内部维护了一组子迭代器child iterator每个 L0 文件一个、每个非空非零 level 一个。Seek时每个子迭代器都要定位到目标 key——默认实现是串行的所需数据块不在缓存时就同步读 SST。开启async_io后Seek被拆成两个阶段阶段一在每个文件/level 中定位Seek所需的数据块并发出异步读——多个文件/level 的读在阶段一并行发起阶段二用相同 key 重新 Seekreseeek此时每个 level 等待各自的异步读完成并定位 table iterator。阶段一并行读取多个 block显著降低了整体 Seek 延迟。这一两阶段逻辑对应BlockBasedTableIterator::AsyncInitDataBlocktable/block_based/block_based_table_iterator.cc它通过is_first_pass参数区分两阶段调用并用async_read_in_progress_成员记录第一阶段的异步读是否仍在进行见 table/block_based/block_based_table_iterator.cc。Next 的双缓冲异步预取对于Next操作RocksDB 通过预取降低 IO 延迟当Next需要的数据块不在缓存中时触发文件读取 预取。读取与预取由FilePrefetchBuffer管理——这是一个每个表迭代器BlockBasedTableIterator一个的对象。默认同步预取策略为从文件的第三次读取开始预取初始预取大小为8KB之后每次读取翻倍上限256KB。预取量还会随用户在ReadOptions与BlockBasedTableOptions中给出的选项变化。同步预取虽好但仍阻塞迭代线程。开启async_io后FilePrefetchBuffer改为维护两个预取缓冲区把计算出的预取大小拆分到两个缓冲区上迭代进行中消费第一个缓冲区第一个缓冲区数据用完后被清空并调度一个异步读去预取更多数据异步读在后台进行迭代器继续处理第二个缓冲区中的数据此时两个缓冲区的角色互换。这套机制并不能 100% 隐藏 IO 延迟——内存数据耗尽后迭代器仍需等待一次异步读完成但它通过重叠 CPU 与 IO隐藏了部分延迟且多个 level 上的异步预取可以并行进行进一步降低延迟。从源码看双缓冲正是FilePrefetchBuffer的num_buffers_ 1模式。构造时num_buffers_(readahead_params.num_buffers)file/file_prefetch_buffer.hfree_bufs_保存空闲缓冲区、bufs_保存含预取数据的缓冲区file/file_prefetch_buffer.hPrefetchAsync负责异步预取file/file_prefetch_buffer.h每个缓冲区通过async_read_in_progress_标记异步读状态。num_buffers_ 1时走同步顺序读流程num_buffers_ 1时走异步后台预取file/file_prefetch_buffer.h。下图展示了双缓冲区异步预取的完整流程图中深蓝/浅蓝分别代表两个预取缓冲区箭头标注各数据块位置的异步预取路径MultiGet 的协程化异步并行从 MultiRead 到文件级并行MultiGet接受一批 key相比循环调用Get更高效。其基础效率来自对落在同一个 SST 文件的多个 key批量读取多个数据块这通过 FileSystem 层的MultiReadAPI 实现。但即便有MultiRead散落在不同文件的 key 子集仍然串行读取。为了把并行度再推进一步——多个文件同时读——MultiGet 实现做了三处根本性改造协程CoroutinesMultiGet 的完整流程是确定批内与某个 SST 文件重叠的 key 集合 → 调用TableReader::MultiGet完成实际查找。TableReader内部依次探测布隆过滤器 → 遍历索引块 → 查询 block cache → 从 SST 读缺失的数据块 → 在数据块中搜索 key。每个阶段都会累积大量上下文若让多个TableReader交错进行数据块读取实现会非常复杂。为此 RocksDB 采用C 协程 异步 IOTableReader::MultiGet实现为协程在发出缺失数据块的异步读后被挂起suspend顶层 MultiGet 借此先遍历完所有 key 对应的TableReader再等待读取完成、恢复resume协程。过滤Filtering协程的 CPU 开销不容忽视应尽量少用。一个可以完全避免调用TableReader::MultiGet协程的场景是预先知道与文件重叠的 key 在文件中并不存在。这通过探测布隆过滤器即可确定。旧实现把布隆过滤嵌入TableReader::MultiGet内部改造后过滤被提前为独立步骤在调用TableReader::MultiGet之前执行。拆分批次Splitting batchesMultiGet 的默认策略是查完一个 level或 L0 文件再进入下一个这限制了 IO 并行度——例如批内 key 可能分散在多个文件中即使 key 空间上聚集也可能不在同一 level。优化思路是先确定很可能位于某个 level的 key 子集把 MultiGet 批次拆成两份——该 level 的子集 剩余部分后者可并行处理很可能位于某 level的判断正来自第 2 步的过滤结果。这三项改造共同实现了两种异步延迟优化Single-level并行读取同一 LSM level 内多个文件的数据块Multi-level并行读取多个 level 中多个文件的数据块。从源码看TableReader::MultiGet确实存在协程版本在 table/block_based/block_based_table_reader.cc 中可以看到通过模板同时生成普通方法与协程方法folly::coro::TaskStatus、folly::coro::TaskT*并在 table/block_based/block_based_table_reader_sync_and_async.h 通过co_await batch-context()-reader().MultiReadAsync(...)挂起等待批量异步读——这印证了协程挂起在发出异步读之后的设计描述。底层MultiReadAsync同样基于ReadAsync体系见 include/rocksdb/file_system.h 的转发实现。下图展示了 MultiGet 异步 IO 的全貌批内 5 个 keyK1~K5跨 L0~L3 多个 level映射到多个 SST 文件并行的异步查询路径实测结果延迟随并行度提升逐步下降文档给出了在远端闪存env_uriws://ws.flash.ftw3preprod1上的基准测试结果。测试数据库的生成使用rocks_db_bench的fillseqdeterministic生成buck-out/opt/gen/rocks/tools/rocks_db_bench —db/rocks_db_team/prefix_scan \ —env_uriws://ws.flash.ftw3preprod1 -logtostderrfalse \ -benchmarksfillseqdeterministic -key_size32 -value_size512 -num5000000 \ -num_levels4 -multiread_batchedtrue -use_direct_readsfalse \ -adaptive_readaheadtrue -threads1 -cache_size10485760000 -async_iofalse \ -multiread_stride40000 -disable_auto_compactionstrue -compaction_style1 \ -bloom_bits10生成的数据库结构共 4 个 levelLevel[0]: /000233.sst(size: 24828520 bytes) Level[0]: /000232.sst(size: 49874113 bytes) Level[0]: /000231.sst(size: 100243447 bytes) Level[0]: /000230.sst(size: 201507232 bytes) Level[1]: /000224.sst - /000229.sst(total size: 405046844 bytes) Level[2]: /000211.sst - /000223.sst(total size: 814190051 bytes) Level[3]: /000188.sst - /000210.sst(total size: 1515327216 bytes)MultiGet 基准MultiGet 测试命令multireadrandombatch_size84 线程async_iotruebuck-out/opt/gen/rocks/tools/rocks_db_bench -use_existing_dbtrue \ —db/rocks_db_team/prefix_scan -benchmarksmultireadrandom -key_size32 \ -value_size512 -num5000000 -batch_size8 -multiread_batchedtrue \ -use_direct_readsfalse -duration60 -ops_between_duration_checks1 \ -readonlytrue -threads4 -cache_size300000000 -async_iotrue \ -multiread_stride40000 -statistics —env_uriws://ws.flash.ftw3preprod1 \ -logtostderrfalse -adaptive_readaheadtrue -bloom_bits10场景配置平均延迟micros/op吞吐ops/secSingle-file基线默认实现一次读一个文件1291.9923095Single-levelasync_iotrue且optimize_multiget_for_iofalse774.5875163Multi-level所有优化全部开启507.5337881对应rocksdb.db.multiget.micros百分位P50/P95/P99数据Single-fileP50 9664.42 / P95 20757.10 / P99 29329.44 / P100 46162.00Single-levelP50 6029.60 / P95 10727.47 / P99 13986.68 / P100 47466.00Multi-levelP50 3923.82 / P95 7356.18 / P99 10880.73 / P100 28511.00可见从 Single-file 到 Single-level延迟下降约 40%再到 Multi-level延迟再降约 34%相对基线整体下降约 60%吞吐提升约 2.5 倍。Scan 基准Scan 测试使用seekrandom并设置-seek_nexts65536每次 Seek 后连续 Next 65536 次模拟长扫描buck-out/opt/gen/rocks/tools/rocks_db_bench -use_existing_dbtrue \ —db/rocks_db_team/prefix_scan -benchmarksseekrandom -key_size32 \ -value_size512 -num5000000 -batch_size8 -multiread_batchedtrue \ -use_direct_readsfalse -duration60 -ops_between_duration_checks1 \ -readonlytrue -threads4 -cache_size300000000 -async_iotrue \ -multiread_stride40000 -statistics —env_uriws://ws.flash.ftw3preprod1 \ -logtostderrfalse -adaptive_readaheadtrue -bloom_bits10 -seek_nexts65536场景平均延迟micros/op吞吐ops/sec带宽开启异步扫描async scan414442.3039326.2 MB/s关闭异步扫描848858.6694158.1 MB/s开启异步扫描后seekrandom 延迟下降约 51%带宽翻倍。说明以上所有数字均来自原文档在特定测试环境远端闪存ws://ws.flash.ftw3preprod1、4 线程、batch_size8下的实测输出性能增益高度依赖存储介质与工作负载特征实际部署请以自身环境的基准测试为准。已知限制与适用前提文档明确列出以下限制使用前务必对照仅适用于 block-based table SSTPlain Table、Cuckoo Table 等其它表格式不受益。文件系统必须支持ReadAsync与Poll接口目前仅PosixFileSystem提供支持底层依赖 io_uring见 env/io_posix.cc其他文件系统将回退到同步路径。MultiGet 异步 IO 的额外限制依赖 folly引入额外的构建步骤更高的 CPU 开销由于协程MultiGet 的 CPU 开销可能上升6%15%。最坏情况是单线程、批量内每个文件仅 1 个 key 重叠且 100% 缓存命中更现实的多线程场景每文件约 4 个 key 重叠CPU 利用率约上升 6%元数据读取不并行元数据读仍会阻塞线程部分场景仍为串行例如 merge 操作数的额外 block 读取。async_io的适用语义源码注释明确其为顺序读取 内部自动预取路径上的优化include/rocksdb/options.h随机点查为主的负载收益有限。如何在实际项目中启用结合ReadOptions声明include/rocksdb/options.h启用方式如下C 示例rocksdb::ReadOptions read_options; // 开启异步 IO作用于 Iterator 扫描与 MultiGet read_options.async_io true; // MultiGet 跨 level 并行默认 true如 CPU 敏感可关闭 read_options.optimize_multiget_for_io true; // 对迭代器生效 auto* it db-NewIterator(read_options); for (it-Seek(start_key); it-Valid(); it-Next()) { // 扫描逻辑 } // 对 MultiGet 生效 std::vectorrocksdb::Slice keys {...}; std::vectorstd::string values; std::vectorrocksdb::Status statuses db-MultiGet(read_options, keys, values);C API 对应写法include/rocksdb/c.hrocksdb_readoptions_t* ropts rocksdb_readoptions_create(); rocksdb_readoptions_set_async_io(ropts, 1); rocksdb_readoptions_set_optimize_multiget_for_io(ropts, 1);实践建议存储介质延迟越高收益越明显文档实测针对远端闪存本地 NVMe 上收益相对有限CPU 敏感场景可将optimize_multiget_for_io置为false保留 single-level 并行换取更低 CPU 开销构建 MultiGet 异步优化需要 folly 依赖若无法引入MultiGet 将退回同步/MultiRead 路径迭代器扫描长顺序扫描大seek_nexts或长 Next 序列场景最能体现双缓冲异步预取的价值。深入阅读原始博客文档docs/_posts/2022-10-07-asynchronous-io-in-rocksdb.markdownReadOptions选项定义include/rocksdb/options.h文件系统异步接口ReadAsync/Pollinclude/rocksdb/file_system.hio_uring 实现env/io_posix.cc双缓冲异步预取file/file_prefetch_buffer.hSeek 两阶段异步化table/block_based/block_based_table_iterator.ccMultiGet 协程版本table/block_based/block_based_table_reader.cc、table/block_based/block_based_table_reader_sync_and_async.h【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表