多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RocksDB 协程读取统计隔离修复解析:TLS 统计状态在异步读与协程读之间的正确隔离

RocksDB 协程读取统计隔离修复解析:TLS 统计状态在异步读与协程读之间的正确隔离 RocksDB 协程读取统计隔离修复解析TLS 统计状态在异步读与协程读之间的正确隔离【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读本文围绕 RocksDB 仓库中 unreleased_history/bug_fixes/coroutine_stats_request_isolation.md 记录的缺陷修复展开当关闭统计的协程读与异步读与开启统计的读请求在同一执行器executor上交错执行时前者会错误继承后者通过线程局部存储TLS暴露的统计状态。本文将从缺陷成因、修复方案按调用捕获配置 请求上下文隔离计数、核心源码实现到测试验证进行完整剖析帮助读者理解 RocksDB 在CoroDB::CoGet、DB::GetAsync/MultiGetAsync路径上如何保证每次读取的统计配置与计数彼此独立。背景RocksDB 的协程读与异步读RocksDB 在启用USE_COROUTINES宏依赖 folly coroutine 支持参见 util/coro_utils.h 中的DECLARE_SYNC_AND_ASYNC*系列宏与CO_AWAIT/CO_RETURN宏时可以走基于 C20 协程的读取路径把一次读取的多个 IO 阶段调度到 IO 线程执行器上从而以更低的线程切换成本实现异步化。仓库中主要的协程读入口集中在 db/coro_db.ccCoroDB::CoGet覆盖DB*与SstFileReader*两种对象异步读入口则位于 db/db_impl/db_impl.cc 的DB::GetAsync与DB::MultiGetAsync。RocksDB 的运行时统计分为两大类PerfContext基于PerfLevel定义于 include/rocksdb/perf_level.h从kDisable 1、kEnableCount 2、kEnableWait 3、kEnableTimeExceptForMutex 4、kEnableTimeAndCPUTimeExceptForMutex 5到kEnableTime 6逐级递增门控记录 block 读取次数、等待耗时、CPU 耗时等IOStatsContext通过disable_iostats标志门控记录bytes_read、cpu_read_nanos、按温度temperature分类的文件 IO 统计等相关访问宏见 monitoring/iostats_context_imp.h 中的IOSTATS_ADD等宏内部先判断if (!iostats_context.disable_iostats)再累加。这两类上下文都以thread_local形式存放参见 monitoring/iostats_context_imp.h 中extern thread_local IOStatsContext iostats_context;也就是说统计状态天然绑定到当前线程。对于普通的同步读一个线程同一时刻只执行一次读取TLS 状态不会串扰但协程读会在执行器线程上挂起suspend与恢复resume多个逻辑请求在同一线程上交错执行这就为统计状态的串台埋下了隐患。缺陷本身TLS 统计状态在交错执行时被错误继承修复条目原文指出Fixed stats-disabled coroutine and asynchronous reads inheriting enabled TLS statistics state when interleaved with stats-enabled reads on the same executor.在修复之前一个典型的错误场景是这样的线程 A 上的读请求 R1 开启统计PerfLevel非kDisable、disable_iostats false并在执行器线程上设置了 TLS 统计同一执行器线程上交错运行着请求 R2而 R2 本应在关闭统计的状态下执行例如用户对 R2 显式设置了PerfLevel::kDisable/disable_iostats true由于 TLS 状态是线程级的R2 在挂起/恢复时继承了 R1 留下的 TLS 统计配置导致本应关闭统计的读取也被采集了统计或者读取到了属于 R1 的计数。更隐蔽的问题是计数污染即便配置正确若两个开启统计的协程读交错执行各自在 TLS 中累加的计数也可能被对方读到造成统计结果串账。而协程可以在多个线程间迁移执行resume 到其他线程TLS 统计在迁移过程中会丢失或被覆盖进一步加剧了不确定性。修复思路按调用捕获配置 请求上下文隔离计数这次修复把统计状态从线程级 TLS 的共享心智模型重构为按请求per-call捕获并隔离的模型核心包含两个层面配置按调用捕获在进入协程读/异步读之前先把当前线程的统计配置快照进一个CoroutineStatsConfig结构体随后立即把 TLS 统计禁用而不是在执行过程中实时读 TLS计数按请求隔离开启统计的请求其计数放在与 follyRequestContext绑定的EnabledCoroutineStatsRequestData对象里在挂起/恢复时与 TLS 之间搬移save/load从而在任意时刻只暴露当前请求自己的计数。1. CoroutineStatsConfig每次调用捕获的配置快照结构体定义在 util/coro_stats_util.hstruct CoroutineStatsConfig { PerfLevel perf_level PerfLevel::kEnableCount; bool per_level_perf_context_enabled false; bool iostats_disabled false; };它一次性携带了决定统计行为的三个要素字段含义关联源码perf_level本次调用应使用的 PerfLevel 级别定义见 include/rocksdb/perf_level.hper_level_perf_context_enabled是否启用按 LSM 层级level细分的 per-level perf context由PerfContext::EnablePerLevelPerfContext()管理iostats_disabled是否禁用 IOStatsContext 统计对应IOStatsContext::disable_iostats见 monitoring/iostats_context_imp.h配套的捕获函数也定义在 util/coro_stats_util.hCaptureCoroutineStatsConfig()仅快照当前 TLS 配置不改动 TLS 状态CaptureAndDisableCoroutineStatsConfig()先快照配置然后立即禁用 TLS 统计DisableCoroutineStatsInTLS()会把disable_iostats置 true 并把PerfLevel设为kDisable。这样在协程读真正开始执行前线程级 TLS 就不再有残留的开启态可供别的请求继承IsCoroutineStatsEnabled()判断捕获到的配置是否真的需要采集统计perf_level ! kDisable或!iostats_disabled用于决定是否需要走计数隔离路径。2. 调用链上的标准使用模式在 db/coro_db.cc 的CoroDB::CoGet中可以看到这套模式的标准用法CoroutineStatsConfig stats_config CaptureAndDisableCoroutineStatsConfig(); if (coro_db nullptr || read_event_base nullptr) { // 降级为同步路径同样用捕获的配置包一层 CoroutineStatsContextScope stats_scope(std::move(stats_config), db-GetEnv()); status db-Get(options, column_family, key, value, timestamp); } else { auto task [](CoroutineStatsConfig task_stats_config, Env* task_env, ...) { CoroutineStatsContextScope stats_scope(std::move(task_stats_config), task_env); co_return co_await folly::coro::co_nothrow( task_db-GetCoroutine(...)); }(...); status co_await folly::coro::co_nothrow(folly::coro::co_withExecutor( folly::Executor::getKeepAliveToken(read_event_base), std::move(task))); }即配置在调用方线程上捕获随任务闭包迁移到读执行器由CoroutineStatsContextScope在任务体内安装。无论走同步降级还是协程路径捕获到的都是发起调用那一刻的配置不再受执行器线程上其他请求影响。同样的模式也出现在 db/db_impl/db_impl.cc 的DB::GetAsync第 8119 行起与DB::MultiGetAsync第 8164 行起中CaptureAndDisableCoroutineStatsConfig()后把CoroutineStatsConfig传入在read_event_base上启动的任务任务内部再以CoroutineStatsContextScope包裹GetCoroutine/MultiGetCoroutine调用而在非协程降级路径上则使用AsyncReadStatsScope同文件第 8101 行配合ThreadLocalStatsEnabledForAsyncRead()判断是否需要重置/采集 TLS 统计两条路径的统计口径在修复后保持一致。3. CoroutineStatsContextScope挂起/恢复时的计数搬移核心类CoroutineStatsContextScope定义在 util/coro_stats_util.h实现于 util/coro_stats_util.cc。它的职责是在作用域内把本请求的统计配置与计数安装到 TLS并在挂起/恢复、离开作用域时正确搬移与清理。其内部依赖 folly 的RequestContext/RequestData机制开启统计的请求会创建一个EnabledCoroutineStatsRequestData继承自folly::RequestData通过folly::ShallowCopyRequestContextScopeGuard挂到请求上下文中。该类拥有自己的PerfContext perf_context_与IOStatsContext iostats_context_副本并在onSet()请求上下文被恢复/进入仅当当前线程是创建线程IsOwnerThread()通过env_-GetThreadID()与owner_thread_id_比对时才把私有计数搬回 TLSLoadThreadLocalStats()、安装捕获的配置InstallCoroutineStatsConfigToTLS()并启动 CPU 计时onUnset()请求上下文被挂起/离开同样仅在创建线程上执行停止 CPU 计时、把 TLS 计数搬回私有对象SaveThreadLocalStats()并把 TLS 统计禁用DisableCoroutineStatsInTLS()。头文件注释对此有非常明确的约定见 util/coro_stats_util.hInstalls the captured stats configuration for one coroutine call. Enabled calls preserve their counters across suspensions and publish them on exit. Request-context restores on other threads are ignored because the collected stats remain owned by the creating thread. All calls leave TLS stats collection disabled.这段话点出了三个关键设计决策启用统计的调用在挂起期间保留自己的计数恢复时继续累加退出时统一发布publish在其他线程上的请求上下文恢复被忽略——因为统计归属于创建线程协程迁移到别的线程执行时不把计数搬到那个线程的 TLS 上避免串账与丢失所有调用在离开时都把 TLS 统计采集置为禁用——这样无论下一个在该线程上执行的请求是否开启统计都不会继承前一个请求的开启态从根上消除了本次修复针对的继承问题。CoroutineStatsContextScope还通过assert(GetCoroutineStatsData() nullptr)NDEBUG 之外启用强制不允许嵌套保证同一时刻一个线程上只有一个活动的统计请求上下文析构时同样有对应断言确保配对清理。CPU 计时get_cpu_nanos只在PerfLevel kEnableTimeAndCPUTimeExceptForMutex时启动StartGetCpuTimer()内的判断并且把请求上下文未安装的时间段排除在本次请求的 CPU 统计之外见测试CoroutineStatsContextScopeCollectsGetCpuNanos用folly::RequestContextScopeGuard模拟 unset 期间的 CPU 消耗不应被计入。测试验证交错执行下的隔离性仓库在 db/perf_context_test.ccPerfContextTest测试套件仅USE_COROUTINES下编译中为本次修复提供了直接的回归测试。其中CoroutineStatsContextsRemainIsolatedWhenInterleaved第 175 行起完整复现了交错场景构造三个配置disabledperf_level kDisable且iostats_disabled true、first开启计数、second开启计数但disable_iostats true等不同组合对应文档中关闭统计的读与开启统计的读两类请求测试线程预先在 TLS 中塞入脏数据block_read_count 100、bytes_read 200模拟上一个请求残留的 TLS 状态通过folly::coro::collectAll让三个make_task交错在同一个ManualExecutor上执行每个任务内部在CoroutineStatsContextScope作用域内验证安装的PerfLevel、per_level_perf_context_enabled、disable_iostats与自己捕获的配置完全一致而不是继承 TLS 的残留值挂起co_await folly::coro::co_reschedule_on_current_executor前后配置保持一致且自己的计数在挂起期间被保留恢复后block_read_count仍等于挂起前累加的值说明计数没有混入其他请求最终断言三个任务的返回计数分别为0disabled、11110、22220互不串扰所有任务结束后ManualExecutor所在线程的 TLS 被重置为PerfLevel::kDisable且disable_iostats true验证离开后 TLS 统计采集保持禁用的清理约定。同文件中的其他相关测试还包括CoroutineStatsContextScopeCollectsStats第 98 行验证开启统计的请求在co_await挂起恢复后PerfContext含 per-level 计数、block_read_time与IOStatsContextbytes_read、cpu_read_nanos、按温度统计的计数在退出作用域后完整发布CoroutineStatsContextRestoredOnForeignThread第 281 行模拟请求上下文被saveContext()捕获后在另一个std::thread上恢复验证外部线程上的onSet()被忽略、计数仍归属创建线程创建线程上block_read_count 3、bytes_read 5保持不变CoroutineStatsContextScopeCollectsGetCpuNanos第 258 行验证 CPU 计时精确覆盖请求上下文安装期间unset 期间的 CPU 时间不计入。对使用者的意义与注意事项对 RocksDB 用户而言本次修复不改变任何公开 API而是修正了内部统计的一致性但从使用角度可以提炼出几点实践认知统计配置是调用时点的协程读/异步读的PerfLevel与iostats开关以发起读取那一刻捕获到的 TLS 配置为准。若希望某次CoGet/GetAsync关闭统计应在调用前通过SetPerfLevel(PerfLevel::kDisable)与IOSTATS_SET_DISABLE(true)设置不必担心执行器线程上其他并发请求的设置影响本次读取计数归属发起线程开启统计的协程读其PerfContext/IOStatsContext计数最终在发起请求的线程上发布创建线程退出作用域时写回 TLS即便执行期间协程被调度到其他线程运行统计也不会跟着迁移统计读取时机由于计数在请求退出CoroutineStatsContextScope即协程任务体返回后才最终发布若要在协程任务内读取统计应确保在CoroutineStatsContextScope作用域内读取如测试所示挂起恢复后、离开作用域前读取到的都是本请求的计数隔离性与嵌套限制同一线程上同时只允许一个活动的统计请求上下文嵌套会被assert拦截这是保证隔离正确性的前提也意味着不要在已经处于CoroutineStatsContextScope内的协程体中再去启动另一层统计请求上下文。小结coroutine_stats_request_isolation修复本质上是把线程级 TLS 统计在协程/异步读场景下重构为按请求捕获配置、按请求隔离计数、离开即禁用 TLS的三段式模型CaptureAndDisableCoroutineStatsConfig()负责快照并清空 TLSCoroutineStatsContextScopeEnabledCoroutineStatsRequestData负责在挂起/恢复时搬移计数并在退出时发布IsOwnerThread()保证跨线程恢复时统计不迁移。配合 db/perf_context_test.cc 中的交错、跨线程、CPU 计时三类回归测试RocksDB 确保了在同一执行器上交错执行的协程读与异步读无论统计开关如何组合都能保持配置与计数的严格隔离。相关实现细节可继续阅读 util/coro_stats_util.cc、db/coro_db.cc 与 db/db_impl/db_impl.cc。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表