
RocksDB MultiGetEntityLazy 跨键合并共享 SuperVersion Pin 与 LazyWideColumnsBatch 批量 Blob 读取优化【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读本文讲解 RocksDB 宽列Wide Column实体读取 APIDB::MultiGetEntityLazy()的一项关键性能优化整批 key 在同一个共享的 per-column-family SuperVersion pin 下完成点查并由一次LazyWideColumnsBatch::MultiResolve()调用把跨 key 的 blob 读取按物理存储位置合并成尽量少的 I/O——每个 blob 文件只读一次嵌入 SST 的 blob 则每个 SST 只读一次整列读取与字节范围byte-range读取一视同仁而不是为每个 key 的 blob 单独发起读取。读完本文你将理解该优化的适用场景、底层调用链与数据结构、批量解析的实现细节以及如何通过测试用例验证合并效果。背景从 MultiGetEntity 到 MultiGetEntityLazyRocksDB 的宽列实体wide-column entity支持一个 key 下挂多个命名列column每列的值既可以是内联字节也可以引用 blob 文件中的大对象。DB::MultiGetEntity()是批量获取实体的非惰性接口一次性把 N 个 key 的完整实体包括 blob 内容读回来返回类型为PinnableWideColumns见 include/rocksdb/db.h。DB::MultiGetEntityLazy()则是其惰性lazy变体声明于 include/rocksdb/db.h当前标注为EXPERIMENTAL and subject to change。它把 N 个 key 的实体批量读入一个LazyWideColumnsBatch其中内联列被零拷贝物化而 blob 列保持为未解析的引用unresolved reference只有当调用方显式按字节范围拉取时才真正从存储读取。与GetEntityLazy()一样使用前需要满足max_open_files -1的前提条件。优化前的问题每个 key 的 blob 各自为战在逐 key 解析per-key resolve的路径下如果一次MultiGetEntityLazy()返回的 N 个 key 各自引用 blob 文件中的大对象随后逐 key 调用ResolveColumnRange()/ResolveColumn()就会产生 N 次独立的 blob 读取即使 N 个 blob 位于同一个 blob 文件也会对该文件发起 N 次独立的Read调用嵌入embedded / same-fileblob 位于 SST 内部时同理会对同一 SST 发起 N 次读每次读取都要额外付出 seek、页缓存/直读模式下的上下文切换与调度开销并且无法充分利用底层文件系统的批量读MultiRead能力。对于批量取回一批大列、但每列往往只需要其中一小段字节的典型场景如只读头部元数据这种逐 key 的方式既浪费带宽也放大 I/O 次数。核心优化一单次调用共享一个 SuperVersion pin本性能改进的第一个要点是DB::MultiGetEntityLazy()现在对整批 key 只建立并共享一个 per-column-family 的 SuperVersion pin而不是每个 key 各持有一个。从实现看LazyWideColumnsBatch::Rep通过std::mapuint32_t /* column_family_id */, Cleanable cf_pins为每个涉及到的列族保存一个共享 pin常见单列族调用只有一个条目见 db/wide/lazy_wide_columns.cc。每个 key 的LazyWideColumns实体通过LazyWideColumnsHelper::FinalizeInBatch()绑定到该 batch 共享的 Version 上db/wide/lazy_wide_columns.cc实体自身不再持有独立 pin。这带来两点收益引用固定与生命周期保障共享 pin 确保在 batch 存活期间其引用的 blob 文件、不可变的 table reader 等资源不会被回收使延迟解析deferred read始终可解析资源开销下降相对逐 key 各自 pin批量场景下 SuperVersion 的 pin/unpin 次数从 N 次降为 1 次也减少了多次 pin 带来的引用计数与可能的旧文件清理 I/O 干扰。LazyWideColumns结果以及其外层 batch必须在其引用有效期间被使用且应在关闭 DB 之前销毁这与迭代器iterator的生命周期约束一致在 include/rocksdb/lazy_wide_columns.h 的类注释中有明确说明。核心优化二一次 MultiResolve 完成跨 key 的 blob 合并读取第二个要点是LazyWideColumnsBatch::MultiResolve()——一个 batch 级别的跨 key 批量解析 API。它接收一个LazyColumnReadRequest数组每个请求通过column指针指向所属实体列本身携带所属结果的回指指针因此不会把不同 batch 的结果混在一起读取范围由offsetlength描述其中length kLazyWholeColumnstd::numeric_limitssize_t::max()表示读到列逻辑值的末尾见 include/rocksdb/lazy_wide_columns.h。LazyWideColumnsBatch::MultiResolve()的实现db/wide/lazy_wide_columns.cc分多个 pass 完成分类 → 分组 → 合并下发 → 缓存采纳 → 切片/单独服务的流水线Pass 1分类逐请求重置输出、校验列归属null 列或不属于本 batch 的列得到InvalidArgument并调用ReadPathBlobResolver::ClassifyColumnRange()把每个读取归入以下计划之一kFetchWholeSeparateFile/kFetchWholeSameFile整列读取或需先取整列的读取按实体, 列去重后进入 whole fetch 列表kFetchRangeSeparateFile/kFetchRangeSameFile真正的字节范围读取未压缩 blob 引用上的严格子区间直接进入 range fetch 列表kServeIndividually单独服务包括无输出缓冲、无 resolver、或涉及force_verify的列/物理 blob 记录等特殊情况详见下文正确性保持。分类阶段同时收集两类必须单独服务的集合force_verify_columnsbatch 内任何一列只要有force_verify读取则该列全部走单独路径和force_verify_blobs基于PhysicalBlobId覆盖不同实体引用同一磁盘记录的重复 key 情形。Pass 2分组与合并下发把 whole/range 两类请求分别按物理位置分组独立 blob 文件引用按Version*分组每组内的请求再交给Version::MultiGetBlobLazy()db/version_set.cc 起按blob file number聚成每文件一批最终每个文件对应一次合并的MultiRead嵌入same-file引用按SameFileBlobReader*即按SST分组交给SameFileBlobReader::MultiGetSameFileBlob()每个 SST 对应一次合并的MultiRead。于是无论整列读取还是字节范围读取独立 blob 文件场景下每个 blob 文件只有一次读嵌入场景下每个 SST 只有一次读这正是 release note 所描述的合并粒度。字节范围请求把range_offset/range_length原样带下只读取需要的字节。Pass 3缓存采纳成功的 whole fetch 通过AdoptResolvedWholeColumn()写入所属实体的 resolver 缓存使后续对该列的读取包括本 batch 内的其他读取、以及之后的重复解析不再产生 I/O。Pass 4切片每个绑定到 whole fetch 的读取从已缓存整列值中切片出请求的字节范围。Pass 5单独服务对无法合并的读取逐一执行ResolveOneRead()。值得注意合并读取本身在“一个组”内通过底层MultiRead一次下发但分组上限受MultiGetContext::MAX_BATCH_SIZE约束——Version::MultiGetBlobLazy()在单文件请求超过上限时会另起新一批db/version_set.cc这是硬性要求而非可选优化BlobFileReader::MultiGetBlob[Range]断言单批不超过上限且BlobSource用 64 位掩码跟踪一组的缓存命中。因此“每个 blob 文件一次读”在请求数量未超上限时成立。整列读取与字节范围读取的统一与差异该优化的一个亮点是整列读取与字节范围读取共用同一条合并路径但两者的底层语义略有差异整列读取whole-column走BlobReadRequest读取完整 blob受ReadOptions::verify_checksums控制是否校验成功后把整列缓存进实体 resolver之后同一列的任意读取整列或子区间都命中缓存、零 I/O。字节范围读取partial/range仅对未压缩 blob 引用无论独立 blob 文件还是嵌入 SST生效走BlobRangeReadRequest只取请求的[offset, offsetlength)字节不填充 blob cache且默认跳过整条记录级别的校验和验证因为子区间无法覆盖整条记录。压缩引用、整列读取、已缓存读取或force_verify的情形则退化为解析整列再切片。LazyColumnReadRequest::force_verify是优先校验和而非 I/O 效率的额外开关include/rocksdb/lazy_wide_columns.h即使ReadOptions::verify_checksums关闭设置它也会读取并校验尽可能多的字节当前即整条记录。LazyWideColumns::MultiResolve()是单实体的同类批量 APIinclude/rocksdb/lazy_wide_columns.hResolveColumnRange()/ResolveColumn()则是它的单条目 sugar底层都汇入同一条可优化的批量代码路径db/wide/lazy_wide_columns.cc。正确性保持force_verify 与缓存语义合并读取会改变先读谁的顺序因此实现必须保证合并不会绕过校验语义。代码中针对性地做了两件事db/wide/lazy_wide_columns.cc任何一列只要在 batch 内出现force_verify读取该列的全部读取都走单独路径——否则一个未校验的合并整列读取可能先把字节采纳进 resolver 缓存随后的force_verify读取命中缓存而跳过校验通过PhysicalBlobId独立文件场景为(Version*, file_number, offset)嵌入场景为(SameFileBlobReader*, 0, offset)识别同一磁盘记录即使force_verify读取来自另一个实体如 batch 内的重复 key也能把相关读取隔离到单独路径避免未校验合并读取先把共享 blob cache 填满导致该校验的读取命中缓存而跳过校验。此外整个解析过程在LazyResolveThreadOpScope作用域内把线程操作标记为OP_LAZY_RESOLVEdb/wide/lazy_wide_columns.cc延迟读取统一携带Env::IOActivity::kLazyResolve区别于发起点查时的kMultiGetEntity保证 db_stress 对 io_activity 的断言与 ThreadStatus 归属一致。用法示例// 伪代码风格示例结合 db/wide/db_lazy_entity_test.cc 中的用法 std::vectorSlice keys {...}; // 同一列族的 N 个 key LazyWideColumnsBatch batch; std::vectorStatus get_statuses(keys.size()); db-MultiGetEntityLazy(ReadOptions(), cf, keys.size(), keys.data(), batch, get_statuses.data()); std::vectorPinnableSlice results(keys.size()); std::vectorStatus statuses(keys.size()); std::vectorLazyColumnReadRequest reads(keys.size()); for (size_t i 0; i keys.size(); i) { reads[i].column batch[i][1]; // 实体 i 的data列引用其所属结果 reads[i].offset 0; // 字节范围起点 reads[i].length kRangeLen; // 长度或 kLazyWholeColumn 读整列 reads[i].result results[i]; reads[i].status statuses[i]; // 每个读取的独立状态不能为 null } ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));API 细节约束LazyColumnReadRequest::status必须非空否则整个MultiResolve以InvalidArgument失败batch 及其包含的实体仅在 batch 存活期间有效读取前通过operator[]拿到的列引用在 batch 被 move、Reset()或销毁后失效ReadTier::kBlockCacheTier下缓存未命中会得到Incomplete状态include/rocksdb/lazy_wide_columns.h。测试验证合并效果有据可查db/wide/db_lazy_entity_test.cc 中有一组专门验证跨 key 合并的测试测试用BlobReadIOActivityFS包装文件系统统计 blob 文件与 SST 上的Read/MultiRead次数BatchCoalescesWholeColumnSeparateFileReads3 个 key 的整列读取在同一 blob 文件上合并为1 次 MultiReadblob_read_count() 0且合并读取已缓存再次解析同列零 I/Odb/wide/db_lazy_entity_test.ccBatchCoalescesRangeSeparateFileReads3 个 key 的 100 字节子区间读取同样合并为 1 次 MultiRead验证LazyPartialBytesSaved N * (value_size - range_len)且 partial 读取不填充 blob cachedb/wide/db_lazy_entity_test.ccBatchCoalescesEmbeddedReads/BatchCoalescesEmbeddedReadsOutOfOrder嵌入 blob 的跨 key 读取在单个 SST 上合并为 1 次 MultiRead后者回归验证乱序偏移输入读取按 key 逆序下发仍能正确合并db/wide/db_lazy_entity_test.ccBatchManyKeysInOneBlobFileExceedsBatchLimit单 blob 文件请求数超过MAX_BATCH_SIZE时按上限分批BatchForceVerifyNotSkippedByCoalescedRead/BatchForceVerifyNotSkippedAcrossDuplicateKeys验证force_verify语义在合并路径下不被破坏db/wide/db_lazy_entity_test.cc。这些测试通过断言N 次逐 key 读取变成了1 次合并 MultiRead把 release note 中的性能结论落实为可自动验证的行为契约。适用范围与注意事项本优化只作用于lazy 批量路径MultiGetEntityLazyLazyWideColumnsBatch::MultiResolve逐 key 的GetEntityLazy单实体解析仍走LazyWideColumns::MultiResolve合并对象是同一实体内的多个列区间。合并按物理位置进行独立 blob 文件按文件号聚合嵌入 blob 按SST聚合。跨文件、跨 SST 的读取不会合并这本身就是正确的——它们物理上不可能合并成一次读。该 API 为实验性接口与行为可能随版本演进调整应用前请以当前仓库 include/rocksdb/lazy_wide_columns.h 与 include/rocksdb/db.h 的声明为准。与性能相关的统计可参考LazyPartialBytesSaved等 lazy 解析统计项测试中通过options.statistics断言便于在真实 workload 中量化节省的读字节数。小结MultiGetEntityLazy()LazyWideColumnsBatch::MultiResolve()的组合把批量惰性宽列读取从逐 key 的多次小 I/O 变为按物理文件聚合的合并批量读共享 SuperVersion pin 降低生命周期管理开销一次MultiResolve内完成分类、分组、合并下发、缓存采纳与切片最终达到每个 blob 文件一次读、每个 SST 一次读的合并粒度且整列读取与字节范围读取统一受益同时通过force_verify隔离机制和 io_activity 归属保证了与逐 key 路径一致的正确性语义并有专门的测试用例对合并行为进行自动化验证。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考