ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RocksDB Secondary 实例 `NewIterators()` 一致性视图修复解析:并发 `TryCatchUpWithPrimary()` 下的多列族迭代器语义

RocksDB Secondary 实例 `NewIterators()` 一致性视图修复解析:并发 `TryCatchUpWithPrimary()` 下的多列族迭代器语义 RocksDB Secondary 实例NewIterators()一致性视图修复解析并发TryCatchUpWithPrimary()下的多列族迭代器语义【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读RocksDB 的 Secondary 实例DBImplSecondary通过重放 MANIFEST 与 WAL 追赶主实例Primary而NewIterators()需要为多个列族一次性创建一组迭代器。本篇文章围绕unreleased_history/bug_fixes/secondary_new_iterators_consistent_view.md记录的缺陷展开当TryCatchUpWithPrimary()与NewIterators()并发执行时后者可能返回来自不同数据库状态的迭代器。通过阅读源码与测试你将理解 Secondary 实例的快照语义、该缺陷的根因以及修复后如何保证一组迭代器始终锚定在同一序列号Sequence Number上的一致性视图。缺陷记录原文该条目位于仓库 unreleased_history/bug_fixes/secondary_new_iterators_consistent_view.md全文仅一句话Fixed secondary DBNewIterators()returning iterators from different database states whenTryCatchUpWithPrimary()runs concurrently.其核心语义是修复了 Secondary DB 的NewIterators()在TryCatchUpWithPrimary()并发运行时返回来自不同数据库状态的迭代器的问题。要理解这句话需要先厘清 Secondary 实例的运行机制再深入NewIterators()与TryCatchUpWithPrimary()的实现。背景Secondary 实例与追赶机制Secondary 实例是 RocksDB 支持一写多读部署形态的机制它与主实例共享同一份存储目录只读地重放主实例产生的 MANIFEST 与 WAL从而在不阻塞主实例写入的前提下提供接近实时的读取。相关说明见 db/db_impl/db_impl_secondary.hThe secondary instance shares access to the storage as the primary. The secondary is able to read and replay changes described in both the MANIFEST and the WAL files without coordination with the primary. The secondary instance can be opened usingDB::OpenAsSecondary. After that, it can callDBImplSecondary::TryCatchUpWithPrimaryto make best effort attempts to catch up with the primary.Secondary 实例的关键操作是TryCatchUpWithPrimary()实现在 db/db_impl/db_impl_secondary.cc在mutex_保护下通过ReactiveVersionSet::ReadAndApply()读取 MANIFEST 的新记录并应用到本地版本集VersionSet调用FindAndRecoverLogFiles()扫描 WAL 目录发现并重放主实例新产生的 WAL遍历所有列族处理封存seal的 memtable、版本安装、文件清理等后续工作。由于TryCatchUpWithPrimary()由应用按需触发called as often as the application chooses它天然可能与并发的读操作如NewIterators()竞争二者共享同一把mutex_。缺陷根因多列族迭代器可能锚定不同数据库状态多列族迭代器的正确语义DB::NewIterators()接收一组列族句柄期望返回一组迭代器。对于这组迭代器合理的语义是它们应当共同呈现数据库在某个时刻的一致视图——即所有迭代器看到的数据应该来自同一个版本状态。普通Primary实例中这一语义由DBImpl::MultiCFSnapshot()保证见 db/db_impl/db_impl.cc。该函数在一次调用中在锁内为所有列族统一获取 SuperVersion 引用记录一个统一的序列号作为快照点若序列号在收集过程中发生变化则重试直到所有列族的引用与序列号来自同一轮状态。注释中也明确了这一点db/db_impl/db_impl.ccMultiCFSnapshot fixes one read sequence number for every key即为所有读取固定一个序列号。Secondary 中的旧实现缺陷从当前源码看DBImplSecondary::NewIterators()的实现位于 db/db_impl/db_impl_secondary.cc。其关键路径是autovectorColumnFamilySuperVersionPair, MultiGetContext::MAX_BATCH_SIZE cf_sv_pairs; SequenceNumber consistent_seqnum; { InstrumentedMutexLock lock_guard(mutex_); consistent_seqnum versions_-LastSequence(); for (auto* cf : column_families) { auto* cfh static_cast_with_checkColumnFamilyHandleImpl(cf); cf_sv_pairs.emplace_back(cfh, cfh-cfd()-GetSuperVersion()-Ref()); } }这段代码在锁内一次性完成两件事记录统一的序列号consistent_seqnum versions_-LastSequence()为每个列族引用当前 SuperVersion。随后在锁外对所有列族使用同一个consistent_seqnum调用NewIteratorImpl()创建迭代器for (const auto cf_sv_pair : cf_sv_pairs) { iterators-push_back(NewIteratorImpl(read_options, cf_sv_pair.cfh, cf_sv_pair.super_version, consistent_seqnum, read_callback)); TEST_SYNC_POINT(DBImplSecondary::NewIterators:AfterCreateIterator); }也就是说修复后的实现在持锁期间统一采样LastSequence()并批量引用所有列族的 SuperVersion使这组迭代器共享同一个快照序列号从而保证它们来自同一数据库状态。NewIteratorImpl()中snapshot kMaxSequenceNumber时回退到versions_-LastSequence()见 db/db_impl/db_impl_secondary.cc而这里显式传入统一的consistent_seqnum正是为了避免每个迭代器各自去采样LastSequence()。从修复逻辑可以推断旧实现的缺陷形态如果实现为在锁内逐列族获取LastSequence()并创建迭代器那么当TryCatchUpWithPrimary()在同一把锁上交错执行时——比如在第一个列族的迭代器创建完成之后、第二个列族创建之前抢占了锁并安装了新的版本、推进了LastSequence()——后续列族的迭代器就会采样到更新后的序列号与 SuperVersion导致同一组迭代器中一部分来自旧状态、一部分来自新状态。这种不一致视图在跨列族扫描、关联读取等场景下会造成数据错乱。修复后的机制解析统一快照点consistent_seqnum修复的核心是引入consistent_seqnum变量在锁内一次性确定后所有迭代器共享该序列号。这样即便TryCatchUpWithPrimary()在锁外阶段完成追赶也不会影响已创建的迭代器——它们锚定在采样时刻的视图上语义与 Primary 实例的MultiCFSnapshot()保持一致。值得注意的边界行为NewIterators()在 Secondary 模式下不支持 snapshot、tailing iterator 与kPersistedTier读取层级均显式返回Status::NotSupporteddb/db_impl/db_impl_secondary.cc因此consistent_seqnum就是迭代器唯一的快照来源若ReadOptions携带 timestamp则会在采样后对所有列族执行FailIfReadCollapsedHistory()检查防止读取已折叠collapsed的历史数据失败时统一释放所有已引用的 SuperVersion。与TryCatchUpWithPrimary()的并发协调TryCatchUpWithPrimary()与NewIterators()共享mutex_。追赶过程在锁内完成 MANIFEST 读取与 WAL 重放NewIterators()在锁内完成采样与 SuperVersion 引用。二者的互斥保证了采样与版本安装的原子性——这正是修复后一致性得以成立的关键要么TryCatchUpWithPrimary()先执行完迭代器看到的是追赶后的新状态要么NewIterators()先完成采样迭代器看到的是追赶前的旧状态绝不可能出现一部分列族看到旧状态、一部分列族看到新状态的中间态。测试验证NewIteratorsConsistentViewDuringCatchUp该缺陷的回归测试位于 db/db_secondary_test.cc测试名NewIteratorsConsistentViewDuringCatchUp直接对应本次修复。测试的编排值得仔细阅读TEST_F(DBSecondaryTest, NewIteratorsConsistentViewDuringCatchUp) { Options options; options.env env_; options.disable_auto_compactions true; CreateAndReopenWithCF({cf1}, options); ASSERT_OK(Put(0, key, old)); ASSERT_OK(Put(1, key, old)); ASSERT_OK(Flush(0)); ASSERT_OK(Flush(1)); ...测试流程构造初始状态在默认列族与cf1中分别写入key - old并 Flush让 Secondary 打开时看到old值在主实例制造新状态使用disableWAL true写入key - newFlush 后执行CompactRange()。之所以disableWAL是为了确保新数据只出现在新版本的 SST 文件中、而不出现在 WAL 中从而让 Secondary 的 WAL 重放路径无法直接看到新值注入并发追赶通过SyncPoint在DBImplSecondary::NewIterators:AfterCreateIterator与DBImpl::MultiCFSnapshot::AfterRefSV两个回调点触发TryCatchUpWithPrimary()。AfterCreateIterator意味着追赶恰好发生在第一个迭代器创建之后——这正是旧实现下最容易暴露缺陷的时序断言一致性调用NewIterators()得到 2 个迭代器逐个Seek(key)并断言值均为old。与此同时VerifySecondaryValue()断言 Secondary 的Get路径已经能看到new。这个测试的精妙之处在于追赶发生在迭代器创建中途但断言要求所有迭代器都返回old追赶前的状态。这验证了修复后的语义——整组迭代器要么统一锚定在旧视图要么统一锚定在新视图绝不允许混搭。在修复前AfterCreateIterator回调点插入的追赶会推进LastSequence()使后续列族的迭代器读到new测试将失败。测试还体现了 RocksDB 使用SyncPoint制造确定性竞态的一贯手法通过SetCallBackEnableProcessing在指定代码点插入用户回调构造出真实并发中难以稳定复现的时序窗口。从本修复看 Secondary 读取的一致性设计本次修复与 Primary 实例的多键读取一致性设计一脉相承读取路径一致性机制实现位置单键Get单次 SuperVersion 引用 单序列号DBImpl::GetImpl()系列多键MultiGetMultiCFSnapshot统一快照必要时重试db/db_impl/db_impl.cc 附近多列族NewIteratorsPrimaryMultiCFSnapshot统一快照db/db_impl/db_impl.cc多列族NewIteratorsSecondary锁内统一采样LastSequence() 批量引用 SuperVersiondb/db_impl/db_impl_secondary.cc可以看出RocksDB 对一批读取共享一致视图的保障原则是统一的在互斥锁内一次性完成版本引用与序列号采样随后在锁外基于已固定的快照构建读取对象。Secondary 的NewIterators()修复正是把这一原则落实到了追赶并发场景。实践建议使用场景若你的应用通过DB::OpenAsSecondary打开只读副本并依赖NewIterators()批量扫描多个列族本修复保证了一次调用返回的迭代器视图一致可用于跨列族关联查询迭代器生命周期Secondary 模式不支持ReadOptions::snapshot与 tailing iterator源码中显式返回NotSupported迭代器的一致性边界由创建时刻确定如需刷新视图应重新调用NewIterators()或配合TryCatchUpWithPrimary()完成追赶升级验证升级到包含本修复的版本后可参考 db/db_secondary_test.cc 中的NewIteratorsConsistentViewDuringCatchUp测试逻辑在自己的并发场景下追赶与批量迭代并发做一次回归确认行为符合预期日志观测TryCatchUpWithPrimary()每轮会输出Last sequence is ...信息日志db/db_impl/db_impl_secondary.cc可据此观察追赶进度辅助判断迭代器锚定视图的时机。小结secondary_new_iterators_consistent_view.md记录的是一个典型的一致性竞态缺陷Secondary 实例的NewIterators()在TryCatchUpWithPrimary()并发追赶时可能为同一组迭代器采样到不同的序列号与 SuperVersion。修复通过在锁内统一采样LastSequence()、批量引用各列族 SuperVersion并将同一consistent_seqnum传递给所有迭代器使多列族迭代器锚定在同一数据库视图上配套的NewIteratorsConsistentViewDuringCatchUp测试用 SyncPoint 精确复现竞态时序验证了新旧视图不可混搭的语义。这一修复使 Secondary 实例的批量读取语义与 Primary 实例的MultiCFSnapshot()对齐为依赖一写多读架构的应用提供了可靠的一致性保证。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表