一条迷你压力测试揪出 CPU 硬件 Bug:RocksDB 唯一 ID 生成与 RDSEED 缺陷的排查实录
2026/9/19 14:09:56 网站建设 项目流程

一条迷你压力测试揪出 CPU 硬件 Bug:RocksDB 唯一 ID 生成与 RDSEED 缺陷的排查实录

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

本篇技术文章完整还原了 RocksDB 开发史上一次罕见的"测试反杀硬件"事件:一条四年前为验证随机数质量而编写的多线程迷你压力测试,在四年平安运行后突然连续失败,最终牵出一个被评定为高危 CVE 的 CPU 硬件缺陷——RDSEED 指令在特定微架构条件下高频返回 0 却报告"成功"。读完本文,你将理解 RocksDB 为 SST 文件生成全局唯一标识的完整设计(熵源组合、准随机方案、冗余校验),掌握"信任但验证"的测试方法论,以及从偶发失败到根因定位的完整排查路径。

背景:SST 文件为何需要"自己的"唯一标识

RocksDB 大约在四年前为 SST 文件引入了唯一标识(Unique ID,对应 PR #9126),其核心动机是为跨文件系统的缓存场景提供稳定、可复现的文件身份。此前 RocksDB 依赖操作系统文件系统提供的文件唯一性保证,但部分文件系统只保证"在现存文件之间唯一",并不保证"在近期历史上所有文件之间唯一"(详见当时社区 issue #7405 的讨论)。一旦文件系统复用旧 inode 或标识,缓存键就可能发生碰撞,导致缓存命中错误数据。

这背后是一种"伟大张力"(great tension):复用现有方案 vs. 代码自给自足。RocksDB 团队既不想重复造轮子,也不愿受制于他人实现的缺陷或变动中的需求。最终结论很明确:不能把正确性押注在"所有可能遇到的文件系统都能提供高质量唯一标识"这一假设上。

仓库中的落地实现

在当前仓库中,这一设计凝结在 table/unique_id.cc 中。核心函数GetSstInternalUniqueId(table/unique_id.cc#L59-L120)将三部分信息组合成内部唯一 ID:

  • db_id:数据库级别的标识(120+ 位熵,通常为 RFC 4122 UUID);
  • db_session_id:进程生命周期内的会话标识,20 个 base-36 字符、约 103 位熵;
  • file_number:SST 文件编号。

组合方式非常讲究:session_lower被完整保留以保证同一进程内生成的 ID 必然唯一;session_upper(约 39 位熵)与db_idHash2x64哈希后提供极高的全局唯一性熵;最后再异或file_number保证"同一会话、同一 DB 下按文件号必然唯一"(table/unique_id.cc#L92-L117)。会话 ID 的编解码由EncodeSessionId/DecodeSessionId实现(table/unique_id.cc#L15-L57),并提供了人类可读的十六进制展示函数UniqueIdToHumanString

这一内部唯一 ID 在 cache/cache_key.cc 中被用来构造缓存键(cache key),其头部注释详细分析了会话 ID 位宽、碰撞概率与缓存键覆盖范围的数学关系——这正是"唯一 ID 必须可靠"之所以性命攸关的直接原因。

如果你熟悉大随机数(例如 128 位),自然会认同:让每个文件持久化一个随机(或准随机,quasi-random)标识,比把正确性寄托在 OS 文件系统的一个次要特性上更安全、更可预测。准随机方案在理论上已被形式化——核心思想是"非结构化随机间隔 + 结构化内部计数器"的组合,使 N 个生成器各产生 M 个 ID 时的首次碰撞期望点从完全随机方案的n * m = 2^64提升到n * sqrt(m) = 2^64,碰撞概率大幅下降。

高质量随机性:不信任单一熵源,组合三者

唯一 ID 方案成立的前提是:能够获得高质量随机数(至少需要一两个好的种子,具体论证见准随机论文)。RocksDB 追求跨平台,希望尽量减少平台相关依赖、优先使用跨平台依赖——但这又可能绕回原点:我们所依赖的某个实现一旦出 bug 或打嗝,就会重新受制于人。

幸运的是,随机熵有一个美妙性质:组合多个熵源时,结果质量与"最好的那个输入源"一样好。即使某个源坏了,只要不是所有源都坏,结果依然可靠。再加上两个工程上的有利条件:

  • 我们只需要唯一性,不需要安全性(密码学强度),这降低了对随机源的审查要求,也允许使用准随机方案;
  • 准随机方案把所需熵量降到最低,因此获取每单位熵的性能开销几乎可以忽略不计。

于是代码中把以下三类熵源组合在一起:

熵源博客原文描述仓库实现(env/unique_id_gen.cc)
std::random_device(C++11)按标准应提供高质量随机,但标准允许它不提供EntropyTrackRandomDevice(L91-L106),连续取192 / (8 * sizeof(result_type))r()填充数组
环境参数哈希主机名、进程 ID、线程 ID、宏/微秒级时间读数EntropyTrackEnvDetails(L71-L89):hostname_bufprocess_idthread_idunix_timenano_time
平台专用 UUID 生成器仅 Linux 和 WindowsEntropyTrackPortUuid(L56-L69):调用port::GenerateRfcUuid取前 36 字节

三条"轨道"各自在哈希后都足以产生 128 位熵,可在测试中独立启用/禁用;生产环境则尽可能全部组合。三轨数据连同 RocksDB 版本标识(ROCKSDB_MAJOR/MINOR/PATCH,防止熵输入"模式变更"造成意外物理碰撞)一起,经Hash2x64压缩为两个 64 位输出(env/unique_id_gen.cc#L128-L134)。

有意思的是,源码注释(env/unique_id_gen.cc#L53-L54)明确给出了 Linux 下的性能对比:EntropyTrackRandomDevicestd::random_device,底层通常是 RDRAND/RDSEED 指令)远快于EntropyTrackEnvDetails,后者又远快于EntropyTrackPortUuid。这一"性能偏好"正是后续硬件 Bug 能隐蔽潜伏多年的土壤——最常用的那个源,恰恰是最容易出问题的那一个。

此外,env/unique_id_gen.h 还定义了两种生成器:

  • SemiStructuredUniqueIdGen(L56-L85):随机基座 + 原子计数器,进程内lower保证每次调用唯一,用于生成 DB session ID(见 cache/cache_key.cc#L48-L49 的注释);
  • UnpredictableUniqueIdGen(L91-L117):256 位熵池 + 计数器 + RDTSC 时间信息,提供合理的不可预测性且初始化后保证不阻塞(与std::random_device不同,它不会被阻塞)。

信任但验证:用多线程迷你压力测试守护随机源质量

方案定下来之后,RocksDB 团队做了一件改变后续命运的事:对每个熵源持续进行质量验证。对应的单元测试来自 PR #8708——它使用大量线程、基于单个熵源(每次只启用一条轨道)批量生成成千上万个唯一 ID,然后逐一校验唯一性。

这条测试的数学底气很硬:对 128 位 ID 而言,即使只有 128 位中的一小部分有效,只要源质量正常,数千个样本中出现任意重复的概率都小到可以忽略——即便这些测试连续运行几十年,也几乎不可能碰撞。反过来,如果测试真的检测到大量重复,那就是熵源出了严重问题,而不是统计上的偶然。

仓库中的测试印证

当前仓库的 env/env_test.cc 完整保留了这一测试家族,且每条"轨道"都有独立的针对性用例:

  • TEST_F(EnvTest, GenerateRawUniqueId)(env/env_test.cc#L3440):默认三轨全开,多线程并发生成并断言唯一;
  • TEST_F(EnvTest, GenerateRawUniqueIdTrackPortUuidOnly)(L3456):仅启用平台 UUID;
  • TEST_F(EnvTest, GenerateRawUniqueIdTrackEnvDetailsOnly)(L3475):仅启用环境细节;
  • TEST_F(EnvTest, GenerateRawUniqueIdTrackRandomDeviceOnly)(L3489):仅启用std::random_device——这正是后来揪出 CPU 缺陷的那条测试。

测试调用的TEST_GenerateRawUniqueId(env/unique_id_gen.cc#L147-L155)是GenerateRawUniqueId的调试版,通过三个布尔参数精确控制哪些熵轨参与,从而实现对单一随机源的隔离验证。这种"轨道可分离"的设计(env/unique_id_gen.cc#L43-L54 的注释明言"各轨道可分离以便测试")是这次硬件缺陷能够被准确定位到std::random_device的前提。

异常信号:四年平安后,两个月内两次失败

故事在几个月前迎来转折:基于std::random_device的那条测试,失败了一次。

起初这个失败并不显眼——异常之处在于缺失的唯一 ID 数量"不是仅仅少了一个,而是少了几十甚至几百个"。不过即便这样,也能用"随机 CPU 抖动或比特翻转导致本来就没生成那么多 ID"来解释。你可能已经注意到,RocksDB 的开发和 CPU 时间中有越来越多"逻辑上冗余、但专为在损坏扩散之前探测 CPU 误算"的检查——这些检查的存在让单次失败显得没那么可怕。

但一个月后,它又失败了。四年零失败,然后两个月内连续失败两次——这味道非常不对。深挖细节时发现了一个关键相关性:两次失败的测试任务运行在完全不同的数据中心,却跑在同一类型的硬件上

冗余校验为何重要:CPU 损坏注入工具链

博客提到的"日益增长的冗余检查",在当前仓库的 tools/cpu_corruption_injector/ 目录中有直接印证:这是一整套用于模拟 CPU 计算错误的工具链,包括injector_register_corruption.py(寄存器损坏注入)、injector_critical_instruction.py(关键指令损坏注入)、injector_navigate.pyinjector_telemetry.py,以及配套的runner.py执行框架(runner_build.pyrunner_execute.pyrunner_randomize_stress_flags.pyrunner_report.py)。这类工具的意义在于:把"CPU 算错"当作一种需要主动防御的故障模式来演练,验证 RocksDB 的校验逻辑能否在错误传播前将其捕获。这也解释了为什么团队面对第一次失败时能保持冷静——失败被设计为"可预期、可解释"的事件。

工程直觉放大:复现实验

面对两次可疑失败,工程师的自然反应是:放大规模,尝试复现。而这次复现异常顺利——

  • 把任务中的线程数提高到接近机器核心数后,所有使用同一类型新 CPU 的系统都会快速、稳定地失败,其他硬件全部通过;
  • 进一步设计变体实验,定位出两条精确边界:
    • std::random_device使用rdrand源和使用/dev/urandom源时不受影响
    • 使用 clang 的libc++不受影响,只有 GCC 的libstdc++受影响。

这两条边界把嫌疑锁定在了libstdc++std::random_device的默认实现路径——也就是直接执行 CPU 随机数指令的那条路径。

根因分析:RDSEED 返回 0 却报告"成功"

随后 Meta 的同事展开了底层调查,最终定位到处理器硬件缺陷

该类型处理器上的RDSEED 指令,在"复杂的、可在内存负载下复现的微架构条件"下,返回 0 并置位"成功"标志的频率远超随机期望——而且只在部分核心上出现。

也就是说,CPU 声称"我给你生成了一个合格的随机数",实际上给出的是常数 0。对于准随机方案来说,单个 0 值不会立刻致命(毕竟还有哈希和其他熵轨兜底),但当失败集中在某条仅启用std::random_device的测试上时,数十到数百个重复 ID 就完全暴露了问题——这正是"每条轨道单独测试"的设计价值。

应对措施随之展开:

  • 开发了一个缓解性质的 Linux 内核补丁,在这些处理器上将 RDSEED 标记为不可用,Meta 内部先行部署,在 OEM 提供修复前规避问题;
  • AMD 很快承认了问题并公布了计划中的缓解方案(官方安全公告 AMD SB-7055),其中包括 CPU 微码(microcode)更新;
  • 该缺陷最终被评定为高危(high severity)CVE

从排查视角看,这次根因分析链条值得记住:随机性测试(检验输出质量)→ 硬件隔离(同一 CPU 型号)→ 线程数放大(复现)→ 实现隔离(库/指令路径)→ 硬件厂商确认。每一步都排除了一个假设层。

披露过程中的插曲与致歉

这篇博文还罕见地公开了一段"事故复盘":作者原本尽力在 OEM 公开承认问题前保密信息,但由于 Meta 内部多个基础设施团队"过于积极的补救行动",信息经由Linux 邮件列表发生了未协调的提前披露。作者代表相关流程致歉,并表示正在改进与 OEM 先协调的管控流程。

这段插曲本身也是工程技术社区治理的重要案例:安全问题的披露不止是"发现问题—修复问题",还涉及跨团队、跨公司的协调纪律。

关键启示

博文结尾给出了三条言简意赅的经验,这不仅是针对随机数,更是针对所有"基础依赖"的通用方法论:

  1. 测试你所依赖的东西(Test what you depend on)——不要假设std::random_device、操作系统、文件系统或 CPU 一定按文档工作;
  2. 为你所依赖的东西设置冗余和/或健全性检查(Have redundancies and/or sanity checks)——组合熵源、逻辑冗余校验、独立轨道测试,都是"防单点失效"的具体形态;
  3. 即便是 CPU 也会出 Bug(Even CPUs can have bugs)——通常是个别单元间歇性故障,但偶尔也会出现影响所有单元的缺陷;正确的态度是把"计算可能出错"纳入系统设计的前提。

仓库中的工程印证:可继续深入的路径

如果你想在代码层面继续验证本文的每一个论断,以下是当前仓库中的关键入口:

  • 熵源组合与三轨设计:env/unique_id_gen.cc、env/unique_id_gen.h;
  • 唯一 ID 生成与 SST 表属性集成:table/unique_id.cc、table/unique_id_impl.h;
  • 缓存键如何消费唯一 ID:cache/cache_key.cc(其头部注释含完整的碰撞概率推导);
  • 多线程唯一性测试家族:env/env_test.cc#L3440-L3496(含GenerateRawUniqueIdTrackRandomDeviceOnly);
  • CPU 损坏注入与故障演练工具链:tools/cpu_corruption_injector/(含 README 与tests/test_injector_critical_instruction.py)。

最后需要说明适用边界:本文描述的唯一 ID 方案面向唯一性而非安全性——GenerateRawUniqueId在 env/unique_id_gen.h 的注释中明确声明"未经验证可用于密码学"。如果你的场景需要密码学强度随机数,请勿直接复用该实现。

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询