<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>有疑说</title>
  
  <subtitle>博学、慎思、明辨、笃行</subtitle>
  <link href="https://www.cyningsun.com/feed.xml" rel="self"/>
  
  <link href="https://www.cyningsun.com/"/>
  <updated>2026-04-03T13:48:10.000Z</updated>
  <id>https://www.cyningsun.com/</id>
  
  <author>
    <name>cyningsun</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>译｜Optimizing Space Amplification in RocksDB</title>
    <link href="https://www.cyningsun.com/04-03-2026/optimizing-space-amplification-in-rocksdb-cn.html"/>
    <id>https://www.cyningsun.com/04-03-2026/optimizing-space-amplification-in-rocksdb-cn.html</id>
    <published>2026-04-02T16:00:00.000Z</published>
    <updated>2026-04-03T13:48:10.000Z</updated>
    
    <content type="html"><![CDATA[<p>siying Dong¹, Mark Callaghan¹, Leonidas Galanis¹, Dhruba Borthakur¹, Tony Savor¹ and Michael Stumm²</p><p>¹Facebook, 1 Hacker Way, Menlo Park, CA USA 94025<br>{siying.d, mcallaghan, lgalanis, dhruba, tsavor}@fb.com</p><p>²多伦多大学电气与计算机工程系，加拿大 M8X 2A6<br><a href="mailto:&#x73;&#x74;&#117;&#109;&#x6d;&#x40;&#x65;&#x65;&#99;&#x67;&#46;&#x74;&#x6f;&#x72;&#x6f;&#x6e;&#x74;&#111;&#46;&#x65;&#100;&#117;">&#x73;&#x74;&#117;&#109;&#x6d;&#x40;&#x65;&#x65;&#99;&#x67;&#46;&#x74;&#x6f;&#x72;&#x6f;&#x6e;&#x74;&#111;&#46;&#x65;&#100;&#117;</a></p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p><em>RocksDB</em> 是 Facebook 开发的一种嵌入式、高性能、持久化的键值存储引擎。当前在开发和配置 RocksDB 时的主要关注点是优先考虑资源效率，而不是优先考虑更标准的性能指标（如响应延迟和吞吐量），只要后者保持在可接受的范围内。特别是，在确保读写延迟满足目标工作负载的服务级别要求的同时，优化空间效率。这一选择的动机在于，在 Facebook 典型的生产工作负载下使用闪存 SSD 时，存储空间通常是最主要的瓶颈。RocksDB 使用<em>日志结构合并树</em>（log-structured merge trees）来获得显著的空间效率和更好的写入吞吐量，同时实现可接受的读取性能。</p><p>本文描述了 RocksDB 用于减少存储使用量的方法。讨论了如何权衡存储效率与 CPU 开销，以及读写放大。基于对使用 RocksDB 作为嵌入式存储引擎的 MySQL 进行的实验评估（使用 TPC-C 和 LinkBench 基准测试）以及从生产数据库获取的测量结果，表明 RocksDB 使用的存储空间不到 InnoDB 的一半，但性能良好，并且在许多情况下甚至优于基于 B 树的 InnoDB 存储引擎。据我们所知，这是基于日志结构合并树的存储引擎，首次在运行大规模 OLTP 工作负载时，展现出具有竞争力的性能。</p><h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1. 引言"></a>1. 引言</h2><p>资源效率是 Facebook 存储系统战略的首要目标。性能必须足以满足 Facebook 服务的需求，但效率应尽可能高以实现规模化。</p><p>Facebook 拥有世界上最大的 MySQL 部署之一，存储着数十 PB 的在线数据。Facebook MySQL 实例的底层存储引擎正越来越多地从 InnoDB 切换到 MyRocks，而后者基于 Facebook 的 RocksDB。切换的主要动机是 MyRocks 使用的存储空间是 InnoDB 所需的一半，并且具有更高的平均事务吞吐量，而读取延迟仅略微增加。</p><p><em>RocksDB</em> 是一个嵌入式、高性能、持久化的键值存储系统 [1]，由 Facebook 在从 Google 的 LevelDB [2, 3] 分叉代码后开发。¹ RocksDB 于 2013 年开源 [5]。MyRocks 是将 RocksDB 集成为 MySQL 的存储引擎。通过 MyRocks，可以使用 RocksDB 作为后端存储，同时仍能受益于 MySQL 的所有功能。</p><blockquote><p>¹ 一篇 Facebook 博客文章列出了 RocksDB 和 LevelDB 之间的许多关键区别 [4]。</p></blockquote><p>RocksDB 在 Facebook 内外的应用并不仅仅局限于 MySQL。在 Facebook 内部，RocksDB 被用作 Laser（一个高查询吞吐量、低延迟的键值存储服务 [6]）、ZippyDB（一个具有 Paxos 式复制的分布式键值存储 [6]）、Dragon（一个存储社交图谱索引的系统 [7]）以及 Stylus（一个流处理引擎 [6]）等系统的存储引擎。在 Facebook 外部，MongoDB [8] 和 Sherpa（雅虎最大的分布式数据存储 [9]）都将 RocksDB 用作其存储引擎之一。此外，RocksDB 还被 LinkedIn 用于存储用户活动 [10]，被 Netflix 用于缓存应用数据 [11]，等等。</p><p>在 Facebook 使用 RocksDB 的主要目标是在确保所有重要服务级别要求（包括目标事务延迟）得以满足的同时，最有效地利用硬件资源。我们专注于效率而非性能，在数据库社区中可以说颇为独特，因为数据库系统通常使用每分钟事务数（例如 tpmC）或响应时间延迟等性能指标进行比较。我们对效率的关注并不意味着认为性能不重要，而是指一旦性能目标达成，就会优化效率。我们的方案部分是由 Facebook 的数据存储需求（可能与其他组织不同）驱动的：</p><ol><li>SSD 越来越多地用于存储持久化数据，并且是 RocksDB 的主要目标；</li><li>Facebook 在其数据中心主要采用基于商用硬件的无共享架构 [12]，数据分布在大量简单的节点上，每个节点配备 1-2 个 SSD；</li><li>需要存储的数据量巨大；</li><li>在许多（但非全部）场景下，由于广泛采用了基于内存的大规模缓存，其读写比率相对较低，大约为 2:1。</li></ol><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-1.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-1.png"></p><p><strong>图 1：SST 文件组织。</strong> 每个层级维护一组 SST 文件。每个 SST 文件由未对齐的 16KB 块组成，并带有一个索引块来标识 SST 内的其他块。Level 0 的处理方式不同，其 SST 文件具有重叠的键范围，而其他层级的 SST 文件具有非重叠的键范围。一个清单文件（manifest file）维护所有 SST 文件及其键范围的列表以辅助查找。</p><p>最小化空间放大对于高效利用硬件至关重要，因为在上文描述的环境中，存储空间是瓶颈。在 Facebook 典型的生产 MySQL 环境中，使用 InnoDB 时，SSD 在高峰时段处理的读取&#x2F;秒和写入&#x2F;秒远低于硬件所能支持的水平。InnoDB 下的吞吐量水平低，并非因为 SSD 或处理节点存在任何瓶颈——例如，CPU 利用率保持在 40% 以下——而是因为每个节点的查询率低。每个节点的查询率低，是因为必须存储（并可访问）的数据量如此之大，必须将其分片到许多节点上才能容纳，而节点越多，每个节点的查询就越少。</p><p>如果 SSD 可以存储两倍的数据，那么预计存储节点效率将翻倍，因为 SSD 可以轻松处理 IOPS 翻倍的预期，并且所需的工作负载节点数将大幅减少。该问题驱动了我们对于空间放大的关注。此外，随着 SSD 价格的持续下降，最小化空间放大使得 SSD 相对于磁盘，在冷数据存储领域正成为越来越具吸引力的替代方案。在追求最小化空间放大的过程中，我们愿意牺牲一些额外的读取或写入放大。这种权衡是必要的，因为不可能同时减少空间、读取和写入放大 [13]。</p><p>RocksDB 基于日志结构合并树（LSM-tree）。LSM-tree 最初旨在最小化对存储的随机写入，因为它从不就地修改数据，而只将数据追加到位于稳定存储的文件中，以利用硬盘的高顺序写入速度 [14]。随着技术的变化，LSM-tree 因其低写入放大和低空间放大的特性而变得有吸引力。</p><p>在本文中，描述了 RocksDB 减少空间放大的技术。我们相信其中一些技术是首次被描述，包括动态 LSM-tree 层级大小调整、分级压缩（tiered compression）、共享压缩字典、前缀布隆过滤器以及不同 LSM-tree 层级使用差异化大小乘数。我们讨论了空间放大技术如何影响读取和写入放大，并描述了其中涉及的一些权衡。通过实证测量表明，RocksDB 平均所需存储空间比 InnoDB 减少约 50%，同时具备更高的事务吞吐量，且读取延迟仅略有增加，完全保持在可接受范围内。我们还探讨了空间放大与 CPU 开销之间的权衡，因为一旦空间放大显著降低，CPU 可能成为新的瓶颈。</p><p>基于实验数据证明，基于 LSM-tree 的存储引擎在用于 OLTP 工作负载时可以具有性能竞争力。我们相信这是首次展示这一点。在 MyRocks 中，每个 MySQL 表行存储为一个 RocksDB 键值对：主键编码在 RocksDB 键中，所有其他行数据编码在值中。非唯一的二级键作为单独的键值对存储，其中 RocksDB 键编码了附加了相应目标主键的二级键，值留空；因此，二级索引查找被转换为 RocksDB 范围查询。所有 RocksDB 键都带有一个 4 字节的表 ID 或索引 ID 作为前缀，以便多个表或索引可以共存于一个 RocksDB 键空间中。最后，每个键值对都存储一个随着每个写操作递增的全局序列 ID，以支持快照。快照用于实现多版本并发控制，进而使我们能够在 RocksDB 内实现 ACID 事务。</p><p>在下一节中，我们简要介绍 LSM-tree 的背景。在第 3 节中，我们描述减少空间放大的技术。在第 4 节中，我们展示如何平衡空间放大与读取放大和 CPU 开销。最后，在第 5 节中，我们展示了使用实际生产工作负载（TPC-C 和 LinkBench）以及从生产数据库实例获取的测量结果进行的实验评估结果。我们以结束语作为结尾。</p><h2 id="2-LSM-TREE-背景"><a href="#2-LSM-TREE-背景" class="headerlink" title="2. LSM-TREE 背景"></a>2. LSM-TREE 背景</h2><p>日志结构合并树（LSM-tree）如今用于许多流行系统，包括 BigTable [15]、LevelDB、Apache Cassandra [16] 和 HBase [17]。近期的重点研究也集中在 LSM-tree 上；例如 [18, 19, 20, 21, 22]。这里我们简要描述在 Facebook 的 MyRocks 中默认实现和配置的 LSM-tree。</p><p>每当数据写入 LSM-tree 时，它会被添加到一个称为内存表（mem-table）的内存写缓冲区中，该缓冲区实现为具有 O(log n) 插入和搜索复杂度的跳表（skiplist）。同时，为了恢复目的，数据会被追加到一个预写日志（WAL）中。写入后，如果 mem-table 的大小达到预定大小，则 (i) 当前的 WAL 和 mem-table 变为不可变的，并分配新的 WAL 和 mem-table 来捕获后续写入，(ii) mem-table 的内容被刷新到一个“排序序列表”（SST）数据文件中，完成后，(iii) 包含刚刷新数据的 WAL 和 mem-table 被丢弃。这种通用方法带来了许多优势：新的写入可以与旧 mem-table 的刷新同时处理；所有 I&#x2F;O 都是顺序的，² 并且，除 WAL 外，只写入整个文件。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-2.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-2.png"></p><p><strong>图 2：压实（Compaction）。</strong> 选定的 level-i SST 文件的内容与 level i+1 中那些键范围与 level-i SST 键范围重叠的 SST 文件合并。图中上半部分的阴影 SST 文件在合并过程后被删除。图中下半部分的阴影 SST 文件是压实过程创建的新文件。压实过程清除了已过时的数据；即，已被标记为删除的数据以及已被覆盖的数据（如果它们不再被快照需要）。</p><p>每个 SST 以排序顺序存储数据，划分为未对齐的 16KB 块（未压缩时）。每个 SST 还有一个索引块，用于二分查找，每个 SST 块一个键。SST 被组织成一系列大小递增的层级，Level-0 – Level-N，每个层级将有多个 SST，如图 1 所示。Level-0 被特殊对待，因为其 SST 可能具有重叠的键范围，而更高级别的 SST 具有不同的非重叠键范围。当 Level-0 中的文件数超过阈值（例如 4）时，Level-0 SST 与具有重叠键范围的 Level-1 SST 合并；完成后，所有合并排序输入（L0 和 L1）文件被删除，并由新的（合并后的）L1 文件替换。对于 L&gt;0，当 level-L 中所有 SST 的组合大小超过阈值（例如 10^(L-1)GB）时，则选择一个或多个 level-L SST 并与 level-(L+1) 中重叠的 SST 合并，之后删除已合并的 level-L 和 level-(L+1) SST。如图 2 所示。</p><p>² 通常存在并发的顺序 IO 流，这会导致寻道。然而，寻道成本将被 LSM-tree 非常大的写入（许多 MB 而不是 KB）所分摊。<br>³ 分层压实（Leveled compaction）与 HBase 和 Cassandra [23] 等使用的压实方法不同。在本文中，所有使用的术语“压实”均指分层压实。</p><p>这个合并过程称为<em>压实</em>（compaction），因为它会移除标记为已删除的数据和已被覆盖的数据（如果不再需要）。该过程是多线程实现的。压实还具有将新更新从 Level-0 逐渐迁移到最后一级的效果，这就是为什么这种特定方法被称为“分层”压实的原因。³ 该过程确保在任何给定时间，每个 SST 对于任何给定键和快照最多包含一个条目。压实期间发生的 I&#x2F;O 是高效的，因为它只涉及整个文件的大块读取和写入：如果一个正在压实的 level-L 文件仅与 level-(L+1) 文件的一部分重叠，那么整个 level-(L+1) 文件仍将用作压实的输入并最终被移除。一次压实可能会触发一系列级联压实。</p><p>一个单独的<em>清单文件</em>（Manifest File）维护每个层级的 SST 列表、它们对应的键范围以及其他一些元数据。它被维护为一个日志，对 SST 信息的更改会追加到该日志中。清单文件中的信息以高效格式缓存在内存中，以便快速识别可能包含目标键的 SST。</p><p>对键的搜索在每个连续层级进行，直到找到该键或确定该键不存在于最后一级。它首先搜索所有 mem-table，然后是所有 Level-0 SST，接着是后续各级的 SST。在每一个连续层级中，需要进行三次二分查找。第一次搜索使用清单文件中的数据定位目标 SST。第二次搜索使用 SST 的索引块定位 SST 文件内的目标数据块。最后一次搜索在数据块内查找键。布隆过滤器（保存在文件中但缓存在内存中）用于消除不必要的 SST 搜索，因此在常见情况下只需要从磁盘读取 1 个数据块。此外，最近读取的 SST 块会缓存在由 RocksDB 维护的块缓存（block cache）和操作系统的页缓存（page cache）中，因此对最近获取的数据的访问可能不需要执行 I&#x2F;O 操作。MyRocks 的块缓存通常配置为 12GB 大小。</p><p>范围查询涉及更多操作，并且总是需要搜索所有层级，因为必须找到范围内的所有键。首先在 mem-table 中搜索范围内的键，然后是所有 Level-0 SST，接着是所有后续层级，同时忽略来自较低层级的范围内的重复键。前缀布隆过滤器（§4）可以减少需要搜索的 SST 数量。</p><p>为了更好地理解 LSM-tree 的系统特性，我们在附录中提供了从三个生产服务器收集的各种统计信息。</p><h2 id="3-空间放大"><a href="#3-空间放大" class="headerlink" title="3. 空间放大"></a>3. 空间放大</h2><p>LSM-tree 通常比 B-tree 的空间效率高得多。在类似于 Facebook 的读写工作负载下，B-tree 的空间利用率会很差 [24]，其页只有 1&#x2F;2 到 2&#x2F;3 满（根据 Facebook 生产数据库的测量）。这种碎片化导致基于 B-tree 的存储引擎的空间放大比大于 1.5。压缩的 InnoDB 在磁盘上具有固定的页大小，这进一步浪费了空间。</p><p>相反，LSM-tree 不会受到碎片化的影响，因为它不要求数据按 SSD 页面对齐写入。LSM-tree 的空间放大主要取决于尚未被垃圾回收的过时数据量。如果我们假设最后一级（last level）已按其目标大小填满数据，并且每一级比前一级大 10 倍，那么在最坏的情况下，LSM-tree 的空间放大将是 1.111…，考虑到直到最后一级的所有层级加起来只有最后一级大小的 11.111…%。</p><p>RocksDB 使用两种策略来减少空间放大：(i) 根据当前数据库大小调整层级大小，以及 (ii) 应用多种压缩策略。</p><h3 id="3-1-动态层级大小调整"><a href="#3-1-动态层级大小调整" class="headerlink" title="3.1 动态层级大小调整"></a>3.1 动态层级大小调整</h3><p>如果为每个层级指定固定大小，那么在实践中，存储在最后一级的数据大小不太可能是前一级目标大小的 10 倍。在更糟的情况下，存储在最后一级的数据大小可能仅略大于前一级的目标大小，这种情况下空间放大会大于 2。</p><p>然而，如果我们动态调整每一级的大小，使其为下一级数据大小的十分之一，那么空间放大将减少到小于 1.111…。</p><p>层级大小乘数（level size multiplier）是 LSM-tree 中的一个可调参数。上面我们假设它是 10。大小乘数越大，空间放大和读取放大越低，但写入放大越高。因此，选择代表了一种权衡。对于 Facebook 大多数生产环境的 RocksDB 安装，使用的大小乘数是 10，尽管有少数实例使用 8。</p><p>一个有趣的问题是每一级的大小乘数是否应该相同。关于 LSM-tree 的原始论文证明，在优化写入放大时，每一级使用相同的乘数是最优的 [14]。⁴ 在优化空间放大时，尤其是考虑到不同级别可能使用不同的压缩算法导致每级压缩比不同的情况下（如下一节所述），这是否仍然成立是一个悬而未决的问题。我们打算在未来的工作中分析这个问题。⁵</p><p>⁴ 原始的 LSM-tree 论文使用固定数量的层级，并随着数据库变大而改变乘数，但保持每级的乘数相同。LevelDB&#x2F;RocksDB 使用固定乘数，但随着数据库变大而改变层级数量。</p><p>⁵ 初步结果表明，调整每级的大小目标以考虑每级实现的压缩比会带来更好的结果。</p><h3 id="3-2-压缩"><a href="#3-2-压缩" class="headerlink" title="3.2 压缩"></a>3.2 压缩</h3><p>可以通过压缩 SST 文件来进一步减少空间放大。我们同时应用多种策略。LSM-tree 提供了许多使压缩策略更有效的特性。特别是，LSM-tree 中的 SST 及其数据块是不可变的。</p><p><strong>键前缀编码。</strong> 通过不写入与前一个键重复的前缀，对键应用前缀编码。我们发现这在实践中可以减少 3% – 17% 的空间需求，具体取决于数据工作负载。</p><p><strong>序列 ID 垃圾回收。</strong> 如果键的序列 ID 比多版本并发控制所需的最旧快照还旧，则将其移除。用户可以任意创建快照以在以后的时间点引用当前数据库状态。移除快照 ID 往往是有效的，因为 7 字节大的序列 ID 压缩效果不好，并且因为大多数序列 ID 在引用它们的相应快照被删除后不再需要。在实践中，这种优化可以减少 0.03%（例如，对于存储具有大值的社交图谱顶点的数据库）到 23%（例如，对于存储具有空值的社交图谱边的数据库）的空间需求。</p><p><strong>数据压缩。</strong> RocksDB 目前支持多种压缩算法，包括 LZ、Snappy、zlib 和 Zstandard。每个层级可以配置为使用这些压缩算法中的任何一种或不使用。压缩按块（block） basis 应用。根据数据的组成，较弱的压缩算法可以将空间需求降低到其原始大小的 40%，而较强的算法在 Facebook 生产数据上可以低至 25%。</p><p>为了减少解压缩数据块的频率，RocksDB 块缓存以未压缩的形式存储块。（请注意，最近访问的压缩文件块将由操作系统页缓存以压缩形式缓存，因此压缩的 SST 将使用更少的存储空间和更少的缓存空间，这反过来允许文件系统缓存更多数据。）</p><p><strong>基于字典的压缩。</strong> 可以使用数据字典来进一步改进压缩。当使用小数据块时，数据字典尤其重要，因为较小的块通常产生较低的压缩比。字典使得较小的块能够从更多的上下文中受益。通过实验，我们发现数据字典可以额外减少 3% 的空间需求。</p><p>LSM-tree 使得构建和维护字典更加容易。它们倾向于生成大的不可变 SST 文件，这些文件可能高达数百兆字节。应用于所有数据块的字典可以存储在文件内部，这样当文件被删除时，字典会自动丢弃。</p><h2 id="4-权衡"><a href="#4-权衡" class="headerlink" title="4. 权衡"></a>4. 权衡</h2><p>LSM-tree 有许多配置参数和选项，使得能够根据目标工作负载的具体情况为每个安装进行多种权衡。Athanassoulis 等人的先前工作确定，可以优化空间、读取和写入放大中的任意两个，但会牺牲第三个 [13]。例如，减少层级数量（比如通过增加层级乘数）会降低空间和读取放大，但会增加写入放大。</p><p>再举一个例子，在 LSM-tree 中，较大的块大小可以在不降低写入放大的情况下改进压缩，但会对读取放大产生负面影响（因为每次查询必须读取更多数据）。这一观察允许我们在处理写入密集型应用程序时使用较大的块大小来获得更好的压缩比。（在 B 树中，较大的块会同时降低写入和读取放大。）</p><p>在许多情况下，如何权衡需要人为判断，并取决于预期的工作负载以及系统最低可接受的服务质量水平。当专注于效率时（正如我们所做的），极难将系统配置为恰当地平衡 CPU、磁盘 I&#x2F;O 和内存利用率，尤其是因为它强烈依赖于高度变化的工作负载。</p><p>正如我们在下一节所示，我们的技术将存储空间需求比 InnoDB 减少了 50%。这使我们能够在每个节点上存储两倍的数据，从而实现现有硬件的显著整合。然而，与此同时，这也意味着每个服务器的工作负载（QPS）翻倍，这可能导致系统达到可用 CPU、随机 I&#x2F;O 和 RAM 容量的极限。</p><p><strong>分级压缩（Tiered compression）。</strong> 压缩（Compression）通常会减少所需的存储空间量，但会增加 CPU 开销，因为数据必须被压缩和解压缩。压缩越强，CPU 开销越高。在我们的安装中，通常在最末级使用强压缩算法（如 zlib 或 Zstandard），即使它带来更高的 CPU 开销，因为大部分（接近 90%）数据位于该级，但只有一小部分读取和写入会访问它。在各种用例中，对最末级应用强压缩比仅使用轻量级压缩额外节省 15%–30% 的存储空间。</p><p>相反，我们在级别 0-2 不使用任何压缩，以牺牲更高的空间和写入放大为代价来允许更低的读取延迟，因为它们只占总存储空间的一小部分。Level-3 到最末级使用轻量级压缩（如 LZ4 或 Snappy），因为其 CPU 开销是可接受的，同时它减少了空间和写入放大。对位于前三个级别的数据的读取更可能位于操作系统缓存的（未压缩的）文件块中，因为这些块被频繁访问。然而，对位于高于 2 的级别中的数据的读取将必须被解压缩，无论它们是否位于操作系统文件缓存中（除非它们也位于 RocksDB 块缓存中）。</p><p><strong>布隆过滤器。</strong> 布隆过滤器可有效减少 I&#x2F;O 操作及随之而来的 CPU 开销，但代价是稍微增加内存使用量，因为过滤器（通常）每个键需要 10 位。为了说明一些权衡是微妙的，我们在最末级不使用布隆过滤器。虽然这会导致更频繁地访问最末级文件，但读取查询到达最末级的概率相对较小。更重要的是，最末级的布隆过滤器很大（比所有较低级别布隆过滤器的总和还大 9 倍），并且它在基于内存的缓存中消耗的空间将阻止缓存其他将被访问的数据。我们根据经验确定，对于我们的工作负载，最末级没有布隆过滤器总体上改善了读取放大。</p><p><strong>前缀布隆过滤器。</strong> 布隆过滤器对范围查询没有帮助。我们开发了一种前缀布隆过滤器来帮助处理范围查询，这是基于我们的观察：许多范围查询通常只针对某个特定前缀；例如，(userid,timestamp) 键的 userid 部分或 (postid,likerid) 键的 postid 部分。为此，我们允许用户定义前缀提取器（prefix extractors），以确定性地从键中提取前缀部分，并据此构建布隆过滤器。查询范围时，用户可以指定查询是针对已定义的前缀。我们发现这种优化在我们的系统上将范围查询的读取放大（及随之而来的 CPU 开销）减少了高达 64%。</p><h2 id="5-评估"><a href="#5-评估" class="headerlink" title="5. 评估"></a>5. 评估</h2><p>对 Facebook 众多 MySQL 安装的回顾普遍表明：</p><blockquote><ol><li>RocksDB 使用的存储空间比启用压缩的 InnoDB 使用的空间低约 50%，</li><li>RocksDB 写入存储的数据量是 InnoDB 写出的数据的 10% 到 15% 之间，并且</li><li>RocksDB 的读取次数和量比 InnoDB 高 10% – 20%（但仍完全在可接受的范围内）。</li></ol></blockquote><p>为了从受控环境中获得更有意义的指标，我们展示了使用两个基准测试对 MySQL 进行广泛实验的结果。第一个基准测试 LinkBench，基于来自 Facebook 存储“社交图谱”数据的生产数据库的跟踪；它发出大量范围查询 [25]。我们运行了 24 个 1 小时间隔的 LinkBench，并测量第 24 个间隔的统计数据，以获取稳态系统的数字。⁶ 第二个基准测试是标准的 TPC-C 基准测试。</p><p>⁶ 我们还收集了加载完整 LinkBench 数据库时的统计数据；（未显示的）结果与稳态数字一致。</p><p>对于这两个基准测试，我们试验了两个变体：一个是数据库完全放入 DRAM 中，因此只需要磁盘活动用于实现持久性的写入；另一个是数据库不能完全放入内存中。我们比较了 RocksDB、InnoDB 和 TokuDB 的行为，配置为使用各种压缩策略。（TokuDB 是另一个开源的、高性能的 MySQL 存储引擎，其核心使用分形树索引数据结构来减少空间和写入放大 [26]。）</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-3.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-3.png"></p><p><strong>图 3：LinkBench 基准测试。</strong> 使用 16 个并发客户端运行 24 小时后，在第 24 小时收集的统计数据，针对三种不同的存储引擎：RocksDB（来自 Facebook MySQL 5.6）以红色显示，InnoDB（来自 MySQL 5.7.10）以蓝色显示，以及 TokuDB（Percona Server 5.6.26-74.0）以绿色显示，配置为使用括号中列出的压缩方案。（同步提交（Sync-on-commit）被禁用，二进制日志&#x2F;操作日志（binlog&#x2F;oplog）和重做日志（redo logs）已启用。）</p><p>系统设置：硬件包括一个 Intel Xeon E5-2678v3 CPU，具有 24 核&#x2F;48 硬件线程，运行频率为 2.50GHz，256GB RAM，以及大约 5T 的快速 NVMe SSD，通过配置为 SW RAID 0 的 3 个设备提供。操作系统是 Linux 4.0.9-30。</p><p>左侧图表：来自配置为存储 5000 万个顶点（完全放入 DRAM）的 LinkBench 的统计数据。<br>右侧图表：来自配置为存储 10 亿个顶点（在将 DRAM 内存限制为 50GB 后无法放入内存）的 LinkBench 的统计数据：除 50GB 外的所有 RAM 都被后台进程 mlock，因此数据库软件、操作系统页缓存和其他监控进程必须共享这 50GB。MyRocks 块缓存设置为 10GB。</p><p>图 3 显示了我们的 LinkBench 实验结果。使用的硬件和软件设置在图的说明中描述。对于数据库不能完全放入内存的 LinkBench 基准测试的一些观察：</p><blockquote><ul><li><strong>空间使用：</strong> 启用压缩的 RocksDB 使用的存储空间比考虑的任何替代方案都少；未压缩时，它使用的存储空间不到未压缩 InnoDB 的一半。</li><li><strong>事务吞吐量：</strong> RocksDB 表现出比所有考虑的替代方案更高的吞吐量：比 InnoDB 好 3%-16%，远优于 TokuDB。图中未显示的是，在所有情况下，CPU 都是阻止吞吐量进一步提高的瓶颈。</li><li><strong>CPU 开销：</strong> 当使用更强的压缩时，RocksDB 表现出比未压缩的 InnoDB 每事务高不到 20% 的 CPU 开销，但只有 TokuDB 的不到 30% 的 CPU 开销。使用强压缩的 RocksDB 产生的 CPU 开销仅相当于启用压缩的 InnoDB 的 80%。</li><li><strong>写入量：</strong> RocksDB 中每事务写入的数据量不到 InnoDB 写入数据量的 20%。⁷ RocksDB 的写入量显著低于 TokuDB 的写入量。</li><li><strong>读取量：</strong> 未使用压缩时，RocksDB 中每个读取事务读取的数据量比 InnoDB 高 20%，使用压缩时则高出 10% 到 22%。RocksDB 的读取量显著少于 TokuDB 的读取量。</li></ul></blockquote><p>⁷ I&#x2F;O 量数字来自 iostat。写入量数字必须进行调整，因为 iostat 将 TRIM 计为写入的字节，而实际上并没有写入任何字节。RocksDB 经常删除整个文件（与 InnoDB 相比）并为此使用 TRIM，iostat 将其报告为好像整个文件已被写入。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-4.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-4.png"></p><p><strong>图 4：LinkBench 服务质量：</strong> 更新顶点、获取顶点、更新链接、获取链接的 99th 百分位数延迟。硬件和存储引擎的设置如图 3 所述。使用了具有 10 亿个顶点的数据库，可用 DRAM 限制为 50GB。</p><p>图 4 描述了不同存储引擎实现的服务质量。具体来说，它显示了 LinkBench 数据库中顶点和边的读写请求的 99th 百分位数延迟。RocksDB 的性能表现比所有其他考虑的替代方案好一个数量级。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-5.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-5.png"></p><p><strong>图 5：TPC-C 基准测试。</strong> 使用与图 3 相同的设置获得的指标。</p><p>左侧：40 个仓库和 10 个并发客户端的配置。数据库完全放入内存。统计数据是在运行 14 小时后的整个第 15 小时内收集的。<br>右侧：1000 个仓库和 20 个并发客户端的配置。统计数据是在运行 11 小时后的整个第 12 小时内收集的。使用的事务隔离级别标记为“rc”表示 READ COMMITTED（读已提交）或“rr”表示 REPEATABLE READ（可重复读）。</p><p>TPC-C 基准测试的结果显示在图 5 中。这里的数据库大小统计数据较难解释，因为 TPC-C 数据库随着事务数量的增长而增长。例如，显示启用压缩的 InnoDB 需要较小的存储空间，但这只是因为该 InnoDB 变体在测量点之前能够处理的事务少得多；实际上，InnoDB 数据库大小在事务时间内的增长比 RocksDB 快得多。</p><p>该图清楚地表明，RocksDB 不仅在 OLTP 工作负载上具有竞争力，而且通常具有更高的事务吞吐量，同时所需的存储空间显著少于替代方案。RocksDB 每事务写出的数据比所有其他测试的配置都少，而读取的数据仅略多，每事务所需的 CPU 开销也仅略高。</p><h2 id="6-结束语"><a href="#6-结束语" class="headerlink" title="6. 结束语"></a>6. 结束语</h2><p>我们描述了 RocksDB 如何能够将存储空间需求比 InnoDB 所需减少 50%，同时提高事务吞吐量并显著降低写入放大，而平均读取延迟仅略微增加。它通过利用 LSM-tree 并应用各种节省空间的技术来实现这一点。</p><p>据我们所知，其中许多技术是首次被描述，包括：(i) 基于当前数据库大小的动态 LSM-tree 层级大小调整；(ii) 分级压缩（tiered compression），即在不同的 LSM-tree 层级使用不同级别的压缩；(iii) 使用共享压缩字典；(iv) 将布隆过滤器应用于键前缀；以及 (v) 在不同的 LSM-tree 层级使用不同的大小乘数。此外，我们相信这是首次表明基于 LSM-tree 的存储引擎在传统 OLTP 工作负载上具有竞争性能。</p><h2 id="7-参考文献"><a href="#7-参考文献" class="headerlink" title="7. 参考文献"></a>7. 参考文献</h2><p>[1] <a href="http://rocksdb.org/">http://rocksdb.org</a>.<br>[2] <a href="http://leveldb.org/">http://leveldb.org</a>.<br>[3] <a href="https://github.com/google/leveldb">https://github.com/google/leveldb</a>.<br>[4] <a href="https://github.com/facebook/rocksdb/wiki/Features-Not-in-LevelDB">https://github.com/facebook/rocksdb/wiki/Features-Not-in-LevelDB</a>.<br>[5] <a href="https://github.com/facebook/rocksdb">https://github.com/facebook/rocksdb</a>.<br>[6] G. J. Chen, J. L. Wiener, S. Iyer, A. Jaiswal, R. Lei, N. Simha, W. Wang, K. Wilfong, T. Williamson, and S. Yilmaz, “Realtime data processing at Facebook,” in Proc. Intl. Conf. on Management of Data, 2016, pp. 1087–1098.<br>[7] A. Sharma, “Blog post: Dragon: a distributed graph query engine,” <a href="https://code.facebook.com/posts/1737605303120405/dragon-a-distributed-graph-query-engine/">https://code.facebook.com/posts/1737605303120405/dragon-a-distributed-graph-query-engine/</a>, 2016.<br>[8] <a href="https://www.mongodb.com/">https://www.mongodb.com</a>.<br>[9] <a href="https://yahooeng.tumblr.com/post/120730204806/sherpa-scales-new-heights">https://yahooeng.tumblr.com/post/120730204806/sherpa-scales-new-heights</a>.<br>[10] <a href="https://www.youtube.com/watch?v=plqVpO%CC%B2nSzg">https://www.youtube.com/watch?v=plqVpO̲nSzg</a>.<br>[11] <a href="http://techblog.netix.com/2016/05/application-data-caching-using-ssds.html">http://techblog.netix.com/2016/05/application-data-caching-using-ssds.html</a>.<br>[12] <a href="http://opencompute.org/">http://opencompute.org</a>.<br>[13] M. Athanassoulis, M. S. Kester, L. M. Maas, R. Stoica, S. Idreos, A. Ailamaki, and M. Callaghan, “Designing access methods: the RUM conjecture,” in Proc. Intl. Conf. on Extending Database Technology (EDBT), 2016.<br>[14] P. O’Neil, E. Cheng, D. Gawlick, and E. O’Neil, “The log-structured merge-tree (LSM-tree),” Acta Informatica, vol. 33, no. 4, pp. 351–385, 1996.<br>[15] F. Chang, J. Dean, S. Ghemawat, W. C. Hsieh, D. A. Wallach, M. Burrows, T. Chandra, A. Fikes, and R. E. Gruber, “BigTable: A distributed storage system for structured data,” ACM Trans. on Computer Systems (TOCS), vol. 26, no. 2, p. 4, 2008.<br>[16] A. Lakshman and P. Malik, “Cassandra: a decentralized structured storage system,” ACM SIGOPS Operating Systems Review, vol. 44, no. 2, pp. 35–40, 2010.<br>[17] A. S. Aiyer, M. Bautin, G. J. Chen, P. Damania, P. Khemani, K. Muthukkaruppan, K. Ranganathan, N. Spiegelberg, L. Tang, and M. Vaidya, “Storage infrastructure behind Facebook messages: Using HBase at scale.” IEEE Data Eng. Bull., vol. 35, no. 2, pp. 4–13, 2012.<br>[18] P. A. Bernstein, S. Das, B. Ding, and M. Pilman, “Optimizing optimistic concurrency control for tree-structured, log-structured databases,” in Proc. 2015 ACM SIGMOD Intl. Conf. on Management of Data, 2015, pp. 1295–1309.<br>[19] H. Lim, D. G. Andersen, and M. Kaminsky, “Towards accurate and fast evaluation of multi-stage log-structured designs,” in 14th USENIX Conf. on File and Storage Technologies (FAST 16), Feb. 2016, pp. 149–166.<br>[20] R. Sears and R. Ramakrishnan, “bLSM: a general purpose log structured merge tree,” in Proc. 2012 ACM SIGMOD Intl. Conf. on Management of Data, 2012, pp. 217–228.<br>[21] H. T. Vo, S. Wang, D. Agrawal, G. Chen, and B. C. Ooi, “LogBase: a scalable log-structured database system in the cloud,” Proc. of the VLDB Endowment, vol. 5, no. 10, pp. 1004–1015, 2012.<br>[22] P. Wang, G. Sun, S. Jiang, J. Ouyang, S. Lin, C. Zhang, and J. Cong, “An efficient design and implementation of LSM-tree based key-value store on open-channel SSD,” in Proc. 9th European Conf. on Computer Systems. ACM, 2014, p. 16.<br>[23] T. Hobbs, “Blog post: When to use leveled compaction,” <a href="http://www.datastax.com/dev/blog/when-to-use-leveled-compaction">http://www.datastax.com/dev/blog/when-to-use-leveled-compaction</a>, june 2012.<br>[24] A. C.-C. Yao, “On random 2|3 trees,” Acta Inf., vol. 9, no. 2, pp. 159–170, Jun. 1978.<br>[25] T. G. Armstrong, V. Ponnekanti, D. Borthakur, and M. Callaghan, “LinkBench: A database benchmark based on the Facebook Social Graph,” in Proc. 2013 ACM SIGMOD Intl. Conf. on Management of Data, 2013, pp. 1185–1196.<br>[26] I. Tokutek, “TokuDB: MySQL performance, MariaDB performance,” <a href="http://www.tokutek.com/products/tokudb-for-mysql/">http://www.tokutek.com/products/tokudb-for-mysql/</a>, 2013.</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>在本附录中，我们展示了三个基于 LSM tree 的生产系统中收集的各种统计数据。目的是让读者更好地了解基于 LSM tree 的系统在实践中的行为。</p><p>我们展示的统计数据是从生产环境中运行 MySQL&#x2F;MyRocks 服务器处理社交网络查询的代表性服务器收集的。工作负载是更新密集型的。这里提供的数据是在超过一个月的观察期内收集的。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-6.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-6.png"></p><p><strong>图 6：每个层级的文件数。</strong> 注意 y 轴是对数刻度。未包括 Level 0，因为该级别的文件数在 0 到 4 之间振荡，并且报告的值很大程度上取决于快照的拍摄时间。</p><p>图 6 描述了观察期结束时每个层级的文件数量。图 7 描述了观察期结束时每个层级文件的聚合大小。在这两个图中，不同层级数字之间的相对差异比绝对数字提供更多洞察。图表显示，文件数量和文件的聚合大小在每一级大约增长 10 倍，这正如第 2 节描述所预期的那样。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-7.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-7.png"></p><p><strong>图 7：每个层级所有文件的聚合大小（兆字节）。</strong> 注意 y 轴是对数刻度。出于与图 6 所述相同的原因，未包括 Level 0。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-8.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-8.png"></p><p><strong>图 8：观察期间三个系统在每个层级发生的压实次数。</strong> 注意 y 轴不是对数刻度。</p><p>图 8 显示了观察期间每个层级发生的压实次数。同样，绝对数字意义不大，尤其是它们受配置参数影响很大。左侧的第一组条形代表将 mem-table 复制到 L0 文件的实例。第二组条形代表将所有 L0 文件合并到 L1；因此，每次这样的压实涉及的数据远多于，例如，将 L1 文件合并到 L2 的压实。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174901-9.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174901-9.png"></p><p><strong>图 9：三个系统在每个层级的磁盘写入。</strong> 每个条形的高度代表观察期间每个层级写入磁盘的总字节数。这进一步细分为：(i) 红色：新数据的写入，即键在当前该层级不存在的写入；(ii) 黄色：正在更新的数据的写入，即键已在该层级存在的写入；以及 (iii) 蓝色：从同一层级的现有 SST 复制的数据的写入。y 轴已隐藏，因为绝对数字不具有信息性。y 轴未按对数缩放。</p><p>图 9 描述了观察期间每个层级写入磁盘的数据量（以 GB 为单位），按新数据写入、更新数据写入以及从同一层级现有 SST 复制的数据写入进行细分。对 Level 0 的写入几乎全是新写入，由红色条形表示。需要谨慎解释数据：在 level Li 的新数据写入仅意味着写入数据时该键值对的键在该层级不存在，但该键很可能存在于 level Lj（j &gt; i）中。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174902-1.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174902-1.png"></p><p><strong>图 10：目标读取查询数据的位置。</strong> 每个条形显示从每个可能来源提供服务的读取查询的百分比。注意该图的 y 轴是对数刻度。此图中表示的读取查询是聚合了正在监控的三个系统的查询。79.3% 的所有读取查询由 RocksDB 块缓存提供服务。未由块缓存提供服务的读取查询由 L0、L1、L2、L3 或 L4 SST 文件提供服务。其中一些由操作系统页缓存提供服务，因此不需要任何磁盘访问。不到 10% 的读取查询导致磁盘访问。</p><p><img src="/images/optimizing-space-amplification-in-rocksdb-cn/%E8%AF%91%EF%BD%9COptimizing%20Space%20Amplification%20in%20RocksDB-20250902174902-2.png" alt="译｜Optimizing Space Amplification in RocksDB-20250902174902-2.png"></p><p><strong>图 11：当不在 RocksDB 块缓存中时，目标读取查询数据的位置。</strong> 该图描述了与图 10 相同的数据，但针对的是在 RocksDB 块缓存中未命中的读取访问。注意该图的 y 轴不是对数刻度。该图显示绝大多数（82%）在 RocksDB 块缓存中未命中的读取查询由最后一级 L4 提供服务。对于这些读取查询，有 46% 的数据是从操作系统文件页缓存中获取的。</p><p>图 10 和图 11 描述了读取查询数据提供服务的位置。对于此图，我们汇总了所有三个系统的读取次数。大多数读取请求由 RocksDB 块缓存成功提供服务：对于数据块，命中率为 79.3%，对于包含索引和布隆过滤器的元数据块，命中率为 99.97%。RocksDB 缓存中的未命中会导致文件系统读取。该图显示大多数（52%）的文件系统读取由操作系统页缓存服务，对于级别 L0-L4，页缓存命中率分别为 98%、98%、93%、77% 和 46%。然而，考虑到近 90% 的磁盘存储空间用于保存来自最后一级 L4 的数据，并且 L4 数据的 RocksDB 块缓存命中率较差（92% 的所有在块缓存中未命中的读取查询由 L4 提供服务），这一点很清楚。由于大多数文件访问是针对 L4 SST，显然布隆过滤器有助于减少这些读取查询在 L0–L3 级别的文件访问次数。</p><blockquote><p><strong>版权及翻译声明</strong>：<br>本文翻译自 Siying Dong 等人发表于 CIDR 2017 的论文《Optimizing Space Amplification in RocksDB》。原论文基于 <a href="http://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution License (CC BY 3.0)</a> 许可协议发布，允许在保留对原作者及出处（CIDR 2017）署名的前提下进行分发、复制和演绎。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/04-03-2026/optimizing-space-amplification-in-rocksdb-cn.html">https://www.cyningsun.com/04-03-2026/optimizing-space-amplification-in-rocksdb-cn.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;siying Dong¹, Mark Callaghan¹, Leonidas Galanis¹, Dhruba Borthakur¹, Tony Savor¹ and Michael Stumm²&lt;/p&gt;
&lt;p&gt;¹Facebook, 1 Hacker Way, Menlo</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB BottommostFiles compaction</title>
    <link href="https://www.cyningsun.com/03-28-2026/rocksdb-bottommost-files-compaction.html"/>
    <id>https://www.cyningsun.com/03-28-2026/rocksdb-bottommost-files-compaction.html</id>
    <published>2026-03-27T16:00:00.000Z</published>
    <updated>2026-03-28T08:36:07.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>本文基于 RocksDB v8.8.1 代码与文档，默认使用 leveled compaction 且 <code>allow_ingest_behind=false</code>，分析 BottommostFiles compaction 的工作原理、触发机制和收敛逻辑，并围绕以下问题展开：</p><ol><li>哪些文件会出现在 bottommost_files_ 列表？Bottom 层的文件是不是就是 bottommost_files_？</li><li>如果读写都没有使用快照，BottommostFiles compaction 是否会触发？</li><li>每次释放快照的时候还是每次版本变更的时候会触发 BottommostFiles compaction？</li><li>每次后台 compaction 调度，BottommostFiles compaction 最多会选中多少个 <code>bottommost_files_</code> 列表中的文件？</li><li>BottommostFiles compaction 选择 <code>bottommost_files_</code> 列表中的文件的时候的规则是怎样的？</li><li>BottommostFiles compaction 的 input 和 output level 分别是多少？</li><li>BottommostFiles compaction 是怎么收敛的？</li></ol><p>下面按代码调用链展开，正文会在流程中逐步回答以上问题。</p><h2 id="Bottommost-File-判定规则"><a href="#Bottommost-File-判定规则" class="headerlink" title="Bottommost File 判定规则"></a>Bottommost File 判定规则</h2><p>先区分两个概念：<strong>Bottommost Files 不一定是 Bottom Level Files</strong>。Bottommost Files 的判断标准不是文件所在层级，而是：<strong>该文件的 key range 在所有更低层级中都没有重叠</strong>。BottommostFiles 的判定还有三个前置条件：</p><ul><li><strong>L0 特例</strong>：仅 <code>LevelFiles(0)</code> 中“当前顺序的最后一个文件”（即 <code>last_l0_idx == LevelFiles(0).size()-1</code>）才可能被判定为 bottommost；其余 L0 文件在 <code>RangeMightExistAfterSortedRun()</code> 中会直接返回 <code>true</code>。源码注释说明该行为用于保持历史语义，并提到可改为按 overlap 判断。</li><li><strong>allow_ingest_behind</strong>：启用该选项时，该机制会被跳过（不生成 <code>bottommost_files_</code>，也不会触发 BottommostFiles compaction）。</li><li><strong>适用范围</strong>：该列表由 <code>VersionStorageInfo</code> 生成并持有，<code>LevelCompactionPicker</code> 在 leveled compaction 下消费它，其它 compaction style 不会使用。</li></ul><pre><code class="hljs cpp"><span class="hljs-comment">// 在 GenerateBottommostFiles() 中的判断逻辑</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">VersionStorageInfo::GenerateBottommostFiles</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-built_in">assert</span>(!finalized_);  <span class="hljs-built_in">assert</span>(bottommost_files_.<span class="hljs-built_in">empty</span>());    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> level = <span class="hljs-number">0</span>; level &lt; level_files_brief_.<span class="hljs-built_in">size</span>(); ++level) &#123;    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> file_idx = <span class="hljs-number">0</span>; file_idx &lt; level_files_brief_[level].num_files; ++file_idx) &#123;      <span class="hljs-type">const</span> FdWithKeyRange&amp; f = level_files_brief_[level].files[file_idx];      <span class="hljs-type">int</span> l0_file_idx;      <span class="hljs-keyword">if</span> (level == <span class="hljs-number">0</span>) &#123;        l0_file_idx = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(file_idx);      &#125; <span class="hljs-keyword">else</span> &#123;        l0_file_idx = <span class="hljs-number">-1</span>;      &#125;      Slice smallest_user_key = <span class="hljs-built_in">ExtractUserKey</span>(f.smallest_key);      Slice largest_user_key = <span class="hljs-built_in">ExtractUserKey</span>(f.largest_key);            <span class="hljs-comment">// 关键判断：该文件的 key range 在更低层级是否存在</span>      <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">RangeMightExistAfterSortedRun</span>(smallest_user_key, largest_user_key,                                         <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(level), l0_file_idx)) &#123;        <span class="hljs-comment">// 这是一个 bottommost 文件</span>        bottommost_files_.<span class="hljs-built_in">emplace_back</span>(<span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(level), f.file_metadata);      &#125;    &#125;  &#125;&#125;</code></pre><p>RangeMightExistAfterSortedRun() 的 L0 特例（仅当前顺序最后一个 L0 文件才可能继续判定）：</p><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (last_level == <span class="hljs-number">0</span> &amp;&amp;    last_l0_idx != <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int</span>&gt;(<span class="hljs-built_in">LevelFiles</span>(<span class="hljs-number">0</span>).<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>)) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;&#125;</code></pre><p>例如，考虑以下 LSM 树结构：</p><pre><code class="hljs yaml"><span class="hljs-attr">L2:</span> [<span class="hljs-attr">FileA:</span> <span class="hljs-string">a-m</span>] [<span class="hljs-attr">FileB:</span> <span class="hljs-string">n-z</span>]<span class="hljs-attr">L3:</span> [<span class="hljs-attr">FileC:</span> <span class="hljs-string">a-f</span>] [<span class="hljs-attr">FileD:</span> <span class="hljs-string">g-k</span>] [<span class="hljs-attr">FileE:</span> <span class="hljs-string">x-z</span>]<span class="hljs-attr">L4:</span> [<span class="hljs-attr">FileF:</span> <span class="hljs-string">b-d</span>]</code></pre><p>在这种情况下：</p><ul><li><strong>FileA (L2)</strong>: 不是 bottommost（L3 的 FileC、FileD 与其重叠）</li><li><strong>FileB (L2)</strong>: 不是 bottommost（L3 的 FileE 与其重叠）</li><li><strong>FileC (L3)</strong>: 不是 bottommost（L4 的 FileF 与其重叠）</li><li><strong>FileD (L3)</strong>: <strong>是 bottommost</strong>（没有更低层级与 g-k 重叠）</li><li><strong>FileE (L3)</strong>: <strong>是 bottommost</strong>（没有更低层级与 x-z 重叠）</li><li><strong>FileF (L4)</strong>: <strong>是 bottommost</strong>（已经在最低层级）</li></ul><h2 id="Bottommost-File-生成与标记"><a href="#Bottommost-File-生成与标记" class="headerlink" title="Bottommost File 生成与标记"></a>Bottommost File 生成与标记</h2><p><code>bottommost_files_</code> 在构建新 Version 时生成（<code>PrepareForVersionAppend()</code> → <code>GenerateBottommostFiles()</code>），释放快照不会重新生成该列表。每次安装新 Version（Flush、Compaction、SetOptions、IngestExternalFile、CreateColumnFamily 等，均通过 <code>AppendVersion()</code> 路径）都会触发 <code>ComputeCompactionScore()</code> → <code>ComputeBottommostFilesMarkedForCompaction()</code> 重新计算标记列表；<code>SuggestCompactRange</code>、<code>CompactFilesImpl</code>、<code>BackgroundCompaction</code> 失败重试等路径也会直接调用 <code>ComputeCompactionScore()</code>。</p><p>生成后需经 <code>ComputeBottommostFilesMarkedForCompaction()</code> 标记才能进入 compaction 调度。标记需同时满足：<code>!being_compacted</code>、<code>largest_seqno != 0</code>（尚未归零）、<code>largest_seqno &lt; oldest_snapshot_seqnum_</code>，若设置了 <code>bottommost_file_compaction_delay</code> 还需满足时间条件。未满足序列号条件的文件会更新 <code>bottommost_files_mark_threshold_</code>（取所有未标记文件中 <code>largest_seqno</code> 的最小值），作为下次守卫判断的阈值。</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction</span><span class="hljs-params">(...)</span> </span>&#123;  <span class="hljs-comment">// ...</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; level_and_file : bottommost_files_) &#123;    <span class="hljs-keyword">if</span> (!level_and_file.second-&gt;being_compacted &amp;&amp;        level_and_file.second-&gt;fd.largest_seqno != <span class="hljs-number">0</span>) &#123;      <span class="hljs-keyword">if</span> (level_and_file.second-&gt;fd.largest_seqno &lt; oldest_snapshot_seqnum_) &#123;        <span class="hljs-comment">// 满足序列号条件，检查延迟后加入标记列表</span>        bottommost_files_marked_for_compaction_.<span class="hljs-built_in">push_back</span>(level_and_file);      &#125; <span class="hljs-keyword">else</span> &#123;        <span class="hljs-comment">// 未满足，更新阈值供下次守卫判断</span>        bottommost_files_mark_threshold_ =            std::<span class="hljs-built_in">min</span>(bottommost_files_mark_threshold_,                     level_and_file.second-&gt;fd.largest_seqno);      &#125;    &#125;  &#125;&#125;</code></pre><p>标记条件中的关键变量 <code>oldest_snapshot_seqnum_</code> 通过 <code>UpdateOldestSnapshot()</code> 推进，调用入口是 <code>ReleaseSnapshot()</code>，因此触发节奏与快照释放强相关。<code>ReleaseSnapshot()</code> 有两层守卫：DB 全局层面 <code>oldest_snapshot &gt; DBImpl::bottommost_files_mark_threshold_</code>，CF 层面 <code>oldest_snapshot_seqnum_ &gt; VersionStorageInfo::bottommost_files_mark_threshold_</code>，两层都通过才执行标记。</p><p>需要区分两种场景：如果应用<strong>从未创建过快照</strong>，<code>oldest_snapshot_seqnum_</code> 始终为初始值 0，没有文件能满足 <code>largest_seqno &lt; 0</code>，BottommostFiles compaction 不会被标记触发。但如果应用<strong>创建并释放了所有快照</strong>，<code>ReleaseSnapshot</code> 会将 <code>oldest_snapshot</code> 设为 <code>GetLastPublishedSequence()</code>（当前最新序列号），此时除 <code>largest_seqno == oldest_snapshot</code>、<code>being_compacted == true</code> 或未通过 delay 条件的文件外，其它满足条件的 Bottommost File 会被标记触发。</p><h2 id="Compaction-文件选择"><a href="#Compaction-文件选择" class="headerlink" title="Compaction 文件选择"></a>Compaction 文件选择</h2><p>被标记的文件进入调度后，按以下规则选择压缩目标，流程以“先选一个起始文件”开始，但<strong>单次 compaction 可能包含多个文件</strong>：</p><p>选择起始输入后会调用 <code>ExpandInputsToCleanCut()</code>，在同一层级内扩展以确保 clean cut（注意：RoundRobin 扩展仅适用于 <code>kLevelMaxLevelSize</code> 类型的 compaction，不适用于 BottommostFiles compaction）。</p><p>假设有 5 个文件需要 BottommostFiles compaction，在<strong>不触发 clean-cut 扩展</strong>的简化情况下，（最多）需要 5 次调度才能完成所有文件的压缩。</p><p>BottommostFiles compaction 按 <code>bottommost_files_marked_for_compaction_</code> 的顺序选择起始文件。具体实现在 <code>LevelCompactionBuilder::PickFileToCompact()</code> 中：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 调用链：</span><span class="hljs-comment">// LevelCompactionBuilder::SetupInitialFiles()</span><span class="hljs-comment">//  → 检查 score-based compaction</span><span class="hljs-comment">//  → 检查 files_marked_for_compaction_</span><span class="hljs-comment">//  → PickFileToCompact(BottommostFilesMarkedForCompaction(), false)</span><span class="hljs-comment">//                                                            ^^^^^ </span><span class="hljs-comment">//                                                compact_to_next_level = false</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">LevelCompactionBuilder::PickFileToCompact</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> autovector&lt;std::pair&lt;<span class="hljs-type">int</span>, FileMetaData*&gt;&gt;&amp; level_files,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">bool</span> compact_to_next_level)</span> </span>&#123;    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; level_file : level_files) &#123;    <span class="hljs-comment">// 检查文件是否可以压缩</span>    <span class="hljs-built_in">assert</span>(!level_file.second-&gt;being_compacted);    start_level_ = level_file.first;        <span class="hljs-comment">// 2. 检查层级限制，跳过不符合条件的层级</span>    <span class="hljs-keyword">if</span> ((compact_to_next_level &amp;&amp;         start_level_ == vstorage_-&gt;<span class="hljs-built_in">num_non_empty_levels</span>() - <span class="hljs-number">1</span>) ||        (start_level_ == <span class="hljs-number">0</span> &amp;&amp;         !compaction_picker_-&gt;<span class="hljs-built_in">level0_compactions_in_progress</span>()-&gt;<span class="hljs-built_in">empty</span>())) &#123;      <span class="hljs-keyword">continue</span>;    &#125;            <span class="hljs-keyword">if</span> (compact_to_next_level) &#123;      output_level_ =          (start_level_ == <span class="hljs-number">0</span>) ? vstorage_-&gt;<span class="hljs-built_in">base_level</span>() : start_level_ + <span class="hljs-number">1</span>;    &#125; <span class="hljs-keyword">else</span> &#123;      output_level_ = start_level_; <span class="hljs-comment">// 原地压缩</span>    &#125;        <span class="hljs-comment">// 关键：只选择第一个符合条件的文件</span>    start_level_inputs_.files = &#123;level_file.second&#125;;    start_level_inputs_.level = start_level_;        <span class="hljs-comment">// 尝试扩展输入文件以确保 clean cut，不破坏 SST 文件的原子边界</span>    <span class="hljs-keyword">if</span> (compaction_picker_-&gt;<span class="hljs-built_in">ExpandInputsToCleanCut</span>(cf_name_, vstorage_, &amp;start_level_inputs_)) &#123;      <span class="hljs-keyword">return</span>;  <span class="hljs-comment">// 成功选择文件，立即返回</span>    &#125;  &#125;    <span class="hljs-comment">// 如果没有找到合适的文件，清空输入</span>  start_level_inputs_.files.<span class="hljs-built_in">clear</span>();&#125;</code></pre><p>假设 <code>bottommost_files_marked_for_compaction_</code> 中依次包含 <code>FileA</code>、<code>FileB</code>、<code>FileC</code>、<code>FileD</code>、<code>FileE</code>，并且每次都不触发 clean-cut 扩展或 RoundRobin 扩展，那么 5 次调度会依次以这 5 个文件作为起始输入。</p><h2 id="Compaction-输出层"><a href="#Compaction-输出层" class="headerlink" title="Compaction 输出层"></a>Compaction 输出层</h2><p>选中文件后，compaction 的输出层级由 <code>PickFileToCompact(..., false)</code> 决定：<code>compact_to_next_level = false</code> 时执行 <code>output_level_ = start_level_</code>，因此 BottommostFiles compaction 为原地压缩（Input Level &#x3D; Output Level）。原地压缩后文件仍输出到原层级，可消除的旧版本、墓碑或其它可裁剪内容在 compaction 中处理。</p><h2 id="Compaction-收敛机制"><a href="#Compaction-收敛机制" class="headerlink" title="Compaction 收敛机制"></a>Compaction 收敛机制</h2><p>BottommostFiles compaction 通过<strong>序列号归零</strong>实现收敛：compaction 输出的文件如果所有键的序列号都被归零，<code>largest_seqno</code> 变为 0，就不再满足 <code>largest_seqno != 0</code> 的标记条件，从而退出后续的 BottommostFiles compaction 循环。</p><p><strong>收敛过程示例</strong></p><p>假设当前有 4 个 Bottommost File，<code>largest_seqno</code> 分别不超过 100、200、300、400，而 <code>oldest_snapshot_seqnum_ = 150</code>。此时只有第一个文件满足标记条件，<code>bottommost_files_mark_threshold_</code> 会收敛到其余未达标文件中的最小 <code>largest_seqno</code>，也就是 200。等 <code>oldest_snapshot_seqnum_</code> 后续推进到 250 时，因为它已经越过 200，系统才会重新计算标记列表；这时第二个文件变为可标记对象。之后重复同样过程，直到文件被重写为 <code>largest_seqno == 0</code>，或者始终无法满足归零条件。这个例子对应的正是阈值跨越后才重新标记的节奏。</p><p><strong>序列号归零的前提：Bottommost Level 判定</strong></p><p>序列号归零不限于 BottommostFiles compaction——无论是什么类型的 Compaction，只要输出范围被判定为 Bottommost Level，满足条件的点键序列号都可能被归零。Bottommost Level 的判定基于<strong>本次 compaction 的输入范围</strong>（包括 clean-cut 扩展后的文件范围），而不是单个文件：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">Compaction::IsBottommostLevel</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">int</span> output_level, VersionStorageInfo* vstorage,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> std::vector&lt;CompactionInputFiles&gt;&amp; inputs)</span> </span>&#123;  <span class="hljs-type">int</span> output_l0_idx;  <span class="hljs-keyword">if</span> (output_level == <span class="hljs-number">0</span>) &#123;    output_l0_idx = <span class="hljs-number">0</span>;    <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>* file : vstorage-&gt;<span class="hljs-built_in">LevelFiles</span>(<span class="hljs-number">0</span>)) &#123;      <span class="hljs-keyword">if</span> (inputs[<span class="hljs-number">0</span>].files.<span class="hljs-built_in">back</span>() == file) &#123;        <span class="hljs-keyword">break</span>;      &#125;      ++output_l0_idx;    &#125;    <span class="hljs-built_in">assert</span>(<span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">size_t</span>&gt;(output_l0_idx) &lt; vstorage-&gt;<span class="hljs-built_in">LevelFiles</span>(<span class="hljs-number">0</span>).<span class="hljs-built_in">size</span>());  &#125; <span class="hljs-keyword">else</span> &#123;    output_l0_idx = <span class="hljs-number">-1</span>;  &#125;  Slice smallest_key, largest_key;  <span class="hljs-built_in">GetBoundaryKeys</span>(vstorage, inputs, &amp;smallest_key, &amp;largest_key);  <span class="hljs-keyword">return</span> !vstorage-&gt;<span class="hljs-built_in">RangeMightExistAfterSortedRun</span>(smallest_key, largest_key,                                                  output_level, output_l0_idx);&#125;</code></pre><p>因此，当 <code>ExpandInputsToCleanCut</code> 扩展包含与下层有重叠的文件时，原本的 Bottommost File 可能不会在 Bottommost Level 上进行压缩，序列号归零也就不会发生。</p><p><strong>序列号归零的 8 个条件</strong></p><p>在 Bottommost Level 压缩过程中，<code>CompactionIterator::PrepareOutput()</code> 需要同时满足以下 8 个条件才会将序列号设置为 0：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">CompactionIterator::PrepareOutput</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Valid</span>()) &#123;    <span class="hljs-comment">//...</span>    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Valid</span>() &amp;&amp; compaction_ != <span class="hljs-literal">nullptr</span> &amp;&amp;        !compaction_-&gt;<span class="hljs-built_in">allow_ingest_behind</span>() &amp;&amp;              <span class="hljs-comment">// 条件1：不允许 ingest behind</span>        bottommost_level_ &amp;&amp;                                 <span class="hljs-comment">// 条件2：必须是 bottommost 层级</span>        <span class="hljs-built_in">DefinitelyInSnapshot</span>(ikey_.sequence, earliest_snapshot_) &amp;&amp;  <span class="hljs-comment">// 条件3：序列号小于最早快照</span>        ikey_.type != kTypeMerge &amp;&amp;                          <span class="hljs-comment">// 条件4：不是 Merge 操作</span>        current_key_committed_ &amp;&amp;                            <span class="hljs-comment">// 条件5：键已提交</span>        !output_to_penultimate_level_ &amp;&amp;                     <span class="hljs-comment">// 条件6：不输出到倒数第二层</span>        ikey_.sequence &lt; preserve_time_min_seqno_ &amp;&amp;         <span class="hljs-comment">// 条件7：序列号小于时间保留阈值</span>        !is_range_del_) &#123;                                    <span class="hljs-comment">// 条件8：不是范围删除</span>      ikey_.sequence = <span class="hljs-number">0</span>;      last_key_seq_zeroed_ = <span class="hljs-literal">true</span>;      <span class="hljs-comment">//...</span>    &#125;  &#125;&#125;</code></pre><p>归零后，<code>CompactionOutputs::AddToOutput()</code> 使用归零后的序列号更新输出文件的 <code>FileMetaData.largest_seqno</code>。如果所有键都被归零，<code>largest_seqno</code> 最终为 0，该文件就不再被标记为待压缩。</p><p><strong>特别注意</strong></p><p>在标准的 BottommostFiles compaction 且输出范围最终仍被判定为 Bottommost Level 的场景下，条件 2（<code>bottommost_level_</code>）、条件 3（<code>DefinitelyInSnapshot</code>）、条件 5（<code>current_key_committed_</code>）和条件 8（<code>!is_range_del_</code>）由对应路径状态直接判定；条件 4（<code>ikey_.type != kTypeMerge</code>）是否满足取决于 merge operator 与该键历史，不能假设一定会被折叠为 <code>kTypeValue</code>。条件 6（<code>!output_to_penultimate_level_</code>）与条件 7（<code>ikey_.sequence &lt; preserve_time_min_seqno_</code>）不满足时会直接阻断归零。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>BottommostFiles compaction 是 RocksDB 针对 Bottommost File 的一类定向重写机制。它在 <code>largest_seqno &lt; oldest_snapshot_seqnum_</code> 时才会被标记；如果该次 compaction 的输出范围又被判定为 <em>Bottommost Level</em>，满足条件的点键还可能执行序列号归零。也就是说，BottommostFiles 的“被调度”与 Bottommost Level 的“可归零”是两层不同的判断，它们相关，但并不等价。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/03-28-2026/rocksdb-bottommost-files-compaction.html">https://www.cyningsun.com/03-28-2026/rocksdb-bottommost-files-compaction.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h2&gt;&lt;p&gt;本文基于 RocksDB v8.8.1 代码与文档，默认使用 leveled compaction 且 &lt;code&gt;allow_ingest</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB 磁盘空间管理</title>
    <link href="https://www.cyningsun.com/03-27-2026/rocksdb-space-mgr.html"/>
    <id>https://www.cyningsun.com/03-27-2026/rocksdb-space-mgr.html</id>
    <published>2026-03-26T16:00:00.000Z</published>
    <updated>2026-03-27T15:58:16.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、引言"><a href="#一、引言" class="headerlink" title="一、引言"></a>一、引言</h2><p>本文基于 RocksDB v8.8.1 代码与文档，从代码层面分析 RocksDB 的空间管理机制。默认使用 leveled compaction，<code>SstFileManager</code> 已启用且设置了 <code>max_allowed_space_ &gt; 0</code>（否则其“硬限制”与 <code>compaction_buffer_size_</code> 的常规检查不生效），除特别说明外，默认单 DB 实例，且主要讨论 <code>db_paths[0]</code> &#x2F; <code>cf_paths[0]</code> 路径。</p><p>当空间不足时，RocksDB 会拒绝新的 Compaction 任务以避免磁盘写满。Flush 不做事前空间检查，第一次 Flush 仍会执行；但如果 Flush 完成后触发 <code>SpaceLimit</code>&#x2F;<code>NoSpace</code> 等硬错误，DB 会进入 stopped 状态，后续 Flush（错误恢复 Flush 除外）会被跳过。这仍可能导致 L0 文件堆积，最终触发 Write Stall，写入延迟急剧上升甚至完全停止。这会形成恶性循环：无法执行 Compaction 就无法回收空间，空间越少就越难执行 Compaction，直到服务完全不可用。一个典型的陷阱是”删除数据后空间不减反增”：删除操作只是写入 Tombstone 标记，真正的空间回收要等 Compaction 完成。如果此时空间已经紧张，Compaction 被拒绝，删除操作反而会占用更多空间。</p><p>LSM-Tree 的设计特性决定了 Compaction 过程中原文件和新文件必须同时存在，峰值空间可达输入数据的两倍。RocksDB 通过预留缓冲区机制来应对这一问题，确保关键操作（如 WAL 写入和 Flush）在空间紧张时仍有足够空间可用。</p><h2 id="二、RocksDB-的四种磁盘数据类型"><a href="#二、RocksDB-的四种磁盘数据类型" class="headerlink" title="二、RocksDB 的四种磁盘数据类型"></a>二、RocksDB 的四种磁盘数据类型</h2><p>RocksDB 在磁盘上会产生四种类型的文件，它们各自独立管理，这正是空间管理复杂性的根源。</p><p><strong>SST 文件</strong>是 RocksDB 存储实际数据的地方，由 Flush 和 Compaction 产生。<code>SstFileManager</code> 可以对其跟踪到的 SST&#x2F;Blob 文件施加硬限制。坏消息是，Compaction 的峰值空间需求使得这个”硬限制”并不能简单地等于磁盘容量。</p><p><strong>WAL 文件</strong>（Write-Ahead Log）记录每次写入操作，用于故障恢复。WAL 的限制是”软”的：当达到 <code>max_total_wal_size</code> 阈值时，RocksDB 会触发 Flush 来清理 WAL，但在 Flush 完成之前，WAL 可能继续增长。高写入压力下，WAL 实际大小可能显著超过配置值。</p><p><strong>LOG 文件</strong>是 RocksDB 的运行日志，记录各种操作信息。LOG 通过 <code>max_log_file_size</code>（单文件大小）和 <code>keep_log_file_num</code>（保留数量）控制，总量上限约为两者的乘积。这只是轮转机制，不是硬限制。</p><p><strong>MANIFEST 文件</strong>记录数据库的元数据和版本信息，通常较小，但在频繁变更的场景下也可能累积。</p><p>理解这四种文件的关键在于：<strong>它们由不同的组件独立管理，互不感知</strong>。SST 空间紧张时，不会自动减少 WAL 或 LOG 的配额。这意味着，即使精确控制了 SST 的空间，WAL 或 LOG 的意外增长仍可能导致磁盘写满。</p><h2 id="三、各类型文件的空间控制机制"><a href="#三、各类型文件的空间控制机制" class="headerlink" title="三、各类型文件的空间控制机制"></a>三、各类型文件的空间控制机制</h2><p><img src="/images/rocksdb-space-mgr/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20RocksDB%20%E7%A3%81%E7%9B%98%E7%A9%BA%E9%97%B4%E7%AE%A1%E7%90%86-20260327231429-1.png" alt="深入理解 RocksDB 磁盘空间管理-20260327231429-1.png"></p><h3 id="3-1-SST-文件：SstFileManager-的硬限制"><a href="#3-1-SST-文件：SstFileManager-的硬限制" class="headerlink" title="3.1 SST 文件：SstFileManager 的硬限制"></a>3.1 SST 文件：SstFileManager 的硬限制</h3><p>SST 文件的空间控制是 RocksDB 中最完善的部分。<code>SstFileManager</code> 通过 <code>max_allowed_space_</code> 设置其跟踪文件（SST&#x2F;Blob）的总量上限。当前实现中，Flush&#x2F;Compaction 输出上报与 Open 扫描主要覆盖 <code>db_paths[0]/cf_paths[0]</code>（<code>path_id=0</code>），多路径场景需特别注意统计口径。</p><p><strong>写入过程的空间检查分为四个阶段：</strong></p><ol><li><strong>写入阶段</strong>：用户写入时<strong>不检查</strong> <code>max_allowed_space</code>，数据直接写入 MemTable 和 WAL</li><li><strong>Flush 阶段</strong>：MemTable Flush 成 SST 后，调用 <code>IsMaxAllowedSpaceReached()</code> 检查 <code>total_files_size_ &gt;= max_allowed_space_</code>，如果超限则设置 <code>BGError</code> 为 <code>Status::SpaceLimit</code></li><li><strong>后续写入</strong>：触发错误的当前写请求（如 WAL append 失败）会直接失败；后续写入是否在 <code>PreprocessWrite()</code> 阶段被拒绝，取决于 DB 是否已进入 stopped 状态</li><li><strong>后续 Flush</strong>：若错误严重级别达到 HardError（默认 <code>paranoid_checks=true</code> 时，Flush 路径的 <code>SpaceLimit/NoSpace</code> 会映射为 HardError），<code>is_db_stopped_</code> 会被置为 <code>true</code>，普通 Flush 在 <code>IsBGWorkStopped()</code> 为 true 时会被跳过（错误恢复 Flush 除外）</li></ol><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">SstFileManagerImpl::IsMaxAllowedSpaceReached</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-function">MutexLock <span class="hljs-title">l</span><span class="hljs-params">(&amp;mu_)</span></span>;  <span class="hljs-keyword">if</span> (max_allowed_space_ &lt;= <span class="hljs-number">0</span>) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;  &#125;  <span class="hljs-keyword">return</span> total_files_size_ &gt;= max_allowed_space_;&#125;</code></pre><p>这种异步检查机制意味着，当空间真正不足时，可能已经有部分数据写入了 MemTable 和 WAL；当前请求可能先失败，而后续写入是否被入口直接拒绝取决于 DB 是否已 stopped。</p><p>相比 Flush 的<strong>事后异步检查</strong>，Compaction 采用的是<strong>事前同步检查</strong>：在 Compaction 任务开始执行前，会调用 <code>EnoughRoomForCompaction()</code> 预先验证是否有足够空间完成整个操作，如果空间不足则直接拒绝该 Compaction 任务。从代码可以看出，Flush 必须尽可能执行（即使可能导致空间超限），而 Compaction 可以延后甚至取消。核心检查逻辑如下：</p><pre><code class="hljs cpp"><span class="hljs-comment">// file/sst_file_manager_impl.cc:148-205</span><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">SstFileManagerImpl::EnoughRoomForCompaction</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    ColumnFamilyData* cfd, <span class="hljs-type">const</span> std::vector&lt;CompactionInputFiles&gt;&amp; inputs,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> Status&amp; bg_error)</span> </span>&#123;  <span class="hljs-function">MutexLock <span class="hljs-title">l</span><span class="hljs-params">(&amp;mu_)</span></span>;  <span class="hljs-type">uint64_t</span> size_added_by_compaction = <span class="hljs-number">0</span>;  <span class="hljs-comment">// First check if we even have the space to do the compaction</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; inputs.<span class="hljs-built_in">size</span>(); i++) &#123;    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> j = <span class="hljs-number">0</span>; j &lt; inputs[i].<span class="hljs-built_in">size</span>(); j++) &#123;      FileMetaData* filemeta = inputs[i][j];      size_added_by_compaction += filemeta-&gt;fd.<span class="hljs-built_in">GetFileSize</span>();    &#125;  &#125;  <span class="hljs-comment">// Update cur_compactions_reserved_size_ so concurrent compaction</span>  <span class="hljs-comment">// don&#x27;t max out space</span>  <span class="hljs-type">size_t</span> needed_headroom = cur_compactions_reserved_size_ +                           size_added_by_compaction + compaction_buffer_size_;  <span class="hljs-keyword">if</span> (max_allowed_space_ != <span class="hljs-number">0</span> &amp;&amp;      (needed_headroom + total_files_size_ &gt; max_allowed_space_)) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;  &#125;  <span class="hljs-comment">// Implement more aggressive checks only if this DB instance has already</span>  <span class="hljs-comment">// seen a NoSpace() error. ...</span>  <span class="hljs-keyword">if</span> (bg_error.<span class="hljs-built_in">IsNoSpace</span>() &amp;&amp; <span class="hljs-built_in">CheckFreeSpace</span>()) &#123;    <span class="hljs-comment">// ... 获取文件路径 fn ...</span>    <span class="hljs-type">uint64_t</span> free_space = <span class="hljs-number">0</span>;    Status s = fs_-&gt;<span class="hljs-built_in">GetFreeSpace</span>(fn, <span class="hljs-built_in">IOOptions</span>(), &amp;free_space, <span class="hljs-literal">nullptr</span>);    s.<span class="hljs-built_in">PermitUncheckedError</span>();  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Check the status</span>    <span class="hljs-comment">// ... If user didn&#x27;t specify any compaction buffer, add reserved_disk_buffer_</span>    <span class="hljs-keyword">if</span> (compaction_buffer_size_ == <span class="hljs-number">0</span>) &#123;      needed_headroom += reserved_disk_buffer_;    &#125;    <span class="hljs-keyword">if</span> (free_space &lt; needed_headroom + size_added_by_compaction) &#123;      <span class="hljs-comment">// ... 日志记录 ...</span>      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;    &#125;  &#125;  cur_compactions_reserved_size_ += size_added_by_compaction;  <span class="hljs-comment">// Take a snapshot of cur_compactions_reserved_size_ for when we encounter</span>  <span class="hljs-comment">// a NoSpace error.</span>  free_space_trigger_ = cur_compactions_reserved_size_;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;&#125;</code></pre><p>理解上述检查逻辑的关键在于明确 <code>total_files_size_</code> 的统计口径。<code>SstFileManager</code> 的 <code>total_files_size_</code> 是 <code>tracked_files_</code> 的求和，由 <code>OnAddFile/OnDeleteFile</code> 驱动，因此它统计的是”被 SstFileManager 注册过”的文件，而不是”某个 Version 视角下的全部文件”。通常会覆盖：当前 Version 使用中的 SST&#x2F;Blob、旧 Version 被 Iterator&#x2F;Snapshot 持有的文件、尚未物理删除的 obsolete 文件、以及已 <code>OnAddFile</code> 但尚未安装到 Version 的临时输出文件；它不包含正在生成但尚未 <code>OnAddFile</code> 的文件。</p><p>需要特别注意两点。第一，<code>DB::Open</code> 只会扫描 <code>db_paths[0]</code> 和各 Column Family 的 <code>cf_paths[0]</code> 来注册现有 SST&#x2F;Blob 文件，可能包含不在 MANIFEST 中的孤立文件。第二，Compaction 输出仅在 <code>path_id == 0</code> 时才会上报给 <code>SstFileManager</code>，因此多路径场景下 <code>total_files_size_</code> 与真实磁盘占用可能存在系统性偏差。</p><p>与之对比，<code>rocksdb.live-sst-files-size</code> 是 Column Family current Version 视角，仅统计 live SST；<code>rocksdb.total-sst-files-size</code> 是该 Column Family 所有 Version 去重后的 SST 总和。它们与 <code>SstFileManager</code> 的 <code>total_files_size_</code> 在对象范围和路径范围上都不同，因此不宜简单用 <code>total_files_size_ - live_sst_files_size</code> 推断 obsolete SST 大小。</p><p>对于可回收空间的精确监控，SST 部分建议直接使用 <code>rocksdb.obsolete-sst-files-size</code>；Blob 部分 RocksDB 未提供对应的 obsolete 指标，<code>rocksdb.total-blob-file-size - rocksdb.live-blob-file-size</code> 更接近”非 current-version blob 规模”，不等价于”待删除 blob 空间”。若启用 BlobDB，应结合 <code>live-blob-file-size</code> &#x2F; <code>total-blob-file-size</code> 观察整体空间使用情况。</p><p>上述代码中 <code>compaction_buffer_size_</code> 和 <code>reserved_disk_buffer_</code> 的具体作用和触发条件，详见 §3.4。</p><p>当且仅当配置了 <code>max_allowed_space_ &gt; 0</code> 时，业务可用空间可以从两个视角理解。从配置规划角度，<strong>业务理论最大可用空间</strong>约为 <code>max_allowed_space_ - compaction_buffer_size_</code>。从运行时监控角度，<strong>当前实际剩余空间</strong>约为 <code>max_allowed_space_ - total_files_size_ - compaction_buffer_size_</code>，反映了在 <code>SstFileManager</code> 统计口径下，扣除已使用空间与保护缓冲区后还能写入的数据规模。</p><h3 id="3-2-WAL-文件：软限制与生命周期"><a href="#3-2-WAL-文件：软限制与生命周期" class="headerlink" title="3.2 WAL 文件：软限制与生命周期"></a>3.2 WAL 文件：软限制与生命周期</h3><p>WAL 的空间控制涉及三个参数：<code>max_total_wal_size</code> 控制活跃 WAL 的总大小，超过时触发 Flush；<code>WAL_ttl_seconds</code> 和 <code>WAL_size_limit_MB</code> 控制归档 WAL 的清理策略。需要注意 <code>max_total_wal_size</code> 主要用于多 Column Family 场景触发 Flush；单 Column Family 场景下，WAL 大小更多受 MemTable&#x2F;Flush 节奏影响。</p><p>如果不显式设置 <code>max_total_wal_size</code>，RocksDB 会使用默认值：</p><pre><code class="hljs abnf">默认值 <span class="hljs-operator">=</span> [Σ write_buffer_size × max_write_buffer_number] × <span class="hljs-number">4</span></code></pre><p>需要注意的是，WAL 的限制是”软”的——超过阈值时触发 Flush，而非阻止写入。在 Flush 完成之前，WAL 仍会继续增长，高写入压力下可能显著超过配置值。</p><p><strong>WAL 写入失败的严重后果：</strong></p><p>与 SST 文件不同，WAL 写入失败会让<strong>当前写请求立即失败</strong>。当系统磁盘空间耗尽，<code>WriteToWAL()</code> 返回 <code>IOStatus::NoSpace()</code> 时，会触发以下处理流程：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db/db_impl/db_impl_write.cc:756-761</span><span class="hljs-keyword">if</span> (!io_s.<span class="hljs-built_in">ok</span>()) &#123;  <span class="hljs-comment">// Check WriteToWAL status</span>  <span class="hljs-built_in">IOStatusCheck</span>(io_s); <span class="hljs-comment">// 立即设置 BGError</span>&#125;<span class="hljs-comment">// ... else 分支省略 ...</span><span class="hljs-comment">// db/db_impl/db_impl_write.cc:1117-1131</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::IOStatusCheck</span><span class="hljs-params">(<span class="hljs-type">const</span> IOStatus&amp; io_status)</span> </span>&#123;  <span class="hljs-comment">// Is setting bg_error_ enough here?  This will at least stop</span>  <span class="hljs-comment">// compaction and fail any further writes.</span>  <span class="hljs-keyword">if</span> ((immutable_db_options_.paranoid_checks &amp;&amp; !io_status.<span class="hljs-built_in">ok</span>() &amp;&amp;       !io_status.<span class="hljs-built_in">IsBusy</span>() &amp;&amp; !io_status.<span class="hljs-built_in">IsIncomplete</span>()) ||      io_status.<span class="hljs-built_in">IsIOFenced</span>()) &#123;    mutex_.<span class="hljs-built_in">Lock</span>();    <span class="hljs-comment">// Maybe change the return status to void?</span>    error_handler_.<span class="hljs-built_in">SetBGError</span>(io_status, BackgroundErrorReason::kWriteCallback);    mutex_.<span class="hljs-built_in">Unlock</span>();  &#125; <span class="hljs-keyword">else</span> &#123;    <span class="hljs-comment">// Force writable file to be continue writable.</span>    logs_.<span class="hljs-built_in">back</span>().writer-&gt;<span class="hljs-built_in">file</span>()-&gt;<span class="hljs-built_in">reset_seen_error</span>();  &#125;&#125;</code></pre><p>当 <code>IOStatusCheck()</code> 将错误上报给 ErrorHandler 且错误严重级别升级到 HardError 时，DB 会进入 stopped，后续写入会在 <code>PreprocessWrite()</code> 入口返回该后台错误。若 <code>paranoid_checks=false</code>，则同样的 WAL 错误不一定会触发这一停库路径。</p><h3 id="3-3-LOG-文件"><a href="#3-3-LOG-文件" class="headerlink" title="3.3 LOG 文件"></a>3.3 LOG 文件</h3><p>LOG 文件通过 <code>max_log_file_size</code>（单文件大小）和 <code>keep_log_file_num</code>（保留数量）控制，总量约为两者的乘积。这只是轮转机制，不是硬限制。</p><h3 id="3-4-CompactionBufferSize：主要的跨系统保护"><a href="#3-4-CompactionBufferSize：主要的跨系统保护" class="headerlink" title="3.4 CompactionBufferSize：主要的跨系统保护"></a>3.4 CompactionBufferSize：主要的跨系统保护</h3><p><strong>核心问题</strong>：SST、WAL、LOG 由不同组件独立管理、互不感知。当 SST 空间紧张时，不会自动为 WAL、LOG、MANIFEST 等文件预留空间。如 §3.1 所述，Flush 无事前检查、仅事后检查，Compaction 同时存在事前和事后检查。这意味着 Compaction 可能因空间不足被拒绝，但 Flush 和 WAL 写入仍需继续执行——如果此时没有预留空间，可能导致磁盘写满。</p><p><strong>解决方案</strong>：<code>compaction_buffer_size_</code> 是 RocksDB 的主要跨系统保护旋钮。需要注意，它在 <code>SstFileManager</code> 常规空间检查中稳定生效的前提是 <code>max_allowed_space_ &gt; 0</code>。在该前提下，它会在 SST 空间检查时预留一块缓冲区，给以下文件和场景留出余量：WAL 文件（包括 Active 和 Archived）、LOG 文件（RocksDB 操作日志）、MANIFEST 文件（数据库元数据）、Flush 过程中正在生成但尚未调用 <code>OnAddFile</code> 的 SST 文件、Compaction 过程中正在生成但尚未调用 <code>OnAddFile</code> 的输出 SST 文件。</p><p><strong>实现细节</strong>：<code>compaction_buffer_size_</code> 是用户显式设置的保护缓冲区大小，在正常检查中会直接参与 <code>needed_headroom</code> 计算。<code>reserved_disk_buffer_</code> 由 <code>ReserveDiskBuffer()</code> 累加：单 DB 实例时通常等于该实例所有 Column Family 的最大 <code>write_buffer_size</code>；若多个 DB 共享同一个 <code>SstFileManager</code>，则会叠加多个实例的预留值。它只在满足全部条件时才会额外加入检查：当前 <code>bg_error</code> 为 <code>IOError::NoSpace</code>（<code>SpaceLimit</code> 不满足）、<code>SstFileManager</code> 内部错误严重级别为 <code>kSoftError</code>、且 <code>compaction_buffer_size_</code> 为 0。</p><h2 id="四、影响磁盘空间的操作分析"><a href="#四、影响磁盘空间的操作分析" class="headerlink" title="四、影响磁盘空间的操作分析"></a>四、影响磁盘空间的操作分析</h2><h3 id="4-1-常规操作的空间影响"><a href="#4-1-常规操作的空间影响" class="headerlink" title="4.1 常规操作的空间影响"></a>4.1 常规操作的空间影响</h3><p><strong>写入操作</strong>会立即增加 WAL 大小，同时 LOG 记录操作日志。数据先写入内存中的 MemTable，此时不占用 SST 空间。</p><p><strong>删除操作</strong>不会立即释放空间，而是写入 Tombstone 标记，反而会短暂增加空间占用。真正的空间回收要等 Compaction 将 Tombstone 与原数据合并后才能完成。</p><p><strong>Flush 操作</strong>将 MemTable 持久化为 SST 文件。Flush 完成后，当所有引用该 WAL 的 Column Family 都已 flush 时，该 WAL 变为 obsolete，可被清理（如果配置了 <code>WAL_ttl_seconds</code> 或 <code>WAL_size_limit_MB</code> 则先归档，否则直接删除）。</p><h3 id="4-2-高风险操作详解"><a href="#4-2-高风险操作详解" class="headerlink" title="4.2 高风险操作详解"></a>4.2 高风险操作详解</h3><p><strong>DB::Open()</strong> 在特定配置下可能触发大规模层级重整。最典型的场景是启用 <code>level_compaction_dynamic_level_bytes</code> 后首次打开数据库：RocksDB 会自动执行 Trivial Move 来调整层级结构。Trivial Move 本身只是元数据操作（VersionEdit），不复制 SST 文件，磁盘空间不会因此变化。但层级重整后，大量文件被移动到底层，随后触发的底层 Compaction 才是真正的空间风险所在（见 §4.4）。</p><pre><code class="hljs cpp"><span class="hljs-comment">// db/db_impl/db_impl_open.cc:582-641</span><span class="hljs-comment">// 当启用 level_compaction_dynamic_level_bytes=true 时</span><span class="hljs-comment">// DB 打开时会自动进行 trivial move，完全不检查 max_compaction_bytes</span><span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> from_level = to_level; from_level &gt;= <span class="hljs-number">0</span>; --from_level) &#123;  <span class="hljs-type">const</span> std::vector&lt;FileMetaData*&gt;&amp; level_files =      cfd-&gt;<span class="hljs-built_in">current</span>()-&gt;<span class="hljs-built_in">storage_info</span>()-&gt;<span class="hljs-built_in">LevelFiles</span>(from_level);  <span class="hljs-keyword">if</span> (level_files.<span class="hljs-built_in">empty</span>() || from_level == <span class="hljs-number">0</span>) &#123;    <span class="hljs-keyword">continue</span>;  &#125;  <span class="hljs-comment">// ... assert 省略 ...</span>  <span class="hljs-comment">// Trivial move files from `from_level` to `to_level`</span>  <span class="hljs-keyword">if</span> (from_level &lt; to_level) &#123;    VersionEdit edit;    edit.<span class="hljs-built_in">SetColumnFamily</span>(cfd-&gt;<span class="hljs-built_in">GetID</span>());    <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> FileMetaData* f : level_files) &#123;      edit.<span class="hljs-built_in">DeleteFile</span>(from_level, f-&gt;fd.<span class="hljs-built_in">GetNumber</span>());      edit.<span class="hljs-built_in">AddFile</span>(to_level, f-&gt;fd.<span class="hljs-built_in">GetNumber</span>(), f-&gt;fd.<span class="hljs-built_in">GetPathId</span>(),                   f-&gt;fd.<span class="hljs-built_in">GetFileSize</span>(), f-&gt;smallest, f-&gt;largest,                   <span class="hljs-comment">/* ... 其他元数据参数 ... */</span>);    &#125;    recovery_ctx-&gt;<span class="hljs-built_in">UpdateVersionEdits</span>(cfd, edit);  &#125;  --to_level;&#125;</code></pre><p><strong>CompactRange()</strong> 在 Leveled Compaction 下，<code>input_level &gt; 0</code> 时文件选择阶段受 <code>max_compaction_bytes</code> 约束；但 <code>input_level == 0</code> 时不受此限制（L0 文件可重叠，必须全部选入）。尽管单次 compaction 有大小限制，全范围压缩 <code>CompactRange(nullptr, nullptr)</code> 会逐层执行多次 compaction，整体仍可能导致大量数据参与，峰值空间占用可达原始数据的两倍（输入文件 + 输出文件同时存在）。</p><p><strong>CompactFiles()</strong> 允许用户直接指定参与压缩的文件列表，绕过了 RocksDB 的自动选择逻辑。如果选择的文件集合过大，同样可能导致严重的空间问题。</p><p><strong>IngestExternalFile()</strong> 导入外部 SST 文件时，内部会构造用于范围冲突管理的”等价 Compaction”对象，该对象使用 <code>LLONG_MAX</code>（并非真实重写任务本身）。真正的空间风险在于：外部文件若相互重叠会落入 L0，随后触发的大型自动压缩可能带来峰值空间压力。</p><h3 id="4-3-自动-Compaction-的空间控制"><a href="#4-3-自动-Compaction-的空间控制" class="headerlink" title="4.3 自动 Compaction 的空间控制"></a>4.3 自动 Compaction 的空间控制</h3><p>自动 Compaction 相对可控，主要受 <code>max_compaction_bytes</code> 约束。这个参数的默认值是 <code>target_file_size_base * 25 = 1.6GB</code>，目的是<strong>控制单次 Compaction 的资源消耗</strong>（临时磁盘空间、I&#x2F;O 带宽、执行时间）。在 Compaction 选择阶段，计算”Ln 层选中的文件大小 + Ln+1 层中与之重叠的文件大小”，总和超过 <code>max_compaction_bytes</code> 就停止扩展更多文件。在 Trivial Move 场景下，还会检查”移动到 Ln+1 的文件大小 + Ln+2 层重叠文件大小”是否超过 <code>max_compaction_bytes</code>，<strong>防止后续 Ln+1 → Ln+2 的 Compaction 过大</strong>。</p><p>但该限制并非硬性的：为了保证 Clean Cut（文件边界对齐），可能必须包含额外文件；某些情况下为了数据一致性，必须将特定文件纳入同一次 Compaction。因此 <code>max_compaction_bytes</code> 更像是”尽量遵守”的软约束。</p><h3 id="4-4-底层-Compaction-的特殊性"><a href="#4-4-底层-Compaction-的特殊性" class="headerlink" title="4.4 底层 Compaction 的特殊性"></a>4.4 底层 Compaction 的特殊性</h3><p>最底层 Compaction 通常没有更低层可作为祖父层（Grandparent），因此”基于祖父层重叠边界的切分优化”在这里不适用。但输出文件仍受 <code>max_output_file_size</code> 和输出切分逻辑约束，并非完全无边界。</p><p>该特性会与 <code>DB::Open()</code> 的 Trivial Move 形成风险叠加：启用 <code>level_compaction_dynamic_level_bytes</code> 后，Open 阶段的 Trivial Move 不检查 <code>max_compaction_bytes</code>，可能把大量文件下沉到底层，随后底层 Compaction 一次处理的数据规模变大，峰值空间可能接近翻倍。</p><h2 id="五、空间相关的监控指标"><a href="#五、空间相关的监控指标" class="headerlink" title="五、空间相关的监控指标"></a>五、空间相关的监控指标</h2><p>RocksDB 提供了以下与空间管理直接相关的内部指标：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 1. Live SST Files Size（当前 Version 中的有效数据）</span>db-&gt;<span class="hljs-built_in">GetProperty</span>(<span class="hljs-string">&quot;rocksdb.live-sst-files-size&quot;</span>, &amp;live_size);<span class="hljs-comment">// 说明：仅包含 current_ Version 中所有层级的文件，不包含 obsolete 和临时文件</span><span class="hljs-comment">// 2. Total SST Files Size（该 Column Family 所有 Version 中的 SST 文件）</span>db-&gt;<span class="hljs-built_in">GetProperty</span>(<span class="hljs-string">&quot;rocksdb.total-sst-files-size&quot;</span>, &amp;total_size);<span class="hljs-comment">// 说明：遍历该 Column Family 的所有 Version（含被 Iterator/Snapshot 持有的旧 Version），去重求和</span><span class="hljs-comment">// 注意：这不等于 SstFileManager 的 total_files_size_（后者还包含 obsolete 文件和</span><span class="hljs-comment">// 已 OnAddFile 但尚未 Install 的临时文件，正常情况下差距很小）</span><span class="hljs-comment">// 3. Pending Compaction Bytes（等待 compaction 的字节数）</span>db-&gt;<span class="hljs-built_in">GetProperty</span>(<span class="hljs-string">&quot;rocksdb.estimate-pending-compaction-bytes&quot;</span>, &amp;pending_bytes);<span class="hljs-comment">// 说明：Level Compaction 下，需要重写的数据量估算</span><span class="hljs-comment">// 4. L0 Files Count（L0 层文件数量）</span>db-&gt;<span class="hljs-built_in">GetProperty</span>(<span class="hljs-string">&quot;rocksdb.num-files-at-level0&quot;</span>, &amp;l0_files);<span class="hljs-comment">// 说明：L0 积压是空间不足的典型表现</span><span class="hljs-comment">// 当 compaction 被取消时，L0 文件会持续堆积</span><span class="hljs-comment">// 5. Compaction Cancelled（累计取消的 compaction 次数）</span><span class="hljs-type">uint64_t</span> cancelled = statistics-&gt;<span class="hljs-built_in">getTickerCount</span>(COMPACTION_CANCELLED);<span class="hljs-comment">// 触发条件：EnoughRoomForCompaction() 返回 false</span><span class="hljs-comment">// 这是最直接的&quot;磁盘空间不足&quot;信号</span><span class="hljs-comment">// 6. Obsolete SST Files Size（已 obsolete 但尚未删除的 SST）</span>db-&gt;<span class="hljs-built_in">GetProperty</span>(<span class="hljs-string">&quot;rocksdb.obsolete-sst-files-size&quot;</span>, &amp;obsolete_sst_bytes);<span class="hljs-comment">// 说明：这是待删 SST 的直接观测指标（全 DB 口径）</span></code></pre><h2 id="六、总结"><a href="#六、总结" class="headerlink" title="六、总结"></a>六、总结</h2><p><code>compaction_buffer_size_</code> 是空间管理的核心保护机制之一。当 <code>compaction_buffer_size_</code> 为 0 时，正常检查不会有额外缓冲区保护（<code>reserved_disk_buffer_</code> 仅在 <code>bg_error</code> 为 <code>IOError::NoSpace</code> 且 <code>SstFileManager</code> 内部错误为 <code>kSoftError</code> 时才会生效）。</p><p>RocksDB 的空间管理有以下关键特征：</p><ol><li><strong>SstFileManager 提供的是”按其跟踪范围”的硬限制</strong>：通过 <code>max_allowed_space_</code> 控制跟踪到的 SST&#x2F;Blob 文件。<strong>风险</strong>：路径覆盖差异与 WAL&#x2F;LOG 的意外增长仍可能导致磁盘写满。</li><li><strong>Flush 和 Compaction 的空间检查策略不同</strong>：Flush 无事前检查且在事后超限时可能停库并跳过后续普通 Flush；Compaction 同时存在事前（<code>EnoughRoomForCompaction()</code>）和事后（输出后 <code>IsMaxAllowedSpaceReached()</code>）检查。<strong>影响</strong>：L0 易堆积并触发 Write Stall。</li><li><strong>四种文件类型独立管理、互不感知</strong>：<code>compaction_buffer_size_</code> 是主要的跨系统保护旋钮，但在 <code>max_allowed_space_ &gt; 0</code> 的常规检查路径下才稳定生效。<strong>风险</strong>：配置为 0 或过小会导致 WAL&#x2F;LOG&#x2F;MANIFEST 保护不足。</li><li><strong>手动操作（CompactRange&#x2F;CompactFiles&#x2F;IngestExternalFile）的空间约束弱于自动 Compaction</strong>。<strong>风险</strong>：全范围压缩或大文件集合可能导致峰值空间接近翻倍，需谨慎使用。</li><li><strong>底层 Compaction 不适用祖父层边界优化</strong>：与 DB::Open 的 Trivial Move 叠加时风险最大。<strong>影响</strong>：启用 <code>level_compaction_dynamic_level_bytes</code> 后首次打开可能触发大规模底层 Compaction，峰值空间需求显著增加。</li></ol><hr><h2 id="附录：运维经验与配置参考"><a href="#附录：运维经验与配置参考" class="headerlink" title="附录：运维经验与配置参考"></a>附录：运维经验与配置参考</h2><blockquote><p>以下内容基于运维经验和假设场景，非 RocksDB 代码内置逻辑，仅供参考。</p></blockquote><h3 id="A-生产环境利用率分析"><a href="#A-生产环境利用率分析" class="headerlink" title="A. 生产环境利用率分析"></a>A. 生产环境利用率分析</h3><p>通过合理设置 <code>compaction_buffer_size_</code> 等参数，只允许利用预留的空间进行 Compaction，业务数据理论上可以占到 <code>max_allowed_space_</code> 的 90% 以上（详见 §C 配置示例）。</p><p>然而，生产环境的实际利用率会更低：</p><ol><li><strong>扩容水位</strong>：如果采用<strong>小的逻辑隔离集群</strong>而非大的共享集群，通常在 70～80% 时触发扩容告警（业界常见做法），预留充足的时间进行数据迁移，避免接近上限</li><li><strong>主从复制</strong>：主从断连需要全量同步时，从节点必须同时保留旧数据和新数据，空间需求翻倍。除非重新新建从节点替代的旧从节点（增加运维负担），或者使用 RAFT&#x2F;Paxos 等 3 副本方案（可安全丢弃旧数据），否则双副本架构下 <code>max_allowed_space_</code> 可能只能设为磁盘容量的 50%</li></ol><p>以 1TB 专用数据盘为例，格式化为 ext4 后空间约 940GB。配置 <code>max_allowed_space_</code> &#x3D; 900GB（含 30GB 的 <code>compaction_buffer</code>，在 900GB 内预留）：</p><ul><li><strong>无主从复制场景</strong>：业务实际可用约 <strong>610～700GB</strong></li><li><strong>主从复制场景</strong>：<code>max_allowed_space_</code> 需限制到 450GB，业务实际可用约 <strong>280～320GB</strong></li></ul><p><strong>从平台产品角度看使用率更低</strong>：上述计算基于扩容水位（70～80%），但实际运维中，业务不会持续保持在高水位并频繁扩容。触发扩容告警后，增加容量会使使用率大幅下降，平台的平均磁盘使用率往往只有 **40-60%**（经验估算）。以 1TB 磁盘为例，按前文无主从复制场景的 610～700GB 可用空间和 40-60% 平均水位计算，长期平均业务数据量约 <strong>250～420GB</strong>。</p><p>此外，如果采用<strong>小的逻辑隔离集群</strong>而非大的共享集群，还会产生<strong>集群外碎片</strong>问题：集群规格通常是固定的（如 100GB、200GB、500GB 等档位），用户实际数据可能只需 80GB，却不得不购买 100GB 规格，造成约 20% 的容量浪费。大共享集群可以更灵活地分配资源，但隔离性较差。这是平台产品设计中的经典权衡。</p><h3 id="B-CompactionBufferSize-参考公式"><a href="#B-CompactionBufferSize-参考公式" class="headerlink" title="B. CompactionBufferSize 参考公式"></a>B. CompactionBufferSize 参考公式</h3><pre><code class="hljs cpp"><span class="hljs-comment">// 参考公式（非 RocksDB 内置逻辑，需根据实际场景调整）：</span><span class="hljs-comment">// WAL 峰值 + LOG 峰值 + 安全边际</span>compaction_buffer_size ≈ max_total_wal_size + (WAL_size_limit_MB / <span class="hljs-number">1024</span>)                         + (max_log_file_size × keep_log_file_num / GB)                         + safety_margin</code></pre><h3 id="C-配置示例（1TB-专用数据盘）"><a href="#C-配置示例（1TB-专用数据盘）" class="headerlink" title="C. 配置示例（1TB 专用数据盘）"></a>C. 配置示例（1TB 专用数据盘）</h3><p>假设有一块 1TB 的专用数据盘（系统使用独立磁盘），格式化为 ext4 文件系统，该如何配置 RocksDB，能存储多少业务数据？</p><p>1TB 原始磁盘格式化为 ext4 后（默认参数），扣除保留块（5%）、元数据（1%）等开销约 60GB，<strong>普通用户实际可用约 940GB</strong>。以此为基础配置 RocksDB：</p><p><strong>配置方案（无主从复制）</strong>：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 1. SST 空间控制</span><span class="hljs-keyword">auto</span> sfm = <span class="hljs-built_in">NewSstFileManager</span>(env);sfm-&gt;<span class="hljs-built_in">SetMaxAllowedSpaceUsage</span>(<span class="hljs-number">900ULL</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);  <span class="hljs-comment">// 900GB（SST 硬限制）</span>sfm-&gt;<span class="hljs-built_in">SetCompactionBufferSize</span>(<span class="hljs-number">30ULL</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>);   <span class="hljs-comment">// 30GB（在 900GB 内预留）</span><span class="hljs-comment">// 2. WAL 空间控制（考虑主从复制场景）</span>options.max_total_wal_size = <span class="hljs-number">512ULL</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>;     <span class="hljs-comment">// 512MB Active WAL（主从延迟/断连容忍）</span>options.WAL_ttl_seconds = <span class="hljs-number">21600</span>;                             <span class="hljs-comment">// 6 小时 TTL（避免从节点短期断连需要全量同步）</span>options.WAL_size_limit_MB = <span class="hljs-number">16384</span>;                           <span class="hljs-comment">// 16GB Archived 限制（对应 6 小时产生量）</span><span class="hljs-comment">// 3. LOG 空间控制</span>options.max_log_file_size = <span class="hljs-number">512</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>;               <span class="hljs-comment">// 512MB/文件</span>options.keep_log_file_num = <span class="hljs-number">8</span>;                               <span class="hljs-comment">// 保留 8 个</span>options.info_log_level = WARN_LEVEL;                         <span class="hljs-comment">// 生产环境建议</span></code></pre><p><strong>空间分配详解</strong>：</p><table><thead><tr><th>项目</th><th>大小</th><th>说明</th></tr></thead><tbody><tr><td>文件系统可用</td><td>940GB</td><td>1024GB - 约 60GB（保留块 5% + 元数据 1%）</td></tr><tr><td><code>max_allowed_space_</code></td><td>900GB</td><td>SST 文件硬限制（含 compaction 临时文件）</td></tr><tr><td>├─ <code>compaction_buffer</code></td><td>30GB</td><td>在 900GB 内预留（防止 SST 占满）</td></tr><tr><td>├─ 业务实际可用</td><td>≤870GB</td><td>900GB - 30GB 缓冲</td></tr><tr><td>WAL 配置空间</td><td>16.5GB</td><td>Active 512MB + Archived 16GB（文件系统层面）</td></tr><tr><td>LOG 配置空间</td><td>4GB</td><td>512MB × 8 ≈ 4GB（文件系统层面）</td></tr><tr><td><strong>文件系统余量（<code>max_allowed_space_</code> 之外）</strong></td><td><strong>40GB</strong></td><td>940 - 900（应对配置误差与突发开销）</td></tr></tbody></table><p>上表中的 40GB 是 “<code>max_allowed_space_</code> 之外的余量” 口径，不是扣除 WAL&#x2F;LOG 后的实时可用值；WAL&#x2F;LOG 与临时文件的安全性依赖 <code>compaction_buffer_size_</code> 和该余量共同兜底。</p><p>业务可用空间为 <code>max_allowed_space_</code>（900GB）中扣除空间检查时强制预留 <code>compaction_buffer</code>（30GB），<strong>业务理论可用约 870GB</strong>（&#x3D; 900 - 30）。但考虑 70-80% 扩容水位，<strong>实际可用约 610～700GB</strong>。</p><h3 id="D-空间不足时的降级策略"><a href="#D-空间不足时的降级策略" class="headerlink" title="D. 空间不足时的降级策略"></a>D. 空间不足时的降级策略</h3><p>当监控到以下信号时，说明空间管理出现问题，需要<strong>主动干预</strong>：</p><ul><li><strong><code>COMPACTION_CANCELLED</code> 持续增长</strong>：空间不足导致 compaction 被频繁取消（最关键信号）</li><li><strong><code>rocksdb.estimate-pending-compaction-bytes</code> 持续增大</strong>：等待 compaction 的数据量不断积压</li><li><strong><code>rocksdb.num-files-at-level0</code> 超阈值</strong>：L0 文件堆积，可能触发 write stall</li><li><strong><code>rocksdb.obsolete-sst-files-size</code> 持续增大</strong>：过期 SST 积压，无法及时删除</li></ul><p><strong>降级策略优先级</strong>：</p><ol><li><p><strong>立即清理 obsolete files</strong></p><pre><code class="hljs cpp"><span class="hljs-comment">// 重启 DB（会强制全量扫描并清理 obsolete 文件）   </span><span class="hljs-comment">// 若之前禁用过文件删除，先恢复（真实可调用 API）</span>db-&gt;<span class="hljs-built_in">EnableFileDeletions</span>(<span class="hljs-comment">/*force=*/</span><span class="hljs-literal">false</span>);</code></pre></li><li><p><strong>临时调整参数</strong></p><pre><code class="hljs cpp"><span class="hljs-comment">// 增加 compaction 线程数（加快空间回收）</span>db-&gt;<span class="hljs-built_in">SetDBOptions</span>(&#123;&#123;<span class="hljs-string">&quot;max_background_jobs&quot;</span>, <span class="hljs-string">&quot;6&quot;</span>&#125;&#125;);  <span class="hljs-comment">// 默认为 2</span><span class="hljs-comment">// 提高 write stall/stop 阈值（延缓写入阻塞，争取更多时间回收空间）</span>db-&gt;<span class="hljs-built_in">SetOptions</span>(&#123;    &#123;<span class="hljs-string">&quot;level0_slowdown_writes_trigger&quot;</span>, <span class="hljs-string">&quot;30&quot;</span>&#125;,    <span class="hljs-comment">// 默认 20</span>    &#123;<span class="hljs-string">&quot;level0_stop_writes_trigger&quot;</span>, <span class="hljs-string">&quot;40&quot;</span>&#125;,        <span class="hljs-comment">// 默认 36</span>    &#123;<span class="hljs-string">&quot;soft_pending_compaction_bytes_limit&quot;</span>, <span class="hljs-string">&quot;128GB&quot;</span>&#125;,  <span class="hljs-comment">// 默认 64GB</span>    &#123;<span class="hljs-string">&quot;hard_pending_compaction_bytes_limit&quot;</span>, <span class="hljs-string">&quot;256GB&quot;</span>&#125;   <span class="hljs-comment">// 默认 256GB</span>&#125;);</code></pre></li><li><p><strong>紧急扩容</strong></p><ul><li>如果以上措施无效，说明配置严重不足，需要紧急扩容</li></ul></li></ol><h3 id="E-提升空间利用率的方案"><a href="#E-提升空间利用率的方案" class="headerlink" title="E. 提升空间利用率的方案"></a>E. 提升空间利用率的方案</h3><p>综合前文分析，传统架构下存储层长期平均使用率较低。以 §C 的 1TB 配置示例为例，无主从复制场景下业务可用约 610～700GB，按 70-80% 扩容水位的平均值估算，长期水位约 35-40%。提升利用率有以下方向：</p><ul><li><strong>共享大集群</strong>：多租户共享总空间，无需每个租户单独预留集群容量和 Compaction 空间，利用率可显著提升（经验估算 **65%～75%**）。主要面临的问题：隔离性差，资源争抢风险。</li><li><strong>存算分离</strong>：SST 文件存储在远程存储（S3&#x2F;OSS），本地磁盘仅存 WAL&#x2F;LOG。本地磁盘无需预留 Compaction 临时文件空间，远程存储按需弹性扩展，无扩容水位损耗，利用率可进一步提升（经验估算 **75%～85%**）。主要面临的问题：读写延迟增加（需通过缓存优化）。</li></ul><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/03-27-2026/rocksdb-space-mgr.html">https://www.cyningsun.com/03-27-2026/rocksdb-space-mgr.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;一、引言&quot;&gt;&lt;a href=&quot;#一、引言&quot; class=&quot;headerlink&quot; title=&quot;一、引言&quot;&gt;&lt;/a&gt;一、引言&lt;/h2&gt;&lt;p&gt;本文基于 RocksDB v8.8.1 代码与文档，从代码层面分析 RocksDB 的空间管理机制。默认使用 leveled</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB CompactionScore 机制</title>
    <link href="https://www.cyningsun.com/03-14-2026/rocksdb-compaction-score.html"/>
    <id>https://www.cyningsun.com/03-14-2026/rocksdb-compaction-score.html</id>
    <published>2026-03-13T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>在 RocksDB 的 Leveled Compaction 策略下，<code>ComputeCompactionScore</code> 函数是<strong>压实调度决策的中枢</strong>。其核心任务是：</p><blockquote><p><strong>为每个层级（level）计算压实分数（score），并据此决定后台压实优先级。</strong></p></blockquote><ul><li><strong>触发阈值</strong>：score ≥ 1.0。</li><li><strong>调度目标</strong>：优先处理高压层级，同时兼顾读放大、写放大、空间放大。</li><li><strong>核心难点</strong>：L0 文件重叠且读路径敏感，L1+ 层有序但存在级联下压风险。</li></ul><p>为便于把阅读路径和代码执行路径对齐，先给出本文主线问题：</p><ol><li>为什么需要比较 L0 与 Base Level 大小，来提升 L0 的优先级？</li><li>什么是”不必要层级”（<code>unnecessary_level</code>）？</li><li>如何确定不必要层级？</li><li>怎么实现不必要层的清理？</li><li>怎么实现”即将流入的数据量”（<code>total_downcompact_bytes</code>）估计的？</li><li>怎么通过”即将流入的数据量”降低当前层压实的优先级？</li><li><code>ComputeCompactionScore</code> 与 <code>PickCompaction</code> 如何衔接？高分是否一定可执行？</li><li>当 <code>score &gt; 1</code> 但选不出任务时，系统如何避免长期抖动或饥饿？</li><li>score 与 write stall（L0 slowdown&#x2F;stop）以及 <code>pending_compaction_bytes</code> 如何协同？</li></ol><h2 id="二、L0-层（level-x3D-x3D-0）精细化评分"><a href="#二、L0-层（level-x3D-x3D-0）精细化评分" class="headerlink" title="二、L0 层（level &#x3D;&#x3D; 0）精细化评分"></a>二、L0 层（level &#x3D;&#x3D; 0）精细化评分</h2><h3 id="（1）进入评分前：先建立可评分的上下文"><a href="#（1）进入评分前：先建立可评分的上下文" class="headerlink" title="（1）进入评分前：先建立可评分的上下文"></a>（1）进入评分前：先建立可评分的上下文</h3><p>在一个新 Version 的准备流程里，<code>PrepareForVersionAppend()</code> 先做三件关键事情，然后才适合讨论 score：</p><ol><li><code>ComputeCompensatedSizes()</code>：计算每个文件的 <code>compensated_file_size</code>。它不是简单文件大小，而是把点删密度（<code>kDeletionWeightOnCompaction</code>）和 <code>compensated_range_deletion_size</code> 一并计入，因此在 tombstone&#x2F;range delete 场景下，能更接近真实压实收益。</li><li><code>CalculateBaseBytes()</code>：计算动态层级字节模式下的 <code>base_level_</code>、<code>level_max_bytes_</code>、<code>lowest_unnecessary_level_</code>。这一步决定后续 L0 与 Base Level 的比较基准，也决定哪些层被视作 unnecessary。</li><li><code>UpdateFilesByCompactionPri()</code>：更新各层内部文件优先级，为后续 <code>PickCompaction()</code> 的文件挑选做准备。</li></ol><p>因此，<code>ComputeCompactionScore()</code> 并不是孤立运行，它建立在这组状态之上。也正因为 <code>base_level_</code> 是在新 Version 形成时重算，而不是每次写入即时重算，短周期内会有状态滞后窗口，但会在后续 Version 切换中被纠正。</p><h3 id="（2）基础统计"><a href="#（2）基础统计" class="headerlink" title="（2）基础统计"></a>（2）基础统计</h3><pre><code class="hljs cpp"><span class="hljs-type">int</span> num_sorted_runs = <span class="hljs-number">0</span>;<span class="hljs-type">uint64_t</span> total_size = <span class="hljs-number">0</span>;<span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>* f : files_[level]) &#123;  total_downcompact_bytes += <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(f-&gt;fd.<span class="hljs-built_in">GetFileSize</span>());  <span class="hljs-keyword">if</span> (!f-&gt;being_compacted) &#123;    total_size += f-&gt;compensated_file_size;    num_sorted_runs++;  &#125;&#125;</code></pre><ul><li><code>num_sorted_runs</code>：L0 每个未压实文件都视作一个独立 run（文件间重叠）。</li><li><code>total_size</code>：只统计未处于 compaction 中的文件，避免把“正在处理的压力”重复计入。</li><li><code>total_downcompact_bytes</code>：累计上层向下游传播的潜在压力，后续会参与 L1+ 评分降权。</li></ul><p>排除 <code>being_compacted</code> 的直接效果，是让分数反映“当前仍待调度的负载”；其代价是短窗口内会低估总压力，但每次 pick&#x2F;完成后都会重算分数，且 <code>level_total_bytes</code> 与 <code>total_downcompact_bytes</code> 机制会在后续层级补回部分前瞻信息。</p><h3 id="（3）基础分数：文件数量驱动"><a href="#（3）基础分数：文件数量驱动" class="headerlink" title="（3）基础分数：文件数量驱动"></a>（3）基础分数：文件数量驱动</h3><pre><code class="hljs cpp">score = num_sorted_runs / level0_file_num_compaction_trigger;</code></pre><p>默认 <code>level0_file_num_compaction_trigger = 4</code>，该规则先把 L0 读放大风险显式纳入优先级。</p><h3 id="（4）动态层级字节模式（level-compaction-dynamic-level-bytes-true）"><a href="#（4）动态层级字节模式（level-compaction-dynamic-level-bytes-true）" class="headerlink" title="（4）动态层级字节模式（level_compaction_dynamic_level_bytes == true）"></a>（4）动态层级字节模式（<code>level_compaction_dynamic_level_bytes == true</code>）</h3><h4 id="分支-A：确保-L0-到达-base-size-必有资格"><a href="#分支-A：确保-L0-到达-base-size-必有资格" class="headerlink" title="分支 A：确保 L0 到达 base size 必有资格"></a>分支 A：确保 L0 到达 base size 必有资格</h4><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (total_size &gt;= mutable_cf_options.max_bytes_for_level_base) &#123;  score = std::<span class="hljs-built_in">max</span>(score, <span class="hljs-number">1.01</span>);&#125;</code></pre><p>较大的 Write Buffer 会产生较大 SST，仅靠文件个数可能低估压力。这里把分数托到 1.0 以上，避免“写入已积压但分数未达阈值”的尴尬状态。</p><h4 id="分支-B：比较-L0-与-Base-Level，控制下刷优先级"><a href="#分支-B：比较-L0-与-Base-Level，控制下刷优先级" class="headerlink" title="分支 B：比较 L0 与 Base Level，控制下刷优先级"></a>分支 B：比较 L0 与 Base Level，控制下刷优先级</h4><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (total_size &gt; level_max_bytes_[base_level_]) &#123;  <span class="hljs-type">uint64_t</span> base_level_size = <span class="hljs-number">0</span>;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> f : files_[base_level_]) &#123;    base_level_size += f-&gt;compensated_file_size;  &#125;  score = std::<span class="hljs-built_in">max</span>(score, <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(total_size) /           <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(std::<span class="hljs-built_in">max</span>(                base_level_size,                 level_max_bytes_[base_level_])));&#125;</code></pre><p>这里比较的是 <code>L0_size</code> 与 <code>max(BaseLevel_size, BaseLevel_target)</code>，而不是固定比较某一层实际大小。这样做可以在 L0 快速增长时，把 L0-&gt;Base 的调度优先级拉高，防止 Base 层下游整理尚未完成时 L0 继续失控堆积。</p><p><strong>例如</strong>（假设 base_level&#x3D;L1，且 L1_target&#x3D;100MB）：L0&#x3D;200MB, L1&#x3D;100MB，则 L0 分数为 <code>200/max(100,100)=2.0</code>，缩放后为 <code>2.0 × 10 (kScoreScale) = 20.0</code>。此时 <code>total_downcompact_bytes ≈ L0_size = 200MB</code>，L1 分数约为 <code>100MB/(100MB + 200MB) ×10 = 3.3</code>。**最终，L0 (20.0) &gt;&gt; L1 (3.3)**，L0 优先级会明显高于同周期内 BaseLevel 的整理任务。</p><h4 id="分支-C：超阈值后分数缩放"><a href="#分支-C：超阈值后分数缩放" class="headerlink" title="分支 C：超阈值后分数缩放"></a>分支 C：超阈值后分数缩放</h4><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (score &gt; <span class="hljs-number">1.0</span>) score *= kScoreScale; <span class="hljs-comment">// kScoreScale = 10.0</span></code></pre><p><code>kScoreScale</code> 的作用不是改变触发阈值，而是扩展超阈值区间的可排序空间。这样即使引入了 <code>total_downcompact_bytes</code> 这类降权项，系统仍能拉开高压层之间的优先级差异。</p><h3 id="（5）静态模式补充判断"><a href="#（5）静态模式补充判断" class="headerlink" title="（5）静态模式补充判断"></a>（5）静态模式补充判断</h3><pre><code class="hljs cpp">score = std::<span class="hljs-built_in">max</span>(score, total_size / max_bytes_for_level_base);</code></pre><p>静态模式下，L0 同时受“文件数”和“总大小”双约束，避免大文件场景只靠 run 数无法及时触发压实。</p><h2 id="三、L1-层（level-gt-0）的动态评分机制"><a href="#三、L1-层（level-gt-0）的动态评分机制" class="headerlink" title="三、L1+ 层（level &gt; 0）的动态评分机制"></a>三、L1+ 层（level &gt; 0）的动态评分机制</h2><h3 id="（1）基础统计"><a href="#（1）基础统计" class="headerlink" title="（1）基础统计"></a>（1）基础统计</h3><pre><code class="hljs cpp"><span class="hljs-type">uint64_t</span> level_bytes_no_compacting = <span class="hljs-number">0</span>;<span class="hljs-type">uint64_t</span> level_total_bytes = <span class="hljs-number">0</span>;<span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> f : files_[level]) &#123;  level_total_bytes += f-&gt;fd.<span class="hljs-built_in">GetFileSize</span>();  <span class="hljs-keyword">if</span> (!f-&gt;being_compacted) &#123;    level_bytes_no_compacting += f-&gt;compensated_file_size;  &#125;&#125;</code></pre><p>其中 <code>level_bytes_no_compacting</code> 统计当前仍待调度的有效压力；<code>level_total_bytes</code> 统计该层总物理大小，后续用于估算向下游传播的压力。</p><h3 id="（2）静态模式"><a href="#（2）静态模式" class="headerlink" title="（2）静态模式"></a>（2）静态模式</h3><pre><code class="hljs cpp">score = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(level_bytes_no_compacting) / <span class="hljs-built_in">MaxBytesForLevel</span>(level);</code></pre><p>仍是“当前大小 &#x2F; 目标大小”的经典定义。</p><h3 id="（3）动态模式（核心）"><a href="#（3）动态模式（核心）" class="headerlink" title="（3）动态模式（核心）"></a>（3）动态模式（核心）</h3><h4 id="分支-A：未超限，常规比值"><a href="#分支-A：未超限，常规比值" class="headerlink" title="分支 A：未超限，常规比值"></a>分支 A：未超限，常规比值</h4><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (level_bytes_no_compacting &lt; <span class="hljs-built_in">MaxBytesForLevel</span>(level)) &#123;  score = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(level_bytes_no_compacting) / <span class="hljs-built_in">MaxBytesForLevel</span>(level);&#125;</code></pre><h4 id="分支-B：超限时引入-total-downcompact-bytes-降权"><a href="#分支-B：超限时引入-total-downcompact-bytes-降权" class="headerlink" title="分支 B：超限时引入 total_downcompact_bytes 降权"></a>分支 B：超限时引入 <code>total_downcompact_bytes</code> 降权</h4><pre><code class="hljs cpp">score = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(level_bytes_no_compacting) /                             (<span class="hljs-built_in">MaxBytesForLevel</span>(level) + total_downcompact_bytes) *                             kScoreScale;</code></pre><p>这一步对应“先看本层压力，再看上游即将流入压力”。当上游即将下压的数据很多时，先把本层 score 压低，避免做出“刚整理完马上又被灌满”的调度决策。</p><p><strong>例如</strong>：L2 超限 50MB，但 L1 正在压 200MB 到 L2，则分母 &#x3D; 100 + 200 &#x3D; 300，计算分数为 0.33 × 10 &#x3D; 3.3。即使超限，该层优先级也会被下调，待后续再评估。</p><p>该估计天然有误差：</p><ul><li>高估常见于整层 unnecessary 清理或后续会大量消解 tombstone 的场景；</li><li>低估常见于突发写入尚未 flush 进当前 Version 视图的时段。</li></ul><p>但它的设计目标不是精确预测，而是给调度加入前瞻约束，降低 compaction 风暴概率。</p><h4 id="分支-C：清理“不必要层级”"><a href="#分支-C：清理“不必要层级”" class="headerlink" title="分支 C：清理“不必要层级”"></a>分支 C：清理“不必要层级”</h4><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (level_bytes_no_compacting &gt; <span class="hljs-number">0</span> &amp;&amp; level &lt;= lowest_unnecessary_level_) &#123;  score = std::<span class="hljs-built_in">max</span>(score, kScoreScale * (<span class="hljs-number">1.001</span> + <span class="hljs-number">0.001</span> * (lowest_unnecessary_level_ - level)));&#125;</code></pre><p><strong>什么是“不必要层级”？</strong> 只在启用动态层级字节数模式 (level_compaction_dynamic_level_bytes &#x3D; true) 时才会产生，指可被<strong>完全合并到下一层</strong>的层级（如 L3 只有 10MB，L4 目标 10GB）。当 <code>CalculateBaseBytes()</code> 把某些层判为 unnecessary 后，这里给它们一个稳定且有限的优先级（<strong>10.01 ~ 10.05</strong>,取决于 <code>lowest_unnecessary_level_ - level</code>）的分数。一方面，分数大于 1.0，意味着有资格压实；另外一方面，远小于 L0 超限时的分数（如 20.0），使其在 L0 不紧急时被逐步清理，但在 L0 高压时仍会让位给更紧急的下刷任务。</p><p>在 CalculateBaseBytes 函数中，<code>lowest_unnecessary_level_</code> 的判定来自 base bytes 反推过程：从高层目标往前按 multiplier 递推，当某层理论目标跌破 <code>base_bytes_min</code> 且满足约束（含 <code>preclude_last_level_data_seconds</code> 相关条件）时，开始标记为 unnecessary 区间。</p><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (lowest_unnecessary_level_ == <span class="hljs-number">-1</span> &amp;&amp;    cur_level_size &lt;= base_bytes_min &amp;&amp;    (ioptions.preclude_last_level_data_seconds == <span class="hljs-number">0</span> ||     i &lt; num_levels_ - <span class="hljs-number">2</span>)) &#123;    lowest_unnecessary_level_ = i;&#125;</code></pre><p>具体条件：</p><ol><li>尚未找到 unnecessary level (lowest_unnecessary_level_ &#x3D;&#x3D; -1)</li><li>当前层级的理论目标大小小于等于 base_bytes_min &#x3D; max_bytes_for_level_base &#x2F; max_bytes_for_level_multiplier</li><li>不是倒数第二层（当启用 per_key_placement 时，倒数第二层是必需的）</li></ol><p>假设：</p><ul><li>max_bytes_for_level_base &#x3D; 256MB</li><li>max_bytes_for_level_multiplier &#x3D; 10</li><li>base_bytes_min &#x3D; 256MB &#x2F; 10 &#x3D; 25.6MB</li><li>最大层级（L6）有 10GB 数据</li></ul><p>计算过程：</p><ul><li>L6: 10GB (实际最大)</li><li>L5: 10GB &#x2F; 10 &#x3D; 1GB (理论)</li><li>L4: 1GB &#x2F; 10 &#x3D; 100MB (理论)</li><li>L3: 100MB &#x2F; 10 &#x3D; 10MB (理论) ，此层级 10MB 小于 25.6MB，会被标记为 unnecessary</li></ul><p>因此 lowest_unnecessary_level_ &#x3D; 3。在该推导前提下（相关层级位于计算窗口且为非空层），可理解为 L1、L2、L3 都属于不必要层级候选。</p><h3 id="（4）total-downcompact-bytes-的更新回路"><a href="#（4）total-downcompact-bytes-的更新回路" class="headerlink" title="（4）total_downcompact_bytes 的更新回路"></a>（4）<code>total_downcompact_bytes</code> 的更新回路</h3><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (level &lt;= lowest_unnecessary_level_) &#123;  total_downcompact_bytes += level_total_bytes; <span class="hljs-comment">// 不必要层级：全部计入</span>&#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (level_total_bytes &gt; <span class="hljs-built_in">MaxBytesForLevel</span>(level)) &#123;  total_downcompact_bytes += <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">double</span>&gt;(level_total_bytes - <span class="hljs-built_in">MaxBytesForLevel</span>(level)); <span class="hljs-comment">// 超限部分计入</span>&#125;</code></pre><p>把“上层会向下传播多少压力”显式反馈到后续层级评分。<code>ComputeCompactionScore()</code> 在层级遍历过程中不断更新该变量，形成一个由上到下的前瞻抑制回路。对于<strong>不必要层级</strong>，因为整个层级都会被压实掉，在估计中全部字节都计入下一层压力。对于 <strong>超限层级</strong>：在估计中仅计入<strong>超出目标的部分</strong>。</p><h3 id="（5）从-score-到可执行-compaction，再到-write-stall"><a href="#（5）从-score-到可执行-compaction，再到-write-stall" class="headerlink" title="（5）从 score 到可执行 compaction，再到 write stall"></a>（5）从 score 到可执行 compaction，再到 write stall</h3><p>score 只是“候选优先级”，是否能执行要继续走调度链路：</p><ol><li><code>ColumnFamilyData::NeedsCompaction()</code> -&gt; <code>LevelCompactionPicker::NeedsCompaction()</code>：只要存在可触发条件（包括 <code>CompactionScore(i) &gt;= 1</code>、TTL、periodic、marked、forced blob GC、bottommost）就会进入候选。</li><li><code>PickCompaction()</code> &#x2F; <code>SetupInitialFiles()</code>：先按 score 从高到低尝试，再进入实际文件选择。</li><li>文件选择阶段还要满足执行约束：<ul><li><code>ExpandInputsToCleanCut()</code> 必须成功；</li><li>不能与正在运行 compaction 的输入&#x2F;输出范围冲突（<code>FilesRangeOverlapWithCompaction()</code>）；</li><li>L0 有并发限制（有 L0 compaction 在跑时，新的 L0-&gt;base 可能被阻塞）。</li></ul></li></ol><p>这解释了“<code>score &gt; 1</code> 但仍选不出任务”的常见原因。系统不是停在原地，会继续尝试后续层级&#x2F;文件，L0 受阻时可回退尝试 <code>PickIntraL0Compaction()</code>，先降低 L0 文件数压力，若按分数选不出，会继续进入标记类任务路径。</p><p>在 Level compaction 下，<code>SetupInitialFiles()</code> 的自动仲裁顺序可概括如下（仅在对应条件满足时生效，例如 <code>compaction_style</code> 为 level，且 TTL&#x2F;periodic&#x2F;blob GC 相关选项开启）：score 驱动任务、<code>FilesMarkedForCompaction</code>、<code>BottommostFilesMarkedForCompaction</code>、<code>ExpiredTtlFiles</code>、<code>FilesMarkedForPeriodicCompaction</code>、<code>FilesMarkedForForcedBlobGC</code></p><p>调度最后还会与写入限流协同，<code>GetWriteStallConditionAndCause()</code> 使用 <code>l0_delay_trigger_count</code> 与 <code>estimated_compaction_needed_bytes</code>（soft&#x2F;hard pending bytes limit）判定 normal&#x2F;delayed&#x2F;stopped；<code>RecalculateWriteStallConditions()</code> 通过 <code>WriteController</code> 发放 delay&#x2F;stop&#x2F;compaction-pressure token；同一个系统里，score 决定“先做谁”，write stall 决定“写入端需要多强背压”，两者共同防止失稳。</p><h2 id="四、总结：调度仲裁顺序与多-CF-行为"><a href="#四、总结：调度仲裁顺序与多-CF-行为" class="headerlink" title="四、总结：调度仲裁顺序与多 CF 行为"></a>四、总结：调度仲裁顺序与多 CF 行为</h2><p>从执行路径看，CompactionScore 相关机制可以压缩成一条主链：</p><p><code>PrepareForVersionAppend</code><br>-&gt; <code>ComputeCompensatedSizes / CalculateBaseBytes</code><br>-&gt; <code>VersionSet::AppendVersion(ComputeCompactionScore)</code><br>-&gt; <code>NeedsCompaction</code><br>-&gt; <code>PickCompaction(SetupInitialFiles)</code><br>-&gt; <code>GetWriteStallConditionAndCause</code></p><p>在这条链上：<code>ComputeCompactionScore</code> 负责表达“压力与优先级”；<code>PickCompaction</code> 负责在并发与重叠约束下寻找“可执行任务”；write stall 负责在压力持续时给写入路径施加背压。</p><p>多 Column Family 场景下，后台线程以 DB 级 <code>compaction_queue_</code> 进行 CF 任务分发，体现为队列轮转而非全局最优 score 调度。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/03-14-2026/rocksdb-compaction-score.html">https://www.cyningsun.com/03-14-2026/rocksdb-compaction-score.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;一、背景&quot;&gt;&lt;a href=&quot;#一、背景&quot; class=&quot;headerlink&quot; title=&quot;一、背景&quot;&gt;&lt;/a&gt;一、背景&lt;/h2&gt;&lt;p&gt;在 RocksDB 的 Leveled Compaction 策略下，&lt;code&gt;ComputeCompactionScor</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>go-redis 连接池重建连接优化</title>
    <link href="https://www.cyningsun.com/11-23-2025/go-redis-connection-success-rate.html"/>
    <id>https://www.cyningsun.com/11-23-2025/go-redis-connection-success-rate.html</id>
    <published>2025-11-22T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在高并发场景下，当 go-redis 连接池耗尽时，客户端会尝试新建连接来服务请求。而旧版本（<a href="https://github.com/redis/go-redis/releases/tag/v9.16.0">9.16.0</a> 及以前）的实现<strong>直接使用请求的 <code>context.Context</code></strong> 来控制整个新连接过程（包括 Dial、TLS 握手、AUTH 等）。这意味着如果请求的上下文（如 HTTP 请求的超时时间）到期，正在进行的<strong>拨号操作会被立即取消</strong>。</p><p>审视旧实现“按需新建连接”的行为时，可以结合项目维护者的设计权衡与实际场景来理解。项目维护者提到：</p><ul><li>如果配置足够数量的 MinIdleConns 和 PoolSize，就不会导致大量的按需新建连接</li><li>每次命令都创建新连接的方式并不理想，它仅作为连接池中没有可用连接时的备用方案</li></ul><p>然而在实际生产环境中，这种设计会导致微小的波动被放大：<strong>本应可用的连接被无谓丢弃，连接池反而得不到补充</strong>。原因如下：</p><h3 id="1-客户端请求的-Context-剩余时间往往不足以支撑拨号"><a href="#1-客户端请求的-Context-剩余时间往往不足以支撑拨号" class="headerlink" title="1. 客户端请求的 Context 剩余时间往往不足以支撑拨号"></a>1. 客户端请求的 Context 剩余时间往往不足以支撑拨号</h3><p>请求上下文的 timeout 会随着链路深度逐层衰减（例如初始 1s timeout 经 3 次微服务透传后可能只剩 200ms）；其次，当连接池无空闲连接时，请求会等待池中连接释放，<code>PoolTimeout</code> 会进一步蚕食请求超时总时长，随后才开始拨号。</p><p>此时请注意，请求上下文本身已经被压榨掉了大部分超时预算，剩下的时间往往远远低于完成一次网络拨号所需。因为在跨机房或网络不稳的场景下，一次完整的 TCP 握手 + TLS 握手往返可能需要几百毫秒甚至几秒的时间。如果请求上下文到期（<code>context.DeadlineExceeded</code>），go-redis 将关闭当前拨号连接并放弃它，而不是将该连接放入池中复用。反复使用短超时上下文去拨号，多次尝试会耗尽请求的剩余时间，造成性能抖动加剧。</p><p>更进一步，超时失败还会导致诊断混淆：因为开发者往往看到了大量 “dial tcp: i&#x2F;o timeout” 错误，难以判断是后端 Redis 真出问题了，还是客户端过早取消了正常的拨号。</p><h3 id="2-服务端已接收连接，资源白白浪费"><a href="#2-服务端已接收连接，资源白白浪费" class="headerlink" title="2. 服务端已接收连接，资源白白浪费"></a>2. 服务端已接收连接，资源白白浪费</h3><p>“客户端单方面丢弃连接”的行为，会给服务端带来直接的资源负担：</p><p>服务端需要为已建立连接分配 FD、Socket Buffer、连接结构体，客户端立即关闭后，会导致服务端产生 TIME_WAIT &#x2F; CLOSE_WAIT 状态。若 Redis 层有连接初始化逻辑，还会进行额外操作。在高并发场景下会导致 accept 队列更快被填满，有效连接减少，FD 周期性分配与销毁增加 CPU 消耗。</p><p>更进一步，如果服务端负载进一步升高，较大概率会需要更长时间来接受新的连接，甚至完全超过客户端连接超时控制，导致服务完全雪崩。</p><h3 id="误解-DialTimeout-过长会拖慢业务"><a href="#误解-DialTimeout-过长会拖慢业务" class="headerlink" title="误解 DialTimeout 过长会拖慢业务"></a>误解 DialTimeout 过长会拖慢业务</h3><p>请求上下文创建连接让许多用户误以为：</p><blockquote><p>“DialTimeout 设置太长，会导致业务变慢甚至请求超时。”</p></blockquote><p>然而这是一种典型误解，实际上 go-redis 会选择 <code>context.Context</code> 和 <code>DialTimeout</code> 的最小者作为最长超时时间。<a href="/08-20-2023/go-redis-connection-timeout.html">过短的 <code>DialTimeout</code> 设置反而会导致请求提前超时（相比业务能够接受的耗时）</a>。</p><p>更进一步，在跨机房环境中，本身就存在更高的 RTT 波动与偶发丢包。当 Redis 客户端在压力下发起大量新连接时，轻微的网络丢包会导致 TCP SYN 或后续握手包被重传。如果客户端的 DialTimeout 设置过短，则在 TCP 仍在重传、连接即将成功建立前，客户端会因为本地超时而中断连接。结果是：</p><p>某些 Redis 节点因轻微丢包导致握手时间稍长 → 客户端提前放弃 → 这些节点被视作“慢节点” → 负载倾向其他节点 → <strong>负载不均被放大</strong>。</p><p><strong>TCP 丢包与重传机制的挑战</strong></p><p>TCP 连接的建立需要经过三次握手：客户端发送 SYN 到服务端，服务端返回 SYN-ACK，客户端再回一个 ACK。如上图所示，如果任意一个握手包（例如初始的 SYN）在网络中丢失，TCP 会在初始<strong>重传超时（RTO）</strong>到期后重新发送该包，并采用<strong>指数回退</strong>的策略逐次延长超时。根据 <a href="https://datatracker.ietf.org/doc/html/rfc1122">RFC1122</a> 的建议，初始 RTO 通常设置在 3 秒左右（一些现代系统的最小 RTO 也在 200ms 以上，并且每次重传等待时间会翻倍增长）。如果应用层将 Dial 操作的超时时间设置得过短，未到第一次重传超时就放弃拨号，那么就可能永远等不到成功的握手应答，即连接直接失败。例如，在跨机房部署时，单程延迟可能达到几十至几百毫秒，加上可能的偶发丢包，完成三次握手通常需要上秒级别的时间；而如果拨号超时设置只有几百毫秒，握手过程还未完成就被强行中断，就会出现大量“dial tcp timeout”错误，严重影响可用连接数。简言之，<strong>应用层短超时可能挡住了底层 TCP 的重传机会</strong>。</p><h2 id="Go-标准库的稳定实践"><a href="#Go-标准库的稳定实践" class="headerlink" title="Go 标准库的稳定实践"></a>Go 标准库的稳定实践</h2><p>在工程实践中，<code>database/sql</code> 和 <code>net/http/Transport</code> 是经受过大量生产验证的连接管理实现，参考的关键实现细节包括：</p><ul><li><strong>连接池与分离的连接生命周期</strong>：<code>database/sql</code> 提供了一个全局 <code>sql.DB</code> 对象作为连接池，维护 <code>maxOpenConns</code>、<code>maxIdleConns</code>、<code>connRequests</code> 等指标。库内部会独立管理“获取连接”和“建立连接”两条逻辑路径：当没有空闲连接时，会将获取请求排队（request queue），并在后台尝试建立新连接来填补池，而不是把连接建立严格绑定到某个请求的剩余超时时间上。建立成功的连接会被放回池中，供后续请求复用，从而把“单次请求的超时”与“连接能否长期复用”解耦。</li><li><strong>独立拨号超时与可重用连接</strong>：<code>net/http.Transport</code> 与 <code>net.Dialer</code> 通常配置独立的网络超时（例如 <code>Dialer.Timeout</code>、<code>IdleConnTimeout</code>、<code>ResponseHeaderTimeout</code> 等），这些超时用于保护底层网络操作，但并不简单地把每个 HTTP 请求的上下文直接映射到拨号超时上。换言之，拨号有一个合理的最小时间窗口，让 TCP 三次握手和必要的重传有机会完成；一旦连接建立，Transport 会将连接作为空闲连接保留，供后续请求使用。</li><li><strong>空闲连接的回收与复用</strong>：两者都实现了空闲连接管理（idle pool），包括最大空闲数、每主机最大空闲等策略，并对连接复用的生命周期做限制（例如 <code>IdleConnTimeout</code>、<code>MaxIdleConnsPerHost</code>）。这保证了即便某次请求因为超时而取消，已建立并健康的连接不会被立即销毁，而是回到空闲池中供其他请求复用，从而节省昂贵的拨号成本。</li><li><strong>公平的等待队列与避免饥饿</strong>：成熟实现通常会有明确的等待队列策略（FIFO 或带优先级的队列）以避免某些请求长期饥饿，保证连接分配的公平性；这对高并发、连接匮乏的场景尤为关键。</li></ul><p>这些实现的共同思想是：<strong>分离连接建立与单次请求的超时控制</strong>，最大化连接复用并让底层的 TCP 有机会完成必要的重传与握手。</p><h3 id="如何在-go-redis-落地"><a href="#如何在-go-redis-落地" class="headerlink" title="如何在 go-redis 落地"></a>如何在 go-redis 落地</h3><p>在 <code>database/sql.DB</code> 中，所有连接由单一后台协程串行创建，创建吞吐受限于单协程，对 DB 连接够用，但却无法满足 Redis 的吞吐要求。相较来说，可以将标准库 <code>net/http.Transport</code> 的实现细节迁移到 go-redis，主要优化点包括：</p><ul><li><strong>独立拨号上下文</strong>：拨号操作不再沿用请求的 <code>context.Context</code>，而是使用独立的上下文结合配置的 <code>DialTimeout</code> 来控制时长，同时保留对原请求上下文的监听用于清理资源。这样，即使请求超时了，拨号在不受影响的超时内仍可继续，给底层 TCP 完成三次握手足够的机会。</li><li><strong>成功连接复用</strong>：如果新连接最终建立成功，即便触发此拨号的请求已经超时，该连接也不会丢弃，而是<strong>直接放入连接池</strong>中供后续请求复用。这一改动避免了“好不容易建立的连接因一个请求超时而被浪费”的现象。</li><li><strong>FIFO 排队机制</strong>：引入了显式的<strong>先进先出队列</strong>，来公平地分配由多个等待请求共享新建连接的机会。</li></ul><p>此外，PR(<a href="https://github.com/redis/go-redis/pull/3518">#3518</a>) 还增加了 <code>putIdleConn</code> 等内部方法优化，用于<strong>直接将新建连接放入池中</strong>，从而避免因为达到了 <code>MaxIdleConns</code> 限制而在放回时关闭本该有效的空闲连接。</p><p>整体上，PR 在连接管理的控制流程上做了调整，把成熟库中的“拨号与请求分离、成功连接一定回池、FIFO 公平分配”这些原则逐步移植到 go-redis 的实现中，从而获得了更稳定的行为与更好的资源利用率。</p><h2 id="优化效果"><a href="#优化效果" class="headerlink" title="优化效果"></a>优化效果</h2><p>这些改动主要带来了以下改善：首先，它<strong>减少了不必要的拨号超时错误</strong>。在实践中，新版 go-redis 在短时网络波动或高延迟场景下，可以避免因为请求上下文取消而放弃即将建立的连接，从而降低类似 “dial tcp i&#x2F;o timeout” 的报错频率（底层 TCP 终于有机会完成重传）。其次，通过将成功连接回归池、提高复用率，<strong>缓解了连接池压力</strong>：原本频繁出现的反复拨号和重建被减少，系统吞吐更加稳定。此外，优化还<strong>改善了资源分配</strong>：健康的连接不再被回退到后续请求中途丢弃，多台 Redis 节点之间的流量分配更加均衡。以往某些节点可能因为新建连接失败而参与度下降的问题得到缓解，从而整体负载倾斜现象减弱。</p><p>参考 Go 标准库中 <code>database/sql</code> 与 <code>net/http</code> 的稳定实践，这些改进让客户端在面对临时的网络丢包和上下文超时时，具备更好的<strong>鲁棒性</strong>：它们保证了单个请求的超时不会牺牲整个连接池的健康，避免了因瞬时失败导致后续流量骤降的连锁反应。总体来看，该 PR 提高了系统对偶发故障的容忍度，使得分布式调用链中，下游服务（如 Redis 节点）的负载分布更加均衡，从而提升了服务的可靠性。</p><h2 id="致谢"><a href="#致谢" class="headerlink" title="致谢"></a>致谢</h2><p>目前该优化已随最新版本（<a href="https://github.com/redis/go-redis/releases/tag/v9.17.0">9.17.0</a>）发布。特别感谢 go-redis 维护者 ndyakov 在 Issue 与 PR 讨论中的耐心解答与宝贵 Review 建议。其对连接池机制的权衡解释与建议，为本次优化提供了重要帮助。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/11-23-2025/go-redis-connection-success-rate.html">https://www.cyningsun.com/11-23-2025/go-redis-connection-success-rate.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;在高并发场景下，当 go-redis 连接池耗尽时，客户端会尝试新建连接来服务请求。而旧版本（&lt;a href=&quot;https://github</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="Redis" scheme="https://www.cyningsun.com/tag/Redis/"/>
    
  </entry>
  
  <entry>
    <title>译｜Facebook&#39;s Tectonic Filesystem: Efficiency from Exascale</title>
    <link href="https://www.cyningsun.com/08-26-2025/facebook-tectonic-filesystem.html"/>
    <id>https://www.cyningsun.com/08-26-2025/facebook-tectonic-filesystem.html</id>
    <published>2025-08-25T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<p><strong>Satadru Pan¹, Theano Stavrinos¹,², Yunqiao Zhang¹, Atul Sikaria¹, Pavel Zakharov¹, Abhinav Sharma¹, Shiva Shankar P¹, Mike Shuey¹, Richard Wareing¹, Monika Gangapuram¹, Guanglei Cao¹, Christian Preseau¹, Pratap Singh¹, Kestutis Patiejunas¹, JR Tipton¹, Ethan Katz-Bassett³, Wyatt Lloyd²</strong></p><p>¹Facebook, Inc., ²普林斯顿大学, ³哥伦比亚大学</p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>Tectonic 是 Facebook 的艾字节级分布式文件系统。Tectonic 将之前使用特定服务系统的庞大租户整合到通用的多租户文件系统实例中，并实现了与专用系统相当的性能。EB 级的整合实例相较于我们之前的方法，能够实现更好的资源利用率、更简单的服务以及更少的运维复杂度。本文描述了 Tectonic 的设计，解释了它如何实现可扩展性、支持多租户，并允许租户定制操作以优化多样化的工作负载。本文还分享了从设计、部署和运维 Tectonic 中获得的经验。</p><h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1 引言"></a>1 引言</h2><p>Tectonic 是 Facebook 的分布式文件系统。它目前服务于大约十个租户，包括存储艾字节数据的 Blob 存储和数据仓库。在 Tectonic 之前，Facebook 的存储基础设施由众多规模较小、专用的存储系统组成。Blob 存储分散在 Haystack [11] 和 f4 [34] 中。数据仓库则分散在多个 HDFS 实例 [15] 中。</p><p>这种多系统方法运维复杂，需要开发、优化和管理许多不同的系统。它效率低下，导致资源被束缚在专用存储系统中，而这些资源本可以重新分配给存储工作负载的其他部分。</p><p>一个 Tectonic 集群可以扩展到艾字节规模，使得单个集群可以覆盖整个数据中心。Tectonic 集群的多艾字节容量使得在同一个集群上托管多个大型租户（如 Blob 存储和数据仓库）成为可能，每个租户反过来又支持数百个应用程序。作为一个艾字节级的多租户文件系统，与基于联邦的存储架构 [8, 17]（其由较小的 PB 级集群组装而成）相比，Tectonic 提供了运维简便性和资源效率。</p><p>Tectonic 简化了运维，因为它是一个单一系统，可用于开发、优化和管理多样化的存储需求。它具有资源效率，因为它允许集群内所有租户之间共享资源。例如，Haystack 是专为新 Blob 设计的存储系统；它受限于硬盘每秒 IO 操作数（IOPS），但拥有富余的磁盘容量。f4 存储较旧的 Blob，受限于磁盘容量，但拥有富余的 IO 能力。通过整合和资源共享，Tectonic 需要更少的磁盘来支持相同的工作负载。</p><p>在构建 Tectonic 时，我们面临三个高层次挑战：扩展到艾字节规模、在租户之间提供性能隔离，以及支持租户特定的优化。艾字节级集群对于运维简便性和资源共享至关重要。性能隔离和租户特定优化则帮助 Tectonic 达到专用存储系统的性能水平。</p><p>为了扩展元数据，Tectonic 将文件系统元数据解耦为可独立扩展的层，类似于 ADLS [42]。与 ADLS 不同，Tectonic 对每个元数据层进行哈希分区（hash-partition），而不是使用范围分区（range partitioning）。哈希分区有效避免了元数据层的热点。结合 Tectonic 高度可扩展的数据块（chunk）存储层，解耦的元数据使 Tectonic 能够扩展到艾字节存储和数十亿文件。</p><p>Tectonic 通过为每个租户内具有相似流量模式和延迟要求的应用程序组解决隔离问题，从而简化了性能隔离。Tectonic 不是在数百个应用程序之间管理资源，而只在数十个流量组之间管理资源。</p><p>Tectonic 使用租户特定的优化来匹配专用存储系统的性能。这些优化通过客户端驱动的微服务架构实现，该架构包含一套丰富的客户端配置，用于控制租户如何与 Tectonic 交互。例如，数据仓库使用 Reed-Solomon (RS) 编码写入，以提高其大型写入的空间、IO 和网络效率。相比之下，Blob 存储使用复制的仲裁追加协议来最小化其小型写入的延迟，并在之后对它们进行 RS 编码以实现空间效率。</p><p>Tectonic 已在单租户集群中托管 Blob 存储和数据仓库数年，完全取代了 Haystack、f4 和 HDFS。多租户集群正在有计划地推出，以确保可靠性并避免性能回归。</p><p>采用 Tectonic 带来了许多运维和效率上的改进。将数据仓库从 HDFS 迁移到 Tectonic 使数据仓库集群数量减少了 10 倍，通过管理更少的集群简化了运维。将 Blob 存储和数据仓库整合到多租户集群中，帮助数据仓库利用 Blob 存储的富余 IO 能力来处理流量高峰。在提供与之前专用存储系统相当或更好性能的同时，Tectonic 实现了这些效率提升。</p><h2 id="2-Facebook-之前的存储基础设施"><a href="#2-Facebook-之前的存储基础设施" class="headerlink" title="2 Facebook 之前的存储基础设施"></a>2 Facebook 之前的存储基础设施</h2><p>在 Tectonic 之前，每个主要存储租户将其数据存储在一个或多个专用的存储系统中。我们在此重点关注两个大型租户：Blob 存储和数据仓库。我们讨论每个租户的性能需求、它们之前的存储系统以及为什么这些系统效率低下。</p><p><img src="/images/facebook-tectonic-filesystem/%E8%AF%91%EF%BD%9CFacebook's%20Tectonic%20Filesystem%EF%BC%9AEfficiency%20from%20Exascale-20250826230736-1.png" alt="译｜Facebook&#39;s Tectonic Filesystem：Efficiency from Exascale-20250826230736-1.png"></p><p><em>图 1：Tectonic 在数据中心内提供持久、容错的存储。每个租户拥有一个或多个独立的名称空间。租户实现跨地域复制（geo-replication）。</em></p><h3 id="2-1-Blob-存储"><a href="#2-1-Blob-存储" class="headerlink" title="2.1 Blob 存储"></a>2.1 Blob 存储</h3><p>Blob 存储用于存储并提供二进制大对象（BLOB）服务。这些对象可能来自 Facebook 应用程序的多媒体（照片、视频或消息附件），也可能来自内部应用程序的数据（核心转储、错误报告）。Blob 是不可变的（immutable）且不透明的（opaque）。它们的大小从几 KB 的小照片到几 MB 的高清视频片段不等 [34]。Blob 存储期望低延迟的读写，因为 Blob 通常是 Facebook 交互式应用程序的关键路径 [29]。</p><p><strong>Haystack 和 f4。</strong> 在 Tectonic 之前，Blob 存储由两个专用系统组成：Haystack 和 f4。Haystack 处理访问频率高的“热” Blob [11]。它以复制形式存储数据，以实现持久性和快速读写。当 Haystack 中的 Blob 变旧且访问频率降低时，它们会被移动到“温” Blob 存储 f4 [34]。f4 以 RS 编码形式 [43] 存储数据，这种方式空间效率更高，但吞吐量较低，因为每个 Blob 只能直接从两个磁盘访问（而 Haystack 是三个）。f4 较低的吞吐量因其较低的请求率而被接受。</p><p>然而，将热 Blob 和温 Blob 分离导致了资源利用率低下，这个问题因硬件和 Blob 存储使用趋势而加剧。Haystack 理想的有效复制因子是 3.6 倍（即每个逻辑字节复制 3 份，加上 RAID-6 存储 [19] 带来的额外 1.2 倍开销）。但是，随着硬盘密度增加而每块硬盘的 IOPS 保持稳定，每 TB 存储容量的 IOPS 随时间推移而下降。</p><p>结果，Haystack 变得受 IOPS 限制；必须额外配置硬盘来处理热 Blob 的高 IOPS 负载。富余的磁盘容量导致 Haystack 的有效复制因子增加到 5.3 倍。相比之下，f4 的有效复制因子为 2.8 倍（在两个不同的数据中心使用 RS(10,4) 编码）。此外，Blob 存储转向了更短暂的多媒体，这些多媒体虽曾存储在 Haystack 中，但在移动到 f4 之前已被删除。结果，总 Blob 数据中越来越大的比例以 Haystack 的高有效复制因子存储。</p><p>最后，由于 Haystack 和 f4 是独立的系统，每个系统都束缚了无法与其他系统共享的资源。Haystack 过度配置存储以容纳峰值 IOPS，而 f4 则因存储大量访问频率较低的数据而拥有丰富的 IOPS。将 Blob 存储迁移到 Tectonic 回收了这些被束缚的资源，并实现了约 2.8 倍的有效复制因子。</p><h3 id="2-2-数据仓库"><a href="#2-2-数据仓库" class="headerlink" title="2.2 数据仓库"></a>2.2 数据仓库</h3><p>数据仓库为数据分析提供存储。数据仓库应用程序存储诸如海量 Map-Reduce 表、社交图谱快照、AI 训练数据和模型等对象。包括 Presto [3]、Spark [10] 和 AI 训练流水线 [4] 在内的多个计算引擎访问这些数据，处理它们并存储派生数据。仓库数据被划分为数据集，用于存储不同产品组（如搜索、信息流、广告）的相关数据。</p><p>数据仓库存储优先考虑读写吞吐量而非延迟，因为数据仓库应用程序通常批量处理数据。数据仓库工作负载的读写操作通常比 Blob 存储更大，读取平均几 MB，写入平均几十 MB。</p><p><strong>HDFS 用于数据仓库存储。</strong> 在 Tectonic 之前，数据仓库使用 Hadoop 分布式文件系统（HDFS）[15, 50]。然而，HDFS 集群规模有限，因为它使用单台机器存储和提供元数据。因此，我们每个数据中心需要数十个 HDFS 集群来存储分析数据。这在运维上效率低下；每个服务都必须了解数据在集群间的放置和移动。单个数据仓库数据集通常大到足以超过单个 HDFS 集群的容量。这使计算引擎逻辑复杂化，因为相关数据通常被分割在单独的集群中。</p><p>最后，将数据集分布在 HDFS 集群中产生了一个二维装箱问题。将数据集打包进集群必须遵守每个集群的容量约束和可用吞吐量。Tectonic 的艾字节规模消除了装箱和数据集分割问题。</p><h2 id="3-架构与实现"><a href="#3-架构与实现" class="headerlink" title="3 架构与实现"></a>3 架构与实现</h2><p>本节描述 Tectonic 的架构和实现，重点关注 Tectonic 如何通过其可扩展的数据块和元数据存储实现艾字节级的单集群。</p><h3 id="3-1-Tectonic：概览"><a href="#3-1-Tectonic：概览" class="headerlink" title="3.1 Tectonic：概览"></a>3.1 Tectonic：概览</h3><p>集群是 Tectonic 的最高层部署单元。Tectonic 集群是数据中心本地的，提供持久存储，并能抵御主机、机架和电源域故障。租户可以在 Tectonic 之上构建跨地域复制以防范数据中心故障（图 1）。</p><p><img src="/images/facebook-tectonic-filesystem/%E8%AF%91%EF%BD%9CFacebook's%20Tectonic%20Filesystem%EF%BC%9AEfficiency%20from%20Exascale-20250826230736-2.png" alt="译｜Facebook&#39;s Tectonic Filesystem：Efficiency from Exascale-20250826230736-2.png"></p><p><em>图 2：Tectonic 架构。箭头表示网络调用。Tectonic 在键值存储中存储文件系统元数据。除数据块存储和元数据存储外，所有组件都是无状态的。</em></p><p>一个 Tectonic 集群由存储节点（storage nodes）、元数据节点（metadata nodes）以及用于后台操作的无状态节点组成。客户端库协调对元数据和存储节点的远程过程调用（RPC）。Tectonic 集群可以非常庞大：单个集群可以满足单个数据中心内所有租户的存储需求。</p><p>Tectonic 集群是 <em>多租户</em> 的，在同一个存储架构上支持大约十个租户（§4）。租户是分布式系统，彼此之间永远不会共享数据；租户包括 Blob 存储和数据仓库。这些租户反过来服务于数百个 <em>应用程序</em> ，包括信息流（Newsfeed）、搜索（Search）、广告（Ads）和内部服务，每个应用程序都有不同的流量模式和性能要求。</p><p>Tectonic 集群在相同的存储和元数据组件上支持任意数量、任意大小的 <em>名称空间</em> ，即文件系统目录层次结构。集群中的每个租户通常拥有一个名称空间。名称空间的大小仅受集群大小的限制。</p><p>应用程序通过具有仅追加语义的分层文件系统 API 与 Tectonic 交互，类似于 HDFS [15]。与 HDFS 不同，Tectonic API 在运行时是可配置的，而不是在集群或租户级别预先配置。Tectonic 租户利用这种灵活性来匹配专用存储系统的性能（§4）。</p><p><strong>Tectonic 组件。</strong> 图 2 显示了 Tectonic 的主要组件。Tectonic 集群的基础是数据块存储（§3.2），这是一组在硬盘上存储和访问数据块的存储节点。</p><p>在数据块存储之上是 <em>元数据存储</em> （§3.3），它由一个可扩展的键值存储和无状态的元数据服务组成，这些服务在键值存储之上构建文件系统逻辑。它们的可扩展性使 Tectonic 能够存储艾字节的数据。</p><p>Tectonic 是一个客户端驱动的、基于微服务的系统，这种设计支持租户特定的优化。数据块存储和元数据存储各自运行独立的服务来处理数据和元数据的读写请求。这些服务由 <em>客户端库</em> （§3.4）协调；该库将客户端的文件系统 API 调用转换为对数据块和元数据存储服务的 RPC。</p><p>最后，每个集群运行无状态的后台服务以维护集群的一致性和容错性（§3.5）。</p><h3 id="3-2-数据块存储：艾字节级存储"><a href="#3-2-数据块存储：艾字节级存储" class="headerlink" title="3.2 数据块存储：艾字节级存储"></a>3.2 数据块存储：艾字节级存储</h3><p>数据块存储（Chunk Store）是一个用于数据块（chunk）的扁平、分布式对象存储，数据块是 Tectonic 中的数据存储单元。数据块构成块（blocks），块又构成 Tectonic 文件。</p><p>数据块存储有两个特性有助于 Tectonic 的可扩展性和支持多租户的能力。首先，数据块存储是扁平的（flat）；存储的数据块数量随着存储节点数量线性增长。因此，数据块存储可以扩展到存储艾字节的数据。其次，它对更高层次的抽象（如块或文件）是无感知的（oblivious）；这些抽象由客户端库使用元数据存储构建。将数据存储与文件系统抽象分离，简化了在一个存储集群上为多样化租户提供良好性能的问题（§5）。这种分离意味着对存储节点的读写操作可以针对租户的性能需求进行专门优化，而无需更改文件系统管理。</p><p><strong>高效存储数据块。</strong> 单个数据块作为文件存储在集群的存储节点上，每个节点运行一个 XFS [26] 的本地实例。存储节点暴露核心 IO API 来获取（get）、放置（put）、追加（append）和删除（delete）数据块，以及列出（list）和扫描（scan）数据块的 API。存储节点负责确保其本地资源在 Tectonic 租户之间公平共享（§4）。</p><p>每个存储节点有 36 个硬盘用于存储数据块 [5]。每个节点还有一个 1TB SSD，用于存储 XFS 元数据和缓存热数据块。存储节点运行一个将本地 XFS 元数据存储在闪存上的 XFS 版本 [47]。这对于 Blob 存储特别有帮助，因为新的 Blob 是作为追加写入的，这会更新数据块大小。SSD 热数据块缓存由一个闪存耐久性感知的缓存库管理 [13]。</p><p><strong>块作为持久存储单元。</strong> 在 Tectonic 中，块是一个逻辑单元，向上层隐藏了原始数据存储和持久性的复杂性。在上层看来，块是一个字节数组。实际上，块由数据块构成，这些数据块共同提供块的持久性。</p><p>Tectonic 提供按块的持久性，允许租户调整存储容量、容错性和性能之间的权衡。块使用 Reed-Solomon（RS）编码 [43] 或复制来实现持久性。对于 RS(r,k) 编码，块数据被分割成 r 个相等的数据块（可能通过填充数据），并从数据块生成 k 个奇偶校验数据块（parity chunks）。对于复制，数据块与块大小相同，并创建多个副本。一个块中的数据块存储在不同的故障域（例如，不同的机架）中以实现容错。后台服务修复损坏或丢失的数据块以维持持久性（§3.5）。</p><h3 id="3-3-元数据存储：命名艾字节数据"><a href="#3-3-元数据存储：命名艾字节数据" class="headerlink" title="3.3 元数据存储：命名艾字节数据"></a>3.3 元数据存储：命名艾字节数据</h3><p>Tectonic 的元数据存储（Metadata Store）存储文件系统层次结构以及块到数据块的映射。为了操作简便性和可扩展性，元数据存储对文件系统元数据进行细粒度分区。文件系统元数据首先被 <em>解耦</em> ，意味着名称（naming）、文件（file）和块（block）层在逻辑上是分离的。然后每一层再进行哈希分区（表 1）。正如我们在本节所述，可扩展性和负载均衡在这种设计中是自然获得的。通过对元数据操作的精心处理，尽管元数据分区很细粒度，文件系统的一致性得以保留。</p><table><thead><tr><th>层 (Layer)</th><th>键 (Key)</th><th>值 (Value)</th><th>共享依据 (Shared by)</th><th>映射 (Mapping)</th></tr></thead><tbody><tr><td>Name</td><td>(dir_id, subdirname)<br/>(dir_id, filename)</td><td>subdir_info, subdir_id<br/>file_info, file_id</td><td>dir_id<br/>dir_id</td><td>dir → list of subdirs (expanded)<br/>dir → list of files (expanded)</td></tr><tr><td>File</td><td>(file_id, blk_id)</td><td>blk_info</td><td>file_id</td><td>file → list of blocks (expanded)</td></tr><tr><td>Block</td><td>blk_id<br/>(disk_id, blk_id)</td><td>list&lt;disk_id&gt;<br/>chunk_info</td><td>blk_id<br/>blk_id</td><td>block → list of disks (i.e., chunks)<br/>disk → list of blocks (expanded)</td></tr></tbody></table><p><em>表 1：Tectonic 的分层元数据模式。目录名（dirname）和文件名（filename）是应用程序暴露的字符串。dir_id、file_id 和 block_id 是内部对象引用。大多数映射是展开的，以便高效更新。</em></p><p><strong>将元数据存储在键值存储中，实现可扩展性和操作简便性。</strong> Tectonic 将文件系统元数据存储委托给 ZippyDB [6] —— 一个具有线性一致性、容错性的分片键值存储。键值存储以分片粒度管理数据：所有操作都限定在一个分片内，分片是复制的单元。键值存储节点内部运行 RocksDB [23] —— 一个基于 SSD 的单节点键值存储，用于存储分片副本。分片使用 Paxos [30] 进行复制以实现容错。任何副本都可以服务读取请求，但必须由主副本提供强一致的读取服务。键值存储不提供跨分片事务，这限制了某些文件系统元数据操作。</p><p>分片的大小被设定，每个元数据节点可以托管多个分片。这允许在节点故障时并行地将分片重新分配到新节点，从而减少恢复时间。它还允许细粒度的负载均衡；键值存储会透明地移动分片以控制每个节点上的负载。</p><p><strong>文件系统元数据层。</strong> 表 1 显示了文件系统元数据层、它们映射的内容以及如何分片。名称层（Name layer）将每个目录映射到其子目录和&#x2F;或文件。文件层（File layer）将文件对象映射到块列表。块层（Block layer）将每个块映射到磁盘（即数据块）位置列表。块层还包含磁盘到块（即记录某磁盘存储了哪些块的数据块）的反向索引，用于维护操作。名称层、文件层和块层分别按目录 ID、文件 ID 和块 ID 进行哈希分区。</p><p>如表 1 所示，名称层和文件层以及磁盘到块列表的映射是 <em>展开</em> 的。映射到列表的键通过以下方式展开：将列表中每个条目存储为独立键，并添加原键作为前缀。例如，如果目录 d1 包含文件 foo 和 bar，我们在 d1 的名称层分片（Name shard）中存储两个键 (d1, foo) 和 (d1, bar)。展开机制允许修改键的内容，而无需先读取整个列表再重新写入。在映射可能非常庞大的文件系统中（例如，目录可能包含数百万文件），展开机制显著减少了某些元数据操作（如文件创建和删除）的开销。展开键的内容通过键前缀扫描列出。</p><p><strong>细粒度元数据分区以避免热点。</strong> 在文件系统中，目录操作经常在元数据存储中引起热点。这对数据仓库工作负载尤为明显：其相关数据按目录分组存储；短时间内可能密集读取同一目录下的多个文件，从而引发对目录的重复访问。</p><p>Tectonic 的分层元数据方法通过将搜索和列出目录内容（名称层）与读取文件数据（文件和块层）分离开来，自然地避免了目录和其他层的热点。这与 ADLS 分离元数据层的方法类似 [42]。然而，ADLS 使用范围分区元数据层，而 Tectonic 使用哈希分区（元数据）层。范围分区倾向于将相关数据（例如目录层次结构的子树）放在同一个分片上，如果不仔细分片，元数据层容易产生热点。</p><p>我们发现哈希分区能有效地负载均衡元数据操作。例如，在名称层，单个目录的直接目录列表始终存储在一个分片中。但同一目录的两个子目录的列表很可能位于不同的分片上。在块层，块定位信息被哈希到各个分片，与块的目录或文件无关。Tectonic 中大约三分之二的元数据操作由块层处理，但哈希分区确保此流量在块层分片之间均匀分布。</p><p><strong>缓存已封存（sealed）对象元数据以减少读取负载。</strong> 元数据分片的可用吞吐量有限，因此为了减少读取负载，Tectonic 允许块、文件和目录被 <em>封存</em> 。目录封存不递归应用，它只阻止在目录的直接层级添加对象。已封存的文件系统对象的内容无法更改；它们的元数据可以在元数据节点和客户端缓存而不会影响一致性。例外是块到数据块的映射；数据块可以在磁盘之间迁移，使块层缓存失效。陈旧的块层缓存可以在读取期间检测到，从而触发缓存刷新。</p><p><strong>提供一致的元数据操作。</strong> Tectonic 依赖键值存储的强一致操作和分片内原子读 - 改 - 写事务来实现同目录内的强一致操作。更具体地说，Tectonic 保证数据操作（例如，追加、读取）、涉及单个对象的文件和目录操作（例如，创建、列表）以及源路径和目标路径位于同一个父目录下的移动操作具有写后读一致性。一个目录中的文件位于该目录的分片中（表 1），因此像文件创建、删除和在父目录内的移动等元数据操作是一致的。</p><p>键值存储不支持一致的跨分片事务，因此 Tectonic 提供非原子的跨目录移动操作。将目录移动到不同分片上的另一个父目录是一个两阶段过程。首先，我们从新的父目录创建一个链接（link），然后从之前的父目录删除该链接。被移动的目录保留一个指向其父目录的回溯指针（backpointer）以检测挂起的移动。这确保一次只有一个移动操作对一个目录是活动的。同样地，跨目录的文件移动通常需要复制文件内容，然后从源目录中删除原文件。复制步骤会创建一个新的文件对象，该对象直接关联源文件的底层数据块，从而避免实际的数据移动。</p><blockquote><p>译者注：</p><p> <strong>阶段一：</strong></p><p> 1、检查回溯指针，如果 <code>bp.state = stable</code>，继续；如果 <code>bp.state = moving</code>，说明已有挂起的 move，拒绝；<br> 2、CAS 更新 <code>bp.state = moving, bp.parent_id=Parent2</code><br> 3、在新父目录 Parent2 下创建新链接</p><p><strong>阶段二：</strong></p><p>1、更新 <code>bp.state = stable</code><br>2、删除旧父目录 Parent1 的链接</p><p><strong>崩溃恢复：</strong></p><p>1、如果崩溃后 <code>bp.state = moving</code>，则检查 Parent2 是否已有新链接：</p><ul><li>有 → 执行阶段二，收尾</li><li>无 → 回滚，CAS 更新 <code>bp.state = stable, bp.parent_id = Parent1</code>，保持在 Parent1<br>2、如果崩溃后 <code>bp.state = stable, bp.parent_id = Parent2</code>，但 Parent1 链接还没删掉，则清理多余的旧链接</li></ul></blockquote><p>在没有跨分片事务的情况下，对同一文件进行的多分片元数据操作必须仔细实现以避免竞态条件。这种竞态条件的一个例子是：当目录 d 中名为 f1 的文件被重命名为 f2 时。同时，创建一个同名的新文件，其中创建操作会覆盖同名的现有文件。括号中列出了每个步骤的元数据层和分片查找键（shard(x)）。</p><p>文件重命名操作包含以下步骤：</p><ul><li>R1: 获取 f1 的文件 ID fid（Name, shard(d)）</li><li>R2: 添加 f2 作为 fid 的拥有者（File, shard(fid)）</li><li>R3: 在一个原子事务中创建映射 f2 → fid 并删除 f1 → fid（Name, shard(d)）</li></ul><blockquote><p>译者注：</p><p><strong>为什么 R3 不涉及跨分片？</strong></p><p><strong>目录项 (filename → fileID)</strong> 映射都存放在 <strong>Name 层</strong>，并且分片的方式是按目录来分片，即 <code>shard(d)</code>。  也就是说，同一个目录 <code>d</code> 下的所有文件名映射（<code>f1 → fid</code>、<code>f2 → fid</code> 等），都会被存在同一个 shard。所以可以在 <strong>一个分片内事务</strong>中完成这两个更新，而无需跨分片协调</p></blockquote><p>文件覆盖创建流程包含以下步骤：</p><ul><li>C1: 创建新文件 ID fid_new（File, shard(fid_new)）</li><li>C2: 映射 f1 → fid_new；删除 f1 → fid（Name, shard(d)）</li></ul><p>交错执行事务中的步骤可能导致文件系统处于不一致状态。若步骤 C1 和 C2 在 R1 之后、R3 之前执行，则 R3 操作将擦除由创建操作生成的新映射。重命名步骤 R3 通过分片内事务确保 f1 指向的文件对象自 R1 步骤后未被修改。</p><blockquote><p>译者注：</p><p>如果 R3 的 “删除 f1→fid”是<strong>不带条件</strong>的“删 key&#x3D;f1”（或没有校验 value 仍是 <code>fid</code>），它会把 <strong>刚刚由 C2 建立的 <code>f1 → fid_new</code></strong> 也一并删掉。于是出现违背语义的坏结局：</p><ul><li>Name：<code>f2 → fid</code>（被写入了），**<code>f1</code> 不存在**（被误删），</li><li>File：有 <code>fid_new</code> 这个新文件，但 <strong>没有任何名字指向它</strong>（成为悬挂对象&#x2F;垃圾），</li><li>等价于把“覆盖创建”的结果给抹掉了——典型的 <strong>丢失更新（lost update）</strong>。</li></ul><p><strong>为什么会发生?</strong></p><ul><li>R1 在 <strong>读到旧现实</strong>（<code>f1 → fid</code>）后，并没有把该现实“锁住”。</li><li>C2 在 <strong>同一个 Name 分片</strong>内把现实改成了 <code>f1 → fid_new</code>。</li><li>R3 <strong>晚到了</strong>，如果它没有基于“R1 看到的版本”做校验，而是直接执行“创建 <code>f2</code> 并删除 <code>f1</code>”，就会错误地删除了 C2 的新映射。</li></ul></blockquote><h3 id="3-4-客户端库"><a href="#3-4-客户端库" class="headerlink" title="3.4 客户端库"></a>3.4 客户端库</h3><p>Tectonic 客户端库协调数据块和元数据存储服务，向应用程序暴露文件系统抽象，这使应用程序能够按操作控制如何配置读写。此外，客户端库在数据块粒度上执行读写操作，这是 Tectonic 中最精细的粒度。这使得客户端库几乎可以自由地以对应用程序最有效的方式执行操作，这些应用程序可能有不同的工作负载或偏好不同的权衡（§5）。</p><p>客户端库复制或 RS 编码数据，并将数据块直接写入数据块存储。它为应用程序从数据块存储读取并重建数据块。客户端库查询元数据存储以定位数据块，并为文件系统操作更新元数据存储。</p><p><strong>单写入者语义实现简单、可优化的写入。</strong> Tectonic 通过允许每个文件只有一个写入者来简化客户端库的协调。单写入者语义避免了从多个写入者序列化对文件写入的复杂性。客户端库可以改为并行地直接写入存储节点，允许它并行复制数据块并进行对冲写入（§5）。需要多写入者语义的租户可以在 Tectonic 之上构建序列化语义。</p><p>Tectonic 通过为每个文件设置一个写入令牌（write token）来强制执行单写入者语义。每当写入者想要向文件添加一个块时，它必须包含一个匹配的令牌才能使元数据写入成功。当一个进程打开文件进行追加时，令牌被添加到文件元数据中，后续写入必须包含此令牌才能更新文件元数据。如果第二个进程尝试打开该文件，它将生成一个新令牌并覆盖第一个进程的令牌，成为该文件新的、也是唯一的写入者。新写入者的客户端库将在打开文件调用中封存前一个写入者打开的任何块。</p><h3 id="3-5-后台服务"><a href="#3-5-后台服务" class="headerlink" title="3.5 后台服务"></a>3.5 后台服务</h3><p>后台服务维护元数据层之间的一致性，通过修复丢失的数据来维持持久性，在存储节点之间重新均衡数据，处理机架下线，并发布有关文件系统使用情况的统计信息。后台服务分层类似于元数据存储，并且它们一次操作一个分片。图 2 列出了重要的后台服务。</p><p>每个元数据层之间的垃圾收集服务（garbage collector）清理（可接受的）元数据不一致性。元数据不一致可能源于失败的多步骤客户端库操作。惰性对象删除是一种实时延迟优化，它在删除时标记已删除对象而不实际移除它们，也会导致不一致。</p><p>再均衡服务（rebalancer）和修复服务（repair service）协同工作来重新定位或删除数据块。再均衡器识别需要移动的数据块以响应硬件故障、增加存储容量和机架下线等事件。修复服务通过为系统中的每个磁盘协调数据块列表与磁盘到块的映射来处理实际的数据移动。为了水平扩展，修复服务在块层分片、单磁盘维度工作，该机制依托磁盘到块的反向索引映射实现（表 1）。</p><p><strong>大规模下的副本集。</strong> 副本集是为同一个块提供冗余的磁盘组合（例如，一个 RS(10,4) 编码块的副本集由 14 个磁盘组成）[20]。副本集过多会在磁盘故障意外激增时带来数据不可用的风险。另一方面，副本集过少会导致当一个磁盘故障时，对等磁盘的重建负载很高，因为它们共享许多数据块。</p><p>块层和再均衡器服务共同尝试维持一个固定的副本集数量，以平衡不可用性和重建负载。它们各自在内存中保留大约一百份集群磁盘的确定性分布拓扑。块层在同一分布拓扑中选取连续磁盘形成副本组。执行写入操作时，块层根据块 ID 的对应分布拓扑，向客户端库提供目标副本组。再均衡服务则致力于将数据块的分片保留在其分布拓扑指定的副本组中。需注意的是，副本组机制采用尽力而为原则，因为集群中的磁盘成员持续动态变化。</p><h2 id="4-多租户"><a href="#4-多租户" class="headerlink" title="4 多租户"></a>4 多租户</h2><p>在租户从单独的、专用的存储系统迁移到整合的文件系统时，为其提供可比的性能面临两个挑战。首先，租户必须共享资源，同时为每个租户提供其公平份额，即至少与其在单租户系统中相同的资源。其次，租户应该能够像在专用系统中一样优化性能。本节描述 Tectonic 如何通过保持操作简便性的简洁设计来支持资源共享。第 5 节描述 Tectonic 的租户特定优化如何使租户获得与专用存储系统相当的性能。</p><h3 id="4-1-有效共享资源"><a href="#4-1-有效共享资源" class="headerlink" title="4.1 有效共享资源"></a>4.1 有效共享资源</h3><p>作为 Facebook 上多样化租户的共享文件系统，Tectonic 需要有效地管理资源。具体来说，Tectonic 需要在租户之间提供近似（加权）公平的资源共享和租户之间的性能隔离，同时在应用程序之间弹性地转移资源以维持高资源利用率。Tectonic 还需要区分延迟敏感的请求，以避免它们被大型请求阻塞。</p><p><strong>资源类型。</strong> Tectonic 区分两种类型的资源：非临时性（non-ephemeral）和临时性（ephemeral）。存储容量是 <em>非临时性</em> 资源。它变化缓慢且可预测。最重要的是，一旦分配给租户，就不能再给另一个租户。存储容量在租户粒度上进行管理。每个租户获得预定义的容量配额，具有严格的隔离性，即分配给不同租户的空间没有自动弹性。租户之间的存储容量重新配置是手动完成的。重新配置不会导致停机，因此在紧急容量紧张的情况下可以立即进行。租户负责在其应用程序之间分配和跟踪存储容量。</p><p><em>临时性</em> 资源是指需求会瞬息变化、并且其分配能够实时调整的资源。存储 IOPS 容量和元数据查询容量是两种临时性资源。由于临时性资源需求变化迅速，这些资源需要更细粒度的实时自动化管理，以确保它们被公平共享、租户彼此隔离，并且资源利用率高。在本节的剩余部分，我们将描述 Tectonic 如何有效地共享临时性资源。</p><p><strong>在租户内部和租户之间分配临时性资源。</strong> 临时性资源共享在 Tectonic 中具有挑战性，因为不仅租户是多样化的，而且每个租户服务于许多具有不同流量模式和性能要求的应用程序。例如，Blob 存储包括来自 Facebook 用户的生产流量和后台垃圾回收流量。在租户粒度管理临时性资源过于粗糙，无法考虑租户内多样化的工作负载和性能要求。另一方面，由于 Tectonic 服务于数百个应用程序，在应用程序粒度管理资源过于复杂且消耗大量资源。</p><p>因此，临时性资源在每个租户内部以应用程序组的粒度进行管理。这些应用程序组称为 <em>流量组（TrafficGroups）</em> ，减少了资源共享问题的基数，降低了管理多租户的开销。同一流量组中的应用程序具有相似的资源和延迟要求。例如，一个流量组可能用于生成后台流量的应用程序，而另一个用于生成生产流量的应用程序。Tectonic 每个集群支持大约 50 个流量组。每个租户可能有不同数量的流量组。租户负责为其每个应用程序选择合适的流量组。每个流量组又被分配一个 <em>流量等级（TrafficClass）</em> 。流量组的流量等级指示其延迟要求，并决定哪些请求应获得富余资源。流量等级分为黄金（Gold）、白银（Silver）和青铜（Bronze），分别对应延迟敏感、正常和后台应用程序。富余资源根据流量等级优先级在租户内分配。</p><p>Tectonic 使用租户和流量组以及流量等级的概念来确保隔离性和高资源利用率。也就是说，租户被分配其公平份额的资源；在每个租户内部，资源按流量组和流量等级分配。每个租户获得集群临时性资源的保证配额，该配额在其租户的流量组之间细分。每个流量组获得其保证的资源配额，这提供了租户之间以及流量组之间的隔离。</p><p>租户内部的任何临时性资源富余按其流量等级降序优先分配给其自身的流量组。任何剩余的富余按流量等级降序分配给其他租户的流量组。这确保了富余资源首先由同一租户的流量组使用，然后再分配给其他租户。当一个流量组使用另一个流量组的资源时，由此产生的流量获得两个流量组中较低的流量等级。这确保了不同等级的流量比例不会基于资源分配而改变，从而确保节点能够满足流量等级的延迟特性。</p><p><strong>强制执行全局资源共享。</strong> 客户端库使用速率限制器（rate limiter）来实现上述弹性。速率限制器使用高性能、近实时的分布式计数器来跟踪每个租户和流量组在过去小时间窗口内对每个被跟踪资源的需求。速率限制器实现了一个改进的漏桶算法。传入的请求增加桶的需求计数器。然后，客户端库在自己的流量组、同一租户的其他流量组以及最后其他租户中检查富余容量，遵守流量等级优先级。如果客户端找到富余容量，请求被发送到后端。否则，根据请求的超时设置，请求被延迟或拒绝。在客户端节流请求，可以在客户端发出可能被浪费的请求之前施加反压。</p><p><strong>强制执行本地资源共享。</strong> 客户端的速率限制器确保近似的全局公平共享和隔离。元数据和存储节点也需要管理资源以避免本地热点。节点通过加权轮询调度器提供公平共享和隔离，如果一个流量组将要超过其资源配额，则临时跳过其轮次。此外，存储节点需要确保小型 IO 请求（例如，Blob 存储操作）不会因为与大型、突发的 IO 请求（例如，数据仓库操作）共置而遭遇更高的延迟。黄金流量等级请求若在存储节点上被阻塞在较低优先级请求之后，则可能无法达到其延迟目标。</p><p>存储节点使用三种优化来确保黄金流量等级请求的低延迟。首先，WRR 调度器提供一种贪婪优化策略，在让位给较高流量类别的请求后仍有足够时间完成自身操作时，系统会允许其主动让位。这一机制可避免高等级请求被低等级请求阻塞。其次，我们对每块磁盘并发处理的非黄金级 IO 请求数量实施限制。当存在挂起的黄金级请求且非黄金级请求并发数已达上限时，系统将阻止新的非黄金流量请求开始调度。这确保磁盘不会在仍有 Blob 存储请求等待时，持续处理大型数据仓库 IO 操作。最后，针对磁盘自身可能重新排列 IO 请求序列（例如优先处理后续的非黄金级请求而搁置先到的黄金级请求）的情况，当某磁盘上的黄金级请求等待时间超过设定阈值时，Tectonic 会停止向该磁盘调度非黄金级请求。三项技术协同作用，即使面对大规模大型 IO 请求的场景，也能有效维持小型 IO 请求的延迟特征。</p><h3 id="4-2-多租户访问控制"><a href="#4-2-多租户访问控制" class="headerlink" title="4.2 多租户访问控制"></a>4.2 多租户访问控制</h3><p>Tectonic 遵循常见的安全原则，确保所有通信和依赖项都是安全的。Tectonic 还提供租户之间的粗粒度访问控制（防止一个租户访问另一个租户的数据）和租户内部的细粒度访问控制。由于客户端库直接与每一层通信，必须在 Tectonic 的每一层强制执行访问控制。由于访问控制位于每次读取和写入的关键路径上，因此它必须是轻量级的。</p><p>Tectonic 使用基于令牌的授权机制，该机制包含可以使用令牌访问哪些资源的信息 [31]。授权服务（authorization service）授权顶级客户端请求（例如，打开文件），为文件系统中的下一层生成授权令牌；后续每一层同样授权下一层。令牌的有效负载描述了授予访问权限的资源，从而实现细粒度的访问控制。每一层完全在内存中验证令牌和有效负载中指示的资源；验证可以在几十微秒内完成。将令牌传递搭载在现有协议上减少了访问控制的开销。</p><h2 id="5-租户特定优化"><a href="#5-租户特定优化" class="headerlink" title="5 租户特定优化"></a>5 租户特定优化</h2><p>Tectonic 在同一个共享文件系统中支持大约十个租户，每个租户都有特定的性能需求和工作负载特征。两种机制允许租户特定的优化。首先，客户端几乎完全控制如何配置应用程序与 Tectonic 的交互；客户端库在数据块级别操作数据，这是可能的最精细粒度（§3.4）。这种客户端库驱动的设计使 Tectonic 能够根据应用程序的性能需求执行操作。</p><p>其次，客户端在每次调用时强制执行配置。许多其他文件系统将配置固化在系统中，或应用于整个文件或名称空间。例如，HDFS 按目录配置持久性 [7]，而 Tectonic 按块写入配置持久性。每次调用的配置得益于元数据存储的可扩展性：元数据存储可以轻松处理这种方法增加的元数据。接下来我们描述数据仓库和 Blob 存储如何利用每次调用的配置实现高效写入。</p><h3 id="5-1-数据仓库写入优化"><a href="#5-1-数据仓库写入优化" class="headerlink" title="5.1 数据仓库写入优化"></a>5.1 数据仓库写入优化</h3><p>数据仓库工作负载的一个常见模式是写入一次数据，稍后会被多次读取。对于这些工作负载，文件只有在创建者关闭文件后才对读者可见。然后文件在其生命周期内是不可变的。由于文件只有在完全写入后才被读取，应用程序优先考虑较低的文件写入时间而非较低的追加延迟。</p><p><strong>全块、RS 编码的异步写入，提高空间、IO 和网络效率。</strong> Tectonic 利用一次写入多次读取模式来提高 IO 和网络效率，同时最小化总文件写入时间。这种模式中不存在部分文件读取，允许应用程序将写入缓冲达到块大小。应用程序然后在内存中对块进行 RS 编码，并将数据块写入存储节点。长期数据通常使用 RS(9,6) 编码；短期数据，例如 Map-Reduce 洗牌（shuffles），通常使用 RS(3,3) 编码。</p><p>写入 RS 编码的全块比复制节省存储空间、网络带宽和磁盘 IO。存储和带宽更低，因为写入的总数据量更少。磁盘 IO 更低，因为磁盘使用更高效。在 RS(9,6) 中写入块需要向 15 个磁盘写入数据块，因此需要更多的 IOPS，但每次写入都很小，并且写入的数据总量远小于复制。这导致磁盘 IO 更高效，因为块大小足够大，使得全块写入的瓶颈是磁盘带宽而非 IOPS。</p><p>一次写入多次读取模式还允许应用程序异步并行写入文件的块，这显著减少了文件写入延迟。一旦文件的块被写入，文件元数据会一次性更新。这种策略没有不一致的风险，因为文件只有在完全写入后才可见。</p><p><strong>对冲仲裁组写入改善尾部延迟。</strong> 对于全块写入，Tectonic 使用一种仲裁组写入的变体，该变体在不增加额外 IO 的情况下减少尾部延迟。Tectonic 不是将数据块写入负载发送到额外的节点，而是首先发送预留请求，然后将数据块写入最先接受预留的节点。预留步骤类似于对冲策略 [22]，但其避免了向以下两类节点传输数据：因资源不足无法接收请求的节点，或请求者已超出在该节点资源份额的节点（§4）。</p><blockquote><p>译者注：</p><p>“对冲”概念来源于金融领域，即通过投资多种资产来降低风险。在计算机系统中，“对冲”的基本思想是：<strong>为了降低延迟和避免个别节点性能不佳的影响，客户端主动将同一个请求同时发送给多个服务器（或副本），然后采用最先返回的那个结果，并取消其他未完成的请求。</strong></p><p>这是一种 <strong>用额外的资源（网络带宽、服务器计算资源）来换取更低延迟和更高可靠性</strong> 的策略。</p></blockquote><p>例如，要写入一个 RS(9,6) 编码的块，客户端库向不同故障域中的 19 个存储节点发送预留请求，比写入所需多 4 个。客户端库将数据和奇偶校验块写入最先响应的 15 个存储节点。一旦 15 个节点中有 14 个（即仲裁数）返回成功，它就向客户端确认写入成功。如果第 15 个写入失败，相应的数据块将在离线时修复。</p><p>当集群负载很高时，对冲步骤更有效。图 3a 显示，在一个吞吐量利用率为 80% 的测试集群中，RS(9,6) 编码的 72MB 全块写入的 99 分位延迟（99th percentile latency）提高了约 20%。</p><h3 id="5-2-Blob-存储优化"><a href="#5-2-Blob-存储优化" class="headerlink" title="5.2 Blob 存储优化"></a>5.2 Blob 存储优化</h3><p>Blob 存储对文件系统具有挑战性，因为需要索引的对象数量巨大。Facebook 存储数万亿个 Blob。Tectonic 通过将许多 Blob 一起存储到日志结构文件（log-structured files）中来管理 Blob 存储元数据的大小，其中新的 Blob 追加在文件末尾。Blob 通过一个从 Blob ID 到其在文件中位置的映射进行定位。</p><p>Blob 存储同样位于许多用户请求的关键路径上，因此需要实现低延迟访问。Blob 通常比 Tectonic 块小得多（§2.1）。因此，为实现低延迟，Blob 存储采用小的、复制的部分块追加写入的方式存储新的 Blob。部分块追加写入需要具有写后读一致性，以便 Blob 在上传成功后可以立即读取。但需要注意的是，复制的数据会比全块 RS 编码的数据使用更多的磁盘空间。</p><p><strong>一致的部分块追加写入实现低延迟。</strong> Tectonic 使用 <em>部分块仲裁组追加写入</em> 来实现持久、低延迟、一致的 Blob 写入。在仲裁组追加写入中，客户端库在存储节点子集成功将数据写入磁盘后确认写入，例如三副本复制中两个节点成功即可。由于块很快会被重新编码，且 Blob 存储会将第二个副本写入另一个数据中心，仲裁组写入导致的持久性短暂降低是可接受的。</p><p>部分块仲裁组追加写入的挑战在于，滞后的追加操作可能导致副本数据块出现大小不一致的情况。Tectonic 通过精细控制块追加权限以及可见时机来维护一致性。块只能由创建该块的写入者执行追加操作。当追加操作完成后，Tectonic 会先将追加后的块大小和校验和提交到块元数据，然后确认部分块仲裁组追加写入完成。</p><p>这种由单一追加者维持的操作顺序确保了一致性。如果块元数据报告块大小为 S，则该块此前所有字节均已被写入至少两个存储节点。读者能够访问该块中偏移量 S 之前的数据。同理，任何已向应用程序返回确认的写入操作，其元数据必然已完成更新，从而确保后续读取操作的可见性。图 3b 和 3c 表明 Tectonic 的 Blob 存储读写延迟与 Haystack 相当，验证了 Tectonic 的通用性没有显著的性能成本。</p><p><img src="/images/facebook-tectonic-filesystem/%E8%AF%91%EF%BD%9CFacebook's%20Tectonic%20Filesystem%EF%BC%9AEfficiency%20from%20Exascale-20250826230736-3.png" alt="译｜Facebook&#39;s Tectonic Filesystem：Efficiency from Exascale-20250826230736-3.png"></p><p><em>图 3：Tectonic 中的尾部延迟优化。(a) 显示了在负载约 80% 的测试集群中，对冲仲裁组写入（72MB 块）对数据仓库尾部延迟的改善。(b) 和 (c) 显示了 Tectonic Blob 存储的写入延迟（带和不带仲裁组追加写入）以及读取延迟与 Haystack 的比较。</em></p><p><strong>重新编码块以提高存储效率。</strong> 直接对小的部分块追加写入进行 RS 编码会导致 IO 效率低下。小型磁盘写入受 IOPS 限制，而 RS 编码会产生更多的 IO 操作（例如，使用 RS(10,4) 需要 14 次 IO，而不是 3 次）。客户端库不是每次追加写入后都执行 RS 编码，而是在块封闭后一次性将副本形式的数据重新编码为 RS(10,4) 格式。与实时 RS 编码相比，重新编码是 IO 高效的，仅需在 14 个目标存储节点上各执行一次大容量 IO 操作。这种由 Tectonic 客户端库驱动设计实现的优化方案，成功融合了两方面优势：既通过快速高效的复制机制处理小型写入，又能及时转换为空间效率更优的 RS 编码格式。</p><h2 id="6-Tectonic-在生产环境"><a href="#6-Tectonic-在生产环境" class="headerlink" title="6 Tectonic 在生产环境"></a>6 Tectonic 在生产环境</h2><p>本节展示 Tectonic 在艾字节规模下的运行情况，证明存储整合的效益，并讨论 Tectonic 如何处理元数据热点。它还讨论了设计 Tectonic 时的权衡和经验教训。</p><h3 id="6-1-艾字节级多租户集群"><a href="#6-1-艾字节级多租户集群" class="headerlink" title="6.1 艾字节级多租户集群"></a>6.1 艾字节级多租户集群</h3><p>生产环境的 Tectonic 集群在艾字节规模下运行。表 2 给出了一个代表性多租户集群的统计数据。本节的所有结果均针对此集群。1250PB 的已使用存储（约占集群当时容量的 70%）包含 107 亿个文件和 150 亿个块。</p><table><thead><tr><th>容量</th><th>已用字节</th><th>文件数</th><th>块数</th><th>存储节点</th></tr></thead><tbody><tr><td>1590 PB</td><td>1250 PB</td><td>10.7 B</td><td>15 B</td><td>4208</td></tr></tbody></table><p><em>表 2：多租户 Tectonic 生产集群的统计数据。文件和块数单位为十亿。</em></p><h3 id="6-2-存储整合的效率提升"><a href="#6-2-存储整合的效率提升" class="headerlink" title="6.2 存储整合的效率提升"></a>6.2 存储整合的效率提升</h3><p>表 2 中的集群托管了两个租户：Blob 存储和数据仓库。Blob 存储约占该集群已用空间的 49%，数据仓库约占 51%。图 4a 和 4b 展示了该集群在三天内处理存储负载的情况。图 4a 显示了集群该时间段内的总 IOPS，图 4b 显示了其总磁盘带宽。数据仓库工作负载因超大规模作业触发而存在显著且规律的负载峰值。相比之下，Blob 存储的流量模式则呈现平滑且可预测的特性。</p><p><img src="/images/facebook-tectonic-filesystem/%E8%AF%91%EF%BD%9CFacebook's%20Tectonic%20Filesystem%EF%BC%9AEfficiency%20from%20Exascale-20250826230736-4.png" alt="译｜Facebook&#39;s Tectonic Filesystem：Efficiency from Exascale-20250826230736-4.png"></p><p><em>图 4：代表性生产集群在三天内的 IO 和元数据负载。(a) 和 (b) 显示了 Blob 存储和数据仓库流量模式的差异，并展示了 Tectonic 在 3 天内成功处理存储 IOPS 和带宽峰值的情况。两个租户在该集群中占据几乎相同的空间。(c) 是该集群元数据分片在三天内峰值元数据负载的累积分布函数（CDF）。每个分片能处理的最大负载是 10K QPS（灰线）。Tectonic 可以处理文件层和块层的所有元数据操作。它可以立即处理几乎所有的名称层操作；剩余的操作在重试时处理。</em></p><p><strong>共享富余 IOPS 容量。</strong> 集群通过整合 Blob 存储所释放的富余 IOPS 容量，处理数据仓库产生的存储负载峰值。Blob 存储请求通常很小且受 IOPS 限制，而数据仓库请求通常很大且受带宽限制。因此，无论是 IOPS 还是带宽都不能公平地衡量磁盘 IO 使用情况。处理存储操作时的瓶颈资源是 <em>磁盘时间（disk time）</em> ，它衡量特定磁盘处于忙碌状态的频率。要处理存储负载峰值，Tectonic 必须具备足够的空闲磁盘时间来应对峰值。例如，若某磁盘在 1 秒内执行 10 次 IO 操作，每次耗时 50 毫秒（寻址 + 读取），则该磁盘在 1000 毫秒中有 500 毫秒处于忙碌状态。我们采用磁盘时间作为公平衡量不同类型请求资源占用的标准。</p><p>在该代表性生产集群中，表 3 显示了数据仓库和 Blob 存储在三个每日峰值期间的标准化磁盘时间需求，以及两者若独立运行时各自的磁盘时间供给量。我们按集群中已用空间对应的总磁盘时间进行标准化。每日峰值与图 4a 和 4b 中三个流量高峰日相对应。在所有三个峰值时段，数据仓库的需求都超过了其独立供给量，若独立运行则需要超额配置磁盘资源。为了满足三天期间的数据仓库峰值需求，集群需要约 17% 的磁盘超额配置。另一方面，Blob 存储拥有富余的磁盘时间，若独立运行这些资源将会闲置。将这两个租户整合到一个 Tectonic 集群中，使得 Blob 存储的富余磁盘时间得以有效利用，从而支撑数据仓库的存储负载峰值。</p><table><thead><tr><th></th><th>数据仓库</th><th>Blob 存储</th><th>合计</th></tr></thead><tbody><tr><td>供给量</td><td>0.51</td><td>0.49</td><td>1.00</td></tr><tr><td>峰值 1</td><td>0.60</td><td>0.12</td><td>0.72</td></tr><tr><td>峰值 2</td><td>0.54</td><td>0.14</td><td>0.68</td></tr><tr><td>峰值 3</td><td>0.57</td><td>0.11</td><td>0.68</td></tr></tbody></table><p><em>表 3：Tectonic 整合数据仓库和 Blob 存储，允许数据仓库利用原本被 Blob 存储闲置的富余磁盘时间来处理大型负载峰值。该图显示了在代表性集群的三个每日峰值期间，磁盘时间需求与供给量的标准化值。</em></p><h3 id="6-3-元数据热点"><a href="#6-3-元数据热点" class="headerlink" title="6.3 元数据热点"></a>6.3 元数据热点</h3><p>元数据存储的负载峰值可能导致元数据分片出现热点。处理元数据操作的瓶颈资源是每秒查询数（QPS）。要应对负载峰值，元数据存储必须确保每个分片都能满足 QPS 需求。在生产环境中，每个分片最多可处理 10K QPS。该限制由当前元数据节点资源的隔离机制所设定。图 4c 显示了集群中元数据分片在名称层、文件层和块层的 QPS 分布情况，其中文件层与块层的所有分片均低于此限值。</p><p>在这三天期间，约 1% 的名称层分片因承载极高热度的目录而触及 QPS 上限。少量未处理的元数据请求会在退避延迟后自动重试。这种退避机制使元数据节点能清除大部分初始峰值流量，并成功处理重试请求。这种机制，加上所有其他分片都低于其最大值，使得 Tectonic 能够成功处理来自数据仓库的元数据负载的大幅峰值。</p><p>分片间的负载分布在名称层、文件层和块层之间存在差异。越高层级的分片间 QPS 分布差异越大，因为它会将租户的更多操作集中处理。例如，特定目录下所有目录到文件的查找操作都由同一个分片处理。若采用类似 ADLS [42] 的范围分区方案，则会将租户更多操作集中到同一分片，导致更剧烈的负载峰值。数据仓库作业经常读取多个名称相似的目录，若采用目录范围分区将引发极端负载峰值。这些作业同时会读取目录中的大量文件，从而在名称层引发负载激增。若对文件层实施范围分区，会将同一目录下的文件集中在相同分片，由于每个作业在文件层的操作量远超名称层，这将导致更严重的负载峰值。Tectonic 采用的哈希分区策略减少了这种集中性，使得系统能用比范围分区更少的节点处理元数据负载峰值。</p><p>Tectonic 还通过与数据仓库协同设计来减少元数据热点。例如，计算引擎通常采用编排器（orchestrator）列出目录中的文件并将文件分发给工作节点，由工作节点并行打开并处理这些文件。在 Tectonic 中，这种模式会向单个目录分片发送大量近乎同时到达的文件打开请求（§3.3），从而引发热点问题。为避免这种反模式，Tectonic 的 list-files API 在返回目录内文件名的同时会同步返回文件 ID。计算引擎协调器（orchestrator）将文件 ID 和名称发送给工作节点后，工作节点可直接通过文件 ID 打开文件，无需再次查询目录分片。</p><h3 id="6-4-简便性与性能的权衡"><a href="#6-4-简便性与性能的权衡" class="headerlink" title="6.4 简便性与性能的权衡"></a>6.4 简便性与性能的权衡</h3><p>Tectonic 的设计通常优先考虑简便性（simplicity）而非效率（efficiency）。我们讨论两个我们选择增加复杂性以换取性能提升的实例。</p><p><strong>管理重建负载。</strong> RS 编码的数据可以 <em>连续</em> 存储，即一个块被分割成数据块，每个数据块连续写入存储节点；也可以条带化存储，即一个数据块被分割成更小的数据块，以轮询方式分布在存储节点上 [51]。因为 Tectonic 使用连续 RS 编码，并且大多数读取小于一个数据块大小，所以通常是 <em>直接</em> 读取：无需 RS 重建，仅需单次磁盘 IO。重建读取所需的 IO 次数是直接读取的 10 倍（对于 RS(10,4) 编码）。虽然重建读取很常见，但难以预测其具体占比——硬件故障和节点过载都可能触发重建。我们认识到，若不加以控制，这种资源需求的剧烈波动可能引发级联故障，进而影响系统可用性与性能。</p><p>若某些存储节点过载，直接读取会失败并触发重建读取。这将增加系统其余部分的负载，进而引发更多重建读取，形成恶性循环。这种级联重建现象被称为 <em>重建风暴</em> 。一种简单的解决方案是采用条带式 RS 编码（所有读取都需要重建），这样可避免重建风暴，因为发生故障时读取所需的 IO 次数不会改变。但该方案会使正常情况下的读取操作成本大幅增加。我们通过将重建读取限制在总读取量的 10% 以内来预防重建风暴。这个重建比例通常足以应对生产集群中的磁盘、主机和机架故障。虽然需要付出一定的调优复杂度作为代价，但我们避免了磁盘资源的过度配置。</p><p><strong>高效访问数据中心内及跨数据中心的数据</strong>。Tectonic 允许客户端直接访问存储节点；另一种替代方案可能使用前端代理来中介所有客户端对存储的访问。虽然向客户端开放客户端库会引入复杂性（因为库中的缺陷会转化为应用程序二进制文件中的缺陷），但直接客户端访问存储节点的网络和硬件资源效率远高于代理设计，可避免每秒数 TB 数据产生的额外网络跳数。</p><p>遗憾的是，直接访问存储节点的模式难以适配远程请求（即客户端与 Tectonic 集群地理距离较远的情况）。额外的网络开销会使协调往返通信的效率变得极其低下。为解决这个问题，Tectonic 对远程数据访问采用与本地数据访问不同的处理方式：远程请求会被转发至与存储节点处于同一数据中心的无状态代理。</p><h3 id="6-5-权衡与妥协"><a href="#6-5-权衡与妥协" class="headerlink" title="6.5 权衡与妥协"></a>6.5 权衡与妥协</h3><p>迁移至 Tectonic 并非没有代价和妥协。本小节将阐述 Tectonic 在灵活性或性能上不如 Facebook 原有基础设施的若干领域，并分析采用哈希分区元数据存储带来的影响。</p><p><strong>元数据延迟增加的影响。</strong> 迁移至 Tectonic 导致数据仓库应用程序面临更高的元数据延迟。HDFS 的元数据操作基于内存且单个节点存储整个名称空间的元数据，而 Tectonic 将元数据存储在分片式键值存储实例中，并采用分层元数据架构（§3.3）。这意味着 Tectonic 的元数据操作可能需要一次或多次网络调用（例如文件打开操作需与名称层和文件层交互）。鉴于元数据延迟的增加，数据仓库必须调整其对特定元数据操作的处理方式。例如，计算引擎在完成计算后需要按顺序逐个重命名文件集：在 HDFS 中每个重命名操作都很快，但在 Tectonic 中，计算引擎通过并行化此步骤来抵消单个重命名操作产生的额外延迟。</p><p><strong>哈希分区元数据的应对方案。</strong> 由于 Tectonic 目录采用哈希分片机制，递归列出目录需要查询多个分片。实际上，Tectonic 不提供递归列表 API，租户需通过在客户端封装多次独立列表调用来实现该功能。因此与 HDFS 不同，Tectonic 无法提供 <code>du</code>（目录空间使用量查询）功能来获取目录的聚合空间使用情况。作为替代方案，Tectonic 会定期聚合每个目录的使用统计信息，这些数据可能存在滞后性。</p><h3 id="6-6-设计与部署经验"><a href="#6-6-设计与部署经验" class="headerlink" title="6.6 设计与部署经验"></a>6.6 设计与部署经验</h3><p><strong>实现高可扩展性是一个通过微服务架构实现的迭代过程</strong>。为满足日益增长的可扩展性需求，Tectonic 的多个组件经历了多次迭代升级。例如，数据块存储的最初版本采用块分组机制来减少元数据：将具有相同冗余方案的多个块分组，通过 RS 编码作为一个单元存储它们的数据块，每个块分组映射到一组存储节点。尽管这是显著减少元数据的常用技术 [37,53]，但对于我们的生产环境而言过于僵化——当仅 5% 的存储节点不可用时，80% 的块分组会变得不可写入。该设计还阻碍了诸如对冲仲裁组写入和仲裁组追加等优化措施的实施（§5）。</p><p>此外，我们最初的元数据存储架构未分离名称层与文件层，客户端需要访问相同的分片来执行目录查找和文件块列表操作。这种设计因元数据热点导致服务不可用，促使我们进一步实施元数据分层。</p><p>Tectonic 的演进历程表明，尝试新设计对于逼近性能目标具有重要意义。我们的开发经验也证明了基于微服务的架构对实验的价值：我们可以对组件进行透明化迭代，而不会影响系统其他部分。</p><p><strong>内存数据损坏在大规模系统中极为常见</strong>。以 Tectonic 的规模而言，每日有数千台机器读写海量数据，内存数据损坏已成为常规现象——其他大型系统也观测到此类现象 [12, 27]。我们通过强制实施进程内部及跨进程的校验和（checksum）检查来解决该问题。</p><p>对于数据 D 及其校验和 C<sub>D</sub>，若需执行内存转换 F 使得 D′ &#x3D; F(D)，则需为 D ′生成校验和 C<sub>D′</sub>。验证 D′ 时，必须通过 F 的逆向函数 G 将 D′ 转换回 D，并比对 C<sub>G(D′)</sub> 与 C<sub>D</sub>。虽然逆向函数 G 的计算成本可能很高（例如 RS 编码或加密的逆向运算），但为保障数据完整性，Tectonic 仍选择承担此开销。</p><p>所有涉及数据移动、复制或转换的 API 边界都必须进行改造以包含校验和信息。客户端写入时需随数据一并向客户端库传递校验和，且 Tectonic 不仅需要在跨进程边界（如客户端库与存储节点之间）传递校验和，还需在进程内部（如数据转换后）保持校验和传递。通过验证转换过程的完整性，可有效防止存储节点故障后数据损坏传播至重建的数据块。</p><h3 id="6-7-不使用-Tectonic-的服务"><a href="#6-7-不使用-Tectonic-的服务" class="headerlink" title="6.7 不使用 Tectonic 的服务"></a>6.7 不使用 Tectonic 的服务</h3><p>Facebook 内部部分服务未采用 Tectonic 进行数据存储。引导服务（例如，必须保持零依赖的软件二进制包部署系统）无法使用 Tectonic，因为该存储系统依赖于众多其他服务（例如，键值存储、配置管理系统、部署管理系统）。图存储系统 [16] 同样未采用 Tectonic，因为 Tectonic 尚未针对键值存储工作负载进行优化——这类负载往往需要 SSD 存储提供的低延迟。</p><p>更多服务选择通过主要租户（例如，Blob 存储或数据仓库）间接使用 Tectonic，这源于 Tectonic “ 关注点分离 “ 的核心设计理念。其内部采用独立软件分层架构，每层仅专注存储系统的核心职责子集（例如，存储节点只需处理数据块层面逻辑，无需感知块或文件概念）。这一理念也体现在 Tectonic 与存储基础设施的协同方式中。例如，Tectonic 专注于数据中心内部的容错机制，它不防范数据中心故障。跨地域复制是一个单独的问题，Tectonic 将其委托给其大型租户，这些租户解决该问题以为应用程序提供透明且易于使用的共享存储。租户同时需要自主处理容量管理、存储部署及多数据中心数据再平衡。对于小型应用而言，直接对接 Tectonic 所需实现的复杂功能无异于重新实现租户已经实现的功能。——因此，它们选择通过租户使用 Tectonic。</p><h2 id="7-相关工作"><a href="#7-相关工作" class="headerlink" title="7 相关工作"></a>7 相关工作</h2><p>Tectonic 借鉴了现有系统和文献中的技术，展示了如何将它们组合成一个新颖的系统，实现支持共享存储架构上多样化工作负载的艾字节级单集群。</p><p><strong>采用单一元数据节点的分布式文件系统</strong>：HDFS [15]、GFS [24] 及其他类似系统 [38, 40, 44] 受限于元数据节点性能，每个实例或集群的存储容量为数十 PB，而 Tectonic 每个集群为艾字节。</p><p><strong>通过联邦名称空间扩展容量</strong>：联邦式 HDFS [8] 和 Windows Azure 存储系统（WAS）[17] 将多个较小的存储集群（各含单一元数据节点）组合成更大的集群。例如，联邦 HDFS 集群虽共享存储节点，但仍维护多个独立的单名称节点名称空间。这类联邦系统仍存在数据集装箱管理的运维复杂性（§2），且在实例间迁移或共享数据时（如实现负载均衡或扩容），仍需跨名称空间执行资源密集型数据拷贝操作 [33, 46, 54]。</p><p><strong>基于哈希的数据定位实现元数据可扩展性</strong>：Ceph [53] 与 FDS [36] 消除了集中式元数据管理，转而采用对象 ID 哈希定位数据。但此类系统的故障处理机制存在可扩展性瓶颈。集群规模越大故障越频繁，需要频繁更新哈希到位置的映射，该映射必须传播到所有节点。Yahoo 的云对象存储系统 [41] 联邦多个 Ceph 实例以隔离故障影响。此外，由于 Ceph 缺乏受控数据迁移能力 [52]，其硬件扩容与节点下线流程异常复杂。Tectonic 采用显式数据块 - 存储节点映射机制，可实现精确受控的数据迁移。</p><p><strong>通过分离式&#x2F;分片式元数据提升可扩展性</strong>：与 Tectonic 类似，ADLS [42] 和 HopsFS [35] 通过将元数据解耦并分层存储于独立的分片化存储库来提高文件系统容量。Tectonic 采用目录哈希分区策略，而 ADLS 与 HopsFS 则将部分关联目录元数据存于相同分片，导致目录树相关区域的元数据物理共存。哈希分区策略帮助 Tectonic 避免目录树局部热点问题。ADLS 采用 WAS 的联邦架构 [17] 实现块存储，相比之下 Tectonic 的块存储采用扁平化架构。</p><p>与 Tectonic 类似，Colossus [28, 32] 同样提供集群级范围艾字节级存储服务，其客户端库直接访问存储节点。但 Colossus 采用具备全局一致性的 Spanner 数据库 [21] 存储文件系统元数据，而 Tectonic 的元数据构建于分片键值存储之上，该方案仅保障分片内的强一致性，且不支持跨分片操作。实践证明这些限制并未造成实际影响。</p><p><strong>Blob 和对象存储。</strong> 相较于分布式文件系统，Blob 及对象存储系统 [14, 18, 36, 37] 更易于扩展，因其无需维护需要保持一致性的层次化目录树或名称空间。但绝大多数数据仓库工作负载恰恰需要层次化名称空间支持。</p><p><strong>其他大规模存储系统。</strong> Lustre [1] 与 GPFS [45] 针对高吞吐量并行访问进行优化。Lustre 因元数据节点数量限制而影响扩展性；GPFS 虽然符合 POSIX 标准，但会为我们的应用场景引入不必要的元数据管理开销。HBase [9] 作为基于 HDFS 的键值存储系统，但其 HDFS 集群未与数据仓库工作负载共享。由于 AWS [2] 设计细节未公开，我们无法进行直接对比。</p><p><strong>多租户技术实现。</strong> Tectonic 的多租户技术与文件系统及租户协同设计，其目标并非实现最优公平共享。相较于文献记载的其他系统，这种设计更易于实现性能隔离。其他系统通常采用更复杂的资源管理技术来适应租户结构和资源使用策略的变化，或实现租户间最优公平资源分配 [25, 48, 49]。</p><p>需说明的是，Tectonic 系统的部分技术细节曾以 “Warm Storage” 为名在技术讲座中披露 [39, 47]。</p><h2 id="8-结论"><a href="#8-结论" class="headerlink" title="8 结论"></a>8 结论</h2><p>本文介绍了 Facebook 分布式文件系统 Tectonic。单一 Tectonic 实例即可支撑 Facebook 数据中心内所有主要存储租户，显著提升资源利用率并降低运维复杂度。其哈希分片式解耦元数据架构与扁平化数据块存储设计，支持艾字节级数据寻址与存储。通过基数降低的资源管理机制，系统既能高效公平地分配资源，又可利用富余资源实现高利用率。依托客户端驱动的租户定制优化，Tectonic 在性能表现上达到甚至超越了此前专用存储系统。</p><p><strong>致谢。</strong> 诚挚感谢指导委员会成员 Peter Macko 以及 FAST 项目委员会的匿名评审专家，他们详尽的指导意见令本研究获益良多。同时感谢 Nar Ganapathy、Mihir Gorecha、Morteza Ghandehari、Bertan Ari、John Doty 等 Facebook 同事对本项目的贡献。感谢 Jason Flinn 与 Qi Huang 对论文改进提出的建议。Theano Stavrinos 在普林斯顿大学期间曾获美国国家科学基金资助 CNS-1910390 的支持。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>[1] Lustre Wiki. <a href="https://wiki.lustre.org/images/6/64/LustreArchitecture-v4.pdf">https://wiki.lustre.org/images/6/64/LustreArchitecture-v4.pdf</a>, 2017.<br>[2] AWS Documentation. <a href="https://docs.aws.amazon.com/">https://docs.aws.amazon.com/</a>, 2020.<br>[3] Presto. <a href="https://prestodb.io/">https://prestodb.io/</a>, 2020.<br>[4] Aditya Kalro. Facebook’s FBLearner Platform with Aditya Kalro. <a href="https://twimlai.com/twiml-talk-197-facebooks-fblearner-platform-with-aditya-kalro/">https://twimlai.com/twiml-talk-197-facebooks-fblearner-platform-with-aditya-kalro/</a>, 2018.<br>[5] J. Adrian. Introducing Bryce Canyon: Our next-generation storage platform. <a href="https://tinyurl.com/yccx2x7v">https://tinyurl.com/yccx2x7v</a>, 2017.<br>[6] M. Annamalai. ZippyDB - A Distributed key value store. <a href="https://www.youtube.com/embed/ZRP7z0HnClc">https://www.youtube.com/embed/ZRP7z0HnClc</a>, 2015.<br>[7] Apache Software Foundation. HDFS Erasure Coding. <a href="https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs/HDFSErasureCoding.html">https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs/HDFSErasureCoding.html</a>, 2018.<br>[8] Apache Software Foundation. HDFS Federation. <a href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/Federation.html">https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/Federation.html</a>, 2019.<br>[9] Apache Software Foundation. Apache HBase. <a href="https://hbase.apache.org/">https://hbase.apache.org/</a>, 2020.<br>[10] Apache Software Foundation. Apache Spark. <a href="https://spark.apache.org/">https://spark.apache.org/</a>, 2020.<br>[11] D. Beaver, S. Kumar, H. C. Li, J. Sobel, and P. Vajgel. Finding a Needle in Haystack: Facebook’s Photo Storage. In Proceedings of the 9th USENIX Symposium on Operating Systems Design and Implementation (OSDI’10), Vancouver, BC, Canada, 2010. USENIX Association.<br>[12] D. Behrens, M. Serafini, F. P. Junqueira, S. Arnautov, and C. Fetzer. Scalable error isolation for distributed systems. In Proceedings of the 12th USENIX Symposium on Networked Systems Design and Implementation (NSDI’15), Oakland, CA, USA, 2015. USENIX Association.<br>[13] B. Berg, D. S. Berger, S. McAllister, I. Grosof, J. Gunasekar, Sathya Lu, M. Uhlar, J. Carrig, N. Beckmann, M. Harchol-Balter, and G. R. Ganger. The CacheLib Caching Engine: Design and Experiences at Scale. In 14th USENIX Symposium on Operating Systems Design and Implementation (OSDI’20), Online, 2020. USENIX Association.<br>[14] A. Bigian. Blobstore: Twitter’s in-house photo storage system. <a href="https://blog.twitter.com/engineering/en_us/a/2012/blobstore-twitter-s-in-house-photo-storage-system.html">https://blog.twitter.com/engineering/en_us/a/2012/blobstore-twitter-s-in-house-photo-storage-system.html</a>, 2012.<br>[15] D. Borthakur. HDFS Architecture Guide. <a href="https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html">https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html</a>, 2019.<br>[16] N. Bronson, Z. Amsden, G. Cabrera, P. Chakka, P. Dimov, H. Ding, J. Ferris, A. Giardullo, S. Kulkarni, H. Li, M. Marchukov, D. Petrov, L. Puzar, Y. J. Song, and V. Venkataramani. TAO: Facebook’s Distributed Data Store for the Social Graph. In Proceedings of the 2013 USENIX Annual Technical Conference. USENIX, 2013.<br>[17] B. Calder, J. Wang, A. Ogus, N. Nilakantan, A. Skjolsvold, S. McKelvie, Y. Xu, S. Srivastav, J. Wu, H. Simitci, J. Haridas, C. Uddaraju, H. Khatri, A. Edwards, V. Bedekar, S. Mainali, R. Abbasi, A. Agarwal, M. F. u. Haq, M. I. u. Haq, D. Bhardwaj, S. Dayanand, A. Adusumilli, M. McNett, S. Sankaran, K. Manivannan, and L. Rigas. Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency. In Proceedings of the 23rd ACM Symposium on Operating Systems Principles (SOSP’11), Cascais, Portugal, 2011. Association for Computing Machinery (ACM).<br>[18] J. Chen, C. Douglas, M. Mutsuzaki, P. Quaid, R. Ramakrishnan, S. Rao, and R. Sears. Walnut: a unified cloud object store. 2012.<br>[19] P. M. Chen, E. K. Lee, G. A. Gibson, R. H. Katz, and D. A. Patterson. RAID: High-performance, reliable secondary storage. ACM Computing Surveys (CSUR), 26(2):145–185, 1994.<br>[20] A. Cidon, S. Rumble, R. Stutsman, S. Katti, J. Ousterhout, and M. Rosenblum. Copysets: Reducing the Frequency of Data Loss in Cloud Storage. In Proceedings of the 2013 USENIX Annual Technical Conference (USENIX ATC’13), San Jose, CA, USA, 2013. USENIX Association.<br>[21] J. C. Corbett, J. Dean, M. Epstein, A. Fikes, C. Frost, J. J. Furman, S. Ghemawat, A. Gubarev, C. Heiser, P. Hochschild, W. Hsieh, S. Kanthak, E. Kogan, H. Li, A. Lloyd, S. Melnik, D. Mwaura, D. Nagle, S. Quinlan, R. Rao, L. Rolig, Y. Saito, M. Szymaniak, C. Taylor, R. Wang, and D. Woodford. Spanner: Google’s globally distributed database. ACM Trans. Comput. Syst., 31(3), Aug. 2013. ISSN 0734-2071. doi: 10.1145&#x2F;2491245. URL <a href="https://doi.org/10.1145/2491245">https://doi.org/10.1145/2491245</a>.<br>[22] J. Dean and L. A. Barroso. The tail at scale. Commun. ACM, 56(2):74–80, Feb. 2013. ISSN 0001-0782. doi: 10.1145&#x2F;2408776.2408794. URL <a href="http://doi.acm.org/10.1145/2408776.2408794">http://doi.acm.org/10.1145/2408776.2408794</a>.<br>[23] Facebook Open Source. RocksDB. <a href="https://rocksdb.org/">https://rocksdb.org/</a>, 2020.<br>[24] S. Ghemawat, H. Gobioff, and S.-T. Leung. The Google File System. In Proceedings of the 19th ACM Symposium on Operating Systems Principles (SOSP’03), Bolton Landing, NY, USA, 2003. Association for Computing Machinery (ACM).<br>[25] R. Gracia-Tinedo, J. Sampé, E. Zamora, M. Sánchez-Artigas, P. García-López, Y. Moatti, and E. Rom. Crystal: Software-defined storage for multi-tenant object stores. In Proceedings of the 15th USENIX Conference on File and Storage Technologies (FAST’17), Santa Clara, CA, USA, 2017. USENIX Association.<br>[26] X. F. Group. The XFS Linux wiki. <a href="https://xfs.wiki.kernel.org/">https://xfs.wiki.kernel.org/</a>, 2018.<br>[27] A. Gupta, F. Yang, J. Govig, A. Kirsch, K. Chan, K. Lai, S. Wu, S. Dhoot, A. Kumar, A. Agiwal, S. Bhansali, M. Hong, J. Cameron, M. Siddiqi, D. Jones, J. Shute, A. Gubarev, S. Venkataraman, and D. Agrawal. Mesa: Geo-replicated, near real-time, scalable data warehousing. In Proceedings of the 40th International Conference on Very Large Data Bases (VLDB’14), Hangzhou, China, 2014. VLDB Endowment.<br>[28] D. Hildebrand and D. Serenyi. A peek behind the VM at the Google Storage infrastructure. <a href="https://www.youtube.com/watch?v=q4WC_6SzBz4">https://www.youtube.com/watch?v=q4WC_6SzBz4</a>, 2020.<br>[29] Q. Huang, P. Ang, P. Knowles, T. Nykiel, I. Tverdokhlib, A. Yajurvedi, P. Dapolito IV, X. Yan, M. Bykov, C. Liang, M. Talwar, A. Mathur, S. Kulkarni, M. Burke, and W. Lloyd. SVE: Distributed video processing at Facebook scale. In Proceedings of the 26th ACM Symposium on Operating Systems Principles (SOSP’17), Shanghai, China, 2017. Association for Computing Machinery (ACM).<br>[30] L. Leslie. The part-time parliament. ACM Transactions on Computer Systems, 16(2):133–169, 1998.<br>[31] K. Lewi, C. Rain, S. A. Weis, Y. Lee, H. Xiong, and B. Yang. Scaling backend authentication at facebook. IACR Cryptol. ePrint Arch., 2018:413, 2018. URL <a href="https://eprint.iacr.org/2018/413">https://eprint.iacr.org/2018/413</a>.<br>[32] M. K. McKusick and S. Quinlan. GFS: Evolution on Fast-forward. Queue, 7(7):10:10–10:20, Aug. 2009. ISSN 1542-7730. doi: 10.1145&#x2F;1594204.1594206. URL <a href="http://doi.acm.org/10.1145/1594204.1594206">http://doi.acm.org/10.1145/1594204.1594206</a>.<br>[33] P. A. Misra, I. n. Goiri, J. Kace, and R. Bianchini. Scaling Distributed File Systems in Resource-Harvesting Datacenters. In Proceedings of the 2017 USENIX Annual Technical Conference (USENIX ATC’17), Santa Clara, CA, USA, 2017. USENIX Association.<br>[34] S. Muralidhar, W. Lloyd, S. Roy, C. Hill, E. Lin, W. Liu, S. Pan, S. Shankar, V. Sivakumar, L. Tang, and S. Kumar. f4: Facebook’s Warm BLOB Storage System. In Proceedings of the 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI’14), Broomfield, CO, USA, 2014. USENIX Association.<br>[35] S. Niazi, M. Ismail, S. Haridi, J. Dowling, S. Grohsschmiedt, and M. Ronström. HopsFS: Scaling hierarchical file system metadata using NewSQL databases. In Proceedings of the 15th USENIX Conference on File and Storage Technologies (FAST’17), Santa Clara, CA, USA, 2017. USENIX Association.<br>[36] E. B. Nightingale, J. Elson, J. Fan, O. Hofmann, J. Howell, and Y. Suzue. Flat Datacenter Storage. In Proceedings of the 10th USENIX Symposium on Operating Systems Design and Implementation (OSDI’12), Hollywood, CA, USA, 2012. USENIX Association.<br>[37] S. A. Noghabi, S. Subramanian, P. Narayanan, S. Narayanan, G. Holla, M. Zadeh, T. Li, I. Gupta, and R. H. Campbell. Ambry: Linkedin’s scalable geo-distributed object store. In Proceedings of the 2016 International Conference on Management of Data (SIGMOD’16), San Francisco, California, USA, 2016. Association for Computing Machinery (ACM).<br>[38] M. Ovsiannikov, S. Rus, D. Reeves, P. Sutter, S. Rao, and J. Kelly. The Quantcast File System. In Proceedings of the 39th International Conference on Very Large Data Bases (VLDB’13), Riva del Garda, Italy, 2013. VLDB Endowment.<br>[39] K. Patiejunas and A. Jaiswal. Facebook’s disaggregated storage and compute for Map&#x2F;Reduce. <a href="https://atscaleconference.com/videos/facebooks-disaggregated-storage-and-compute-for-mapreduce/">https://atscaleconference.com/videos/facebooks-disaggregated-storage-and-compute-for-mapreduce/</a>, 2016.<br>[40] A. J. Peters and L. Janyst. Exabyte scale storage at CERN. Journal of Physics: Conference Series, 331(5):052015, dec 2011. doi: 10.1088&#x2F;1742-6596&#x2F;331&#x2F;5&#x2F;052015. URL <a href="https://doi.org/10.1088/1742-6596/331/5/052015">https://doi.org/10.1088/1742-6596/331/5/052015</a>.<br>[41] N. P.P.S, S. Samal, and S. Nanniyur. Yahoo Cloud Object Store - Object Storage at Exabyte Scale. <a href="https://yahooeng.tumblr.com/post/116391291701/yahoo-cloud-object-store-object-storage-at">https://yahooeng.tumblr.com/post/116391291701/yahoo-cloud-object-store-object-storage-at</a>, 2015.<br>[42] R. Ramakrishnan, B. Sridharan, J. R. Douceur, P. Kasturi, B. Krishnamachari-Sampath, K. Krishnamoorthy, P. Li, M. Manu, S. Michaylov, R. Ramos, N. Sharman, Z. Xu, Y. Barakat, C. Douglas, R. Draves, S. S. Naidu, S. Shastry, A. Sikaria, S. Sun, and R. Venkatesan. Azure Data Lake Store: a hyperscale distributed file service for big data analytics. In Proceedings of the 2017 International Conference on Management of Data (SIGMOD’17), Chicago, IL, USA, 2017. Association for Computing Machinery (ACM).<br>[43] I. S. Reed and G. Solomon. Polynomial codes over certain finite fields. Journal of the Society for Industrial and Applied Mathematics, 8(2):300–304, 1960.<br>[44] Rousseau, Hervé, Chan Kwok Cheong, Belinda, Contescu, Cristian, Espinal Curull, Xavier, Iven, Jan, Gonzalez Labrador, Hugo, Lamanna, Massimo, Lo Presti, Giuseppe, Mascetti, Luca, Moscicki, Jakub, and van der Ster, Dan. Providing large-scale disk storage at cern. EPJ Web Conf., 214:04033, 2019. doi: 10.1051&#x2F;epjconf&#x2F;201921404033. URL <a href="https://doi.org/10.1051/epjconf/201921404033">https://doi.org/10.1051/epjconf/201921404033</a>.<br>[45] F. Schmuck and R. Haskin. GPFS: A Shared-Disk File System for Large Computing Clusters. In Proceedings of the 1st USENIX Conference on File and Storage Technologies (FAST’02), Monterey, CA, USA, 2002. USENIX Association.<br>[46] R. Shah. Enabling HDFS Federation Having 1B File System Objects. <a href="https://tech.ebayinc.com/engineering/enabling-hdfs-federation-having-1b-file-system-objects/">https://tech.ebayinc.com/engineering/enabling-hdfs-federation-having-1b-file-system-objects/</a>, 2020.<br>[47] S. Shamasunder. Hybrid XFS—Using SSDs to Supercharge HDDs at Facebook. <a href="https://www.usenix.org/conference/srecon19asia/presentation/shamasunder">https://www.usenix.org/conference/srecon19asia/presentation/shamasunder</a>, 2019.<br>[48] D. Shue, M. J. Freedman, and A. Shaikh. Performance isolation and fairness for multi-tenant cloud storage. In Proceedings of the 10th USENIX Symposium on Operating Systems Design and Implementation (OSDI’12), Hollywood, CA, USA, 2012. USENIX Association.<br>[49] A. K. Singh, X. Cui, B. Cassell, B. Wong, and K. Daudjee. Microfuge: A middleware approach to providing performance isolation in cloud storage systems. In Proceedings of the 34th IEEE International Conference on Distributed Computing Systems (ICDCS’14), Madrid, Spain, 2014. IEEE Computer Society.<br>[50] A. Thusoo, Z. Shao, S. Anthony, D. Borthakur, N. Jain, J. Sarma, R. Murthy, and H. Liu. Data warehousing and analytics infrastructure at facebook. In Proceedings of the 2010 ACM SIGMOD International Conference on Management of Data (SIGMOD’10), Indianapolis, IN, USA, 2010. Association for Computing Machinery (ACM).<br>[51] A. Wang. Introduction to HDFS Erasure Coding in Apache Hadoop. <a href="https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/">https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/</a>, 2015.<br>[52] L. Wang, Y. Zhang, J. Xu, and G. Xue. MAPX: Controlled Data Migration in the Expansion of Decentralized Object-Based Storage Systems. In Proceedings of the 18th USENIX Conference on File and Storage Technologies (FAST’20), Santa Clara, CA, USA, 2020. USENIX Association.<br>[53] S. A. Weil, S. A. Brandt, E. L. Miller, D. D. Long, and C. Maltzahn. Ceph: A scalable, high-performance distributed file system. In Proceedings of the 7th USENIX Symposium on Operating Systems Design and Implementation (OSDI’06), Seattle, WA, USA, 2006. USENIX Association.<br>[54] A. Zhang and W. Yan. Scaling Uber’s Apache Hadoop Distributed File System for Growth. <a href="https://eng.uber.com/scaling-hdfs/">https://eng.uber.com/scaling-hdfs/</a>, 2018. </p><blockquote><p>原文链接：<a href="https://www.usenix.org/system/files/fast21-pan.pdf">Facebook’s Tectonic Filesystem: Efficiency from Exascale</a></p><p>本文为中文翻译，仅用于学习与分享，版权归原作者所有。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/08-26-2025/facebook-tectonic-filesystem.html">https://www.cyningsun.com/08-26-2025/facebook-tectonic-filesystem.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;Satadru Pan¹, Theano Stavrinos¹,², Yunqiao Zhang¹, Atul Sikaria¹, Pavel Zakharov¹, Abhinav Sharma¹, Shiva Shankar P¹, Mike Shuey¹</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="Filesystem" scheme="https://www.cyningsun.com/tag/Filesystem/"/>
    
  </entry>
  
  <entry>
    <title>译｜Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications: The RocksDB Experience</title>
    <link href="https://www.cyningsun.com/08-03-2025/the-rocksdb-experience.html"/>
    <id>https://www.cyningsun.com/08-03-2025/the-rocksdb-experience.html</id>
    <published>2025-08-02T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<p>Siying Dong, Andrew Kryczka, Yanqin Jin and Michael Stumm</p><p>Facebook Inc., 1 Hacker Way, Menlo Park, CA, U.S.A<br>University of Toronto, Toronto, Canada</p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>RocksDB 是一个面向大规模分布式系统、针对固态硬盘（SSD）优化的键值存储系统。本文描述了过去八年 RocksDB 开发优先级的演变过程。这种演变既是硬件趋势的结果，也是在多个组织中大规模生产环境运行 RocksDB 的丰富经验的结果。文中阐述了 RocksDB 的资源优化目标如何以及为何从写放大转向空间放大，再转向 CPU 利用率。大规模应用的实践经验表明，资源分配需要跨多个 RocksDB 实例进行统筹管理，数据格式需要保持向后和向前兼容，以支持渐进式软件部署，同时需要完善的数据库复制与备份机制支持。在故障处理方面获得的教训是：必须在系统各层尽早检测数据损坏错误。</p><h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1 引言"></a>1 引言</h2><p>RocksDB [19, 54] 是 Facebook 于 2012 年基于 Google 的 LevelDB 代码库 [22] 创建的高性能持久化键值存储引擎。它针对固态硬盘（SSD）的特性进行了优化，面向大规模（分布式）应用，并被设计为可嵌入到更高级应用中的库组件。因此，每个 RocksDB 实例只管理单个服务器节点存储设备上的数据；它不处理任何跨主机的操作，如复制和负载均衡，也不执行高级操作，例如检查点——而是将这些操作的实现留给应用程序，RocksDB 只提供相应的支持以便应用高效实现这些功能。</p><p>RocksDB 及其各种组件高度可定制，允许存储引擎适应广泛的需求和工作负载；可定制项包括预写日志（WAL）处理、压缩策略和压实策略（一种移除无效数据并优化 LSM 树的过程，如 §2 所述）。RocksDB 可以调整为高写入吞吐量或高读取吞吐量、追求空间效率，或介于两者之间的目标。由于其可配置性，RocksDB 被许多应用采用，涵盖了广泛的使用场景。仅在 Facebook，就有 30 多个不同的应用使用 RocksDB，总共存储了数百 PB 的生产数据。除了作为<strong>数据库</strong>（如 MySQL [37]、Rocksandra [6]、CockroachDB [64]、MongoDB [40] 和 TiDB [27]）的存储引擎外，RocksDB 还用于以下类型、特性迥异的服务（见表 1 总结）：</p><ul><li><strong>流处理</strong>：RocksDB 用于 Apache Flink [12]、Kafka Stream [31]、Samza [43] 和 Facebook 的 Stylus [15] 中存储中间数据。</li><li><strong>日志&#x2F;队列服务</strong>：RocksDB 被 Facebook 的 LogDevice [5]（同时支持 SSD 和 HDD）、Uber 的 Cherami [8] 以及 Iron.io [29] 使用。</li><li><strong>索引服务</strong>：RocksDB 被 Facebook 的 Dragon [59] 和 Rockset [58] 使用。</li><li><strong>SSD 缓存</strong>：内存缓存服务，如 Netflix 的 EVCache [7]、奇虎的 Pika [51] 和 Redis [46] 等，使用 RocksDB 将 DRAM 淘汰的数据存储到 SSD 上。</li></ul><table><thead><tr><th>用例</th><th>读&#x2F;写模式</th><th>读取类型</th><th>特殊特性</th></tr></thead><tbody><tr><td>数据库</td><td>混合型</td><td>Get + Iterator</td><td>事务与备份</td></tr><tr><td>流处理</td><td>写密集型</td><td>Get 或 Iterator</td><td>时间窗口与检查点</td></tr><tr><td>日志&#x2F;队列</td><td>写密集型</td><td>Iterator</td><td>也支持 HDD</td></tr><tr><td>索引服务</td><td>读密集型</td><td>Iterator</td><td>批量加载</td></tr><tr><td>缓存</td><td>写密集型</td><td>Get</td><td>可丢弃数据</td></tr></tbody></table><p>表 1：RocksDB 用例及其工作负载特性</p><p>此前的一篇论文分析了若干使用 RocksDB 的数据库应用 [11]。表 2 总结了从生产工作负载获取的一些关键系统指标。</p><table><thead><tr><th></th><th>CPU</th><th>空间利用率</th><th>闪存耐久度</th><th>读带宽</th></tr></thead><tbody><tr><td>流处理</td><td>11%</td><td>48%</td><td>16%</td><td>1.6%</td></tr><tr><td>日志&#x2F;队列</td><td>46%</td><td>45%</td><td>7%</td><td>1.0%</td></tr><tr><td>索引服务</td><td>47%</td><td>61%</td><td>5%</td><td>10.0%</td></tr><tr><td>缓存</td><td>3%</td><td>78%</td><td>74%</td><td>3.5%</td></tr></tbody></table><p>表 2：各应用类别中典型用例的系统指标</p><p>拥有一个能够支持多种不同用例的存储引擎的优势在于，可以在不同应用中复用同一个存储引擎。确实，让每个应用构建自己的存储子系统是有问题的，因为这样做具有挑战性。即便是最简单的应用，也需要使用校验和防止介质损坏，保证崩溃后的数据一致性，按正确顺序发起系统调用以确保写入持久性，并以正确的方式处理文件系统返回的错误。一个成熟通用的存储引擎可以在在所有这些领域提供完善的解决方案。</p><p>当客户端应用在统一的基础设施中运行时，通用的存储引擎还能带来额外好处：监控框架、性能分析工具和调试工具都可以共享。例如，公司内不同应用的负责人可以利用相同的内部框架，将统计数据上报到同一个仪表板，使用相同的工具监控系统，并使用相同的嵌入式管理服务管理 RocksDB。这种整合不仅便于专业知识在不同团队间复用，还能将信息汇聚到统一门户，并促进开发工具来管理它们。</p><p>鉴于采用 RocksDB 的应用类型多样，其开发优先级自然会发生演变。本文描述了我们的优先级在过去八年中是如何演变的，因为我们从实际应用（包括 Facebook 内部和其他组织）中汲取了实践经验，并观察到了硬件趋势的变化，促使我们重新审视一些早期的假设。我们还描述了 RocksDB 在近期未来的开发优先级。</p><p>§2 提供了 SSD 和日志结构合并（LSM）树 [45] 的背景知识。从一开始，RocksDB 就选择 LSM 树作为其主要数据结构，以解决闪存读写性能不对称和耐久度有限的问题。我们认为 LSM 树非常适合 RocksDB，并且即使面对未来硬件趋势（见 §3），也依然适用。LSM 树数据结构是 RocksDB 能够适应具有不同需求的各类应用的原因之一。</p><p>§3 描述了主要优化目标如何从最小化写放大转向最小化空间放大，以及从优化性能转变为优化效率。</p><p>§4 总结了在服务大规模分布式系统时的经验教训。例如：（i）由于单个服务器可能托管多个实例，因此必须跨多个 RocksDB 实例管理资源分配；（ii）由于 RocksDB 软件更新是增量部署&#x2F;回滚的，因此数据格式必须保持向后和向前兼容；（iii）数据库复制与备份的完善支持至关重要。</p><p>§5 讲述了我们在故障处理方面的经验。大规模分布式系统通常使用复制实现容错和高可用性。然而，必须正确处理单节点故障才能实现该目标。我们发现，简单地识别和传播文件系统及校验和错误是不够的。相反，每一层的故障（例如，比特翻转）都应尽早识别，并且应用应该能够尽可能以自动化方式指定处理它们的策略。</p><p>§6 提出了我们对改进键值接口的看法。虽然核心接口因其灵活性而简单且强大，但它局限了一些关键用例的性能。我们描述了对独立于键和值的用户定义时间戳的支持。</p><p>§8 列出了几个 RocksDB 将受益于未来研究的领域。</p><h2 id="2-背景"><a href="#2-背景" class="headerlink" title="2 背景"></a>2 背景</h2><p>闪存的特性深刻影响了 RocksDB 的设计。读写性能的不对称和有限的耐久度，在数据结构和系统架构设计上带来了挑战和机遇。因此，RocksDB 采用了闪存友好型的数据结构，并为现代硬件进行了优化。</p><h3 id="2-1-基于闪存-SSD-的嵌入式存储"><a href="#2-1-基于闪存-SSD-的嵌入式存储" class="headerlink" title="2.1 基于闪存 SSD 的嵌入式存储"></a>2.1 基于闪存 SSD 的嵌入式存储</h3><p>在过去十年里，基于闪存的 SSD 在服务在线数据服务方面的普及。这种低延迟、高吞吐量的设备不仅挑战了软件充分发挥其全部能力，也改变了许多有状态服务的实现方式。SSD 可为读写操作都提供每秒数十万次输入&#x2F;输出操作 (IOPS)，比机械硬盘快数千倍。它还能支持数百 MB 的带宽。然而，由于编程&#x2F;擦除 (P&#x2F;E) 周期有限，无法持续维持高写入带宽。这些因素促使我们重新审视存储引擎的数据结构，以针对此类硬件进行优化。</p><p>在许多情况下，SSD 的高性能也将性能瓶颈从设备 I&#x2F;O 转移到了网络（包括延迟和吞吐量）。使得应用更倾向于将数据存储在本地 SSD 上，而不是使用远程数据存储服务。这促使嵌入于应用的键值存储引擎的需求显著增长。</p><p>RocksDB 的创建正是为满足这些需求。我们希望打造一个灵活的键值存储系统，服务于使用本地 SSD 驱动器的各种应用，并针对 SSD 的特性进行优化。LSM 树在实现这些目标中发挥了关键作用。</p><h3 id="2-2-RocksDB-架构"><a href="#2-2-RocksDB-架构" class="headerlink" title="2.2 RocksDB 架构"></a>2.2 RocksDB 架构</h3><p>RocksDB 采用日志结构合并（LSM）树 [45] 作为其存储数据的主要数据结构。</p><p><strong>写入。</strong> 每当数据写入 RocksDB 时，数据会被添加到名为 MemTable 的内存写缓冲区，同时写入磁盘上的预写日志（WAL）。MemTable 采用跳表实现，以保持数据有序，插入和查找的时间复杂度为 O(log n)。WAL 用于故障恢复，但不是强制性的。当 MemTable 达到配置的大小后，（i）MemTable 和 WAL 变为只读，（ii）为处理后续写入操作，系统会创建新的内存表（MemTable）和预写日志（WAL），（iii）MemTable 的内容被刷新到磁盘上的 “ 有序字符串表 “（SSTable）数据文件中，（iv）已刷新的 MemTable 及其关联的 WAL 将被丢弃。每个 SSTable 以有序方式存储数据，划分为固定大小的数据块。每个 SSTable 还有一个索引块，每个数据块对应一条索引条目，支持二分查找操作。</p><p><strong>压实 (Compaction)。</strong> LSM 树具有多层 SSTable，如图 1 所示。最新的 SSTable 由 MemTable 刷新创建，放在 Level-0。Level-0 以上的层级由一个称为压实 (compaction) 的过程创建。给定层级上的 SSTable 大小受配置参数限制。当 Level-L 的大小目标被超过时，会从 Level-L 中选择一些 SSTable，并与 Level-(L+1) 中键范围重叠的 SSTable 合并。在此过程中，删除和覆盖的数据被移除，并且表针对读取性能和空间效率进行了优化。这个过程将写入的数据从 Level-0 逐渐迁移到最后一层 (last level)。压实 I&#x2F;O 是高效的，因为它可以并行化，并且只涉及对整个文件进行批量读取和写入。</p><p>Level-0 层的 SSTable 存在键范围重叠，因为每个 SSTable 都对应一个完整的有序数据集（sorted run）。后面层级的 SSTable 各自仅包含一个有序数据集，因此这些层级的 SSTable 实际存储的是该层级有序数据集的分区片段。</p><p><strong>读取。</strong> 在读取路径中，键查找会依次在每一层进行，直到找到该键或确定键在最后一层不存在。它从搜索所有 MemTable 开始，然后是所有 Level-0 SSTable，接着是更高层级的 SSTable。在每一层，都使用二分查找。布隆过滤器 (Bloom filter) 用于避免在 SSTable 文件内不必要的查找。扫描 (Scan) 操作则需要遍历所有层级的数据。</p><p><img src="/images/the-rocksdb-experience/%E8%AF%91%EF%BD%9CEvolution%20of%20Development%20Priorities%20in%20Key-value%20Stores%20Serving%20Large-scale%20Applications%EF%BC%9AThe%20RocksDB%20Experience-20250803190534-1.png" alt="译｜Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications：The RocksDB Experience-20250803190534-1.png"></p><p>图 1：使用分层压实 (leveled compaction) 的 RocksDB LSM 树。每个白框代表一个 SSTable。</p><p>RocksDB 支持多种不同类型的压实。<strong>分层压实 (Leveled Compaction)</strong> 源自 LevelDB 并随后进行了改进 [19]。在这种压实方式下，各层被分配呈指数级增长目标的容量，如图 1 中的虚线框所示。<strong>分级压实 (Tiered Compaction)<strong>（在 RocksDB 中称为 Universal Compaction）类似于 Apache Cassandra 或 HBase 的做法。多个有序数据集会被惰性地 (lazily) 一起压实，触发时机要么是有序数据集数量过多，要么是数据库总大小与最大有数据序集大小的比值超过可配置阈值。最后，</strong>FIFO 压实 (FIFO Compaction)</strong> 则是在数据库达到大小限制后直接丢弃旧文件，仅执行轻量级压实，主要面向内存缓存类应用。</p><p>能够配置压实类型使得 RocksDB 能服务于广泛的用例。通过选择不同的压实方式，RocksDB 可被配置为读友好型、写友好型，或针对特殊缓存工作负载的极致写友好型。然而，应用所有者需要根据其特定用例权衡不同的指标 [2]。更惰性的压实算法改善了写放大和写入吞吐量，但会牺牲读取性能；而更积极的压实则会牺牲写入性能以换取更快的读取。像日志或流处理服务可以采用写密集型配置，而数据库服务则需要一个平衡的方案。表 3 通过微基准测试结果展示了这种灵活性。</p><table><thead><tr><th>压实方式</th><th>写放大</th><th>最大空间开销</th><th>平均空间开销</th><th>带布隆过滤器的每次 Get() I&#x2F;O 次数</th><th>无过滤器的每次 Get() I&#x2F;O 次数</th><th>每次迭代器 seek 的 I&#x2F;O 次数</th></tr></thead><tbody><tr><td>Leveled</td><td>16.07</td><td>9.8%</td><td>9.5%</td><td>0.99</td><td>1.7</td><td>1.84</td></tr><tr><td>Tiered</td><td>4.8</td><td>94.4%</td><td>45.5%</td><td>1.03</td><td>3.39</td><td>4.80</td></tr><tr><td>FIFO</td><td>2.14</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>1.16</td><td>528</td><td>967</td></tr></tbody></table><p>表 3：RocksDB 5.9 下三种主要压实类型的写放大、开销和读取 I&#x2F;O。分级压实 (Tiered) 的有序数据集数设为 12，FIFO 压实使用每个 key 20 位的布隆过滤器。使用直接 I&#x2F;O，块缓存大小为完全压缩后数据库大小的 10%。写放大计算为总 SSTable 文件写入量与刷新的 MemTable 字节数之比。WAL 写入不计入。 </p><h2 id="3-资源优化目标的演变"><a href="#3-资源优化目标的演变" class="headerlink" title="3 资源优化目标的演变"></a>3 资源优化目标的演变</h2><p>本节描述了我们的资源优化目标如何随着时间演变：从写放大到空间放大，再到 CPU 利用率。</p><h3 id="写放大"><a href="#写放大" class="headerlink" title="写放大"></a>写放大</h3><p>在最初开发 RocksDB 时，我们主要关注于节省闪存擦写周期，因此重点优化写放大，这也是当时业界的普遍观点（例如 [34]）。对于许多应用，尤其是写密集型工作负载（见表 1），写放大依然是一个重要问题。</p><p>写放大体现在两个层面。SSD 本身会引入写放大：据我们的观察，放大倍数在 1.1 到 3 之间。存储和数据库软件同样会产生写放大；有时甚至高达 100（例如，仅仅更改不到 100 字节时却要写出整个 4KB&#x2F;8KB&#x2F;16KB 页面）。</p><p>RocksDB 的分层压实 (Leveled Compaction) 通常表现出 10 到 30 倍的写放大，这在许多情况下比 B 树方案好几倍。例如，在 MySQL 上运行 LinkBench 时，RocksDB 每个事务发出的写入量仅为基于 B 树的存储引擎 InnoDB 的 5% [37]。然而，对于写密集型应用来说，10–30 倍的写放大仍然太高。为此，我们增加了分级压实 (Tiered Compaction)，它将写放大降至 4–10 倍，尽管读取性能较低；见表 3。图 2 描述了 RocksDB 在不同数据导入速率下的写放大。RocksDB 应用所有者通常在写入速率高时选择一种压实方法来减少写放大，而在写入速率低时进行更积极的压实，以实现空间效率和读取性能目标。</p><p><img src="/images/the-rocksdb-experience/%E8%AF%91%EF%BD%9CEvolution%20of%20Development%20Priorities%20in%20Key-value%20Stores%20Serving%20Large-scale%20Applications%EF%BC%9AThe%20RocksDB%20Experience-20250803190534-2.png" alt="译｜Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications：The RocksDB Experience-20250803190534-2.png"></p><p>图 2：42 个随机抽样的 ZippyDB 与 MyRocks 应用写放大与写入速率的对比分析。</p><h3 id="空间放大"><a href="#空间放大" class="headerlink" title="空间放大"></a>空间放大</h3><p>经过数年开发，我们发现对于大多数应用而言，空间利用率远比写放大重要，因为闪存写入周期和写入开销都不是瓶颈。实际上，实践中使用的 IOPS 数远低于 SSD 能提供的上限（但仍然高到足以让 HDD 失去吸引力，即使忽略维护开销）。因此，我们将资源优化目标转向了磁盘空间。</p><p>幸运的是，由于其无碎片的数据布局，LSM 树在优化磁盘空间时也表现良好。然而，我们看到有机会通过减少 LSM 树中无效数据（即已删除和覆盖的数据）的数量来改进分层压实 (Leveled Compaction)。我们开发了**动态分层压实 (Dynamic Leveled Compaction)**，其中树中每个层级的大小会根据最后一层的实际大小自动调整（而不是静态设置每个层级的大小）[19]。这种方法比分层压实能获得更好、更稳定的空间效率。表 4 显示了在随机写入基准测试中测量的空间效率：动态分层压实将空间开销限制在 13%，而分层压实可能增加超过 25%。此外，分层压实下最坏情况的空间开销可能高达 90%，而动态层级调整下空间开销是稳定的。事实上，对于 Facebook 的主要数据库之一 UDB，当 InnoDB 被 RocksDB 替换时，空间占用减少了 50% [36]。 </p><table><thead><tr><th></th><th><strong>动态分层压实</strong></th><th></th><th></th><th><strong>LevelDB 风格压实</strong></th><th></th><th></th></tr></thead><tbody><tr><td>键数（百万）</td><td>完全压实大小（GB）</td><td>稳态 DB 大小（GB）</td><td>空间开销（%）</td><td>完全压实大小（GB）</td><td>稳态 DB 大小（GB）</td><td>空间开销（%）</td></tr><tr><td>200</td><td>12.0</td><td>13.5</td><td>12.4</td><td>12.0</td><td>15.1</td><td>25.6</td></tr><tr><td>400</td><td>24.0</td><td>26.9</td><td>11.8</td><td>24.0</td><td>26.9</td><td>12.2</td></tr><tr><td>600</td><td>36.0</td><td>40.4</td><td>12.2</td><td>36.4</td><td>42.5</td><td>16.9</td></tr><tr><td>800</td><td>48.0</td><td>54.2</td><td>12.7</td><td>48.3</td><td>57.9</td><td>19.7</td></tr><tr><td>1,000</td><td>60.1</td><td>67.5</td><td>12.4</td><td>60.3</td><td>73.8</td><td>22.4</td></tr></tbody></table><p>表 4: 在微基准测试中测量的 RocksDB 空间效率：数据被预先填充，每次写入都是随机选择预填充键空间中的一个键。使用 RocksDB 5.9 及所有默认选项。恒定写入速率 2MB&#x2F;s。</p><h3 id="CPU-利用率"><a href="#CPU-利用率" class="headerlink" title="CPU 利用率"></a>CPU 利用率</h3><p>有时会有人担忧 SSD 变得如此之快，以至于软件无法再充分利用它们的全部潜力。换言之，随着 SSD 的普及，瓶颈已从存储设备转移到 CPU，因此需要对软件进行根本性改进。根据我们的经验，我们并不认同这种担忧，也不认为未来基于 NAND 闪存的 SSD 会出现这个问题，原因有二。首先，只有极少数应用会受到 SSD IOPS 限制；正如 §4.2 所讨论的，大多数应用受到空间的限制。</p><p>其次，我们发现任何配备高端 CPU 的服务器都有足够的计算能力来饱和一块高端 SSD。在我们的环境中，RocksDB 从未遇到过无法充分利用 SSD 性能的问题。当然，配置一个导致 CPU 成为瓶颈的系统是可能的；例如，一个 CPU 配多个 SSD 的系统。但有效的系统通常是配置均衡的，这在当今技术条件下是可以实现的。写入密集型工作负载也可能导致 CPU 成为瓶颈。对于部分场景，可以通过配置 RocksDB 使用更轻量级的压缩选项来缓解。对于其他情况，该工作负载可能根本不适合 SSD，因为它会超过允许 SSD 持续使用 2-5 年的典型闪存耐久度预算。</p><p>为验证我们的观点，我们调研了生产环境中 42 个不同的 ZippyDB [65] 和 MyRocks 部署，每个服务于不同的应用。图 3 展示了结果。大多数工作负载受限于空间。有些确实是 CPU 密集型的，但主机通常不会满载，以便为业务增长和数据中心或区域级故障留有余量（或因配置不当）。这些部署大多包含数百台主机，因此平均值能反映这些用例的资源需求，考虑到工作负载可以在这些主机之间自由（重新）平衡（§4）。 </p><p><img src="/images/the-rocksdb-experience/%E8%AF%91%EF%BD%9CEvolution%20of%20Development%20Priorities%20in%20Key-value%20Stores%20Serving%20Large-scale%20Applications%EF%BC%9AThe%20RocksDB%20Experience-20250803190534-3.png" alt="译｜Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications：The RocksDB Experience-20250803190534-3.png"></p><p>图 3：四项资源利用率。每条线代表一个不同工作负载的部署。监控周期为一个月。所有数值取该部署集群中所有主机节点的平均值。其中 CPU 利用率与读取带宽取当月峰值小时的数据，闪存耐久度与空间利用率则取整月平均值。</p><p>尽管如此，考虑到减少空间放大这个低垂的果实已经被摘取，降低 CPU 开销已成为一个重要的优化目标。降低 CPU 开销可以提升少数确实受限于 CPU 的应用的性能。更重要的是，优化 CPU 开销可以让硬件配置更具性价比——几年前，CPU 和内存的价格相对 SSD 较低，但如今 CPU 和内存价格大幅上涨，因此降低 CPU 和内存开销变得更加重要。早期降低 CPU 开销的努力包括引入前缀布隆过滤器、在索引查找前应用布隆过滤器，以及其他布隆过滤器的改进。未来仍有进一步优化空间。</p><h3 id="适应新技术"><a href="#适应新技术" class="headerlink" title="适应新技术"></a>适应新技术</h3><p>与 SSD 相关的新架构改进很容易破坏 RocksDB 的相关性。例如，开放通道 SSD (open-channel SSDs) [50,66]、多流 SSD (multi-stream SSD) [68] 和 ZNS (Zoned Namespaces) [4] 有望改善查询延迟并节省闪存擦写周期。然而，这些新技术只会让少数 RocksDB 应用受益，因为大多数应用受限于空间，而非擦写周期或延迟。此外，让 RocksDB 直接适配这些技术会挑战其统一体验。一个值得探索的方向是将这些技术的适配下放到底层文件系统，也许 RocksDB 只需提供额外的提示 (hints)。</p><p>存储内计算（in-storage computing）或许能带来显著收益，但目前尚不清楚有多少 RocksDB 应用能真正受益。我们认为 RocksDB 适配存储内计算会很有挑战，可能需要对整个软件栈的 API 进行变更才能充分利用。我们期待未来有相关最佳实现的研究。</p><p>分离式（远程）存储看起来是更有吸引力的优化目标，也是当前的优先事项。迄今为止，我们的优化都假设闪存是本地挂载的，因为我们的系统基础设施主要是这样配置的。然而，网络速度的提升让越来越多的 I&#x2F;O 可以远程完成，因此使用远程存储运行 RocksDB 的性能对于越来越多的应用变得可行。采用远程存储后，更容易同时充分利用 CPU 和 SSD 资源，因为两者可以按需独立配置（本地 SSD 很难做到）。因此，优化 RocksDB 以适配远程闪存存储已成为优先事项。我们目前正通过合并和并行化 I&#x2F;O 来应对远程 I&#x2F;O 延迟挑战。我们已改造 RocksDB 能处理瞬时故障、将 QoS 需求下传到底层系统，并报告性能分析信息，但仍有改进空间。</p><p>存储级内存（Storage Class Memory - SCM）是一项有前景的技术。我们正在研究如何最好地利用它。有几种可能值得考虑：1. 将 SCM 作为 DRAM 的扩展——这引发了关键问题：如何使用混合 DRAM 和 SCM 最优的实现关键数据结构（例如块缓存或 MemTable）以及在尝试利用所提供的持久性时会引入什么开销；2. 将 SCM 作为数据库的主存储，但我们注意到 RocksDB 的瓶颈往往是空间或 CPU，而非 I&#x2F;O；3. 将 SCM 用于 WAL，但需考量：仅作为数据写入 SSD 前的暂存区这一用途，是否值得承担 SCM 的高成本。</p><h3 id="重新审视主要数据结构"><a href="#重新审视主要数据结构" class="headerlink" title="重新审视主要数据结构"></a>重新审视主要数据结构</h3><p>我们不断重新审视 LSM 树是否依然合适的问题，但始终得出肯定结论。SSD 的价格还未降到足以改变大多数用例的空间和闪存耐久度瓶颈的程度，而用 CPU 或 DRAM 换 SSD 使用量的替代方案只适合少数应用。虽然主要结论未变，但我们经常听到用户希望写放大能比 RocksDB 现有水平更低。值得注意的是，当对象较大时，通过分离 key 和 value（如 WiscKey [35] 和 ForrestDB [1]）可以降低写放大，因此我们正在将其添加到 RocksDB 中（称为 BlobDB）。</p><h2 id="4-服务大规模系统的经验教训"><a href="#4-服务大规模系统的经验教训" class="headerlink" title="4 服务大规模系统的经验教训"></a>4 服务大规模系统的经验教训</h2><p>RocksDB 是构建各种需求各异的大规模分布式系统的基础模块。随着时间的推移，我们发现在资源管理、WAL 处理、批量文件删除、数据格式兼容性和配置管理方面需要改进。</p><h3 id="资源管理"><a href="#资源管理" class="headerlink" title="资源管理"></a>资源管理</h3><p>大规模分布式数据服务通常将数据分区成分片 (shard)，并分布到多台服务器节点上进行存储。分片的大小是有限的，因为分片是负载均衡和复制的单位，，并且为了这个目的，分片在节点之间是原子性地复制的。因此，每台服务器通常会运行数十甚至上百个分片。在我们的场景下，每个分片由一个独立的 RocksDB 实例服务，这意味着一台存储主机上会运行多个 RocksDB 实例。这些实例可以运行在单一地址空间中，也可以分别运行在各自的地址空间中。</p><p>一台主机运行多个 RocksDB 实例对资源管理提出了要求。由于实例共享主机资源，必须在全局（主机级）和局部（实例级）两层进行资源管理，以确保公平高效地利用资源。单进程模式下，全局资源限制很重要，包括（1）写缓冲区和块缓存的内存、（2）压实 I&#x2F;O 带宽、（3）压实线程数、（4）总磁盘使用量和（5）文件删除速率（见下文），这些限制还可能需要按 I&#x2F;O 设备分别设定。局部资源限制同样重要，例如确保单个实例不能占用任何资源的过多份额。RocksDB 允许应用为每类资源创建一个或多个资源控制器（C++ 对象，传递给不同的 DB 对象），也可以按实例分别设置。最后，支持实例间的优先级调度也很重要，确保关键实例优先获得资源。</p><p>另一个经验教训是：在单进程运行多个实例时，随意创建非池化线程 (unpooled threads) 可能存在问题，尤其是线程生命周期较长时。线程过多会增加 CPU 争用、上下文切换开销，使调试极其困难，并可能引发 I&#x2F;O 尖峰。如果 RocksDB 实例需要用线程执行可能会休眠或等待条件的任务，最好用线程池，这样可以方便地限制线程数量和资源消耗。</p><p>当 RocksDB 实例运行在不同进程时，全局（主机级）资源管理更具挑战性，因为每个分片只能感知局部信息。有两种策略可用。第一，每个实例都配置为保守地使用资源，而不是贪婪地占用。例如，压实时每个实例可以比 “ 正常 “ 情况下发起更少的压缩，只有压实落后时才增加。缺点是全局资源可能无法被充分利用，导致整体资源利用率不高。第二种更具操作难度的策略是让各实例间共享资源使用信息，并据此自适应调整，以期实现全局最优。RocksDB 的主机级资源管理仍有改进空间。</p><h3 id="WAL-处理"><a href="#WAL-处理" class="headerlink" title="WAL 处理"></a>WAL 处理</h3><p>传统数据库通常在每次写操作后强制写入预写日志（WAL），以确保持久性。而大规模分布式存储系统通常为了性能和可用性使用多副本实现，并提供不同的一致性保证。例如，如果同一数据在多个副本中存在，一旦某个副本损坏或不可访问，存储系统会用其他未受影响主机上的有效副本重建失效副本。对于这类系统，RocksDB 的 WAL 写入就没那么关键了。此外，分布式系统往往有自己的复制日志（如 Paxos 日志），此时 RocksDB 的 WAL 完全可以不用。</p><p>我们的经验是，提供可调的 WAL 同步行为的选项以满足不同应用的需求是很有用的。具体来说，我们引入了多种 WAL 操作模式：（i）同步 WAL 写入，（ii）缓冲 WAL 写入，以及（iii）完全不写 WAL。对于缓冲 WAL 处理，WAL 在后台以低优先级定期写入磁盘，以免影响 RocksDB 的业务延迟。</p><h3 id="限速文件删除"><a href="#限速文件删除" class="headerlink" title="限速文件删除"></a>限速文件删除</h3><p>RocksDB 通常通过文件系统与底层存储设备交互。这些文件系统是感知闪存 SSD 的，例如 XFS 的实时丢弃功能会在文件删除时向 SSD 发出 TRIM 命令 [28]。TRIM 命令被普遍认为有助于提升性能和闪存寿命 [21]，我们的生产经验也验证了这一点。但 TRIM 也可能带来性能问题。TRIM 比我们最初想象的更具破坏性：除了更新地址映射（通常在 SSD 内部内存中），SSD 固件还需将这些更改写入 FTL(译者注：Flash Translation Layer) 的日志，这反过来可能触发 SSD 内部的垃圾回收，导致大量数据移动，随之对前台 I&#x2F;O 延迟产生负面影响。为避免 TRIM 活动高峰及相关的 I&#x2F;O 延迟增加，我们引入了文件删除限速机制，防止在压实后同时删除多个文件。</p><h3 id="数据格式兼容性"><a href="#数据格式兼容性" class="headerlink" title="数据格式兼容性"></a>数据格式兼容性</h3><p>大规模分布式应用通常在多台主机上运行服务，并要求零停机。因此，软件升级是在各主机上增量式滚动部署的；如果出现问题，更新会被回滚。随着持续部署 [56] 的普及，软件升级变得频繁；RocksDB 每月发布一个新版本。为此，磁盘上的数据必须在不同软件版本间保持前后兼容非常重要。新升级（或回滚）的 RocksDB 实例必须能理解前一版本实例写入的数据。此外，RocksDB 数据文件可能需要为了构建副本或负载平衡在分布式实例之间复制，而这些实例可能运行着不同的版本。缺乏前向兼容性保证曾导致一些 RocksDB 部署的操作困难，这促使我们添加了该保证。</p><p>RocksDB 竭尽全力确保数据保持前后兼容（新特性除外）。这在技术和流程上都具有挑战性，但我们发现这些努力是值得的。为实现向后兼容，RocksDB 必须能理解之前写入磁盘的所有格式，这增加了软件和维护复杂度。为实现前向兼容，需要理解未来的数据格式，我们旨在至少保持一年的前向兼容性。这可以通过使用通用技术部分实现，例如 Protocol Buffer [63] 或 Thrift [62] 所采用的技术。对于配置文件条目，RocksDB 需要能识别新字段，并尽力猜测如何应用或何时丢弃。我们持续使用不同版本的数据测试不同版本的 RocksDB。 </p><h3 id="管理配置"><a href="#管理配置" class="headerlink" title="管理配置"></a>管理配置</h3><p>RocksDB 高度可配置，便于应用根据其工作负载进行优化。然而，我们发现配置管理是个挑战。最初，RocksDB 继承了 LevelDB 的配置参数方法，其中参数选项直接嵌入在代码中。这带来了两个问题。首先，参数选项通常与存储在磁盘上的数据相关联，导致当使用一个选项创建的数据文件无法被新配置了另一个选项的 RocksDB 实例打开时，可能产生兼容性问题。其次，代码未明确指定的配置选项会自动设置为 RocksDB 的默认值。当 RocksDB 软件更新包含对默认配置参数的更改时（例如增加内存使用量或压实并行度），应用程序有时会遇到意外的后果。</p><p>为了解决这些问题，RocksDB 首先引入了 RocksDB 实例可以使用包含配置选项的字符串参数打开数据库的能力。后来 RocksDB 引入了可选地随数据库一起存储 option 文件的支持。我们还引入了两种工具：（i）验证工具，用于验证打开数据库的选项是否与目标数据库兼容；（ii）迁移工具，用于重写数据库以使其与所需选项兼容（尽管此工具功能有限）。</p><p>RocksDB 配置管理中一个更严重的问题是配置选项数量庞大。在 RocksDB 的早期，我们做出了支持高度可定制的设计选择：我们引入了大量新参数，并支持可插拔组件，所有这些都是为了让应用程序实现其性能潜力。事实证明，这对于早期获得初始吸引力是一个成功的策略。然而，现在一个普遍的抱怨是选项太多，理解它们的影响太困难；即，指定一个“最优”配置变得非常困难。</p><p>比拥有许多需要调整的配置参数更令人望而生畏的是，最优配置不仅取决于嵌入了 RocksDB 的系统，还取决于其上应用程序产生的工作负载。例如，ZippyDB [65] ，一个内部开发的大规模分布式键值存储，其节点使用 RocksDB。ZippyDB 服务于众多不同的应用，有时单独部署，有时多租户部署。尽管在可能的情况下付出了巨大努力在所有 ZippyDB 用例中使用统一的配置，但不同用例的工作负载差异如此之大，在性能重要时，统一的配置在实践中是不可行的。表 5 显示，在我们抽样的 39 个 ZippyDB 部署中，使用了超过 25 种不同的配置。</p><table><thead><tr><th>配置领域:</th><th>压实         ｜   I&#x2F;O</th><th>压缩算法</th><th>SSTable 文件</th><th>插件功能</th></tr></thead><tbody><tr><td>配置数量:</td><td>14</td><td>4</td><td>2</td><td>7</td></tr></tbody></table><p>表 5：39 个 ZippyDB 部署中使用的不同配置数量</p><p>对于嵌入了 RocksDB 并交付给第三方的系统来说，调整配置参数也特别具有挑战性。考虑第三方在其某个应用中使用像 MySQL 或 ZippyDB 这样的数据库。第三方通常对 RocksDB 及其最佳调整方式知之甚少。而数据库所有者对调整其客户系统的意愿不高。</p><p>这些现实世界的经验教训引发了我们在配置支持策略上的改变。我们投入了大量精力来改进开箱即用性能并简化配置。当前重点是提供 _自动适应性_，同时继续支持丰富的显式配置，因为 RocksDB 仍服务于专业化应用。我们注意到，在保留显式可配置性的同时追求适应性会带来显著的代码维护开销，但我们相信拥有统一存储引擎的好处超过了代码的复杂性。</p><h3 id="复制与备份支持"><a href="#复制与备份支持" class="headerlink" title="复制与备份支持"></a>复制与备份支持</h3><p>RocksDB 是一个单节点库。使用 RocksDB 的应用程序负责在需要时实现复制和备份。每个应用程序以自己的方式实现这些功能（有正当理由），因此 RocksDB 提供适当的支持这些功能至关重要。</p><p>引导一个新副本可以通过两种方式从现有副本复制所有数据来完成。第一种方式，可以从源副本读取所有键，然后写入目标副本（_逻辑复制_）。在源端，RocksDB 通过提供最小化对并发在线查询影响的能力来支持数据扫描操作；例如，通过提供不缓存这些操作结果的选项，从而防止缓存抖动 (cache trashing)。在目标端，支持批量加载 (bulk loading)，并针对此场景进行了优化。</p><p>第二种方式，可以通过直接复制 SSTable 和其他文件（_物理复制_）来引导新副本。RocksDB 通过识别当前时间点的已有数据库文件，并防止它们被删除或更改，来辅助物理复制。支持物理复制是 RocksDB 将数据存储在底层文件系统上的一个重要原因，因为它允许每个应用程序使用自己的工具。我们认为 RocksDB 直接使用块设备接口或与 FTL 深度集成的潜在性能提升，无法超过上述好处。</p><p>对于大多数数据库和其他应用来说，备份是一个重要特性。与复制一样，应用可以选择逻辑或物理方式备份。备份与复制的一个区别在于，应用通常需要管理多个备份。虽然大多数应用程序实现自己的备份（以满足其自身需求），但如果备份需求简单，RocksDB 提供了一个备份引擎供应用程序使用。</p><p>我们认为该领域还有两个改进空间，但都需要修改键值 API，详见 §6。第一个问题涉及在不同副本上以一致顺序应用更新，这带来了性能挑战。第二个问题与串行处理的写请求性能问题有关，且副本可能出现滞后，而应用程序可能希望这些副本能更快地赶上。不同应用程序已实现多种解决方案来处理这些问题，但它们都有局限性 [20]。该挑战在于：由于 RocksDB 目前不支持用户自定义时间戳的多版本并发控制，应用程序无法进行乱序写入操作，也无法使用序列号执行快照读取。</p><h2 id="5-故障处理的经验教训"><a href="#5-故障处理的经验教训" class="headerlink" title="5 故障处理的经验教训"></a>5 故障处理的经验教训</h2><p>通过生产实践，我们在故障处理方面获得了三条主要经验。首先，需要尽早检测数据损坏，，以最小化数据不可用或丢失的风险，并精确定位错误起源。其次，完整性保护必须覆盖整个系统，防止静默损坏暴露给 RocksDB 客户端或传播到其他副本（见图 4）。第三，错误应当差异化的方式处理。</p><p><img src="/images/the-rocksdb-experience/%E8%AF%91%EF%BD%9CEvolution%20of%20Development%20Priorities%20in%20Key-value%20Stores%20Serving%20Large-scale%20Applications%EF%BC%9AThe%20RocksDB%20Experience-20250803190535-1.png" alt="译｜Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications：The RocksDB Experience-20250803190535-1.png"></p><p>图 4：四种校验和类型</p><h3 id="静默损坏的频率"><a href="#静默损坏的频率" class="headerlink" title="静默损坏的频率"></a>静默损坏的频率</h3><p>RocksDB 用户出于性能原因通常不会使用 SSD 的数据保护功能（例如 DIF&#x2F;DIX），存储介质损坏主要通过 RocksDB 的块校验和检测，这也是所有成熟数据库的常规功能，这里不再赘述。CPU&#x2F;内存损坏虽极为罕见，但确实会发生，且难以准确量化。使用 RocksDB 的应用常常会运行数据一致性检查，通过比对副本来验证完整性。这能捕获错误，但这些错误可能是 RocksDB 也可能是客户端应用（例如在复制、备份或恢复数据时）引入的。</p><p>我们发现，通过比较 MyRocks 数据库表中同时具有主索引和二级索引的表，可以估计在 RocksDB 层引入损坏的频率；任何不一致都可能是在 RocksDB 层引入的，包括 CPU 或内存损坏。根据我们的测量，对于每 100PB 的数据，大约每三个月会在 RocksDB 层引入一次损坏。更糟糕的是，在 40% 的情况下，损坏已经传播到其他副本。</p><p>数据损坏在数据传输过程中也会发生，通常是由于软件缺陷。例如，底层存储系统在处理网络故障时的一个缺陷，导致我们在某段时间内观察到大约每传输 1PB 物理数据会出现 17 次校验和不匹配。</p><h3 id="多层保护"><a href="#多层保护" class="headerlink" title="多层保护"></a>多层保护</h3><p>需要尽早检测数据损坏，以最大限度减少停机和数据丢失。大多数 RocksDB 应用会在多台主机上复制其数据；当检测到校验和不匹配时，损坏的副本会被丢弃并用正确的副本替换。然而，这只有在正确的副本仍然存在时才是一个可行的选项。</p><p>目前，RocksDB 在多个层级对文件数据进行校验和，以识别其下方各层的损坏。这些以及计划中的应用层校验和如图 4 所示。多层校验和很重要，主要是因为它有助于及早检测损坏，并保护免受不同类型的威胁。从 LevelDB 继承的块校验和 (Block checksums) 防止文件系统或以下层损坏的数据暴露给客户端。2020 年添加的文件校验和 (File checksums) 防止由底层存储系统引起的损坏传播到其他副本，并防止在网络上传输 SSTable 文件时引起的损坏。WAL 文件的 handoff 校验和则能在写入时高效地早期检测损坏。</p><p><strong>块完整性。</strong>  每个 SSTable 块或 WAL 片段都有一个附加的校验和，在数据创建时生成。与仅在文件移动时验证的文件校验和不同，由于其范围较小，此校验和在每次读取数据时都会被验证。这样做可以防止存储层损坏的数据暴露给 RocksDB 客户端。</p><p><strong>文件完整性。</strong> 文件内容在传输操作期间特别容易损坏；例如，备份或分发 SSTable 文件时。为了解决这个问题，SSTable 受到其自身校验和的保护，该校验和在表创建时生成。SSTable 的校验和记录在其元数据的 SSTable 文件条目中，并在文件被传输到任何地方时与 SSTable 文件一起验证。然而，我们注意到其他文件，如 WAL 文件，仍未受到这种方式保护。</p><p><strong>交接完整性。</strong> 一种用于及早检测写入损坏的成熟技术是，在要写入底层文件系统的数据上生成一个交接校验和 (handoff checksum)，并将其与数据一起向下传递，在较低层进行验证 [48,70]。我们希望使用这样的写入 API 来保护 WAL 写入，因为与 SSTable 不同，WAL 受益于每次追加时的增量验证。不幸的是，本地文件系统很少支持这一点——不过一些专有栈，如 Oracle ASM [49] ，确实支持。</p><p>另一方面，当在远程存储上运行时，可以更改写入 API 以接受校验和，连接到存储服务的内部 ECC (纠错码)。RocksDB 可以在现有 WAL 片段校验和上使用校验和组合技术来高效计算写入交接校验和。由于我们的存储服务执行写入时验证，我们预计将损坏检测延迟到读取时的情况（即，在写入时验证未检出问题，直到后续读取时才被发现损坏）会极其罕见。</p><h3 id="端到端保护"><a href="#端到端保护" class="headerlink" title="端到端保护"></a>端到端保护</h3><p>上述多层保护能在许多情况下防止了客户端读到损坏数据，但并不全面。迄今为止提到的所有保护的一个缺陷是，文件 I&#x2F;O 层之上的数据未受保护，如 MemTable 和块缓存 (block cache) 中的数据。在这一层损坏的数据将是无法检测的，因此最终会暴露给用户。此外，刷新 (flush) 或压实操作可能会持久化损坏的数据，使损坏永久化。</p><p><strong>键值完整性。</strong> 为了解决这个问题，我们目前正在实施每个键值对的校验和，以检测发生在文件 I&#x2F;O 层之上的损坏。这个校验和将随键&#x2F;值一起传输到它被复制的任何地方，尽管在已有替代完整性保护的文件数据中我们会省略它。</p><h3 id="分级错误处理"><a href="#分级错误处理" class="headerlink" title="分级错误处理"></a>分级错误处理</h3><p>RocksDB 遇到的大多数故障是底层存储系统返回的错误。这些错误可能源于多种问题，从严重问题（如只读文件系统）到瞬时问题（如磁盘已满或访问远程存储时的网络错误）。早期，RocksDB 对这类问题的反应，要么是简单地向客户端返回错误消息，要么是永久停止所有写入操作。</p><p>如今，我们的目标是仅在错误不可本地恢复时才中断 RocksDB 操作；例如，瞬时网络错误不应要求用户干预来重启 RocksDB 实例。为了实现这一点，我们改进了 RocksDB，使其在遇到被归类为瞬时的错误后能周期性重试恢复操作。因此，对于大部分发生的故障，用户无需手动干预 RocksDB，我们获得了运营上的收益。</p><h2 id="6-键值接口的经验教训"><a href="#6-键值接口的经验教训" class="headerlink" title="6 键值接口的经验教训"></a>6 键值接口的经验教训</h2><p>核心的键值（KV）接口出乎意料地通用。几乎所有存储工作负载都可以通过具有 KV API 的数据存储来服务；我们很少见到有应用无法用该接口实现所需功能。这或许正是 KV 存储如此流行的原因。KV 接口是通用的。键和值都是可变长度的字节数组。应用具有很大的灵活性可以决定每个键和值中包含哪些信息，并可自由选择丰富的编码方案。因此，解析和解释键值的责任完全在应用端。KV 接口的另一个好处是可移植性。从一个键值系统迁移到另一个相对容易。不过，虽然许多用例通过这个简单接口能获得最佳性能，但我们注意到它可能限制某些应用的性能。</p><p>例如，在 RocksDB 之外实现并发控制是可行的，但很难高效，尤其是需要支持两阶段提交、在提交事务前需要部分数据持久化的场景。为此我们添加了事务支持，MyRocks (MySQL+RocksDB) 使用了它。我们继续添加特性；例如，间隙&#x2F;下一键锁 和大事务支持。</p><p>在其他情况下，局限则源于键值接口本身。因此，我们已开始研究对基本键值接口的可能扩展。其中一个扩展是对用户定义时间戳的支持。</p><h3 id="版本与时间戳"><a href="#版本与时间戳" class="headerlink" title="版本与时间戳"></a>版本与时间戳</h3><p>在过去的几年里，我们已经逐渐认识到数据版本化的重要性。我们得出结论，版本信息应成为 RocksDB 的一等公民，以便更好地支持多版本并发控制（MVCC）和时间点 (point-in-time) 读取等特性。为此，RocksDB 需要能够高效访问不同版本的数据。</p><p>到目前为止，RocksDB 在内部使用 56 位序列号 (sequence numbers) 来标识不同版本的 KV 对。序列号由 RocksDB 生成，并在每次客户端写入时递增（因此，所有数据在逻辑上按序列号排序）。客户端应用程序无法影响序列号。然而，RocksDB 允许应用程序获取数据库的 _快照_，之后 RocksDB 保证在快照被应用程序显式释放之前，快照时刻存在的所有 KV 对都将持续存在。因此，多个具有相同键的 KV 对可能共存，通过它们的序列号来区分。</p><p>这种版本控制方法是不充分的，因为它不能满足许多应用的需求。要读取过去的状态，必须在之前的某个时间点已经创建了快照。RocksDB 不支持获取过去的快照，因为没有 API 可以指定一个时间点。此外，支持时间点读取效率低下。最后，每个 RocksDB 实例分配自己的序列号，并且快照只能在每个实例级获取。这使具有多个（可能被复制的）分片的应用程序的版本控制复杂化，每个分片都是一个 RocksDB 实例。总之，创建能够提供跨分片一致读取的数据版本基本上是不可能的。</p><p>应用程序可以通过在键内或值内编码时间戳来绕过这些局限。然而，在两种方式下它们都会带来性能下降。在键内编码会牺牲点查找 (point-lookups) 的性能，而在值内编码会牺牲同一键乱序写入的性能，并使旧版本键的读取复杂化。我们相信应用指定的时间戳能更好地解决这些局限，应用程序可以用全局可理解的时间戳标记其数据，并将其置于键或值之外。</p><p>我们已经增加了应用指定时间戳的基本支持，并使用 DB-Bench 评估了此方法。结果如表 6 所示。每个工作负载有两个步骤：第一步填充数据库，第二步测量性能。例如，在 “fill_seq + read_random” 中，按升序写入大量键来填充初始数据库，然后在第 2 步执行随机读取操作。相较于基线（应用程序将时间戳作为键的一部分编码，对 RocksDB 透明），应用指定的时间戳 API 可以实现 1.2 倍或更高的吞吐量提升。提升源于将时间戳视为独立于用户键的元数据，因为这样可以使用点查找而不是迭代器来获取键的最新值，并且布隆过滤器可以识别不包含该键的 SSTable。此外，SSTable 可以存储覆盖的时间戳范围在其属性中，用于排除只可能包含陈旧值的 SSTable。</p><table><thead><tr><th>工作负载</th><th>吞吐提升</th></tr></thead><tbody><tr><td>fill_seq + read_random</td><td>1.2</td></tr><tr><td>fill_seq + read_while_writing</td><td>1.9</td></tr><tr><td>fill_random + read_random</td><td>1.9</td></tr><tr><td>fill_random + read_while_writing</td><td>2.0</td></tr></tbody></table><p>表 6: 使用时间戳 API 的 DB_bench 微基准测试可见 ≥ 1.2 倍的吞吐量提升。</p><p>我们希望此功能将使开发人员更容易在其系统中实现单节点 MVCC、分布式事务或解决多主复制冲突的多版本控制。然而，更复杂的 API 使用起来不那么直观，可能容易被误用。此外，与不存储时间戳相比，数据库将消耗更多的磁盘空间，并且可移植到其他系统的能力会降低。</p><h2 id="7-相关工作"><a href="#7-相关工作" class="headerlink" title="7 相关工作"></a>7 相关工作</h2><p>我们在 RocksDB 上的工作受益于多个领域的广泛研究。</p><h3 id="存储引擎库"><a href="#存储引擎库" class="headerlink" title="存储引擎库"></a>存储引擎库</h3><p>许多存储引擎被构建为可嵌入应用的库。RocksDB 的 KV 接口比 BerkeleyDB [44]、SQLite [47] 和 Hekaton [18] 等更为原始。此外，RocksDB 与这些系统的不同之处在于，专注于现代服务器工作负载的性能，这些负载需要高吞吐量和低延迟，并且通常在高端 SSD 和多核 CPU 上运行。这与面向更通用目标或为更快存储介质构建的系统不同 [18,30]。</p><h3 id="面向-SSD-的键值存储"><a href="#面向-SSD-的键值存储" class="headerlink" title="面向 SSD 的键值存储"></a>面向 SSD 的键值存储</h3><p>多年来，人们付出了大量努力来优化键值存储，特别是针对 SSD。早在 2011 年，SILT [34] 就提出了一种在内存效率、CPU 和性能之间取得平衡的键值存储。ForestDB [45] 在日志之上使用 HB+ 树进行索引。TokuDB [32] 和其他数据库使用 FractalTree&#x2F;Bε 树。LOCS [67]、NoFTL-KV [66] 和 FlashKV [69] 针对开放通道 SSD 优化性能。虽然 RocksDB 受益于这些成果，但我们在提高性能方面的立场和策略是不同的，并且我们继续依赖 LSM 树。一些研究比较了 RocksDB 与其他数据库的性能，例如 InnoDB [41]、TokuDB [19][37] 和 WiredTiger [10]。</p><h3 id="LSM-tree-改进"><a href="#LSM-tree-改进" class="headerlink" title="LSM-tree 改进"></a>LSM-tree 改进</h3><p>许多系统也使用 LSM 树并改进了其性能。写放大通常是主要的优化目标；例如，WiscKey [35]、PebblesDB [52]、IAM-tree [25] 和 TRIAD [3]。这些系统在优化写放大方面比 RocksDB 走得更远，而 RocksDB 更注重不同指标之间的权衡。SlimDB [53] 针对空间效率优化了 LSM 树；RocksDB 也专注于删除无效数据。Monkey [17] 尝试在 DRAM 和 IOPs 之间取得平衡。bLSM [57]、VT-tree [60] 和 cLSM [24] 针对 LSM 树的整体性能进行优化。</p><h3 id="大规模存储系统"><a href="#大规模存储系统" class="headerlink" title="大规模存储系统"></a>大规模存储系统</h3><p>存在许多分布式存储系统 [13,14,16,26,38,64]。它们通常具有跨越多个进程、主机和数据中心的复杂架构。它们与 RocksDB 这种单节点存储引擎库没有直接可比性。其他系统（例如 MongoDB、MySQL [42]、Microsoft SQL Server [38]）可以使用模块化存储引擎；它们解决了与 RocksDB 面临的类似挑战，包括故障处理和时间戳支持。</p><p><strong>故障处理。</strong> 校验和经常用于检测数据损坏 [9,23,42]。我们关于需要端到端和交接校验和的论点，与经典的端到端论点 [55] 一致，并且与其他系统使用的策略相似：[61]、ZFS [71]、Linux [48] 和 [70]。我们关于早期检测损坏的论点与 [33] 类似，该文认为特定领域的检查是不够的。</p><p><strong>时间戳支持。</strong> 几个存储系统提供时间戳支持：HBase [26]、WiredTiger [39] 和 BigTable [14]；Cassandra [13] 将时间戳作为普通列支持。在这些系统中，时间戳是自 UNIX 纪元以来的毫秒数。Hekaton [18] 使用单调递增计数器来分配时间戳，这类似于 RocksDB 序列号。RocksDB 正在进行的用户时间戳工作可以与上述成果互补。我们希望带有用户定义时间戳扩展的键值 API，可以使上层系统更容易支持数据版本化相关的功能，同时在性能和效率方面保持低开销。</p><h2 id="8-未来工作与开放问题"><a href="#8-未来工作与开放问题" class="headerlink" title="8 未来工作与开放问题"></a>8 未来工作与开放问题</h2><p>除了完成上述改进，包括优化分离式存储、键值分离、多层校验和和应用指定的时间戳外，我们计划统一分层压实 (leveled) 和分级压实 (tiered)，并提升自适应性。然而，仍有若干开放问题值得进一步研究。</p><ol><li>我们如何使用 SSD&#x2F;HDD 混合存储来提高效率？</li><li>当存在大量连续的删除标记时，如何减轻对读取器的性能影响？</li><li>如何改进写入节流算法？</li><li>能否开发一种有效的方法来比较两个副本以确保它们包含相同的数据？</li><li>如何最好地利用 SCM？是否还应该使用 LSM 树？如何组织存储层次结构？</li><li>能否有一个通用的完整性 API 来处理 RocksDB 和文件系统层之间的数据交接？</li></ol><h2 id="9-结论"><a href="#9-结论" class="headerlink" title="9 结论"></a>9 结论</h2><p>RocksDB 已从一个服务于小众应用的键值存储发展到目前被众多工业级大规模分布式应用广泛采用的系统。LSM 树作为主要数据结构很好地服务了 RocksDB，因为它表现出良好的写放大和空间放大。然而，我们对性能的看法随着时间的推移而演变。虽然写放大和空间放大仍然是主要关注点，但更多的注意力已转向 CPU 和 DRAM 效率，以及远程存储。</p><p>运行大规模应用的经验教训告诉我们：需要在不同的 RocksDB 实例之间管理资源分配；数据格式需要保持后向和前向兼容以支持增量软件部署；对数据库复制和备份的适当支持是必需的；配置管理需要简单且最好是自动化的。故障处理的经验教训告诉我们：数据损坏错误需要尽早、在系统的每一层检测。键值接口因其简单性而广受欢迎，但在性能上存在一些局限。对接口进行些许修订，或可实现更优的平衡点。</p><h2 id="致谢"><a href="#致谢" class="headerlink" title="致谢"></a>致谢</h2><p>我们将 RocksDB 的成功归功于 Facebook 所有现任和前任团队成员、所有开源社区贡献者以及 RocksDB 用户。特别感谢 该项目多年的导师 Mark Callaghan，以及 RocksDB 的核心创始成员 Dhruba Borthakur。同时感谢 Jason Flinn 和 Mahesh Balakrishnan 对本文提出的宝贵意见。最后，感谢我们的指导者 Ethan Miller 和匿名审稿人提供的宝贵反馈。</p><hr><h2 id="附录-A：RocksDB-功能时间线"><a href="#附录-A：RocksDB-功能时间线" class="headerlink" title="附录 A：RocksDB 功能时间线"></a>附录 A：RocksDB 功能时间线</h2><table><thead><tr><th>年份</th><th>性能</th><th>可配置性</th><th>功能</th></tr></thead><tbody><tr><td>2012</td><td>• 多线程压实</td><td></td><td>• 压实过滤器<br>• 锁定 SSTable 防止删除</td></tr><tr><td>2013</td><td>• 分层压实<br>• 前缀布隆过滤器<br>• MemTable 布隆过滤器<br>• MemTable 刷新的独立线程池</td><td>• 可插拔 MemTable<br>• 可插拔文件格式</td><td>• 合并算子 (Merge Operator)</td></tr><tr><td>2014</td><td>• FIFO 压实<br>• 压实速率限制<br>• 缓存友好型布隆过滤器</td><td>• 字符串型配置选项<br>• 动态配置变更</td><td>• 备份引擎<br>• 多键空间（” 列族 “）支持<br>• 物理检查点</td></tr><tr><td>2015</td><td>• 动态分层压实<br>• 文件删除速率限制<br>• Level 0 和 Level 1 并行压实</td><td>• 独立配置文件<br>• 配置兼容性检查器</td><td>• SSTable 文件集成的批量加载<br>• 乐观&#x2F;悲观事务</td></tr><tr><td>2016</td><td>• 最底层不同压缩算法<br>• MemTable 并行插入</td><td>• 跨实例 MemTable 总内存上限<br>• 压实迁移工具</td><td>• DeleteRange()</td></tr><tr><td>2017</td><td>• 最底层压实的独立线程池<br>• 两级文件索引<br>• Level 0 到 Level 0 压实</td><td>• 块缓存与 MemTable 统一内存上限</td><td></td></tr><tr><td>2018</td><td>• 字典压缩<br>• 数据块哈希索引</td><td></td><td>• 从空间不足错误中自动恢复<br>• 查询追踪与重放工具</td></tr><tr><td>2019</td><td>• 并行 I&#x2F;O 的批量 MultiGet()</td><td>• 使用对象注册表配置插件函数</td><td>• 次级实例</td></tr><tr><td>2020</td><td>• 单文件多线程压缩</td><td></td><td>• 整文件校验和<br>• 从可重试错误中自动恢复<br>• 部分支持用户自定义时间戳</td></tr></tbody></table><hr><h2 id="附录-B：经验教训回顾"><a href="#附录-B：经验教训回顾" class="headerlink" title="附录 B：经验教训回顾"></a>附录 B：经验教训回顾</h2><p>我们学到的一些经验教训包括：</p><ol><li>存储引擎可调优以适配不同性能特征非常重要。（§1）</li><li>空间效率是大多数使用 SSD 应用的瓶颈。（§3，空间放大）</li><li>CPU 开销日益重要，有助于系统更高效运行。（§3，CPU 利用率）</li><li>当许多 RocksDB 实例在同一主机上运行时，全局的、每主机资源管理是必要的。（§4，资源管理）</li><li>WAL 行为可配置（同步 WAL 写入、缓冲 WAL 写入或禁用 WAL）能为应用带来性能优势。（§4，WAL 处理）</li><li>SSD 的 TRIM 操作有利于性能，但文件删除需限速以防偶发性能问题。（§4，速率限制的文件删除）</li><li>RocksDB 需同时提供后向和 “ 前向 “ 兼容性。（§4，数据格式兼容性）</li><li>自动配置自适应有助于简化配置管理。（§4，配置管理）</li><li>数据复制与备份需得到妥善支持。（§4，复制与备份支持）</li><li>越早检测数据损坏越有利，而不是等到最后才发现。（§5）</li><li>虽极为罕见，CPU&#x2F;内存损坏确实会发生，并且有时无法通过数据复制处理。（§5）</li><li>完整性保护必须覆盖整个系统，防止损坏数据（如 CPU&#x2F;内存位翻转）暴露给客户端或其他副本，仅在数据静止或通过网络传输时检测损坏是不够的。（§5）</li><li>用户常要求 RocksDB 能自动从瞬时 I&#x2F;O 错误中恢复，例如空间不足或由网络问题引起的错误。（§5）</li><li>错误处理需根据原因和后果区别对待。（§5）</li><li>键&#x2F;值接口是通用的，但存在一些性能局限；为键&#x2F;值添加时间戳可以在性能和简单性之间提供良好的平衡。（§6）</li></ol><hr><h2 id="附录-C：重新审视的设计选择回顾"><a href="#附录-C：重新审视的设计选择回顾" class="headerlink" title="附录 C：重新审视的设计选择回顾"></a>附录 C：重新审视的设计选择回顾</h2><p>一些值得注意的重新审视的设计选择包括：</p><ol><li>可定制性总是对用户有益的。（§4，配置管理）</li><li>RocksDB 可能无法感知 CPU 位翻转。（§5）</li><li>遇到任何 I&#x2F;O 错误时直接 panic 是可以的。（§5）</li></ol><hr><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>[1] Jung-Sang Ahn, Chiyoung Seo, Ravi Mayuram, Rahim Yaseen, Jin-Soo Kim, and Seungryoul Maeng. ForestDB: A fast key-value storage system for variable-length string keys. IEEE Trans. on Computers, 65(3):902–915, 2015.<br>[2] Manos Athanassoulis, Michael S Kester, Lukas M Maas, Radu Stoica, Stratos Idreos, Anastasia Ailamaki, and Mark Callaghan. Designing access methods: The RUM conjecture. In Proc. Intl. Conf on Extending Database Technology (EDBT), volume 2016, pages 461–466, 2016.<br>[3] Oana Balmau, Diego Didona, Rachid Guerraoui, Willy Zwaenepoel, Huapeng Yuan, Aashray Arora, Karan Gupta, and Pavan Konka. TRIAD: Creating synergies between memory, disk and log in log-structured key-value stores. In Proc. USENIX Annual Technical Conference (USENIX-ATC’17), pages 363–375, 2017.<br>[4] Matias Bjørling. Zone Append: A new way of writing to zoned storage. In Proc. Usenix Linux Storage and Filesystems Conference (VAULT’20), 2020.<br>[5] Facebook Engineering Blog. LogDevice: A distributed data store for logs. <a href="https://engineering.fb.com/core-data/logdevice-a-distributed-data-store-for-logs/">https://engineering.fb.com/core-data/logdevice-a-distributed-data-store-for-logs/</a>. [Online; retrieved September 2020].<br>[6] Instagram Engineering Blog. Open-sourcing a 10x reduction in Apache Cassandra tail latency. <a href="https://instagram-engineering.com/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency-d64f86b43589">https://instagram-engineering.com/open-sourcing-a-10x-reduction-in-apache-cassandra-tail-latency-d64f86b43589</a>. [Online; retrieved September 2020].<br>[7] Netflix Technology Blog. Application data caching using SSDs: The Moneta project: Next generation EV-Cache for better cost optimization. <a href="https://netflixtechblog.com/application-data-caching-using-ssds-5bf25df851ef">https://netflixtechblog.com/application-data-caching-using-ssds-5bf25df851ef</a>. [Online; retrieved September 2020].<br>[8] Uber Engineering Blog. Cherami: Uber Engineering’s durable and scalable task queue in Go. <a href="https://eng.uber.com/cherami-message-queue-system/">https://eng.uber.com/cherami-message-queue-system/</a>. [Online; retrieved September 2020].<br>[9] Dhruba Borthakur. HDFS architecture guide. Hadoop Apache Project, 53(1-13):2, 2008.<br>[10] Mark Callaghan. MongoRocks and WiredTiger versus LinkBench on a small server. <a href="http://smalldatum.blogspot.com/2016/10/mongorocks-and-wiredtiger-versus.html">http://smalldatum.blogspot.com/2016/10/mongorocks-and-wiredtiger-versus.html</a>. [Online; retrieved Jan 2021].<br>[11] Zhichao Cao, Siying Dong, Sagar Vemuri, and David H.C. Du. Characterizing, modeling, and benchmarking RocksDB key-value workloads at Facebook. In 18th USENIX Conf. on File and Storage Technologies (FAST’20), pages 209–223, February 2020.<br>[12] Paris Carbone, Asterios Katsifodimos, Stephan Ewen, Volker Markl, Seif Haridi, and Kostas Tzoumas. Apache Flink: Stream and batch processing in a single engine. Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, 36(4), 2015.<br>[13] Apache Cassandra. <a href="https://cassandra.apache.org/">https://cassandra.apache.org/</a>. [Online; retrieved September 2020].<br>[14] Fay Chang, Jeffrey Dean, Sanjay Ghemawat, Wilson C Hsieh, Deborah A Wallach, Mike Burrows, Tushar Chandra, Andrew Fikes, and Robert E Gruber. Bigtable: A distributed storage system for structured data. ACM Trans. on Computer Systems (TOCS), 26(2):1–26, 2008.<br>[15] Guoqiang Jerry Chen, Janet L Wiener, Shridhar Iyer, Anshul Jaiswal, Ran Lei, Nikhil Simha, Wei Wang, Kevin Wilfong, Tim Williamson, and Serhat Yilmaz. Realtime data processing at Facebook. In Proc. Intl. Conf. on Management of Data, pages 1087–1098, 2016.<br>[16] James C Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, Jeffrey John Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, et al. Spanner: Google’s globally distributed database. ACM Trans. on Computer Systems (TOCS), 31(3):1–22, 2013.<br>[17] Niv Dayan, Manos Athanassoulis, and Stratos Idreos. Monkey: Optimal navigable key-value store. In Proc. Intl. Conf. on Management of Data (SIGMOD’17), pages 79–94, 2017.<br>[18] Cristian Diaconu, Craig Freedman, Erik Ismert, Per-Ake Larson, Pravin Mittal, Ryan Stonecipher, Nitin Verma, and Mike Zwilling. Hekaton: SQL server’s memory-optimized OLTP engine. In Proc. ACM SIGMOD Intl. Conf. on Management of Data (SIGMOD’13), pages 1243–1254, 2013.<br>[19] Siying Dong, Mark Callaghan, Leonidas Galanis, Dhruba Borthakur, Tony Savor, and Michael Stumm. Optimizing space amplification in RocksDB. In Proc. Conf. on Innovative Data Systems Research (CIDR’17), 2017.<br>[20] Jose Faleiro. The dangers of logical replication and a practical solution. In Proc. 18th Intl. Workshop on High Performance Transaction Systems (HPTS’19), 2019.<br>[21] Tasha Frankie, Gordon Hughes, and Ken Kreutz-Delgado. A mathematical model of the trim command in NAND-flash SSDs. In Proc. 50th Annual Southeast Regional Conference (ACM-SE’12), pages 59–64, 2012.<br>[22] S. Ghemawat and J. Dean. LevelDB. <a href="https://github.com/google/leveldb">https://github.com/google/leveldb</a>, 2011.<br>[23] Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung. The Google File System. In Proc. 19th ACM Symp. on Operating systems principles (SOSP’13), pages 29–43, 2003.<br>[24] Guy Golan-Gueta, Edward Bortnikov, Eshcar Hillel, and Idit Keidar. Scaling concurrent log-structured data stores. In Proc. European Conf. on Computer Systems (EUROSYS’15), pages 1–14, 2015.<br>[25] Caixin Gong, Shuibing He, Yili Gong, and Yingchun Lei. On integration of appends and merges in log-structured merge trees. In Proc. 48th Intl. Conf. on Parallel Processing (ICPP’19), pages 1–10, 2019.<br>[26] Apache HBase. <a href="https://hbase.apache.org/">https://hbase.apache.org/</a>. [Online; retrieved September 2020].<br>[27] Dongxu Huang, Qi Liu, Qiu Cui, Zhuhe Fang, Xiaoyu Ma, Fei Xu, Li Shen, Liu Tang, Yuxing Zhou, Menglong Huang, Wan Wei, Cong Liu, Jian Zhang, Jianjun Li, Xuelian Wu, Lingyu Song, Ruoxi Sun, Shuaipeng Yu, Lei Zhao, Nicholas Cameron, Liquan Pei, and Xin Tang. TiDB: A Raft-based HTAP database. Proc. VLDB Endow., 13(12):3072–3084, August 2020.<br>[28] Intel. Trim overview. <a href="https://www.intel.com/content/www/us/en/support/articles/000016148/memory-and-storage.html">https://www.intel.com/content/www/us/en/support/articles/000016148/memory-and-storage.html</a>. [Online; retrieved Jan 2021].<br>[29] Iron.io. Confluent <a href="https://www.iron.io/">https://www.iron.io</a>. [Online; retrieved September 2020].<br>[30] Hideaki Kimura. FOEDUS: OLTP engine for a thousand cores and NVRAM. In Proc. SIGMOD Intl. Conf. on Management of Data (SIGMOD’15), pages 691–706, 2015.<br>[31] Jay Kreps. Introducing Kafka Streams: Stream processing made simple. Confluent <a href="https://www.confluent.io/blog/introducing-kafka-streams-stream-processing-made-simple/">https://www.confluent.io/blog/introducing-kafka-streams-stream-processing-made-simple/</a>. [Online; retrieved September 2020].<br>[32] B Kuszmaul. How TokuDB fractal tree indexes work. Technical report, Technical report, TokuTek, 2010.<br>[33] Chuck Lever. End-to-end data integrity requirements for NFS. Oracle Corp. <a href="https://datatracker.ietf.org/meeting/83/materials/slides-83-nfsv4-2">https://datatracker.ietf.org/meeting/83/materials/slides-83-nfsv4-2</a>. [Online; retrieved September 2020].<br>[34] Hyeontaek Lim, Bin Fan, David G Andersen, and Michael Kaminsky. SILT: A memory-efficient, high-performance key-value store. In Proc. 23rd ACM Symp. on Operating Systems Principles (SOSP’11), pages 1–13, 2011.<br>[35] Lanyue Lu, Thanumalayan Sankaranarayana Pillai, Hariharan Gopalakrishnan, Andrea C Arpaci-Dusseau, and Remzi H Arpaci-Dusseau. Wisckey: Separating keys from values in SSD-conscious storage. ACM Trans. on Storage (TOS), 13(1):1–28, 2017.<br>[36] Yoshinori Matsunobu. Migrating a database from InnoDB to MyRock. Facebook Engineering Blog 2017. [Online; retrieved September 2020].<br>[37] Yoshinori Matsunobu, Siying Dong, and Herman Lee. MyRocks: LSM-tree database storage engine serving Facebook’s Social Graph. Proc. VLDB Endowment, 13(12):3217–3230, August 2020.<br>[38] Microsoft. Microsoft SQL Server. <a href="https://www.microsoft.com/en-us/sql-server/">https://www.microsoft.com/en-us/sql-server/</a>. [Online; retrieved September 2020].<br>[39] MongoDB. WiredTiger Storage Engine. <a href="https://docs.mongodb.com/manual/core/wiredtiger/">https://docs.mongodb.com/manual/core/wiredtiger/</a>. [Online; retrieved September 2020].<br>[40] MongoRocks. RocksDB storage engine module for MongoDB. <a href="https://github.com/mongodb-partners/mongo-rocks">https://github.com/mongodb-partners/mongo-rocks</a>. [Online; retrieved September 2020].<br>[41] MySQL. Introduction to InnoDB. <a href="https://dev.mysql.com/doc/refman/5.6/en/innodb-introduction.html">https://dev.mysql.com/doc/refman/5.6/en/innodb-introduction.html</a>. [Online; retrieved September 2020].<br>[42] MySQL. MySQL. <a href="https://www.mysql.com/">https://www.mysql.com/</a>. [Online; retrieved September 2020].<br>[43] Shadi A Noghabi, Kartik Paramasivam, Yi Pan, Navina Ramesh, Jon Bringhurst, Indranil Gupta, and Roy H Campbell. Samza: Stateful scalable stream processing at LinkedIn. Proc. of the VLDB Endowment, 10(12):1634–1645, 2017.<br>[44] Michael A Olson, Keith Bostic, and Margo I Seltzer. Berkeley DB. In USENIX Annual Technical Conference, FREENIX Track, pages 183–191, 1999.<br>[45] Patrick O’Neil, Edward Cheng, Dieter Gawlick, and Elizabeth O’Neil. The log-structured merge-tree (LSM-tree). Acta Informatica, 33(4):351–385, 1996.<br>[46] Keren Ouaknine, Oran Agra, and Zvika Guz. Optimization of RocksDB for Redis on flash. In Proc. Intl. Conf. on Compute and Data Analysis, pages 155–161, 2017.<br>[47] Mike Owens. The definitive guide to SQLite. Apress, 2006.<br>[48] Martin K Petersen. Linux data integrity extensions. In Linux Symposium, volume 4, page 5, 2008.<br>[49] Martin K. Petersen and Sergio Leunissen. Eliminating silent data corruption with Oracle Linux. Oracle Corp. <a href="https://oss.oracle.com/~mkp/docs/data-integrity-webcast.pdf">https://oss.oracle.com/~mkp/docs/data-integrity-webcast.pdf</a>. [Online; retrieved September 2020].<br>[50] Ivan Luiz Picoli, Niclas Hedam, Philippe Bonnet, and Pinar Tözün. Open-channel SSD (What is it good for). In Proc. Conf. on Innovative Data Systems Research (CIDR’20), 2020.<br>[51] Qihoo. Confluent <a href="https://github.com/Qihoo360/pika">https://github.com/Qihoo360/pika</a>. [Online; retrieved September 2020].<br>[52] Pandian Raju, Rohan Kadekodi, Vijay Chidambaram, and Ittai Abraham. PebblesDB: Building key-value stores using fragmented log-structured merge trees. In Proc. 26th Symp. on Operating Systems Principles (SOSP’17), pages 497–514, 2017.<br>[53] Kai Ren, Qing Zheng, Joy Arulraj, and Garth Gibson. SlimDB: A space-efficient key-value storage engine for semi-sorted data. Proc. of the VLDB Endowment (VLDB’17), 10(13):2037–2048, 2017.<br>[54] RocksDB.org. A persistent key-value store for fast storage environments. <a href="https://rocksdb.org/">https://rocksdb.org</a>. [Online; retrieved September 2020].<br>[55] Jerome H Saltzer, David P Reed, and David D Clark. End-to-end arguments in system design. ACM Trans. on Computer Systems (TOCS), 2(4):277–288, 1984.<br>[56] Tony Savor, Mitchell Douglas, Michael Gentili, Laurie Williams, Kent Beck, and Michael Stumm. Continuous deployment at Facebook and OANDA. In 2016 IEEE&#x2F;ACM 38th International Conference on Software Engineering Companion (ICSE-C), pages 21–30. IEEE, 2016.<br>[57] Russell Sears and Raghu Ramakrishnan. bLSM: a general purpose log-structured merge tree. In Proc. Intl. Conf. on Management of Data (SIGMOD’12), pages 217–228, 2012.<br>[58] Arun Sharma. How we use RocksDB at Rockset. Rockset Blog <a href="https://rockset.com/blog/how-we-use-rocksdb-at-rockset/">https://rockset.com/blog/how-we-use-rocksdb-at-rockset/</a>. [Online; retrieved September 2020].<br>[59] Arun Sharma. LogDevice: A distributed data store for logs. Facebook Engineering Blog <a href="https://engineering.fb.com/data-infrastructure/dragon-a-distributed-graph-query-engine/">https://engineering.fb.com/data-infrastructure/dragon-a-distributed-graph-query-engine/</a>. [Online; retrieved September 2020].<br>[60] Pradeep J Shetty, Richard P Spillane, Ravikant R Malpani, Binesh Andrews, Justin Seyster, and Erez Zadok. Building workload-independent storage with VT-trees. In Proc. 11th USENIX Conf. on File and Storage Technologies (FAST’13), pages 17–30, 2013.<br>[61] Gopalan Sivathanu, Charles P Wright, and Erez Zadok. Enhancing file system integrity through checksums. Technical report, Citeseer, 2004.<br>[62] Mark Slee, Aditya Agarwal, and Marc Kwiatkowski. Thrift: Scalable cross-language services implementation. Facebook White Paper, 5(8), 2007.<br>[63] Google Open Source. Protobuf. <a href="https://opensource.google/projects/protobuf">https://opensource.google/projects/protobuf</a>. [Online; retrieved September 2020].<br>[64] Rebecca Taft, Irfan Sharif, Andrei Matei, Nathan VanBenschoten, Jordan Lewis, Tobias Grieger, Kai Niemi, Andy Woods, Anne Birzin, Raphael Poss, Paul Bardea, Amruta Ranade, Ben Darnell, Bram Gruneir, Justin Jaffray, Lucy Zhang, and Peter Mattis. CockroachDB: The resilient geo-distributed SQL database. In Proc. ACM SIGMOD Intl. Conf. on Management of Data (SIGMOD’20), page 1493–1509, 2020.<br>[65] Amy Tai, Andrew Kryczka, Shobhit O. Kanaujia, Kyle Jamieson, Michael J. Freedman, and Asaf Cidon. Who’s afraid of uncorrectable bit errors? Online recovery of flash errors with distributed redundancy. In 2019 USENIX Annual Technical Conference (USENIX ATC’19), pages 977–992, Renton, WA, July 2019.<br>[66] Tobias Vinçon, Sergej Hardock, Christian Riegger, Julian Oppermann, Andreas Koch, and Ilia Petrov. NoFTL-KV: Tackling write-amplification on KV-stores with native storage management. In Proc. 21st Intl. Conf. on Extending Database Technology (EDBT’18), pages 457–460, 2018.<br>[67] Peng Wang, Guangyu Sun, Song Jiang, Jian Ouyang, Shiding Lin, Chen Zhang, and Jason Cong. An efficient design and implementation of LSM-tree based key-value store on open-channel SSD. In Proc. 9th European Conf. on Computer Systems (EUROSYS’14), pages 1–14, 2014.<br>[68] Fei Yang, K Dou, S Chen, JU Kang, and S Cho. Multi-streaming RocksDB. In Proc. Non-Volatile Memories Workshop, 2015.<br>[69] Jiacheng Zhang, Youyou Lu, Jiwu Shu, and Xiongjun Qin. FlashKV: Accelerating KV performance with open-channel SSDs. ACM Trans on Embedded Computing Systems (TECS), 16(5s):1–19, 2017.<br>[70] Yupu Zhang, Daniel S Myers, Andrea C Arpaci-Dusseau, and Remzi H Arpaci-Dusseau. Zettabyte reliability with flexible end-to-end data integrity. In Proc. 29th IEEE Symp. on Mass Storage Systems and Technologies (MSST’13), pages 1–14, 2013.<br>[71] Yupu Zhang, Abhishek Rajimwale, Andrea C Arpaci-Dusseau, and Remzi H Arpaci-Dusseau. End-to-end data integrity for file systems: A ZFS case study. In Proc. 8th USENIX Conf. on File and Storage Technologies (FAST’10), pages 29–42, 2010.</p><blockquote><p>原文链接：<a href="https://www.usenix.org/system/files/fast21-dong.pdf">Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications: The RocksDB Experience</a></p><p>本文为中文翻译，仅用于学习与分享，版权归原作者所有。</p><p>Slides: <a href="https://www.usenix.org/sites/default/files/conference/protected-files/fast21_slides_dong.pdf">Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications: The RocksDB Experience</a><br>相关论文：<a href="https://dl.acm.org/doi/10.1145/3483840">RocksDB: Evolution of Development Priorities in a Key-value Store Serving Large-scale Applications</a></p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/08-03-2025/the-rocksdb-experience.html">https://www.cyningsun.com/08-03-2025/the-rocksdb-experience.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Siying Dong, Andrew Kryczka, Yanqin Jin and Michael Stumm&lt;/p&gt;
&lt;p&gt;Facebook Inc., 1 Hacker Way, Menlo Park, CA, U.S.A&lt;br&gt;University of Toro</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>译｜Disaggregating RocksDB: A Production Experience</title>
    <link href="https://www.cyningsun.com/06-01-2025/disaggregating-rocksdb-a-production-experience-cn.html"/>
    <id>https://www.cyningsun.com/06-01-2025/disaggregating-rocksdb-a-production-experience-cn.html</id>
    <published>2025-05-31T16:00:00.000Z</published>
    <updated>2025-08-26T03:07:01.000Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://orcid.org/0000-0003-0576-2226">SIYING DONG,</a> <a href="https://orcid.org/0009-0004-4528-3857">SHIVA SHANKAR P,</a> <a href="https://orcid.org/0000-0002-8515-5233">SATADRU PAN,</a> <a href="https://orcid.org/0009-0005-7734-6741">ANAND ANANTHABHOTLA,</a> <a href="https://orcid.org/0009-0006-3564-5185">DHANABAL EKAMBARAM,</a> <a href="https://orcid.org/0009-0005-8662-6067">ABHINAV SHARMA,</a> <a href="https://orcid.org/0009-0007-4459-9958">SHOBHIT DAYAL,</a> <a href="https://orcid.org/0009-0003-4612-9010">NISHANT VINAYBHAI PARIKH,</a><a href="https://orcid.org/0009-0007-5463-2125">YANQIN JIN,</a><a href="https://orcid.org/0009-0004-5200-8095">ALBERT KIM,</a><a href="https://orcid.org/0009-0009-6800-0123">SUSHIL PATIL,</a><a href="https://orcid.org/0009-0007-0455-9564">JAY ZHUANG,</a> <a href="https://orcid.org/0009-0004-4818-8396">SAM DUNSTER,</a> <a href="https://orcid.org/0009-0008-1200-5948">AKANKSHA MAHAJAN,</a> <a href="https://orcid.org/0000-0001-5051-1624">ANIRUDH CHELLURI,</a> <a href="https://orcid.org/0009-0006-4128-7361">CHAITANYA DATYE,</a> <a href="https://orcid.org/0009-0002-6831-0139">LUCAS VASCONCELOS SANTANA,</a> <a href="https://orcid.org/0009-0002-8458-2134">NITIN GARG,</a> 和 <a href="https://orcid.org/0009-0006-0245-8877">OMKAR GAWDE,</a>Meta, USA</p><p>正如业界的普遍趋势，Meta 数据中心也在将数据从本地直连 SSD 迁移到云存储。我们扩展了 RocksDB [26]，这是一款广泛使用、为本地 SSD 设计和构建的开源存储引擎，使其能够利用分离式存储。RocksDB 的设计（如其数据和日志文件的访问模式）使得追加写分布式文件系统成为理想的底层存储。在 Meta，我们基于 Tectonic 文件系统 [35] 构建了存算分离式 RocksDB，Tectonic 之前主要用于数据仓库和大对象存储。我们发现元数据开销和尾延迟是 Tectonic 的主要性能瓶颈，并针对性地进行了优化。我们通过对 RocksDB 核心引擎的通用和定制优化，提升了可靠性、性能及其他需求。我们还深入理解了运行在 RocksDB 上的应用所面临的常见挑战，并据此做了增强。这一架构使 RocksDB 能够适应更分布式的架构以提升性能。</p><p><strong>CCS 概念：</strong> • 信息系统 → DBMS 引擎架构；分布式存储。<br><strong>附加关键词和短语：</strong> 分离式存储，日志结构合并树（LSM-tree），rocksdb，分布式文件系统</p><h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1 引言"></a>1 引言</h2><p>RocksDB [26] 是一款在 Meta 内外广泛使用的开源存储引擎。历史上，它主要用于本地 SSD 存储数据。然而，分离式存储（RocksDB 通过网络访问底层存储系统）可以通过让 CPU 和存储按需独立配置来实现更高的效率。</p><p>几年前，几乎没有 RocksDB 应用能承受将存储放在远程网络上的代价，但近年来网络带宽迅速提升。例如，十年前我们还在将主机从 1Gbps 网卡升级到 10Gbps，而现在至少是 25Gbps，常见的还有 50Gbps 或 100Gbps。另一方面，大多数用户对磁盘 IO 带宽的需求与十年前相似。这一趋势使得分离式存储对越来越多的应用具有吸引力。尽管网络仍是瓶颈，但许多场景受限于空间而非 IOPS，在这种情况下，降低 IOPS 可能是可以接受的权衡。此外，如果分离式存储实现为可靠服务，用户可以以多种方式利用其可靠性，例如提升区域内可用性、减少跨区域数据传输。因此，我们开始设计一种运行在分离式存储之上的存储引擎方案。</p><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212519-1.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-1.png"><br>图 1. 不同的使用 RocksDB 的应用程序可以在分离式存储上运行。</p><p>在 Meta，我们选择继续使用 RocksDB，但将数据存储在分布式文件系统而非本地 SSD 上。我们之所以坚持使用 RocksDB，是因为其主要数据结构——日志结构合并树（LSM-tree）[34]——能够最大限度地减少空间占用（这是常见瓶颈），并且已被证明对分布式文件系统高效 [21]。LSM-tree 生成的数据文件一旦创建即不可变。托管存储服务还能提供容错能力，RocksDB 应用可以多种方式利用这一点，比如减少数据中心内的故障切换时间。由于 RocksDB 已服务于多种服务，扩展其支持的存储类型自然能让这些服务运行在分离式存储上，如图 1 所示；我们的经验表明，这样做可以获得令人满意的性能。Tectonic 文件系统 [35] 是一种最初为数据仓库和大对象存储构建的追加写分布式文件系统，符合所需的存储特性。我们决定让 RocksDB 运行在 Tectonic 之上。</p><p>要让 RocksDB 应用运行在分布式文件系统（DFS）上，我们面临四大挑战（§3）：a）分离式存储引入了额外的网络跳数，通常导致延迟较本地 SSD 倒退；b）只有数据以容错方式存储时，托管存储系统的优势（如快速故障切换）才能实现，但增加冗余会带来额外的存储和 SSD 耗损开销；c）在分离式存储下，多个节点可访问同一 RocksDB 目录的文件，发生故障切换时，必须保证前一节点无法再修改数据；d）RocksDB 库需要针对远程 IO 行为（故障、超时等）与本地 IO 行为做出不同响应。第 4 节介绍了我们对这些问题的解决方案。</p><p>在追加写分布式文件系统上运行基于 LSM-tree 的数据库引擎并非新鲜事，但我们认为我们的经验和收获在多个方面具有独特性。首先，我们证明了 RocksDB 可以同时支持本地和存算分离模式：即使存算分离模式的性能不及本地，但对于 Meta 内大量 RocksDB 应用来说已足够（§5）。</p><p>其次，我们分享了将一个为机械硬盘、主要用于数据仓库和大对象存储的分布式文件系统转型为服务 RocksDB 的经验（§8）。我们认为这种转型是常见模式，这里的经验具有广泛适用性。</p><p>最后，我们还分享了 RocksDB 应用常见挑战及其解决方案。我们以 Meta 内成熟数据库服务 ZippyDB [15] 为案例，介绍其如何解决非 RocksDB 文件、构建新副本、质量验证和文件垃圾回收等问题（§6）。据我们所知，类似经验尚未有公开分享。</p><p>凭借 RocksDB 的广泛应用，我们的经验对那些考虑将生产环境 RocksDB 从本地迁移到分离式存储的用户具有参考价值。</p><h2 id="2-背景与动机"><a href="#2-背景与动机" class="headerlink" title="2 背景与动机"></a>2 背景与动机</h2><p>本节将概述 RocksDB 和 Tectonic，并介绍在 Tectonic 上运行 RocksDB 的动机。</p><h3 id="2-1-RocksDB"><a href="#2-1-RocksDB" class="headerlink" title="2.1 RocksDB"></a>2.1 RocksDB</h3><p>RocksDB 是一款被多种数据服务广泛使用的开源数据库存储引擎。它主要用于 SSD 存储数据，但也有用户让 RocksDB 存储在机械硬盘或基于内存的文件系统上。统一的引擎为不同场景带来了可靠性、性能和可管理性等优势。</p><p>RocksDB 采用日志结构合并树（LSM-tree）[34] 实现。每当数据写入 RocksDB 时，数据首先被缓存在内存中，并写入磁盘上的预写日志（WAL）。数据随后会被刷新到磁盘上的有序字符串表（SST）数据文件中。每个 SST 文件以有序方式存储数据，并划分为多个块。热点块会被缓存在基于内存的块缓存中以减少 I&#x2F;O。一旦写入，SST 文件即不可变。</p><p>SST 文件经常通过合并（compaction）过程被合并生成新的一组 SST 文件。在此过程中，被删除和被覆盖的数据会被移除，新的 SST 文件会针对读取性能和空间效率进行优化。由于每个 SST 文件中的键都是有序的，因此合并过程中的读写都是顺序进行的。写入可以使用任意大小的缓冲区进行缓冲，因为合并过程输出的 SST 文件在合并完成前不会被读取。通常，大部分对存储系统的写入都是由合并过程完成的。</p><h3 id="2-2-为什么选择分离式存储？"><a href="#2-2-为什么选择分离式存储？" class="headerlink" title="2.2 为什么选择分离式存储？"></a>2.2 为什么选择分离式存储？</h3><p>基于闪存的 SSD 在 Meta 的各类服务中被广泛使用。最初，SSD 总是以本地方式使用，数据库服务器通过直连 PCI-e 访问 SSD 数据。这种架构让数据库服务器能够充分利用 SSD 的高吞吐和低延迟特性，至今仍适用于许多应用。但这种架构可能导致资源浪费，并增加了服务管理的难度。</p><p>在本地 SSD 架构下，主机内的 CPU 和存储资源往往不均衡。[26] 此外，一些用户由于闪存擦写预算或读取带宽限制，无法用满所有空间，而另一些用户虽然用满了空间，但也会浪费部分 I&#x2F;O 或擦写周期。此外，每个服务都需要预留足够的缓冲和冗余空间，但大多数时间这些空间并未被使用。</p><p>如果计算与存储分离，存储可以按需从共享池中分配。未用空间可以共享，因此只需为缓冲和冗余预留更小比例的总空间。CPU 也可以更灵活地在数据库间调度和迁移，因为无需数据拷贝。这种快速的 CPU 配置让用户能够以更高的 CPU 利用率配置主机。结果，分离式存储让用户能够最大化整体 CPU 和存储利用率。使用分布式文件系统进行存算分离的更多好处见 §2.5。</p><h3 id="2-3-为什么仍然选择-RocksDB？"><a href="#2-3-为什么仍然选择-RocksDB？" class="headerlink" title="2.3 为什么仍然选择 RocksDB？"></a>2.3 为什么仍然选择 RocksDB？</h3><p>我们没有选择构建或使用其他存储引擎，而是决定继续使用 RocksDB，原因如下。首先，即使采用了分离式存储，许多场景的瓶颈并非 I&#x2F;O，而是空间占用。RocksDB 优秀的空间效率特性 [25] 同样适用于存算分离架构。</p><p>其次，统一的存储引擎支持本地和分离式存储，使迁移更容易，并允许两种模式长期共存，便于随时切换。</p><p>第三，一个同时支持本地和分离式存储的简单引擎意味着未来的改进只需在同一处实现，便可同时惠及两种模式。这对 RocksDB 这样流行的开源项目尤其有益，外部研究者也能利用这些改进。</p><p>最后，我们认为 RocksDB 的主数据结构 LSM-tree 非常适合分离式存储。</p><h3 id="2-4-为什么选择-Tectonic-文件系统？"><a href="#2-4-为什么选择-Tectonic-文件系统？" class="headerlink" title="2.4 为什么选择 Tectonic 文件系统？"></a>2.4 为什么选择 Tectonic 文件系统？</h3><p>我们最初尝试构建存算分离方案时，聚焦于远程块设备。我们使用 ATA over Ethernet（AoE）或网络块设备（NBD）将远程磁盘连接到计算节点，提供标准本地访问的假象。这种简单架构实现了分离式存储，但我们发现它无法充分发挥潜力。由于不支持精简配置，效率提升有限。此外，存储节点或网络故障处理困难，服务拥有者也觉得这种系统难以运维。</p><p>为了解决上述方案的局限，我们探索了包括 NFS 和分布式可靠块设备在内的多种方案。然而，NFS 过于复杂，难以高效利用；而块设备接口下，不同主机间的数据共享也很困难。我们意识到，通用的分布式块设备或文件系统过于泛化，可能错失针对性优化机会，从而限制了系统的潜力。RocksDB 的数据和日志文件采用顺序写入，写入后即不可变。任何允许随机写的方案都不可避免地引入额外开销和复杂性。假设数据块不可变的文件系统能实现更高效率，这正是 Tectonic 文件系统的设计假设。因此我们决定以 RocksDB 运行在 Tectonic 上为起点，逐步演进方案。Tectonic 是 Meta 的艾字节（EB）级分布式文件系统，最初为数据仓库和大对象存储场景设计 [35]。它提供类似 Hadoop 文件系统（HDFS）的分层文件系统 API，大多数云服务商也有兼容的存储方案。在很多方面，我们的设计选择得到了前人系统的验证，如 BigTable[21]、HBase[6] 和 Spanner[22]，它们都是基于追加写分布式文件系统的数据库。</p><p>不过，我们为 RocksDB 支持分离式存储的方式，使其可以运行在大多数分布式文件系统和对象存储系统之上，因此我们的工作具有广泛适用价值。原则上，我们需要如下 API 原语：a）数据以文件（或称对象、blob）为单位分组，文件名由用户指定；b）文件以追加写方式构建，数据可通过文件偏移读取；c）文件可分组到目录（或桶）中。我们在 §8 讨论了如何让存储系统在效率和易用性上达到满意水平的经验。</p><p>关于 Tectonic 文件系统的更多细节见 [35]。这里仅强调部分方面，帮助读者理解后续章节中我们提出的优化。Tectonic 中的数据通过冗余（纠删码或副本）实现持久性。Tectonic 文件由多个块组成。每个块的数据片段（以及若采用纠删码则包括校验片段）被存储在不同故障域的独立存储节点上，这样即使少量节点失效，数据也能从其他片段重建。Tectonic 的元数据层存储了目录到文件列表、文件到块列表、块到数据片段列表等多种映射。Tectonic 客户端库（在本例中运行于 RocksDB 库内部）负责协调与元数据和存储节点的 RPC 调用，管理数据片段的存储。</p><h3 id="2-5-使用-Tectonic-的额外好处"><a href="#2-5-使用-Tectonic-的额外好处" class="headerlink" title="2.5 使用 Tectonic 的额外好处"></a>2.5 使用 Tectonic 的额外好处</h3><p>如上所述，分离式存储带来了更好的资源利用率和 CPU、存储的独立扩展能力。使用 Tectonic 作为分离式存储还带来以下额外好处：</p><ul><li><strong>减少副本数量</strong>。使用 RocksDB 的应用需要提供高可用性保障，并能容忍各种故障。Tectonic 能确保常见故障场景（主机、机架或电源域故障）下的数据可用性。这种可用性提升让部分应用可以减少所需副本数。在许多情况下，应用可以将副本数从三份降至两份，或从五份降至三份，同时维持同等可用性。</li><li><strong>快速故障切换</strong>。应用还可以依赖 Tectonic 快速从单主机故障中恢复。当服务将数据存储在本地 SSD 上时，主机故障可能导致数 TB 数据副本数不足，必须快速重建副本以避免永久性数据丢失。Tectonic 的容错特性允许应用在无需数据拷贝的情况下恢复（§6.2）。</li><li><strong>简化存储管理</strong>。管理 SSD 并非易事，尤其对小团队而言。许多 RocksDB 用户希望避免这类复杂性，因此选择专用存储服务来屏蔽所有问题。例如，Tectonic 会自动将副本分布在不同电源故障域，应用天然获得大规模电源故障的保护。</li><li><strong>存储共享</strong>。Tectonic 允许多台主机访问同一组文件。这使得备份、合并、校验等后台操作可以卸载到独立层（§7），也支持快速数据克隆。</li></ul><h3 id="2-6-何时不宜存算分离"><a href="#2-6-何时不宜存算分离" class="headerlink" title="2.6 何时不宜存算分离"></a>2.6 何时不宜存算分离</h3><p>尽管存算分离带来诸多好处，RocksDB 应用仍需权衡多种因素。a）RocksDB 在 Tectonic 上的查询延迟和吞吐仍有性能差距（§5），应用需验证能否容忍性能下降。b）为保证持久性，Tectonic 需要对数据做一定冗余，通常会使存储使用量增加 20% 以上，实际配置常高达 50%。c）Tectonic 系统复杂，依赖较多。对大多数应用不是问题，但某些原始服务可能要求依赖极少。例如，Tectonic 的元数据服务本身就难以运行在 RocksDB on Tectonic 上。</p><h2 id="3-架构概览与主要挑战"><a href="#3-架构概览与主要挑战" class="headerlink" title="3 架构概览与主要挑战"></a>3 架构概览与主要挑战</h2><p>本节将介绍 RocksDB 存算分离架构的高层概览，并描述需要应对的一些挑战。</p><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212519-2.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-2.png"><br>图 2. 在本地 SSD 上使用 RocksDB 的典型服务。</p><h3 id="3-1-架构"><a href="#3-1-架构" class="headerlink" title="3.1 架构"></a>3.1 架构</h3><p>图 2 展示了存算分离前 Meta 内典型数据服务的架构，图 3 展示了存算分离后的对应架构。 </p><p>Tectonic 集群是数据中心区域内的，因此应用通常需要继续使用自身的地理复制逻辑，每个副本都将数据存储在数据中心内的 Tectonic 集群上。如果计算节点发生故障或需要负载均衡，另一台计算主机会接管并在 Tectonic 上操作同一份数据。</p><p>让 RocksDB 运行在 Tectonic 上所需的核心变更很简单。首先，为了支持 Tectonic 文件的读写，需要开发一个新插件，实现 RocksDB 的存储接口（§6.1），使 RocksDB 能与 Tectonic 协作。其次，RocksDB 用户需要通过共享的 Tectonic 命名空间管理文件，而不是使用独占的本地文件系统。不同应用可以在集群中拥有各自逻辑隔离的命名空间。为了从独占本地文件系统迁移到 Tectonic 命名空间，有些应用通过目录结构（如 ‘namespace&#x2F;…&#x2F;<host_name>&#x2F;<db_name>‘，§6.2 为例）保持独占性，另一些用户则采用扁平结构（’namespace&#x2F;…&#x2F;<db_name>‘），并依赖应用特定逻辑同步目录访问。无论哪种方式，Tectonic 都需增加支持，帮助用户在需要时保证对某目录的独占访问（§4.3）。此外，还可能需要垃圾回收功能来移除 Tectonic 上的 RocksDB 目录（§6.4）。</p><p>应用配置好 Tectonic 插件并传递给 RocksDB 库，同时指定基础目录信息，确保数据被读写到期望的远程目录。插件还会读取 Tectonic 特定配置，如副本方案。有些配置（如副本方案）可能基于 RocksDB 传递的文件类型。每个 RocksDB 实例是对应目录下文件的唯一写入者，因此该实例可以安全地缓存数据和元数据，无需担心一致性问题（§4.1.2, §4.1.3）。</p><p>上述基础设置可作为原型运行，但我们在将其扩展到生产可用、可扩展和高效时遇到了若干问题。这些挑战及其对应解决方案分别在第 3.2 节和第 4 节详细描述。</p><blockquote><p>需要实现的接口：<a href="https://github.com/facebook/rocksdb/blob/7.6.fb/include/rocksdb/file_system.h">https://github.com/facebook/rocksdb/blob/7.6.fb/include/rocksdb/file_system.h</a></p></blockquote><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212519-3.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-2.png"></p><p>图 3. 典型服务在 Tectonic 上使用 RocksDB。</p><h3 id="3-2-挑战"><a href="#3-2-挑战" class="headerlink" title="3.2 挑战"></a>3.2 挑战</h3><h4 id="3-2-1-性能"><a href="#3-2-1-性能" class="headerlink" title="3.2.1 性能"></a>3.2.1 性能</h4><p>分离式存储引入了网络跳数，导致性能相较本地 SSD 架构有所倒退。我们接受部分应用永远无法由远程存储服务，但仍希望拓展可服务的应用范围。</p><p>我们调研了典型工作负载模式及其性能预期。通常我们期望小型读写（KB 级到数百 KB，通常为点查或 Get&#x2F;Put 请求）的 99 分位延迟小于 5 毫秒。对于 MultiGet 查询、迭代器或扫描操作，99 分位延迟期望在数十毫秒级。我们在第 4.1 节介绍了为满足上述需求所做的性能优化。</p><h4 id="3-2-2-低开销冗余"><a href="#3-2-2-低开销冗余" class="headerlink" title="3.2.2 低开销冗余"></a>3.2.2 低开销冗余</h4><p>为实现托管存储系统的优势并支持快速故障切换，DFS 数据需具备持久性（§2.4）。但增加冗余会带来存储空间和 SSD 寿命的额外开销。由于 Tectonic 客户端驱动架构，数据的副本或纠删码操作在 Tectonic 客户端内部完成，这也会为 RocksDB 应用带来网络开销。我们在第 4.2 节介绍了如何为 RocksDB 的 SST 和 WAL 文件采用不同的副本方案。</p><h4 id="3-2-3-多写者下的数据完整性"><a href="#3-2-3-多写者下的数据完整性" class="headerlink" title="3.2.3 多写者下的数据完整性"></a>3.2.3 多写者下的数据完整性</h4><p>为支持快速故障切换，分离式存储下多个计算节点可访问同一 RocksDB 目录下的文件。通常只有一个节点能修改数据。当该节点失效时，应用会选取另一节点接管。此时需保证前一节点无法再修改数据。实现这一点并不简单，因为通常无法联系失效节点，且该节点未来可能重新上线并尝试操作。换句话说，问题可转化为：如何保证某一目录及其内容在任一时段只能被唯一指定进程操作。我们的解决方案见第 4.3 节。</p><h4 id="3-2-4-适配远程-IO"><a href="#3-2-4-适配远程-IO" class="headerlink" title="3.2.4 适配远程 IO"></a>3.2.4 适配远程 IO</h4><p>RocksDB 需改变部分假设以支持远程 IO。本地文件系统通常无需处理瞬时 I&#x2F;O 错误，除非极端情况（如磁盘空间耗尽）。RocksDB 过去将 I&#x2F;O 错误视为本地文件系统损坏，此时会将整个数据库设为只读。现在，RocksDB 需更合理地处理不同错误，确保数据库在可能情况下持续运行。更多示例及其解决方案见第 4.4 节。</p><h2 id="4-应对挑战"><a href="#4-应对挑战" class="headerlink" title="4 应对挑战"></a>4 应对挑战</h2><p>本节介绍我们如何克服上述挑战。大部分变更都在 RocksDB 库内部。但为解决部分挑战，我们还需要底层 DFS 提供特定功能或性能保障。我们会在本节中标注这些需求，并在 §8 总结经验。</p><h3 id="4-1-性能优化"><a href="#4-1-性能优化" class="headerlink" title="4.1 性能优化"></a>4.1 性能优化</h3><p>RocksDB 存算分离方案需弥合远程 IO 带来的延迟差距（§3.2.1）。底层 DFS（Tectonic）需提供良好的尾延迟，RocksDB 层则需尽量屏蔽额外延迟。我们将介绍 DFS 尾延迟改进及 RocksDB 侧的专用优化。最后在第 5 节评估存算分离方案的性能。</p><h4 id="4-1-1-优化-I-x2F-O-尾延迟"><a href="#4-1-1-优化-I-x2F-O-尾延迟" class="headerlink" title="4.1.1 优化 I&#x2F;O 尾延迟"></a>4.1.1 优化 I&#x2F;O 尾延迟</h4><p>整体 I&#x2F;O 可能因一两个慢存储节点而变慢，导致读写操作出现长尾延迟。为应对该问题，当怀疑某节点变慢时，我们尝试通过其他节点服务流量。具体采用了三种技术：</p><ul><li><strong>动态主动重构（Dynamic Eager Reconstructions）</strong>：通常先发起第一次读取，延迟后有条件地发起第二次读取，取最早返回结果。对副本数据来说很直接，但对纠删码数据，第二次读取涉及多个并行 IO，资源消耗更大。此外，若集群健康状况导致延迟升高，重构读取若处理不当会进一步恶化延迟。我们通过密切跟踪集群读取延迟分位数，并持续调整发起第二次读取的阈值来应对。</li><li><strong>动态追加写入超时（Dynamic Append Timeouts）</strong>：写入 Tectonic 通常涉及将数据刷新到一组存储节点。我们只等待部分节点（仲裁组&#x2F;子集）确认后即向客户端确认，其余写入在后台继续。这带来更好的尾延迟。但若集群有维护活动，更多节点响应变慢，该技术就失效了。为此，我们采用类似动态主动重构的方法：若超时发生，则终止正在进行的追加，记录元数据中最后成功的大小，选择新一组存储节点，更新元数据，并在新节点上重试。这能屏蔽少数慢节点带来的影响。我们密切跟踪集群追加延迟分位数，并持续调整新追加的超时时间。我们计划进一步优化，消除该流程中的元数据更新。</li><li><strong>对冲仲裁组全块写入（Hedged Quorum Full Block Writes）</strong>：与追加写入不同，要求必须在特定存储节点组完成数据写入，对于大型写入任务，Tectonic 会创建全块，因此可选择任意存储节点执行写入。我们将块写分为两个阶段：许可获取和数据传输。第一阶段，客户端从远超所需数量的存储节点池获取写入许可，节点根据自身资源（内存、带宽、CPU）决定是否授予许可。第二阶段，客户端选择最早响应的节点进行实际写入。</li></ul><h4 id="4-1-2-RocksDB-元数据缓存"><a href="#4-1-2-RocksDB-元数据缓存" class="headerlink" title="4.1.2 RocksDB 元数据缓存"></a>4.1.2 RocksDB 元数据缓存</h4><p>RocksDB 对某些文件元数据操作（如目录列举、文件存在性检查、文件大小查询）有更高性能要求。这些操作也被数据仓库等其他应用使用，但 RocksDB 使用频率更高且延迟要求更严格。为应对这一挑战，我们利用了这样一个事实：底层 RocksDB 目录在任一时刻只会被一个进程访问和修改，因此元数据可以主动缓存。通过缓存，我们几乎绕过了所有元数据查找操作。由于目录始终只被一个进程访问和修改，结合 §4.3 的 IO Fencing 方法，该缓存始终保持一致。</p><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212519-4.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-2.png"><br>图 4. 在 ZippyDB 中启用辅助缓存之前&#x2F;之后，每个 ZippyDB 读取查询的 SST 文件读取图。</p><h4 id="4-1-3-RocksDB-本地闪存缓存。"><a href="#4-1-3-RocksDB-本地闪存缓存。" class="headerlink" title="4.1.3 RocksDB 本地闪存缓存。"></a>4.1.3 RocksDB 本地闪存缓存。</h4><p>为让部分读密集型应用能在 Tectonic 上使用 RocksDB，我们实现了基于非易失性介质（如本地闪存设备或 NVM&#x2F;SCM）的块缓存。它可视为 RocksDB 现有块缓存（基于 DRAM）的扩展。非易失性块缓存作为二级缓存，存放从主缓存驱逐的块。这些块在访问变热时会被提升回主缓存。官方称该缓存为 SecondaryCache，内部基于 Cachelib [14][17] 实现。</p><p>在生产环境下，ZippyDB 启用二级缓存后，读 IOPS 提升 50-60%（见图 4），ZippyDB 层面的读延迟降低 30-40%（见图 5）。缓存配置为 20GB 主缓存和 100GB 二级缓存。</p><blockquote><p>该优化已完全开源，包括基于 cachelib 的 SecondaryCache 插件 [2]</p></blockquote><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212520-1.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-2.png"><br>图 5. ZippyDB 启用二级缓存前后的平均读延迟。</p><h4 id="4-1-4-RocksDB-IO-处理。"><a href="#4-1-4-RocksDB-IO-处理。" class="headerlink" title="4.1.4 RocksDB IO 处理。"></a>4.1.4 RocksDB IO 处理。</h4><p>由于 Tectonic 的 IO 特性与本地 SSD 不同，我们思考是否应调整 RocksDB 的 IO 策略。我们发现 Tectonic 在 IO 特性上与 HDD 有相似之处，如读写延迟更高、偏好大块写入。通过调整 RocksDB 针对 HDD 的相关参数，在 Tectonic 上的性能也大幅提升。</p><p>RocksDB 在合并路径上提供了 IO 调整的灵活性，因为合并读写 IO 都是顺序的，缓冲区大小可调。在 Tectonic 上运行时，通常将合并读取大小设为 4MB 或 8MB，合并写缓冲区设为 64MB 或更大，应用即可获得满意性能。合并延迟仍可能增加——幸运的是，RocksDB 支持并行 memtable 刷新和合并，可帮助吸收延迟。</p><p>虽然调整现有参数能解决许多性能问题，但我们仍观察到某些需要大量 IO 的操作存在长查询延迟。举例：1）用户用 MultiGet() 读取过多键，可通过并行 IO（§4.1.5）缓解；2）迭代器读取过多连续数据块。RocksDB 通过预读减少迭代器路径上的 IO，预读有两种模式：固定配置和自适应。HDD 用户通常用固定预读，但在 Tectonic 上会导致过多网络带宽消耗。自适应模式从读取一个块开始，不断翻倍直到最大 256KB。但这种方式在 Tectonic 上预热太慢。为此，我们让 RocksDB 基于历史统计设置初始预读大小，并使最大值可配置。</p><h4 id="4-1-5-RocksDB-并行-IO。"><a href="#4-1-5-RocksDB-并行-IO。" class="headerlink" title="4.1.5 RocksDB 并行 IO。"></a>4.1.5 RocksDB 并行 IO。</h4><p>即使 Tectonic 单次读取的平均延迟只比本地 SSD 高几百微秒，若一次查询需多次 IO，累计延迟差距会很大。这常见于应用用 MultiGet() 一次读取多个键。为此，我们优化了 IO 密集型 MultiGet 的延迟：对同一 SST 文件的多个键，数据块读取可并行发起 <sup>3</sup> 。这对 Tectonic 尤为重要，因为远程存储读取延迟更高。IO 并行化会提升 Tectonic 客户端 IO 路径的 CPU 占用，微基准测试显示提升达 50%，但绝对值很小，因为 IO 仅占 MultiGet 总处理的一小部分。</p><h4 id="4-1-6-RocksDB-合并调优。"><a href="#4-1-6-RocksDB-合并调优。" class="headerlink" title="4.1.6 RocksDB 合并调优。"></a>4.1.6 RocksDB 合并调优。</h4><p>Tectonic 的读写 IOPS 较低，会限制 RocksDB 的读 QPS 和合并吞吐。此外，数据冗余也占用更多空间。</p><p>我们未观察到瓶颈转移的统一模式，主要取决于工作负载。若用户在迁移到 Tectonic 后发现瓶颈变化，可据此调整 RocksDB 合并策略。有趣的是，极少用户在迁移后需要更改合并策略，或许说明瓶颈通常不会因迁移而变化。</p><p>一个可能影响 Tectonic 性能的合并参数是目标 SST 文件大小。本地文件通常设为 32MB-256MB。直观上，SST 文件太小会导致文件过多，可能拖慢数据库打开等操作。根据我们的经验，除非目标 SST 文件小于 64MB，否则对性能影响很小。</p><blockquote><p><sup>3</sup> 本节中提到的改进都在开源 RocksDB 中，除了文件异步 IO 的存储特定实现</p></blockquote><h3 id="4-2-低开销冗余"><a href="#4-2-低开销冗余" class="headerlink" title="4.2 低开销冗余"></a>4.2 低开销冗余</h3><p>存算分离方案需以低开销实现冗余（§3.2.2）。Tectonic 提供多种编码方案，具备不同的持久性和可用性保障。RocksDB 用户可为不同文件类型选择合适的编码方案。</p><p>SST 文件需高持久性且低开销编码，因为它们占用大部分空间和写带宽。我们为 SST 文件选择 [12,8] 编码（8 数据块 +4 校验块），只需 1.5 倍空间和带宽开销，且能适配 6 或 12 个故障域的部署，提供高持久性 SLA。</p><p>WAL 及其他日志文件需支持小块（子块）追加的低尾延迟持久化。我们为 WAL 及日志文件采用 5 副本（R5）编码，原因如下：1）副本编码对小写入无 RS 编码开销，尾延迟更优；2）与 RS 编码不同，副本编码无需写入对齐或填充；3）R5 足以满足主机失效概率下的可用性需求。</p><p>对于日志更新频繁、R5 带来 5 倍网络开销过高的场景，Tectonic 侧增加了条带化 RS 编码 [12] 的小块追加持久化支持。条带化编码需收集一条带（或多条带）数据后再刷新。例如 [12,8] 编码 +8KB 条带时，将 8KB 数据分成 8 个 1KB 数据块，生成 4 个 1KB 校验块，共 12 个 1KB 块分发到 12 个存储节点。每个节点将 1KB 块追加到对应的 XFS 文件（通常为 8MB）。条带大小按文件类型预设。偶尔需刷新非对齐数据时，会用零填充对齐后编码并刷新。条带较小会降低随机读效率，因为需从多个节点组装并解码，但日志文件几乎总是顺序读取，因此该方案适用。</p><p>少数场景下，我们用更高开销的纠删码配合对冲技术（§4.1.1）降低尾延迟。</p><h3 id="4-3-多写者下的数据完整性"><a href="#4-3-多写者下的数据完整性" class="headerlink" title="4.3 多写者下的数据完整性"></a>4.3 多写者下的数据完整性</h3><p>为解决 §3.2.3 所述多写者导致数据损坏问题，我们实现了协作式 IO Fencing 协议，将前一节点的写操作隔离开来，原理类似于单调递增 token 的分布式锁 [4]。</p><p>我们要求试图 “ 拥有 “ 某 RocksDB 目录的进程，必须先用一个 token（变长字节串）对目录进行 IO Fence，后续对该目录及其下文件的所有操作都需携带该 token。只有 token 字典序大于此前任意进程用过的 token，fence 才会成功。Tectonic 内部会依次完成以下步骤以保证 fence：首先在元数据系统中更新目录 token（前提是新 token 更大），这样可阻止持有旧 token 的进程对文件进行新建、重命名、删除等变更。随后，Tectonic 遍历目录下所有可变（未封存）文件，对每个文件（a）更新元数据 token，（b）通过连接存储节点封存文件尾部可写块。步骤（b）可迫使旧写者与元数据系统同步，从而获知自身已过期并放弃重试。若任一步骤因 token 被更大 token 取代而失败，则本次 fence 失败，进程不得尝试以写模式打开 RocksDB。</p><h3 id="4-4-适配远程调用"><a href="#4-4-适配远程调用" class="headerlink" title="4.4 适配远程调用"></a>4.4 适配远程调用</h3><p>RocksDB 库本身需适配远程 IO 调用，其行为可能与本地 IO 不同（§3.2.4）。RocksDB 内部所有变更均已开源，但需针对不同存储实现专用插件（我们内部实现为 Tectonic）。</p><h4 id="4-4-1-区分-IO-超时"><a href="#4-4-1-区分-IO-超时" class="headerlink" title="4.4.1 区分 IO 超时"></a>4.4.1 区分 IO 超时</h4><p>远程 IO 操作可能因多种原因比本地慢。例如，瞬时故障重试后可恢复，这类 IO 可能需数秒才能成功。但对用户请求的 IO，等待数秒太久。RocksDB 应用通常要求查询在 1 秒内完成。若某 IO 需数秒，结果可能已无意义。相反，数据库内部操作（如合并、刷盘）发起的 IO 可容忍单次慢 IO，哪怕耗时数秒甚至数分钟。这类 IO 的故障处理更复杂，因此 Tectonic 可适当延长超时以避免失败。我们结论是，不同类型 IO 应设置不同超时。</p><p>我们为刷盘和合并等操作设置较长的超时时间，而对 Get() 或迭代器等操作则设置亚秒级超时。RocksDB 新增了可配置参数 request deadline，若请求超时则直接返回失败。用户设置的 deadline 会传递给 Tectonic。</p><h4 id="4-4-2-RocksDB-的故障处理"><a href="#4-4-2-RocksDB-的故障处理" class="headerlink" title="4.4.2 RocksDB 的故障处理"></a>4.4.2 RocksDB 的故障处理</h4><p>历史上，RocksDB 在 WAL 写入&#x2F;同步、后台刷盘和合并等关键数据库操作遇到 IO 错误时，会切换为只读模式，以保证数据库一致性。这与 Ext4、XFS 等本地文件系统的做法类似。</p><p>在 Tectonic 文件系统上读写数据可能遇到高延迟、瞬时写&#x2F;读失败、短时数据不可用，甚至系统级故障。与本地文件系统不同，这些情况发生频率更高，但往往是瞬时且可恢复的。我们意识到，若能合理分类错误并对瞬时错误做恢复，可大幅提升数据库可用性。为此，我们增强了文件系统 API 的返回状态，不仅包含错误码，还包括可重试性、作用域（文件级或全局）及是否永久丢失等元数据。Tectonic 可据此判断错误是否可恢复。RocksDB 侧则聚焦于在文件系统写入错误后恢复数据库一致性并恢复写入。</p><p>对于某些写入失败（如后台刷盘或合并），操作会自动重试且无用户停机。而 WAL 写入失败等情况，则需暂时停止写入，将 memtable 刷盘以保证一致性。</p><p>避免因 Tectonic 瞬时故障导致用户写入停机非常重要，因为写入失败对部分服务代价极高。例如，写入失败会导致 ZippyDB（基于 RocksDB 的应用）将受影响副本移出 Paxos 仲裁组，并重建新副本。</p><h4 id="4-4-3-IO-监控"><a href="#4-4-3-IO-监控" class="headerlink" title="4.4.3 IO 监控"></a>4.4.3 IO 监控</h4><p>随着存储栈变得更复杂，提升 IO 路径可观测性对故障排查尤为重要。用户常用本地文件系统的 IO 跟踪工具（如 strace），但 Tectonic 不支持这些工具。因此我们在 RocksDB 中开发了更多 IO 监控能力，使同一套工具可用于所有类型文件系统。</p><h4 id="4-4-4-工具支持"><a href="#4-4-4-工具支持" class="headerlink" title="4.4.4 工具支持"></a>4.4.4 工具支持</h4><p>RocksDB 有多种命令行工具，但原本只支持本地文件系统。这些工具用于检查数据库状态、SST 文件，有时还可帮助用户对数据库进行离线操作。用户在使用 Tectonic 时也需运行这些工具。同样，针对 Tectonic 运行基准测试、压力测试等工具也很有价值。我们在 RocksDB 中实现了通用 “ 对象注册表 “，维护对象名模式到工厂函数的映射，用于创建特定类型对象。若用户提供如 “tfs:&#x2F;&#x2F;cluster1&#x2F;db1” 的 Tectonic 集群 URL，RocksDB 会查表并用 Tectonic 插件对象与底层 Tectonic 文件系统通信。我们仅对开源代码做了极小改动，即可迁移现有工具和测试到 Tectonic，其他用户开发的文件系统插件也可用同样方式集成。</p><h2 id="5-性能基准"><a href="#5-性能基准" class="headerlink" title="5 性能基准"></a>5 性能基准</h2><h3 id="基准测试设置"><a href="#基准测试设置" class="headerlink" title="基准测试设置"></a>基准测试设置</h3><p>我们用 RocksDB 的微基准工具 db_bench 评估了 RocksDB+Tectonic 的性能。该工具的基准测试被 RocksDB 开发者、用户和硬件厂商广泛采用。我们采用 RocksDB 官方 wiki[10] 推荐的基准设置，验证每个 RocksDB 官方版本。基准测试用 20 字节键、400 字节值，值可压缩到原来一半。总共 10 亿个键，数据库物理占用约 200GB。块缓存设为 16GB，保证大多数查询至少需一次 IO。块大小 8KB，磁盘数据主要用 ZSTD 压缩，新数据用 LZ4。RocksDB 选项未针对 Tectonic 特别调优，仅调整 IO 相关参数。本地 SSD 场景用 direct IO，避免用 DRAM 做页缓存，未用本地闪存缓存。</p><p>所有实验均用 3 个 1.6GHz 物理核、64GB DRAM。我们在 Tectonic 上跑基准，并用本地 SSD 结果作参考。正如预期，本地 SSD 查询延迟更低、QPS 更高。我们仅将本地 SSD 结果作为参考，目标并非追平本地 SSD 性能。RocksDB 版本为 7.4，唯一非开源部分是让 RocksDB 支持 Tectonic 的存储接口实现。</p><h3 id="写入吞吐"><a href="#写入吞吐" class="headerlink" title="写入吞吐"></a>写入吞吐</h3><p>表 1 展示了顺序写和随机写基准结果。在 RocksDB 中，顺序写入的键会写入不同 SST 文件，这些文件无需合并。基准显示，RocksDB 在 Tectonic 上的顺序写吞吐与本地模式相当。随机写时，Tectonic 吞吐比本地低约 25%。虽然 Tectonic 能为多文件并发读写提供足够吞吐，但单文件处理速度有限，成为瓶颈。调优 RocksDB 合并参数可缓解，但为公平对比我们保留默认设置。</p><table><thead><tr><th>工作负载</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>顺序写入</td><td>262.4</td><td>264.1</td></tr><tr><td>随机写入</td><td>19.2</td><td>26</td></tr></tbody></table><p>表 1. 写入工作负载的 RocksDB 吞吐量（MB&#x2F;s）。</p><h3 id="读取性能"><a href="#读取性能" class="headerlink" title="读取性能"></a>读取性能</h3><p>我们做了三组读测试。第一组对随机键做 Get()，每次读通常对应一次底层 IO。单次查询延迟与 Tectonic、本地 SSD 的随机读延迟接近，Tectonic IO 延迟约为本地 SSD 的 5 倍。MultiGet 测试一次查询 10 个相近键，键间距为 32，确保分布在相近但非相邻数据块（相邻块会被 RocksDB 合并为一次 IO）。迭代器测试从随机位置读取 10 个键。MultiGet 和迭代器都可通过并行 IO（§4.1.5）获益。未用并行 IO 时，Tectonic 查询延迟约为本地 SSD 的 5 倍，启用并行 IO 可将差距缩小到 3 倍。</p><table><thead><tr><th></th><th>Threads</th><th>Tectonic(Parallel I&#x2F;O on)</th><th></th><th></th><th>Tectonic(Parallel I&#x2F;O off)</th><th></th><th></th><th>本地 SSD</th><th></th><th></th></tr></thead><tbody><tr><td></td><td></td><td>QPS</td><td>P50 延迟（ms）</td><td>P99 延迟 (ms)</td><td>QPS</td><td>P50 延迟（ms）</td><td>P99 延迟 (ms)</td><td>QPS</td><td>P50 延迟（ms）</td><td>P99 延迟 (ms)</td></tr><tr><td>Get</td><td>64</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>54.8K</td><td>1.1</td><td>2.9</td><td>334K</td><td>0.19</td><td>0.42</td></tr><tr><td></td><td>32</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>41K</td><td>0.74</td><td>1.6</td><td>208K</td><td>0.15</td><td>0.34</td></tr><tr><td></td><td>1</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>1.2K</td><td>9.76</td><td>2.0</td><td>7.2K</td><td>0.14</td><td>0.26</td></tr><tr><td>MultiGet</td><td>32</td><td>6.4K</td><td>4.51</td><td>13.23</td><td>5.8K</td><td>5.35</td><td>12.48</td><td>39.3K</td><td>0.79</td><td>1.51</td></tr><tr><td></td><td>16</td><td>5.5K</td><td>2.74</td><td>6.50</td><td>4.2K</td><td>3.77</td><td>6.54</td><td>23.1K</td><td>0.70</td><td>1.26</td></tr><tr><td></td><td>8</td><td>4.1K</td><td>1.88</td><td>3.67</td><td>2.4K</td><td>3.39</td><td>6.42</td><td>12.4K</td><td>0.66</td><td>1.20</td></tr><tr><td></td><td>1</td><td>0.6K</td><td>1.69</td><td>5.49</td><td>0.3K</td><td>3.42</td><td>9.06</td><td>1.7K</td><td>0.61</td><td>1.07</td></tr><tr><td>迭代器</td><td>32</td><td>14.2K</td><td>2.12</td><td>5.38</td><td>11.3K</td><td>2.77</td><td>6.38</td><td>70.2K</td><td>0.30</td><td>0.61</td></tr><tr><td></td><td>16</td><td>10.7K</td><td>1.33</td><td>2.88</td><td>6.4K</td><td>2.34</td><td>5.94</td><td>34.4K</td><td>0.26</td><td>0.57</td></tr><tr><td></td><td>8</td><td>6.1K</td><td>1.27</td><td>2.72</td><td>3.2K</td><td>2.38</td><td>6.01</td><td>15.7K</td><td>0.25</td><td>0.55</td></tr><tr><td></td><td>1</td><td>0.7K</td><td>1.29</td><td>3.24</td><td>0.3K</td><td>3.03</td><td>9.36</td><td>1.8K</td><td>0.25</td><td>0.54</td></tr></tbody></table><p>表 2. 读取工作负载的 RocksDB 基准测试结果。</p><p>所有读测试中，Tectonic 客户端允许的并发 IO 数有限，导致无法通过增加线程提升吞吐。该限制并非本质，可移除，但当前用户对现有吞吐已满意，且移除后需引入流控以保护 Tectonic 存储节点不被过载。</p><h2 id="6-应用实践：ZippyDB"><a href="#6-应用实践：ZippyDB" class="headerlink" title="6 应用实践：ZippyDB"></a>6 应用实践：ZippyDB</h2><p>我们已将多种原本用本地存储的 RocksDB 应用迁移到 Tectonic。我们发现，虽然 RocksDB on Tectonic 让应用更易采用分离式存储，但应用仍需应对一些常见生产挑战。</p><p>本节将以 ZippyDB 为例说明这些挑战。ZippyDB[15] [40] 是 Meta 原生构建的可靠、一致、高可用、可扩展的分布式键值存储服务，服务场景包括存储存储系统元数据、事件计数（内外部）、产品数据等。其底层用 Multi Paxos 做地理复制，存储引擎为 RocksDB。ZippyDB 采用分离式存储的动机包括：提升存储池利用率、加快故障切换&#x2F;下线速度、简化托管存储运维。</p><h3 id="6-1-处理非-RocksDB-文件"><a href="#6-1-处理非-RocksDB-文件" class="headerlink" title="6.1 处理非 RocksDB 文件"></a>6.1 处理非 RocksDB 文件</h3><p>与许多使用 RocksDB 的服务类似，ZippyDB 也会直接在文件系统上存储部分数据，如 MultiPaxos 协议的复制日志。虽然 Tectonic 可直接支持日志，但我们认为有必要为 Tectonic、RocksDB 及其应用（如 ZippyDB）提供统一的资源控制工具。例如，对所有后台任务做 IO 限流，控制最大 IO 大小、维护最大未刷脏数据量等。为解决这些问题，ZippyDB 将所有文件操作迁移到 RocksDB 的存储接口上，这样文件系统操作就能满足 RocksDB 的所有需求。该接口实现为本地文件系统和 Tectonic 的统一存储接口，因此 ZippyDB 可像 RocksDB 一样同时运行在本地文件系统和 Tectonic 上。</p><p>由于复制日志（rlog）写入发生在用户 IO 上下文中，在 Tectonic 上运行时，写入因网络通信带来更多开销。在本地文件系统上，ZippyDB 会先将 rlog 写入 OS 页缓存再确认，这种权衡在多副本主机并发内核崩溃（或主机故障）概率极低的前提下，能为客户端提供足够的性能和持久性。为优化 Tectonic 上的 rlog 写入，我们保持同样模型：先写入共享内存缓冲区再确认，后台异步将内核内存中的数据写入 Tectonic。我们对这类数据采用条带化纠删码（§4.2）的小块追加，以低开销实现高可用。这样，用户写入延迟不受 Tectonic 影响，Tectonic IO 次数减少，网络放大也最小化。</p><h3 id="6-2-构建新副本"><a href="#6-2-构建新副本" class="headerlink" title="6.2 构建新副本"></a>6.2 构建新副本</h3><p>与所有数据服务类似，ZippyDB 的副本需在多种场景下重建。常见场景是主机故障。在本地文件系统下，主机故障时需从健康副本（通常在其他区域）复制数据库持久状态（所有 DB 文件）以构建新副本。在复制完成前，系统处于降级副本状态，因此重建新副本所需时间直接影响整体可用性——此期间若再有副本失效，Paxos 仲裁组（如三副本）节点数不足，服务可用性受损。Tectonic 下，主机故障需重建副本时，ZippyDB 会将失效副本用到的所有文件复制到新数据目录，新计算节点从该目录打开 DB。得益于 Tectonic 的快速文件复制操作（新文件元数据指向原物理数据），该过程极快。负载均衡等场景下重建同区域副本也采用类似流程，旧副本在快速复制成功后销毁。Tectonic 让 ZippyDB 将同区域副本重建时长从约 50 分钟缩短到 1 分钟以内。</p><p>跨区域负载均衡、集群下线等场景仍需数据复制，此时无法用 Tectonic 快速复制。可用 RocksDB 的统一存储接口像本地文件系统一样复制，或用 Tectonic 提供的跨集群文件复制工具，保证用合适的网络和 IO 优先级完成。</p><h3 id="6-3-正确性与性能验证"><a href="#6-3-正确性与性能验证" class="headerlink" title="6.3 正确性与性能验证"></a>6.3 正确性与性能验证</h3><p>应用通常希望在全面迁移到 Tectonic 前验证数据正确性和性能。为保证平滑迁移且不影响可靠性，我们设计了可快速回退到迁移前状态的迁移方案。为此，我们基于 RocksDB 存储接口实现了镜像文件系统。该实现底层用两个存储系统，一个作为真实源，处理所有 IO 请求，另一个异步镜像第一个的更新。</p><p><strong>正向镜像</strong>：此模式下，ZippyDB 继续用本地闪存为真实源，同时在 Tectonic HDD 上维护异步副本。这样，ZippyDB 和 Tectonic 可在低风险&#x2F;低成本下互相熟悉。</p><p><strong>基于镜像的恢复</strong>：当使用镜像的 DB 实例切换到其他服务器时，我们用 Tectonic 的异步副本初始化 DB。由于 DB 状态可能与异步副本不一致，我们在 RocksDB 中实现了 “ 尽力恢复 “ 模式，可将 DB 恢复到异步副本的最近一致状态。我们在边从 Tectonic 拷贝数据到本地文件系统边启动 DB。</p><p><strong>反向镜像</strong>：迁移第二步（正向镜像后），ZippyDB 以 Tectonic 闪存为真实源，同时在本地闪存保留异步副本。新负载在此模式下运行数周，验证可靠性和性能后，最终切换为仅用 Tectonic 闪存。</p><h3 id="6-4-文件垃圾回收"><a href="#6-4-文件垃圾回收" class="headerlink" title="6.4 文件垃圾回收"></a>6.4 文件垃圾回收</h3><p>如用例删除、硬件故障等场景，若拥有 DB 实例的计算节点宕机&#x2F;不可达，可能在闪存上遗留 DB 实例存储。本地闪存下，节点回收时会清理这些状态；Tectonic 下，这些 DB 实例状态会一直遗留。我们增加了后台服务，通过比对 ZippyDB 活跃实例和 Tectonic 上所有实例，识别并清理无主 DB 实例。</p><h3 id="6-5-RocksDB-on-Tectonic-的收益"><a href="#6-5-RocksDB-on-Tectonic-的收益" class="headerlink" title="6.5 RocksDB on Tectonic 的收益"></a>6.5 RocksDB on Tectonic 的收益</h3><p>许多 ZippyDB 服务器因 §2.2 所述原因无法高效利用全部空间或 CPU，我们为提升效率做了多项努力。RocksDB on Tectonic 让我们实现了更高利用率。某 ZippyDB 集群用本地 SSD 时空间利用率为 35%，而用 Tectonic 时为 75%。即使考虑每字节存储的额外开销，依然实现了显著节省。存储可靠性也帮助我们缩短了硬件故障导致的服务不可用时间（§6.2）。实时监控显示，Tectonic 上 ZippyDB 可用性通常高于 99.99999%，本地 SSD 上则常见为 99.99993%（见图 6）。表 3 对比了两者部分指标。</p><table><thead><tr><th>生产指标</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>空间利用率</td><td>75%</td><td>35%</td></tr><tr><td>可用性</td><td>99.99999%</td><td>99.99993%</td></tr><tr><td>副本重建时间</td><td>&lt;1 分钟</td><td>~50 分钟</td></tr></tbody></table><p>表 3. 本地 SSD 和 Tectonic 上 ZippyDB 的比较。</p><p><img src="/images/disaggregating-rocksdb-a-production-experience-cn/%E8%AF%91%EF%BD%9CDisaggregating%20RocksDB%20A%20Production%20Experience-20250601212520-2.png" alt="译｜Disaggregating RocksDB A Production Experience-20250601212519-2.png"><br>图 6. ZippyDB 一周可用性比较。</p><h3 id="6-6-性能分析"><a href="#6-6-性能分析" class="headerlink" title="6.6 性能分析"></a>6.6 性能分析</h3><p>本次性能分析选取了 4 个基于 ZippyDB、每个存储至少数百 TB 数据的应用（见表 4），它们读写负载较轻，适合 Tectonic。每个用 3 副本，其中 1 个在 Tectonic，2 个在本地 SSD。主副本&#x2F;从副本无优先级，读流量均衡。这种设置便于对比两者性能。</p><table><thead><tr><th>Use Case</th><th>QPS Per TB</th><th></th><th>Bandwidth(MB) Per TB</th><th></th><th>工作负载描述</th></tr></thead><tbody><tr><td></td><td>Read</td><td>Write</td><td>Read</td><td>Write</td><td></td></tr><tr><td>1</td><td>14</td><td>548</td><td>0.4</td><td>0.37</td><td>存储推荐系统的不同受众的标志。读取通常使用迭代器完成。</td></tr><tr><td>2</td><td>33K</td><td>28</td><td>294</td><td>28.42</td><td>存储推荐系统的目标组的估计和统计信息。读取通常使用迭代器完成。</td></tr><tr><td>3</td><td>54</td><td>12</td><td>0.77</td><td>5.55</td><td>存储以多种方式折叠的内容的曝光、点击和其他指标，由推荐系统使用。读取由迭代器和 MultiGet() 完成，比率约为 6:1。</td></tr><tr><td>4</td><td>3K</td><td>1.7K</td><td>0.17</td><td>0.27</td><td>从多媒体中提取标志，用于 ML 模型。读取通常使用 MultiGet() 完成。</td></tr></tbody></table><p>表 4. 分析的 ZippyDB 用例摘要。</p><p>Tectonic 和 SSD 副本都运行在同类型服务器上的 RocksDB 实例，但配置略有不同。本地 SSD 主机不使用 direct IO，OS 页缓存命中率很高；Tectonic 不用 OS 页缓存，而用本地闪存缓存，容量约为块缓存 4 倍，仅占 DB 小部分。这样通常能减少 Tectonic 的 IO 次数。尽管配置不同，我们认为延迟数据能反映真实体验。</p><p>每个用例我们都收集了 ZippyDB 客户端的端到端延迟（见表 5-8），包括客户端计算、网络通信、服务器排队、RocksDB 延迟等。我们还展示了单次 RocksDB 操作延迟。需注意，ZippyDB 操作不一定直接映射到 RocksDB 操作，ZippyDB 代理层常将多个客户端请求合并为一次 RocksDB 操作，且代理层有小缓存，部分重复查询直接命中缓存。我们还展示了 Tectonic 和本地文件系统的小读 IO 延迟。许多本地文件系统读操作被 OS 页缓存命中，我们过滤了 30 微秒内完成的请求。</p><table><thead><tr><th>生产指标</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>端到端 MultiScan 延迟（P50，ms）</td><td>8.9</td><td>5.5</td></tr><tr><td>端到端 MultiScan 延迟（P99，ms）</td><td>49.6</td><td>40.0</td></tr><tr><td>端到端 Write 延迟（P50，ms）</td><td>103</td><td>114</td></tr><tr><td>端到端 Write 延迟（P99，ms）</td><td>876</td><td>766</td></tr><tr><td>RocksDB IteratorSeek 延迟（P50，ms）</td><td>0.44</td><td>0.33</td></tr><tr><td>RocksDB IteratorSeek 延迟（P99，ms）</td><td>6.6</td><td>3.9</td></tr><tr><td>平均从 SST 文件读取的块数量</td><td>0.077</td><td>0.857</td></tr><tr><td>文件系统小数据读取延迟（P50，us）</td><td>1325</td><td>388</td></tr><tr><td>文件系统小数据读取延迟（P99，us）</td><td>5220</td><td>2330</td></tr></tbody></table><p>表 5. ZippyDB 用例 1 的性能分析</p><table><thead><tr><th>指标</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>端到端 MultiScan 延迟（P50，ms）</td><td>1.5</td><td>1.4</td></tr><tr><td>端到端 MultiScan 延迟（P99，ms）</td><td>6.5</td><td>7.4</td></tr><tr><td>端到端 Write 延迟（P50，ms）</td><td>44</td><td>36</td></tr><tr><td>端到端 Write 延迟（P99，ms）</td><td>180</td><td>145</td></tr><tr><td>RocksDB IteratorSeek 延迟（P50，ms）</td><td>0.12</td><td>0.11</td></tr><tr><td>RocksDB IteratorSeek 延迟（P99，ms）</td><td>2.3</td><td>1.1</td></tr><tr><td>平均从 SST 文件读取的块数量</td><td>0.03</td><td>0.06</td></tr><tr><td>文件系统小数据读取延迟（P50，us）</td><td>1382</td><td>346</td></tr><tr><td>文件系统小数据读取延迟（P99，us）</td><td>8943</td><td>1993</td></tr></tbody></table><p>表 6. ZippyDB 用例 2 的性能分析</p><table><thead><tr><th>指标</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>端到端 Iterator 延迟（P50，ms）</td><td>10.4</td><td>5.8</td></tr><tr><td>端到端 Iterator 延迟（P99，ms）</td><td>74</td><td>50</td></tr><tr><td>端到端 MultiGet 延迟（P50，ms）</td><td>3.4</td><td>2.1</td></tr><tr><td>端到端 MultiGet 延迟（P90，ms）</td><td>23</td><td>16</td></tr><tr><td>端到端 Write 延迟（P50，ms）</td><td>34</td><td>46</td></tr><tr><td>端到端 Write 延迟（P99，ms）</td><td>63</td><td>72</td></tr><tr><td>RocksDB IteratorSeek 延迟（P50，ms）</td><td>0.89</td><td>0.99</td></tr><tr><td>RocksDB IteratorSeek 延迟（P99，ms）</td><td>17.5</td><td>7.4</td></tr><tr><td>RocksDB MultiGet 延迟（P50，ms）</td><td>1.6</td><td>1.2</td></tr><tr><td>RocksDB  延迟（P99，ms）</td><td>21</td><td>24</td></tr><tr><td>IteratorSeek 平均从 SST 文件读取的块数量</td><td>0.57</td><td>0.71</td></tr><tr><td>MultiGet 平均从 SST 文件读取的块数量</td><td>1.4</td><td>2.7</td></tr><tr><td>文件系统小数据读取延迟（P50，us）</td><td>1019</td><td>325</td></tr><tr><td>文件系统小数据读取延迟（P99，us）</td><td>4593</td><td>4806</td></tr></tbody></table><p>表 7. ZippyDB 用例 3 的性能分析</p><table><thead><tr><th>指标</th><th>Tectonic</th><th>本地 SSD</th></tr></thead><tbody><tr><td>端到端 MultiGet 延迟（P50，ms）</td><td>0.754</td><td>5.7</td></tr><tr><td>端到端 MultiGet 延迟（P99，ms）</td><td>60</td><td>132</td></tr><tr><td>端到端 Write 延迟（P50，ms）</td><td>129</td><td>137</td></tr><tr><td>端到端 Write 延迟（P99，ms）</td><td>235</td><td>265</td></tr><tr><td>RocksDB IteratorSeek 延迟（P50，ms）</td><td>0.098</td><td>0.11</td></tr><tr><td>RocksDB IteratorSeek 延迟（P99，ms）</td><td>3.1</td><td>1.5</td></tr><tr><td>平均从 SST 文件读取的块数量</td><td>0.17</td><td>0.25</td></tr><tr><td>文件系统小数据读取延迟（P50，us）</td><td>1080</td><td>346</td></tr><tr><td>文件系统小数据读取延迟（P99，us）</td><td>3154</td><td>2432</td></tr></tbody></table><p>表 8. ZippyDB 用例 4 的性能分析 </p><p>所有用例中，端到端写入延迟无明显变化，</p><p>这是因为地理复制操作主导了延迟，这部分不会因 Tectonic 而变化。端到端读延迟在 Tectonic 上有时更高，但 P99（ZippyDB 用户最关心的指标）差距通常较小。幸运的是，这些 ZippyDB 用户可以接受性能倒退。Tectonic 的读延迟在这些服务中相对稳定，P50 读延迟为 1-1.3ms，是本地 SSD 的数倍，P99 为数毫秒。部分用例中，Tectonic 和本地 SSD 的 P99 延迟接近，另一些则差距较大。</p><p>在用例 1（表 5）和用例 2（表 6）中，平均 RocksDB 读查询无需 IO，仅需亚毫秒。但用例 1 中，ZippyDB 用户发起 MultiScan 命令，需多个迭代器，P50 延迟受影响。有趣的是，P99 延迟差距更小，可能因 Tectonic 的读对冲特性。用例 2 中，大多数 ZippyDB 请求对应单次 RocksDB 请求，延迟相当。用例 3（表 7）中，平均 RocksDB 查询需 IO，P99 端到端延迟提升 50%。</p><p>总体来看，虽然 Tectonic 单次读延迟通常是本地 SSD 的数倍，但对终端用户的影响远小于此，且对性能异常值影响更小。许多 ZippyDB 用户认为这种性能倒退是可接受的。</p><h3 id="7-持续工作与挑战"><a href="#7-持续工作与挑战" class="headerlink" title="7 持续工作与挑战"></a>7 持续工作与挑战</h3><p>本节介绍 RocksDB on Tectonic 方案的部分在研项目。这些都是开放性挑战，希望分享能激发社区进一步创新。</p><h4 id="7-1-次级实例"><a href="#7-1-次级实例" class="headerlink" title="7.1 次级实例"></a>7.1 次级实例</h4><p>计算 - 存储分离的一个好处是资源利用更高效。用户可灵活扩缩机器、存储空间、网络带宽等资源，以应对不同负载或同一负载的不同阶段。例如，读请求高峰时可增加机器共享底层数据，专门服务读请求；高峰过后可将这些机器释放。合并操作（compaction）对 CPU&#x2F;IO 消耗大，若与主服务同主机运行，易导致 SLA 违约，因此有了远程合并的需求。</p><p>为支持多个 RocksDB 实例访问共享数据，我们开发了 “ 次级实例 “ 支持。主实例和次级实例以单写多读模式运行，次级实例重放主实例生成的日志文件。</p><p>仍有一些开放挑战，如防止次级实例使用期间文件被删除，以及在次级实例中查找并应用最新更新等。</p><h4 id="7-2-远程合并"><a href="#7-2-远程合并" class="headerlink" title="7.2 远程合并"></a>7.2 远程合并</h4><p>合并操作会与主服务争抢 CPU&#x2F;IO 资源。远程合并将合并任务卸载到专用主机，Tectonic 文件系统让我们能构建统一的合并服务，为任意 RocksDB 数据库分发合并任务。这不仅提升了主服务的性能和可靠性，还能跨数据库统一调度和管理合并任务，这是本地合并无法实现的。我们也希望通过跨 DB 负载均衡合并任务，提升突发流量和倾斜负载下的吞吐。</p><p>我们在 ZippyDB 用例上测试了该特性。虽然 ZippyDB 主机只在同一区域用 Tectonic，但常分布于不同数据中心。Tectonic 集群通常部署在数据中心内，因此可通过让远程合并主机与 Tectonic 同地部署，节省跨数据中心网络。在测试中，我们节省了 50% 以上的跨数据中心 IO，平均合并时间缩短 20.4%。</p><p>远程合并在跨实例调度、优先级管理和用户插件支持等方面仍有挑战。</p><h4 id="7-3-分层存储"><a href="#7-3-分层存储" class="headerlink" title="7.3 分层存储"></a>7.3 分层存储</h4><p>闪存每字节成本和功耗远高于机械硬盘。将闪存（SSD）和机械硬盘（HDD）结合用于数据库有助于优化成本&#x2F;能效。RocksDB on Tectonic 文件系统（支持 HDD 和 SSD）为我们设计分层存储方案提供了机会：冷 SSTable 存 HDD，热数据存 SSD。我们实现了分离冷热数据到不同 SST 文件并分别放置到不同介质的方案。通过分析数据插入时间预测冷热效果不错，但更复杂的冷热预测仍有挑战。</p><h3 id="8-经验与反思"><a href="#8-经验与反思" class="headerlink" title="8 经验与反思"></a>8 经验与反思</h3><p><strong>RocksDB 的通用性。</strong> RocksDB 被 Meta 内外多种应用广泛采用。我们发现 RocksDB on Tectonic 方案足够通用，适合广泛服务。例如，我们的数据仓库索引服务为 HIVE 表内容提供低延迟查找。数据仓库表每日刷新时，索引服务的 RocksDB 实例会在短时间内被大量加载，这种访问模式会导致 Tectonic 文件打开请求激增。我们通过减少 SST 文件数来减少元数据调用次数。我们还实现了基于 RocksDB 的 FIFO 缓存服务的存算分离，最老的 SST 文件直接删除而非合并。</p><p><strong>底层 DFS 的经验。</strong> Tectonic 最初为机械硬盘服务数据仓库和大对象存储而建。转型服务 SSD 场景下的 RocksDB 时，我们发现以下特性尤为重要：a）支持应用进程独占写目录（§4.3 IO Fencing）；b）支持可配置副本方案（§4.2）；c）提供满意的性能，尤其是尾延迟（§3.2.1、§4.1）；d）高效支持小写入，RocksDB 通常以几 KB 为单位追加 WAL 文件且需持久化（§4.2）。</p><p><strong>RocksDB 应用需做的工作。</strong> RocksDB 应用需做一定改造以适配 RocksDB on Tectonic，包括处理非 RocksDB 文件（§6.1）、构建新副本（§6.2）、服务质量验证（§6.3）。</p><h3 id="9-相关工作"><a href="#9-相关工作" class="headerlink" title="9 相关工作"></a>9 相关工作</h3><p>我们对 RocksDB on Tectonic 的探索建立在前人研究基础上，受益于计算 - 存储分离提升弹性和成本效率的观察。以往分布式文件系统和数据库（尤其是 LSM-tree 结构）设计与实现的经验为我们提供了宝贵启示。</p><p><strong>计算 - 存储分离。</strong> 近期研究 [27,29,31,32] 探讨了存储、内存和网络的设计，以实现资源分离满足不同应用需求。研究者将资源分离思想应用于操作系统 [38]、文件系统 [18]、大对象存储 [33]、分析 [19,37]、数据仓库 [42] 等领域。</p><p><strong>分布式文件系统。</strong> 分离式存储的虚拟化有多种方式，暴露给上层程序的接口也因部署规模、网络拓扑和应用需求而异 [1,3,5,7,11,28,30,33,35]。关于哪种接口最优的讨论超出本文范围。</p><p>分布式文件系统是集群环境下管理分离式存储的常用接口 [3,5,7,28,35]。部分文件系统兼容 POSIX[3,5]，但也有不少分布式文件系统只支持或优化部分文件操作（如 GFS[28] 和 HDFS[7] 都假定覆盖写很少）。Tectonic 也有类似设计，只支持文件追加。追加写语义与 LSM 结构存储的访问模式高度契合。分布式文件系统社区为 LSM 存储做了大量优化。Hailstorm[18] 是专为 LSM 键值数据库设计和优化的轻量级机架级远程文件系统。</p><p><strong>数据库与分离式存储。</strong> 已有多种分布式数据库系统为分离式存储而设计 [6,20-22,24,39,41]，这些系统架构复杂，与作为库嵌入应用进程空间的 RocksDB 存储引擎不可直接类比。</p><p>BigTable[21] 及其开源实现 HBase[6] 是半结构化数据的分布式存储系统。BigTable 将数据划分为 tablet，每个 tablet 可由不同服务器托管。专用服务器（master）负责维护 tablet 到服务器的映射。BigTable 的设计与实现经验对我们后续在 Tectonic 文件系统上运行 RocksDB 有重要借鉴意义。Spanner[22] 是分布式数据库，数据存储在分布式文件系统 Colossus[23]（新一代 GFS[28]）上。Amazon Aurora[41] 是云原生关系数据库，数据库实例将 redo 处理卸载到多租户、可扩展的存储服务。Aurora 基于 MySQL[8]，将备份和 redo 恢复卸载到存储集群以摊薄成本。[41] 中对比了 Aurora 和运行在 EBS[1] 上的 MySQL。PolarDB[20] 用 RDMA[9] 连接分离式存储与计算节点。</p><p><strong>LSM-tree 与分离式存储。</strong> 日志结构合并树（LSM-tree）[34] 是分离式存储数据库常用的数据结构。BigTable、HBase 和 PolarDB 都将数据存为 SST 文件，因 SST 文件一旦写入即不可变，可被多个计算节点并发读取，即使作为合并输入也不影响。这与只支持追加写的分布式文件系统高度契合。</p><p>将合并操作卸载到分离式存储的思路也被其他系统探索过。[16] 提出将合并卸载集成到 HBase。Rockset 的 RocksDB-cloud[36] 是 RocksDB 的变体，支持将合并卸载到远程无状态服务器 [13]。RocksDB-cloud 在本地缓存 SST 和 WAL 文件，定期同步到云端。</p><h3 id="10-结论"><a href="#10-结论" class="headerlink" title="10 结论"></a>10 结论</h3><p>将数据库运行在直连 SSD 上可以获得更好的性能，但将数据存储在分离式存储上可能更高效且更易于管理。拥有一个能够同时支持本地和分离式存储的数据库存储引擎非常方便，我们的经验表明，通过有针对性的改进，这是完全可行的。我们通过扩展 RocksDB 支持 Tectonic 文件系统，实现了预期的效率提升。数据服务在生产中使用 RocksDB on Tectonic 也积累了许多经验。运行在分离式存储上还让 RocksDB 能够演进为更分布式的架构，我们也在持续探索这一方向。</p><h3 id="11-致谢"><a href="#11-致谢" class="headerlink" title="11 致谢"></a>11 致谢</h3><p>我们感谢 SIGMOD 评审专家的宝贵意见和建议，这些意见提升了本文的质量。我们还要感谢 Michael Stumm 教授提供的深刻反馈和润色，以及 Mark Callaghan 的有益建议。</p><p>RocksDB on Tectonic 项目及其在 ZippyDB 上的应用，离不开这些团队成员及众多合作团队的宝贵贡献。我们特别感谢 Dan Meredith、David Felty、Federico Piccinini、Giang Nguyen、Guna Lakshminarayanan、JR Tipton、Jennifer Chan、Joe Hirschfeld、Jorge Guerra、Junjie Wu、Junqing Deng、Kapil Kataria、Karthik Krishnamurthy、Lachlan Mulcahy、Lujin Luo、Madhu Anantha、Michael C Huang、Michael Meng、Mikhail Antonov、Murali Vilayannur、Naveen Ganapathi Subramanian、Nicholas Ormrod、Peter Dillinger、Pratap Singh、Ramkumar Vadivelu、Sachin Lakhanpal、Sai Bathina、Sankalp Kohli、Sarah Wang、Shrikanth Shankar、Shubham Singhal、Sorin Stoiana、Tejasvi Aswathanarayana、Tyler Heucke、Victoria Tsai、Xiaoyu Wang、Yunqiao Zhang、Zhichao Cao 以及所有为本项目做出重要贡献的同事。</p><h3 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h3><p>[1] [n.d.]. Amazon EBS. <a href="https://aws.amazon.com/ebs/">https://aws.amazon.com/ebs/.</a><br>[2] [n.d.]. Cachelib Repo. <a href="https://github.com/facebook/CacheLib">https://github.com/facebook/CacheLib.</a><br>[3] [n.d.]. Ceph File system. <a href="https://docs.ceph.com/en/pacific/cephfs/index.html">https://docs.ceph.com/en/pacific/cephfs/index.html.</a><br>[4] [n.d.]. Distributed locks with Redis. <a href="https://redis.io/topics/distlock">https://redis.io/topics/distlock.</a><br>[5] [n.d.]. GlusterFS. <a href="https://www.gluster.org/">https://www.gluster.org/.</a><br>[6] [n.d.]. Hbase. <a href="https://hbase.apache.org/">https://hbase.apache.org/.</a><br>[7] [n.d.]. HDFS. <a href="https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html">https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html.</a><br>[8] [n.d.]. MySQL. <a href="https://www.mysql.com/">https://www.mysql.com/.</a><br>[9] [n.d.]. RDMA. <a href="http://www.rdmaconsortium.org/">http://www.rdmaconsortium.org/.</a><br>[10] [n.d.]. RocksDB Benchmark Wiki Page. <a href="https://github.com/facebook/rocksdb/wiki/Performance-Benchmarks">https://github.com/facebook/rocksdb/wiki/Performance-Benchmarks.</a><br>[11] 2009. Rados. <a href="https://ceph.io/en/news/blog/2009/the-rados-distributed-object-store/">https://ceph.io/en/news/blog/2009/the-rados-distributed-object-store/.</a><br>[12] 2015. Introduction to HDFS Erasure Coding in Apache Hadoop. <a href="https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/">https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/.</a><br>[13] 2020. RocksDB-Cloud remote compaction. [<a href="https://rockset.com/blog/remote-compactions-in-rocksdb-cloud/.]">https://rockset.com/blog/remote-compactions-in-rocksdb-cloud/.]</a>(<a href="https://rockset.com/blog/remote-compactions-in-rocksdb-cloud/">https://rockset.com/blog/remote-compactions-in-rocksdb-cloud/</a><br>[14] 2021. Cachelib. <a href="https://engineering.fb.com/2021/09/02/open-source/cachelib/">https://engineering.fb.com/2021/09/02/open-source/cachelib/.</a><br>[15] 2021. How we built a general purpose key value store for Facebook with ZippyDB. <a href="https://engineering.fb.com/2021/08/06/core-data/zippydb/">https://engineering.fb.com/2021/08/06/core-data/zippydb/.</a><br>[16] Muhammad Yousuf Ahmad and Bettina Kemme. 2015. Compaction management in distributed key-value datastores. Proceedings of the VLDB Endowment 8, 8 (2015), 850–861.<br>[17] Benjamin Berg, Daniel S. Berger, Sara McAllister, Isaac Grosof, Sathya Gunasekar, Jimmy Lu, Michael Uhlar, Jim Carrig, Nathan Beckmann, Mor Harchol-Balter, and Gregory R. Ganger. 2020. The CacheLib Caching Engine: Design and Experiences at Scale. In 14th USENIX Symposium on Operating Systems Design and Implementation (OSDI 20). USENIX Association, 753–768. <a href="https://www.usenix.org/conference/osdi20/presentation/berg">https://www.usenix.org/conference/osdi20/presentation/berg</a><br>[18] Laurent Bindschaedler, Ashvin Goel, and Willy Zwaenepoel. 2020. Hailstorm: Disaggregated compute and storage for distributed lsm-based databases. In Proceedings of the Twenty-Fifth International Conference on Architectural Support for Programming Languages and Operating Systems. 301–316.<br>[19] Laurent Bindschaedler, Jasmina Malicevic, Nicolas Schiper, Ashvin Goel, and Willy Zwaenepoel. 2018. Rock You like a Hurricane: Taming Skew in Large Scale Analytics. In Proceedings of the Thirteenth EuroSys Conference (Porto, Portugal) (EuroSys ’18). Association for Computing Machinery, New York, NY, USA, Article 20, 15 pages. <a href="https://doi.org/10.1145/3190508.3190532">https://doi.org/10.1145/3190508.3190532</a><br>[20] Wei Cao, Yang Liu, Zhushi Cheng, Ning Zheng, Wei Li, Wenjie Wu, Linqiang Ouyang, Peng Wang, Yijing Wang, Ray Kuan, et al. 2020. {POLARDB} Meets Computational Storage: Eficiently Support Analytical Workloads in {Cloud-Native} Relational Database. In 18th USENIX Conference on File and Storage Technologies (FAST 20). 29–41.<br>[21] Fay Chang, Jeffrey Dean, Sanjay Ghemawat, Wilson C Hsieh, Deborah A Wallach, Mike Burrows, Tushar Chandra, Andrew Fikes, and Robert E Gruber. 2008. Bigtable: A distributed storage system for structured data. ACM Transactions on Computer Systems (TOCS) 26, 2 (2008), 1–26.<br>[22] James C Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, Jeffrey John Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, et al. 2013. Spanner: Google’s globally distributed database. ACM Transactions on Computer Systems (TOCS) 31, 3 (2013), 1–22.<br>[23] Jeffrey Dean. 2010. Evolution and future directions of large-scale storage and computation systems at Google. (2010).<br>[24] David DeWitt and Jim Gray. 1992. Parallel database systems: The future of high performance database systems. Commun. ACM 35, 6 (1992), 85–98.<br>[25] Siying Dong, Mark Callaghan, Leonidas Galanis, Dhruba Borthakur, Tony Savor, and Michael Strum. 2017. Optimizing Space Amplification in RocksDB.. In CIDR, Vol. 3. 3.<br>[26] Siying Dong, Andrew Kryczka, Yanqin Jin, and Michael Stumm. 2021. RocksDB: Evolution of Development Priorities in a Key-Value Store Serving Large-Scale Applications. ACM Trans. Storage 17, 4, Article 26 (oct 2021), 32 pages. <a href="https://doi.org/10.1145/3483840">https://doi.org/10.1145/3483840</a><br>[27] Peter X Gao, Akshay Narayan, Sagar Karandikar, Joao Carreira, Sangjin Han, Rachit Agarwal, Sylvia Ratnasamy, and Scott Shenker. 2016. Network requirements for resource disaggregation. In 12th USENIX symposium on operating systems design and implementation (OSDI 16). 249–264.<br>[28] Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung. 2003. The Google filesystem. In Proceedings of the nineteenth ACM symposium on Operating systems principles. 29–43.<br>[29] Zvika Guz, Harry Li, Anahita Shayesteh, and Vijay Balakrishnan. 2018. Performance characterization of nvme-over-fabrics storage disaggregation. ACM Transactions on Storage (TOS) 14, 4 (2018), 1–18.<br>[30] Dave Hitz, James Lau, and Michael A Malcolm. 1994. File System Design for an NFS File Server Appliance.. In USENIX winter, Vol. 94. 10–5555.<br>[31] Ana Klimovic, Christos Kozyrakis, Eno Thereska, Binu John, and Sanjeev Kumar. 2016. Flash storage disaggregation. In Proceedings of the Eleventh European Conference on Computer Systems. 1–15.<br>[32] Mihir Nanavati, Jake Wires, and Andrew Warfield. 2017. Decibel: Isolation and Sharing in Disaggregated {Rack-Scale} Storage. In 14th USENIX Symposium on Networked Systems Design and Implementation (NSDI 17). 17–33.<br>[33] Edmund B Nightingale, Jeremy Elson, Jinliang Fan, Owen Hofmann, Jon Howell, and Yutaka Suzue. 2012. Flat datacenter storage. In 10th USENIX Symposium on Operating Systems Design and Implementation (OSDI 12). 1–15.<br>[34] Patrick O’Neil, Edward Cheng, Dieter Gawlick, and Elizabeth O’Neil. 1996. The log-structured merge-tree (LSM-tree). Acta Informatica 33, 4 (1996), 351–385.<br>[35] Satadru Pan, Theano Stavrinos, Yunqiao Zhang, Atul Sikaria, Pavel Zakharov, Abhinav Sharma, Shiva Shankar P, Mike Shuey, Richard Wareing, Monika Gangapuram, Guanglei Cao, Christian Preseau, Pratap Singh, Kestutis Patiejunas, JR Tipton, Ethan Katz-Bassett, and Wyatt Lloyd. 2021. Facebook’s Tectonic Filesystem: Eficiency from Exascale. In 19th USENIX Conference on File and Storage Technologies (FAST 21). USENIX Association, 217–231. <a href="https://www.usenix.org/conference/fast21/presentation/pan">https://www.usenix.org/conference/fast21/presentation/pan</a><br>[36] Rockset.2018. RocksDBCloud. <a href="https://rockset.com/blog/rocksdb-cloud-enabling-the-next-generation-of-cloud-native-databases/">https://rockset.com/blog/rocksdb-cloud-enabling-the-next-generation-of-cloud-native-databases/.</a><br>[37] Amitabha Roy, Laurent Bindschaedler, Jasmina Malicevic, and Willy Zwaenepoel. 2015. Chaos: Scale-out graph processing from secondary storage. In Proceedings of the 25th Symposium on Operating Systems Principles. 410–424.<br>[38] Yizhou Shan, Yutong Huang, Yilun Chen, and Yiying Zhang. 2018. LegoOS: A Disseminated, Distributed OS for Hardware Resource Disaggregation. In 13th USENIX Symposium on Operating Systems Design and Implementation (OSDI 18). USENIX Association, Carlsbad, CA, 69–87. <a href="https://www.usenix.org/conference/osdi18/presentation/shan">https://www.usenix.org/conference/osdi18/presentation/shan</a><br>[39] Michael Stonebraker. 1986. The case for shared nothing. IEEE Database Eng. Bull. 9, 1 (1986), 4–9.<br>[40] Amy Tai, Andrew Kryczka, Shobhit Kanaujia, Chris Petersen, Mikhail Antonov, Muhammad Waliji, Kyle Jamieson, Michael J Freedman, and Asaf Cidon. 2018. Live recovery of bit corruptions in datacenter storage systems. arXiv preprint arXiv:1805.02790 (2018).<br>[41] Alexandre Verbitski, Anurag Gupta, Debanjan Saha, Murali Brahmadesam, Kamal Gupta, Raman Mittal, Sailesh Krishnamurthy, Sandor Maurice, Tengiz Kharatishvili, and Xiaofeng Bao. 2017. Amazon aurora: Design considerations for high throughput cloud-native relational databases. In Proceedings of the 2017 ACM International Conference on Management of Data. 1041–1052.<br>[42] Midhul Vuppalapati, Justin Miron, Rachit Agarwal, Dan Truong, Ashish Motivala, and Thierry Cruanes. 2020. Building an elastic query engine on disaggregated storage. In 17th USENIX Symposium on Networked Systems Design and Implementation (NSDI 20). 449–462.</p><blockquote><p>本文翻译自：</p><p>原文链接：<a href="https://dl.acm.org/doi/10.1145/3589772">Disaggregating RocksDB: A Production Experience</a></p><p>原文采用 <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> 授权发布。本文为中文翻译，仅用于学习与分享，版权归原作者所有。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/06-01-2025/disaggregating-rocksdb-a-production-experience-cn.html">https://www.cyningsun.com/06-01-2025/disaggregating-rocksdb-a-production-experience-cn.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://orcid.org/0000-0003-0576-2226&quot;&gt;SIYING DONG,&lt;/a&gt; &lt;a href=&quot;https://orcid.org/0009-0004-4528-3857&quot;&gt;SHIVA SHANKAR P,&lt;/a&gt; &lt;a </summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB Memtable Flush 机制</title>
    <link href="https://www.cyningsun.com/05-30-2025/rocksdb-memtable-flush.html"/>
    <id>https://www.cyningsun.com/05-30-2025/rocksdb-memtable-flush.html</id>
    <published>2025-05-29T16:00:00.000Z</published>
    <updated>2025-05-29T14:06:52.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、引言"><a href="#一、引言" class="headerlink" title="一、引言"></a>一、引言</h2><p>在 RocksDB 的核心机制中，Flush 操作扮演着至关重要的角色，它是连接内存数据结构 (Memtable) 和持久化存储 (SST 文件) 的桥梁。该机制不仅影响写入性能和内存使用效率，更直接关系到数据安全性和系统恢复速度。</p><p>本文将基于 RocksDB v8.8.1 详细介绍在未启用 <code>atomic_flush</code> 的情况下，深入解析 RocksDB 的 Flush 机制，包括触发条件、执行策略、相关配置，以及与之密切相关的 WAL（预写式日志）管理和恢复机制。不妨带着以下问题，来详细深入了解下具体的实现细节：</p><ol><li>为什么集群滚动升级会 Flush 生成很多 SST 文件，进而触发 compaction？</li><li>哪些情况会触发 Memtable Flush？</li><li>当 Memtable Flush 时，会选中哪些 Memtable？</li><li>多个列族 (Column Family) 会 Flush 到同一个 SST 文件么？</li><li>不活跃的列族会自动 Flush 么？会有什么影响？</li><li>Flush 完毕之后，WAL 是怎么处理的？WAL 什么时候会归档？什么时候会删除？</li><li>RocksDB 重启的时候，怎么确定从哪个 WAL 文件的哪个位置开始读取数据，恢复 Memtable？</li><li>如何加速恢复，降低恢复所需要的时长？几种恢复模式在数据丢失量和恢复速度上有何异同？</li><li>如果因数据同步需要调大 WAL 的保留时间，会增加异常重启恢复时间么？</li></ol><h2 id="二、基础概念"><a href="#二、基础概念" class="headerlink" title="二、基础概念"></a>二、基础概念</h2><h3 id="2-1-Memtable-的生命周期"><a href="#2-1-Memtable-的生命周期" class="headerlink" title="2.1 Memtable 的生命周期"></a>2.1 Memtable 的生命周期</h3><p>Memtable 是 RocksDB 的内存数据结构，用于存储最近写入的数据。它具有以下特点：</p><ol><li><p><strong>写入流程</strong>：当用户写入数据时，数据首先被写入预写式日志 (WAL) 用于崩溃恢复，然后被插入当前活跃的 Memtable。</p></li><li><p><strong>状态转换</strong>：Memtable 有三种状态：</p><ul><li>**活跃 (Active)**：接收新的写入请求</li><li>**不可变 (Immutable)**：不再接收新写入，等待刷新到存储</li><li>**已刷新 (Flushed)**：数据已持久化到 SST 文件，Memtable 可以被销毁</li></ul></li><li><p><strong>切换机制</strong>：当活跃 Memtable 达到一定大小 (<code>write_buffer_size</code>) 后，会被标记为不可变，并创建新的 Memtable 接收后续写入。</p></li></ol><p>Memtable 的实现通常基于跳表 (SkipList) 数据结构，保证了高效的随机写入和有序遍历能力。</p><h3 id="2-2-写入缓冲区机制"><a href="#2-2-写入缓冲区机制" class="headerlink" title="2.2 写入缓冲区机制"></a>2.2 写入缓冲区机制</h3><p>RocksDB 的写入缓冲区实现了高效的内存管理策略：</p><ol><li><strong>单 CF 写入缓冲区</strong>：每个列族 (Column Family) 配置有自己的 <code>write_buffer_size</code>，控制单个 Memtable 的大小。</li><li><strong>全局写入缓冲区</strong>：通过 <code>db_write_buffer_size</code> 参数限制所有列族的 Memtable 总内存占用。</li><li><strong>Memtable 数量控制</strong>：<ul><li><code>max_write_buffer_number</code>：每个 CF 允许的最大 Memtable 数量</li><li><code>min_write_buffer_number_to_merge</code>：刷新前合并的最小 Memtable 数量</li></ul></li></ol><p>当一个 Memtable 被标记为不可变后，RocksDB 会调度后台线程执行 Flush 操作，将其数据持久化到 SST 文件中。</p><h3 id="2-3-Flush-与-WAL-的关系"><a href="#2-3-Flush-与-WAL-的关系" class="headerlink" title="2.3 Flush 与 WAL 的关系"></a>2.3 Flush 与 WAL 的关系</h3><p>Flush 操作与 WAL(Write-Ahead Log) 密切相关：</p><ol><li><strong>数据安全保障</strong>：WAL 记录所有写操作，确保即使在内存数据 (Memtable) 丢失的情况下也能恢复数据。</li><li><strong>日志回收机制</strong>：只有当 WAL 中的所有数据都已通过 Flush 持久化到 SST 文件后，该 WAL 文件才可以被归档或删除。</li><li><strong>WAL 文件限制</strong>：<code>max_total_wal_size</code> 参数控制 WAL 文件的总大小，超过限制会触发 Flush 以减小 WAL 占用。</li></ol><h3 id="2-4-相关配置"><a href="#2-4-相关配置" class="headerlink" title="2.4 相关配置"></a>2.4 相关配置</h3><p>RocksDB 提供了多种参数用于配置 Memtable 和 Flush 行为：</p><pre><code class="hljs cpp"><span class="hljs-comment">// DBOptions（数据库级别选项）</span><span class="hljs-keyword">struct</span> <span class="hljs-title class_">DBOptions</span> &#123;  <span class="hljs-comment">// ... 其他选项 ...</span>  <span class="hljs-comment">// 总写入缓存大小。所有列族共享的写缓冲区总大小 (字节)</span>  <span class="hljs-comment">// 所有列族共享的写入缓存（MemTable）的总大小。当所有 MemTable 的总大小超过这个值时，RocksDB 会触发一个列族的刷新操作，通常是最大的 MemTable 所在的列族</span>  <span class="hljs-comment">// 控制 RocksDB 实例的整体内存使用量。更大的值可以提高写入吞吐量，但会增加内存占用</span>  <span class="hljs-type">size_t</span> db_write_buffer_size = <span class="hljs-number">0</span>;    <span class="hljs-comment">// 最大后台刷新线程数。用于执行刷新操作的后台线程的最大数量</span>  <span class="hljs-comment">// 控制刷新操作的并发度。增加此值可以提高刷新吞吐量，尤其是在有多个列族的情况下，但也可能增加资源竞争</span>  <span class="hljs-type">int</span> max_background_flushes = <span class="hljs-number">1</span>;  <span class="hljs-comment">// 是否避免不必要的阻塞 I/O。如果设置为 true，则工作线程可能会避免执行不必要的、长时间的 I/O 操作（例如直接删除过时的文件或删除 MemTable），而是安排一个后台任务来执行</span>  <span class="hljs-comment">// 提高延迟敏感型应用的性能，将潜在的阻塞操作卸载到后台线程</span>  <span class="hljs-type">bool</span> avoid_unnecessary_blocking_io = <span class="hljs-literal">true</span>;    <span class="hljs-comment">// 是否原子刷新，如果设置为 true，RocksDB 支持原子地刷新多个列族，并将它们的结果原子地提交到 MANIFEST 文件</span>  <span class="hljs-comment">// 确保跨多个列族的数据一致性。如果某些列族的数据写入没有受到 WAL 保护，这个选项就很有用</span>  <span class="hljs-type">bool</span> atomic_flush = <span class="hljs-literal">false</span>;  <span class="hljs-comment">// 是否手动刷新 WAL。如果设置为 true，则在每次写入后不会自动刷新 WAL（Write-Ahead Log）</span>  <span class="hljs-comment">// 禁用自动 WAL 刷新，需要手动调用 `SyncWAL()` 来刷新 WAL。这可以提高写入性能，但会增加数据丢失的风险</span>  <span class="hljs-type">bool</span> manual_wal_flush = <span class="hljs-literal">false</span>;  <span class="hljs-comment">// 活跃的 WAL 文件总大小的最大值 (字节)。当总大小超过此值时，RocksDB 将开始刷新列族以减小活跃的 WAL 大小</span>  <span class="hljs-comment">// 实时控制活跃 WAL 文件的总大小，超过限制时强制刷新 Memtable 以减少 WAL 依赖</span>  <span class="hljs-type">uint64_t</span> max_total_wal_size = <span class="hljs-number">0</span>;  <span class="hljs-comment">// 不活跃的 WAL 文件总大小。以下两个字段影响归档 WAL 删除方式，防止历史 WAL 文件占用过多磁盘空间</span>  <span class="hljs-comment">// 如果均为 0，则 WAL 立刻删除不会归档</span>  <span class="hljs-comment">// 如果 WAL_ttl_seconds 为 0，且 WAL_size_limit_MB 不为 0，则每十分钟检查一次，删除超过大小限制的 WAL，从最旧的 WAL 开始</span>  <span class="hljs-comment">// 如果 WAL_ttl_seconds 不为 0，且 WAL_size_limit_MB 为 0，则每 WAL_ttl_seconds / 2 检查一次，删除超过时间限制的 WAL</span>  <span class="hljs-comment">// 如果两者均不为 0，则每十分钟检查一次，先检查时间限制，再检查大小限制</span>  <span class="hljs-type">uint64_t</span> WAL_ttl_seconds = <span class="hljs-number">0</span>;  <span class="hljs-type">uint64_t</span> WAL_size_limit_MB = <span class="hljs-number">0</span>;  <span class="hljs-comment">// 用户定义的事件监听器列表</span>  <span class="hljs-comment">// 监听器可以接收刷新开始和刷新完成事件的通知，允许用户监控和响应刷新活动</span>  std::vector&lt;std::shared_ptr&lt;EventListener&gt;&gt; listeners;   <span class="hljs-comment">// ... 其他选项 ...</span>&#125;;<span class="hljs-comment">// ColumnFamilyOptions（列族级别选项）</span><span class="hljs-keyword">struct</span> <span class="hljs-title class_">ColumnFamilyOptions</span> &#123;  <span class="hljs-comment">// ... 其他选项 ...</span>  <span class="hljs-comment">// 每个 MemTable 的大小 (字节)。一旦 MemTable 达到此大小，它将被标记为不可变，并触发刷新</span>  <span class="hljs-comment">// 控制每个列族的内存使用量和刷新频率。更大的值会降低刷新频率，但会增加内存使用量</span>  <span class="hljs-type">size_t</span> write_buffer_size = <span class="hljs-number">64</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>;   <span class="hljs-comment">// 内存中 MemTable 的最大数量。在阻止写入之前，内存中要保留的最大 MemTable 数量</span>  <span class="hljs-comment">// 限制未刷新的 MemTable 的数量。达到此限制时，写入将被暂停，直到刷新完成</span>  <span class="hljs-type">int</span> max_write_buffer_number = <span class="hljs-number">2</span>;  <span class="hljs-comment">// 刷新前要合并的最小 MemTable 数量。在刷新到存储之前要合并的最小 MemTable 数量</span>  <span class="hljs-comment">// 控制刷新期间合并到单个 SST 文件中的 MemTable 数量。更大的值可以减少 SST 文件的数量，但可能会增加刷新延迟</span>  <span class="hljs-type">int</span> min_write_buffer_number_to_merge = <span class="hljs-number">1</span>;  <span class="hljs-comment">// 刷新时是否验证 MemTable 计数。验证 MemTable 中的条目数是否与刷新期间读取的条目数匹配</span>  <span class="hljs-comment">// 启用刷新期间 MemTable 计数的验证</span>  <span class="hljs-type">bool</span> flush_verify_memtable_count = <span class="hljs-literal">false</span>;   <span class="hljs-comment">// 实验性 MemPurge 阈值。触发 MemPurge 的阈值</span>  <span class="hljs-comment">// 如果设置为 &gt;0.0，则所有自动刷新操作将首先通过 MemPurge 过程</span>  <span class="hljs-type">double</span> experimental_mempurge_threshold = <span class="hljs-number">0.0</span>;  <span class="hljs-comment">// ... 其他选项 ...</span>&#125;;<span class="hljs-comment">// FlushOptions（传递给Flush API调用的选项）</span><span class="hljs-keyword">struct</span> <span class="hljs-title class_">FlushOptions</span> &#123;  <span class="hljs-comment">// 是否等待刷新完成。如果为 true，则刷新操作将阻塞，直到完成。如果为 false，则刷新是异步的</span>  <span class="hljs-comment">// 确定 `Flush()` 调用是同步还是异步</span>  <span class="hljs-type">bool</span> wait = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 是否允许刷新导致写入暂停。如果为 true，即使这意味着写入将在刷新期间暂停，刷新操作也会立即进行</span>  <span class="hljs-comment">// 允许刷新继续进行，即使它会导致写入暂停</span>  <span class="hljs-type">bool</span> allow_write_stall = <span class="hljs-literal">false</span>; &#125;;</code></pre><h2 id="三、Flush-触发机制"><a href="#三、Flush-触发机制" class="headerlink" title="三、Flush 触发机制"></a>三、Flush 触发机制</h2><p>RocksDB 中的 Flush 操作由多种条件触发，可分为自动触发、手动触发和系统状态变更触发三类。</p><h3 id="3-1-自动触发条件"><a href="#3-1-自动触发条件" class="headerlink" title="3.1 自动触发条件"></a>3.1 自动触发条件</h3><h4 id="3-1-1-单个-Memtable-大小达到阈值"><a href="#3-1-1-单个-Memtable-大小达到阈值" class="headerlink" title="3.1.1 单个 Memtable 大小达到阈值"></a>3.1.1 单个 Memtable 大小达到阈值</h4><p>当单个 Memtable 的大小达到 <code>write_buffer_size</code> 配置值时，会触发 Flush：</p><pre><code class="hljs cpp"><span class="hljs-comment">// memtable.cc</span><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">MemTable::ShouldFlushNow</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-comment">// if user keeps adding entries that exceeds write_buffer_size, we need to</span>  <span class="hljs-comment">// flush earlier even though we still have much available memory left.</span>  <span class="hljs-keyword">if</span> (allocated_memory &gt;      write_buffer_size + kArenaBlockSize * kAllowOverAllocationRatio) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><p>当 Memtable 达到阈值后，系统会将其标记为不可变，并创建新的 Memtable 接收后续写入，同时安排后台任务执行实际的 Flush 操作。</p><h4 id="3-1-2-总写入缓冲区大小超限"><a href="#3-1-2-总写入缓冲区大小超限" class="headerlink" title="3.1.2 总写入缓冲区大小超限"></a>3.1.2 总写入缓冲区大小超限</h4><p>当所有 Memtable 的总大小超过 <code>db_write_buffer_size</code> 时，会触发 Flush 操作：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_write.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::HandleWriteBufferManagerFlush</span><span class="hljs-params">(WriteContext* write_context)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  ColumnFamilyData* cfd_picked = <span class="hljs-literal">nullptr</span>;  SequenceNumber seq_num_for_cf_picked = kMaxSequenceNumber;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *versions_-&gt;<span class="hljs-built_in">GetColumnFamilySet</span>()) &#123;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">IsDropped</span>()) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-keyword">if</span> (!cfd-&gt;<span class="hljs-built_in">mem</span>()-&gt;<span class="hljs-built_in">IsEmpty</span>() &amp;&amp; !cfd-&gt;<span class="hljs-built_in">imm</span>()-&gt;<span class="hljs-built_in">IsFlushPendingOrRunning</span>()) &#123;      <span class="hljs-comment">// We only consider flush on CFs with bytes in the mutable memtable,</span>      <span class="hljs-comment">// and no immutable memtables for which flush has yet to finish. If</span>      <span class="hljs-comment">// we triggered flush on CFs already trying to flush, we would risk</span>      <span class="hljs-comment">// creating too many immutable memtables leading to write stalls.</span>      <span class="hljs-type">uint64_t</span> seq = cfd-&gt;<span class="hljs-built_in">mem</span>()-&gt;<span class="hljs-built_in">GetCreationSeq</span>();      <span class="hljs-keyword">if</span> (cfd_picked == <span class="hljs-literal">nullptr</span> || seq &lt; seq_num_for_cf_picked) &#123;        cfd_picked = cfd;        seq_num_for_cf_picked = seq;      &#125;    &#125;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><p>RocksDB 会 Flush 序号最小的 Memtable。</p><h4 id="3-1-3-WAL-文件大小超过限制"><a href="#3-1-3-WAL-文件大小超过限制" class="headerlink" title="3.1.3 WAL 文件大小超过限制"></a>3.1.3 WAL 文件大小超过限制</h4><p>当 WAL 文件的总大小超过 <code>max_total_wal_size</code> 时，RocksDB 会触发 Flush 以减小 WAL 体积：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_write.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::SwitchWAL</span><span class="hljs-params">(WriteContext* write_context)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *versions_-&gt;<span class="hljs-built_in">GetColumnFamilySet</span>()) &#123;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">IsDropped</span>()) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">OldestLogToKeep</span>() &lt;= oldest_alive_log) &#123;      cfds.<span class="hljs-built_in">push_back</span>(cfd);    &#125;  &#125;  <span class="hljs-built_in">MaybeFlushStatsCF</span>(&amp;cfds);  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><p>RocksDB 会 Flush 与最旧 WAL 文件关联的 Memtable 以释放 WAL 空间。</p><h3 id="3-2-手动触发情况"><a href="#3-2-手动触发情况" class="headerlink" title="3.2 手动触发情况"></a>3.2 手动触发情况</h3><h4 id="3-2-1-用户显式调用-Flush-API"><a href="#3-2-1-用户显式调用-Flush-API" class="headerlink" title="3.2.1 用户显式调用 Flush API"></a>3.2.1 用户显式调用 Flush API</h4><p>用户可以通过调用 <code>DB::Flush()</code> 方法手动触发 Flush 操作：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 手动触发Flush的示例</span>FlushOptions flush_options;flush_options.wait = <span class="hljs-literal">true</span>; <span class="hljs-comment">// 等待Flush完成</span>db-&gt;<span class="hljs-built_in">Flush</span>(flush_options);  <span class="hljs-comment">// 触发所有列族的Flush</span><span class="hljs-comment">// 或者</span>db-&gt;<span class="hljs-built_in">Flush</span>(flush_options, handles[<span class="hljs-number">1</span>]); <span class="hljs-comment">// 只Flush特定列族</span></code></pre><p>手动 Flush 在需要确保数据持久化或准备备份时非常有用。</p><h4 id="3-2-2-外部文件导入前的-Flush"><a href="#3-2-2-外部文件导入前的-Flush" class="headerlink" title="3.2.2 外部文件导入前的 Flush"></a>3.2.2 外部文件导入前的 Flush</h4><p>当使用 <code>IngestExternalFile()</code> 导入外部 SST 文件时，RocksDB 需要确保 MemTable 和摄取的外部文件之间没有重叠的键范围。 刷新 MemTable 会创建一个新的 SST 文件，然后可以将其与外部文件一起原子地添加到数据库中：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::IngestExternalFile</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    ColumnFamilyHandle* column_family,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> std::vector&lt;std::string&gt;&amp; external_files,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> IngestExternalFileOptions&amp; ingestion_options)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">if</span> (status.<span class="hljs-built_in">ok</span>() &amp;&amp; at_least_one_cf_need_flush) &#123;    FlushOptions flush_opts;    flush_opts.allow_write_stall = <span class="hljs-literal">true</span>;    <span class="hljs-keyword">if</span> (immutable_db_options_.atomic_flush) &#123;      mutex_.<span class="hljs-built_in">Unlock</span>();      status = <span class="hljs-built_in">AtomicFlushMemTables</span>(          flush_opts, FlushReason::kExternalFileIngestion,          &#123;&#125; <span class="hljs-comment">/* provided_candidate_cfds */</span>, <span class="hljs-literal">true</span> <span class="hljs-comment">/* entered_write_thread */</span>);      mutex_.<span class="hljs-built_in">Lock</span>();    &#125; <span class="hljs-keyword">else</span> &#123;      <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i != num_cfs; ++i) &#123;        <span class="hljs-keyword">if</span> (need_flush[i]) &#123;          mutex_.<span class="hljs-built_in">Unlock</span>();          <span class="hljs-keyword">auto</span>* cfd =              <span class="hljs-built_in">static_cast</span>&lt;ColumnFamilyHandleImpl*&gt;(args[i].column_family)                  -&gt;<span class="hljs-built_in">cfd</span>();          status = <span class="hljs-built_in">FlushMemTable</span>(cfd, flush_opts,                                  FlushReason::kExternalFileIngestion,                                  <span class="hljs-literal">true</span> <span class="hljs-comment">/* entered_write_thread */</span>);          mutex_.<span class="hljs-built_in">Lock</span>();          <span class="hljs-keyword">if</span> (!status.<span class="hljs-built_in">ok</span>()) &#123;            <span class="hljs-keyword">break</span>;          &#125;        &#125;      &#125;    &#125;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h4 id="3-2-3-手动压缩前的-Flush"><a href="#3-2-3-手动压缩前的-Flush" class="headerlink" title="3.2.3 手动压缩前的 Flush"></a>3.2.3 手动压缩前的 Flush</h4><p>确保要压缩的数据都持久化到了 SST 文件：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_compaction_flush.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::CompactRange</span><span class="hljs-params">(<span class="hljs-type">const</span> CompactRangeOptions&amp; options,</span></span><span class="hljs-params"><span class="hljs-function">                            ColumnFamilyHandle* column_family,</span></span><span class="hljs-params"><span class="hljs-function">                            <span class="hljs-type">const</span> Slice* begin_without_ts,</span></span><span class="hljs-params"><span class="hljs-function">                            <span class="hljs-type">const</span> Slice* end_without_ts)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-type">bool</span> flush_needed = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// ...</span>  <span class="hljs-keyword">if</span> (s.<span class="hljs-built_in">ok</span>() &amp;&amp; flush_needed) &#123;    FlushOptions fo;    fo.allow_write_stall = options.allow_write_stall;    <span class="hljs-keyword">if</span> (immutable_db_options_.atomic_flush) &#123;      s = <span class="hljs-built_in">AtomicFlushMemTables</span>(fo, FlushReason::kManualCompaction);    &#125; <span class="hljs-keyword">else</span> &#123;      s = <span class="hljs-built_in">FlushMemTable</span>(cfd, fo, FlushReason::kManualCompaction);    &#125;    <span class="hljs-keyword">if</span> (!s.<span class="hljs-built_in">ok</span>()) &#123;      <span class="hljs-built_in">LogFlush</span>(immutable_db_options_.info_log);      <span class="hljs-keyword">return</span> s;    &#125;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h4 id="3-2-4-修剪键空间前的-Flush"><a href="#3-2-4-修剪键空间前的-Flush" class="headerlink" title="3.2.4 修剪键空间前的 Flush"></a>3.2.4 修剪键空间前的 Flush</h4><p>当用户调用 <code>DB::ClipColumnFamily</code> API ，主动触发对指定 Column Family 的数据裁剪操作。操作会将 Column Family 中指定 Key 范围之外的数据物理删除。在删除文件之前，务必确保这些文件可能引用的任何数据都已安全地持久保存在其他位置。 刷新 MemTable 可确保将任何最近的写入都写入新的 SST 文件，因此可以安全地删除旧文件而不会丢失数据。</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::ClipColumnFamily</span><span class="hljs-params">(ColumnFamilyHandle* column_family,</span></span><span class="hljs-params"><span class="hljs-function">                                <span class="hljs-type">const</span> Slice&amp; begin_key, <span class="hljs-type">const</span> Slice&amp; end_key)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-comment">// Flush memtable</span>  FlushOptions flush_opts;  flush_opts.allow_write_stall = <span class="hljs-literal">true</span>;  <span class="hljs-keyword">auto</span>* cfd =      <span class="hljs-built_in">static_cast_with_check</span>&lt;ColumnFamilyHandleImpl&gt;(column_family)-&gt;<span class="hljs-built_in">cfd</span>();  <span class="hljs-keyword">if</span> (immutable_db_options_.atomic_flush) &#123;    status = <span class="hljs-built_in">AtomicFlushMemTables</span>(flush_opts, FlushReason::kDeleteFiles,                                  &#123;&#125; <span class="hljs-comment">/* provided_candidate_cfds */</span>,                                  <span class="hljs-literal">false</span> <span class="hljs-comment">/* entered_write_thread */</span>);  &#125; <span class="hljs-keyword">else</span> &#123;    status = <span class="hljs-built_in">FlushMemTable</span>(cfd, flush_opts, FlushReason::kDeleteFiles,                           <span class="hljs-literal">false</span> <span class="hljs-comment">/* entered_write_thread */</span>);  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h3 id="3-3-系统状态变更触发"><a href="#3-3-系统状态变更触发" class="headerlink" title="3.3 系统状态变更触发"></a>3.3 系统状态变更触发</h3><h4 id="3-3-1-数据库打开时"><a href="#3-3-1-数据库打开时" class="headerlink" title="3.3.1 数据库打开时"></a>3.3.1 数据库打开时</h4><p>当 <code>avoid_flush_during_recovery</code> 设置为 <code>false</code> 时，虽然 RocksDB 不执行传统的 memtable flush 操作，仍然会将 WAL 中的数据即时刷新到 SST 文件。确保了即使在大量 WAL 数据情况下，恢复过程也能保持可控的内存使用。</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_open.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::RecoverLogFiles</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;<span class="hljs-type">uint64_t</span>&gt;&amp; wal_numbers,</span></span><span class="hljs-params"><span class="hljs-function">                               SequenceNumber* next_sequence, <span class="hljs-type">bool</span> read_only,</span></span><span class="hljs-params"><span class="hljs-function">                               <span class="hljs-type">bool</span>* corrupted_wal_found,</span></span><span class="hljs-params"><span class="hljs-function">                               RecoveryContext* recovery_ctx)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-comment">// flush the final memtable (if non-empty)</span>  <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">mem</span>()-&gt;<span class="hljs-built_in">GetFirstSequenceNumber</span>() != <span class="hljs-number">0</span>) &#123;    <span class="hljs-comment">// If flush happened in the middle of recovery (e.g. due to memtable</span>    <span class="hljs-comment">// being full), we flush at the end. Otherwise we&#x27;ll need to record</span>    <span class="hljs-comment">// where we were on last flush, which make the logic complicated.</span>    <span class="hljs-keyword">if</span> (flushed || !immutable_db_options_.avoid_flush_during_recovery) &#123;        status = <span class="hljs-built_in">WriteLevel0TableForRecovery</span>(job_id, cfd, cfd-&gt;<span class="hljs-built_in">mem</span>(), edit);        <span class="hljs-keyword">if</span> (!status.<span class="hljs-built_in">ok</span>()) &#123;            <span class="hljs-comment">// Recovery failed</span>        <span class="hljs-keyword">break</span>;      &#125;      flushed = <span class="hljs-literal">true</span>;      cfd-&gt;<span class="hljs-built_in">CreateNewMemtable</span>(*cfd-&gt;<span class="hljs-built_in">GetLatestMutableCFOptions</span>(),                             versions_-&gt;<span class="hljs-built_in">LastSequence</span>());    &#125;    data_seen = <span class="hljs-literal">true</span>;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h4 id="3-3-2-数据库关闭时"><a href="#3-3-2-数据库关闭时" class="headerlink" title="3.3.2 数据库关闭时"></a>3.3.2 数据库关闭时</h4><p>当数据库正常关闭时，会执行 Flush 以确保所有内存数据持久化（除非设置了 <code>avoid_flush_during_shutdown = true</code>）：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::Close</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">if</span> (!shutting_down_.<span class="hljs-built_in">load</span>(std::memory_order_acquire) &amp;&amp;      has_unpersisted_data_.<span class="hljs-built_in">load</span>(std::memory_order_relaxed) &amp;&amp;      !mutable_db_options_.avoid_flush_during_shutdown) &#123;    s = DBImpl::<span class="hljs-built_in">FlushAllColumnFamilies</span>(<span class="hljs-built_in">FlushOptions</span>(), FlushReason::kShutDown);    s.<span class="hljs-built_in">PermitUncheckedError</span>();   &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h4 id="3-3-3-错误恢复过程中"><a href="#3-3-3-错误恢复过程中" class="headerlink" title="3.3.3 错误恢复过程中"></a>3.3.3 错误恢复过程中</h4><p>在错误恢复过程中，可能需要 Flush 以确保数据一致性：</p><pre><code class="hljs cpp"><span class="hljs-comment">// error_handler.cc</span><span class="hljs-function">Status <span class="hljs-title">ErrorHandler::RecoverFromBGError</span><span class="hljs-params">(<span class="hljs-type">bool</span> is_manual)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">if</span> (context.flush_reason == FlushReason::kErrorRecoveryRetryFlush) &#123;    s = <span class="hljs-built_in">RetryFlushesForErrorRecovery</span>(FlushReason::kErrorRecoveryRetryFlush,                                      <span class="hljs-literal">true</span> <span class="hljs-comment">/* wait */</span>);  &#125; <span class="hljs-keyword">else</span> &#123;    <span class="hljs-comment">// We cannot guarantee consistency of the WAL. So force flush Memtables of</span>    <span class="hljs-comment">// all the column families</span>    FlushOptions flush_opts;    <span class="hljs-comment">// We allow flush to stall write since we are trying to resume from error.</span>    flush_opts.allow_write_stall = <span class="hljs-literal">true</span>;    s = <span class="hljs-built_in">FlushAllColumnFamilies</span>(flush_opts, context.flush_reason);  &#125;  <span class="hljs-keyword">if</span> (!s.<span class="hljs-built_in">ok</span>()) &#123;    <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log,                    <span class="hljs-string">&quot;DB resume requested but failed due to Flush failure [%s]&quot;</span>,                    s.<span class="hljs-built_in">ToString</span>().<span class="hljs-built_in">c_str</span>());  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h4 id="3-3-4-创建备份-x2F-快照时"><a href="#3-3-4-创建备份-x2F-快照时" class="headerlink" title="3.3.4 创建备份&#x2F;快照时"></a>3.3.4 创建备份&#x2F;快照时</h4><p>当调用 <code>GetLiveFiles()</code> 并指定 <code>flush_memtable=true</code> 时，会触发 Flush 以确保返回完整的文件列表，常用于创建备份或快照：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_filesnapshot.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::GetLiveFiles</span><span class="hljs-params">(std::vector&lt;std::string&gt;&amp; ret,</span></span><span class="hljs-params"><span class="hljs-function">                              <span class="hljs-type">uint64_t</span>* manifest_file_size, </span></span><span class="hljs-params"><span class="hljs-function">                              <span class="hljs-type">bool</span> flush_memtable)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">if</span> (flush_memtable) &#123;    Status status = <span class="hljs-built_in">FlushForGetLiveFiles</span>();    <span class="hljs-keyword">if</span> (!status.<span class="hljs-built_in">ok</span>()) &#123;      mutex_.<span class="hljs-built_in">Unlock</span>();      <span class="hljs-built_in">ROCKS_LOG_ERROR</span>(immutable_db_options_.info_log, <span class="hljs-string">&quot;Cannot Flush data %s\n&quot;</span>,                      status.<span class="hljs-built_in">ToString</span>().<span class="hljs-built_in">c_str</span>());      <span class="hljs-keyword">return</span> status;    &#125;  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h2 id="四、Flush-执行策略"><a href="#四、Flush-执行策略" class="headerlink" title="四、Flush 执行策略"></a>四、Flush 执行策略</h2><p>除了主动刷新时选择特定的列族，以及特定列族的 Immutable Memtable 总数达到 <code>min_write_buffer_number_to_merge</code> 触发被动 Flush，在 Non-Atomic Flush 模式下 RocksDB 需要决定哪些 Memtable 应该被 Flush。选择策略会根据触发 Flush 的原因不同而变化。</p><h3 id="4-1-基于-Memtable-时间的选择策略"><a href="#4-1-基于-Memtable-时间的选择策略" class="headerlink" title="4.1 基于 Memtable 时间的选择策略"></a>4.1 基于 Memtable 时间的选择策略</h3><p>当总 Memtable 内存占用过高时，选择策略倾向于选择<strong>创建序列号最小（即最老）的，有数据且没有正在刷盘的 Memtable</strong>。：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_write.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::HandleWriteBufferManagerFlush</span><span class="hljs-params">(WriteContext* write_context)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  autovector&lt;ColumnFamilyData*&gt; cfds;  ColumnFamilyData* cfd_picked = <span class="hljs-literal">nullptr</span>;  SequenceNumber seq_num_for_cf_picked = kMaxSequenceNumber;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *versions_-&gt;<span class="hljs-built_in">GetColumnFamilySet</span>()) &#123;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">IsDropped</span>()) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-keyword">if</span> (!cfd-&gt;<span class="hljs-built_in">mem</span>()-&gt;<span class="hljs-built_in">IsEmpty</span>() &amp;&amp; !cfd-&gt;<span class="hljs-built_in">imm</span>()-&gt;<span class="hljs-built_in">IsFlushPendingOrRunning</span>()) &#123;      <span class="hljs-comment">// We only consider flush on CFs with bytes in the mutable memtable,</span>      <span class="hljs-comment">// and no immutable memtables for which flush has yet to finish. If</span>      <span class="hljs-comment">// we triggered flush on CFs already trying to flush, we would risk</span>      <span class="hljs-comment">// creating too many immutable memtables leading to write stalls.</span>      <span class="hljs-type">uint64_t</span> seq = cfd-&gt;<span class="hljs-built_in">mem</span>()-&gt;<span class="hljs-built_in">GetCreationSeq</span>();      <span class="hljs-keyword">if</span> (cfd_picked == <span class="hljs-literal">nullptr</span> || seq &lt; seq_num_for_cf_picked) &#123;        cfd_picked = cfd;        seq_num_for_cf_picked = seq;      &#125;      &#125;  <span class="hljs-keyword">if</span> (cfd_picked != <span class="hljs-literal">nullptr</span>) &#123;    cfds.<span class="hljs-built_in">push_back</span>(cfd_picked);  &#125;  <span class="hljs-built_in">MaybeFlushStatsCF</span>(&amp;cfds);  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h3 id="4-2-基于-WAL-时间的选择策略"><a href="#4-2-基于-WAL-时间的选择策略" class="headerlink" title="4.2 基于 WAL 时间的选择策略"></a>4.2 基于 WAL 时间的选择策略</h3><p>当 WAL 文件大小超过限制时，选择与最旧 WAL 关联的 CF 的所有 Memtable 进行 Flush：</p><pre><code class="hljs cpp"><span class="hljs-comment">// db_impl_write.cc</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::SwitchWAL</span><span class="hljs-params">(WriteContext* write_context)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *versions_-&gt;<span class="hljs-built_in">GetColumnFamilySet</span>()) &#123;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">IsDropped</span>()) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-keyword">if</span> (cfd-&gt;<span class="hljs-built_in">OldestLogToKeep</span>() &lt;= oldest_alive_log) &#123;      cfds.<span class="hljs-built_in">push_back</span>(cfd);    &#125;  &#125;  <span class="hljs-built_in">MaybeFlushStatsCF</span>(&amp;cfds);  <span class="hljs-comment">// 省略代码...</span>&#125;</code></pre><h3 id="4-3-总结"><a href="#4-3-总结" class="headerlink" title="4.3 总结"></a>4.3 总结</h3><p>因为同一 CF 内的较新 Memtable 也会被连带 Flush，两者刷新的 Memtable 的类型几乎一样，被刷新的 Memtable <strong>一定包含当前最旧未刷新的 Memtable</strong>，但<strong>会包含较新的 Memtable</strong>。</p><h2 id="五、WAL-恢复机制"><a href="#五、WAL-恢复机制" class="headerlink" title="五、WAL 恢复机制"></a>五、WAL 恢复机制</h2><p>从上面的触发可知，RocksDB 不会专门针对不活跃的列族进行自动 Flush。除了额外的内存占用（不活跃列族的数据会在 Memtable 中保留，直到触发 Flush）之外，还会导致 WAL 文件内的数据累积，影响恢复时读取的数据量和时长</p><h3 id="5-1-WAL-恢复过程"><a href="#5-1-WAL-恢复过程" class="headerlink" title="5.1 WAL 恢复过程"></a>5.1 WAL 恢复过程</h3><h4 id="5-1-1-恢复原理概述"><a href="#5-1-1-恢复原理概述" class="headerlink" title="5.1.1 恢复原理概述"></a>5.1.1 恢复原理概述</h4><p>RocksDB 的崩溃恢复流程:</p><ol><li><strong>读取 MANIFEST</strong>：确定数据库状态、SST 文件列表和列族信息。</li><li><strong>确定恢复点</strong>：确定需要回放的 WAL 文件及起始点。</li><li><strong>回放 WAL</strong>：重新执行 WAL 中记录的写操作，重建内存状态。</li><li><strong>执行恢复后 Flush</strong>：可选地执行 Flush 以持久化恢复的数据。</li></ol><h4 id="5-1-2-确定需要读取的-WAL-起始文件"><a href="#5-1-2-确定需要读取的-WAL-起始文件" class="headerlink" title="5.1.2 确定需要读取的 WAL 起始文件"></a>5.1.2 确定需要读取的 WAL 起始文件</h4><p>在 RecoverLogFiles 中，起始 WAL 文件的确定流程如下：</p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">DBImpl::RecoverLogFiles</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;<span class="hljs-type">uint64_t</span>&gt;&amp; wal_numbers,</span></span><span class="hljs-params"><span class="hljs-function">                               SequenceNumber* next_sequence, <span class="hljs-type">bool</span> read_only,</span></span><span class="hljs-params"><span class="hljs-function">                               <span class="hljs-type">bool</span>* corrupted_wal_found,</span></span><span class="hljs-params"><span class="hljs-function">                               RecoveryContext* recovery_ctx)</span> </span>&#123;  <span class="hljs-comment">// 省略代码...</span>  <span class="hljs-comment">// 从 VersionSet 中获取需要保留的最小 WAL 编号</span>  <span class="hljs-type">uint64_t</span> min_wal_number = <span class="hljs-built_in">MinLogNumberToKeep</span>();  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">allow_2pc</span>()) &#123;      <span class="hljs-comment">// 计算包含未刷盘数据的最小 WAL 编号</span>      min_wal_number = std::<span class="hljs-built_in">max</span>(min_wal_number, versions_-&gt;<span class="hljs-built_in">MinLogNumberWithUnflushedData</span>());  &#125;    <span class="hljs-comment">// 遍历所有WAL文件，跳过比最小保留编号还小的WAL文件</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> wal_number : wal_numbers) &#123;      <span class="hljs-keyword">if</span> (wal_number &lt; min_wal_number) &#123;          <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log,                       <span class="hljs-string">&quot;Skipping log #%&quot;</span> PRIu64                       <span class="hljs-string">&quot; since it is older than min log to keep #%&quot;</span> PRIu64,                       wal_number, min_wal_number);          <span class="hljs-keyword">continue</span>;      &#125;      <span class="hljs-comment">// 处理 WAL 文件...</span>  &#125;  <span class="hljs-comment">// 省略代码...</span>&#125;<span class="hljs-comment">// Returns the minimum log number which still has data not flushed to any SST</span><span class="hljs-comment">// file, except data from `cfd_to_skip`.</span><span class="hljs-function"><span class="hljs-type">uint64_t</span> <span class="hljs-title">PreComputeMinLogNumberWithUnflushedData</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> ColumnFamilyData* cfd_to_skip)</span> <span class="hljs-type">const</span> </span>&#123;  <span class="hljs-type">uint64_t</span> min_log_num = std::numeric_limits&lt;<span class="hljs-type">uint64_t</span>&gt;::<span class="hljs-built_in">max</span>();  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *column_family_set_) &#123;    <span class="hljs-keyword">if</span> (cfd == cfd_to_skip) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-comment">// It&#x27;s safe to ignore dropped column families here:</span>    <span class="hljs-comment">// cfd-&gt;IsDropped() becomes true after the drop is persisted in MANIFEST.</span>    <span class="hljs-keyword">if</span> (min_log_num &gt; cfd-&gt;<span class="hljs-built_in">GetLogNumber</span>() &amp;&amp; !cfd-&gt;<span class="hljs-built_in">IsDropped</span>()) &#123;      min_log_num = cfd-&gt;<span class="hljs-built_in">GetLogNumber</span>();    &#125;  &#125;  <span class="hljs-keyword">return</span> min_log_num;&#125;</code></pre><h4 id="5-1-3-确定起始的-Record"><a href="#5-1-3-确定起始的-Record" class="headerlink" title="5.1.3 确定起始的 Record"></a>5.1.3 确定起始的 Record</h4><p>对于每个需要处理的 WAL 文件，<strong>从文件头开始顺序读取所有 Record</strong>：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 创建日志读取器，从文件开头开始读取</span><span class="hljs-function">log::Reader <span class="hljs-title">reader</span><span class="hljs-params">(immutable_db_options_.info_log, std::move(file_reader),</span></span><span class="hljs-params"><span class="hljs-function">                   &amp;reporter, <span class="hljs-literal">true</span> <span class="hljs-comment">/*checksum*/</span>, wal_number)</span></span>;<span class="hljs-comment">// 从头开始读取所有记录</span>std::string scratch;Slice record;<span class="hljs-keyword">while</span> (reader.<span class="hljs-built_in">ReadRecord</span>(&amp;record, &amp;scratch,                         immutable_db_options_.wal_recovery_mode,                         &amp;record_checksum) &amp;&amp; status.<span class="hljs-built_in">ok</span>()) &#123;    <span class="hljs-comment">// 处理每条记录...</span>&#125;</code></pre><p><strong>不是从某个特定位置开始，而是完整读取整个 WAL 文件的所有记录</strong>。</p><h4 id="5-1-4-确定写入-CF-Memtable-的-Record"><a href="#5-1-4-确定写入-CF-Memtable-的-Record" class="headerlink" title="5.1.4 确定写入 CF Memtable 的 Record"></a>5.1.4 确定写入 CF Memtable 的 Record</h4><p>该过程通过 <code>WriteBatchInternal::InsertInto</code> 和 <code>MemTableInserter</code> 类来完成：</p><h5 id="5-1-4-1-解析-WriteBatch"><a href="#5-1-4-1-解析-WriteBatch" class="headerlink" title="5.1.4.1 解析 WriteBatch"></a>5.1.4.1 解析 WriteBatch</h5><pre><code class="hljs cpp"><span class="hljs-comment">// 将WAL记录解析为WriteBatch</span>WriteBatch batch;status = WriteBatchInternal::<span class="hljs-built_in">SetContents</span>(&amp;batch, record);<span class="hljs-comment">// 应用批处理到memtable</span>status = WriteBatchInternal::<span class="hljs-built_in">InsertInto</span>(    batch_to_use, column_family_memtables_.<span class="hljs-built_in">get</span>(), &amp;flush_scheduler_,    &amp;trim_history_scheduler_, <span class="hljs-literal">true</span>, wal_number, <span class="hljs-keyword">this</span>,    <span class="hljs-literal">false</span> <span class="hljs-comment">/* concurrent_memtable_writes */</span>, next_sequence,    &amp;has_valid_writes, seq_per_batch_, batch_per_txn_);</code></pre><h5 id="5-1-4-2-按列族过滤和应用"><a href="#5-1-4-2-按列族过滤和应用" class="headerlink" title="5.1.4.2 按列族过滤和应用"></a>5.1.4.2 按列族过滤和应用</h5><p>在 <code>MemTableInserter::SeekToColumnFamily</code> 中进行过滤：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">SeekToColumnFamily</span><span class="hljs-params">(<span class="hljs-type">uint32_t</span> column_family_id, Status* s)</span> </span>&#123;    <span class="hljs-comment">// 查找对应的列族</span>    <span class="hljs-type">bool</span> found = cf_mems_-&gt;<span class="hljs-built_in">Seek</span>(column_family_id);    <span class="hljs-keyword">if</span> (!found) &#123;        <span class="hljs-keyword">if</span> (ignore_missing_column_families_) &#123;            *s = Status::<span class="hljs-built_in">OK</span>();        &#125; <span class="hljs-keyword">else</span> &#123;            *s = Status::<span class="hljs-built_in">InvalidArgument</span>(<span class="hljs-string">&quot;Invalid column family specified in write batch&quot;</span>);        &#125;        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;    &#125;        <span class="hljs-comment">// 检查是否需要跳过此记录（恢复模式下的关键逻辑）</span>    <span class="hljs-keyword">if</span> (recovering_log_number_ != <span class="hljs-number">0</span> &amp;&amp;        recovering_log_number_ &lt; cf_mems_-&gt;<span class="hljs-built_in">GetLogNumber</span>()) &#123;        <span class="hljs-comment">// 如果恢复的日志编号小于列族的当前日志编号，</span>        <span class="hljs-comment">// 说明列族已经包含了来自此日志的更新，跳过以避免重复应用</span>        *s = Status::<span class="hljs-built_in">OK</span>();        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;    &#125;        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;&#125;</code></pre><h5 id="5-1-4-3-写入-Memtable"><a href="#5-1-4-3-写入-Memtable" class="headerlink" title="5.1.4.3 写入 Memtable"></a>5.1.4.3 写入 Memtable</h5><p>通过 <code>MemTableInserter::PutCF</code>、<code>DeleteCF</code> 等方法将数据写入对应列族的 memtable：</p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">PutCF</span><span class="hljs-params">(<span class="hljs-type">uint32_t</span> column_family_id, <span class="hljs-type">const</span> Slice&amp; key, <span class="hljs-type">const</span> Slice&amp; value)</span> </span>&#123;    <span class="hljs-comment">// 检查列族是否存在和有效</span>    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">SeekToColumnFamily</span>(column_family_id, &amp;ret_status)) &#123;        <span class="hljs-keyword">return</span> ret_status;    &#125;        <span class="hljs-comment">// 获取目标memtable</span>    MemTable* mem = cf_mems_-&gt;<span class="hljs-built_in">GetMemTable</span>();        <span class="hljs-comment">// 将数据添加到memtable</span>    ret_status = mem-&gt;<span class="hljs-built_in">Add</span>(sequence_, value_type, key, value, kv_prot_info,                         concurrent_memtable_writes_, <span class="hljs-built_in">get_post_process_info</span>(mem),                         hint_per_batch_ ? &amp;<span class="hljs-built_in">GetHintMap</span>()[mem] : <span class="hljs-literal">nullptr</span>);    <span class="hljs-keyword">return</span> ret_status;&#125;</code></pre><h4 id="5-1-5-总结"><a href="#5-1-5-总结" class="headerlink" title="5.1.5 总结"></a>5.1.5 总结</h4><ol><li>WAL 起始文件：基于各列族的 <code>log_number_</code> 和系统的 <code>min_log_number_to_keep_</code> 确定</li><li>起始 Record：每个 WAL 文件都从头开始完整读取</li><li>Record 过滤：<ul><li>根据 <code>WriteBatch</code> 中的 <code>column_family_id</code> 找到对应列族</li><li>检查列族的 <code>log_number_</code> 避免重复应用已处理的数据</li><li>只有通过过滤的 Record 才会被应用到对应列族的 memtable</li></ul></li></ol><p>如果因数据同步需要调大 WAL 的保留时间，可以通过调大 <code>WAL_ttl_seconds</code> 或者 <code>WAL_size_limit_MB</code> ，并且保持 <code>max_total_wal_size</code> 不变实现，此时并不会影响恢复速度</p><h3 id="5-2-不同恢复模式比较"><a href="#5-2-不同恢复模式比较" class="headerlink" title="5.2 不同恢复模式比较"></a>5.2 不同恢复模式比较</h3><p>RocksDB 提供了四种 WAL 恢复模式，在数据丢失量和恢复速度之间做出不同的权衡：</p><h4 id="5-2-1-kTolerateCorruptedTailRecords"><a href="#5-2-1-kTolerateCorruptedTailRecords" class="headerlink" title="5.2.1 kTolerateCorruptedTailRecords"></a>5.2.1 kTolerateCorruptedTailRecords</h4><pre><code class="hljs cpp"><span class="hljs-comment">// 原始的LevelDB恢复模式</span><span class="hljs-comment">// 我们容忍WAL文件末尾的损坏记录</span><span class="hljs-comment">// 能够恢复大部分仍然可读的数据</span>WALRecoveryMode::kTolerateCorruptedTailRecords</code></pre><p>特点：</p><ul><li><strong>数据丢失量</strong>：文件尾部损坏的记录会丢失</li><li><strong>恢复速度</strong>：中等</li><li><strong>适用场景</strong>：对部分数据丢失可接受，但要尽量恢复的场景</li></ul><h4 id="5-2-2-kAbsoluteConsistency"><a href="#5-2-2-kAbsoluteConsistency" class="headerlink" title="5.2.2 kAbsoluteConsistency"></a>5.2.2 kAbsoluteConsistency</h4><pre><code class="hljs cpp"><span class="hljs-comment">// 如果发现任何损坏记录，恢复会失败</span><span class="hljs-comment">// 确保数据的绝对一致性</span>WALRecoveryMode::kAbsoluteConsistency</code></pre><p>特点：</p><ul><li><strong>数据丢失量</strong>：零容忍，有任何损坏就会恢复失败</li><li><strong>恢复速度</strong>：较慢，需要验证所有记录</li><li><strong>适用场景</strong>：金融等要求数据完全准确的场景</li></ul><h4 id="5-2-3-kPointInTimeRecovery"><a href="#5-2-3-kPointInTimeRecovery" class="headerlink" title="5.2.3 kPointInTimeRecovery"></a>5.2.3 kPointInTimeRecovery</h4><pre><code class="hljs cpp"><span class="hljs-comment">// 恢复到损坏记录之前的最后一个完整记录</span><span class="hljs-comment">// 确保数据一致性但可能丢失最近的写入</span>WALRecoveryMode::kPointInTimeRecovery</code></pre><p>特点：</p><ul><li><strong>数据丢失量</strong>：损坏点之后的所有数据</li><li><strong>恢复速度</strong>：较快，发现损坏立即停止</li><li><strong>适用场景</strong>：需要一致性视图且接受部分数据丢失的场景</li></ul><h4 id="5-2-4-kSkipAnyCorruptedRecords"><a href="#5-2-4-kSkipAnyCorruptedRecords" class="headerlink" title="5.2.4 kSkipAnyCorruptedRecords"></a>5.2.4 kSkipAnyCorruptedRecords</h4><pre><code class="hljs cpp"><span class="hljs-comment">// 跳过所有损坏的记录但继续处理</span><span class="hljs-comment">// 可能导致数据不一致但恢复速度最快</span>WALRecoveryMode::kSkipAnyCorruptedRecords</code></pre><p>特点：</p><ul><li><strong>数据丢失量</strong>：仅损坏的记录</li><li><strong>恢复速度</strong>：最快，不会因损坏而停止</li><li><strong>适用场景</strong>：恢复速度优先，可容忍潜在的不一致性</li></ul><h3 id="5-3-恢复速度优化"><a href="#5-3-恢复速度优化" class="headerlink" title="5.3 恢复速度优化"></a>5.3 恢复速度优化</h3><p>恢复速度与 Flush 策略紧密相关，优化方法包括：</p><ol><li><p>**设置 <code>avoid_flush_during_recovery</code>**：</p><pre><code class="hljs cpp">Options options;options.avoid_flush_during_recovery = <span class="hljs-literal">true</span>;</code></pre></li></ol><p>此选项避免在恢复期间进行额外的 Flush，减少 I&#x2F;O 开销。</p><ol start="2"><li><p><strong>设置并行 WAL 恢复</strong>：</p><pre><code class="hljs cpp">Options options;options.wal_recovery_mode = WALRecoveryMode::kPointInTimeRecovery;options.max_background_jobs = <span class="hljs-number">8</span>;  <span class="hljs-comment">// 增加并行恢复线程</span></code></pre></li><li><p><strong>优化 WAL 文件数量</strong>：<br>合理设置 <code>max_total_wal_size</code> 并经常触发 Flush，减少崩溃时需要回放的 WAL 数量。</p></li><li><p><strong>统计更新优化</strong></p></li></ol><p>跳过 DB 打开时的统计信息更新可加快启动速度：</p><pre><code class="hljs cpp">Options options;<span class="hljs-comment">// 不更新用于优化压缩决策的统计信息</span>options.skip_stats_update_on_db_open = <span class="hljs-literal">true</span>;</code></pre><ol start="5"><li><strong>文件检查优化</strong></li></ol><p>跳过检查 SST 文件大小可加快数据库打开：</p><pre><code class="hljs cpp">Options options;<span class="hljs-comment">// 跳过在DB打开时获取和检查所有SST文件的大小</span>options.skip_checking_sst_file_sizes_on_db_open = <span class="hljs-literal">true</span>;</code></pre><p>当使用非默认 Env 且获取文件大小开销较大时，这一优化尤为有效。</p><ol start="6"><li><p><strong>实现不活跃 CF 的定期 Flush 机制</strong>：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 实现定时任务，定期执行Flush</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">PeriodicFlushTask</span><span class="hljs-params">()</span> </span>&#123;  FlushOptions fopts;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cf_handle : inactive_cf_handles) &#123;    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">TimeExceeds</span>(last_flush_time[cf_handle], max_idle_time)) &#123;      db-&gt;<span class="hljs-built_in">Flush</span>(fopts, cf_handle);    &#125;  &#125;&#125;</code></pre></li></ol><h2 id="六、Flush-与-SST-文件"><a href="#六、Flush-与-SST-文件" class="headerlink" title="六、Flush 与 SST 文件"></a>六、Flush 与 SST 文件</h2><h3 id="6-1-列族与-SST-文件的关系"><a href="#6-1-列族与-SST-文件的关系" class="headerlink" title="6.1 列族与 SST 文件的关系"></a>6.1 列族与 SST 文件的关系</h3><p>多 CF 是否会 flush 到同一个 SST ？从代码实现上看，FlushJob 总是针对单个 CF 创建并运行的：</p><pre><code class="hljs cpp"><span class="hljs-built_in">FlushJob</span>(<span class="hljs-type">const</span> std::string&amp; dbname, ColumnFamilyData* cfd,   <span class="hljs-type">const</span> ImmutableDBOptions&amp; db_options,   <span class="hljs-type">const</span> MutableCFOptions&amp; mutable_cf_options, <span class="hljs-type">uint64_t</span> max_memtable_id,   <span class="hljs-type">const</span> FileOptions&amp; file_options, VersionSet* versions,   InstrumentedMutex* db_mutex, std::atomic&lt;<span class="hljs-type">bool</span>&gt;* shutting_down,   std::vector&lt;SequenceNumber&gt; existing_snapshots,   SequenceNumber earliest_write_conflict_snapshot,   SnapshotChecker* snapshot_checker, JobContext* job_context,   FlushReason flush_reason, LogBuffer* log_buffer,   FSDirectory* db_directory, FSDirectory* output_file_directory,   CompressionType output_compression, Statistics* stats,   EventLogger* event_logger, <span class="hljs-type">bool</span> measure_io_stats,   <span class="hljs-type">const</span> <span class="hljs-type">bool</span> sync_output_directory, <span class="hljs-type">const</span> <span class="hljs-type">bool</span> write_manifest,   Env::Priority thread_pri, <span class="hljs-type">const</span> std::shared_ptr&lt;IOTracer&gt;&amp; io_tracer,   <span class="hljs-type">const</span> SeqnoToTimeMapping&amp; seq_time_mapping,   <span class="hljs-type">const</span> std::string&amp; db_id = <span class="hljs-string">&quot;&quot;</span>, <span class="hljs-type">const</span> std::string&amp; db_session_id = <span class="hljs-string">&quot;&quot;</span>,   std::string full_history_ts_low = <span class="hljs-string">&quot;&quot;</span>,   BlobFileCompletionCallback* blob_callback = <span class="hljs-literal">nullptr</span>);</code></pre><p>多个 CF 不会 Flush 到同一个 SST 文件的原因包括：</p><ol><li><strong>数据隔离</strong>：每个 CF 可能有不同的压缩选项、比较器等，需要独立存储。</li><li><strong>独立生命周期</strong>：每个 CF 可以独立删除或修改，分开存储便于管理。</li><li><strong>性能考虑</strong>：分开存储可以并行处理不同 CF 的数据访问和压缩。</li></ol><h3 id="6-2-SST-文件的组织与管理"><a href="#6-2-SST-文件的组织与管理" class="headerlink" title="6.2 SST 文件的组织与管理"></a>6.2 SST 文件的组织与管理</h3><p>SST 文件通常以数字作为文件名，表示文件编号：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 生成 SST 文件名的函数</span>meta_.fd = <span class="hljs-built_in">FileDescriptor</span>(versions_-&gt;<span class="hljs-built_in">NewFileNumber</span>(), <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);file_name = <span class="hljs-built_in">TableFileName</span>(ioptions-&gt;cf_paths, file_meta-&gt;fd.<span class="hljs-built_in">GetNumber</span>(),  file_meta-&gt;fd.<span class="hljs-built_in">GetPathId</span>());<span class="hljs-function">std::string <span class="hljs-title">MakeTableFileName</span><span class="hljs-params">(<span class="hljs-type">const</span> std::string&amp; path, <span class="hljs-type">uint64_t</span> number)</span> </span>&#123;  <span class="hljs-comment">// static const std::string kRocksDbTFileExt = &quot;sst&quot;;</span>  <span class="hljs-keyword">return</span> <span class="hljs-built_in">MakeFileName</span>(path, number, kRocksDbTFileExt.<span class="hljs-built_in">c_str</span>());&#125;<span class="hljs-function"><span class="hljs-type">static</span> std::string <span class="hljs-title">MakeFileName</span><span class="hljs-params">(<span class="hljs-type">uint64_t</span> number, <span class="hljs-type">const</span> <span class="hljs-type">char</span>* suffix)</span> </span>&#123;  <span class="hljs-type">char</span> buf[<span class="hljs-number">100</span>];  <span class="hljs-built_in">snprintf</span>(buf, <span class="hljs-built_in">sizeof</span>(buf), <span class="hljs-string">&quot;%06llu.%s&quot;</span>,           <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> <span class="hljs-type">long</span>&gt;(number), suffix);  <span class="hljs-keyword">return</span> buf;&#125;</code></pre><p>文件编号通过 <code>VersionSet</code> 类中的原子计数器 <code>next_file_number_</code> 生成的全局递增序列，确保了所有文件的唯一性标识，并通过 MANIFEST 文件持久化，在数据库重启时能够正确恢复。文件编号在整个数据库实例中全局唯一，不同的列族共享同一个计数器</p><pre><code class="hljs cpp"><span class="hljs-comment">// Allocate and return a new file number</span><span class="hljs-function"><span class="hljs-type">uint64_t</span> <span class="hljs-title">NewFileNumber</span><span class="hljs-params">()</span> </span>&#123; <span class="hljs-keyword">return</span> next_file_number_.<span class="hljs-built_in">fetch_add</span>(<span class="hljs-number">1</span>); &#125;</code></pre><h2 id="七、总结"><a href="#七、总结" class="headerlink" title="七、总结"></a>七、总结</h2><p>RocksDB 的 Flush 机制直接影响写入性能、内存占用和重启恢复速度。合理配置参数、关注不活跃列族和活跃的 WAL 管理，能有效提升系统整体表现和可用性。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/05-30-2025/rocksdb-memtable-flush.html">https://www.cyningsun.com/05-30-2025/rocksdb-memtable-flush.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;一、引言&quot;&gt;&lt;a href=&quot;#一、引言&quot; class=&quot;headerlink&quot; title=&quot;一、引言&quot;&gt;&lt;/a&gt;一、引言&lt;/h2&gt;&lt;p&gt;在 RocksDB 的核心机制中，Flush 操作扮演着至关重要的角色，它是连接内存数据结构 (Memtable) 和持久化</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB 过期文件清理</title>
    <link href="https://www.cyningsun.com/05-05-2025/rocksdb-obsolete-files.html"/>
    <id>https://www.cyningsun.com/05-05-2025/rocksdb-obsolete-files.html</id>
    <published>2025-05-04T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<p>RocksDB 作为一个高性能的 KV 存储引擎，会产生多种类型的文件：SST 数据文件、WAL 日志文件、MANIFEST 元数据文件、LOG 运行日志等。随着数据库运行，这些文件会不断生成、更新和过期。尤其是一些极端情况下，会导致<strong>磁盘空间耗尽</strong>，数据库无法继续写入数据，引发服务中断。</p><p>不妨带着以下问题，来详细深入了解下具体的实现细节</p><ol><li>compaction 结束之后旧的 sst 文件、WAL 文件、LOG 文件是立刻删除，还是定时删除？</li><li>compaction 进行到一半，进程因为发布、崩溃重启，此时 compaction 生成的尚未 install 的 sst 文件会成为过期文件么？如果是，什么时间会清理？</li><li>什么时候会强制触发全量清理？</li><li>如果没有任何写入落盘，是否也会定时触发清理？</li></ol><h2 id="一、什么是过期文件"><a href="#一、什么是过期文件" class="headerlink" title="一、什么是过期文件"></a>一、什么是过期文件</h2><p>在 RocksDB 中，过期文件（Obsolete Files）指的是那些逻辑上已不再需要但物理上仍存在于磁盘上的文件。文件主要包括：</p><ol><li><strong>SST 文件 (kTableFile, kBlobFile)：</strong>  当 Compaction 操作将多个 SST 文件合并生成新的 SST 文件后，原来的输入 SST 文件就可能变为过期。当 Flush、Compaction 和 Ingestion 出现异常时，创建新的 SST 文件不会被正式添加到版本控制中，也会被视作过期。</li><li><strong>WAL 文件 (kWalFile)：</strong>  当 WAL 文件中的所有数据变更都已成功刷入 MemTable 并最终持久化到 SST 文件后，该 WAL 文件就可能变为过期。</li><li><strong>Manifest 文件 (kDescriptorFile)：</strong>  当数据库元信息更新，生成新的 Manifest 文件后，旧的 Manifest 文件就变为过期。</li><li><strong>Info LOG 文件 (kInfoLogFile)：</strong>  RocksDB 会保留一定数量的 Info LOG 文件，旧的日志文件会根据配置被删除。</li><li><strong>Options 文件 (kOptionsFile)：</strong>  记录数据库配置的文件，RocksDB 通常会保留最新的几个版本。</li><li><strong>临时文件 (kTempFile)：</strong>  在 Flush、Compaction 或 Manifest 写入过程中产生的临时文件，操作完成后应被删除。</li></ol><h2 id="二、过期文件清理的核心机制"><a href="#二、过期文件清理的核心机制" class="headerlink" title="二、过期文件清理的核心机制"></a>二、过期文件清理的核心机制</h2><h3 id="2-1-核心数据结构"><a href="#2-1-核心数据结构" class="headerlink" title="2.1 核心数据结构"></a>2.1 核心数据结构</h3><h4 id="JobContext"><a href="#JobContext" class="headerlink" title="JobContext"></a>JobContext</h4><p><code>JobContext</code> 是清理过期文件的关键结构体，存储待清理文件的信息：</p><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">JobContext</span> &#123;  <span class="hljs-comment">// 检查是否有过时文件需要删除，通过检查各种过时文件列表是否为空来确定。</span>  <span class="hljs-function"><span class="hljs-keyword">inline</span> <span class="hljs-type">bool</span> <span class="hljs-title">HaveSomethingToDelete</span><span class="hljs-params">()</span> <span class="hljs-type">const</span> </span>&#123;    <span class="hljs-keyword">return</span> !(full_scan_candidate_files.<span class="hljs-built_in">empty</span>() &amp;&amp; sst_delete_files.<span class="hljs-built_in">empty</span>() &amp;&amp;             blob_delete_files.<span class="hljs-built_in">empty</span>() &amp;&amp; log_delete_files.<span class="hljs-built_in">empty</span>() &amp;&amp;             manifest_delete_files.<span class="hljs-built_in">empty</span>());  &#125;  <span class="hljs-comment">// 检查是否有任何资源需要清理，包括过时文件、内存表、日志写入器和快照等。</span>  <span class="hljs-function"><span class="hljs-keyword">inline</span> <span class="hljs-type">bool</span> <span class="hljs-title">HaveSomethingToClean</span><span class="hljs-params">()</span> <span class="hljs-type">const</span> </span>&#123;    <span class="hljs-type">bool</span> sv_have_sth = <span class="hljs-literal">false</span>;    <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; sv_ctx : superversion_contexts) &#123;      <span class="hljs-keyword">if</span> (sv_ctx.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;        sv_have_sth = <span class="hljs-literal">true</span>;        <span class="hljs-keyword">break</span>;      &#125;    &#125;    <span class="hljs-keyword">return</span> memtables_to_free.<span class="hljs-built_in">size</span>() &gt; <span class="hljs-number">0</span> || logs_to_free.<span class="hljs-built_in">size</span>() &gt; <span class="hljs-number">0</span> ||           job_snapshot != <span class="hljs-literal">nullptr</span> || sv_have_sth;  &#125;  <span class="hljs-comment">// 存储全扫描过程中识别出的所有潜在可删除文件的信息</span>  <span class="hljs-comment">// 当执行全目录扫描时，会将数据库目录中所有文件加入此列表，随后筛选哪些是过时的</span>  <span class="hljs-comment">// **包含信息**：文件名和完整路径</span>  std::vector&lt;CandidateFileInfo&gt; full_scan_candidate_files;  <span class="hljs-comment">// 专门存储已确定为过时的 SST 文件信息</span>  <span class="hljs-comment">// 压缩或版本控制过程中识别的不再需要的 SST 文件</span>  <span class="hljs-comment">// **包含信息**：文件编号、文件大小、文件路径等元数据</span>  std::vector&lt;ObsoleteFileInfo&gt; sst_delete_files;  <span class="hljs-comment">// 存储已确定为过时的 Blob 文件信息</span>  <span class="hljs-comment">// 当 Blob 文件中的数据被压缩或覆盖后，标记为过时</span>  <span class="hljs-comment">// **包含信息**：Blob 文件编号、文件路径和其他相关元数据</span>  std::vector&lt;ObsoleteBlobFileInfo&gt; blob_delete_files;  <span class="hljs-comment">// 存储需要删除的预写式日志（WAL）文件编号</span>  <span class="hljs-comment">// 当日志文件中的所有记录都已持久化到 SST 文件后，这些日志文件变为过时</span>  std::vector&lt;<span class="hljs-type">uint64_t</span>&gt; log_delete_files;  <span class="hljs-comment">// 存储在清理过程中需要保留的日志文件编号</span>  <span class="hljs-comment">// 这些文件虽然逻辑上已过时，但计划被重用，避免频繁创建新文件</span>  std::vector&lt;<span class="hljs-type">uint64_t</span>&gt; log_recycle_files;  <span class="hljs-comment">// 存储需要删除的过时清单文件的路径</span>  <span class="hljs-comment">// 当生成新的清单文件后，旧的清单文件成为过时文件</span>  std::vector&lt;std::string&gt; manifest_delete_files;  <span class="hljs-comment">// 存储不再需要的内存表指针，等待释放</span>  <span class="hljs-comment">// 当内存表被刷新到磁盘后，需要释放其占用的内存</span>  autovector&lt;MemTable*&gt; memtables_to_free;  <span class="hljs-comment">// 存储不再需要的日志写入器指针，等待释放</span>  <span class="hljs-comment">// 当对应的日志文件被关闭或不再使用时</span>  autovector&lt;log::Writer*&gt; logs_to_free;  <span class="hljs-comment">// 执行实际的资源清理工作，释放不再需要的内存和文件句柄，但不执行实际的文件删除操作（文件删除通常由专门的线程处理）。</span>  <span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">Clean</span><span class="hljs-params">()</span> </span>&#123;    <span class="hljs-comment">// free superversions</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; sv_context : superversion_contexts) &#123;      sv_context.<span class="hljs-built_in">Clean</span>();    &#125;    <span class="hljs-comment">// free pending memtables</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> m : memtables_to_free) &#123;      <span class="hljs-keyword">delete</span> m;    &#125;    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> l : logs_to_free) &#123;      <span class="hljs-keyword">delete</span> l;    &#125;    memtables_to_free.<span class="hljs-built_in">clear</span>();    logs_to_free.<span class="hljs-built_in">clear</span>();    job_snapshot.<span class="hljs-built_in">reset</span>();  &#125;  <span class="hljs-comment">// 省略其他成员...</span>&#125;;</code></pre><h4 id="DBImpl"><a href="#DBImpl" class="headerlink" title="DBImpl"></a>DBImpl</h4><p><code>DBImpl</code> 类中包含多个与文件清理相关的成员：</p><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">DBImpl</span> : <span class="hljs-keyword">public</span> DB &#123;  <span class="hljs-comment">// 省略其他成员...</span>  <span class="hljs-comment">// `pending_outputs_` 实现一个安全机制，确保后台任务(如压缩、刷盘)创建的文件在任务完成前不会被误删除。它解决了多个并发后台操作之间的文件安全问题</span>  <span class="hljs-comment">// 当后台作业开始时，会捕获当前的文件号并添加到 `pending_outputs_` 中</span>  <span class="hljs-comment">// 由于 RocksDB 的文件号是单调递增的，这意味着 `pending_outputs_` 中的任何文件号都表示&quot;保护线&quot; - 任何编号大于等于这个值的文件都不应被删除</span>  <span class="hljs-comment">// `FindObsoleteFiles()`/`PurgeObsoleteFiles()` 在识别可删除文件时，会参考 `pending_outputs_`，确保不会删除编号大于列表中任何值的文件</span>  <span class="hljs-comment">// 后台任务完成后，从 `pending_outputs_` 中删除对应的文件号，允许这些文件在不再需要时被清理</span>  std::list&lt;<span class="hljs-type">uint64_t</span>&gt; pending_outputs_;  <span class="hljs-comment">// 踪已找到需要删除但尚未完成删除的文件批次数量</span>  <span class="hljs-comment">// 当 `FindObsoleteFiles` 识别出过时文件后，在 `PurgeObsoleteFiles` 真正删除前增加此计数</span>  <span class="hljs-type">int</span> pending_purge_obsolete_files_;  <span class="hljs-comment">// 存储待清理文件的详细信息(文件号、文件名、路径、类型等)</span>  <span class="hljs-comment">// `FindObsoleteFiles`收集要删除的文件，`PurgeObsoleteFiles`从此映射中读取并执行删除</span>  std::unordered_map&lt;<span class="hljs-type">uint64_t</span>, PurgeFileInfo&gt; purge_files_;  <span class="hljs-comment">// 记录已分配给特定任务上下文的文件号</span>  <span class="hljs-comment">// 确保正在进行删除操作的文件不会被其他任务处理</span>  std::unordered_set&lt;<span class="hljs-type">uint64_t</span>&gt; files_grabbed_for_purge_;  <span class="hljs-comment">// 维护当前活跃(未过时)的WAL日志文件列表</span>  <span class="hljs-comment">// 跟踪哪些日志文件仍在使用，防止被错误删除</span>  std::deque&lt;LogFileNumberSize&gt; alive_log_files_;  <span class="hljs-comment">// 存储尚未完全同步和当前正在写入的日志文件</span>  <span class="hljs-comment">// 管理活跃日志的生命周期，日志同步后可能成为过时候选</span>  std::deque&lt;LogWriterNumber&gt; logs_;  <span class="hljs-comment">// 存储可重用的日志文件号</span>  <span class="hljs-comment">// 避免频繁创建新文件，优先重用已有文件提高效率</span>  std::deque&lt;<span class="hljs-type">uint64_t</span>&gt; log_recycle_files_;  <span class="hljs-comment">// 存储需要在后台线程中删除的日志写入器</span>  <span class="hljs-comment">// 异步清理不再需要的日志文件，减少主线程阻塞</span>  autovector&lt;log::Writer*&gt; logs_to_free_;  <span class="hljs-comment">// 存储等待关闭的日志写入器队列</span>  <span class="hljs-comment">// 推迟日志文件的关闭操作，以优化I/O操作</span>  std::deque&lt;log::Writer*&gt; logs_to_free_queue_;  <span class="hljs-comment">// 记录后台任务正在使用的文件号，防止清理过程删除这些文件</span>  <span class="hljs-comment">// 保护正在创建或处理中的文件不被过早删除</span>  std::list&lt;<span class="hljs-type">uint64_t</span>&gt; pending_outputs_;  <span class="hljs-comment">// 控制是否允许删除过时文件的开关</span>  <span class="hljs-comment">// 在特定操作(如备份、快照)期间临时禁用文件删除</span>  <span class="hljs-type">int</span> disable_delete_obsolete_files_;  <span class="hljs-comment">// 记录上次执行完整扫描删除操作的时间戳</span>  <span class="hljs-comment">// 控制删除操作的频率，避免过于频繁的磁盘扫描</span>  <span class="hljs-type">uint64_t</span> delete_obsolete_files_last_run_;  <span class="hljs-comment">// 省略其他成员...</span>&#125;</code></pre><h3 id="2-2-核心函数"><a href="#2-2-核心函数" class="headerlink" title="2.2 核心函数"></a>2.2 核心函数</h3><h4 id="2-2-1-SST-文件生命周期"><a href="#2-2-1-SST-文件生命周期" class="headerlink" title="2.2.1 SST 文件生命周期"></a>2.2.1 SST 文件生命周期</h4><p>RocksDB 使用引用计数机制管理 SST 文件生命周期。每个文件有一个引用计数器，当引用计数变为 0 时，文件被标记为可删除。</p><p>关键数据结构：</p><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Version</span> &#123;  <span class="hljs-comment">// 引用计数</span>  <span class="hljs-type">int</span> refs_;    <span class="hljs-comment">// 层级化存储的文件</span>  std::vector&lt;FileMetaData*&gt; files_[num_levels_];&#125;;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">FileMetaData</span> &#123;  <span class="hljs-comment">// 文件的引用计数</span>  <span class="hljs-type">int</span> refs;    <span class="hljs-comment">// 文件描述符</span>  FileDescriptor fd;&#125;;</code></pre><p>RocksDB 使用 <code>VersionSet</code> 来管理数据库在不同时间点的状态快照，每个快照称为一个 <code>Version</code>。每个 <code>Version</code> 包含一组在该时间点“存活”的 SST 文件列表。SST 文件通过引用计数（<code>FileMetaData::refs</code>）来跟踪其被多少个 <code>Version</code> 引用。</p><p>当一个 <code>Version</code> 不再被任何快照、迭代器或其他内部结构引用时，它的析构函数 <code>Version::~Version</code> 会被调用。 <code>Version</code> 析构函数会减少所有引用文件的计数。</p><p>当文件的引用计数降为 0，说明没有任何版本在使用该文件，此时它会被添加到 <code>obsolete_files_</code> 列表中，等待后续的物理删除。</p><pre><code class="hljs cpp">Version::~<span class="hljs-built_in">Version</span>() &#123;  <span class="hljs-comment">// 确保引用计数为0，即没有任何地方引用此版本</span>  <span class="hljs-built_in">assert</span>(refs_ == <span class="hljs-number">0</span>);  <span class="hljs-comment">// 从版本双向链表中移除自身</span>  prev_-&gt;next_ = next_;  next_-&gt;prev_ = prev_;  <span class="hljs-comment">// 遍历每个层级的所有文件，减少它们的引用计数</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> level = <span class="hljs-number">0</span>; level &lt; storage_info_.num_levels_; level++) &#123;    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; storage_info_.files_[level].<span class="hljs-built_in">size</span>(); i++) &#123;      FileMetaData* f = storage_info_.files_[level][i];      <span class="hljs-built_in">assert</span>(f-&gt;refs &gt; <span class="hljs-number">0</span>);      f-&gt;refs--;      <span class="hljs-comment">// 如果引用计数降为0，表示没有任何版本在使用此文件</span>      <span class="hljs-keyword">if</span> (f-&gt;refs &lt;= <span class="hljs-number">0</span>) &#123;        <span class="hljs-built_in">assert</span>(cfd_ != <span class="hljs-literal">nullptr</span>);        <span class="hljs-type">uint32_t</span> path_id = f-&gt;fd.<span class="hljs-built_in">GetPathId</span>();        <span class="hljs-built_in">assert</span>(path_id &lt; cfd_-&gt;<span class="hljs-built_in">ioptions</span>()-&gt;cf_paths.<span class="hljs-built_in">size</span>());        <span class="hljs-comment">// 将文件添加到版本集的过期文件列表中(obsolete_files_)</span>        vset_-&gt;obsolete_files_.<span class="hljs-built_in">push_back</span>(            <span class="hljs-built_in">ObsoleteFileInfo</span>(f, cfd_-&gt;<span class="hljs-built_in">ioptions</span>()-&gt;cf_paths[path_id].path,                           cfd_-&gt;<span class="hljs-built_in">GetFileMetadataCacheReservationManager</span>()));      &#125;    &#125;  &#125;&#125;</code></pre><h4 id="2-2-2-识别过期文件：FindObsoleteFiles"><a href="#2-2-2-识别过期文件：FindObsoleteFiles" class="headerlink" title="2.2.2 识别过期文件：FindObsoleteFiles"></a>2.2.2 识别过期文件：FindObsoleteFiles</h4><p><code>FindObsoleteFiles</code> 负责识别哪些文件已经过期，需要在<strong>持有数据库互斥锁</strong>的情况下调用：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 该方法用于寻找过时文件，将它们添加到 job_context 中以便后续删除</span><span class="hljs-comment">// * 将活跃的 SST 文件列表存储在 &#x27;sst_live&#x27; 和活跃的 blob 文件列表存储在 &#x27;blob_live&#x27;</span><span class="hljs-comment">// 如果执行全量扫描:</span><span class="hljs-comment">// * 将文件系统中所有文件的列表存储在 &#x27;full_scan_candidate_files&#x27;</span><span class="hljs-comment">// 否则，从 VersionSet 获取过时文件</span><span class="hljs-comment">//</span><span class="hljs-comment">// no_full_scan = true -- 第一优先级：明确禁止全量扫描</span><span class="hljs-comment">// force = true -- 第二优先级：强制全量扫描</span><span class="hljs-comment">// force = false -- 第三优先级：除非到达周期（每 mutable_db_options_.delete_obsolete_files_period_micros 一次），否则不强制全量扫描</span><span class="hljs-comment">// 函数声明：void FindObsoleteFiles(JobContext* job_context, bool force, bool no_full_scan = false);</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::FindObsoleteFiles</span><span class="hljs-params">(JobContext* job_context, <span class="hljs-type">bool</span> force,</span></span><span class="hljs-params"><span class="hljs-function">                               <span class="hljs-type">bool</span> no_full_scan)</span> </span>&#123;  mutex_.<span class="hljs-built_in">AssertHeld</span>();  <span class="hljs-comment">// 确认互斥锁已被持有</span>  <span class="hljs-comment">// 如果禁用了文件删除功能，则不执行任何操作</span>  <span class="hljs-keyword">if</span> (disable_delete_obsolete_files_ &gt; <span class="hljs-number">0</span>) &#123;    <span class="hljs-keyword">return</span>;  &#125;  <span class="hljs-type">bool</span> doing_the_full_scan = <span class="hljs-literal">false</span>;  <span class="hljs-comment">// 是否执行全量扫描的标志</span>  <span class="hljs-comment">// 判断是否执行全量扫描的逻辑</span>  <span class="hljs-keyword">if</span> (no_full_scan) &#123;    doing_the_full_scan = <span class="hljs-literal">false</span>;  <span class="hljs-comment">// 如果明确指定不进行全量扫描，则不执行</span>  &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (force ||             mutable_db_options_.delete_obsolete_files_period_micros == <span class="hljs-number">0</span>) &#123;    doing_the_full_scan = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 如果强制执行或删除周期设置为0，则执行全量扫描</span>  &#125; <span class="hljs-keyword">else</span> &#123;    <span class="hljs-type">const</span> <span class="hljs-type">uint64_t</span> now_micros = immutable_db_options_.clock-&gt;<span class="hljs-built_in">NowMicros</span>();    <span class="hljs-comment">// 根据上次扫描时间和配置的周期决定是否执行全量扫描</span>    <span class="hljs-keyword">if</span> ((delete_obsolete_files_last_run_ +         mutable_db_options_.delete_obsolete_files_period_micros) &lt;        now_micros) &#123;      doing_the_full_scan = <span class="hljs-literal">true</span>;      delete_obsolete_files_last_run_ = now_micros;  <span class="hljs-comment">// 更新上次扫描时间</span>    &#125;  &#125;  <span class="hljs-comment">/****** SST/Blob 文件处理部分 ******/</span>  <span class="hljs-comment">// 设置最小的 pending output 文件号，防止删除正在被 compaction 线程写入的文件</span>  <span class="hljs-comment">// 注意：扫描期间不能释放 mutex_，否则可能出现竞态</span>  job_context-&gt;min_pending_output = <span class="hljs-built_in">MinObsoleteSstNumberToKeep</span>();  <span class="hljs-comment">// 获取过时文件。此函数还将更新 VersionSet 的 pending 文件列表</span>  versions_-&gt;<span class="hljs-built_in">GetObsoleteFiles</span>(      &amp;job_context-&gt;sst_delete_files, &amp;job_context-&gt;blob_delete_files,      &amp;job_context-&gt;manifest_delete_files, job_context-&gt;min_pending_output);  <span class="hljs-comment">// 将 job_context-&gt;sst_delete_files 和 job_context-&gt;blob_delete_files 中的元素</span>  <span class="hljs-comment">// 标记为&quot;已获取用于清理&quot;，其他线程调用 FindObsoleteFiles 时将不会将这些文件</span>  <span class="hljs-comment">// 添加到清理候选列表中，避免多线程重复处理</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; sst_to_del : job_context-&gt;sst_delete_files) &#123;    <span class="hljs-built_in">MarkAsGrabbedForPurge</span>(sst_to_del.metadata-&gt;fd.<span class="hljs-built_in">GetNumber</span>());  &#125;  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; blob_file : job_context-&gt;blob_delete_files) &#123;    <span class="hljs-built_in">MarkAsGrabbedForPurge</span>(blob_file.<span class="hljs-built_in">GetBlobFileNumber</span>());  &#125;  <span class="hljs-comment">// 存储当前的文件编号、日志编号等信息到 job_context</span>  job_context-&gt;manifest_file_number = versions_-&gt;<span class="hljs-built_in">manifest_file_number</span>();  job_context-&gt;pending_manifest_file_number =      versions_-&gt;<span class="hljs-built_in">pending_manifest_file_number</span>();  job_context-&gt;log_number = <span class="hljs-built_in">MinLogNumberToKeep</span>();  <span class="hljs-comment">// 获取需要保留的最小日志编号</span>  job_context-&gt;prev_log_number = versions_-&gt;<span class="hljs-built_in">prev_log_number</span>();  <span class="hljs-keyword">if</span> (doing_the_full_scan) &#123;    <span class="hljs-comment">// 如果执行全量扫描，收集所有活跃的文件</span>    versions_-&gt;<span class="hljs-built_in">AddLiveFiles</span>(&amp;job_context-&gt;sst_live, &amp;job_context-&gt;blob_live);    <span class="hljs-function">InfoLogPrefix <span class="hljs-title">info_log_prefix</span><span class="hljs-params">(!immutable_db_options_.db_log_dir.empty(),</span></span><span class="hljs-params"><span class="hljs-function">                                  dbname_)</span></span>;    <span class="hljs-comment">// 收集所有数据库路径</span>    <span class="hljs-comment">// 多路径数据库路径支持 “热冷数据分层存储”&amp;“当单个存储设备容量不足时，可以将数据分散到多个设备”</span>    <span class="hljs-comment">//</span>    <span class="hljs-comment">// 举例：</span>    <span class="hljs-comment">// L0-L1层(热数据) → 快速SSD</span>    <span class="hljs-comment">// L2-L3层(温数据) → 普通SSD</span>    <span class="hljs-comment">// L4-L6层(冷数据) → 大容量HDD</span>    std::set&lt;std::string&gt; paths;    <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> path_id = <span class="hljs-number">0</span>; path_id &lt; immutable_db_options_.db_paths.<span class="hljs-built_in">size</span>();         path_id++) &#123;      paths.<span class="hljs-built_in">insert</span>(immutable_db_options_.db_paths[path_id].path);    &#125;    <span class="hljs-comment">// 注意：如果列族选项中没有指定 cf_paths，使用 db_paths 作为 cf_paths 设置。</span>    <span class="hljs-comment">// 因此，在下面的代码中可能会有多个重复的 db_paths 文件。重复项在 PurgeObsoleteFiles 中标识唯一文件时会被删除。</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> cfd : *versions_-&gt;<span class="hljs-built_in">GetColumnFamilySet</span>()) &#123;      <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> path_id = <span class="hljs-number">0</span>; path_id &lt; cfd-&gt;<span class="hljs-built_in">ioptions</span>()-&gt;cf_paths.<span class="hljs-built_in">size</span>();           path_id++) &#123;        <span class="hljs-keyword">auto</span>&amp; path = cfd-&gt;<span class="hljs-built_in">ioptions</span>()-&gt;cf_paths[path_id].path;        <span class="hljs-keyword">if</span> (paths.<span class="hljs-built_in">find</span>(path) == paths.<span class="hljs-built_in">end</span>()) &#123;          paths.<span class="hljs-built_in">insert</span>(path);        &#125;      &#125;    &#125;    IOOptions io_opts;    io_opts.do_not_recurse = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 不进行递归查询</span>    <span class="hljs-comment">// 遍历所有路径，查找潜在的过时文件</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; path : paths) &#123;      <span class="hljs-comment">// 获取目录中的所有文件列表</span>      <span class="hljs-comment">// 后续处理将排除仍然活跃的文件</span>      std::vector&lt;std::string&gt; files;      Status s = immutable_db_options_.fs-&gt;<span class="hljs-built_in">GetChildren</span>(          path, io_opts, &amp;files, <span class="hljs-comment">/*IODebugContext*=*/</span><span class="hljs-literal">nullptr</span>);      s.<span class="hljs-built_in">PermitUncheckedError</span>();  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 错误处理需要改进</span>      <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> std::string&amp; file : files) &#123;        <span class="hljs-type">uint64_t</span> number;        FileType type;        <span class="hljs-comment">// 如果无法解析文件名，跳过</span>        <span class="hljs-comment">// 如果文件已被其他压缩任务获取用于清除，或已安排清除，也跳过</span>        <span class="hljs-comment">// 避免在竞态条件下重复删除相同的文件</span>        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">ParseFileName</span>(file, &amp;number, info_log_prefix.prefix, &amp;type) ||            !<span class="hljs-built_in">ShouldPurge</span>(number)) &#123;          <span class="hljs-keyword">continue</span>;        &#125;        <span class="hljs-comment">// 将文件添加到候选清除文件列表</span>        job_context-&gt;full_scan_candidate_files.<span class="hljs-built_in">emplace_back</span>(<span class="hljs-string">&quot;/&quot;</span> + file, path);      &#125;    &#125;    <span class="hljs-comment">/****** WAL 路径单独配置时，WAL 文件处理部分 ******/</span>    <span class="hljs-comment">// 添加 wal_dir 中的日志文件</span>    <span class="hljs-keyword">if</span> (!immutable_db_options_.<span class="hljs-built_in">IsWalDirSameAsDBPath</span>(dbname_)) &#123;      std::vector&lt;std::string&gt; log_files;      Status s = immutable_db_options_.fs-&gt;<span class="hljs-built_in">GetChildren</span>(          immutable_db_options_.wal_dir, io_opts, &amp;log_files,          <span class="hljs-comment">/*IODebugContext*=*/</span><span class="hljs-literal">nullptr</span>);      s.<span class="hljs-built_in">PermitUncheckedError</span>();  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 错误处理需要改进</span>      <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> std::string&amp; log_file : log_files) &#123;        job_context-&gt;full_scan_candidate_files.<span class="hljs-built_in">emplace_back</span>(            log_file, immutable_db_options_.wal_dir);      &#125;    &#125;    <span class="hljs-comment">/****** LOG 文件处理部分 ******/</span>    <span class="hljs-comment">// 添加 db_log_dir 中的信息日志文件</span>    <span class="hljs-keyword">if</span> (!immutable_db_options_.db_log_dir.<span class="hljs-built_in">empty</span>() &amp;&amp;        immutable_db_options_.db_log_dir != dbname_) &#123;      std::vector&lt;std::string&gt; info_log_files;      Status s = immutable_db_options_.fs-&gt;<span class="hljs-built_in">GetChildren</span>(          immutable_db_options_.db_log_dir, io_opts, &amp;info_log_files,          <span class="hljs-comment">/*IODebugContext*=*/</span><span class="hljs-literal">nullptr</span>);      s.<span class="hljs-built_in">PermitUncheckedError</span>();  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 错误处理需要改进</span>      <span class="hljs-keyword">for</span> (std::string&amp; log_file : info_log_files) &#123;        job_context-&gt;full_scan_candidate_files.<span class="hljs-built_in">emplace_back</span>(            log_file, immutable_db_options_.db_log_dir);      &#125;    &#125;  &#125; <span class="hljs-keyword">else</span> &#123;    <span class="hljs-comment">// 如果不执行全量扫描，直接从待删除文件列表中移除在任何版本中出现的文件</span>    <span class="hljs-comment">// 因为候选文件通常只占所有文件的一小部分，所以与构建所有文件的映射相比，</span>    <span class="hljs-comment">// 直接检查它们是否在任何版本中出现更高效</span>    versions_-&gt;<span class="hljs-built_in">RemoveLiveFiles</span>(job_context-&gt;sst_delete_files,                               job_context-&gt;blob_delete_files);  &#125;  <span class="hljs-comment">// 在可能释放互斥锁和等待条件变量之前，增加 pending_purge_obsolete_files_</span>  <span class="hljs-comment">// 这样另一个执行 `GetSortedWals` 的线程将等待直到这个线程完成执行</span>  <span class="hljs-comment">// 因为另一个线程将等待 `pending_purge_obsolete_files_`</span>  <span class="hljs-comment">// 如果没有需要删除的内容，必须递减 pending_purge_obsolete_files_</span>  ++pending_purge_obsolete_files_;  <span class="hljs-comment">// 设置一个延迟执行的清理操作，确保在没有需要删除的内容时减少 pending_purge_obsolete_files_</span>  <span class="hljs-function">Defer <span class="hljs-title">cleanup</span><span class="hljs-params">([job_context, <span class="hljs-keyword">this</span>]() &#123;</span></span><span class="hljs-params"><span class="hljs-function">    assert(job_context != <span class="hljs-literal">nullptr</span>);</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-keyword">if</span> (!job_context-&gt;HaveSomethingToDelete()) &#123;</span></span><span class="hljs-params"><span class="hljs-function">      mutex_.AssertHeld();</span></span><span class="hljs-params"><span class="hljs-function">      --pending_purge_obsolete_files_;</span></span><span class="hljs-params"><span class="hljs-function">    &#125;</span></span><span class="hljs-params"><span class="hljs-function">  &#125;)</span></span>;  <span class="hljs-comment">// 当在恢复期间调用时，logs_ 为空，此时还没有任何需要跟踪的过时日志</span>  log_write_mutex_.<span class="hljs-built_in">Lock</span>();  <span class="hljs-keyword">if</span> (alive_log_files_.<span class="hljs-built_in">empty</span>() || logs_.<span class="hljs-built_in">empty</span>()) &#123;    mutex_.<span class="hljs-built_in">AssertHeld</span>();    <span class="hljs-comment">// 如果数据库是 DBImplSecondary，可能会到达这里</span>    log_write_mutex_.<span class="hljs-built_in">Unlock</span>();    <span class="hljs-keyword">return</span>;  &#125;  <span class="hljs-comment">/****** 物理日志文件处理部分 ******/</span>  <span class="hljs-type">bool</span> mutex_unlocked = <span class="hljs-literal">false</span>;  <span class="hljs-keyword">if</span> (!alive_log_files_.<span class="hljs-built_in">empty</span>() &amp;&amp; !logs_.<span class="hljs-built_in">empty</span>()) &#123;    <span class="hljs-type">uint64_t</span> min_log_number = job_context-&gt;log_number;    <span class="hljs-type">size_t</span> num_alive_log_files = alive_log_files_.<span class="hljs-built_in">size</span>();    <span class="hljs-comment">// 查找新的过时日志文件</span>    <span class="hljs-keyword">while</span> (alive_log_files_.<span class="hljs-built_in">begin</span>()-&gt;number &lt; min_log_number) &#123;      <span class="hljs-keyword">auto</span>&amp; earliest = *alive_log_files_.<span class="hljs-built_in">begin</span>();      <span class="hljs-comment">// 如果配置了日志文件回收，且回收列表未满，则添加到回收列表</span>      <span class="hljs-keyword">if</span> (immutable_db_options_.recycle_log_file_num &gt;          log_recycle_files_.<span class="hljs-built_in">size</span>()) &#123;        <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log,                       <span class="hljs-string">&quot;adding log %&quot;</span> PRIu64 <span class="hljs-string">&quot; to recycle list\n&quot;</span>,                       earliest.number);        <span class="hljs-comment">// 放入回收列表以便复用</span>        log_recycle_files_.<span class="hljs-built_in">push_back</span>(earliest.number);      &#125; <span class="hljs-keyword">else</span> &#123;        <span class="hljs-comment">// 否则添加到待删除列表</span>        job_context-&gt;log_delete_files.<span class="hljs-built_in">push_back</span>(earliest.number);      &#125;      <span class="hljs-keyword">if</span> (job_context-&gt;size_log_to_delete == <span class="hljs-number">0</span>) &#123;        job_context-&gt;prev_total_log_size = total_log_size_;        job_context-&gt;num_alive_log_files = num_alive_log_files;      &#125;      <span class="hljs-comment">// 更新统计信息</span>      job_context-&gt;size_log_to_delete += earliest.size;      total_log_size_ -= earliest.size;      <span class="hljs-comment">// 从活跃列表中移除</span>      alive_log_files_.<span class="hljs-built_in">pop_front</span>();      <span class="hljs-comment">// 当前日志应该始终保持活跃状态，因为它不可能有 number &lt; MinLogNumber()</span>      <span class="hljs-built_in">assert</span>(alive_log_files_.<span class="hljs-built_in">size</span>());    &#125;    log_write_mutex_.<span class="hljs-built_in">Unlock</span>();    mutex_.<span class="hljs-built_in">Unlock</span>();    mutex_unlocked = <span class="hljs-literal">true</span>;    <span class="hljs-built_in">TEST_SYNC_POINT_CALLBACK</span>(<span class="hljs-string">&quot;FindObsoleteFiles::PostMutexUnlock&quot;</span>, <span class="hljs-literal">nullptr</span>);    log_write_mutex_.<span class="hljs-built_in">Lock</span>();    <span class="hljs-comment">/****** 日志 Writer 对象处理部分 ******/</span>    <span class="hljs-keyword">while</span> (!logs_.<span class="hljs-built_in">empty</span>() &amp;&amp; logs_.<span class="hljs-built_in">front</span>().number &lt; min_log_number) &#123;      <span class="hljs-keyword">auto</span>&amp; log = logs_.<span class="hljs-built_in">front</span>();      <span class="hljs-keyword">if</span> (log.<span class="hljs-built_in">IsSyncing</span>()) &#123;        <span class="hljs-comment">// 如果日志正在同步，等待同步完成</span>        log_sync_cv_.<span class="hljs-built_in">Wait</span>();        <span class="hljs-comment">// 等待期间 logs_ 可能已更改，继续下一轮循环</span>        <span class="hljs-keyword">continue</span>;      &#125;      logs_to_free_.<span class="hljs-built_in">push_back</span>(log.<span class="hljs-built_in">ReleaseWriter</span>());      logs_.<span class="hljs-built_in">pop_front</span>();    &#125;    <span class="hljs-comment">// 当前日志不可能过时</span>    <span class="hljs-built_in">assert</span>(!logs_.<span class="hljs-built_in">empty</span>());  &#125;  <span class="hljs-comment">// 清理 DB::Write() 操作</span>  <span class="hljs-built_in">assert</span>(job_context-&gt;logs_to_free.<span class="hljs-built_in">empty</span>());  job_context-&gt;logs_to_free = logs_to_free_;  logs_to_free_.<span class="hljs-built_in">clear</span>();  log_write_mutex_.<span class="hljs-built_in">Unlock</span>();  <span class="hljs-keyword">if</span> (mutex_unlocked) &#123;    mutex_.<span class="hljs-built_in">Lock</span>();  &#125;  job_context-&gt;log_recycle_files.<span class="hljs-built_in">assign</span>(log_recycle_files_.<span class="hljs-built_in">begin</span>(),                                        log_recycle_files_.<span class="hljs-built_in">end</span>());&#125;</code></pre><h5 id="2-2-2-1-常规清理-vs-全量扫描-Full-Scan"><a href="#2-2-2-1-常规清理-vs-全量扫描-Full-Scan" class="headerlink" title="2.2.2.1 常规清理 vs. 全量扫描 (Full Scan)"></a>2.2.2.1 常规清理 vs. 全量扫描 (Full Scan)</h5><p><code>FindObsoleteFiles</code> 方法区分<strong>常规清理</strong>和<strong>全量扫描</strong>，在持有锁的情况下，是基于性能和可靠性之间的权衡设计。<strong>常规清理模式主要依赖 RocksDB 的内存状态来识别过时文件，该模式只检查那些已经在版本控制系统（<code>VersionSet</code>）中被标记为过时的文件。全量扫描模式会扫描数据库目录中的所有物理文件，需要遍历文件系统中的所有文件，这是一个 I&#x2F;O 密集型操作。</strong></p><p>在以下情况下，文件可能不会被正确标记为过时，但仍然需要清理：</p><ul><li>进程在 compaction&#x2F;flush&#x2F;ingestion 中途崩溃，留下未完成的临时文件</li><li>软件版本升级或 bug 修复，导致旧版本产生的一些文件未被跟踪</li></ul><p>全量扫描可以识别这些 “ 孤儿 “ 文件，防止长期的空间泄漏。在 <code>DBImpl::Open</code> 方法中，我们可以看到 RocksDB 会主动触发清理：</p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">DBImpl::Open</span><span class="hljs-params">(<span class="hljs-type">const</span> DBOptions&amp; db_options, <span class="hljs-type">const</span> std::string&amp; dbname,</span></span><span class="hljs-params"><span class="hljs-function">                    <span class="hljs-type">const</span> std::vector&lt;ColumnFamilyDescriptor&gt;&amp; column_families,</span></span><span class="hljs-params"><span class="hljs-function">                    std::vector&lt;ColumnFamilyHandle*&gt;* handles, DB** dbptr,</span></span><span class="hljs-params"><span class="hljs-function">                    <span class="hljs-type">const</span> <span class="hljs-type">bool</span> seq_per_batch, <span class="hljs-type">const</span> <span class="hljs-type">bool</span> batch_per_txn)</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>    <span class="hljs-keyword">if</span> (s.<span class="hljs-built_in">ok</span>()) &#123;    <span class="hljs-comment">// Persist RocksDB Options before scheduling the compaction.</span>    <span class="hljs-comment">// The WriteOptionsFile() will release and lock the mutex internally.</span>    persist_options_status =        impl-&gt;<span class="hljs-built_in">WriteOptionsFile</span>(<span class="hljs-literal">true</span> <span class="hljs-comment">/*db_mutex_already_held*/</span>);    *dbptr = impl;    impl-&gt;opened_successfully_ = <span class="hljs-literal">true</span>;    impl-&gt;<span class="hljs-built_in">DeleteObsoleteFiles</span>();  &#125;    <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><p>确保即使是上次异常退出留下的未完成 compaction 文件也能被及时清理。</p><h5 id="2-2-2-2-过期清理触发事件"><a href="#2-2-2-2-过期清理触发事件" class="headerlink" title="2.2.2.2 过期清理触发事件"></a>2.2.2.2 过期清理触发事件</h5><p>RocksDB 在多个关键点触发过期文件清理，保证及时释放磁盘空间：</p><p><strong>迭代器销毁时：ForwardIterator::SVCleanup</strong></p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">ForwardIterator::SVCleanup</span><span class="hljs-params">(DBImpl* db, SuperVersion* sv,</span></span><span class="hljs-params"><span class="hljs-function">                                <span class="hljs-type">bool</span> background_purge_on_iterator_cleanup)</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>  db-&gt;<span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, <span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>);  <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;    db-&gt;<span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context, background_purge_on_iterator_cleanup);  &#125;  <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><p>当迭代器被销毁时，它可能持有对某些版本的引用。一旦释放这些引用，可能导致某些 SST 文件变为过期状态。迭代器销毁时会检查并清理这些文件。如果设置了 <code>background_purge_on_iterator_cleanup=true</code>，清理操作会在后台执行，避免阻塞用户线程。</p><p><strong>范围删除后：DBImpl::DeleteFilesInRanges</strong></p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">DBImpl::DeleteFilesInRanges</span><span class="hljs-params">(ColumnFamilyHandle* column_family,</span></span><span class="hljs-params"><span class="hljs-function">                                   <span class="hljs-type">const</span> RangePtr* ranges, <span class="hljs-type">size_t</span> n,</span></span><span class="hljs-params"><span class="hljs-function">                                   <span class="hljs-type">bool</span> include_end)</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>  <span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, <span class="hljs-literal">false</span>);  <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;    <span class="hljs-comment">// Call PurgeObsoleteFiles() without holding mutex.</span>    <span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context);  &#125;  <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><p>执行范围删除（DeleteFilesInRanges）后，会有大量 SST 文件变为过期，此时会触发文件清理。</p><p><strong>启用文件删除功能时：DBImpl::EnableFileDeletions</strong></p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">DBImpl::EnableFileDeletions</span><span class="hljs-params">(<span class="hljs-type">bool</span> force)</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>  <span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, <span class="hljs-literal">true</span>);  <span class="hljs-keyword">if</span> (saved_counter == <span class="hljs-number">0</span>) &#123;    <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log, <span class="hljs-string">&quot;File Deletions Enabled&quot;</span>);    <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;      <span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context);    &#125;  &#125;  <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><p>RocksDB 允许暂时禁用文件删除（例如在备份或创建快照时）。当文件删除功能被重新启用时，会清理之前累积的过期文件。</p><p>不同线程同时需要暂停文件删除时，每个线程都会调用 <code>DisableFileDeletions()</code>，导致计数器累加。RocksDB 内部不同组件可能各自调用 <code>DisableFileDeletions()</code>，例如：</p><ul><li>快照创建过程</li><li>备份操作进行时</li><li>某些迭代器依赖特定文件时</li></ul><p>计数器设计是有意为之，确保只有当所有禁用请求都被释放后（计数器回到 0），才会重新启用文件删除。</p><p><strong>刷盘操作后：DBImpl::BackgroundCallFlush</strong></p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::BackgroundCallFlush</span><span class="hljs-params">(Env::Priority thread_pri)</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>  <span class="hljs-comment">// If flush failed, we want to delete all temporary files that we might</span>  <span class="hljs-comment">// have created. Thus, we force full scan in FindObsoleteFiles()</span>  <span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, !s.<span class="hljs-built_in">ok</span>() &amp;&amp; !s.<span class="hljs-built_in">IsShutdownInProgress</span>() &amp;&amp;                            !s.<span class="hljs-built_in">IsColumnFamilyDropped</span>());  <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToClean</span>() || job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>() || !log_buffer.<span class="hljs-built_in">IsEmpty</span>()) &#123;    mutex_.<span class="hljs-built_in">Unlock</span>();    <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;      <span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context);    &#125;  &#125;  <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><p>当内存表刷新到磁盘形成 SST 文件后，对应的 WAL 文件可能变为过期，此时会触发清理。</p><p><strong>Compact 操作后：DBImpl::CompactFiles</strong></p><pre><code class="hljs cpp"><span class="hljs-function">Status <span class="hljs-title">DBImpl::CompactFiles</span><span class="hljs-params">(<span class="hljs-type">const</span> CompactionOptions&amp; compact_options,</span></span><span class="hljs-params"><span class="hljs-function">                            ColumnFamilyHandle* column_family,</span></span><span class="hljs-params"><span class="hljs-function">                            <span class="hljs-type">const</span> std::vector&lt;std::string&gt;&amp; input_file_names,</span></span><span class="hljs-params"><span class="hljs-function">                            <span class="hljs-type">const</span> <span class="hljs-type">int</span> output_level, <span class="hljs-type">const</span> <span class="hljs-type">int</span> output_path_id,</span></span><span class="hljs-params"><span class="hljs-function">                            std::vector&lt;std::string&gt;* <span class="hljs-type">const</span> output_file_names,</span></span><span class="hljs-params"><span class="hljs-function">                            CompactionJobInfo* compaction_job_info)</span> </span>&#123;    <span class="hljs-comment">// If !s.ok(), this means that Compaction failed. In that case, we want</span>    <span class="hljs-comment">// to delete all obsolete files we might have created and we force</span>    <span class="hljs-comment">// FindObsoleteFiles(). This is because job_context does not</span>    <span class="hljs-comment">// catch all created files if compaction failed.</span>    <span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, !s.<span class="hljs-built_in">ok</span>());  <span class="hljs-comment">// delete unnecessary files if any, this is done outside the mutex</span>  <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToClean</span>() ||      job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>() || !log_buffer.<span class="hljs-built_in">IsEmpty</span>()) &#123;    <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;      <span class="hljs-comment">// no mutex is locked here.  No need to Unlock() and Lock() here.</span>      <span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context);    &#125;    job_context.<span class="hljs-built_in">Clean</span>();  &#125;&#125;</code></pre><p>压缩是 RocksDB 中导致文件过期的主要操作。压缩会将多个小文件合并为较少的大文件，原来的小文件变为过期文件需要清理。</p><p><strong>主动清理：<code>DBImpl::DeleteObsoleteFiles</code></strong></p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::DeleteObsoleteFiles</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-comment">// 省略其他代码...</span>  <span class="hljs-function">JobContext <span class="hljs-title">job_context</span><span class="hljs-params">(next_job_id_.fetch_add(<span class="hljs-number">1</span>))</span></span>;  <span class="hljs-built_in">FindObsoleteFiles</span>(&amp;job_context, <span class="hljs-literal">true</span>);  <span class="hljs-keyword">if</span> (job_context.<span class="hljs-built_in">HaveSomethingToDelete</span>()) &#123;    <span class="hljs-type">bool</span> defer_purge = immutable_db_options_.avoid_unnecessary_blocking_io;    <span class="hljs-built_in">PurgeObsoleteFiles</span>(job_context, defer_purge);  &#125;  <span class="hljs-comment">// 省略其他代码...</span>&#125;</code></pre><h5 id="2-2-2-3-“周期性”全量清理"><a href="#2-2-2-3-“周期性”全量清理" class="headerlink" title="2.2.2.3 “周期性”全量清理"></a>2.2.2.3 “周期性”全量清理</h5><p>在 <code>DBImpl::FindObsoleteFiles</code> 中，有一段“周期性”执行全量扫描的代码：</p><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> ((delete_obsolete_files_last_run_ +     mutable_db_options_.delete_obsolete_files_period_micros) &lt;    now_micros) &#123;  doing_the_full_scan = <span class="hljs-literal">true</span>;  delete_obsolete_files_last_run_ = now_micros;&#125;</code></pre><p>这段代码容易产生误解，看起来像是定时任务的实现。但实际上，该参数 <code>delete_obsolete_files_period_micros</code>（默认 6 小时）只是用来判断<strong>已触发</strong>的 <code>FindObsoleteFiles</code> 调用是否应执行全量扫描。它不会自动创建调用 <code>FindObsoleteFiles</code> 的计时器或后台任务。<strong>如果数据库长时间没有任何过期清理触发事件，那么即使超过 <code>delete_obsolete_files_period_micros</code> 设置的时间（默认 6 小时），也不会自动触发全量扫描来清理过期文件。积累的过期文件会直至下一次触发事件发生，才会实际删除。</strong></p><h5 id="2-2-2-4-文件回收机制"><a href="#2-2-2-4-文件回收机制" class="headerlink" title="2.2.2.4 文件回收机制"></a>2.2.2.4 文件回收机制</h5><p>RocksDB 支持 WAL 文件回收，避免频繁创建新文件：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 如果配置了日志文件回收，且回收列表未满</span><span class="hljs-keyword">if</span> (immutable_db_options_.recycle_log_file_num &gt; log_recycle_files_.<span class="hljs-built_in">size</span>()) &#123;  log_recycle_files_.<span class="hljs-built_in">push_back</span>(earliest.number);&#125; <span class="hljs-keyword">else</span> &#123;  <span class="hljs-comment">// 否则添加到待删除列表</span>  job_context-&gt;log_delete_files.<span class="hljs-built_in">push_back</span>(earliest.number);&#125;</code></pre><h4 id="2-2-3-从-VersionSet-获取过期文件：GetObsoleteFiles"><a href="#2-2-3-从-VersionSet-获取过期文件：GetObsoleteFiles" class="headerlink" title="2.2.3 从 VersionSet 获取过期文件：GetObsoleteFiles"></a>2.2.3 从 VersionSet 获取过期文件：GetObsoleteFiles</h4><p><code>VersionSet::GetObsoleteFiles</code> 负责从 <code>VersionSet</code> 获取过时文件列表，并根据 <code>min_pending_output</code> 进行过滤：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">VersionSet::GetObsoleteFiles</span><span class="hljs-params">(std::vector&lt;ObsoleteFileInfo&gt;* files,</span></span><span class="hljs-params"><span class="hljs-function">                                  std::vector&lt;ObsoleteBlobFileInfo&gt;* blob_files,</span></span><span class="hljs-params"><span class="hljs-function">                                  std::vector&lt;std::string&gt;* manifest_filenames,</span></span><span class="hljs-params"><span class="hljs-function">                                  <span class="hljs-type">uint64_t</span> min_pending_output)</span> </span>&#123;  <span class="hljs-comment">// 确保传入的参数指针非空</span>  <span class="hljs-built_in">assert</span>(files);  <span class="hljs-built_in">assert</span>(blob_files);  <span class="hljs-built_in">assert</span>(manifest_filenames);  <span class="hljs-comment">// 确保传入的容器为空</span>  <span class="hljs-built_in">assert</span>(files-&gt;<span class="hljs-built_in">empty</span>());  <span class="hljs-built_in">assert</span>(blob_files-&gt;<span class="hljs-built_in">empty</span>());  <span class="hljs-built_in">assert</span>(manifest_filenames-&gt;<span class="hljs-built_in">empty</span>());  <span class="hljs-comment">// 用于临时保存不能立即删除的过时SST文件</span>  std::vector&lt;ObsoleteFileInfo&gt; pending_files;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; f : obsolete_files_) &#123;    <span class="hljs-comment">// 如果文件号小于min_pending_output，表示该文件可以安全删除</span>    <span class="hljs-comment">// 因为不会有正在进行的写操作使用这个文件号</span>    <span class="hljs-keyword">if</span> (f.metadata-&gt;fd.<span class="hljs-built_in">GetNumber</span>() &lt; min_pending_output) &#123;      <span class="hljs-comment">// 添加到可删除文件列表中</span>      files-&gt;<span class="hljs-built_in">emplace_back</span>(std::<span class="hljs-built_in">move</span>(f));    &#125; <span class="hljs-keyword">else</span> &#123;      <span class="hljs-comment">// 文件号大于或等于min_pending_output，表示可能有待处理的操作</span>      <span class="hljs-comment">// 将其保留在待处理队列中</span>      pending_files.<span class="hljs-built_in">emplace_back</span>(std::<span class="hljs-built_in">move</span>(f));    &#125;  &#125;  <span class="hljs-comment">// 更新obsolete_files_，只保留那些暂时不能删除的文件</span>  obsolete_files_.<span class="hljs-built_in">swap</span>(pending_files);  <span class="hljs-comment">// 处理过时的Blob文件，逻辑与处理SST文件类似</span>  std::vector&lt;ObsoleteBlobFileInfo&gt; pending_blob_files;  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; blob_file : obsolete_blob_files_) &#123;    <span class="hljs-comment">// 同样判断文件号是否小于min_pending_output</span>    <span class="hljs-keyword">if</span> (blob_file.<span class="hljs-built_in">GetBlobFileNumber</span>() &lt; min_pending_output) &#123;      <span class="hljs-comment">// 可以安全删除的Blob文件</span>      blob_files-&gt;<span class="hljs-built_in">emplace_back</span>(std::<span class="hljs-built_in">move</span>(blob_file));    &#125; <span class="hljs-keyword">else</span> &#123;      <span class="hljs-comment">// 暂时不能删除的Blob文件</span>      pending_blob_files.<span class="hljs-built_in">emplace_back</span>(std::<span class="hljs-built_in">move</span>(blob_file));    &#125;  &#125;  <span class="hljs-comment">// 更新obsolete_blob_files_，只保留那些暂时不能删除的Blob文件</span>  obsolete_blob_files_.<span class="hljs-built_in">swap</span>(pending_blob_files);  <span class="hljs-comment">// 处理过时的MANIFEST文件</span>  <span class="hljs-comment">// 所有过时的MANIFEST文件都可以直接删除</span>  <span class="hljs-comment">// 将obsolete_manifests_中的内容移到manifest_filenames中，并清空obsolete_manifests_</span>  obsolete_manifests_.<span class="hljs-built_in">swap</span>(*manifest_filenames);&#125;</code></pre><h5 id="2-2-3-1-pending-outputs-机制"><a href="#2-2-3-1-pending-outputs-机制" class="headerlink" title="2.2.3.1 pending_outputs_ 机制"></a>2.2.3.1 <code>pending_outputs_</code> 机制</h5><p>compaction 在开始时，会将“下一个将要分配的 file_number” 记录到 <code>pending_outputs_</code> 中。</p><pre><code class="hljs cpp">std::list&lt;<span class="hljs-type">uint64_t</span>&gt;::<span class="hljs-function">iterator</span><span class="hljs-function"><span class="hljs-title">DBImpl::CaptureCurrentFileNumberInPendingOutputs</span><span class="hljs-params">()</span> </span>&#123;  <span class="hljs-comment">// 需要记住插入的迭代器，因为在后台作业完成后，需要从 pending_output 中删除该元素。</span>  pending_outputs_.<span class="hljs-built_in">push_back</span>(versions_-&gt;<span class="hljs-built_in">current_next_file_number</span>());  <span class="hljs-keyword">auto</span> pending_outputs_inserted_elem = pending_outputs_.<span class="hljs-built_in">end</span>();  --pending_outputs_inserted_elem;  <span class="hljs-keyword">return</span> pending_outputs_inserted_elem;&#125;</code></pre><p>机制的目的是<strong>保护所有大于等于这个 file number 的文件在该任务执行期间不会被误删</strong>。它与文件在 <code>VersionSet</code> 中的逻辑引用<strong>无关</strong>。即使 compaction 还在运行，并且它的 <code>pending_outputs_</code> file number 的文件被物理删除，但这并不意味着 <code>VersionSet</code> 中的任何 <code>Version</code> 仍然在逻辑上引用 file number 的文件。</p><p>compaction 任务结束后会从 <code>pending_outputs_</code> 释放对应的 file_number，允许 file_number 及之后的文件被清理</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::ReleaseFileNumberFromPendingOutputs</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    std::unique_ptr&lt;std::list&lt;<span class="hljs-type">uint64_t</span>&gt;::iterator&gt;&amp; v)</span> </span>&#123;  <span class="hljs-keyword">if</span> (v.<span class="hljs-built_in">get</span>() != <span class="hljs-literal">nullptr</span>) &#123;    pending_outputs_.<span class="hljs-built_in">erase</span>(*v.<span class="hljs-built_in">get</span>());    v.<span class="hljs-built_in">reset</span>();  &#125;&#125;</code></pre><p>举例场景如下：</p><ol><li>当前的 file number 是 13（<code>versions_-&gt;current_next_file_number()</code>）。</li><li>compaction (1) 启动，此时会把 13 的 file number（准确说是“下一个将要分配的 file number”，即 <code>versions_-&gt;current_next_file_number()</code>）加入 <code>pending_outputs_</code>，</li><li>compaction (2) 创建了 file 13。</li><li>compaction (3) 消耗了 file 13，并生成了 file 15。此时 file 13 已经没有引用，被加入 <code>VersionSet::obsolete_files_</code>，表示它可以被删除。</li><li>FindObsoleteFiles() 检查到 file 13 在 <code>obsolete_files_</code> 集合中，于是将其移出 <code>obsolete_files_</code>，准备删除。</li><li>PurgeObsoleteFiles() 尝试删除 file 13，但由于 compaction (1) 还在运行，<code>pending_outputs_</code> 仍然阻挡着 file 13 的删除。此时 file 13 已经不在 <code>obsolete_files_</code> 集合中，但也没有被真正删除，导致它永远不会被清理。</li></ol><blockquote><p><strong>file 13 虽然是 compaction (2) 生成的，但它的 file number 可能早于 compaction (2) 启动时记录到 <code>pending_outputs_</code> 的 file number</strong>，因为 file number 的分配是全局递增的，多个 compaction&#x2F;flush 任务并发时，file number 分配顺序和任务实际完成顺序可能不同。</p></blockquote><p><code>pending_outputs_</code> 和 <code>obsolete_files_</code> 之间存在协作关系：一旦从 <code>obsolete_files_</code> 移除（<code>FindObsoleteFiles()</code> 逻辑）后，但文件因为 <code>pending_outputs_</code> 的存在被阻挡删除（<code>PurgeObsoleteFiles()</code> 逻辑），如果没有额外机制跟踪，就会出现“文件既不在 <code>obsolete_files_</code>，也没被删除”的死角，造成空间泄漏。 <a href="https://github.com/facebook/rocksdb/commit/863009b5a594fd4ecd7ed38ba1540f8cbc15011e#diff-69c1d266e43d561de12a9ef15cd32ae400d7623580e1e94f1c1cd22a9958efe6">Fix deleting obsolete files# 863009b</a></p><h4 id="2-2-4-删除过期文件：PurgeObsoleteFiles"><a href="#2-2-4-删除过期文件：PurgeObsoleteFiles" class="headerlink" title="2.2.4 删除过期文件：PurgeObsoleteFiles"></a>2.2.4 删除过期文件：PurgeObsoleteFiles</h4><p><code>PurgeObsoleteFiles</code> 执行实际的文件删除操作，<strong>不需要持有数据库互斥锁</strong>，降低了锁持有的时间：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 删除不属于活跃文件列表的文件，同时删除在sst_delete_files和log_delete_files中标记的文件。</span><span class="hljs-comment">// 调用此方法时不需要持有互斥锁。</span><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">DBImpl::PurgeObsoleteFiles</span><span class="hljs-params">(JobContext&amp; state, <span class="hljs-type">bool</span> schedule_only)</span> </span>&#123;  <span class="hljs-comment">// 同步点，用于测试</span>  <span class="hljs-built_in">TEST_SYNC_POINT</span>(<span class="hljs-string">&quot;DBImpl::PurgeObsoleteFiles:Begin&quot;</span>);  <span class="hljs-comment">// 断言确保我们有东西要删除</span>  <span class="hljs-built_in">assert</span>(state.<span class="hljs-built_in">HaveSomethingToDelete</span>());  <span class="hljs-comment">// FindObsoleteFiles()应该已经填充了manifest_file_number，确保它不为0</span>  <span class="hljs-built_in">assert</span>(state.manifest_file_number != <span class="hljs-number">0</span>);  <span class="hljs-comment">// 将活跃文件列表转换为无序集合，不需要持有互斥锁；set操作较慢</span>  <span class="hljs-comment">// 这些集合用于快速查找文件是否是活跃的</span>  <span class="hljs-function">std::unordered_set&lt;<span class="hljs-type">uint64_t</span>&gt; <span class="hljs-title">sst_live_set</span><span class="hljs-params">(state.sst_live.begin(),</span></span><span class="hljs-params"><span class="hljs-function">                                          state.sst_live.end())</span></span>;  <span class="hljs-function">std::unordered_set&lt;<span class="hljs-type">uint64_t</span>&gt; <span class="hljs-title">blob_live_set</span><span class="hljs-params">(state.blob_live.begin(),</span></span><span class="hljs-params"><span class="hljs-function">                                           state.blob_live.end())</span></span>;  <span class="hljs-function">std::unordered_set&lt;<span class="hljs-type">uint64_t</span>&gt; <span class="hljs-title">log_recycle_files_set</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">      state.log_recycle_files.begin(), state.log_recycle_files.end())</span></span>;  <span class="hljs-comment">// 准备候选文件列表，这包括全扫描找到的候选文件</span>  <span class="hljs-keyword">auto</span> candidate_files = state.full_scan_candidate_files;  <span class="hljs-comment">// 预先分配足够空间以避免频繁的内存重分配</span>  candidate_files.<span class="hljs-built_in">reserve</span>(      candidate_files.<span class="hljs-built_in">size</span>() + state.sst_delete_files.<span class="hljs-built_in">size</span>() +      state.blob_delete_files.<span class="hljs-built_in">size</span>() + state.log_delete_files.<span class="hljs-built_in">size</span>() +      state.manifest_delete_files.<span class="hljs-built_in">size</span>());  <span class="hljs-comment">// 将要删除的SST文件添加到候选列表</span>  <span class="hljs-comment">// 我们可能在生成文件名时忽略dbname</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; file : state.sst_delete_files) &#123;    <span class="hljs-comment">// 如果不只是删除元数据，将文件添加到候选列表</span>    <span class="hljs-keyword">if</span> (!file.only_delete_metadata) &#123;      candidate_files.<span class="hljs-built_in">emplace_back</span>(          <span class="hljs-built_in">MakeTableFileName</span>(file.metadata-&gt;fd.<span class="hljs-built_in">GetNumber</span>()), file.path);    &#125;    <span class="hljs-comment">// 如果文件有table_reader_handle，释放它</span>    <span class="hljs-keyword">if</span> (file.metadata-&gt;table_reader_handle) &#123;      table_cache_-&gt;<span class="hljs-built_in">Release</span>(file.metadata-&gt;table_reader_handle);    &#125;    <span class="hljs-comment">// 删除文件元数据</span>    file.<span class="hljs-built_in">DeleteMetadata</span>();  &#125;  <span class="hljs-comment">// 将要删除的BLOB文件添加到候选列表</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; blob_file : state.blob_delete_files) &#123;    candidate_files.<span class="hljs-built_in">emplace_back</span>(<span class="hljs-built_in">BlobFileName</span>(blob_file.<span class="hljs-built_in">GetBlobFileNumber</span>()),                               blob_file.<span class="hljs-built_in">GetPath</span>());  &#125;  <span class="hljs-comment">// 获取WAL目录</span>  <span class="hljs-keyword">auto</span> wal_dir = immutable_db_options_.<span class="hljs-built_in">GetWalDir</span>();  <span class="hljs-comment">// 将要删除的WAL文件添加到候选列表</span>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> file_num : state.log_delete_files) &#123;    <span class="hljs-keyword">if</span> (file_num &gt; <span class="hljs-number">0</span>) &#123;      candidate_files.<span class="hljs-built_in">emplace_back</span>(<span class="hljs-built_in">LogFileName</span>(file_num), wal_dir);    &#125;  &#125;  <span class="hljs-comment">// 将要删除的manifest文件添加到候选列表</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; filename : state.manifest_delete_files) &#123;    candidate_files.<span class="hljs-built_in">emplace_back</span>(filename, dbname_);  &#125;  <span class="hljs-comment">// 对候选文件列表进行排序和去重，避免尝试删除同一个文件两次</span>  std::<span class="hljs-built_in">sort</span>(candidate_files.<span class="hljs-built_in">begin</span>(), candidate_files.<span class="hljs-built_in">end</span>(),            [](<span class="hljs-type">const</span> JobContext::CandidateFileInfo&amp; lhs,               <span class="hljs-type">const</span> JobContext::CandidateFileInfo&amp; rhs) &#123;              <span class="hljs-comment">// 先按文件名排序</span>              <span class="hljs-keyword">if</span> (lhs.file_name &lt; rhs.file_name) &#123;                <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;              &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (lhs.file_name &gt; rhs.file_name) &#123;                <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;              &#125; <span class="hljs-keyword">else</span> &#123;                <span class="hljs-comment">// 如果文件名相同，按文件路径排序</span>                <span class="hljs-built_in">return</span> (lhs.file_path &lt; rhs.file_path);              &#125;            &#125;);  <span class="hljs-comment">// 去除重复的文件条目</span>  candidate_files.<span class="hljs-built_in">erase</span>(      std::<span class="hljs-built_in">unique</span>(candidate_files.<span class="hljs-built_in">begin</span>(), candidate_files.<span class="hljs-built_in">end</span>()),      candidate_files.<span class="hljs-built_in">end</span>());  <span class="hljs-comment">// 如果之前有WAL文件，记录删除日志信息</span>  <span class="hljs-keyword">if</span> (state.prev_total_log_size &gt; <span class="hljs-number">0</span>) &#123;    <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log,                 <span class="hljs-string">&quot;[JOB %d] Try to delete WAL files size %&quot;</span> PRIu64                 <span class="hljs-string">&quot;, prev total WAL file size %&quot;</span> PRIu64                 <span class="hljs-string">&quot;, number of live WAL files %&quot;</span> ROCKSDB_PRIszt <span class="hljs-string">&quot;.\n&quot;</span>,                 state.job_id, state.size_log_to_delete,                 state.prev_total_log_size, state.num_alive_log_files);  &#125;  <span class="hljs-comment">// 用于保存旧的info log文件</span>  std::vector&lt;std::string&gt; old_info_log_files;  <span class="hljs-comment">// 创建信息日志前缀</span>  <span class="hljs-function">InfoLogPrefix <span class="hljs-title">info_log_prefix</span><span class="hljs-params">(!immutable_db_options_.db_log_dir.empty(),</span></span><span class="hljs-params"><span class="hljs-function">                              dbname_)</span></span>;  <span class="hljs-comment">// candidate_files中最近两个OPTIONS文件的文件编号</span>  <span class="hljs-comment">// 此时，candidate_files中不能有重复的文件编号</span>  <span class="hljs-type">uint64_t</span> optsfile_num1 = std::numeric_limits&lt;<span class="hljs-type">uint64_t</span>&gt;::<span class="hljs-built_in">min</span>();  <span class="hljs-type">uint64_t</span> optsfile_num2 = std::numeric_limits&lt;<span class="hljs-type">uint64_t</span>&gt;::<span class="hljs-built_in">min</span>();  <span class="hljs-comment">// 遍历候选文件找出最近的两个OPTIONS文件</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; candidate_file : candidate_files) &#123;    <span class="hljs-type">const</span> std::string&amp; fname = candidate_file.file_name;    <span class="hljs-type">uint64_t</span> number;    FileType type;    <span class="hljs-comment">// 解析文件名，如果不是OPTIONS文件则跳过</span>    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">ParseFileName</span>(fname, &amp;number, info_log_prefix.prefix, &amp;type) ||        type != kOptionsFile) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-comment">// 更新最近的两个OPTIONS文件编号</span>    <span class="hljs-keyword">if</span> (number &gt; optsfile_num1) &#123;      optsfile_num2 = optsfile_num1;      optsfile_num1 = number;    &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (number &gt; optsfile_num2) &#123;      optsfile_num2 = number;    &#125;  &#125;  <span class="hljs-comment">// 在尝试删除WAL文件前先关闭它们</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span> w : state.logs_to_free) &#123;    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 可能需要检查Close()的返回值</span>    <span class="hljs-keyword">auto</span> s = w-&gt;<span class="hljs-built_in">Close</span>();    s.<span class="hljs-built_in">PermitUncheckedError</span>();  <span class="hljs-comment">// 允许未检查的错误</span>  &#125;  <span class="hljs-comment">// 检查是否拥有表和日志文件</span>  <span class="hljs-type">bool</span> own_files = <span class="hljs-built_in">OwnTablesAndLogs</span>();  <span class="hljs-comment">// 记录要删除的文件编号</span>  std::unordered_set&lt;<span class="hljs-type">uint64_t</span>&gt; files_to_del;  <span class="hljs-comment">// 遍历所有候选文件，决定哪些需要删除</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">const</span> <span class="hljs-keyword">auto</span>&amp; candidate_file : candidate_files) &#123;    <span class="hljs-type">const</span> std::string&amp; to_delete = candidate_file.file_name;    <span class="hljs-type">uint64_t</span> number;    FileType type;    <span class="hljs-comment">// 如果无法识别文件，则跳过</span>    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">ParseFileName</span>(to_delete, &amp;number, info_log_prefix.prefix, &amp;type)) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-comment">// 默认保留文件</span>    <span class="hljs-type">bool</span> keep = <span class="hljs-literal">true</span>;    <span class="hljs-comment">// 根据文件类型决定是否要保留</span>    <span class="hljs-keyword">switch</span> (type) &#123;      <span class="hljs-keyword">case</span> kWalFile:  <span class="hljs-comment">// WAL文件</span>        <span class="hljs-comment">// 保留条件：文件编号&gt;=log_number 或 文件编号==prev_log_number 或 在回收文件集中</span>        keep = ((number &gt;= state.log_number) ||                (number == state.prev_log_number) ||                (log_recycle_files_set.<span class="hljs-built_in">find</span>(number) !=                 log_recycle_files_set.<span class="hljs-built_in">end</span>()));        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kDescriptorFile:  <span class="hljs-comment">// 描述符文件(manifest)</span>        <span class="hljs-comment">// 保留我的manifest文件和任何更新的版本（可能在manifest滚动期间发生）</span>        keep = (number &gt;= state.manifest_file_number);        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kTableFile:  <span class="hljs-comment">// SST表文件</span>        <span class="hljs-comment">// 如果第二个条件不存在，会导致DontDeletePendingOutputs失败</span>        <span class="hljs-comment">// 保留条件：在活跃SST集中 或 文件编号&gt;=min_pending_output</span>        keep = (sst_live_set.<span class="hljs-built_in">find</span>(number) != sst_live_set.<span class="hljs-built_in">end</span>()) ||               number &gt;= state.min_pending_output;        <span class="hljs-keyword">if</span> (!keep) &#123;          files_to_del.<span class="hljs-built_in">insert</span>(number);  <span class="hljs-comment">// 记录要删除的文件编号</span>        &#125;        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kBlobFile:  <span class="hljs-comment">// Blob文件</span>        <span class="hljs-comment">// 保留条件：文件编号&gt;=min_pending_output 或 在活跃的blob文件集中</span>        keep = number &gt;= state.min_pending_output ||               (blob_live_set.<span class="hljs-built_in">find</span>(number) != blob_live_set.<span class="hljs-built_in">end</span>());        <span class="hljs-keyword">if</span> (!keep) &#123;          files_to_del.<span class="hljs-built_in">insert</span>(number);  <span class="hljs-comment">// 记录要删除的文件编号</span>        &#125;        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kTempFile:  <span class="hljs-comment">// 临时文件</span>        <span class="hljs-comment">// 当前正在写入的任何临时文件必须记录在pending_outputs_中，</span>        <span class="hljs-comment">// 它被插入到&quot;live&quot;集合中。</span>        <span class="hljs-comment">// 此外，SetCurrentFile在写出新的manifest时会创建一个临时文件，</span>        <span class="hljs-comment">// 等于state.pending_manifest_file_number，我们不应该删除那个文件</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// TODO(yhchiang): 仔细修改第三个条件以安全地移除临时options文件</span>        keep = (sst_live_set.<span class="hljs-built_in">find</span>(number) != sst_live_set.<span class="hljs-built_in">end</span>()) ||               (blob_live_set.<span class="hljs-built_in">find</span>(number) != blob_live_set.<span class="hljs-built_in">end</span>()) ||               (number == state.pending_manifest_file_number) ||               (to_delete.<span class="hljs-built_in">find</span>(kOptionsFileNamePrefix) != std::string::npos);        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kInfoLogFile:  <span class="hljs-comment">// 信息日志文件</span>        keep = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 总是保留</span>        <span class="hljs-keyword">if</span> (number != <span class="hljs-number">0</span>) &#123;          old_info_log_files.<span class="hljs-built_in">push_back</span>(to_delete);  <span class="hljs-comment">// 收集旧的日志文件</span>        &#125;        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kOptionsFile:  <span class="hljs-comment">// 选项文件</span>        <span class="hljs-comment">// 保留最近的两个OPTIONS文件</span>        keep = (number &gt;= optsfile_num2);        <span class="hljs-keyword">break</span>;      <span class="hljs-keyword">case</span> kCurrentFile:   <span class="hljs-comment">// CURRENT文件</span>      <span class="hljs-keyword">case</span> kDBLockFile:    <span class="hljs-comment">// 数据库锁文件</span>      <span class="hljs-keyword">case</span> kIdentityFile:  <span class="hljs-comment">// 身份文件</span>      <span class="hljs-keyword">case</span> kMetaDatabase:  <span class="hljs-comment">// 元数据库</span>        keep = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 这些特殊文件总是保留</span>        <span class="hljs-keyword">break</span>;    &#125;    <span class="hljs-comment">// 如果需要保留，跳到下一个文件</span>    <span class="hljs-keyword">if</span> (keep) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-comment">// 确定要删除的文件名和要同步的目录</span>    std::string fname;    std::string dir_to_sync;    <span class="hljs-keyword">if</span> (type == kTableFile) &#123;  <span class="hljs-comment">// SST文件</span>      <span class="hljs-comment">// 从缓存中移除</span>      TableCache::<span class="hljs-built_in">Evict</span>(table_cache_.<span class="hljs-built_in">get</span>(), number);      fname = <span class="hljs-built_in">MakeTableFileName</span>(candidate_file.file_path, number);      dir_to_sync = candidate_file.file_path;    &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (type == kBlobFile) &#123;  <span class="hljs-comment">// Blob文件</span>      fname = <span class="hljs-built_in">BlobFileName</span>(candidate_file.file_path, number);      dir_to_sync = candidate_file.file_path;    &#125; <span class="hljs-keyword">else</span> &#123;  <span class="hljs-comment">// 其他类型文件</span>      <span class="hljs-comment">// 确定同步目录</span>      dir_to_sync = (type == kWalFile) ? wal_dir : dbname_;      <span class="hljs-comment">// 构建完整文件路径，处理路径分隔符</span>      fname = dir_to_sync +              ((!dir_to_sync.<span class="hljs-built_in">empty</span>() &amp;&amp; dir_to_sync.<span class="hljs-built_in">back</span>() == <span class="hljs-string">&#x27;/&#x27;</span>) ||                       (!to_delete.<span class="hljs-built_in">empty</span>() &amp;&amp; to_delete.<span class="hljs-built_in">front</span>() == <span class="hljs-string">&#x27;/&#x27;</span>)                   ? <span class="hljs-string">&quot;&quot;</span>  <span class="hljs-comment">// 如果目录以/结尾或文件名以/开头，不添加额外的/</span>                   : <span class="hljs-string">&quot;/&quot;</span>) +  <span class="hljs-comment">// 否则添加/</span>              to_delete;    &#125;    <span class="hljs-comment">// 对于WAL文件，如果配置了TTL或大小限制，尝试归档而不是删除</span>    <span class="hljs-keyword">if</span> (type == kWalFile &amp;&amp; (immutable_db_options_.WAL_ttl_seconds &gt; <span class="hljs-number">0</span> ||                           immutable_db_options_.WAL_size_limit_MB &gt; <span class="hljs-number">0</span>)) &#123;      wal_manager_.<span class="hljs-built_in">ArchiveWALFile</span>(fname, number);      <span class="hljs-keyword">continue</span>;  <span class="hljs-comment">// 已归档，不需要进一步处理</span>    &#125;    <span class="hljs-comment">// 如果我不拥有这些文件，例如，secondary实例使用max_open_files = -1，</span>    <span class="hljs-comment">// 则无需删除或安排删除这些文件，因为它们将由其所有者删除，例如primary实例</span>    <span class="hljs-keyword">if</span> (!own_files) &#123;      <span class="hljs-keyword">continue</span>;    &#125;    <span class="hljs-comment">// 根据schedule_only决定是安排删除还是立即删除</span>    <span class="hljs-keyword">if</span> (schedule_only) &#123;      <span class="hljs-comment">// 如果是安排删除，需要获取互斥锁</span>      <span class="hljs-function">InstrumentedMutexLock <span class="hljs-title">guard_lock</span><span class="hljs-params">(&amp;mutex_)</span></span>;      <span class="hljs-comment">// 将文件安排到待删除队列</span>      <span class="hljs-built_in">SchedulePendingPurge</span>(fname, dir_to_sync, type, number, state.job_id);    &#125; <span class="hljs-keyword">else</span> &#123;      <span class="hljs-comment">// 立即删除文件</span>      <span class="hljs-built_in">DeleteObsoleteFileImpl</span>(state.job_id, fname, dir_to_sync, type, number);    &#125;  &#125;  <span class="hljs-comment">// 删除完过期文件后，从files_grabbed_for_purge_中移除它们</span>  &#123;    <span class="hljs-function">InstrumentedMutexLock <span class="hljs-title">guard_lock</span><span class="hljs-params">(&amp;mutex_)</span></span>;    autovector&lt;<span class="hljs-type">uint64_t</span>&gt; to_be_removed;    <span class="hljs-comment">// 查找已删除文件的编号</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> fn : files_grabbed_for_purge_) &#123;      <span class="hljs-keyword">if</span> (files_to_del.<span class="hljs-built_in">count</span>(fn) != <span class="hljs-number">0</span>) &#123;        to_be_removed.<span class="hljs-built_in">emplace_back</span>(fn);      &#125;    &#125;    <span class="hljs-comment">// 从files_grabbed_for_purge_中移除已删除的文件</span>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> fn : to_be_removed) &#123;      files_grabbed_for_purge_.<span class="hljs-built_in">erase</span>(fn);    &#125;  &#125;  <span class="hljs-comment">// 删除旧的info log文件</span>  <span class="hljs-type">size_t</span> old_info_log_file_count = old_info_log_files.<span class="hljs-built_in">size</span>();  <span class="hljs-keyword">if</span> (old_info_log_file_count != <span class="hljs-number">0</span> &amp;&amp;      old_info_log_file_count &gt;= immutable_db_options_.keep_log_file_num) &#123;    <span class="hljs-comment">// 只保留配置的数量的日志文件，删除多余的</span>    std::<span class="hljs-built_in">sort</span>(old_info_log_files.<span class="hljs-built_in">begin</span>(), old_info_log_files.<span class="hljs-built_in">end</span>());    <span class="hljs-comment">// 计算需要删除的文件数量</span>    <span class="hljs-type">size_t</span> end =        old_info_log_file_count - immutable_db_options_.keep_log_file_num;    <span class="hljs-comment">// 删除多余的日志文件</span>    <span class="hljs-keyword">for</span> (<span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt;= end; i++) &#123;      std::string&amp; to_delete = old_info_log_files.<span class="hljs-built_in">at</span>(i);      std::string full_path_to_delete =          (immutable_db_options_.db_log_dir.<span class="hljs-built_in">empty</span>()               ? dbname_  <span class="hljs-comment">// 如果没有专门的日志目录，使用DB目录</span>               : immutable_db_options_.db_log_dir) +          <span class="hljs-string">&quot;/&quot;</span> + to_delete;      <span class="hljs-comment">// 记录删除信息</span>      <span class="hljs-built_in">ROCKS_LOG_INFO</span>(immutable_db_options_.info_log,                   <span class="hljs-string">&quot;[JOB %d] Delete info log file %s\n&quot;</span>, state.job_id,                   full_path_to_delete.<span class="hljs-built_in">c_str</span>());      <span class="hljs-comment">// 执行删除</span>      Status s = env_-&gt;<span class="hljs-built_in">DeleteFile</span>(full_path_to_delete);      <span class="hljs-keyword">if</span> (!s.<span class="hljs-built_in">ok</span>()) &#123;        <span class="hljs-comment">// 处理删除失败的情况</span>        <span class="hljs-keyword">if</span> (env_-&gt;<span class="hljs-built_in">FileExists</span>(full_path_to_delete).<span class="hljs-built_in">IsNotFound</span>()) &#123;          <span class="hljs-comment">// 文件不存在的情况</span>          <span class="hljs-built_in">ROCKS_LOG_INFO</span>(              immutable_db_options_.info_log,              <span class="hljs-string">&quot;[JOB %d] Tried to delete non-existing info log file %s FAILED &quot;</span>              <span class="hljs-string">&quot;-- %s\n&quot;</span>,              state.job_id, to_delete.<span class="hljs-built_in">c_str</span>(), s.<span class="hljs-built_in">ToString</span>().<span class="hljs-built_in">c_str</span>());        &#125; <span class="hljs-keyword">else</span> &#123;          <span class="hljs-comment">// 其他删除失败情况</span>          <span class="hljs-built_in">ROCKS_LOG_ERROR</span>(immutable_db_options_.info_log,                        <span class="hljs-string">&quot;[JOB %d] Delete info log file %s FAILED -- %s\n&quot;</span>,                        state.job_id, to_delete.<span class="hljs-built_in">c_str</span>(),                        s.<span class="hljs-built_in">ToString</span>().<span class="hljs-built_in">c_str</span>());        &#125;      &#125;    &#125;  &#125;  <span class="hljs-comment">// 让WAL管理器清理过期的WAL文件</span>  wal_manager_.<span class="hljs-built_in">PurgeObsoleteWALFiles</span>();  <span class="hljs-comment">// 刷新日志</span>  <span class="hljs-built_in">LogFlush</span>(immutable_db_options_.info_log);  <span class="hljs-comment">// 获取互斥锁并减少pending_purge_obsolete_files_计数</span>  <span class="hljs-function">InstrumentedMutexLock <span class="hljs-title">l</span><span class="hljs-params">(&amp;mutex_)</span></span>;  --pending_purge_obsolete_files_;  <span class="hljs-built_in">assert</span>(pending_purge_obsolete_files_ &gt;= <span class="hljs-number">0</span>);  <span class="hljs-comment">// 如果只是安排删除，调用SchedulePurge()</span>  <span class="hljs-keyword">if</span> (schedule_only) &#123;    <span class="hljs-comment">// 必须在持有互斥锁的情况下从pending_purge_obsolete_files_变为bg_purge_scheduled_</span>    <span class="hljs-comment">// （用于GetSortedWalFiles()等）</span>    <span class="hljs-built_in">SchedulePurge</span>();  &#125;  <span class="hljs-comment">// 如果没有更多待清理的文件，通知所有等待的线程</span>  <span class="hljs-keyword">if</span> (pending_purge_obsolete_files_ == <span class="hljs-number">0</span>) &#123;    bg_cv_.<span class="hljs-built_in">SignalAll</span>();  &#125;  <span class="hljs-comment">// 同步点，用于测试</span>  <span class="hljs-built_in">TEST_SYNC_POINT</span>(<span class="hljs-string">&quot;DBImpl::PurgeObsoleteFiles:End&quot;</span>);&#125;</code></pre><h5 id="2-2-4-1-异步删除机制"><a href="#2-2-4-1-异步删除机制" class="headerlink" title="2.2.4.1 异步删除机制"></a>2.2.4.1 异步删除机制</h5><p>为避免阻塞主线程，RocksDB 支持将文件删除操作安排到后台线程：</p><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (schedule_only) &#123;  <span class="hljs-comment">// 安排后台删除</span>  <span class="hljs-function">InstrumentedMutexLock <span class="hljs-title">guard_lock</span><span class="hljs-params">(&amp;mutex_)</span></span>;  <span class="hljs-built_in">SchedulePendingPurge</span>(fname, dir_to_sync, type, number, state.job_id);&#125; <span class="hljs-keyword">else</span> &#123;  <span class="hljs-comment">// 立即删除</span>  <span class="hljs-built_in">DeleteObsoleteFileImpl</span>(state.job_id, fname, dir_to_sync, type, number);&#125;</code></pre><h2 id="三、不同类型文件的清理策略"><a href="#三、不同类型文件的清理策略" class="headerlink" title="三、不同类型文件的清理策略"></a>三、不同类型文件的清理策略</h2><p>RocksDB 对不同文件类型有不同的删除策略。</p><p><strong>1 WAL (Write-Ahead Log) 文件 (kWalFile)</strong></p><p><strong>会被删除的 WAL 文件：</strong></p><ul><li>文件号小于当前的 <code>log_number</code></li><li>不是前一个日志文件（<code>prev_log_number</code>）</li><li>不在日志回收列表中（<code>log_recycle_files_set</code>）</li></ul><pre><code class="hljs cpp">keep = ((number &gt;= state.log_number) ||        (number == state.prev_log_number) ||        (log_recycle_files_set.<span class="hljs-built_in">find</span>(number) != log_recycle_files_set.<span class="hljs-built_in">end</span>()));</code></pre><p><strong>特殊处理：</strong></p><ul><li>如果设置了 WAL TTL 或大小限制，过期的 WAL 文件不会被直接删除，而是被移动到归档目录：</li></ul><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (type == kWalFile &amp;&amp; (immutable_db_options_.WAL_ttl_seconds &gt; <span class="hljs-number">0</span> ||                         immutable_db_options_.WAL_size_limit_MB &gt; <span class="hljs-number">0</span>)) &#123;  wal_manager_.<span class="hljs-built_in">ArchiveWALFile</span>(fname, number);  <span class="hljs-keyword">continue</span>;&#125;</code></pre><p><strong>2 SST (Static Sorted Table) 文件 (kTableFile)</strong></p><p><strong>会被删除的 SST 文件：</strong></p><ul><li>不在活跃文件集合中（<code>sst_live_set</code>）</li><li>文件号小于最小待处理输出号（<code>min_pending_output</code>）</li></ul><pre><code class="hljs cpp">keep = (sst_live_set.<span class="hljs-built_in">find</span>(number) != sst_live_set.<span class="hljs-built_in">end</span>()) ||       number &gt;= state.min_pending_output;</code></pre><p><strong>删除前处理：</strong></p><ul><li>从 TableCache 中驱逐该文件：<code>TableCache::Evict(table_cache_.get(), number);</code></li></ul><p><strong>3 Blob 文件 (kBlobFile)</strong></p><p><strong>会被删除的 Blob 文件：</strong></p><ul><li>不在活跃 Blob 文件集合中（<code>blob_live_set</code>）</li><li>文件号小于最小待处理输出号（<code>min_pending_output</code>）</li></ul><pre><code class="hljs cpp">keep = number &gt;= state.min_pending_output ||       (blob_live_set.<span class="hljs-built_in">find</span>(number) != blob_live_set.<span class="hljs-built_in">end</span>());</code></pre><p><strong>4 清单文件 (kDescriptorFile)</strong></p><p><strong>会被删除的清单文件：</strong></p><ul><li>文件号小于当前清单文件号（<code>manifest_file_number</code>）</li></ul><pre><code class="hljs cpp">keep = (number &gt;= state.manifest_file_number);</code></pre><p><strong>5 临时文件 (kTempFile)</strong></p><p><strong>会被删除的临时文件：</strong></p><ul><li>不在活跃 SST 文件集合中</li><li>不在活跃 Blob 文件集合中</li><li>不是待处理的清单文件</li><li>不是选项文件（不包含 <code>kOptionsFileNamePrefix</code>）</li></ul><pre><code class="hljs cpp">keep = (sst_live_set.<span class="hljs-built_in">find</span>(number) != sst_live_set.<span class="hljs-built_in">end</span>()) ||       (blob_live_set.<span class="hljs-built_in">find</span>(number) != blob_live_set.<span class="hljs-built_in">end</span>()) ||       (number == state.pending_manifest_file_number) ||       (to_delete.<span class="hljs-built_in">find</span>(kOptionsFileNamePrefix) != std::string::npos);</code></pre><p><strong>6 信息日志文件 (kInfoLogFile)</strong></p><p><strong>处理逻辑：</strong></p><ul><li>默认所有日志文件都标记为保留（<code>keep = true</code>）</li><li>但如果日志文件总数超过配置的保留数量（<code>keep_log_file_num</code>），则删除最旧的日志文件</li></ul><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (old_info_log_file_count != <span class="hljs-number">0</span> &amp;&amp;    old_info_log_file_count &gt;= immutable_db_options_.keep_log_file_num) &#123;  std::<span class="hljs-built_in">sort</span>(old_info_log_files.<span class="hljs-built_in">begin</span>(), old_info_log_files.<span class="hljs-built_in">end</span>());  <span class="hljs-type">size_t</span> end = old_info_log_file_count - immutable_db_options_.keep_log_file_num;  <span class="hljs-comment">// 删除最旧的文件，直到文件数等于keep_log_file_num</span>&#125;</code></pre><p><strong>7 选项文件 (kOptionsFile)</strong></p><p><strong>会被删除的选项文件：</strong></p><ul><li>不是最新的两个选项文件（<code>optsfile_num1</code> 和 <code>optsfile_num2</code>）</li></ul><pre><code class="hljs cpp">keep = (number &gt;= optsfile_num2);</code></pre><p><strong>8 其他常驻文件类型</strong></p><p><strong>永不删除的文件类型：</strong></p><ul><li>当前文件 (kCurrentFile)</li><li>数据库锁文件 (kDBLockFile)</li><li>标识文件 (kIdentityFile)</li><li>元数据库 (kMetaDatabase)</li></ul><pre><code class="hljs cpp">keep = <span class="hljs-literal">true</span>;</code></pre><h2 id="四、统计指标"><a href="#四、统计指标" class="headerlink" title="四、统计指标"></a>四、统计指标</h2><p><strong>rocksdb.min-obsolete-sst-number-to-keep</strong></p><ul><li>定义位置：DB::Properties::kMinObsoleteSstNumberToKeep</li><li>含义：表示数据库需要保留的最小过时 SST 文件编号。低于此编号的过时 SST 文件可以安全地删除。</li></ul><p><strong>rocksdb.obsolete-sst-files-size</strong></p><ul><li>定义位置：DB::Properties::kObsoleteSstFilesSize</li><li>含义：表示数据库中所有过时 SST 文件的总大小（以字节为单位）。那些逻辑上不再需要的文件，但由于后台任务仍在使用，暂时还不能物理删除的文件。<strong>如果有未成功计入版本的文件（由于崩溃、异常），它们不会计入该属性中，直到系统将它们识别为 “ 垃圾文件 “ 并添加到 <code>obsolete_files_</code> 列表。</strong></li></ul><h2 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a>五、总结</h2><p>RocksDB 的过期文件清理机制依赖于正确的状态转换，当磁盘利用率因为 compaction 增长到接近 100%，系统处于资源耗尽状态时，依赖被动触发的机制可能无法正常工作。此时就需要提供主动触发清理的调用，或者重启数据库以触发正常的清理、恢复流程。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/05-05-2025/rocksdb-obsolete-files.html">https://www.cyningsun.com/05-05-2025/rocksdb-obsolete-files.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;RocksDB 作为一个高性能的 KV 存储引擎，会产生多种类型的文件：SST 数据文件、WAL 日志文件、MANIFEST 元数据文件、LOG 运行日志等。随着数据库运行，这些文件会不断生成、更新和过期。尤其是一些极端情况下，会导致&lt;strong&gt;磁盘空间耗尽&lt;/stro</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 RocksDB SST 文件大小控制</title>
    <link href="https://www.cyningsun.com/05-04-2025/rocksdb-sst-file-size.html"/>
    <id>https://www.cyningsun.com/05-04-2025/rocksdb-sst-file-size.html</id>
    <published>2025-05-03T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<p>在 RocksDB 中，SST（Sorted String Table）文件是持久化存储数据的基本单位。SST 文件的大小对 RocksDB 的性能有着深远影响：太小的文件会导致文件数量过多，增加元数据开销和文件打开&#x2F;关闭的操作负担；太大的文件则可能导致读放大和更高的 compaction 负担。</p><p>本文将基于 RocksDB v8.8.1 详细介绍在启用 Leveled Compaction、MinOverlapping 策略和 <code>level_compaction_dynamic_file_size</code> 的情况下，RocksDB 如何精确控制 SST 文件的大小，并结合图解详细阐述其复杂的文件切分决策机制。</p><h2 id="一、控制-SST-文件大小的配置参数"><a href="#一、控制-SST-文件大小的配置参数" class="headerlink" title="一、控制 SST 文件大小的配置参数"></a>一、控制 SST 文件大小的配置参数</h2><p>RocksDB 通过以下核心参数控制 SST 文件大小：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 主要控制参数</span>Options options;options.target_file_size_base = <span class="hljs-number">64</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>;        <span class="hljs-comment">// 默认 64MB</span>options.target_file_size_multiplier = <span class="hljs-number">1</span>;                 <span class="hljs-comment">// 默认为 1</span>options.max_compaction_bytes = <span class="hljs-number">1.6</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>; <span class="hljs-comment">// 默认 1.6GB</span>options.write_buffer_size = <span class="hljs-number">64</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>;           <span class="hljs-comment">// 默认 64MB，影响 L0 文件大小</span></code></pre><p>以上参数的具体含义是：</p><ul><li><strong><code>target_file_size_base</code></strong>: 定义 L1 层文件的目标大小，默认为 64MB</li><li><strong><code>target_file_size_multiplier</code></strong>: 文件大小在各层之间的增长倍数，默认为 1</li><li><strong><code>max_compaction_bytes</code></strong>: 限制单次压缩操作产生的最大文件大小，默认为 1.6GB</li><li><strong><code>write_buffer_size</code></strong>: 控制 Memtable 的大小，间接影响从 Memtable 刷盘生成的 L0 文件大小</li></ul><h2 id="二、不同层级-SST-文件大小的计算规则"><a href="#二、不同层级-SST-文件大小的计算规则" class="headerlink" title="二、不同层级 SST 文件大小的计算规则"></a>二、不同层级 SST 文件大小的计算规则</h2><p>RocksDB 会根据层级不同，为每个层级计算一个目标文件大小。这一计算过程在 <code>MutableCFOptions::RefreshDerivedOptions</code> 函数中实现：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">MutableCFOptions::RefreshDerivedOptions</span><span class="hljs-params">(<span class="hljs-type">int</span> num_levels,</span></span><span class="hljs-params"><span class="hljs-function">                                            CompactionStyle compaction_style)</span> </span>&#123;  max_file_size.<span class="hljs-built_in">resize</span>(num_levels);  <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; num_levels; ++i) &#123;    <span class="hljs-keyword">if</span> (i == <span class="hljs-number">0</span> &amp;&amp; compaction_style == kCompactionStyleUniversal) &#123;      max_file_size[i] = ULLONG_MAX;  <span class="hljs-comment">// Universal 压缩中的 L0 层不限制大小</span>    &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (i &gt; <span class="hljs-number">1</span>) &#123;      <span class="hljs-comment">// 非 L0 和 L1 层：上一层文件大小 * 倍数</span>      max_file_size[i] = <span class="hljs-built_in">MultiplyCheckOverflow</span>(max_file_size[i - <span class="hljs-number">1</span>],                                              target_file_size_multiplier);    &#125; <span class="hljs-keyword">else</span> &#123;      <span class="hljs-comment">// L0 (非Universal) 和 L1 层：使用基础大小</span>      max_file_size[i] = target_file_size_base;    &#125;  &#125;&#125;</code></pre><p>根据上述代码，可以得出不同层级 SST 文件大小的计算公式：</p><pre><code class="hljs excel"><span class="hljs-built_in">Ln</span> 层文件大小 = target_file_size_base * (target_file_size_multiplier^(<span class="hljs-built_in">n</span>-<span class="hljs-number">1</span>))</code></pre><p>具体来说：</p><ol><li><p><strong>L0 层</strong>:</p><ul><li>对于 Universal 压缩风格：文件大小不受限制 (ULLONG_MAX)</li><li>其他压缩风格：<code>target_file_size_base</code> (默认 64MB)</li><li>但实际上 L0 的文件大小主要取决于 memtable 刷盘过程</li></ul></li><li><p><strong>L1 层</strong>:</p><ul><li>总是设置为 <code>target_file_size_base</code> (默认 64MB)</li></ul></li><li><p><strong>L2 及更高层级</strong>:</p><ul><li>按照公式：上一层大小 × <code>target_file_size_multiplier</code></li><li>例如，若 <code>target_file_size_multiplier</code> 为 2：<ul><li>L1: 64MB</li><li>L2: 128MB</li><li>L3: 256MB</li><li>以此类推</li></ul></li></ul></li></ol><h2 id="三、不同场景下的-SST-文件大小控制"><a href="#三、不同场景下的-SST-文件大小控制" class="headerlink" title="三、不同场景下的 SST 文件大小控制"></a>三、不同场景下的 SST 文件大小控制</h2><h3 id="3-1-L0-层文件大小控制"><a href="#3-1-L0-层文件大小控制" class="headerlink" title="3.1 L0 层文件大小控制"></a>3.1 L0 层文件大小控制</h3><p>L0 层的 SST 文件有两种生成方式：</p><h4 id="3-1-1-Memtable-刷盘生成"><a href="#3-1-1-Memtable-刷盘生成" class="headerlink" title="3.1.1 Memtable 刷盘生成"></a>3.1.1 Memtable 刷盘生成</h4><p>当 memtable 满或达到其他刷盘条件时，会被刷到 L0 层生成 SST 文件：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 文件大小由 memtable 大小决定，通常由 write_buffer_size 控制</span><span class="hljs-comment">// 这类文件的大小不受 target_file_size 参数限制</span><span class="hljs-function">Status <span class="hljs-title">DBImpl::FlushMemTableToOutputFile</span><span class="hljs-params">(...)</span> </span>&#123;  <span class="hljs-comment">// ...</span>  <span class="hljs-comment">// 从 memtable 创建 SST 文件，大小取决于 memtable 实际内容大小</span>  s = <span class="hljs-built_in">BuildTable</span>(...)  <span class="hljs-comment">// ...</span>&#125;</code></pre><p>该方式生成的 L0 文件大小近似等于 memtable 中实际数据大小，<strong>不受</strong> <code>target_file_size</code> 参数限制。当 <code>min_write_buffer_number_to_merge</code> 设置为 2 时，Memtable 刷盘生成的 SST 文件大小约为 2 个 <code>write_buffer_size</code> 的大小（实际情况下，数据压缩、记录合并或删除会降低文件大小）。因为该参数控制了在刷盘前需要合并的最小 memtable 数量。</p><h4 id="3-1-2-Intra-L0-compaction-生成"><a href="#3-1-2-Intra-L0-compaction-生成" class="headerlink" title="3.1.2 Intra-L0 compaction 生成"></a>3.1.2 Intra-L0 compaction 生成</h4><p>当 L0 层文件过多时，RocksDB 会触发 Intra-L0 compaction，合并多个 L0 文件：</p><pre><code class="hljs cpp"><span class="hljs-comment">// L0 compaction 生成的文件大小最大不超过 max_compaction_bytes</span><span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">output_level</span>() == <span class="hljs-number">0</span>) &#123;  <span class="hljs-comment">// L0 层不应用基于祖父文件边界的文件切分启发式算法</span>  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;&#125;</code></pre><p>Intra-L0 compaction 生成的文件大小主要受 <code>max_compaction_bytes</code> 限制，文件最大为 <code>max_compaction_bytes</code>。</p><h3 id="3-2-非-L0-和非-Bottommost-层文件大小控制"><a href="#3-2-非-L0-和非-Bottommost-层文件大小控制" class="headerlink" title="3.2 非 L0 和非 Bottommost 层文件大小控制"></a>3.2 非 L0 和非 Bottommost 层文件大小控制</h3><p>对于 L1 到倒数第二层的文件，RocksDB 在 <code>Compaction</code> 构造函数中设置其大小限制：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 在 Compaction 构造函数中计算目标输出文件大小</span>Compaction::<span class="hljs-built_in">Compaction</span>(...) &#123;  <span class="hljs-comment">// ...</span>  target_output_file_size_ = <span class="hljs-built_in">MaxFileSizeForLevel</span>(      mutable_cf_options, output_level, compaction_style, base_level,      level_compaction_dynamic_level_bytes);  <span class="hljs-comment">// 非底层文件可能是目标大小的 2 倍</span>  max_output_file_size_ =      bottommost_level_ || grandparents_.<span class="hljs-built_in">empty</span>() ||              !_immutable_options.level_compaction_dynamic_file_size          ? target_output_file_size_          : <span class="hljs-number">2</span> * target_output_file_size_;  <span class="hljs-comment">// ...</span>&#125;</code></pre><p>从代码可以看出，当满足下列条件时，非底层文件的最大大小可能是目标大小的 2 倍：</p><ol><li>非底层 compaction</li><li>有祖父层文件</li><li>启用了动态文件大小调整选项</li></ol><p>这种设计允许 RocksDB 在特定场景下生成更大的中间层文件，减少文件数量。</p><h3 id="3-3-Bottommost-层文件大小控制"><a href="#3-3-Bottommost-层文件大小控制" class="headerlink" title="3.3 Bottommost 层文件大小控制"></a>3.3 Bottommost 层文件大小控制</h3><p>最底层的文件大小严格遵循 <code>target_output_file_size_</code>：</p><pre><code class="hljs cpp">max_output_file_size_ =    bottommost_level_ || grandparents_.<span class="hljs-built_in">empty</span>() ||            !_immutable_options.level_compaction_dynamic_file_size        ? target_output_file_size_  <span class="hljs-comment">// 底层使用目标大小</span>        : <span class="hljs-number">2</span> * target_output_file_size_;</code></pre><p>当 compaction 输出到最底层时（<code>bottommost_level_</code> 为 true），最大输出文件大小就等于目标文件大小，不会有 2 倍的扩展。</p><h3 id="3-4-动态层级大小下的文件大小计算"><a href="#3-4-动态层级大小下的文件大小计算" class="headerlink" title="3.4 动态层级大小下的文件大小计算"></a>3.4 动态层级大小下的文件大小计算</h3><p>当启用 <code>level_compaction_dynamic_level_bytes</code> 选项时，RocksDB 使用相对于基础层级的方式计算文件大小：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">uint64_t</span> <span class="hljs-title">MaxFileSizeForLevel</span><span class="hljs-params">(<span class="hljs-type">const</span> MutableCFOptions&amp; cf_options,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">int</span> level, CompactionStyle compaction_style, <span class="hljs-type">int</span> base_level,</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">bool</span> level_compaction_dynamic_level_bytes)</span> </span>&#123;  <span class="hljs-comment">// 检查是否使用动态层大小</span>  <span class="hljs-keyword">if</span> (!level_compaction_dynamic_level_bytes || level &lt; base_level ||      compaction_style != kCompactionStyleLevel) &#123;    <span class="hljs-comment">// 常规计算方式：直接使用层级对应的大小</span>    <span class="hljs-built_in">assert</span>(level &gt;= <span class="hljs-number">0</span>);    <span class="hljs-built_in">assert</span>(level &lt; (<span class="hljs-type">int</span>)cf_options.max_file_size.<span class="hljs-built_in">size</span>());    <span class="hljs-keyword">return</span> cf_options.max_file_size[level];  &#125; <span class="hljs-keyword">else</span> &#123;    <span class="hljs-comment">// 动态层大小方式：使用相对于基础层的偏移</span>    <span class="hljs-built_in">assert</span>(level &gt;= <span class="hljs-number">0</span> &amp;&amp; base_level &gt;= <span class="hljs-number">0</span>);    <span class="hljs-built_in">assert</span>(level - base_level &lt; (<span class="hljs-type">int</span>)cf_options.max_file_size.<span class="hljs-built_in">size</span>());    <span class="hljs-keyword">return</span> cf_options.max_file_size[level - base_level];  &#125;&#125;</code></pre><p>这是因为在 <code>level_compaction_dynamic_level_bytes</code> 模式下，RocksDB 的基础层级可能不是 L1，而是由系统根据数据量动态调整的。在这种情况下，我们需要使用相对于基础层级的偏移量来索引文件大小数组。</p><h2 id="四、Compaction-中的-SST-文件切分策略"><a href="#四、Compaction-中的-SST-文件切分策略" class="headerlink" title="四、Compaction 中的 SST 文件切分策略"></a>四、Compaction 中的 SST 文件切分策略</h2><p>在 compaction 过程中，RocksDB 需要决定何时应该 “ 切割 “ 一个正在写入的输出 SST 文件。这是由 <code>CompactionOutputs::ShouldStopBefore</code> 函数实现的，这是控制 SST 文件大小的关键逻辑。</p><p>该函数需要依赖一个重要的辅助函数 <code>UpdateGrandparentBoundaryInfo</code>，该函数负责跟踪正在构建的输出文件与祖父层（L+2 层）文件的关系，为切分决策提供必要的信息。先来深入理解这个辅助函数的工作原理。</p><h3 id="4-1-追踪祖父层文件边界：UpdateGrandparentBoundaryInfo-函数"><a href="#4-1-追踪祖父层文件边界：UpdateGrandparentBoundaryInfo-函数" class="headerlink" title="4.1 追踪祖父层文件边界：UpdateGrandparentBoundaryInfo 函数"></a>4.1 追踪祖父层文件边界：UpdateGrandparentBoundaryInfo 函数</h3><p><code>UpdateGrandparentBoundaryInfo</code> 函数的主要作用是跟踪键与祖父层文件的相对位置关系，更新状态并返回键跨越的边界数。</p><p>下面是该函数的代码和详细注释：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 更新关于当前内部键 `internal_key` 与祖父层（L+2）文件边界和重叠的信息。</span><span class="hljs-function"><span class="hljs-type">size_t</span> <span class="hljs-title">CompactionOutputs::UpdateGrandparentBoundaryInfo</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> Slice&amp; internal_key)</span> </span>&#123;  <span class="hljs-comment">// 初始化当前键跨越的边界数量为 0。</span>  <span class="hljs-type">size_t</span> curr_key_boundary_switched_num = <span class="hljs-number">0</span>;  <span class="hljs-comment">// 获取祖父层文件列表。</span>  <span class="hljs-type">const</span> std::vector&lt;FileMetaData*&gt;&amp; grandparents = compaction_-&gt;<span class="hljs-built_in">grandparents</span>();  <span class="hljs-comment">// 如果没有祖父文件（例如，输出到 L0 或最底层），则无需执行任何操作。</span>  <span class="hljs-keyword">if</span> (grandparents.<span class="hljs-built_in">empty</span>()) &#123;    <span class="hljs-keyword">return</span> curr_key_boundary_switched_num;  &#125;  <span class="hljs-comment">// 获取用户键比较器。</span>  <span class="hljs-type">const</span> Comparator* ucmp = compaction_-&gt;<span class="hljs-built_in">column_family_data</span>()-&gt;<span class="hljs-built_in">user_comparator</span>();  <span class="hljs-comment">// 移动 `grandparent_index_` 指向包含当前用户键的文件。</span>  <span class="hljs-comment">// 如果多个文件包含相同的用户键，请确保索引指向包含该键的最后一个文件。</span>  <span class="hljs-keyword">while</span> (grandparent_index_ &lt; grandparents.<span class="hljs-built_in">size</span>()) &#123;    <span class="hljs-comment">// 检查当前是否处于祖父文件之间的间隙中。</span>    <span class="hljs-keyword">if</span> (being_grandparent_gap_) &#123;      <span class="hljs-comment">// 如果当前键的用户键仍然小于下一个祖父文件的最小用户键，</span>      <span class="hljs-comment">// 则表示仍在间隙中，跳出循环。</span>      <span class="hljs-keyword">if</span> (<span class="hljs-built_in">sstableKeyCompare</span>(ucmp, internal_key,                            grandparents[grandparent_index_]-&gt;smallest) &lt; <span class="hljs-number">0</span>) &#123;        <span class="hljs-keyword">break</span>;      &#125;      <span class="hljs-comment">// 当前键已进入 `grandparent_index_` 指向的祖父文件。</span>      <span class="hljs-comment">// 只有在处理过至少一个键后（`seen_key_` 为 true），才计算边界切换。</span>      <span class="hljs-keyword">if</span> (seen_key_) &#123;        <span class="hljs-comment">// 当前键跨越了一个边界（从间隙进入文件）。</span>        curr_key_boundary_switched_num++;        <span class="hljs-comment">// 将新进入的祖父文件的完整大小添加到重叠字节数中。</span>        grandparent_overlapped_bytes_ +=            grandparents[grandparent_index_]-&gt;fd.<span class="hljs-built_in">GetFileSize</span>();        <span class="hljs-comment">// 增加当前输出文件跨越的总边界数。</span>        grandparent_boundary_switched_num_++;      &#125;      <span class="hljs-comment">// 不再处于间隙中。</span>      being_grandparent_gap_ = <span class="hljs-literal">false</span>;    &#125; <span class="hljs-keyword">else</span> &#123; <span class="hljs-comment">// 当前处于 `grandparent_index_` 指向的祖父文件内部。</span>      <span class="hljs-comment">// 比较当前键的用户键与当前祖父文件的最大用户键。</span>      <span class="hljs-type">int</span> cmp_result = <span class="hljs-built_in">sstableKeyCompare</span>(          ucmp, internal_key, grandparents[grandparent_index_]-&gt;largest);      <span class="hljs-comment">// 如果满足以下条件之一，则跳出循环（表示当前键仍在当前祖父文件范围内）：</span>      <span class="hljs-comment">// 1. 当前键严格小于当前祖父文件的最大键。</span>      <span class="hljs-comment">// 2. 当前键等于当前祖父文件的最大键，并且：</span>      <span class="hljs-comment">//    a) 这是最后一个祖父文件。</span>      <span class="hljs-comment">//    b) 或者，当前键严格小于下一个祖父文件的最小键（确保 `grandparent_index_` 指向包含该键的最后一个文件）。</span>      <span class="hljs-keyword">if</span> (cmp_result &lt; <span class="hljs-number">0</span> ||          (cmp_result == <span class="hljs-number">0</span> &amp;&amp;           (grandparent_index_ == grandparents.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span> ||            <span class="hljs-built_in">sstableKeyCompare</span>(ucmp, internal_key,                              grandparents[grandparent_index_ + <span class="hljs-number">1</span>]-&gt;smallest) &lt;                <span class="hljs-number">0</span>))) &#123;        <span class="hljs-keyword">break</span>;      &#125;      <span class="hljs-comment">// 当前键已超出当前祖父文件的范围。</span>      <span class="hljs-comment">// 只有在处理过至少一个键后（`seen_key_` 为 true），才计算边界切换。</span>      <span class="hljs-keyword">if</span> (seen_key_) &#123;        <span class="hljs-comment">// 当前键跨越了一个边界（从文件进入间隙）。</span>        curr_key_boundary_switched_num++;        <span class="hljs-comment">// 增加当前输出文件跨越的总边界数。</span>        grandparent_boundary_switched_num_++;      &#125;      <span class="hljs-comment">// 现在处于间隙中。</span>      being_grandparent_gap_ = <span class="hljs-literal">true</span>;      <span class="hljs-comment">// 移动到下一个祖父文件（或文件后的间隙）。</span>      grandparent_index_++;    &#125;  &#125;  <span class="hljs-comment">// 特殊处理第一个键 (`seen_key_` 为 false)。</span>  <span class="hljs-comment">// 如果第一个键直接落入某个祖父文件内部（而不是间隙），则计算其初始重叠字节数。</span>  <span class="hljs-keyword">if</span> (!seen_key_ &amp;&amp; !being_grandparent_gap_) &#123;    <span class="hljs-comment">// 初始重叠应为 0。</span>    <span class="hljs-built_in">assert</span>(grandparent_overlapped_bytes_ == <span class="hljs-number">0</span>);    <span class="hljs-comment">// 调用 GetCurrentKeyGrandparentOverlappedBytes 计算初始重叠。</span>    grandparent_overlapped_bytes_ =        <span class="hljs-built_in">GetCurrentKeyGrandparentOverlappedBytes</span>(internal_key);  &#125;  <span class="hljs-comment">// 标记已处理过至少一个键。</span>  seen_key_ = <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 返回当前这个 `internal_key` 跨越的边界数量。</span>  <span class="hljs-keyword">return</span> curr_key_boundary_switched_num;&#125;</code></pre><h4 id="4-1-1-UpdateGrandparentBoundaryInfo-函数的核心状态变量"><a href="#4-1-1-UpdateGrandparentBoundaryInfo-函数的核心状态变量" class="headerlink" title="4.1.1 UpdateGrandparentBoundaryInfo 函数的核心状态变量"></a>4.1.1 UpdateGrandparentBoundaryInfo 函数的核心状态变量</h4><p>该函数维护了几个关键的状态变量：</p><ul><li><strong><code>seen_key_</code></strong>: 是否已处理过至少一个键</li><li><strong><code>being_grandparent_gap_</code></strong>: 当前键是否位于祖父文件之间的间隙中</li><li><strong><code>grandparent_index_</code></strong>: 指向当前祖父文件数组中的位置索引</li><li><strong><code>grandparent_boundary_switched_num_</code></strong>: 当前输出文件已跨越的祖父边界总数</li><li><strong><code>grandparent_overlapped_bytes_</code></strong>: 与当前输出文件重叠的祖父文件总大小</li><li><strong><code>curr_key_boundary_switched_num</code></strong>: <strong>当前的键</strong>跨越的祖父边界数量 (返回值)</li></ul><h4 id="4-1-2-图解-UpdateGrandparentBoundaryInfo-六种典型场景"><a href="#4-1-2-图解-UpdateGrandparentBoundaryInfo-六种典型场景" class="headerlink" title="4.1.2 图解 UpdateGrandparentBoundaryInfo 六种典型场景"></a>4.1.2 图解 UpdateGrandparentBoundaryInfo 六种典型场景</h4><p>按照函数处理 KEY 的数量，通过图解来详细分析 <code>UpdateGrandparentBoundaryInfo</code> 函数在六种不同场景下的行为：</p><p><img src="/images/rocksdb-sst-file-size/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20RocksDB%20SST%20%E6%96%87%E4%BB%B6%E5%A4%A7%E5%B0%8F%E6%8E%A7%E5%88%B6-20250423164046-1.png" alt="深入理解 RocksDB SST 文件大小控制-20250423164046-1.png"></p><p>图中上方显示了三层文件：灰色为输入层文件（L 和 L+1），蓝色为祖父层文件（L+2）。使用两种 L 层的例子来覆盖所有的场景，以颜色深浅对应 L 层以及具体的场景。下面则详细展示了处理不同键时的状态变化。</p><p><strong>1. 第一个 KEY，不在 grandparent file 中</strong></p><p><strong>情景</strong>：处理第一个键 (key&#x3D;1)，该键不在任何祖父文件范围内（位于间隙中）。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; false</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 0（指向 [2, 4] 文件）</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; 0</li></ul><p><strong>调用后</strong>：</p><ul><li><strong><code>seen_key_</code> &#x3D; true</strong></li><li><strong><code>being_grandparent_gap_</code> &#x3D; true（从 false 变为 true，表示进入文件前的间隙）</strong></li><li><code>grandparent_index_</code> &#x3D; 0</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; 0</li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 0</li></ul><p><strong>关键点</strong>：首次调用时不会计算边界切换，只是确定初始状态。由于键在间隙中，设置 <code>being_grandparent_gap_</code> &#x3D; true。</p><p><strong>2. 第一个 KEY，在 grandparent file 中</strong></p><p><strong>情景</strong>：处理第一个键 (key&#x3D;2)，该键位于祖父文件 [2, 4] 范围内。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; false</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 0</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; 0</li></ul><p><strong>调用后</strong>：</p><ul><li><strong><code>seen_key_</code> &#x3D; true</strong></li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 0</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><strong><code>grandparent_overlapped_bytes_</code> &#x3D; size([2, 4])</strong></li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 0</li></ul><p><strong>关键点</strong>：首次调用且键在文件内时，计算并初始化 <code>grandparent_overlapped_bytes_</code>，但不增加边界切换计数。</p><p><strong>3. 后续的 KEY，在 grandparent file 中</strong></p><p><strong>情景</strong>：处理后续键 (key&#x3D;3)，该键仍在同一个祖父文件 [2, 4] 范围内。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 0</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2, 4])</li></ul><p><strong>调用后</strong>：</p><ul><li>所有值保持不变</li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 0</li></ul><p><strong>关键点</strong>：键仍在同一文件中，没有跨越边界，所有状态保持不变。</p><p><strong>4. 后续的 KEY，在 grandparent 的 GAP 中</strong></p><p><strong>情景</strong>：处理后续键 (key&#x3D;5)，该键已离开祖父文件 [2, 4]，进入了文件间的间隙。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 0</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 0</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2, 4])</li></ul><p><strong>调用后</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><strong><code>being_grandparent_gap_</code> &#x3D; true</strong></li><li><strong><code>grandparent_index_</code> &#x3D; 1</strong></li><li><strong><code>grandparent_boundary_switched_num_</code> &#x3D; 1</strong></li><li><strong><code>grandparent_overlapped_bytes_</code> &#x3D; size([2, 4])</strong></li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 1</li></ul><p><strong>关键点</strong>：键跨越了文件边界（从文件到间隙），增加边界切换计数，但重叠字节数不变（因为只是离开文件，而非进入新文件）。</p><p><strong>5. 后续的 KEY，在最后一个 grandparent 的末尾</strong></p><p><strong>情景</strong>：处理后续键 (key&#x3D;24)，该键位于最后一个祖父文件 [22, 24] 的末尾。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 2（指向 [22, 24] 文件）</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 2</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2,4]) + size([11,15]) + size([22,24])</li></ul><p><strong>调用后</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 2</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 2</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2,4]) + size([11,15]) + size([22,24])</li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 0</li></ul><p><strong>关键点</strong>：当 key&#x3D;24 恰好等于最后一个文件 [22, 24] 的 largest key 时，仍被视为在文件内，所有状态保持不变。</p><p><strong>6. 后续的 KEY，超出所有 grandparent 范围</strong></p><p><strong>情景</strong>：处理后续键 (key&#x3D;25)，该键超出了所有祖父文件的范围。</p><p><strong>调用前</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><code>being_grandparent_gap_</code> &#x3D; false</li><li><code>grandparent_index_</code> &#x3D; 2（指向 [22, 24] 文件）</li><li><code>grandparent_boundary_switched_num_</code> &#x3D; 2</li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2,4]) + size([11,15]) + size([22,24])</li></ul><p><strong>调用后</strong>：</p><ul><li><code>seen_key_</code> &#x3D; true</li><li><strong><code>being_grandparent_gap_</code> &#x3D; true（从 false 变为 true，表示进入文件后的间隙）</strong></li><li><strong><code>grandparent_index_</code> &#x3D; 3（从 2 增加到 3，指向文件列表结束后的位置）</strong></li><li><strong><code>grandparent_boundary_switched_num_</code> &#x3D; 3（从 2 增加到 3，增加了一次边界切换）</strong></li><li><code>grandparent_overlapped_bytes_</code> &#x3D; size([2,4]) + size([11,15]) + size([22,24])</li><li>返回 <code>curr_key_boundary_switched_num</code> &#x3D; 1</li></ul><p><strong>关键点</strong>：键超出了最后一个文件范围，标记为进入间隙，增加边界切换计数和索引，但重叠字节数不变。</p><h3 id="4-2-计算祖父层重叠文件大小"><a href="#4-2-计算祖父层重叠文件大小" class="headerlink" title="4.2 计算祖父层重叠文件大小"></a>4.2 计算祖父层重叠文件大小</h3><p>为了计算重叠字节数，RocksDB 实现了 <code>GetCurrentKeyGrandparentOverlappedBytes</code> 函数：</p><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">uint64_t</span> <span class="hljs-title">CompactionOutputs::GetCurrentKeyGrandparentOverlappedBytes</span><span class="hljs-params">(</span></span><span class="hljs-params"><span class="hljs-function">    <span class="hljs-type">const</span> Slice&amp; internal_key)</span> <span class="hljs-type">const</span> </span>&#123;  <span class="hljs-keyword">if</span> (being_grandparent_gap_) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  <span class="hljs-comment">// 在间隙中，无重叠</span>  &#125;  <span class="hljs-type">uint64_t</span> overlapped_bytes = <span class="hljs-number">0</span>;  <span class="hljs-type">const</span> std::vector&lt;FileMetaData*&gt;&amp; grandparents = compaction_-&gt;<span class="hljs-built_in">grandparents</span>();  <span class="hljs-type">const</span> Comparator* ucmp = compaction_-&gt;<span class="hljs-built_in">column_family_data</span>()-&gt;<span class="hljs-built_in">user_comparator</span>();  InternalKey ikey;  ikey.<span class="hljs-built_in">DecodeFrom</span>(internal_key);  <span class="hljs-comment">// 加上主要重叠文件的大小</span>  overlapped_bytes += grandparents[grandparent_index_]-&gt;fd.<span class="hljs-built_in">GetFileSize</span>();  <span class="hljs-comment">// 查找所有边界重叠的文件</span>  <span class="hljs-keyword">for</span> (<span class="hljs-type">int64_t</span> i = <span class="hljs-built_in">static_cast</span>&lt;<span class="hljs-type">int64_t</span>&gt;(grandparent_index_) - <span class="hljs-number">1</span>;       i &gt;= <span class="hljs-number">0</span> &amp;&amp; <span class="hljs-built_in">sstableKeyCompare</span>(ucmp, ikey, grandparents[i]-&gt;largest) == <span class="hljs-number">0</span>;       i--) &#123;    overlapped_bytes += grandparents[i]-&gt;fd.<span class="hljs-built_in">GetFileSize</span>();  &#125;  <span class="hljs-keyword">return</span> overlapped_bytes;&#125;</code></pre><p>该函数处理了一种特殊情况：当多个祖父文件有相同的边界键时，一个键可能与多个文件重叠。例如：</p><pre><code class="hljs llvm">输出文件: [<span class="hljs-keyword">c</span>...祖父文件: [b<span class="hljs-punctuation">,</span> b] [<span class="hljs-keyword">c</span><span class="hljs-punctuation">,</span> <span class="hljs-keyword">c</span>] [<span class="hljs-keyword">c</span><span class="hljs-punctuation">,</span> <span class="hljs-keyword">c</span>] [<span class="hljs-keyword">c</span><span class="hljs-punctuation">,</span> d]</code></pre><p>在这种情况下，键 ‘c’ 可能与多个祖父文件重叠，函数会累加所有这些重叠文件的大小。</p><h3 id="4-3-文件切分决策：ShouldStopBefore-函数"><a href="#4-3-文件切分决策：ShouldStopBefore-函数" class="headerlink" title="4.3 文件切分决策：ShouldStopBefore 函数"></a>4.3 文件切分决策：ShouldStopBefore 函数</h3><p>在 compaction 过程中，RocksDB 需要决定何时应该 “ 切割 “ 一个正在写入的输出 SST 文件。这是由 <code>CompactionOutputs::ShouldStopBefore</code> 函数实现的：</p><pre><code class="hljs cpp"><span class="hljs-comment">// 决定是否应在添加来自 `c_iter` 的键之前完成（停止写入）当前的输出文件。</span><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">CompactionOutputs::ShouldStopBefore</span><span class="hljs-params">(<span class="hljs-type">const</span> CompactionIterator&amp; c_iter)</span> </span>&#123;  <span class="hljs-comment">// 断言迭代器有效并指向一个键。</span>  <span class="hljs-built_in">assert</span>(c_iter.<span class="hljs-built_in">Valid</span>());  <span class="hljs-comment">// 从迭代器获取内部键 (user_key + seq + type + ts)。</span>  <span class="hljs-type">const</span> Slice&amp; internal_key = c_iter.<span class="hljs-built_in">key</span>();  <span class="hljs-comment">// 存储在考虑当前键 *之前* 的重叠大小。稍后用于查看重叠 *增加* 了多少。</span>  <span class="hljs-type">const</span> <span class="hljs-type">uint64_t</span> previous_overlapped_bytes = grandparent_overlapped_bytes_;  <span class="hljs-comment">// 初始化变量以跟踪边界交叉和 TTL 决策。</span>  <span class="hljs-type">size_t</span> num_grandparent_boundaries_crossed = <span class="hljs-number">0</span>;  <span class="hljs-type">bool</span> should_stop_for_ttl = <span class="hljs-literal">false</span>;  <span class="hljs-comment">// 更新祖父文件信息和TTL状态</span>  <span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">output_level</span>() &gt; <span class="hljs-number">0</span>) &#123;    <span class="hljs-comment">// 根据当前键更新祖父文件跟踪状态。返回此键跨越的祖父文件边界数量。</span>    num_grandparent_boundaries_crossed =        <span class="hljs-built_in">UpdateGrandparentBoundaryInfo</span>(internal_key);    <span class="hljs-comment">// 检查当前键是否根据 TTL 规则触发文件切割</span>    should_stop_for_ttl = <span class="hljs-built_in">UpdateFilesToCutForTTLStates</span>(internal_key);  &#125;  <span class="hljs-comment">// 基本检查 - 如果没有活动的TableBuilder，不能切割文件</span>  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">HasBuilder</span>()) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;  &#125;  <span class="hljs-comment">// 如果TTL逻辑决定需要切割文件，立即执行</span>  <span class="hljs-keyword">if</span> (should_stop_for_ttl) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  &#125;  <span class="hljs-comment">// 分区器检查 - 询问自定义SST分区器是否应切割文件</span>  <span class="hljs-keyword">if</span> (partitioner_ &amp;&amp; partitioner_-&gt;<span class="hljs-built_in">ShouldPartition</span>(<span class="hljs-built_in">PartitionerRequest</span>(                          last_key_for_partitioner_,                          c_iter.<span class="hljs-built_in">user_key</span>(),                          current_output_file_size_                          )) == kRequired) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>; <span class="hljs-comment">// 分区器要求切割</span>  &#125;  <span class="hljs-comment">// 级别特定检查 - L0层通常不按大小或祖父重叠启发式分割</span>  <span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">output_level</span>() == <span class="hljs-number">0</span>) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;  &#125;  <span class="hljs-comment">// 大小检查 - 如果达到最大文件大小，则强制切割</span>  <span class="hljs-keyword">if</span> (current_output_file_size_ &gt;= compaction_-&gt;<span class="hljs-built_in">max_output_file_size</span>()) &#123;    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  &#125;  <span class="hljs-comment">// RoundRobin分割检查 - 针对kRoundRobin压缩优先级</span>  <span class="hljs-keyword">if</span> (local_output_split_key_ != <span class="hljs-literal">nullptr</span> &amp;&amp; !is_split_) &#123;    <span class="hljs-comment">// 当下一个键大于或等于游标时发生分割</span>    <span class="hljs-keyword">if</span> (icmp-&gt;<span class="hljs-built_in">Compare</span>(internal_key, local_output_split_key_-&gt;<span class="hljs-built_in">Encode</span>()) &gt;= <span class="hljs-number">0</span>) &#123;      is_split_ = <span class="hljs-literal">true</span>;      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;    &#125;  &#125;  <span class="hljs-comment">// 祖父文件边界启发式逻辑 (仅适用于当前键跨越了祖父边界时)</span>  <span class="hljs-keyword">if</span> (num_grandparent_boundaries_crossed &gt; <span class="hljs-number">0</span>) &#123;    <span class="hljs-comment">// 启发式1：防止大型未来Compaction</span>    <span class="hljs-keyword">if</span> (grandparent_overlapped_bytes_ + current_output_file_size_ &gt;        compaction_-&gt;<span class="hljs-built_in">max_compaction_bytes</span>()) &#123;      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;    &#125;    <span class="hljs-comment">// 启发式2：隔离可跳过的祖父文件（动态大小）</span>    <span class="hljs-type">const</span> <span class="hljs-type">size_t</span> num_skippable_boundaries_crossed =        being_grandparent_gap_ ? <span class="hljs-number">2</span> : <span class="hljs-number">3</span>;    <span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;compaction_style ==            kCompactionStyleLevel &amp;&amp;        compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;level_compaction_dynamic_file_size &amp;&amp;        num_grandparent_boundaries_crossed &gt;=            num_skippable_boundaries_crossed &amp;&amp;        grandparent_overlapped_bytes_ - previous_overlapped_bytes &gt;            compaction_-&gt;<span class="hljs-built_in">target_output_file_size</span>() / <span class="hljs-number">8</span>) &#123;      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;    &#125;    <span class="hljs-comment">// 启发式3：接近目标大小时的预先切割（动态大小）</span>    <span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;compaction_style ==            kCompactionStyleLevel &amp;&amp;        compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;level_compaction_dynamic_file_size &amp;&amp;        current_output_file_size_ &gt;=            ((compaction_-&gt;<span class="hljs-built_in">target_output_file_size</span>() + <span class="hljs-number">99</span>) / <span class="hljs-number">100</span>) *                (<span class="hljs-number">50</span> + std::<span class="hljs-built_in">min</span>(grandparent_boundary_switched_num_ * <span class="hljs-number">5</span>,                               <span class="hljs-type">size_t</span>&#123;<span class="hljs-number">40</span>&#125;))) &#123;      <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;    &#125;  &#125;  <span class="hljs-comment">// 如果以上条件均未满足，则暂时不切割文件</span>  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;&#125;</code></pre><p>让我们详细分析 <code>ShouldStopBefore</code> 函数中的核心启发式策略：</p><h4 id="4-3-1-基本大小限制"><a href="#4-3-1-基本大小限制" class="headerlink" title="4.3.1 基本大小限制"></a>4.3.1 基本大小限制</h4><p>当文件大小达到配置的最大输出文件大小时，强制切分文件：</p><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (current_output_file_size_ &gt;= compaction_-&gt;<span class="hljs-built_in">max_output_file_size</span>()) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 达到最大大小，必须切分文件</span>&#125;</code></pre><h4 id="4-3-2-祖父层文件边界启发式"><a href="#4-3-2-祖父层文件边界启发式" class="headerlink" title="4.3.2 祖父层文件边界启发式"></a>4.3.2 祖父层文件边界启发式</h4><p>在 compaction 过程中，RocksDB 会跟踪当前处理的键与祖父层（L+2 层）文件的关系。当键跨越祖父文件边界时，会触发一系列复杂的启发式规则：</p><h5 id="4-3-2-1-防止未来-Compaction-过大"><a href="#4-3-2-1-防止未来-Compaction-过大" class="headerlink" title="4.3.2.1 防止未来 Compaction 过大"></a>4.3.2.1 防止未来 Compaction 过大</h5><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (grandparent_overlapped_bytes_ + current_output_file_size_ &gt;    compaction_-&gt;<span class="hljs-built_in">max_compaction_bytes</span>()) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 切分文件以避免将来 compaction 过大</span>&#125;</code></pre><p>这个逻辑检查当前输出文件的大小加上它与祖父层文件的重叠大小是否超过了最大 compaction 字节数限制。如果超过，则切分文件，这是为了防止将来该生成的文件参与 compaction 时导致处理的数据量过大。</p><h5 id="4-3-2-2-隔离可跳过的祖父文件"><a href="#4-3-2-2-隔离可跳过的祖父文件" class="headerlink" title="4.3.2.2 隔离可跳过的祖父文件"></a>4.3.2.2 隔离可跳过的祖父文件</h5><pre><code class="hljs cpp"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> num_skippable_boundaries_crossed = being_grandparent_gap_ ? <span class="hljs-number">2</span> : <span class="hljs-number">3</span>;<span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;compaction_style == kCompactionStyleLevel &amp;&amp;    compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;level_compaction_dynamic_file_size &amp;&amp;    <span class="hljs-comment">// 是否跨越了足够多的边界以可能隔离一个文件？</span>    num_grandparent_boundaries_crossed &gt;= num_skippable_boundaries_crossed &amp;&amp;    <span class="hljs-comment">// 新增加的重叠（刚刚开始重叠的祖父文件的大小）是否合理地大？</span>    grandparent_overlapped_bytes_ - previous_overlapped_bytes &gt;        compaction_-&gt;<span class="hljs-built_in">target_output_file_size</span>() / <span class="hljs-number">8</span>) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 切割文件以隔离祖父文件</span>&#125;</code></pre><p>这段代码包含了一个精妙的优化策略。考虑以下场景：</p><pre><code class="hljs inform7">L1:    <span class="hljs-comment">[1,   21]</span>  &lt;- 当前正在合并的文件L2:  <span class="hljs-comment">[3,   23]</span>    &lt;- 当前正在合并的文件L3: <span class="hljs-comment">[2, 4]</span> <span class="hljs-comment">[11, 15]</span> <span class="hljs-comment">[22, 24]</span>  &lt;- 祖父层（L+2）文件</code></pre><p>如果不进行切分，L2 层的输出将是 <code>[1,3, 21,23]</code>，与 L3 层的三个文件都有重叠。但是，如果在跨越 L3 中间文件 <code>[11, 15]</code> 时切分，L2 的输出将变为两个文件：<code>[1,3]</code> 和 <code>[21,23]</code>，那么未来这两个文件分别 compact 到 L3 时，可以跳过中间的 <code>[11, 15]</code> 文件，从而减少重复读写。</p><p>RocksDB 会检查以下条件：</p><ol><li>使用 Level 压缩风格</li><li>启用了动态文件大小调整</li><li>当前键跨越的边界足够多（通常是完整跨越了一个文件）</li><li>刚跨越的祖父文件足够大（&gt;&#x3D; 目标大小的 1&#x2F;8）</li></ol><p>当所有这些条件都满足时，会切分当前输出文件。</p><h5 id="4-3-2-3-动态文件大小预分割"><a href="#4-3-2-3-动态文件大小预分割" class="headerlink" title="4.3.2.3 动态文件大小预分割"></a>4.3.2.3 动态文件大小预分割</h5><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;compaction_style == kCompactionStyleLevel &amp;&amp;    compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;level_compaction_dynamic_file_size &amp;&amp;    current_output_file_size_ &gt;=        <span class="hljs-comment">// 计算动态阈值</span>        ((compaction_-&gt;<span class="hljs-built_in">target_output_file_size</span>() + <span class="hljs-number">99</span>) / <span class="hljs-number">100</span>) *            (<span class="hljs-number">50</span> + std::<span class="hljs-built_in">min</span>(grandparent_boundary_switched_num_ * <span class="hljs-number">5</span>, <span class="hljs-type">size_t</span>&#123;<span class="hljs-number">40</span>&#125;))) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 在边界处预先切割文件</span>&#125;</code></pre><p>这是第三种启发式策略，目的是提前进行文件分割。当文件大小达到一个动态计算的阈值，且正好位于祖父文件边界处时，会提前切分文件。</p><p>阈值计算公式：</p><pre><code class="hljs text">阈值 = 目标大小 × (50% + min(已跨越边界数 × 5%, 40%))</code></pre><p>这意味着：</p><ul><li>初始阈值是目标文件大小的 50%</li><li>每跨越一个祖父边界，阈值增加 5%</li><li>阈值上限是目标文件大小的 90%</li></ul><p>这种动态阈值机制基于一个观察：如果一个文件已经跨越了多个祖父边界，那么它更有可能在未来继续跨越边界。因此，随着已跨越边界数的增加，文件切分的阈值也会提高，使系统更倾向于在边界处切分文件。</p><h2 id="五、业务特征对-SST-文件大小的影响"><a href="#五、业务特征对-SST-文件大小的影响" class="headerlink" title="五、业务特征对 SST 文件大小的影响"></a>五、业务特征对 SST 文件大小的影响</h2><p>从如上的分析上可以看出，RocksDB 能够较好的避免过大文件的产生，但是对于小文件却处理的不是很理想。以下是生产环境的数据量不到 300G 的 Rocksdb 实例，SST 文件达到数十万之多，可以想象其性能之差。full compaction 完毕之后文件数量只有数千，文件数量差两个数量级：</p><pre><code class="hljs sh">$&gt; find /data/kv-datanode/dbs/[0-9]* -<span class="hljs-built_in">type</span> f -name <span class="hljs-string">&quot;*.sst&quot;</span> | <span class="hljs-built_in">wc</span> -l147084</code></pre><h3 id="5-1-“隔离可跳过的祖父文件”-的-Corner-Case"><a href="#5-1-“隔离可跳过的祖父文件”-的-Corner-Case" class="headerlink" title="5.1 “隔离可跳过的祖父文件” 的 Corner Case"></a>5.1 “隔离可跳过的祖父文件” 的 Corner Case</h3><p>首先回顾“隔离可跳过的祖父文件”启发式算法 的代码注释明确指出：对于随机数据集（无论是均匀分布还是偏斜分布），很少会触发这个条件，但如果用户添加两个没有重叠的不同数据集，这种情况就很可能发生。</p><pre><code class="hljs cpp"><span class="hljs-comment">// ...</span><span class="hljs-comment">// For random datasets (either evenly distributed or skewed), it rarely</span><span class="hljs-comment">// triggers this condition, but if the user is adding 2 different datasets</span><span class="hljs-comment">// without any overlap, it may likely happen.</span><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> num_skippable_boundaries_crossed =    being_grandparent_gap_ ? <span class="hljs-number">2</span> : <span class="hljs-number">3</span>;<span class="hljs-keyword">if</span> (compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;compaction_style == kCompactionStyleLevel &amp;&amp;    compaction_-&gt;<span class="hljs-built_in">immutable_options</span>()-&gt;level_compaction_dynamic_file_size &amp;&amp;    num_grandparent_boundaries_crossed &gt;= num_skippable_boundaries_crossed &amp;&amp;    grandparent_overlapped_bytes_ - previous_overlapped_bytes &gt;        compaction_-&gt;<span class="hljs-built_in">target_output_file_size</span>() / <span class="hljs-number">8</span>) &#123;  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;  <span class="hljs-comment">// 切割文件以隔离祖父文件</span>&#125;</code></pre><h3 id="5-2-雪花算法与小文件问题"><a href="#5-2-雪花算法与小文件问题" class="headerlink" title="5.2 雪花算法与小文件问题"></a>5.2 雪花算法与小文件问题</h3><p>雪花算法（Snowflake ID）是一种流行的分布式 ID 生成算法，通常会被业务使用作为 RocksDB 键的一部分。通常由以下部分组成：</p><pre><code class="hljs text">+----------------------+----------------+---------------+-----------+| 时间戳（41位）       | 机器ID（10位） | 序列号（12位）| 预留（1位）|+----------------------+----------------+---------------+-----------+</code></pre><p>其主要特性包括：</p><ol><li><strong>时间有序性</strong>：ID 中包含时间戳，使得生成的 ID 大体上按时间递增</li><li><strong>局部单调</strong>：在单台机器上，生成的 ID 严格单调递增</li><li><strong>可能存在间隙</strong>：时间跳跃或机器重启时会产生 ID 间隙</li><li><strong>不同机器 ID 区分</strong>：不同机器生成的 ID 在特定位上有差异</li></ol><p>在使用雪花算法作为 RocksDB 键时，会存在以上问题：</p><ol><li><strong>严格单调递增</strong>：单机生成的雪花 ID 严格递增，使键的分布缺乏随机性</li><li><strong>时间间隙触发分割</strong>：当系统在两个不连续的时间段写入数据（例如日间批处理、系统重启后继续写入），两段数据之间会有明显的键值间隙。当 compaction 处理到新的一批数据时，就会检测到 “ 跨越了祖父层文件边界 “ 的情况</li><li>**类似于 “ 两个不同数据集 “**：代码注释中特别提到的 “ 两个不同数据集无重叠 “ 的情况与单机雪花 ID 的两个时间段的数据极为相似</li><li><strong>频繁触发边界切分</strong>：由于满足了算法条件（跨越了足够多的边界且新增的重叠文件足够大），会比随机数据更频繁地触发文件切分</li></ol><h3 id="5-3-流量峰值特征与小文件问题"><a href="#5-3-流量峰值特征与小文件问题" class="headerlink" title="5.3 流量峰值特征与小文件问题"></a>5.3 流量峰值特征与小文件问题</h3><p>当业务有定期（例如：每小时触发的任务）的流量峰值时，会加重 “ 隔离可跳过的祖父文件 “ 启发式策略导致的小文件问题，特别是当使用雪花算法等单调递增 ID 作为键时。这种影响主要体现在以下几个方面：</p><ol><li><strong>时间间隙效应：</strong> 当业务每小时流量峰值会导致数据写入呈现明显的 “ 块状 “ 分布：<ul><li><p>峰值期间：大量数据密集写入</p></li><li><p>峰值之间：数据写入稀疏或几乎停止</p><p>写入模式会在键空间中产生规律性的 “ 高密度区 “ 和 “ 低密度区 “，特别是使用时间相关的键（如雪花 ID）时，数据在键空间分布上会呈现明显的 “ 台阶状 “。</p></li></ul></li><li><strong>边界切分频率增加</strong><ul><li>增加边界跨越次数：当 compaction 处理从一个流量峰值时段到另一个时段的数据时，由于键值间隙的存在，更容易达成 <code>num_grandparent_boundaries_crossed &gt;= num_skippable_boundaries_crossed</code> 条件</li><li>文件边界自然对齐：随着数据经历多次 compaction，祖父层文件的边界很可能会与这些流量峰值的时间边界自然对齐</li></ul></li><li><strong>周期性峰值的累积效应:</strong> 这种影响还会随着系统运行时间而累积：<ul><li>第一阶段：初始写入与 L0 形成。每小时峰值写入会在 L0 层形成多个文件，每个峰值期间的文件之间存在时间和键空间上的间隙。</li><li>第二阶段：初次下沉 compaction。当这些 L0 文件下沉到 L1 时，启发式算法可能检测到峰值间隙，并在这些位置切分文件，使 L1 层文件边界与峰值边界部分对齐。</li><li>第三阶段：多层级传播。随着数据继续下沉，L2、L3 等底层的文件边界会越来越精确地与这些周期性流量峰值的边界对齐，形成一种 “ 回声效应 “。</li></ul></li></ol><h3 id="5-4-验证小-SST-文件逻辑触发"><a href="#5-4-验证小-SST-文件逻辑触发" class="headerlink" title="5.4 验证小 SST 文件逻辑触发"></a>5.4 验证小 SST 文件逻辑触发</h3><p>使用 objdump 获取程序 <code>ShouldStopBefore</code> 汇编代码</p><pre><code class="hljs sh">$&gt; objdump -dC kv-datanode |grep ShouldStopBefore -A 500</code></pre><p>“ 隔离可跳过的祖父文件 “ 分支的汇编代码如下：</p><pre><code class="hljs asm">00000000009580d0 &lt;rocksdb::CompactionOutputs::ShouldStopBefore(rocksdb::CompactionIterator const&amp;)&gt;:  ...  // 如果标志为 true (假设满足 style 和 dynamic_size 条件):  // 计算并比较 overlap delta: grandparent_overlapped_bytes_ - previous_overlapped_bytes &gt; target_output_file_size() / 8  95820c:   48 8b 40 10             mov    0x10(%rax),%rax  ; 加载 this-&gt;target_output_file_size_ (偏移 0x10) 到 %rax。  958210:   48 2b 55 98             sub    -0x68(%rbp),%rdx ; %rdx = 当前 grandparent_overlapped_bytes_ (来自 9581c0) - 初始 grandparent_overlapped_bytes_ (来自栈 -0x68(%rbp))。即 overlap delta。  958214:   48 89 c7                mov    %rax,%rdi      ; %rdi = target_output_file_size_。  958217:   48 c1 ef 03             shr    $0x3,%rdi       ; %rdi = target_output_file_size_ / 8。  95821b:   48 39 fa                cmp    %rdi,%rdx      ; 比较 overlap delta (%rdx) 与 target_output_file_size_ / 8 (%rdi)。  95821e:   0f 87 98 00 00 00       ja     9582bc &lt;...&gt;   ; 如果 overlap delta &gt; target_output_file_size_ / 8，跳转到 9582bc (返回 true)。 **即，代码 326 行 &quot;隔离可跳过的祖父文件&quot;分支 的 `return true;`**  ...</code></pre><p>使用 bpftrace 工具在汇编指令前插入探测点</p><pre><code class="hljs c">#!/usr/bin/env bpftrace BEGIN &#123;    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;开始监控 ShouldStopBefore 函数的分支执行情况\n\n&quot;</span>);    @calls = <span class="hljs-number">0</span>;     <span class="hljs-comment">// 分支计数器初始化</span>    @branch_condition_checked = <span class="hljs-number">0</span>;  <span class="hljs-comment">// 到达比较指令的次数</span>    @branch_condition_true = <span class="hljs-number">0</span>;     <span class="hljs-comment">// 条件为true的次数</span>&#125; <span class="hljs-comment">// 函数入口点</span>uprobe:kv-datanode:<span class="hljs-number">0x9580d0</span>&#123;    @calls++;&#125; <span class="hljs-comment">// 监控 grandparent_overlapped 比较指令执行点</span>uprobe:kv-datanode:<span class="hljs-number">0x95821b</span>&#123;    @branch_condition_checked++;     $dx = reg(<span class="hljs-string">&quot;dx&quot;</span>);    $di = reg(<span class="hljs-string">&quot;di&quot;</span>);    <span class="hljs-keyword">if</span> ($dx &gt; $di) &#123;        @branch_condition_true++;    &#125;&#125; interval:s:<span class="hljs-number">5</span> &#123;    time(<span class="hljs-string">&quot;当前时间: %H:%M:%S\n&quot;</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;当前统计:\n&quot;</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;函数调用总次数: %d\n&quot;</span>, @calls);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;条件比较执行次数: %d\n&quot;</span>, @branch_condition_checked);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;条件比较为 True 次数: %d\n\n&quot;</span>, @branch_condition_true);&#125; END &#123;    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;\n最终统计:\n&quot;</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;函数调用总次数: %d\n&quot;</span>, @calls);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;条件比较执行次数: %d\n&quot;</span>, @branch_condition_checked);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">&quot;条件比较为 True 次数: %d\n\n&quot;</span>, @branch_condition_true);&#125;</code></pre><p>在相关机器上执行探测，结果如下</p><pre><code class="hljs sh">$&gt; bpftrace -p 379970 /data/scripts/datanode_probe.btAttaching 5 probes... 开始监控 ShouldStopBefore 函数的分支执行情况      ^C  最终统计:  函数调用总次数: 40242437  条件比较执行次数: 69  条件比较为 True 次数: 63    @branch_condition_checked: 69  @branch_condition_true: 63  @calls: 40242437</code></pre><h2 id="六、统计指标"><a href="#六、统计指标" class="headerlink" title="六、统计指标"></a>六、统计指标</h2><p><strong><code>rocksdb.live-sst-files-size</code></strong></p><p><strong>含义</strong>：返回当前列族中所有活跃 (live) 的 SST 文件的总大小（字节）。</p><pre><code class="hljs apache"><span class="hljs-attribute">rocksdb</span>.live-sst-files-size:<span class="hljs-number">0</span></code></pre><p><strong><code>rocksdb.num-files-at-level&lt;N&gt;</code></strong></p><p><strong>含义</strong>：返回当前列族指定层级 N 中 SST 文件的数量。</p><pre><code class="hljs livecodeserver">rocksdb.<span class="hljs-built_in">num</span>-<span class="hljs-built_in">files</span>-<span class="hljs-keyword">at</span>-level0:<span class="hljs-number">1</span>rocksdb.<span class="hljs-built_in">num</span>-<span class="hljs-built_in">files</span>-<span class="hljs-keyword">at</span>-level1:<span class="hljs-number">5</span>rocksdb.<span class="hljs-built_in">num</span>-<span class="hljs-built_in">files</span>-<span class="hljs-keyword">at</span>-level12:<span class="hljs-number">12</span>...</code></pre><p><strong><code>rocksdb.levelstats</code></strong></p><p><strong>含义</strong>：提供一个简洁的表格，显示每个层级的文件数量和总大小。</p><p><strong>输出格式</strong>：</p><pre><code class="hljs asciidoc"><span class="hljs-section">Level Files Size(MB)</span><span class="hljs-section">--------------------</span><span class="hljs-code">  0      1     0.01</span><span class="hljs-code">  1      5     2.31</span><span class="hljs-code">  2     12    10.22</span><span class="hljs-code">  ...</span></code></pre><h2 id="七、总结"><a href="#七、总结" class="headerlink" title="七、总结"></a>七、总结</h2><p>总结前文分析，RocksDB 在不同场景下生成的 SST 文件大小大致如下：</p><p><strong>1. L0 层（写入&#x2F;刷盘层）</strong></p><ul><li><p><strong>Memtable 刷盘生成</strong>：  </p><ul><li>文件大小 ≈ <code>write_buffer_size</code> × <code>min_write_buffer_number_to_merge</code>。例如，<code>write_buffer_size=64MB</code>，<code>min_write_buffer_number_to_merge=2</code>，则单个 L0 文件约为 128MB</li><li>实际大小可能略小（memtable 未满、压缩等因素）</li></ul></li><li><p><strong>Intra-L0 Compaction 生成</strong>：  </p><ul><li>文件大小最大不超过 <code>max_compaction_bytes</code>（如 1.6GB），通常远小于此值</li></ul></li></ul><p><strong>2. 非 L0、非底层（如 L1&#x2F;L2&#x2F;…&#x2F;Lmax-1）</strong></p><ul><li><strong>目标文件大小</strong>：  <ul><li>由 <code>target_file_size_base</code> 和 <code>target_file_size_multiplier</code> 计算</li><li>例如，L1: 64MB，L2: 128MB，L3: 256MB（假设 multiplier&#x3D;2）</li></ul></li><li><strong>实际文件大小</strong>：  <ul><li>动态文件大小启用时，最大可达目标大小的 2 倍（如 128MB、256MB、512MB 等）</li><li>受 compaction 启发式（如祖父层边界）影响，部分文件可能较小</li></ul></li></ul><p><strong>3. 最底层（Bottommost Level）</strong>  </p><ul><li>严格等于 <code>target_output_file_size_</code>（如 64MB、128MB、256MB 等）</li><li>不会超过目标大小，也不会因启发式切分而变小</li></ul><p>理解这些机制有助于我们更好地调整 RocksDB 的配置，使其在特定的工作负载下获得最佳性能，并在读性能、写放大、空间使用和管理开销之间找到适合自己应用场景的平衡点。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/05-04-2025/rocksdb-sst-file-size.html">https://www.cyningsun.com/05-04-2025/rocksdb-sst-file-size.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 RocksDB 中，SST（Sorted String Table）文件是持久化存储数据的基本单位。SST 文件的大小对 RocksDB 的性能有着深远影响：太小的文件会导致文件数量过多，增加元数据开销和文件打开&amp;#x2F;关闭的操作负担；太大的文件则可能导致读放大和更</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.cyningsun.com/category/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="RocksDB" scheme="https://www.cyningsun.com/tag/RocksDB/"/>
    
  </entry>
  
  <entry>
    <title>Kubernetes 下的文件、账号与权限</title>
    <link href="https://www.cyningsun.com/05-03-2025/files-accounts-and-permissions-under-kubernetes.html"/>
    <id>https://www.cyningsun.com/05-03-2025/files-accounts-and-permissions-under-kubernetes.html</id>
    <published>2025-05-02T16:00:00.000Z</published>
    <updated>2025-05-04T02:17:13.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>在容器化环境中，文件权限和用户管理常常会引发各种问题，例如：遇到 “Permission denied” 错误，却不知从何处着手解决。问题背后往往涉及容器文件系统、用户权限和挂载机制等细节。本文将从几个关键问题出发，系统地探讨容器中的文件、账号与权限管理，以更好地理解和解决这些问题。</p><ol><li>进程创建的目录、文件，默认权限是怎么指定的？</li><li>宿主机不存在的路径，是由谁来创建，权限是怎样的？</li><li>容器的挂载路径中不存在的目录是由谁来创建的，权限是怎样的？</li><li>虚拟文件系统是怎么实现挂载、挂载目录读写？</li><li>同一个文件系统 (目录) 挂载到不同的容器 (机器)，权限是怎么管理的？</li><li>root 启动的容器，进程启动用户一定是 root 么？</li><li>“Permission denied” 错误如何排查？</li></ol><p>本文从最基础的 Linux 文件权限系统开始，逐步深入分析容器中的权限机制。</p><h2 id="二、文件-x2F-目录创建掩码"><a href="#二、文件-x2F-目录创建掩码" class="headerlink" title="二、文件&#x2F;目录创建掩码"></a>二、文件&#x2F;目录创建掩码</h2><p>在 Linux 系统中，当进程创建新文件或目录时，它们的默认权限是由进程的 umask（用户文件创建模式掩码）决定的。这个机制在容器环境中同样适用，是理解权限问题的关键。</p><h3 id="2-1-umask-的作用"><a href="#2-1-umask-的作用" class="headerlink" title="2.1 umask 的作用"></a>2.1 umask 的作用</h3><p>umask 是一个三位或四位八进制数，每一位分别对应文件权限的用户（owner）、用户组（group）和其他人（others）。它定义了从基准权限中需要减去哪些权限位。</p><h3 id="2-2-权限计算机制"><a href="#2-2-权限计算机制" class="headerlink" title="2.2 权限计算机制"></a>2.2 权限计算机制</h3><p>当创建新文件或目录时：</p><ul><li>文件的基准权限是 0666（<code>rw-rw-rw-</code>）</li><li>目录的基准权限是 0777（<code>rwxrwxrwx</code>）</li><li>实际权限 &#x3D; 基准权限 - umask</li></ul><p>例如，如果 umask 是 022：</p><ul><li>新文件权限：0666 - 022 &#x3D; 0644（<code>rw-r--r--</code>）</li><li>新目录权限：0777 - 022 &#x3D; 0755（<code>rwxr-xr-x</code>）</li></ul><p>容器中可通过以下命令查看和设置 umask：</p><pre><code class="hljs sh"><span class="hljs-comment"># 查看当前 umask</span><span class="hljs-built_in">umask</span>0022<span class="hljs-comment"># 设置新的 umask</span><span class="hljs-built_in">umask</span> 0027</code></pre><p>设置 umask 为 0027 后，新创建的文件默认权限为 0640（<code>rw-r-----</code>），新创建的目录默认权限为 0750（<code>rwxr-x---</code>）。</p><p>以下是一个简单的验证实验：</p><pre><code class="hljs sh"><span class="hljs-comment"># 设置 umask 为 0022</span><span class="hljs-built_in">umask</span> 0022<span class="hljs-comment"># 创建文件和目录</span><span class="hljs-built_in">touch</span> test-file<span class="hljs-built_in">mkdir</span> test-dir<span class="hljs-comment"># 检查权限</span><span class="hljs-built_in">ls</span> -l test-file-rw-r--r-- 1 root root 0 Jun 14 10:15 test-file<span class="hljs-built_in">ls</span> -ld test-dirdrwxr-xr-x 2 root root 4096 Jun 14 10:15 test-dir<span class="hljs-comment"># 修改 umask 为 0027</span><span class="hljs-built_in">umask</span> 0027<span class="hljs-comment"># 创建文件和目录</span><span class="hljs-built_in">touch</span> test-file-2<span class="hljs-built_in">mkdir</span> test-dir-2<span class="hljs-comment"># 检查权限</span><span class="hljs-built_in">ls</span> -l test-file-2-rw-r----- 1 root root 0 Jun 14 10:16 test-file-2<span class="hljs-built_in">ls</span> -ld test-dir-2drwxr-x--- 2 root root 4096 Jun 14 10:16 test-dir-2</code></pre><p>理解 umask 机制是掌握容器环境中目录和文件权限设置的基础。下面将分析这一机制在宿主机和容器环境中的具体应用。</p><h2 id="三、宿主机目录权限"><a href="#三、宿主机目录权限" class="headerlink" title="三、宿主机目录权限"></a>三、宿主机目录权限</h2><p>在 Kubernetes 中，当需要将主机上的目录挂载到容器内时，常常会使用 HostPath 卷。如果该主机目录不存在，可以使用 HostPathType 设置为 DirectoryOrCreate 来自动创建该目录。这时，一个关键问题出现了：<strong>这个目录是由谁创建的？它的权限是什么？</strong></p><p>目录由节点上的 <strong>kubelet</strong> 组件创建。kubelet 是运行在每个节点上的 Kubernetes 代理，负责管理 Pod 和容器生命周期。如果目标目录不存在，kubelet 会在挂载卷时自动创建该目录。</p><p><strong>目录的权限</strong>：</p><ul><li><strong>所有者</strong>：目录的所有者为 <strong>kubelet 进程的运行用户</strong>（通常为 <code>root</code>，但取决于节点上的 kubelet 配置）。</li><li><strong>权限</strong>：默认权限为 <strong>0755</strong>（即 <code>drwxr-xr-x</code>），遵循 Linux 的默认目录权限规则。如果 kubelet 的配置被修改过，权限可能会变化，但通常情况下默认是 <code>0755</code>。</li></ul><p>以下是一个实际的例子：</p><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span><span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span><span class="hljs-attr">metadata:</span>  <span class="hljs-attr">name:</span> <span class="hljs-string">hostpath-pod</span><span class="hljs-attr">spec:</span>  <span class="hljs-attr">containers:</span>  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">mycontainer</span>    <span class="hljs-attr">image:</span> <span class="hljs-string">busybox:1.28</span>    <span class="hljs-attr">command:</span> [<span class="hljs-string">&quot;/bin/sh&quot;</span>]    <span class="hljs-attr">args:</span> [<span class="hljs-string">&quot;-c&quot;</span>, <span class="hljs-string">&quot;sleep 3600&quot;</span>]  <span class="hljs-comment"># 使容器保持运行</span>    <span class="hljs-attr">volumeMounts:</span>    <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/container/path</span>      <span class="hljs-attr">name:</span> <span class="hljs-string">host-volume</span>  <span class="hljs-attr">volumes:</span>  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">host-volume</span>    <span class="hljs-attr">hostPath:</span>      <span class="hljs-attr">path:</span> <span class="hljs-string">/data/log/hostpath-pod</span>      <span class="hljs-attr">type:</span> <span class="hljs-string">DirectoryOrCreate</span></code></pre><p>当该 Pod 被创建后，可以检查宿主机上自动创建的目录权限：</p><pre><code class="hljs sh"><span class="hljs-comment"># 查看 Pod 所在宿主机</span>kubectl get pod hostpath-pod -o wideNAME           READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATEShostpath-pod   1/1     Running   0          16m   192.168.1.100   worker-node-01   &lt;none&gt;           &lt;none&gt;<span class="hljs-comment"># 登录宿主机查看目录权限</span><span class="hljs-built_in">ls</span> -ld /data/log/hostpath-pod/drwxr-xr-x 2 root root 4096 Jun 13 10:06 /data/log/hostpath-pod/</code></pre><p>如上所示，自动创建的目录属于 root 用户和 root 组，权限为 0755，这与 umask 0022 结合基础权限 0777 的结果一致。</p><h2 id="四、容器挂载目录权限"><a href="#四、容器挂载目录权限" class="headerlink" title="四、容器挂载目录权限"></a>四、容器挂载目录权限</h2><p>类似地，在容器挂载过程中，如果容器内的挂载路径不存在，Kubelet 也会自动创建这个路径。这里的权限管理与宿主机上相似，同样受 umask 影响：</p><p><strong>容器内路径的创建</strong></p><ul><li><strong>自动创建逻辑</strong>：<br> 无论挂载的是 <strong>HostPath 卷</strong>、<strong>emptyDir 卷</strong>还是其他类型的卷，如果容器内的挂载路径（如 <code>/app/data</code>）不存在，Kubelet 会在容器启动前自动创建该路径。</li><li><strong>创建者身份</strong>：<br> 容器内的路径由 <strong>Kubelet 以容器运行时（如 Docker、containerd）的默认用户身份创建</strong>。默认情况下，容器运行时可能以 <code>root</code> 用户运行（除非显式配置了非 root 用户）。</li></ul><p>继续使用上面的例子，进入容器检查挂载目录的权限：</p><pre><code class="hljs sh"><span class="hljs-comment"># 进入容器</span>kubectl <span class="hljs-built_in">exec</span> -it hostpath-pod -- /bin/sh<span class="hljs-comment"># 检查挂载目录权限</span><span class="hljs-built_in">ls</span> -ld /container/pathdrwxr-xr-x    2 root     root          4096 Jun 13 02:06 /container/path</code></pre><p>在容器内，挂载目录同样属于 root 用户和 root 组，权限为 0755，这再次验证了 umask 机制在容器环境中的应用。</p><p>理解了基本的权限机制后，需要进一步分析更复杂的场景：当同一个文件系统被挂载到不同的容器时，权限是如何管理的？这涉及到虚拟文件系统的实现。</p><h2 id="五、虚拟文件系统-–-挂载机制"><a href="#五、虚拟文件系统-–-挂载机制" class="headerlink" title="五、虚拟文件系统 – 挂载机制"></a>五、虚拟文件系统 – 挂载机制</h2><p>要深入理解容器中的文件权限管理，必须了解底层的虚拟文件系统挂载原理。容器环境中的挂载实际依赖于 Linux VFS(虚拟文件系统) 层的实现。</p><p><img src="/images/files-accounts-and-permissions-under-kubernetes/Kubernetes%20%E4%B8%8B%E7%9A%84%E6%96%87%E4%BB%B6%E3%80%81%E8%B4%A6%E5%8F%B7%E4%B8%8E%E6%9D%83%E9%99%90-20250503232727-1.png" alt="Kubernetes 下的文件、账号与权限-20250503232727-1.png"></p><ul><li><strong>目录游走</strong>：目录游走是逐渐实例化该组件对应的 inode 和 dentry 的过程。在没有任何缓存的情况下，dentry 会先被初始化，在 dentry 中包含文件&#x2F;目录名字符串。在具体某一级目录中，会调用该目录 inode 的 lookup() 函数查找该目录中的对应子项（子目录或子文件），然后完成子项 dentry 和 inode 的初始化</li></ul><p><img src="/images/files-accounts-and-permissions-under-kubernetes/Kubernetes%20%E4%B8%8B%E7%9A%84%E6%96%87%E4%BB%B6%E3%80%81%E8%B4%A6%E5%8F%B7%E4%B8%8E%E6%9D%83%E9%99%90-20250503232727-2.png" alt="Kubernetes 下的文件、账号与权限-20250503232727-2.png"></p><ul><li><p><strong>挂载点初始化</strong>：涉及挂载的关键信息的初始化在挂载的时候就已经完成。即，为源目录添加挂载点标记，同时添加挂载信息（包括，源和目标文件系统的信息）到挂载点列表</p></li><li><p><strong>挂载点游走</strong>：在目录游走时，如果发现该目录标记为挂载点，则从挂载点列表寻找目标文件系统的信息，然后从目标文件系统继续往下遍历</p></li></ul><p>这种挂载机制是理解不同类型的卷在权限处理上存在差异的基础。当目录被挂载后，访问该目录的进程实际上会穿过挂载点，访问到目标文件系统上的内容，而权限检查则会基于目标文件系统上的权限设置。</p><p>有了这些挂载机制的基础知识，接下来可以进一步分析 Kubernetes 中如何管理挂载卷的权限。</p><h2 id="六、目录挂载权限管理"><a href="#六、目录挂载权限管理" class="headerlink" title="六、目录挂载权限管理"></a>六、目录挂载权限管理</h2><p>基于前面介绍的挂载机制，下面分析容器环境中如何管理挂载卷的权限。当文件系统挂载到容器中时，权限管理涉及几个核心机制：</p><h3 id="6-1-基本权限原理"><a href="#6-1-基本权限原理" class="headerlink" title="6.1 基本权限原理"></a>6.1 基本权限原理</h3><p>文件系统的权限体系在容器环境中仍然遵循 Linux 标准：</p><ul><li><strong>权限继承</strong>：挂载的文件和目录保留其原始的 UID&#x2F;GID 和权限位</li><li><strong>用户映射</strong>：容器内的进程根据其 UID&#x2F;GID 访问文件，若容器内不存在对应用户，则直接显示数字 ID</li></ul><pre><code class="hljs sh"><span class="hljs-comment"># 宿主机上以特定用户创建文件</span>cyningsun$&gt; <span class="hljs-built_in">touch</span> uid-gid.txt<span class="hljs-comment"># 查看文件归属用户、用户组</span>$&gt; <span class="hljs-built_in">ls</span> -l /data/cyningsun/k8s/dir/uid-gid.txt-rw-r--r-- 1 cyningsun dev 0 6月  13 11:29 /data/cyningsun/k8s/dir/uid-gid.txt<span class="hljs-comment"># 查看用户 UID、GID</span>$&gt; <span class="hljs-built_in">id</span> cyningsunuid=1002(cyningsun) gid=1001(dev) <span class="hljs-built_in">groups</span>=1001(dev),1002(dev_sudo)<span class="hljs-comment"># 查看文件归属 UID、GID</span>$&gt; <span class="hljs-built_in">ls</span> -<span class="hljs-built_in">ln</span> /data/cyningsun/k8s/dir/uid-gid.txt-rw-r--r-- 1 1002 1001 0 6月  13 11:29 /data/cyningsun/k8s/dir/uid-gid.txt</code></pre><h3 id="6-2-Pod-安全上下文"><a href="#6-2-Pod-安全上下文" class="headerlink" title="6.2 Pod 安全上下文"></a>6.2 Pod 安全上下文</h3><p>Kubernetes 通过 SecurityContext 控制 Pod 和容器的权限：</p><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span><span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span><span class="hljs-attr">metadata:</span>  <span class="hljs-attr">name:</span> <span class="hljs-string">security-context-pod</span><span class="hljs-attr">spec:</span>  <span class="hljs-attr">securityContext:</span>    <span class="hljs-attr">runAsUser:</span> <span class="hljs-number">1000</span>  <span class="hljs-comment"># 进程用户ID</span>    <span class="hljs-attr">runAsGroup:</span> <span class="hljs-number">2000</span> <span class="hljs-comment"># 进程组ID</span>    <span class="hljs-attr">fsGroup:</span> <span class="hljs-number">2000</span>    <span class="hljs-comment"># 文件系统组ID</span>  <span class="hljs-attr">containers:</span>  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">busybox-container</span>    <span class="hljs-attr">image:</span> <span class="hljs-string">busybox:1.28</span>    <span class="hljs-attr">command:</span> [<span class="hljs-string">&quot;/bin/sh&quot;</span>]    <span class="hljs-attr">args:</span> [<span class="hljs-string">&quot;-c&quot;</span>, <span class="hljs-string">&quot;sleep 3600&quot;</span>]  <span class="hljs-comment"># 使容器保持运行</span>    <span class="hljs-attr">volumeMounts:</span>    <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/container/path</span>      <span class="hljs-attr">name:</span> <span class="hljs-string">host-volume</span>  <span class="hljs-attr">volumes:</span>  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">host-volume</span>    <span class="hljs-attr">hostPath:</span>      <span class="hljs-attr">path:</span> <span class="hljs-string">/data/log/security-context-pod</span></code></pre><p><strong>关键参数作用</strong>：</p><ul><li><code>runAsUser/runAsGroup</code>: 控制容器进程的用户&#x2F;组身份</li><li><code>fsGroup</code>: 控制挂载卷的组权限，影响现有文件和新创建文件的组所有权</li></ul><h3 id="6-3-不同卷类型的权限特性"><a href="#6-3-不同卷类型的权限特性" class="headerlink" title="6.3 不同卷类型的权限特性"></a>6.3 不同卷类型的权限特性</h3><p>Kubernetes 中不同卷类型对 <code>fsGroup</code> 的处理机制存在显著差异，这主要与它们的存储实现原理、安全模型和权限管理方式有关：</p><ol><li><p><strong>持久卷（PersistentVolume，如 AWS EBS、Azure Disk、NFS 等）</strong>：</p><ul><li><strong>支持 <code>fsGroup</code></strong>: 对大多数类型的 PV，<code>fsGroup</code> 设置<strong>生效</strong></li><li><strong>原因</strong>：<ul><li><strong>存储驱动支持</strong>：持久卷通常由云服务商或分布式存储系统提供，其存储驱动支持 Kubernetes 的所有权和权限动态修改</li><li><strong>多租户隔离需求</strong>：PV 是集群级别的资源，设计上需要支持多租户场景，确保不同用户或 Pod 挂载同一卷时能通过 <code>fsGroup</code> 自动隔离权限</li></ul></li><li><strong>生效机制</strong>：<ul><li>挂载时自动递归修改卷内所有文件和目录的组所有权为 <code>fsGroup</code> 值</li><li>确保组权限生效，例如设置目录的 setgid 位，使新创建的文件继承父目录的组所有权</li><li>若同一 PV 被多个 Pod 挂载，后挂载的 Pod 的 <code>fsGroup</code> 会覆盖之前的设置</li></ul></li><li><strong>持久性</strong>：<code>fsGroup</code> 的修改是持久的，即使 Pod 删除后也会保留</li></ul></li><li><p><strong>临时卷（emptyDir）</strong>：</p><ul><li><strong>支持 <code>fsGroup</code></strong>: <code>fsGroup</code> 对 emptyDir 卷<strong>生效</strong></li><li><strong>原因</strong>：<ul><li>emptyDir 卷是 Pod 级别的临时存储，完全受 Kubernetes 控制</li><li>每次 Pod 创建时都会新建，无需考虑多租户权限冲突</li></ul></li><li><strong>生效机制</strong>：<ul><li>卷创建时自动应用 <code>fsGroup</code> 设置</li><li>确保 Pod 中的所有容器都能通过组权限访问卷内文件</li></ul></li><li><strong>临时性</strong>：Pod 删除时卷内容被清除，权限问题不会持续存在</li></ul></li><li><p><strong>hostPath 卷</strong>：</p><ul><li><strong>不支持 <code>fsGroup</code></strong>: <code>fsGroup</code> 设置对 hostPath 卷<strong>不生效</strong></li><li><strong>原因</strong>：<ul><li><strong>直接绑定宿主机文件系统</strong>：hostPath 卷直接挂载宿主机上的目录或文件，权限完全依赖宿主机现有设置</li><li><strong>安全限制</strong>：Kubernetes 设计上避免自动修改宿主机文件系统权限，防止因权限篡改引发安全风险</li><li><strong>存储驱动不支持</strong>：hostPath 卷的实现不包含动态修改文件组所有权的逻辑</li></ul></li><li><strong>权限行为</strong>：<ul><li>挂载后，容器内访问的目录权限与宿主机目录完全一致，不会触发任何所有权或权限修改</li><li>如需修改宿主机上的文件权限，需要通过初始化容器或其他机制手动设置</li></ul></li><li><strong>权限协调</strong>：若容器进程用户需要访问 hostPath 目录，必须手动设置宿主机目录的权限</li></ul></li></ol><h3 id="6-4-新文件创建的权限规则"><a href="#6-4-新文件创建的权限规则" class="headerlink" title="6.4 新文件创建的权限规则"></a>6.4 新文件创建的权限规则</h3><p>当 Pod 内进程创建新文件时，权限规则根据卷类型有所不同：</p><h4 id="6-4-1-emptyDir-和持久卷-PV-的新文件"><a href="#6-4-1-emptyDir-和持久卷-PV-的新文件" class="headerlink" title="6.4.1 emptyDir 和持久卷 (PV) 的新文件"></a>6.4.1 emptyDir 和持久卷 (PV) 的新文件</h4><p>当进程在 emptyDir 卷或支持 fsGroup 的持久卷上创建新文件时：</p><ol><li><strong>文件所有者</strong>：为创建进程的 UID（受 <code>runAsUser</code> 影响）</li><li><strong>文件组</strong>：为 Pod 的 <code>fsGroup</code> 值，不受进程主组影响</li><li><strong>权限位</strong>：由基准权限减去 umask 值计算得出<ul><li>文件基准权限：0666 (<code>rw-rw-rw-</code>)</li><li>目录基准权限：0777 (<code>rwxrwxrwx</code>)</li><li>常见 umask 为 0022，则新文件权限为 0644 (<code>rw-r--r--</code>)</li></ul></li></ol><h4 id="6-4-2-hostPath-卷的新文件"><a href="#6-4-2-hostPath-卷的新文件" class="headerlink" title="6.4.2 hostPath 卷的新文件"></a>6.4.2 hostPath 卷的新文件</h4><p>当进程在 hostPath 卷上创建新文件时：</p><ol><li><strong>文件所有者</strong>：为创建进程的 UID（受 <code>runAsUser</code> 影响）</li><li><strong>文件组</strong>：为创建进程的主组 ID（受 <code>runAsGroup</code> 影响），<strong>不会</strong>应用 Pod 的 <code>fsGroup</code> 值</li><li><strong>权限位</strong>：同样受 umask 影响，但权限最终保存在宿主机文件系统上</li></ol><p>这种差异解释了为什么在使用 hostPath 卷时，即使设置了 <code>fsGroup</code>，新创建的文件组所有权也不会按预期设置。</p><p>通过这种机制，Kubernetes 在支持 fsGroup 的卷类型上确保了跨容器的文件权限一致性，同时对 hostPath 卷保持了安全限制。</p><h2 id="七、容器启动-–-进程账号"><a href="#七、容器启动-–-进程账号" class="headerlink" title="七、容器启动 – 进程账号"></a>七、容器启动 – 进程账号</h2><p>在容器环境中，一个重要问题是：当以 root 用户启动容器时，容器内的进程是否也一定以 root 用户运行？答案并不总是肯定的。</p><p>在很多容器镜像中，即使以 root 用户启动容器，实际运行的应用进程可能会自动降级到非 root 用户。这是一种安全最佳实践，因为以 root 用户运行容器会有以下风险：</p><ul><li>进程拥有容器内的全部权限</li><li>如果有数据卷映射到宿主机，容器内的 root 用户可能会影响宿主机文件</li></ul><p>这种权限降级通常通过 docker-entrypoint.sh 脚本实现。以下是一个简化的示例：</p><pre><code class="hljs bash"><span class="hljs-meta">#!/bin/sh</span><span class="hljs-built_in">set</span> -e<span class="hljs-comment"># 如果是以 root 用户运行 redis-server 命令</span><span class="hljs-keyword">if</span> [ <span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span> = <span class="hljs-string">&#x27;redis-server&#x27;</span> -a <span class="hljs-string">&quot;<span class="hljs-subst">$(id -u)</span>&quot;</span> = <span class="hljs-string">&#x27;0&#x27;</span> ]; <span class="hljs-keyword">then</span>    <span class="hljs-comment"># 将当前目录下所有非 redis 用户拥有的文件改为 redis 用户所有</span>    find . \! -user redis -<span class="hljs-built_in">exec</span> <span class="hljs-built_in">chown</span> redis <span class="hljs-string">&#x27;&#123;&#125;&#x27;</span> +    <span class="hljs-comment"># 使用 gosu 切换到 redis 用户运行命令</span>    <span class="hljs-built_in">exec</span> gosu redis <span class="hljs-string">&quot;<span class="hljs-variable">$0</span>&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$@</span>&quot;</span><span class="hljs-keyword">fi</span><span class="hljs-comment"># 执行传入的命令</span><span class="hljs-built_in">exec</span> <span class="hljs-string">&quot;<span class="hljs-variable">$@</span>&quot;</span></code></pre><p>在这个例子中，如果容器以 root 用户启动并执行 redis-server 命令，entrypoint 脚本会自动将进程降级为 redis 用户运行。这是许多官方容器镜像的常见做法。</p><p>通过以下命令可验证实际运行的进程用户：</p><pre><code class="hljs sh"><span class="hljs-comment"># 启动容器</span>docker run --name redis-test -d redis<span class="hljs-comment"># 查看容器内进程</span>docker <span class="hljs-built_in">exec</span> redis-test ps auxUSER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMANDredis        1  0.2  0.1  55344 15080 ?        Ssl  10:20   0:00 redis-server *:6379</code></pre><p>可以看到，尽管容器是以 root 用户启动的，但实际运行 redis-server 的进程是以 redis 用户运行的。</p><h2 id="八、”Permission-denied”-错误排查"><a href="#八、”Permission-denied”-错误排查" class="headerlink" title="八、”Permission denied” 错误排查"></a>八、”Permission denied” 错误排查</h2><p>通过前面分析的 umask 机制、挂载原理和权限管理知识，可以系统性地排查容器中常见的 “Permission denied” 错误。这些错误通常源于文件系统权限、进程用户身份和挂载卷类型之间的不匹配。当遇到 “Permission denied” 错误时，可按照以下步骤进行系统排查：</p><h3 id="8-1-确定容器启动账号"><a href="#8-1-确定容器启动账号" class="headerlink" title="8.1 确定容器启动账号"></a>8.1 确定容器启动账号</h3><p>首先查看 POD&#x2F;容器 的配置，确定启动账号：</p><pre><code class="hljs sh"><span class="hljs-comment"># 查看 Pod 的 SecurityContext</span>kubectl get pod security-context-pod -o jsonpath=<span class="hljs-string">&#x27;&#123;.spec.securityContext&#125;&#x27;</span>&#123;<span class="hljs-string">&quot;fsGroup&quot;</span>:2000,<span class="hljs-string">&quot;runAsGroup&quot;</span>:2000,<span class="hljs-string">&quot;runAsUser&quot;</span>:1000&#125;</code></pre><p>如果这些设置为空，则默认使用 root 用户（UID 0）。</p><h3 id="8-2-确定进程启动账号"><a href="#8-2-确定进程启动账号" class="headerlink" title="8.2 确定进程启动账号"></a>8.2 确定进程启动账号</h3><p>接下来，判断实际运行进程的用户：</p><pre><code class="hljs sh"><span class="hljs-comment"># 查看容器启动命令</span>kubectl get pod security-context-pod -o jsonpath=<span class="hljs-string">&#x27;&#123;.spec.containers[*].command&#125;&#x27;</span>[<span class="hljs-string">&quot;/bin/sh&quot;</span>]<span class="hljs-comment"># 如果启动命令为空，可能使用镜像的默认 ENTRYPOINT</span>kubectl get pod dts-controller -n dts -o jsonpath=<span class="hljs-string">&#x27;&#123;.spec.containers[*].command&#125;&#x27;</span></code></pre><p>如果容器使用的是镜像默认的 ENTRYPOINT，需要检查 entrypoint 脚本：</p><pre><code class="hljs sh"><span class="hljs-comment"># 查看镜像的 Entrypoint</span>docker inspect redis:latest -f <span class="hljs-string">&#x27;&#123;&#123;.Config.Entrypoint&#125;&#125;&#x27;</span>[docker-entrypoint.sh]<span class="hljs-comment"># 检查 entrypoint 脚本内容</span>docker run --<span class="hljs-built_in">rm</span> redis:latest <span class="hljs-built_in">cat</span> /usr/local/bin/docker-entrypoint.sh</code></pre><h3 id="8-3-检查目录和文件权限"><a href="#8-3-检查目录和文件权限" class="headerlink" title="8.3 检查目录和文件权限"></a>8.3 检查目录和文件权限</h3><p>创建临时容器，以 root 用户检查相关目录和文件的权限：</p><pre><code class="hljs sh"><span class="hljs-comment"># 启动临时容器，挂载相关目录</span>docker run --<span class="hljs-built_in">rm</span> -it --user root -v /path/to/problem/dir:/inspect alpine sh<span class="hljs-comment"># 在容器内检查文件权限</span><span class="hljs-built_in">ls</span> -la /inspect</code></pre><h3 id="8-4-验证权限匹配"><a href="#8-4-验证权限匹配" class="headerlink" title="8.4 验证权限匹配"></a>8.4 验证权限匹配</h3><p>最后，确认进程用户是否有权限访问所需的目录和文件：</p><pre><code class="hljs sh"><span class="hljs-comment"># 检查进程用户</span>ps aux | grep [process_name]<span class="hljs-comment"># 确认文件权限是否匹配</span><span class="hljs-built_in">ls</span> -la /path/to/file</code></pre><p>通过这四个步骤，大多数权限问题都能被准确定位和解决。下面通过一个实际案例来展示这些概念的应用。</p><h2 id="九、案例分析：Redis-Dockerfile"><a href="#九、案例分析：Redis-Dockerfile" class="headerlink" title="九、案例分析：Redis Dockerfile"></a>九、案例分析：Redis Dockerfile</h2><p>Redis 的官方 Dockerfile 是一个很好的案例，展示了如何在容器环境中正确处理文件权限和用户管理。下面分析 Redis Dockerfile 的关键部分：</p><h3 id="9-1-用户创建"><a href="#9-1-用户创建" class="headerlink" title="9.1 用户创建"></a>9.1 用户创建</h3><p>Redis 的 Alpine 版本 Dockerfile 中创建了专用的系统用户：</p><pre><code class="hljs dockerfile"><span class="hljs-comment"># add our user and group first to make sure their IDs get assigned consistently</span><span class="hljs-keyword">RUN</span><span class="language-bash"> <span class="hljs-built_in">set</span> -eux; \</span><span class="language-bash"><span class="hljs-comment"># alpine already has a gid 999, so we&#x27;ll use the next id</span></span>    addgroup -S -g <span class="hljs-number">1000</span> redis; \    adduser -S -G redis -u <span class="hljs-number">999</span> redis</code></pre><p>这段代码创建了一个系统用户 redis（UID 999）和一个系统组 redis（GID 1000），确保了 Redis 进程将使用固定 UID&#x2F;GID 的非 root 用户运行，提高了安全性。</p><h3 id="9-2-权限降级工具安装"><a href="#9-2-权限降级工具安装" class="headerlink" title="9.2 权限降级工具安装"></a>9.2 权限降级工具安装</h3><p>Redis 镜像安装 gosu 工具实现权限降级：</p><pre><code class="hljs dockerfile"><span class="hljs-comment"># grab gosu for easy step-down from root</span><span class="hljs-keyword">ENV</span> GOSU_VERSION <span class="hljs-number">1.17</span><span class="hljs-keyword">RUN</span><span class="language-bash"> <span class="hljs-built_in">set</span> -eux; \</span><span class="language-bash">    apk add --no-cache --virtual .gosu-fetch gnupg; \</span><span class="language-bash">    <span class="hljs-built_in">arch</span>=<span class="hljs-string">&quot;<span class="hljs-subst">$(apk --print-arch)</span>&quot;</span>; \</span><span class="language-bash">    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;<span class="hljs-variable">$arch</span>&quot;</span> <span class="hljs-keyword">in</span> \</span><span class="language-bash">        <span class="hljs-string">&#x27;x86_64&#x27;</span>) url=<span class="hljs-string">&#x27;https://github.com/tianon/gosu/releases/download/1.17/gosu-amd64&#x27;</span>; sha256=<span class="hljs-string">&#x27;bbc4136d03ab138b1ad66fa4fc051bafc6cc7ffae632b069a53657279a450de3&#x27;</span> ;; \</span><span class="language-bash">        <span class="hljs-string">&#x27;aarch64&#x27;</span>) url=<span class="hljs-string">&#x27;https://github.com/tianon/gosu/releases/download/1.17/gosu-arm64&#x27;</span>; sha256=<span class="hljs-string">&#x27;c3805a85d17f4454c23d7059bcb97e1ec1af272b90126e79ed002342de08389b&#x27;</span> ;; \</span><span class="language-bash">        <span class="hljs-comment"># ... 其他架构 ...</span></span>    esac; \    <span class="hljs-comment"># ... 下载和验证 gosu ...</span>    chmod +x /usr/local/bin/gosu; \    gosu --version; \    gosu nobody true</code></pre><p>这确保了容器可以安全地从 root 用户降级到非 root 用户。</p><h3 id="9-3-权限降级实现"><a href="#9-3-权限降级实现" class="headerlink" title="9.3 权限降级实现"></a>9.3 权限降级实现</h3><p>在 docker-entrypoint.sh 中实现实际的用户切换：</p><pre><code class="hljs bash"><span class="hljs-comment"># allow the container to be started with `--user`</span><span class="hljs-keyword">if</span> [ <span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span> = <span class="hljs-string">&#x27;redis-server&#x27;</span> -a <span class="hljs-string">&quot;<span class="hljs-subst">$(id -u)</span>&quot;</span> = <span class="hljs-string">&#x27;0&#x27;</span> ]; <span class="hljs-keyword">then</span>    find . \! -user redis -<span class="hljs-built_in">exec</span> <span class="hljs-built_in">chown</span> redis <span class="hljs-string">&#x27;&#123;&#125;&#x27;</span> +    <span class="hljs-built_in">exec</span> gosu redis <span class="hljs-string">&quot;<span class="hljs-variable">$0</span>&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$@</span>&quot;</span><span class="hljs-keyword">fi</span></code></pre><p>这段代码在以 root 用户启动容器时，会自动将当前目录下的文件所有权调整为 redis 用户，然后切换到 redis 用户继续执行。这正是前面讨论的 “ 容器启动 – 进程账号 “ 部分的实际应用。</p><h3 id="9-4-数据目录权限管理"><a href="#9-4-数据目录权限管理" class="headerlink" title="9.4 数据目录权限管理"></a>9.4 数据目录权限管理</h3><p>Redis 为数据目录设置了正确的权限：</p><pre><code class="hljs dockerfile"><span class="hljs-keyword">RUN</span><span class="language-bash"> <span class="hljs-built_in">mkdir</span> /data &amp;&amp; <span class="hljs-built_in">chown</span> redis:redis /data</span><span class="hljs-keyword">VOLUME</span><span class="language-bash"> /data</span><span class="hljs-keyword">WORKDIR</span><span class="language-bash"> /data</span></code></pre><p>这确保了数据目录完全归属于 redis 用户，避免了权限问题。这也呼应了 “ 容器挂载目录权限 “ 部分的讨论。</p><h3 id="9-5-umask-设置"><a href="#9-5-umask-设置" class="headerlink" title="9.5 umask 设置"></a>9.5 umask 设置</h3><p>在 docker-entrypoint.sh 中，Redis 还实现了精细的 umask 控制：</p><pre><code class="hljs bash"><span class="hljs-comment"># set an appropriate umask (if one isn&#x27;t set already)</span><span class="hljs-comment"># - https://github.com/docker-library/redis/issues/305</span><span class="hljs-comment"># - https://github.com/redis/redis/blob/bb875603fb7ff3f9d19aad906bd45d7db98d9a39/utils/systemd-redis_server.service#L37</span>um=<span class="hljs-string">&quot;<span class="hljs-subst">$(umask)</span>&quot;</span><span class="hljs-keyword">if</span> [ <span class="hljs-string">&quot;<span class="hljs-variable">$um</span>&quot;</span> = <span class="hljs-string">&#x27;0022&#x27;</span> ]; <span class="hljs-keyword">then</span>    <span class="hljs-built_in">umask</span> 0077<span class="hljs-keyword">fi</span></code></pre><p>这将默认的 umask 从 0022 改为 0077，确保新创建的文件只对所有者开放权限，大大提高了安全性。这与 “ 文件&#x2F;目录创建掩码 “ 部分讨论的内容完全吻合。</p><p>通过实际操作可验证 Redis 镜像的这些特性：</p><pre><code class="hljs sh"><span class="hljs-comment"># 启动 Redis 容器</span>docker run --name redis-test -d redis<span class="hljs-comment"># 查看容器内进程</span>docker <span class="hljs-built_in">exec</span> redis-test ps auxUSER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMANDredis        1  0.2  0.1  55344 15080 ?        Ssl  10:20   0:00 redis-server *:6379<span class="hljs-comment"># 查看数据目录权限</span>docker <span class="hljs-built_in">exec</span> redis-test <span class="hljs-built_in">ls</span> -ld /datadrwxr-xr-x 1 redis redis 4096 Jun 14 10:30 /data<span class="hljs-comment"># 在容器内创建文件，检查权限</span>docker <span class="hljs-built_in">exec</span> -it redis-test sh -c <span class="hljs-string">&quot;cd /data &amp;&amp; touch test-file &amp;&amp; ls -l test-file&quot;</span>-rw------- 1 redis redis 0 Jun 14 10:31 test-file</code></pre><p>可以看到，Redis 容器成功地实现了:</p><ol><li>以非 root 用户运行服务</li><li>正确设置数据目录权限</li><li>使用安全的 umask 值（0077）</li></ol><h2 id="十、总结"><a href="#十、总结" class="headerlink" title="十、总结"></a>十、总结</h2><p>Kubernetes 生态中的文件账号与权限管理涉及多层次的技术细节，从基础的 Linux 权限机制到容器特有的挂载与隔离特性。正确理解 umask 机制、挂载原理和安全上下文设置，是解决权限问题的关键。在实践中，遵循最小权限原则，采用非 root 用户运行容器进程，并为不同场景选择合适的存储卷类型，能有效构建安全稳定的容器化应用，避免常见的 “Permission denied” 等问题。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/05-03-2025/files-accounts-and-permissions-under-kubernetes.html">https://www.cyningsun.com/05-03-2025/files-accounts-and-permissions-under-kubernetes.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;一、背景&quot;&gt;&lt;a href=&quot;#一、背景&quot; class=&quot;headerlink&quot; title=&quot;一、背景&quot;&gt;&lt;/a&gt;一、背景&lt;/h2&gt;&lt;p&gt;在容器化环境中，文件权限和用户管理常常会引发各种问题，例如：遇到 “Permission denied” 错误，却不知从何处</summary>
      
    
    
    
    <category term="Kubernetes" scheme="https://www.cyningsun.com/category/Kubernetes/"/>
    
    
    <category term="Permission" scheme="https://www.cyningsun.com/tag/Permission/"/>
    
  </entry>
  
  <entry>
    <title>Flame Graph 机制小结</title>
    <link href="https://www.cyningsun.com/04-13-2025/flamegraph-summary.html"/>
    <id>https://www.cyningsun.com/04-13-2025/flamegraph-summary.html</id>
    <published>2025-04-12T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="什么是火焰图？"><a href="#什么是火焰图？" class="headerlink" title="什么是火焰图？"></a>什么是火焰图？</h3><p>2011 年，时任 Netflix 高级性能工程师的 Brendan Gregg 面临一个棘手问题：尽管 <code>perf</code> 能采集到海量性能数据，但使用 <code>perf report</code> 显示调用树摘要时，数千行堆栈信息让人如同“大海捞针”，难以发现关联路径和 CPU 热点。在  Roch Bourbonnais 的 CallStackAnalyzer 和 Jan Boerhout 的 vftrace 启发下，火焰图诞生了</p><p>火焰图（Flame Graph）是一种<strong>可视化的性能分析工具</strong>，其核心目标是将复杂的性能采样数据转化为<strong>一目了然的图形</strong>。通过横向宽度表示资源消耗（如 CPU 占用时间），纵向层级表示函数调用关系，形似跳动的火焰，让开发者能够快速锁定性能瓶颈的“火源”。</p><h3 id="经典火焰图原理"><a href="#经典火焰图原理" class="headerlink" title="经典火焰图原理"></a><a href="https://youtu.be/D53T1Ejig1Q?t=390">经典火焰图原理</a></h3><p>通常意义上的 On-CPU 火焰图是指 <code>On-CPU</code> 火焰图用来定位代码 <code>On-CPU</code> 的执行热点</p><p><strong>1. 数据采集</strong></p><ul><li><strong>采样机制：</strong> 以固定频率（如每秒 99 次）中断程序，记录当前的函数调用链（Stack Trace）</li></ul><p><strong>2. 数据处理</strong></p><ul><li><strong>聚合统计</strong>：合并相同调用链的采样点，计算每个函数在调用链中的出现频率</li><li><strong>归一化处理</strong>：将采样次数转换为百分比，消除采样时长对宽度的影响</li></ul><p><strong>3. 可视化规则</strong></p><ul><li><strong>方框</strong>：每个框代表函数栈中的一个函数（一个“栈帧”）。方框的宽度显示该函数 on-CPU 的<strong>总</strong>时间，或部分祖先函数 on-CPU 的<strong>总</strong>时间（基于样本计数）。带有宽方框的函数每次执行可能比带有窄方框的函数消耗更多 CPU，或者可能只是调用频率更高。</li><li><strong>Y 轴：</strong> 表示栈深度（栈上的帧数）。顶部的方框显示当前处于 CPU 运行状态的函数。函数下方的第一个函数是其父函数，下方的所有函数均为其祖先函数</li><li><strong>X 轴：</strong> 涵盖整体样本。从左到右按字母顺序排列，以最大化合并帧（从左到右并非显示时间的流逝）</li></ul><h3 id="Off-CPU-火焰图原理"><a href="#Off-CPU-火焰图原理" class="headerlink" title="Off-CPU 火焰图原理"></a>Off-CPU 火焰图原理</h3><p>经典的 CPU 火焰图虽然能精准定位代码在 CPU 上的执行热点，但现实中线程可能因 I&#x2F;O 阻塞、锁竞争、内存争用等原因离开 CPU，这些等待时间占比较高但传统火焰图无法捕捉；就催生了 <strong>Off-CPU 火焰图</strong>，目标是处于阻塞状态和 <code>Off-CPU</code> 状态的线程，如下图中蓝色部分所示。<code>Off-CPU</code> 分析是对 CPU 分析的补充，因此可以了解 100% 的线程时间。</p><p><img src="/images/flamegraph-summary/Flame%20Graph%20%E6%9C%BA%E5%88%B6%E5%B0%8F%E7%BB%93-20250413102927-1.png" alt="Flame Graph 机制小结-20250413102927-1.png"></p><p><strong>1. 数据收集：</strong> 通过内核级工具（如 <code>offcputime</code> from BCC）记录线程的 <strong>上下文切换（context switch）</strong> 事件</p><ul><li><strong>Off-CPU 开始</strong>：当线程被调度出 CPU（如调用 <code>schedule()</code> 函数）时，记录时间戳和调用栈</li><li><strong>On-CPU 恢复</strong>：当线程重新被调度到 CPU 时，计算阻塞时长（<code>恢复时间戳 - 离开时间戳</code>）</li><li><strong>阻塞类型：</strong> 结合阻塞事件的内核态信息（如系统调用、锁类型、I&#x2F;O 类型）</li><li><strong>调用栈：</strong> 用户态 + 内核态</li></ul><p><strong>2. 数据聚合：</strong> 按调用栈路径合并相同栈的阻塞时间，生成 <code>[调用栈] -&gt; 总耗时</code> 的映射表</p><ul><li><strong>时间累加</strong>：将同一调用栈路径的所有阻塞时间累加，形成时间占比。</li></ul><p><strong>3. 可视化规则：</strong> 将调用栈按层级展开，生成火焰图</p><ul><li><strong>宽度</strong>：表示阻塞时间的占比</li><li><strong>颜色</strong>：可区分阻塞类型（如红色为 I&#x2F;O，蓝色为锁）</li><li><strong>层级</strong>：显示从顶层函数到底层系统调用的完整路径</li></ul><blockquote><p><strong>注意：</strong></p><p><strong>数据收集开销</strong></p><ul><li>调度程序事件可能非常频繁——在极端情况下，每秒可能会有数百万个事件——由于事件发生频率高，数据开销可能会累积起来变得非常可观，比仅在 CPU 数量上进行 CPU 采样的开销要高出几个数量级。</li><li>如果对新的调度跟踪器一无所知，可以先收集十分之一秒（0.1 秒），然后逐步增加跟踪时间，同时密切关注其对系统 CPU 利用率、应用程序请求率和应用程序延迟的影响。同时考虑上下文切换的速率（例如，通过 vmstat 中的“cs”列测量），并且在速率更高的服务器上要更加小心</li></ul><p><strong>阻塞唤醒</strong></p><ul><li>许多 Off-CPU 堆栈显示了阻塞路径，但没有显示阻塞的完整原因。该原因和代码路径位于另一个线程，即调用唤醒阻塞线程的线程</li><li>另外的工具 wakeuptime 和 offwaketime，可以测量唤醒堆栈并将它们与 off-CPU 堆栈关联起来</li></ul></blockquote><h3 id="Broken-stack"><a href="#Broken-stack" class="headerlink" title="Broken stack"></a>Broken stack</h3><p>火焰图的数据采集步骤，一般会使用 <a href="https://perf.wiki.kernel.org/index.php/Main_Page">perf</a> Linux 分析器。该工具的使用工作流详见：<a href="https://www.brendangregg.com/Slides/KernelRecipes_Perf_Events.pdf">slides</a>、<a href="https://www.youtube.com/watch?v=UVM3WX8Lq2k">youtube</a>，不重复。着重记录：如何处理函数栈不完整。由于省略帧指针 (Omitting frame pointer) 通常是编译器优化的默认选项，就导致 perf_events 中的函数栈不完整。有三种方法可以解决这个问题：使用 dwarf 数据展开堆栈，使用最后分支记录 (LBR，如果可用，处理器特性），或者返回帧指针。</p><p><strong>Frame Pointers  帧指针</strong></p><p>应用程序使用编译器优化 (-O2) 会省略了帧指针，可以使用 <strong>-fno-omit-frame-pointer</strong> 重新编译。内核堆栈跟踪不完整，需要调整内核配置选项 <strong>CONFIG_FRAME_POINTER&#x3D;y</strong>。该方法不适合已经有问题的线上环境，调整选项的成本过高。</p><p><strong>Dwarf</strong></p><p>从 3.9 内核开始，perf_events 支持一种解决用户级堆栈中缺少帧指针的解决方法：libunwind，它使用 dwarf 函数。可以使用“–call-graph dwarf”（或“-g dwarf”）启用此功能</p><pre><code class="hljs sh">perf record -F 99 -p 59715 --call-graph dwarf -- <span class="hljs-built_in">sleep</span> 120</code></pre><p><strong>LBR</strong></p><p>必须拥有“最后分支记录”访问权限才能使用此功能。该权限在大多数云环境中均处于禁用状态，您会收到以下错误：</p><pre><code class="hljs sh"><span class="hljs-comment"># perf record -F 99 -a --call-graph lbr</span>Error:PMU Hardware doesn<span class="hljs-string">&#x27;t support sampling/overflow-interrupts.</span></code></pre><p>另外，LBR 的堆栈深度通常有限（8、16 或 32 帧），因此不适合用于深层堆栈或火焰图生成，因为火焰图需要走到公共根节点进行合并。</p><p><strong>容器环境</strong></p><p>容器化部署的场景下，如果容器是 Alpine，而宿主机是 ubuntu。首先在宿主机上对容器内的进程执行 perf record，然后在宿主机执行 perf script，也会因为容器与宿主机的 <strong>用户态符号环境不兼容</strong>导致函数栈异常。可以进入容器环境，然后指定宿主机的内核符号表路径 (应该有更好的处理方案？)</p><pre><code class="hljs sh">perf script --header -i perf.data --kallsyms /proc/kallsyms --no-inline &gt; perf.perf</code></pre><h3 id="火焰图的局限"><a href="#火焰图的局限" class="headerlink" title="火焰图的局限"></a>火焰图的局限</h3><p>On-CPU&#x2F;Off-CPU 火焰图覆盖了 100% 的线程时间，那是否把它们结合起来就能解决所有的性能问题呢? 答案是否定的。在分析 <strong>吞吐量（Throughput）</strong> 和 <strong>延迟（Latency）</strong> 时，既要关注指标的平均值，还要关注到 P99、P99.9 分位值、Max 值。 On-CPU&#x2F;Off-CPU 火焰图就会失效，主要原因在于：</p><p><strong>1. 采样机制的天然局限</strong></p><ul><li>基于定时采样的工具（如 <code>perf</code>）更易捕获高频执行的代码路径。</li><li>低频冷路径可能从未被采样命中（如采样间隔为 10ms，而冷路径 2 秒仅触发一次）。</li></ul><p><strong>2. 时间聚合的视角陷阱</strong></p><ul><li>On-CPU&#x2F;Off-CPU 的宽度反映<strong>总时间</strong>，而非单次执行成本，无法区分以下两种场景：<ul><li>高频低耗（热路径）：<code>执行次数 × 单次时间 = 总时间</code></li><li>低频高耗（冷路径）：<code>执行次数 × 单次时间 = 总时间</code></li></ul></li><li>冷路径因总时间占比低，在火焰图中会被压缩成“细线”而忽视</li></ul><p><a href="https://www.brendangregg.com/blog/2018-12-15/flamescope-origin.html">Flamescope</a> 使用 <a href="https://www.brendangregg.com/HeatMaps/subsecondoffset.html">亚秒级偏移热力图</a> 和 <a href="https://www.brendangregg.com/flamegraphs.html">火焰图</a> 来分析<strong>周期性</strong>活动、 <strong>方差</strong>和<strong>扰动</strong>，在一定程度上解决了这些问题，但对于一些极端 Case 仍然力有未逮。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/04-13-2025/flamegraph-summary.html">https://www.cyningsun.com/04-13-2025/flamegraph-summary.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;什么是火焰图？&quot;&gt;&lt;a href=&quot;#什么是火焰图？&quot; class=&quot;headerlink&quot; title=&quot;什么是火焰图？&quot;&gt;&lt;/a&gt;什么是火焰图？&lt;/h3&gt;&lt;p&gt;2011 年，时任 Netflix 高级性能工程师的 Brendan Gregg 面临一个棘手问题：</summary>
      
    
    
    
    <category term="Performance" scheme="https://www.cyningsun.com/category/Performance/"/>
    
    
    <category term="Flamegraph" scheme="https://www.cyningsun.com/tag/Flamegraph/"/>
    
  </entry>
  
  <entry>
    <title>译｜Linux Page Cache mini book</title>
    <link href="https://www.cyningsun.com/12-11-2024/linux-page-cache-minibook-cn.html"/>
    <id>https://www.cyningsun.com/12-11-2024/linux-page-cache-minibook-cn.html</id>
    <published>2024-12-10T16:00:00.000Z</published>
    <updated>2026-04-03T14:52:49.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="SRE-深入理解-Linux-Page-Cache"><a href="#SRE-深入理解-Linux-Page-Cache" class="headerlink" title="SRE 深入理解 Linux Page Cache"></a>SRE 深入理解 Linux Page Cache</h2><p>在本系列文章中，我将讨论 <strong>Linux Page Cache</strong>。我相信，掌握以下的理论知识和工具<strong>对于每一位 SRE 来说都是至关重要</strong>的。这种理解不仅有助于日常的 DevOps 任务，也有助于紧急调试和救火。Page Cache 经常被忽视，更好地理解它有以下好处：</p><ul><li>更<strong>精确的容量规划</strong>和<strong>容器限制计算</strong>；</li><li>更好地<strong>调试和调查</strong>内存和磁盘密集型应用（如<strong>数据库管理系统</strong>和文件共享<strong>存储</strong>）</li><li>构建内存和/或 I/O 密集型临时任务（例如：备份和恢复脚本、 rsync 一行代码等）的<strong>安全和可预测的运行时</strong>。</li></ul><p>我将展示在处理 Page Cache 相关任务和问题时，您应该记住的<strong>实用工具</strong>，如何正确使用它们<strong>理解实际内存使用情况</strong>，以及如何使用它们<strong>揭示问题</strong>。我将尝试为您提供一些接近实际情况的使用这些工具的示例。下面是我所讨论所涉及的一些工具：<code>vmtouch</code>、<code>perf</code>、 <code>cgtouch</code>、<code>strace</code>、<code>sar</code> 和 <code>page-type</code>。</p><p>此外，正如标题所说，“深入理解”，<strong>这些实用工具的内部结构</strong>将重点展示 <strong>Page Cache</strong> 的统计、事件、系统调用和内核接口。以下是在接下来的文章中我将涉及的一些示例：</p><ul><li>文件：<code>/proc/PID/smaps</code>、<code>/proc/pid/pagemap</code>、 <code>/proc/kpageflags</code>、<code>/proc/kpagecgroup</code> 和 <code>sysfs</code> 文件： <code>/sys/kernel/mm/page_idle</code> ；</li><li>系统调用： <code>mincore()</code>、<code>mmap()</code>、<code>fsync()</code>、<code>msync()</code>、<code>posix_fadvise()</code>、<code>madvise()</code> 及其他；</li><li>不同 open 和 advise 标志 <code>O_SYNC</code>、<code>FADV_DONTNEED</code>、<code>POSIX_FADV_RANDOM</code>、<code>MADV_DONTNEED</code> 等等。</li></ul><p>我将尝试使用 Python、Go 和少量 C 语言编写的简单（几乎全部）代码示例，尽可能详细地进行说明。</p><p>最后，任何有关现代 GNU/Linux 系统的对话都必须涉及 <code>cgroup</code>（在我们的例子中是 <code>v2</code>）和 <code>systemd</code> 主题。我将向您展示<strong>如何利用它们</strong>来充分发挥系统的潜力，构建可靠、可观察、可控的服务，并在值班时睡个好觉。</p><p>如果读者具有中等程度的 GNU/Linux 知识和基本的编程技能，那么他们应该能够轻松理解本文内容。</p><p>所有超过 5 行的代码示例都可以在 github 上找到：<a href="https://github.com/brk0v/sre-page-cache-article">sre-page-cache-article</a>。</p><h2 id="准备实验环境"><a href="#准备实验环境" class="headerlink" title="准备实验环境"></a>准备实验环境</h2><p>在开始之前，我希望与读者达成共识，以便能够执行、编译和检查任何示例或代码片段。因此，我们需要一个现代的 GNU/Linux 安装来处理代码和内核。</p><p>如果您使用的是 Windows 或 Mac OS，我建议使用 <a href="https://www.virtualbox.org/">Virtual Box</a> 安装 <a href="https://www.vagrantup.com/">Vagrant</a> 。对于 GNU/Linux 发行版，我倾向于使用 <a href="https://archlinux.org/">Arch Linux</a>。Arch 是现代 GNU/Linux 系统的实际示例（<a href="https://i.redd.it/qxsttm8sg5k11.png">顺便说一句，我使用 Arch Linux</a>）。它支持最新的内核、systemd 和 cgroup v2。</p><p>如果您已经在使用 Linux，那么您知道该怎么做 😉。</p><blockquote><p><strong>我可以使用 docker 吗？</strong></p><p>很遗憾，不行。我们需要一个系统，可以自由发挥、突破 cgroup 限制、使用底层工具调试程序并以 root 用户身份运行代码且不受任何限制。</p></blockquote><p>下面我将展示您需要在 Arch 上安装的所有内容。</p><h2 id="Arch-Linux-配置"><a href="#Arch-Linux-配置" class="headerlink" title="Arch Linux 配置"></a>Arch Linux 配置</h2><p>当您的 Arch 运行时，请更新系统并安装以下软件包：</p><pre><code class="hljs bash">$ pacman -Sy git, base-devel, go</code></pre><p>我们需要安装 <code>yay</code> (<a href="https://github.com/Jguer/yay">https://github.com/Jguer/yay</a>) 以便能够从社区驱动的存储库安装软件：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cd</span> ~$ git <span class="hljs-built_in">clone</span> https://aur.archlinux.org/yay.git$ <span class="hljs-built_in">cd</span> yay$ makepkg -si</code></pre><p>从 <code>aur</code> 安装 <code>vmtouch</code> 工具：</p><pre><code class="hljs bash">$ yay -Sy vmtouch</code></pre><p>我们需要从内核仓库获取 <code>page-type</code> 工具，因此安装它的最简单方法是下载 Linux 内核版本并手动编译：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">mkdir</span> kernel$ <span class="hljs-built_in">cd</span> kernel$ wget https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-5.14.tar.gz$ tar -xzf linux-5.14.tar.gz$ <span class="hljs-built_in">cd</span> linux-5.14/tools/vm$ make$ sudo make install</code></pre><p>现在我们几乎准备好了。我们需要生成一个测试数据文件，它将用于我们对 Page Cache 的实验：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">dd</span> <span class="hljs-keyword">if</span>=/dev/random of=/var/tmp/file1.db count=128 bs=1M</code></pre><p>最后一步是删除所有 Linux 缓存，使系统变得干净：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">sync</span>; <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches</code></pre><h2 id="Page-Cache-关键原理"><a href="#Page-Cache-关键原理" class="headerlink" title="Page Cache 关键原理"></a>Page Cache 关键原理</h2><p>首先我们先来问一些关于 Page Cache 的合理问题：</p><ul><li><strong>Linux Page Cache</strong> 是什么？</li><li>它解决了什么问题？</li><li>为什么我们称之为 <strong>«Page»</strong> Cache？</li></ul><p>本质上，Page Cache 是虚拟文件系统（<a href="https://en.wikipedia.org/wiki/Virtual_file_system">VFS</a>）的一部分，其主要目的（正如您所猜测的）是改善读写操作的 IO 延迟。write-back 缓存算法是 Page Cache 的核心构建块。</p><blockquote><p><strong>注意</strong></p><p>如果你对 write-back 算法感到好奇（您应该如此），它在 <a href="https://en.wikipedia.org/wiki/Cache_%28computing%29#Writing_policies">维基百科</a> 上有很好的描述，我鼓励您阅读它，或者至少查看带有流程图及其主要操作的图表。</p></blockquote><p>Page Cache 中的 “Page” 表示 Linux 内核使用称为页的内存单元。跟踪和管理信息的字节甚至比特会很麻烦和困难。因此，Linux 的方法（顺便说一句，不仅仅是 Linux）是在几乎所有结构和操作中使用页（**通常长度为 <code>4K</code>**）。因此，Page Cache 中的最小存储单位是页，无论您要读取或写入多少数据都无关紧要。所有文件 IO 请求都与一定数量的页对齐。</p><p>上述内容引出了一个重要的事实：<strong>如果您的写入小于页大小，则内核将在您的写入完成之前读取整个页</strong>。</p><p>下图展示了 Page Cache 的基本操作。我将其分为读取和写入。</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233132-1.png"></p><p>可以看到，所有数据读写都经过 Page Cache。不过 <code>Direct IO</code> （ <code>DIO</code> ）有一些例外，我会在本系列的最后讨论。目前，我们先忽略它们。</p><blockquote><p><strong>注意</strong></p><p>在接下来的章节中，我将讨论 <code>read()</code>、<code>write()</code>、<code>mmap()</code> 以及其他系统调用。我需要指出的是，一些编程语言（例如 Python）具有同名的 file 函数。但是，这些函数并不 <em>完全</em> 对应到相应的系统调用。此类函数通常执行缓冲 IO。请记住这一点。</p></blockquote><h3 id="读取请求"><a href="#读取请求" class="headerlink" title="读取请求"></a>读取请求</h3><p>一般来说，内核按以下方式处理读取：</p><ol><li>当用户空间应用程序想要从磁盘读取数据时，它使用特殊的系统调用（例如 <code>read()</code>、<code>pread()</code>、<code>vread()</code>、<code>mmap()</code>、<code>sendfile()</code> 等）向内核请求数据。</li><li>Linux 内核则会检查页是否存在于 Page Cache 中，如果存在，则立即将其返回给调用者。如您所见，在这种情况下，内核没有进行任何磁盘操作。</li><li>如果 Page Cache 中没有这些页，内核必须从磁盘加载它们。为此，它必须在 Page Cache 中为请求的页找到一个位置。如果没有可用内存（在调用者的 cgroup 或系统中），则必须执行内存回收过程。之后，内核会安排读取磁盘 IO 操作，将目标页存储在内存中，并最终将请求的数据从 Page Cache 返回给目标进程。从此刻开始，任何未来读取文件该部分数据的请求（无论来自哪个进程或 cgroup）都将由 Page Cache 处理，而无需任何磁盘 IOP，直至这些页被驱逐。</li></ol><h3 id="写入请求"><a href="#写入请求" class="headerlink" title="写入请求"></a>写入请求</h3><p>让我们一步步地重复写入的流程：</p><ol><li>当用户空间程序想要将一些数据写入磁盘时，它也会使用一堆系统调用，例如：<code>write()</code>、<code>pwrite()</code>、<code>writev()</code>、<code>mmap()</code> 等。与读取相比，写入通常更快，因为真正的磁盘 IO 操作不会立即执行。然而，只有在系统或 cgroup 没有内存压力问题，并且有足够的可用页时，才是正确的（我们稍后会讨论驱逐过程）。所以通常内核只更新 Page Cache 中的页。它使写入流本质上是异步的。调用者不知道何时发生实际的页刷新，但它知道后续读取将返回最新数据。Page Cache 维持所有进程和 cgroup 之间的数据一致性。包含未刷新数据的此类页有一个特殊的名称：<strong>脏页</strong>。</li><li>如果进程的数据并不重要，它可以依靠内核及其 flush 进程，最终将数据持久保存到物理磁盘。但是，如果您开发数据库管理系统（例如，用于货币交易），则需要写入保证以保护您的记录免受突然断电的影响。对于这种情况，Linux 提供了 <code>fsync()</code>、<code>fdatasync()</code> 和 <code>msync()</code> 系统调用，它们会阻塞，直到文件的所有脏页都提交到磁盘。还有 <code>open()</code> 标志：<code>O_SYNC</code> 和 <code>O_DSYNC</code>，您也可以使用它们来使所有文件写入操作默认持久。我稍后会展示此逻辑的一些示例。</li></ol><h2 id="Page-Cache-和基本文件操作"><a href="#Page-Cache-和基本文件操作" class="headerlink" title="Page Cache 和基本文件操作"></a>Page Cache 和基本文件操作</h2><p>现在是时候撸起袖子，开始实践一些实际的例子了。读完本章后，你将知道如何与 Page Cache 交互以及可以使用哪些工具。</p><p>本节所需的实用程序：</p><ul><li><code>sync</code>( <a href="https://man7.org/linux/man-pages/man1/sync.1.html"><code>man 1 sync</code></a>) – 将所有脏页刷新到持久存储的工具；</li><li><code>/proc/sys/vm/drop_caches</code>（ <a href="https://man7.org/linux/man-pages/man5/proc.5.html"><code>man 5 proc</code></a>） – 触发 Page Cache 清除的内核 <code>procfs</code> 文件；</li><li><a href="https://github.com/hoytech/vmtouch"><code>vmtouch</code></a> – 一种通过文件路径获取特定文件的 Page Cache 信息的工具。</li></ul><blockquote><p><strong>注意</strong></p><p>当前我们先忽略 <code>vmtouch</code> 的工作原理。稍后我将展示如何编写一个几乎包含所有功能的替代版本。</p></blockquote><h3 id="文件读取"><a href="#文件读取" class="headerlink" title="文件读取"></a>文件读取</h3><h4 id="使用-read-系统调用读取文件"><a href="#使用-read-系统调用读取文件" class="headerlink" title="使用 read() 系统调用读取文件"></a>使用 <code>read()</code> 系统调用读取文件</h4><p>我从简单的程序开始，该程序从测试文件 <code>/var/tmp/file1.db</code> 中读取前 2 个字节。</p><pre><code class="hljs python"><span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"br"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-built_in">print</span>(f.read(<span class="hljs-number">2</span>))</code></pre><p>通常，这些类型的读取请求会被转换为 <code>read()</code> 系统调用。让我们使用 <code>strace</code>( <a href="https://man7.org/linux/man-pages/man1/strace.1.html"><code>man 1 strace</code></a> ) 运行脚本以确认 <code>f.read()</code> 使用了 <code>read()</code> 系统调用：</p><pre><code class="hljs bash">$ strace -s0 python3 ./read_2_bytes.py</code></pre><p>输出应如下所示：</p><pre><code class="hljs bash">...openat(AT_FDCWD, <span class="hljs-string">"./file1.db"</span>, O_RDONLY|O_CLOEXEC) = 3...<span class="hljs-built_in">read</span>(3, <span class="hljs-string">"%B\353\276\0053\356\346Nfy2\354[&amp;\357\300\260%D6<span class="hljs-variable">$b</span>?'\31\237_fXD\234"</span>..., 4096) = 4096...</code></pre><blockquote><p><strong>注意</strong></p><p>尽管脚本仅请求 2 个字节，但 <code>read()</code> 系统调用返回了 4096 个字节（一页）。这是 Python 优化和内部缓冲 IO 的一个例子。虽然这超出了本文的范围，但在某些情况下，记住这一点很重要。</p></blockquote><p>现在让我们检查一下内核缓存了多少数据。为了获取此信息，我们使用 <code>vmtouch</code>：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db</code></pre><pre><code class="hljs bash">         Files: 1       LOOK HERE   Directories: 0          ⬇Resident Pages: 20/32768  80K/128M  0.061%       Elapsed: 0.001188 seconds</code></pre><p>从输出可以看到，内核缓存的数据量不是 <code>Python</code> 请求的 2B，而是 80KiB 或 20 页。</p><p>根据设计，内核无法将小于 4KiB 或一页的内容加载到 Page Cache 中，但其他 19 页是怎么回事？这是内核<strong>预读</strong>逻辑和优先执行顺序 IO 操作而非随机 IO 操作的一个很好的例子。基本思想是<strong>预测后续读取并尽量减少磁盘寻道次数</strong>。系统调用可以控制此行为：<code>posix_fadvise()</code>（<a href="https://man7.org/linux/man-pages/man2/posix_fadvise.2.html"><code>man 2 posix_fadvise</code></a>）和 <code>readahead()</code>（<a href="https://man7.org/linux/man-pages/man2/readahead.2.html"><code>man 2 readahead</code></a>）。</p><blockquote><p><strong>注意</strong></p><p>通常，在生产环境中，数据库管理系统和存储调整默认预读参数不会产生太大影响。如果 DBMS 不需要预读缓存的数据，则内核内存回收策略最终应将这些页从 Page Cache 中逐出。通常，顺序 IO 对内核和硬件来说并不昂贵。完全禁用预读甚至可能会导致性能下降，因为内核队列中的磁盘 IO 操作数量增加、上下文切换增多以及内核内存管理子系统识别工作集所需时间增加。我们将在本系列的后面讨论内存回收策略、内存压力和缓存写回。</p></blockquote><p>现在让我们使用 <code>posix_fadvise()</code> 通知内核我们正在随机读取文件，因此我们不想有任何预读功能：</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"br"</span>) <span class="hljs-keyword">as</span> f:    fd = f.fileno()    os.posix_fadvise(fd, <span class="hljs-number">0</span>, os.fstat(fd).st_size, os.POSIX_FADV_RANDOM)    <span class="hljs-built_in">print</span>(f.read(<span class="hljs-number">2</span>))</code></pre><p>在运行脚本之前，我们需要清除所有缓存：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches &amp;&amp; python3 ./read_2_random.py</code></pre><p>现在，如果你检查 <code>vmtouch</code> 输出，你会看到只有一页，正如预期的那样：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db</code></pre><pre><code class="hljs bash">         Files: 1     LOOK HERE   Directories: 0        ⬇Resident Pages: 1/32768  4K/128M  0.00305%       Elapsed: 0.001034 seconds</code></pre><h4 id="使用-mmap-系统调用读取文件"><a href="#使用-mmap-系统调用读取文件" class="headerlink" title="使用 mmap() 系统调用读取文件"></a>使用 <code>mmap()</code> 系统调用读取文件</h4><p>为了从文件中读取数据，我们还可以使用 <code>mmap()</code> 系统调用 ( <a href="https://man7.org/linux/man-pages/man2/mmap.2.html"><code>man 2 mmap</code></a>)。<code>mmap()</code> 是一种“神奇”工具，可用于处理各种任务。但对于我们的测试，我们只需要其一个特性 —— 将文件映射到进程内存中，以便将文件作为扁平的数组访问。我稍后会更详细地讨论 <code>mmap()</code>。但目前，如果您不熟悉它，应该可以从以下示例中理解 <code>mmap()</code> API ：</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> mmap<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-keyword">with</span> mmap.mmap(f.fileno(), <span class="hljs-number">0</span>, prot=mmap.PROT_READ) <span class="hljs-keyword">as</span> mm:        <span class="hljs-built_in">print</span>(mm[:<span class="hljs-number">2</span>])</code></pre><p>上述代码与我们刚刚使用 <code>read()</code> 系统调用所做的操作相同。它读取文件的前 2 个字节。</p><p>此外，出于测试目的，我们需要在执行脚本之前清除所有缓存：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches &amp;&amp; python3 ./read_2_mmap.py</code></pre><p>检查 Page Cache 内容：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db</code></pre><pre><code class="hljs bash">         Files: 1.       LOOK HERE   Directories: 0           ⬇Resident Pages: 1024/32768  4M/128M  3.12%       Elapsed: 0.000627 seconds</code></pre><p>正如您所见，<code>mmap()</code> 执行了更为激进的预读。</p><p>让我们像之前 <code>fadvise()</code> 所做的那样，使用 <code>madvise()</code> 系统调用来改变预读。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> mmap<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-keyword">with</span> mmap.mmap(f.fileno(), <span class="hljs-number">0</span>, prot=mmap.PROT_READ) <span class="hljs-keyword">as</span> mm:        mm.madvise(mmap.MADV_RANDOM)        <span class="hljs-built_in">print</span>(mm[:<span class="hljs-number">2</span>])</code></pre><p>运行它：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches &amp;&amp; python3 ./read_2_mmap_random.py</code></pre><p>Page Cache 内容：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db</code></pre><pre><code class="hljs bash">         Files: 1     LOOK HERE   Directories: 0        ⬇Resident Pages: 1/32768  4K/128M  0.00305%       Elapsed: 0.001077 seconds</code></pre><p>从上面的输出可以看出，使用该 <code>MADV_RANDOM</code> 标志，我们成功地从磁盘读取了一页，并在 Page Cache 中存储了一页数据。</p><h3 id="文件写入"><a href="#文件写入" class="headerlink" title="文件写入"></a>文件写入</h3><p>现在让我们来试下写入。</p><h4 id="使用-write-系统调用写入"><a href="#使用-write-系统调用写入" class="headerlink" title="使用 write() 系统调用写入"></a>使用 <code>write()</code> 系统调用写入</h4><p>让我们继续使用我们的实验文件，并尝试更新前 2 个字节：</p><pre><code class="hljs py"><span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"br+"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-built_in">print</span>(f.write(<span class="hljs-string">b"ab"</span>))</code></pre><blockquote><p><strong>注意</strong></p><p>小心，不要用 <code>w</code> 模式打开文件。它会用 2 个字节重写你的文件。我们需要 <code>r+</code> 模式。</p></blockquote><p>清除所有缓存，并运行上述脚本：</p><pre><code class="hljs bash"><span class="hljs-built_in">sync</span>; <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches &amp;&amp; python3 ./write_2_bytes.py</code></pre><p>现在让我们检查一下 Page Cache 的内容。</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db           Files: 1     LOOK HERE     Directories: 0        ⬇  Resident Pages: 1/32768  4K/128M  0.00305%         Elapsed: 0.000674 seconds</code></pre><p>如您所见，我们仅写入 2B 就缓存了 1 个页。这是一个重要的观察，为了填充 Page Cache，<strong>如果您的写入大小小于页大小，则在写入之前将进行 4KiB 读取</strong>。</p><p>另外，我们可以通过读取当前 cgroup 内存统计文件来检查脏页。</p><p>获取当前终端的 cgroup：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /proc/self/cgroup0::/user.slice/user-1000.slice/session-4.scope</code></pre><pre><code class="hljs bash">$ grep dirty /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope/memory.statfile_dirty 4096</code></pre><p>如果看到 0，显然您很幸运，脏页已经写入磁盘，请再次运行该脚本。</p><h4 id="使用-mmap-系统调用写入"><a href="#使用-mmap-系统调用写入" class="headerlink" title="使用 mmap() 系统调用写入"></a>使用 <code>mmap()</code> 系统调用写入</h4><p>现在让我们使用 <code>mmap()</code> 重复写入：</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> mmap<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"r+b"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-keyword">with</span> mmap.mmap(f.fileno(), <span class="hljs-number">0</span>) <span class="hljs-keyword">as</span> mm:        mm[:<span class="hljs-number">2</span>] = <span class="hljs-string">b"ab"</span></code></pre><p>您可以重复上述命令，并使用 <code>vmtouch</code> 和 cgroup<code>grep</code> 来获取脏页，您应该会得到相同的输出。唯一的例外是预读策略。默认情况下，即使对于写入请求，<code>mmap()</code> 也会在 Page Cache 中加载更多数据。</p><h4 id="脏页"><a href="#脏页" class="headerlink" title="脏页"></a>脏页</h4><p>正如我们之前看到的，进程通过 Page Cache 写入文件会生成脏页。</p><p>Linux 提供了几种方法获取脏页数量。最早且最古老的一种方法是读取 <code>/proc/meminfo</code>：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /proc/meminfo | grep DirtyDirty:                 4 kB</code></pre><p>完整的系统信息通常很难理解和使用，因为我们无法确定哪个进程和文件包含这些脏页。</p><p>这就是为什么获取脏页信息的最佳选择是使用 cgroup：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope/memory.stat  | grep dirtfile_dirty 4096</code></pre><p>如果您的程序使用 <code>mmap()</code> 写入文件，您还有另一个方法可以获取进程级粒度的脏页统计信息。<code>procfs</code> 的 <code>/proc/PID/smaps</code> 文件。它包含按虚拟内存区域 (VMA) 细分的进程内存计数器。通过查找以下内容，我们可以获取脏页信息：</p><ul><li><code>Private_Dirty</code> – 此进程产生的脏数据量；</li><li><code>Shared_Dirty</code> – 以及其他进程写入的数量。此指标仅显示引用页的数据。这意味着进程应该访问页并将其保存在其 <a href="https://en.wikipedia.org/wiki/Page_table">页表</a> 中（稍后将详细介绍）。</li></ul><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /proc/578097/smaps | grep file1.db -A 12 | grep DirtyShared_Dirty:          0 kBPrivate_Dirty:       736 kB</code></pre><p>但是如果我们想要获取某个文件的脏页统计信息该怎么办？为了回答这个问题，Linux 内核的 <code>procfs</code> 提供了 <a href="https://www.kernel.org/doc/Documentation/vm/pagemap.txt">2 个文件</a>：<code>/proc/PID/pagemap</code> 和 <code>/proc/kpageflags</code>。我将在本系列的后面部分展示如何使用它们编写我们自己的工具，但现在我们可以使用 Linux 内核仓库中的调试工具来获取每个文件的页信息：<a href="https://github.com/torvalds/linux/blob/master/tools/vm/page-types.c"><code>page-types</code></a>。</p><pre><code class="hljs bash">$ sudo page-types -f /var/tmp/file1.db -b dirty</code></pre><pre><code class="hljs bash">             flags      page-count       MB  symbolic-flags                     long-symbolic-flags0x0000000000000838             267        1  ___UDl_____M________________________________       uptodate,dirty,lru,mmap0x000000000000083c              20        0  __RUDl_____M________________________________       referenced,uptodate,dirty,lru,mmap             total             287        1</code></pre><p>我根据 <code>dirty</code> 标志过滤出文件 <code>/var/tmp/file1.db</code> 的所有页 。在输出中，你可以看到文件有 287 个脏页或 1 MiB 的脏数据，这些数据最终将持久化到存储中。<code>page-type</code> 根据标志聚合页，因此输出中有 2 组。两者都有脏标志 <code>D</code>，它们之间的区别在于引用标志 <code>R</code>（我将在后面的 Page Cache 驱逐部分简要介绍它）。</p><h4 id="使用-fsync-、fdatasync-和-msync-同步文件更改"><a href="#使用-fsync-、fdatasync-和-msync-同步文件更改" class="headerlink" title="使用 fsync()、fdatasync() 和 msync() 同步文件更改"></a>使用 <code>fsync()</code>、<code>fdatasync()</code> 和 <code>msync()</code> 同步文件更改</h4><p>我们已经在每次测试之前使用 <code>sync</code>(<code>man 1 sync</code>) 将所有脏页刷新到磁盘，以获得一个没有任何干扰的干净系统。但是，如果我们想编写一个数据库管理系统，并且需要确保在断电或其他硬件错误发生之前的所有写操作都将写入到磁盘，该怎么办？对于这种情况，Linux 提供了几种方法来强制内核对 Page Cache 中的文件执行同步：</p><ul><li><code>fsync()</code> – 阻塞直至目标文件及其元数据的所有脏页都被同步为止；</li><li><code>fdatasync()</code> – 与上述相同，但不包括元数据；</li><li><code>msync()</code> – 与 <code>fsync()</code> 相同，但用于内存映射文件；</li><li>使用 <code>O_SYNC</code> 或 <code>O_DSYNC</code> 标志打开文件，使所有文件写入默认同步，并相应地作为 <code>fsync()</code>、<code>fdatasync()</code> 系统调用工作。</li></ul><blockquote><p><strong>注意</strong></p><p>您仍然需要关注写屏障并了解底层文件系统的工作原理，因为内核调度程序可能会重新排列写操作的顺序。通常，文件追加操作是安全的，不会破坏之前写入的数据。其他类型的变异操作可能会弄乱您的文件（例如，对于 ext4，即使使用默认日志也是如此）。这就是为什么几乎所有数据库管理系统（如 MongoDB、PostgreSQL、Etcd、Dgraph 等）都具有仅追加的预写日志 (WAL)。但也有一些例外。如果您对这个主题更感兴趣，<a href="https://dgraph.io/blog/post/alice/">Dgraph 的这篇博客文章</a> 是一个很好的起点。</p><p>不过也有一些例外。例如， <a href="http://www.lmdb.tech/doc/"><code>lmdb</code></a>（及其克隆，<a href="https://github.com/etcd-io/bbolt"><code>bboltdb</code></a> 来自 <a href="https://etcd.io/"><code>etcd</code></a>）<a href="https://www.youtube.com/watch?v=tEa5sAh-kVk">使用了一个巧妙的想法，即保留其 B+ 树的两个根并执行写时复制</a>。</p></blockquote><p>以下是文件同步的示例：</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"br+"</span>) <span class="hljs-keyword">as</span> f:    fd = f.fileno()    os.fsync(fd)</code></pre><h4 id="使用-mincore-检查-Page-Cache-中的文件存在"><a href="#使用-mincore-检查-Page-Cache-中的文件存在" class="headerlink" title="使用 mincore() 检查 Page Cache 中的文件存在"></a>使用 <code>mincore()</code> 检查 Page Cache 中的文件存在</h4><p>在进一步之前，我们先弄清楚 <code>vmtouch</code> 如何显示目标文件 Page Cache 包含多少页。</p><p>秘密在于 <code>mincore()</code> 系统调用（<a href="https://man7.org/linux/man-pages/man2/mincore.2.html"><code>man 2 mincore</code></a>）。<code>mincore()</code> 代表“核心内存”。其参数是起始虚拟内存地址、地址空间长度和结果向量。 <code>mincore()</code> 与内存（而非文件）交互，因此可用于检查匿名内存是否已被换出。</p><blockquote><p><code>man 2 mincore</code></p><p><code>mincore()</code> 返回一个向量，该向量指示调用进程的虚拟内存页是否驻留在内核 (RAM) 中，因此在引用时，不会导致磁盘访问 (缺页中断)。内核返回从地址 addr，长度为 length 个字节的页驻留信息。</p></blockquote><p>因此，要进行复制 <code>vmtouch</code>，我们需要将文件映射到进程的虚拟内存中，即使我们不进行读取或写入。我们只是希望将其放在进程内存区域中（稍后在 <code>mmap()</code> 部分将详细介绍这一点）。</p><p>现在，我们已经准备好编写自己的简单版本 <code>vmtouch</code>，以便通过文件路径显示缓存页。我在这里使用 <code>go</code>，因为不幸的是，Python 没有一种简单的方法来调用 <code>mincore()</code> 系统调用：</p><pre><code class="hljs go"><span class="hljs-keyword">package</span> main<span class="hljs-keyword">import</span> (<span class="hljs-string">"fmt"</span><span class="hljs-string">"log"</span><span class="hljs-string">"os"</span><span class="hljs-string">"syscall"</span><span class="hljs-string">"unsafe"</span>)<span class="hljs-keyword">var</span> (pageSize = <span class="hljs-type">int64</span>(syscall.Getpagesize())mode     = os.FileMode(<span class="hljs-number">0600</span>))<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {path := <span class="hljs-string">"/var/tmp/file1.db"</span>file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NOATIME, mode)<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {log.Fatal(err)}<span class="hljs-keyword">defer</span> file.Close()stat, err := os.Lstat(path)<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {log.Fatal(err)}size := stat.Size()pages := size / pageSizemm, err := syscall.Mmap(<span class="hljs-type">int</span>(file.Fd()), <span class="hljs-number">0</span>, <span class="hljs-type">int</span>(size), syscall.PROT_READ, syscall.MAP_SHARED)<span class="hljs-keyword">defer</span> syscall.Munmap(mm)mmPtr := <span class="hljs-type">uintptr</span>(unsafe.Pointer(&amp;mm[<span class="hljs-number">0</span>]))cached := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, pages)sizePtr := <span class="hljs-type">uintptr</span>(size)cachedPtr := <span class="hljs-type">uintptr</span>(unsafe.Pointer(&amp;cached[<span class="hljs-number">0</span>]))ret, _, err := syscall.Syscall(syscall.SYS_MINCORE, mmPtr, sizePtr, cachedPtr)<span class="hljs-keyword">if</span> ret != <span class="hljs-number">0</span> {log.Fatal(<span class="hljs-string">"syscall SYS_MINCORE failed: %v"</span>, err)}n := <span class="hljs-number">0</span><span class="hljs-keyword">for</span> _, p := <span class="hljs-keyword">range</span> cached {<span class="hljs-comment">// the least significant bit of each byte will be set if the corresponding page</span><span class="hljs-comment">// is currently resident in memory, and be clear otherwise.</span><span class="hljs-keyword">if</span> p%<span class="hljs-number">2</span> == <span class="hljs-number">1</span> {n++}}fmt.Printf(<span class="hljs-string">"Resident Pages: %d/%d  %d/%d\n"</span>, n, pages, n*<span class="hljs-type">int</span>(pageSize), size)}</code></pre><p>运行它：</p><pre><code class="hljs bash">$ go run ./main.go</code></pre><pre><code class="hljs bash">Resident Pages: 1024/32768  4194304/134217728</code></pre><p>并将其与 <code>vmtouch</code> 输出进行比较：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db           Files: 1         LOOK HERE     Directories: 0            ⬇  Resident Pages: 1024/32768  4M/128M  3.12%         Elapsed: 0.000804 seconds</code></pre><h2 id="Page-Cache-驱逐与回收"><a href="#Page-Cache-驱逐与回收" class="headerlink" title="Page Cache 驱逐与回收"></a>Page Cache 驱逐与回收</h2><p>到目前为止，我们已经讨论了通过读取和写入文件向 Page Cache 添加数据、检查缓存中文件的存在以及手动刷新缓存内容。但任何缓存系统最关键的部分是其<strong>驱逐策略</strong>，或者对于 Linux Page Cache，它也是内存<strong>页回收</strong>策略。与任何其他缓存一样，Linux Page Cache 会持续监视最后使用的页，并决定应删除哪些页以及应将哪些页保留在缓存中。</p><p>控制和调整 Page Cache 的主要方法是 cgroup 子系统。您可以将服务器的内存划分为几个较小的缓存（cgroup），从而控制和保护应用程序和服务。此外，cgroup 内存和 IO 控制器提供大量统计数据，这些数据对于调优软件和了解缓存的内部情况非常有用。</p><h3 id="理论"><a href="#理论" class="headerlink" title="理论"></a>理论</h3><p>Linux Page Cache 与 Linux 内存管理、cgroup 和虚拟文件系统 (VFS) 紧密相关。因此，为了理解驱逐的工作原理，我们需要从内存回收策略的一些基本内部原理开始。其核心结构是<strong>active 和 inactive 列表</strong>，每个 cgroup <strong>一对</strong>：</p><ol><li>第一对用于匿名内存（例如，使用 <code>malloc()</code> 或非文件的 <code>mmap()</code> 分配）；</li><li>第二对用于 Page Cache 文件内存（所有文件操作包括 <code>read()</code>、<code>write</code>、文件的 <code>mmap()</code> 访问等）。</li></ol><p>前者正是我们感兴趣的，linux 用于 Page Cache 驱逐过程的就是这一对，每个链表的核心都是最近最少使用算法 <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies%23Least_recently_used_%2528LRU%2529">LRU</a>，反过来，这 2 个链表又组成了一个 <a href="https://en.wikipedia.org/wiki/Page_replacement_algorithm%23Clock">双时钟</a> 的数据结构，一般来说 linux 应该选择最近没用过（inactive）的页，因为最近没用过的页在短时间内不会被频繁使用，这就是 LRU 算法的基本思想。active 链表和 inactive 链表的条目都采用了 FIFO（先进先出）的形式，新元素被添加到链表的头部，中间的元素逐渐向尾部移动，当需要内存回收的时候，内核总是选择 inactive 链表尾部的页进行释放，下图是该思想的简化：</p><p>!<img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233132-2.png"></p><p>例如，系统启动时，列表的内容如下。用户进程刚刚从磁盘读取了一些数据。此操作触发内核将数据加载到缓存中。这是内核第一次访问该文件。因此，内核在进程 cgroup 的 inactive 列表的头部添加一个页 <code>h</code> ：</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233132-3.png"></p><p>一段时间后，系统又加载了 2 个额外的页：<code>i</code> 和 <code>j</code> 到 inactive 列表中，并相应地需要从列表中驱逐页 <code>a</code> 和 <code>b</code>。此操作也将所有页向 inactive LRU 列表的尾部移动，包括我们的页 <code>h</code> ：</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233133-1.png"></p><p>现在，对页 <code>h</code> 执行新的文件操作会将该页提升到 active LRU 列表的头部，将其置于头部。此操作还会将该页 <code>1</code> 移至 inactive LRU 列表的头部，并移动所有其他成员。</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233133-2.png"></p><p>随着时间的推移，页 <code>h</code> 在 active LRU 列表中失去了其头部位置。</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233133-3.png"></p><p>但一个新的文件访问到 <code>h</code> 在文件中的位置会将 <code>h</code> 移动到 active LRU 列表的头部。</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233133-4.png"></p><p>上图展示了该算法的简化版本。</p><p>但值得一提的是，页提升和降级的实际过程要复杂精妙得多。</p><p>首先，如果系统有 <a href="https://en.wikipedia.org/wiki/Non-uniform_memory_access">NUMA</a> 硬件节点 ( <code>man 8 numastat</code>)，<strong>那么它将拥有 2 倍数量的 LRU 列表</strong>。原因是内核尝试将内存信息存储在 NUMA 节点中，以减少锁争用。</p><p>此外，Linux Page Cache 具有<strong>特殊的影子和引用标志逻辑</strong>，用于页的提升、降级和重新提升。</p><p><strong>影子条目</strong>有助于缓解 **<a href="https://en.wikipedia.org/wiki/Thrashing_%2528computer_science%2529">内存抖动问题</a>**。当程序的工作集大小接近或大于实际内存大小（可能是 cgroup 限制或系统 RAM 限制）时，就会发生此问题。在这种情况下，读取模式可能会在随后的第二个读取请求出现之前从 inactive 列表中逐出页。完整的想法描述于 <a href="https://elixir.bootlin.com/linux/v5.14-rc7/source/mm/workingset.c">mm/workingset.c</a>，其中包括计算 <strong>refault distance</strong>。此距离用于判断是否立即将影子条目放入 active LRU 列表。</p><p>我做的另一个简化是关于 <a href="https://elixir.bootlin.com/linux/v5.14.3/source/include/linux/page-flags.h%23L105"><code>PG_referenced</code></a> 页标志。实际上，页提升和降级使用此标志作为决策算法中的额外输入参数。页提升的更正确流程：</p><pre class="mermaid">flowchart LR    Start["Inactive LRU,<br>unreferenced"]    Second["Inactive LRU,<br>referenced"]    Third["Active LRU,<br>unreferenced"]    Stop["Active LRU,<br>referenced"]    Start --&gt; Second    Second --&gt; Third    Third --&gt; Stop</pre><h3 id="使用-POSIX-FADV-DONTNEED-手动驱逐页"><a href="#使用-POSIX-FADV-DONTNEED-手动驱逐页" class="headerlink" title="使用 POSIX_FADV_DONTNEED 手动驱逐页"></a>使用 <code>POSIX_FADV_DONTNEED</code> 手动驱逐页</h3><p>我已经展示了如何使用 <code>/proc/sys/vm/drop_caches</code> 文件清除所有页缓存条目。但如果我们出于某种原因想要清除某个文件的缓存，该怎么办？</p><blockquote><p><strong>示例</strong></p><p>在实际情况下，从缓存中清除文件有时很有用。假设我们想测试 MongoDB 在系统重启后恢复到最佳状态的速度。您可以停止一个副本，从 Page Cache 中清除其所有文件，然后重新启动它。</p></blockquote><p><code>vmtouch</code> 已经可以做到这一点。它的 <code>-e</code> 标志命令内核从 Page Cache 中逐出所请求文件的所有页：</p><p>例如：</p><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db -e           Files: 1     Directories: 0   Evicted Pages: 32768 (128M)         Elapsed: 0.000704 seconds</code></pre><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db           Files: 1.    LOOK HERE     Directories: 0        ⬇  Resident Pages: 0/32768  0/128M  0%            Elapsed: 0.000566 seconds</code></pre><p>让我们深入研究一下，弄清楚它是如何工作的。为了编写我们自己的工具，我们需要使用已见过的 <code>posix_fadvise</code> 系统调用和 <code>POSIX_FADV_DONTNEED</code> 选项。</p><p>代码：</p><pre><code class="hljs py"><span class="hljs-keyword">import</span> os<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"br"</span>) <span class="hljs-keyword">as</span> f:    fd = f.fileno()    os.posix_fadvise(fd, <span class="hljs-number">0</span>, os.fstat(fd).st_size, os.POSIX_FADV_DONTNEED)</code></pre><p>为了测试，我使用 <code>dd</code> 将整个测试文件读入 Page Cache：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">dd</span> <span class="hljs-keyword">if</span>=/var/tmp/file1.db of=/dev/null  262144+0 records <span class="hljs-keyword">in</span>  262144+0 records out  134217728 bytes (134 MB, 128 MiB) copied, 0.652248 s, 206 MB/s</code></pre><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db             Files: 1         LOOK HERE     Directories: 0             ⬇  Resident Pages: 32768/32768  128M/128M  100%         Elapsed: 0.002719 seconds</code></pre><p>现在，运行脚本后，我们应该在 Page Cache 中看到 0 个页：</p><pre><code class="hljs bash">$ python3 ./evict_full_file.py</code></pre><pre><code class="hljs bash">$ vmtouch /var/tmp/file1.db             Files: 1     LOOK HERE     Directories: 0        ⬇  Resident Pages: 0/32768  0/128M  0%         Elapsed: 0.000818 seconds</code></pre><h3 id="让内存不可驱逐"><a href="#让内存不可驱逐" class="headerlink" title="让内存不可驱逐"></a>让内存不可驱逐</h3><p>但是，如果你想要强制内核将文件内存保留在 Page Cache, 中，无论如何，该怎么办呢？这称为使文件内存<strong>不可驱逐</strong>。</p><blockquote><p><strong>示例</strong></p><p>有时，您必须强制内核 100% 保证您的文件不会被从内存中逐出。即使使用现代 Linux 内核和正确配置的 cgroup 限制，您也可能需要这样做，这应该会将工作数据集保留在 Page Cache 中。例如，由于共享磁盘和网络 IO 的系统上的其他进程出现问题。或者，例如，由于网络附加存储（NAS）的中断。</p></blockquote><p>内核提供了一系列系统调用用于执行此操作： <code>mlock()</code> 、 <code>mlock2()</code> 和 <code>mlockall()</code> 。与 <code>mincore()</code> 类似，您必须首先映射文件。</p><p><code>mlock2()</code> 是用于 Page Cache 操作的理想系统调用，因为它具有方便的标志 <code>MLOCK_ONFAULT</code> :</p><blockquote><p>锁定当前驻留的页，并标记整个范围，当剩余的非驻留页因缺页错误而填充时，锁定新填充的页。</p></blockquote><p>不要忘记考虑 <strong>limits</strong> ( <code>man 5 limits.conf</code>)。你可能需要增加它：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">ulimit</span> -l64</code></pre><p>最后，要获取不可驱逐内存的数量，请检查对应 cgroup 的 cgroup 内存控制器的统计信息：</p><pre><code class="hljs bash">$ grep unevictable /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope/memory.statunevictable 0</code></pre><h3 id="Page-Cache、vm-swappiness-和现代内核"><a href="#Page-Cache、vm-swappiness-和现代内核" class="headerlink" title="Page Cache、vm.swappiness 和现代内核"></a>Page Cache、<code>vm.swappiness</code> 和现代内核</h3><p>现在我们了解了基本的回收理论，包括 4 个 LRU 列表（用于匿名和文件内存）以及可驱逐/不可驱逐类型的内存，我们可以讨论重新填充系统空闲内存的来源。内核不断维护空闲页列表，以满足自身和用户空间的需求。如果此类列表低于阈值，Linux 内核将开始扫描 LRU 列表以查找要回收的页。使得内核能够保持内存处于某种平衡状态。</p><p>Page Cache 内存通常是可驱逐内存（除了一些罕见的 <code>mlock()</code> 例外）。因此，Page Cache 应该是内存驱逐和回收的首选和唯一选项，这看起来可能很明显。因为磁盘已经拥有了所有数据，对吧？但幸运或不幸的是，在实际生产情况下，这并不总是最好的选择。</p><p>如果系统有内存交换（<a href="https://chrisdown.name/2018/01/02/in-defence-of-swap.html">现代内核应该有</a>），内核就多了一个选择。它可以交换出匿名（非文件的）页。这似乎违反直觉，但实际情况是，有时用户空间的守护进程可以加载大量的初始化代码，但之后永远不会使用它们。例如，某些程序（尤其是静态构建的程序）的二进制文件中可能有很多功能，仅在某些边缘情况下使用几次。在所有这些情况下，将它们保存在宝贵的内存中没有多大意义。</p><p>所以，为了控制优先使用哪个 inactive LRU 列表进行扫描，内核有一个 <code>sysctl</code> <code>vm.swappiness</code> 参数。</p><pre><code class="hljs bash">$ sudo sysctl -a | grep swapvm.swappiness = 60</code></pre><p>关于这个神奇的设置有很多博客文章、故事和论坛帖子。除此之外，旧版 cgroup v1 内存子系统的每个 cgroup 有自己的 <code>swappiness</code> 参数。所有这些都使得当前 <code>vm.swappiness</code> 含义的信息难以理解和更改。但让我尝试解释一些最近的更改，并为你提供最新的链接。</p><p>首先，默认 <code>vm.swappiness</code> 设置为 60，<a href="https://github.com/torvalds/linux/blob/a3fa7a101dcff93791d1b1bdb3affcad1410c8c1/mm/vmscan.c%23L174-L177">最小值为 0，最大值为 200</a>：</p><pre><code class="hljs C"><span class="hljs-comment">/*</span><span class="hljs-comment"> * From 0 .. 200.  Higher means more swappy.</span><span class="hljs-comment"> */</span><span class="hljs-type">int</span> vm_swappiness = <span class="hljs-number">60</span>;</code></pre><p>值 100 意味着内核在回收方面同等考虑匿名页和 Page Cache 页。</p><p>其次，cgroup v2 内存控制器根本没有 <code>swappiness</code> <a href="https://elixir.bootlin.com/linux/v5.15-rc1/source/include/linux/swap.h%23L706">参数</a>：</p><pre><code class="hljs C"><span class="hljs-meta">#<span class="hljs-keyword">ifdef</span> CONFIG_MEMCG</span><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">int</span> <span class="hljs-title function_">mem_cgroup_swappiness</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> mem_cgroup *memcg)</span>{<span class="hljs-comment">/* Cgroup2 doesn't have per-cgroup swappiness */</span><span class="hljs-keyword">if</span> (cgroup_subsys_on_dfl(memory_cgrp_subsys))<span class="hljs-keyword">return</span> vm_swappiness;<span class="hljs-comment">/* root ? */</span><span class="hljs-keyword">if</span> (mem_cgroup_disabled() || mem_cgroup_is_root(memcg))<span class="hljs-keyword">return</span> vm_swappiness;<span class="hljs-keyword">return</span> memcg-&gt;swappiness;</code></pre><p>相反，内核开发者决定大幅改变 swappiness 逻辑。你可以通过在 <a href="https://github.com/torvalds/linux/blame/a3fa7a101dcff93791d1b1bdb3affcad1410c8c1/mm/vmscan.c#L2574"><code>mm/vmscan.c</code> 上运行  <code>git blame</code>  并搜索 <code>get_scan_count()</code> 函数来检查它</a>。</p><p>例如，在撰写本文时，无论 <code>vm.swappiness</code> 如何，只要 <a href="https://github.com/torvalds/linux/blob/a3fa7a101dcff93791d1b1bdb3affcad1410c8c1/mm/vmscan.c#L2623-L2630">inactive 的 LRU Page Cache 列表中有足够的页</a>，匿名内存都不会被触及：</p><pre><code class="hljs C"><span class="hljs-comment">/*</span><span class="hljs-comment"> * If there is enough inactive page cache, we do not reclaim</span><span class="hljs-comment"> * anything from the anonymous working right now.</span><span class="hljs-comment"> */</span><span class="hljs-keyword">if</span> (sc-&gt;cache_trim_mode) {scan_balance = SCAN_FILE;<span class="hljs-keyword">goto</span> out;}</code></pre><p>在关于从哪个 LRU 回收以及回收什么的决策的完整逻辑，您可以在 <a href="https://github.com/torvalds/linux/blob/master/mm/vmscan.c"><code>mm/vmscan.c</code>  的 <code>get_scan_count()</code> 函数</a> 中找到。</p><p>另外，请查看 <code>memory.swap.high</code> 和 <code>memory.swap.max</code> cgroup v2 设置。如果您想纠正 <code>vm.swappiness</code> 逻辑以适应您的 cgroup 和负载模式，您可以控制它们。</p><p>处理交换和 Page Cache 时，另一个值得注意的问题是换入/出过程中的 IO 负载。如果有 IO 压力，则很容易达到 IO 限制，例如，降低 Page Cache 的写回性能。</p><h3 id="通过-proc-pid-pagemap-理解内存回收过程"><a href="#通过-proc-pid-pagemap-理解内存回收过程" class="headerlink" title="通过 /proc/pid/pagemap 理解内存回收过程"></a>通过 <code>/proc/pid/pagemap</code> 理解内存回收过程</h3><p>现在是时候探讨初级故障排查技术了。</p><p>有一个 <code>/proc/PID/pagemap</code> 文件，包含 PID 的页表信息。<a href="https://en.wikipedia.org/wiki/Page_table">页表</a>，从根本上讲，是内核在页框（存储在 RAM 中的实际物理内存页）和进程的虚拟页之间的内部映射。Linux 系统中的每个进程都有自己的虚拟内存地址空间，该空间完全独立于其他进程和物理内存地址。</p><p><code>/proc/PID/pagemap</code> 相关的文件的完整的文档，包括数据格式和读取方式，可以在 <a href="https://www.kernel.org/doc/Documentation/vm/pagemap.txt">内核文档文件夹</a> 中找到。我强烈建议您在继续阅读以下部分之前先阅读它。</p><h4 id="page-types-内核页工具"><a href="#page-types-内核页工具" class="headerlink" title="page-types 内核页工具"></a><code>page-types</code> 内核页工具</h4><p><code>page-types</code> 是每个内核内存黑客的瑞士军刀。其源代码随 Linux 内核源代码 <a href="https://github.com/torvalds/linux/blob/master/tools/vm/page-types.c">tools/vm/page-types.c</a> 一起提供。</p><p>如果你没有在第一章节安装它：</p><pre><code class="hljs bash">$ wget https://github.com/torvalds/linux/archive/refs/tags/v5.13.tar.gz$ tar -xzf ./v5.13.tar.gz$ <span class="hljs-built_in">cd</span> v5.13/vm/tools$ make</code></pre><p>现在让我们用它来理解，内核将我们的测试文件 <code>/var/tmp/file1.db</code> 的多少页放在了 Active 和 Inactive LRU 列表中：</p><pre><code class="hljs bash">$ sudo ./page-types --raw -Cl -f /var/tmp/file1.db</code></pre><pre><code class="hljs bash">foffset cgroup  offset  len     flags/var/tmp/file1.db       Inode: 133367   Size: 134217728 (32768 pages)Modify: Mon Aug 30 13:14:19 2021 (13892 seconds ago)Access: Mon Aug 30 13:15:47 2021 (13804 seconds ago)10689   @1749   21fa    1       ___U_lA_______________________P____f_____F_1...18965   @1749   24d37   1       ___U_l________________________P____f_____F_118966   @1749   28874   1       ___U_l________________________P____f_____F_118967   @1749   10273   1       ___U_l________________________P____f_____F_118968   @1749   1f6ad   1       ___U_l________________________P____f_____F_1             flags      page-count       MB  symbolic-flags                     long-symbolic-flags0xa000010800000028             105        0  ___U_l________________________P____f_____F_1       uptodate,lru,private,softdirty,file,mmap_exclusive0xa00001080000002c              16        0  __RU_l________________________P____f_____F_1       referenced,uptodate,lru,private,softdirty,file,mmap_exclusive0xa000010800000068             820        3  ___U_lA_______________________P____f_____F_1       uptodate,lru,active,private,softdirty,file,mmap_exclusive0xa001010800000068               1        0  ___U_lA_______________________P____f_I___F_1       uptodate,lru,active,private,softdirty,readahead,file,mmap_exclusive0xa00001080000006c              16        0  __RU_lA_______________________P____f_____F_1       referenced,uptodate,lru,active,private,softdirty,file,mmap_exclusive             total             958        3</code></pre><p>输出包含两部分：第一部分提供每页信息，第二部分汇总所有具有相同标志的页并计算摘要。为了回答 LRU 问题，我们需要从输出中获得 <code>A</code> 和 <code>l</code> 标志，正如您所猜想的那样，它们代表 “active” 和 “inactive” 列表。</p><p>如您所见，我们有：</p><ul><li><code>105 + 16 = 121 pages</code> 或者 <code>121 * 4096 = 484 KiB</code> 在 inactive LRU 列表中。</li><li><code>820 + 1 + 16 = 837 pages</code> 或者 <code>837 * 4096 = 3.2 MiB</code> 在 active LRU 列表中。</li></ul><h4 id="编写-Page-Cache-LRU-监控工具"><a href="#编写-Page-Cache-LRU-监控工具" class="headerlink" title="编写 Page Cache LRU 监控工具"></a>编写 Page Cache LRU 监控工具</h4><p><code>page-types</code> 是一款非常有用的初级调试和调查工具，但其输出格式难以阅读和汇总。我之前承诺过我们会编写自己的 <code>vmtouch</code>，所以现在我们正在实现它。我们的替代版本将提供更多关于页的信息。它不仅会显示 Page Cache 中有多少页，还会显示其中有多少页在 active 和 inactive LRU 列表中。</p><p>为此，我们需要两个内核文件：<a href="https://www.kernel.org/doc/Documentation/vm/pagemap.txt"><code>/proc/PID/pagemap</code>和<code>/proc/kpageflags</code></a>。</p><p>您可以在 <a href="https://github.com/brk0v/sre-page-cache-article/tree/main/lru">github repo</a> 中找到完整的代码，但在这里，我想重点介绍几个重要时刻：</p><pre><code class="hljs bash">    ...①  err = syscall.Madvise(mm, syscall.MADV_RANDOM)    ...②  ret, _, err := syscall.Syscall(syscall.SYS_MINCORE, mmPtr, sizePtr, cachedPtr)     <span class="hljs-keyword">for</span> i, p := range cached {③      <span class="hljs-keyword">if</span> p%2 == 1 { ④           _ = *(*int)(unsafe.Pointer(mmPtr + uintptr(pageSize*int64(i))))        }    }   ...        ⑤  err = syscall.Madvise(mm, syscall.MADV_SEQUENTIAL)    ...</code></pre><ul><li>① – 在这里，我们需要禁用目标文件的预读逻辑，以防止我们的工具将不需要的数据加载到 Page Cache 中；</li><li>② – 使用 <code>mincore()</code> 系统调用获取 Page Cache 中的页向量；</li><li>③ – 在这里，我们检查页是否在 Page Cache 中；</li><li>④ – 如果 Page Cache 包含一个页，我们需要通过引用该页来更新相应进程的页表条目。我们的工具必须这样做才能使用 <code>/proc/pid/pagemap</code>。否则 <code>/proc/pid/pagemap</code> 文件将不包含目标文件页及其标志。</li><li>⑤ – 在这里，我们关闭了引用位的收集。这是由于内核回收逻辑的需要。我们的工具读取内存，因此影响内核 LRU 列表。通过使用 <code>madvise()</code> 与 <code>MADV_SEQUENTIAL</code>，我们通知 Linux 内核忽略我们的操作。</li></ul><p>让我们测试一下我们的工具。我们需要 2 个终端。在第一个终端中，使用 <code>watch</code>( <code>man 1 watch</code>) 启动我们的工具，以每 100 毫秒一次，无限循环运行我们的工具：</p><pre><code class="hljs bash">watch -n 0.1 <span class="hljs-string">'sudo go run ./lru.go'</span></code></pre><p>在第二个终端中，我们使用 dd<code> ( </code>man 1 dd` ) 读取文件：</p><pre><code class="hljs bash"><span class="hljs-built_in">dd</span> <span class="hljs-keyword">if</span>=/var/tmp/file1.db of=/dev/null</code></pre><p>您应该看到的演示：</p><p><a href="https://asciinema.org/a/a6Ox5TnM6R8WiwfxlvKUM1hys" target="_blank"><img src="https://asciinema.org/a/a6Ox5TnM6R8WiwfxlvKUM1hys.svg"></a></p><p>使用上述方法，您现在可以执行初级 Page Cache 调查。</p><h2 id="关于-mmap-文件访问的更多信息"><a href="#关于-mmap-文件访问的更多信息" class="headerlink" title="关于 mmap() 文件访问的更多信息"></a>关于 <code>mmap()</code> 文件访问的更多信息</h2><p>在开始 cgroup 章节之前，我将展示如何利用内存和 IO 限制来控制 Page Cache 驱逐并提高服务的可靠性，我想更深入地研究一下 <code>mmap()</code> 系统调用。我们需要了解底层发生了什么，并进一步了解 <code>mmap()</code> 读写过程。</p><h3 id="mmap-概述"><a href="#mmap-概述" class="headerlink" title="mmap() 概述"></a><code>mmap()</code> 概述</h3><p>内存映射是 Linux 系统最有趣的功能之一。其特性之一是，软件开发者可以透明地处理文件，即使文件的大小超过系统的实际物理内存。在下图中，您可以看到进程的 <a href="https://en.wikipedia.org/wiki/Virtual_memory">虚拟内存</a> 是什么样子。每个进程都有自己的 <code>mmap()</code> 映射文件的区域。</p><p><img src="/images/linux-page-cache-minibook-cn/linux%20page%20cache%20minibook-20241211233133-5.png"></p><p>我这里不触及的的是，在你的软件中是否使用 <code>mmap()</code> 或文件系统调用，例如 <code>read()</code> 和 <code>write()</code>。哪种方法更好、更快或更安全超出了本文的讨论范围。但你确实需要了解如何获取 <code>mmap()</code> 统计数据，因为几乎所有的 Page Cache 用户空间工具都使用它。</p><p>让我们使用 <code>mmap()</code> 再写一个脚本。它打印进程的 PID，映射测试文件并休眠。休眠时间应该足以用该进程试验。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> mmap<span class="hljs-keyword">import</span> os<span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> sleep<span class="hljs-built_in">print</span>(<span class="hljs-string">"pid:"</span>, os.getpid())<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"rb"</span>) <span class="hljs-keyword">as</span> f:    <span class="hljs-keyword">with</span> mmap.mmap(f.fileno(), <span class="hljs-number">0</span>, prot=mmap.PROT_READ) <span class="hljs-keyword">as</span> mm:f        sleep(<span class="hljs-number">10000</span>)</code></pre><p>在一个终端窗口中运行它，然后在另一个终端窗口中， 使用脚本的 PID 运行 <code>pmap -x PID</code>。</p><pre><code class="hljs bash">pmap -x 369029 | less</code></pre><p><code>369029</code> 是我的 PID。</p><p> <code>pmap</code> 的输出展示了进程的所有连续虚拟内存区域 (VMA 或 <a href="https://elixir.bootlin.com/linux/v5.14.1/source/include/linux/mm_types.h#L311">struct vm_area_struct</a>)。我们可以确定 mmaped 测试文件 <code>file1.db</code> 的虚拟地址。在我的例子中：</p><pre><code class="hljs bash">Address           Kbytes     RSS   Dirty Mode  Mapping...00007f705cc12000  131072       0       0 r--s- file1.db</code></pre><p>我们可以看到，该文件有 0 个脏页（它仅显示此进程的脏页）。该 <code>RSS</code> 列等于 0，这告诉我们进程已引用了多少 KiB 内存。顺便说一句，这个 0 并不意味着 Page Cache 中没有该文件的页。这意味着我们的进程尚未访问任何页。</p><blockquote><p><strong>注意</strong></p><p><code>pmap</code> 可以使用 <code>-XX</code> 显示更详细的输出。如果没有 <code>-XX</code>，它使用 <code>/proc/pid/maps</code>，但对于扩展模式，它显示来自 <code>/proc/pid/smaps</code> 的统计信息。更多信息可以在 <code>man 5 proc</code> 和 <a href="https://www.kernel.org/doc/Documentation/filesystems/proc.rst">内核文档 filesystems/proc.rst</a> 中找到。</p></blockquote><p>因此，对于 SRE 而言，<code>mmap()</code> 最令人兴奋的部分是它如何在访问和写入时透明地加载数据。我将在后续章节中展示这一切。</p><h3 id="什么是缺页中断？"><a href="#什么是缺页中断？" class="headerlink" title="什么是缺页中断？"></a>什么是缺页中断？</h3><p>在开始讨论文件工具之前，我们需要了解缺页中断的概念。一般来说，缺页中断是 CPU 与 Linux 内核及其内存子系统进行通信的机制。缺页中断是虚拟内存概念和 <a href="https://en.wikipedia.org/wiki/Demand_paging">请求分页</a> 的组成部分。简而言之，内核通常不会在 <code>mmap()</code> 或 <code>malloc()</code> 内存请求完成后立即分配物理内存。相反，内核会在进程的 <a href="https://en.wikipedia.org/wiki/Page_table">页表结构</a> 中创建一些记录，并将其用作内存承诺的存储。此外，页表还包含每个页的额外信息，例如内存权限和页标志（我们已经看到了其中一些：LRU 标志、脏标志等）。</p><p>从第 2 章中的示例可以看出，为了在任何位置读取映射的文件，与文件操作不同，代码不需要执行任何查找 ( <code>man 2 lseek</code>)。我们可以从映射区域的任何位置开始读取或写入。因此，当应用程序想要访问页时，如果目标页尚未加载到 Page Cache 中，或者 Page Cache 中的页与进程的页表之间没有连接，则可能会发生缺页中断。</p><p>有两种对我们有用的缺页中断类型：<strong>次要（minor）</strong> 和 <strong>主要</strong>。次要缺页中断基本上意味着为了满足进程的内存访问，不会有任何磁盘访问。另一方面，主要缺页中断意味着将有磁盘 IO 操作。</p><p>例如，如果我们使用 <code>dd</code> 加载文件一半数据到 Page Cache 中，然后从程序中使用 <code>mmap()</code> 访问前半部分，就会触发次要缺页中断。内核不需要访问磁盘，因为这些页已经加载到 Page Cache 中。内核只需要使用进程的页表条目引用这些已加载的页。但是，如果进程尝试在相同的映射区域中读取文件的后半部分，内核将不得不访问磁盘以加载页，系统将生成主要缺页中断。</p><p>如果您想获得有关请求分页、Linux 内核和系统内部的更多信息，请观看嵌入式 Linux Conf 的 <a href="https://www.youtube.com/watch?v=7aONIVSXiJ8">“Linux 内存管理简介”</a> 视频。</p><p>我们来做一个实验，写一个对文件进行不定式随机读取的脚本：</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> mmap<span class="hljs-keyword">import</span> os<span class="hljs-keyword">from</span> random <span class="hljs-keyword">import</span> randint<span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> sleep<span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(<span class="hljs-string">"/var/tmp/file1.db"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:    fd = f.fileno()    size = os.fstat(fd).st_size    <span class="hljs-keyword">with</span> mmap.mmap(fd, <span class="hljs-number">0</span>, prot=mmap.PROT_READ) <span class="hljs-keyword">as</span> mm:        <span class="hljs-keyword">try</span>:            <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:                pos = randint(<span class="hljs-number">0</span>, size-<span class="hljs-number">4</span>)                <span class="hljs-built_in">print</span>(mm[pos:pos+<span class="hljs-number">4</span>])                sleep(<span class="hljs-number">0.05</span>)        <span class="hljs-keyword">except</span> KeyboardInterrupt:            <span class="hljs-keyword">pass</span></code></pre><p>现在我们需要 3 个终端窗口。第一个：</p><pre><code class="hljs bash">$ sar -B 1</code></pre><p>它显示每秒的系统内存统计信息，包括缺页中断。</p><p>第二个是 <code>perf trace</code>：</p><pre><code class="hljs bash">$ sudo perf trace -F maj --no-syscalls</code></pre><p>显示主要缺页中断及其对应的文件路径。</p><p>最后，在第 3 个终端窗口中，启动上述 python 脚本：</p><pre><code class="hljs bash">$ python3 ./mmap_random_read.py</code></pre><p>输出应该接近以下内容：</p><pre><code class="hljs bash">$ sar -B 1</code></pre><pre><code class="hljs bash">....                                  LOOK HERE                                      ⬇      ⬇05:45:55 PM  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff  05:45:56 PM   8164.00      0.00     39.00      4.00      5.00      0.00      0.00      0.00      0.00  05:45:57 PM   2604.00      0.00     20.00      1.00      1.00      0.00      0.00      0.00      0.00  05:45:59 PM   5600.00      0.00     22.00      3.00      2.00      0.00      0.00      0.00      0.00...</code></pre><p>查看 <code>fault/s</code> 和 <code>majflt/s</code> 字段。它们显示了我刚刚解释的内容。</p><p>通过 <code>perf trace</code>，我们可以获取发生主要缺页中断的文件的内部信息：</p><pre><code class="hljs bash">$ sudo perf trace -F maj --no-syscalls</code></pre><pre><code class="hljs bash">...SCROLL ➡                                                                                     LOOK HERE                                                                                                 ⬇                                                                                       5278.737 ( 0.000 ms): python3/64915 majfault [__memmove_avx_unaligned_erms+0xab] =&gt; /var/tmp/file1.db@0x2aeffb6 (d.)  5329.946 ( 0.000 ms): python3/64915 majfault [__memmove_avx_unaligned_erms+0xab] =&gt; /var/tmp/file1.db@0x539b6d9 (d.)  5383.701 ( 0.000 ms): python3/64915 majfault [__memmove_avx_unaligned_erms+0xab] =&gt; /var/tmp/file1.db@0xb3dbc7 (d.)  5434.786 ( 0.000 ms): python3/64915 majfault [__memmove_avx_unaligned_erms+0xab] =&gt; /var/tmp/file1.db@0x18f7c4f (d.)  ...</code></pre><p>cgroup 也有关于每个 cgroup 的缺页中断的统计信息：</p><pre><code class="hljs bash">$ grep fault /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope/memory.stat</code></pre><pre><code class="hljs bash">...pgfault 53358pgmajfault 13...</code></pre><h3 id="微妙的-MADV-DONT-NEED-mmap-特性"><a href="#微妙的-MADV-DONT-NEED-mmap-特性" class="headerlink" title="微妙的 MADV_DONT_NEED mmap() 特性"></a>微妙的 <code>MADV_DONT_NEED</code> <code>mmap()</code> 特性</h3><p>现在我们再做一次实验。停止所有脚本并清除所有缓存：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">sync</span>; <span class="hljs-built_in">echo</span> 3 | sudo <span class="hljs-built_in">tee</span> /proc/sys/vm/drop_caches</code></pre><p>重启脚本，进行无限读取，并开始监控进程的每个内存区域的使用情况：</p><pre><code class="hljs bash">watch -n 0.1 <span class="hljs-string">"grep 'file1' /proc/<span class="hljs-variable">$pid</span>/smaps -A 24"</span></code></pre><p>现在您可以看到文件的映射区域及其信息。引用字段应该在增长。</p><p>在另一个窗口中，尝试使用 <code>vmtouch</code> 命令驱逐页：</p><pre><code class="hljs bash">vmtouch -e /var/tmp/file1.db</code></pre><p>请注意，<code>smaps</code> 输出中的统计数据并没有完全下降。运行 <code>vmtouch -e</code> 命令时，<code>smaps</code> 应该会显示内存使用量有所下降。问题是，发生了什么？为什么当我们通过设置 <code>FADVISE_DONT_NEED</code> 标志，明确要求内核驱逐文件页时，其中一些页仍然存在于 Page Cache 中？</p><p>答案有点令人困惑，但理解它非常重要。如果 Linux 内核没有内存压力问题，它为什么要从 Page Cache 中删除页？程序将来很有可能需要它们。但是，如果您作为软件开发人员确定这些页是无用的，则有 <code>madvise()</code> 和 <code>MADV_DONT_NEED</code> 标志可以使用。它通知内核可以从相应的页表中删除这些页，随后的 <code>vmtouch -e</code> 调用将成功地从 Page Cache 中移除文件数据。</p><p>如果出现内存压力情况，内核将开始从非活动 LRU 列表中回收内存。这意味着如果这些页适合回收，内核最终可以删除它们。</p><h2 id="Cgroup-v2-和-Page-Cache"><a href="#Cgroup-v2-和-Page-Cache" class="headerlink" title="Cgroup v2 和 Page Cache"></a>Cgroup v2 和 Page Cache</h2><p>cgroup 子系统是公平分配和限制系统资源的方法。它以层次结构组织所有数据，其中叶节点依赖于其父节点并继承其设置。此外，cgroup 还提供了许多有用的资源计数器和统计数据。</p><p>控制组无处不在。即使您可能没有明确使用它们，所有现代的 GNU/Linux 发行版默认都已经启用了它们，并且已经集成到了 <code>systemd</code> 中。这意味着现代 Linux 系统中的每个服务都在自己的 cgroup 下运行。</p><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>在本系列文章中，我们已经多次提到了 cgroup 子系统，但现在让我们更深入地了解一下整体情况。cgroup 在理解 Page Cache 使用情况方面起着至关重要的作用。它还通过提供详细的统计数据，来帮助调试问题并更好地配置软件。如前所述，LRU 列表使用 cgroup 内存限制来做出驱逐决定并确定 LRU 列表的长度。</p><p>在 <a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html">cgroup v2</a> 中，另一个重要主题是正确跟踪 Page Cache IO 写回的方式，而之前的 v1 版本无法实现这一点。v1 无法理解哪个内存 cgroup 会生成磁盘 IOPS，因此会错误地跟踪和限制磁盘操作。幸运的是，新的 v2 版本修复了这些问题。它已经提供了许多新功能来帮助 Page Cache 写回。</p><p>找出所有 cgroup 及其限制的最简单方法是访问 <code>/sys/fs/cgroup</code>。但您可以使用更方便的方法来获取此类信息：</p><ul><li><code>systemd-cgls</code> 和 <code>systemd-top</code> 以了解 cgroups <code>systemd</code> 包含的内容；</li><li><code>below</code>，<code>top</code> 类似 cgroups 的工具 <a href="https://github.com/facebookincubator/below">https://github.com/facebookincubator/below</a></li></ul><h3 id="内存-cgroup-文件"><a href="#内存-cgroup-文件" class="headerlink" title="内存 cgroup 文件"></a>内存 cgroup 文件</h3><p>现在我们从 Page Cache 的角度来回顾一下 cgroup 内存控制器中最重要的部分。</p><ol><li><code>memory.current</code> – 显示 cgroup 及其后代当前使用的总内存量。当然，它包括 Page Cache 大小。</li></ol><blockquote><p><strong>注意</strong></p><p>您可能很想使用这个值来设置您的 cgroup/容器内存限制，但是请等待下一章。</p></blockquote><ol start="2"><li><code>memory.stat</code> – 显示了很多内存计数器，对我们来说最重要的可以通过 <code>file</code> 关键字进行过滤：</li></ol><pre><code class="hljs bash">$ grep file /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope/memory.statfile 19804160                  ❶               file_mapped 0                  ❷file_dirty 0                   ❸file_writeback 0               ❹inactive_file 6160384          ❺active_file 13643776           ❺workingset_refault_file 0      ❻workingset_activate_file 0     ❻workingset_restore_file 0      ❻</code></pre><p>在此处</p><ul><li>❶ <code>file</code> – Page Cache 的大小；</li><li>❷ <code>file_mapped</code> – 使用 <code>mmap()</code> 的映射文件内存大小；</li><li>❸ <code>file_dirty</code> – 脏页大小；</li><li>❹ <code>file_writeback</code> – 目前正在刷新多少数据；</li><li>❺<code>inactive_file</code> 和 <code> active_file</code> – LRU 列表的大小;</li><li>❻ <code>workingset_refault_file</code>、<code>workingset_activate_file</code> 和 <code>workingset_restore_file</code> – 指标，以便更好地理解内存抖动和二次缺页中断（refault）逻辑。</li></ul><ol start="3"><li><code>memory.numa_stat</code> – 显示上述统计数据，但针对每个 <a href="https://en.wikipedia.org/wiki/Non-uniform_memory_access">NUMA 节点</a>。</li><li><code>memory.min</code> , <code>memory.low</code> , <code>memory.high</code> 和 <code>memory.max</code> – cgroup 限制。我不想重复 <a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html%23usage-guidelines">cgroup v2 文档</a>，建议您先阅读它。但您需要记住的是，使用硬性限制 <code>max</code> 或 <code>min</code> 并不是您的应用程序和系统的最佳策略。您可以选择的更好方法是仅设置 <code>low</code> 和/或 <code>high</code> 限制，使其更接近您认为的应用程序工作集大小。我们将在下一节中讨论测量和预测。</li><li><code>memory.events</code> – 显示 cgroup 触及上述限制的次数：</li></ol><pre><code class="hljs bash">memory.eventslow 0high 0max 0oom 0oom_kill 0</code></pre><ol start="6"><li><code>memory.pressure</code> – 此文件包含压力阻塞信息 (PSI，Pressure Stall Information)。它通过测量由于内存不足而损失的 CPU 时间，来显示 cgroup 内存的总体健康状况。此文件是理解 cgroup 中的回收过程以及 Page Cache 的关键。让我们更详细地讨论一下 PSI。</li></ol><h3 id="压力阻塞信息-PSI"><a href="#压力阻塞信息-PSI" class="headerlink" title="压力阻塞信息 (PSI)"></a>压力阻塞信息 (PSI)</h3><p>在 PSI 出现之前，很难判断系统和/或 cgroup 是否存在资源竞争；cgroup 限制是过度承诺还是配置不足。如果 cgroup 的限制可以设置得更低，那么它的阈值在哪里？PSI 功能可以缓解这些困惑，不仅让我们能够实时获取这些信息，还让我们能够设置用户空间触发器并获取通知，以最大限度地提高硬件利用率，而不会降低服务质量和带来 OOM 风险。</p><p>PSI 适用于内存、CPU 和 IO 控制器。例如，内存的输出：</p><pre><code class="hljs bash">some avg10=0.00 avg60=0.00 avg300=0.00 total=0full avg10=0.00 avg60=0.00 avg300=0.00 total=0</code></pre><p>在此处</p><ul><li><code>some</code> – 表示在 10、60 和 300 秒内，至少有一项任务在内存中阻塞了一定百分比的挂机时间。“总计”字段显示以微秒为单位的绝对值，以显示峰值；</li><li><code>full</code> – 含义相同，但适用于 cgroup 中的所有任务。此指标可以很好地指示问题，通常意味着资源配置不足或软件设置错误。</li></ul><blockquote><p><strong>示例</strong></p><p><a href="https://man7.org/linux/man-pages/man8/systemd-oomd.service.8.html"><code>systemd-oom</code></a> 守护进程，作为现代 GNU/Linux 系统的一部分，使用 PSI 比内核的 OOM 更主动地识别内存稀缺并找到要终止的目标。</p></blockquote><p>我强烈建议阅读原始的 <a href="https://www.kernel.org/doc/html/latest/accounting/psi.html">PSI 文档</a>。</p><h3 id="写回和-IO"><a href="#写回和-IO" class="headerlink" title="写回和 IO"></a>写回和 IO</h3><p>cgroup v2 实现的最重要特性之一是可以跟踪、观察和限制每个 cgroup 的 Page Cache 异步写回。现在，内核写回过程可以识别要使用哪个 cgroup IO 限制来将脏页持久保存到磁盘。</p><p>但同样重要的是，它也能在另一个方面发挥作用。如果一个 cgroup 遇到内存压力，并试图通过刷新其脏页来回收一些页，它将使用自己的 IO 限制，不会损害其他 cgroup。因此，内存压力转化为磁盘 IO，如果有大量写入，最终转化为 cgroup 的磁盘压力。两个控制器都有 PSI 文件，应该用于主动管理和调整软件设置。</p><p>为了控制脏页刷新频率，Linux 内核有几个 <a href="https://www.kernel.org/doc/Documentation/sysctl/vm.txt"><code>sysctl</code> 参数</a>。如果你愿意，你可以让后台写回过程更积极或更消极：</p><pre><code class="hljs bash">$ sudo sysctl -a | grep dirtyvm.dirty_background_bytes = 0  vm.dirty_background_ratio = 10  vm.dirty_bytes = 0  vm.dirty_expire_centisecs = 3000  vm.dirty_ratio = 20  vm.dirty_writeback_centisecs = 500  vm.dirtytime_expire_seconds = 43200</code></pre><p>上述某些方法也适用于 cgroup。内核选择并应用最先到达的整个系统或 cgroup 的项。</p><p>cgroup v2 还带来了新的 IO 控制器：<code>io.cost</code> 和 <code>io.latency</code>。它们提供了两种不同的方法来限制和保证磁盘操作。请阅读 cgroup v2 文档以获取更多详细信息和区别。但我想说，如果您的设置并不复杂，那么从侵入性较小的方法 <code>io.latency</code> 开始是有意义的。</p><p>与内存控制器一样，内核也提供了一堆文件来控制和观察 IO：</p><ul><li><code>io.stat</code> – 包含每个设备数据的统计文件；</li><li><code>io.latency </code>– 延迟目标时间（单位：微秒）；</li><li><code>io.pressure</code> – PSI 文件；</li><li><code>io.weight</code> – 如果选择了 <code>io.cost</code> 的目标权重；</li><li><code>io.cost.qos</code> 以及 <code>io.cost.model</code> – <code>io.cost</code> cgroup 控制器的配置文件。</li></ul><h3 id="内存和-IO-cgroup-所有权"><a href="#内存和-IO-cgroup-所有权" class="headerlink" title="内存和 IO cgroup 所有权"></a>内存和 IO cgroup 所有权</h3><p>多个 cgroups 中的几个进程显然可以处理相同的文件。例如， <code>cgroup1</code> 可以打开并读取文件的前 10 KiB，稍后，另一个 <code>cgroup2</code> 可以向同一文件的末尾追加 2 KiB 并读取前 4 KiB。问题在于，内核将使用哪个进程的内存和 IO 限制？</p><p>内存所有权（包括 Page Cache）的逻辑是基于每个页构建的。页的所有权在首次访问（缺页中断）时确定，并且在此页被完全回收和驱逐之前，不会切换到任何其他 cgroup。所有权一词意味着这些页将用于计算 cgroup Page Cache 使用量，并将被纳入所有统计数据中。</p><p>例如，<code>cgroup1</code> 是前 10KiB 的所有者，而 <code>cgroup2</code> – 是最后 2KiB 的所有者。无论 <code>cgroup1</code> 对文件做什么，甚至关闭文件，只要 <code>cgroup2</code> 与文件的前 4KiB 进行交互， <code>cgroup1</code> 就会一直保留前 4KiB（而不是全部 10KiB）的所有权。在这种情况下，内核会将页保存在 Page Cache 中，并相应地不断更新 LRU 列表。</p><p>对于 cgroup IO，所有权按 inode 计算所有权。因此，对于我们的示例，<code>cgroup2</code> 拥有文件的所有写回操作。在首次写回时，inode 被分配给 cgroup，但与内存所有权逻辑不同，如果内核注意到另一个 cgroup 生成的脏页更多，IO 所有权可能会迁移到另一个 cgroup。</p><p>为了排除内存所有权问题，我们应该使用一对 <code>procfs</code> 文件：<code>/proc/pid/pagemap</code> 和 <code>/proc/kpagecgroup</code>。<code>page-type</code> 工具支持显示每页 cgroup 信息，但很难将其用于文件目录并获得格式良好的输出。这就是为什么我编写了自己的 <a href="https://github.com/brk0v/cgtouch"><code>cgtouch</code></a> 工具来排查 cgroup 内存所有权问题的原因。</p><pre><code class="hljs bash">$ sudo go run ./main.go /var/tmp/ -v</code></pre><pre><code class="hljs bash">/var/tmp/file1.dbcgroup inode    percent       pages        path           -      85.9%       28161        not charged        1781      14.1%        4608        /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope--/var/tmp/ubuntu-21.04-live-server-amd64.isocgroup inode    percent       pages        pat           -       0.0%           0        not charged        2453     100.0%       38032        /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u10.service--         Files: 2   Directories: 7Resident Pages: 42640/70801 166.6M/276.6M 60.2%cgroup inode    percent       pages        path           -      39.8%       28161        not charged        1781       6.5%        4608        /sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope        2453      53.7%       38032        /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u10.service</code></pre><h3 id="安全的临时任务"><a href="#安全的临时任务" class="headerlink" title="安全的临时任务"></a>安全的临时任务</h3><p>假设我们需要运行 <code>wget</code> 命令或通过调用配置管理系统（例如 <code>saltstack</code>）手动安装某些软件包。这两项任务的磁盘 I/O 都可能非常繁重。为了安全地运行它们并且不与任何生产负载交互，我们不应该在根 cgroup 或当前终端 cgroup 中运行它们，因为它们通常没有任何限制。所以我们需要一个具有一些限制的新 cgroup。手动为您的任务创建一个 cgroup，并手动配置每个临时任务会非常繁琐和麻烦。但幸运的是，我们不必这样做，所以所有现代 GNU/Linux 发行版都内置了 <code>systemd</code>，带有开箱即用的 cgroup v2。<code>systemd-run</code> 以及 <code>systemd</code> 许多其他很酷的功能使我们的生活更轻松，并节省了大量时间。</p><p>例如，<code>wget</code> 任务可以按以下方式运行：</p><pre><code class="hljs bash">systemd-run --user -P -t -G --<span class="hljs-built_in">wait</span> -p MemoryMax=12M wget http://ubuntu.ipacct.com/releases/21.04/ubuntu-21.04-live-server-amd64.isoRunning as unit: run-u2.service                         ⬅  LOOK HEREPress ^] three <span class="hljs-built_in">times</span> within 1s to disconnect TTY.--2021-09-11 19:53:33--  http://ubuntu.ipacct.com/releases/21.04/ubuntu-21.04-live-server-amd64.isoResolving ubuntu.ipacct.com (ubuntu.ipacct.com)... 195.85.215.252, 2a01:9e40::252Connecting to ubuntu.ipacct.com (ubuntu.ipacct.com)|195.85.215.252|:80... connected.HTTP request sent, awaiting response... 200 OKLength: 1174243328 (1.1G) [application/octet-stream]Saving to: ‘ubuntu-21.04-live-server-amd64.iso.5’...</code></pre><p><code>run-u2.service</code> 是我的全新 cgroup，具有内存限制。我可以获取其指标：</p><pre><code class="hljs bash">$ find /sys/fs/cgroup/ -name run-u2.service/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u2.service</code></pre><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span>  /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u2.service/memory.pressuresome avg10=0.00 avg60=0.00 avg300=0.00 total=70234full avg10=0.00 avg60=0.00 avg300=0.00 total=69717</code></pre><pre><code class="hljs bash">$ grep file  /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u2.service/memory.statfile 11100160file_mapped 0file_dirty 77824file_writeback 0file_thp 0inactive_file 5455872active_file 5644288workingset_refault_file 982workingset_activate_file 0workingset_restore_file 0</code></pre><p>如您所见，我们有近 12MiB 的文件内存和一些二次缺页中断（refault）。</p><p>要了解 systemd 和 cgroup 的所有功能，请阅读其 <a href="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html">资源控制文档</a>。</p><h2 id="我的程序使用了多少内存或工作集大小的故事"><a href="#我的程序使用了多少内存或工作集大小的故事" class="headerlink" title="我的程序使用了多少内存或工作集大小的故事"></a>我的程序使用了多少内存或工作集大小的故事</h2><p>目前，在容器、自动扩展和按需云的世界中，理解服务在正常常规情况和接近软件极限的压力下的资源需求至关重要。但每当有人谈到内存使用量时，几乎立即就不清楚要测量什么和如何测量。RAM 是一种宝贵且通常昂贵的硬件类型。在某些情况下，它的延迟甚至比磁盘延迟更重要。因此，Linux 内核会尽可能地优化内存利用率，例如通过在进程之间共享相同的页。此外，Linux 内核还具有 Page Cache，以便通过将磁盘数据的子集存储在内存中来提高存储 IO 速度。Page Cache 不仅本质上执行隐式内存共享（通常会让用户感到困惑），而且还在后台主动异步地与存储一起工作。因此，Page Cache 为内存使用量估算表带来了更多的复杂性。</p><p>在本章中，我将演示一些方法，您可以使用它们来确定内存（以及 Page Cache）限制的初始值，并从一个不错的起点开始您的旅程。</p><h3 id="一切都关乎谁重要，或独一无二的集合大小的故事"><a href="#一切都关乎谁重要，或独一无二的集合大小的故事" class="headerlink" title="一切都关乎谁重要，或独一无二的集合大小的故事"></a>一切都关乎谁重要，或独一无二的集合大小的故事</h3><p>我听到过的关于内存和 Linux 的两个最常见的问题是：</p><ul><li>我所有的可用内存在哪里？</li><li>您/我/他们的应用程序/服务/数据库使用了多少内存？</li></ul><p>第一个问题的答案应该对读者显而易见（悄悄说 “Page Cache”）。但第二个问题要棘手得多。通常，人们认为 <code>top</code> 或 <code>ps</code> 输出的 <code>RSS</code> 列是评估内存利用率的良好起点。虽然这种说法在某些情况下可能是正确的，但它通常会导致对 Page Cache 重要性，及其对服务性能和可靠性的影响的误解。</p><p>让我们以著名的 <code>top</code>( <code>man 1 top</code>)<a href="https://github.com/mmalecki/procps/blob/master/top.c">工具</a> 为例，来调查它的内存消耗。它是用 C 语言编写，只做一件事，就是在循环中打印进程的状态。<code>top</code> 并不大量使用磁盘，因此也不使用 Page Cache。它不涉及网络。它的唯一目的是从 <code>procfs</code> 中读取数据，并以友好的格式显示给用户。所以它的工作集应该很容易理解，不是吗？</p><p>让我们在新的 cgroup 中启动 <code>top</code> 过程：</p><pre><code class="hljs bash">$ systemd-run --user -P -t -G --<span class="hljs-built_in">wait</span> top</code></pre><p>在另一个终端，让我们开始学习。从 <code>ps</code> 开始：</p><pre><code class="hljs bash">$ ps axu | grep topUSER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND...vagrant   611963  0.1  0.2  10836  4132 pts/4    Ss+  11:55   0:00 /usr/bin/top...                                  ⬆                                  LOOK HERE</code></pre><p>如上所示，根据 <code>ps</code> 输出， <code>top</code> 进程使用了大约 4MiB 的内存。</p><p>现在让我们从 <code>procfs</code> 及其 <a href="https://www.kernel.org/doc/Documentation/filesystems/proc.rst"><code>/proc/pid/smaps_rollup</code>文件</a> 获取更多详细信息，基本上是 <code>/proc/pid/smaps</code> 中所有内存区域的总和。对于我的 PID：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /proc/628011/smaps_rollup55df25e91000-7ffdef5f7000 ---p 00000000 00:00 0                          [rollup]Rss:                3956 kB  ⓵Pss:                1180 kB  ⓶Pss_Anon:            668 kBPss_File:            512 kB Pss_Shmem:             0 kBShared_Clean:       3048 kB  ⓷Shared_Dirty:          0 kB  ⓸Private_Clean:       240 kBPrivate_Dirty:       668 kBReferenced:         3956 kB  ⓹Anonymous:           668 kB  ⓺...</code></pre><p>我们主要关心以下几行：</p><ul><li>⓵ <code>RSS</code> – 一个众所周知的指标，正如我们在 <code>ps</code> 输出中看到的内容。</li><li>⓶ <code>PSS</code> – 代表进程的比例共享内存。这是一个人工内存指标，它应该能给你一些关于内存共享的洞察：</li></ul><blockquote><p>进程的“比例集大小”( <code>PSS</code>) 是其在内存中的页数，其中每个页除以共享它的进程数。因此，如果一个进程有 1000 个页完全属于自己，还有 1000 个页与另一个进程共享，则其 PSS 为 1500。</p></blockquote><ul><li>⓷ <code>Shared_Clean</code> – 是一个有趣的指标。正如我们之前假设的，我们的进程理论上不应该使用任何 Page Cache，但事实证明它确实使用了 Page Cache。正如您所见，它是内存使用的主要部分。如果您打开每区域的文件 <code>/proc/pid/smaps</code>，您可以找出原因是共享库。它们都是用 <code>mmap()</code> 打开的，并且驻留在 Page Cache 中。</li><li>⓸ <code>Shared_Dirty</code> – 如果我们的进程使用 <code>mmap()</code> 写入文件，则此行将显示未保存的脏 Page Cache 的数量。</li><li>⓹ <code>Referenced</code> - 表示进程迄今为止标记为引用或访问的内存量。我们在本 <code>mmap()</code> 部分提到过这个指标。如果没有内存压力，它应该接近 RSS。</li><li>⓺ <code>Anonymous</code> – 显示不属于任何文件的内存量。</li></ul><p>从上面我们可以看出，虽然 <code>top</code> 输出的 RSS 为 4MiB，但其大部分 RSS 都隐藏在 Page Cache 中。理论上，如果这些页在一段时间内处于非活动状态，内核可以将它们从内存中驱逐。</p><p>我们也来看看 cgroup 统计数据：</p><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /proc/628011/cgroup0::/user.slice/user-1000.slice/user@1000.service/app.slice/run-u2.service</code></pre><pre><code class="hljs bash">$ <span class="hljs-built_in">cat</span> /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u2.service/memory.statanon 770048file 0...file_mapped 0file_dirty 0file_writeback 0...inactive_anon 765952active_anon 4096inactive_file 0active_file 0...</code></pre><p>我们在 cgroup 中看<strong>不到</strong>任何文件内存。这是 cgroup 内存记账特性的另一个很好的例子。另一个 cgroup 已经计算了这些库。</p><p>为了完成并复查，让我们使用 <code>page-type</code> 工具：</p><pre><code class="hljs bash">$ sudo ./page-types --pid 628011 --raw             flags      page-count       MB  symbolic-flags                     long-symbolic-flags0x2000010100000800               1        0  ___________M_______________r_______f_____F__       mmap,reserved,softdirty,file0xa000010800000868              39        0  ___U_lA____M__________________P____f_____F_1       uptodate,lru,active,mmap,private,softdirty,file,mmap_exclusive0xa00001080000086c              21        0  __RU_lA____M__________________P____f_____F_1       referenced,uptodate,lru,active,mmap,private,softdirty,file,mmap_exclusive0x200001080000086c             830        3  __RU_lA____M__________________P____f_____F__       referenced,uptodate,lru,active,mmap,private,softdirty,file0x8000010000005828             187        0  ___U_l_____Ma_b____________________f_______1       uptodate,lru,mmap,anonymous,swapbacked,softdirty,mmap_exclusive0x800001000000586c               1        0  __RU_lA____Ma_b____________________f_______1       referenced,uptodate,lru,active,mmap,anonymous,swapbacked,softdirty,mmap_exclusive             total            1079        4</code></pre><p>我们可以看到，<code>top</code> 进程的内存包含文件 <code>mmap()</code> 区域，因此使用了 Page Cache。</p><p>现在让我们为我们的 <code>top</code> 进程获取一个唯一的内存集大小。进程的唯一内存集大小或 USS 是仅此目标进程使用的内存量。此内存可以是共享的，但如果没有其他进程使用它，它仍然归入 USS 中。</p><p>我们可以使用 <code>page-types</code> 的 <code>-N</code> 标志和一些 shell 魔法来计算进程的 USS：</p><pre><code class="hljs bash">$ sudo ../vm/page-types --pid 628011 --raw -M -l -N | awk <span class="hljs-string">'{print $2}'</span> | grep -E <span class="hljs-string">'^1$'</span> | <span class="hljs-built_in">wc</span> -l248</code></pre><p>上述表示该 <code>top</code> 进程的唯一集合大小（USS）是 <code>248 pages</code> 或者 <code>992 KiB</code>。</p><p>或者我们可以利用我们对 <code>/proc/pid/pagemap</code>、<code>/proc/kpagecount</code> 和 <code>/proc/pid/maps</code> 的知识，编写自己的工具来获取唯一集合大小。此类工具的完整代码可以在 <a href="https://github.com/brk0v/sre-page-cache-article/tree/main/uss">github repo</a> 中找到。</p><p>如果我们运行它，我们应该得到与 <code>page-type</code> 相同的输出：</p><pre><code class="hljs bash">$ sudo go run ./main.go 628011248</code></pre><p>既然我们了解了估计内存使用量有多么困难，以及 Page Cache 在这种计算中的重要性，我们准备向前迈出一大步，开始考虑具有更多活跃磁盘活动的软件。</p><h3 id="空闲页和工作集大小"><a href="#空闲页和工作集大小" class="headerlink" title="空闲页和工作集大小"></a>空闲页和工作集大小</h3><p>读到这里读者可能会对另一个内核文件感到好奇：<a href="https://www.kernel.org/doc/Documentation/vm/idle_page_tracking.txt"><code>/sys/kernel/mm/page_idle</code></a>。</p><p>您可以使用它来估计进程的工作集大小。主要思想是使用特殊空闲标志标记一些页，并在一段时间后检查有关工作数据集大小的差异假设。</p><p>您可以在 Brendan Gregg 的 <a href="https://github.com/brendangregg/wss">仓库</a> 中找到很棒的参考工具。</p><p>让我们为 <code>top</code> 进程运行它：</p><pre><code class="hljs bash">$ sudo ./wss-v1 628011 60 Watching PID 628011 page references during 60.00 seconds...Est(s)     Ref(MB) 60.117        2.00</code></pre><p>上述意味着，在 4MiB 的 RSS 数据中，该进程在 60 秒间隔内仅使用 2MiB。</p><p>欲了解更多信息，您还可以阅读这篇 <a href="https://lwn.net/Articles/642202/">LWN 文章</a>。</p><p>该方法的缺点如下：</p><ul><li>对于占用大量内存的进程来说，它可能会很慢；</li><li>所有测量都在用户空间进行，因此会消耗额外的 CPU；</li><li>它完全脱离了您的进程可能产生的写回压力。</li></ul><p>虽然这可能成为您的容器的合理起始限制，我将向您展示一种更好的方法，使用 cgroup 统计信息和压力阻塞信息 (PSI) 。</p><h3 id="使用压力阻塞信息（PSI）计算内存限制"><a href="#使用压力阻塞信息（PSI）计算内存限制" class="headerlink" title="使用压力阻塞信息（PSI）计算内存限制"></a>使用压力阻塞信息（PSI）计算内存限制</h3><p>正如系列中所见，我强调将所有服务分别运行在自己的 cgroups 中，并且精心配置限制是非常重要的。这通常会带来更好的服务性能以及更均匀、更正确地系统资源使用。</p><p>但仍然不清楚从哪里开始。选择哪个值？使用 <code>memory.current</code> 值好吗？还是使用唯一集合大小？还是使用空闲页标志来估计工作集大小？虽然所有这些方法在某些情况下可能都很有用，但我建议在一般情况下使用以下的 PSI 方法。</p><p>在继续使用 PSI 之前，关于 <code>memory.current</code> 还有一点需要注意。如果 cgroup 没有内存限制，并且系统有大量可用内存供进程使用，则 <code>memory.current</code> 只会显示应用程序到目前为止使用的所有内存（包括 Page Cache）。它可能包含应用程序运行时不需要的大量垃圾。例如，日志记录、不需要的库等。使用 <code>memory.current</code> 值作为内存限制会浪费系统资源，并且不会对您进行容量规划有帮助。</p><p>解决这个难题的现代方法是，使用 PSI 来了解 cgroup 如何对新的内存分配和 Page Cache 驱逐的反应。<a href="https://github.com/facebookincubator/senpai/blob/main/senpai.py"><code>senapi</code></a> 是一个简单的自动脚本，用于收集和解析 PSI 信息并调整 <code>memory.high</code>：</p><p>让我们用我的测试 MongoDB 安装进行实验。我有 2.6GiB 的数据：</p><pre><code class="hljs bash">$ sudo <span class="hljs-built_in">du</span> -hs /var/lib/mongodb/2.4G    /var/lib/mongodb/</code></pre><p>现在我需要生成一些随机读取查询。在 <code>mongosh</code> 中，我可以运行一个无限循环，并每 500 毫秒读取一条随机记录：</p><pre><code class="hljs javascript"><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {    <span class="hljs-title function_">printjson</span>(db.<span class="hljs-property">collection</span>.<span class="hljs-title function_">aggregate</span>([{ <span class="hljs-attr">$sample</span>: { <span class="hljs-attr">size</span>: <span class="hljs-number">1</span> } }]));     <span class="hljs-title function_">sleep</span>(<span class="hljs-number">500</span>); }</code></pre><p>在第二个终端窗口中，我使用带有 mongodb 服务 cgroup 启动 <code>senpai</code></p><pre><code class="hljs bash">sudo python senpai.py /sys/fs/cgroup/system.slice/mongodb.service2021-09-05 16:39:25 Configuration:2021-09-05 16:39:25   cgpath = /sys/fs/cgroup/system.slice/mongodb.service2021-09-05 16:39:25   min_size = 1048576002021-09-05 16:39:25   max_size = 1073741824002021-09-05 16:39:25   interval = 62021-09-05 16:39:25   pressure = 100002021-09-05 16:39:25   max_probe = 0.012021-09-05 16:39:25   max_backoff = 1.02021-09-05 16:39:25   coeff_probe = 102021-09-05 16:39:25   coeff_backoff = 202021-09-05 16:39:26 Resetting <span class="hljs-built_in">limit</span> to memory.current....2021-09-05 16:38:15 <span class="hljs-built_in">limit</span>=503.90M pressure=0.030000 time_to_probe= 1 total=1999415 delta=601 integral=33662021-09-05 16:38:16 <span class="hljs-built_in">limit</span>=503.90M pressure=0.030000 time_to_probe= 0 total=1999498 delta=83 integral=34492021-09-05 16:38:16   adjust: -0.0008406468912331542021-09-05 16:38:17 <span class="hljs-built_in">limit</span>=503.48M pressure=0.020000 time_to_probe= 5 total=2000010 delta=512 integral=5122021-09-05 16:38:18 <span class="hljs-built_in">limit</span>=503.48M pressure=0.020000 time_to_probe= 4 total=2001688 delta=1678 integral=21902021-09-05 16:38:19 <span class="hljs-built_in">limit</span>=503.48M pressure=0.020000 time_to_probe= 3 total=2004119 delta=2431 integral=46212021-09-05 16:38:20 <span class="hljs-built_in">limit</span>=503.48M pressure=0.020000 time_to_probe= 2 total=2006238 delta=2119 integral=67402021-09-05 16:38:21 <span class="hljs-built_in">limit</span>=503.48M pressure=0.010000 time_to_probe= 1 total=2006238 delta=0 integral=67402021-09-05 16:38:22 <span class="hljs-built_in">limit</span>=503.48M pressure=0.010000 time_to_probe= 0 total=2006405 delta=167 integral=69072021-09-05 16:38:22   adjust: -0.00020961438729431614</code></pre><p>如您所见，根据 PSI，503.48M 内存足以支持我的读取工作负载，不会出现任何问题。</p><p>这显然是 PSI 功能的预览，对于真正的生产服务，您可能也应该考虑一下 <code>io.pressure</code>。</p><h3 id="…-那么写回又如何呢？"><a href="#…-那么写回又如何呢？" class="headerlink" title="… 那么写回又如何呢？"></a>… 那么写回又如何呢？</h3><p>说实话，这个问题比较难回答。在我写这篇文章的时候，我还不知道有什么好的工具可以评估和预测写回和 IO 的使用情况。不过，经验法则是先从中学习 <code>io.latency</code>，然后在需要的时候尝试使用 <code>io.cost</code>。</p><p>还有一个有趣的新项目 <a href="https://github.com/facebookexperimental/resctl-demo">resctl-demo</a>，它可以帮助正确识别限制。</p><h2 id="直接-IO-DIO-（NOT-READY）"><a href="#直接-IO-DIO-（NOT-READY）" class="headerlink" title="直接 IO (DIO)（NOT READY）"></a>直接 IO (DIO)（NOT READY）</h2><p>像往常一样，任何规则总有例外。Page Cache 也不例外。因此，让我们来谈谈文件读写，这些操作可以忽略 Page Cache 内容。</p><h3 id="为什么它很好"><a href="#为什么它很好" class="headerlink" title="为什么它很好"></a>为什么它很好</h3><p>某些应用程序需要对存储子系统进行底层访问，Linux 内核通过提供 <code>O_DIRECT</code> 文件打开标志提供了这样的功能。此 IO 称为直接 IO 或 DIO。使用此标志打开文件，程序完全绕过内核 Page Cache，直接与 VFS 和底层文件系统通信。</p><p>优点是：</p><ul><li>降低 CPU 占用率，从而获得更高的吞吐量；</li><li>Linux Async IO( <code>man 7 aio</code>) 仅适用于 DIO( <code>io_submit</code>)；</li><li>零拷贝避免 Page Cache 和用户空间缓冲区之间的双缓冲；</li><li>更好地控制写回。</li><li>…</li></ul><h3 id="为什么它不好，需要-io-uring-替代方案"><a href="#为什么它不好，需要-io-uring-替代方案" class="headerlink" title="为什么它不好，需要 io_uring 替代方案"></a>为什么它不好，需要 <code>io_uring</code> 替代方案</h3><ul><li>需要将读写与块大小对齐；</li><li>并非所有文件系统在实现 DIO 时都相同；</li><li>没有 Linux AIO 的 DIO 很慢而且根本没用；</li><li>非跨平台；</li><li>不能同时对文件进行 DIO 和缓冲 IO。</li><li>…</li></ul><p>如果没有 AIO，DIO 通常就没有意义，但是 AIO 有很多 <a href="https://lwn.net/Articles/671657/">糟糕的设计决策</a>：</p><blockquote><p>所以我认为这极其丑陋。</p><p>AIO 是一种糟糕的临时设计，其主要借口是“其他不太有天赋的人做出了这种设计，而我们为了兼容性而实现它，因为数据库人员——他们实际上很少有品味——实际上会使用它”。</p><p>但 AIO 总是非常非常丑陋。<br>                                                Linus Torvalds</p></blockquote><blockquote><p>注意！使用 DIO 仍然需要在文件上运行 <code>fsync()</code> ！</p></blockquote><p>让我们用 iouring-go 库编写 <code>golang</code> 一个 <a href="https://github.com/Iceber/iouring-go">例子</a>：</p><pre><code class="hljs go">TODO</code></pre><h2 id="高级-Page-Cache-可观察性和故障排除工具"><a href="#高级-Page-Cache-可观察性和故障排除工具" class="headerlink" title="高级 Page Cache 可观察性和故障排除工具"></a>高级 Page Cache 可观察性和故障排除工具</h2><p>让我们介绍一些高级工具，可以用于执行底层内核跟踪和调试。</p><h3 id="eBPF-工具"><a href="#eBPF-工具" class="headerlink" title="eBPF 工具"></a>eBPF 工具</h3><p>首先，我们可以使用 <code>eBPF</code> 工具。当你想获取一些内部内核信息时，<a href="https://github.com/iovisor/bcc"><code>bcc</code></a> 和 <a href="https://github.com/iovisor/bpftrace"><code>bpftrace</code></a> 是你的好帮手。</p><p>让我们来看看它自带的一些工具。</p><h4 id="写回监控"><a href="#写回监控" class="headerlink" title="写回监控"></a>写回监控</h4><pre><code class="hljs bash">$ sudo bpftrace ./writeback.btAttaching 4 probes...Tracing writeback... Hit Ctrl-C to end.TIME      DEVICE   PAGES    REASON           ms15:01:48  btrfs-1  7355     periodic         0.00315:01:49  btrfs-1  7355     periodic         0.00315:01:51  btrfs-1  7355     periodic         0.00615:01:54  btrfs-1  7355     periodic         0.00515:01:54  btrfs-1  7355     periodic         0.00415:01:56  btrfs-1  7355     periodic         0.005</code></pre><h4 id="Page-Cache-Top"><a href="#Page-Cache-Top" class="headerlink" title="Page Cache Top"></a>Page Cache Top</h4><pre><code class="hljs bash">19:49:52 Buffers MB: 0 / Cached MB: 610 / Sort: HITS / Order: descending  PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%     66229 vagrant  vmtouch             44745    44032        0      50.4%      49.6%     66229 vagrant  bash                  205        0        0     100.0%       0.0%     66227 root     cachetop               17        0        0     100.0%       0.0%       222 dbus     dbus-daemon            16        0        0     100.0%       0.0%       317 vagrant  tmux: server            4        0        0     100.0%       0.0%</code></pre><h4 id="缓存统计信息"><a href="#缓存统计信息" class="headerlink" title="缓存统计信息"></a>缓存统计信息</h4><pre><code class="hljs bash">[vagrant@archlinux tools]$ sudo ./cachestat      HITS   MISSES  DIRTIES HITRATIO   BUFFERS_MB  CACHED_MB        10        0        0  100.00%            0        610         4        0        0  100.00%            0        610         4        0        0  100.00%            0        610        21        0        0  100.00%            0        610       624        0        0  100.00%            0        438         2        0        0  100.00%            0        438         4        0        0  100.00%            0        438         0        0        0    0.00%            0        438        19        0        0  100.00%            0        438         0      428        0    0.00%            0        546     28144    16384        0   63.21%            0        610         0        0        0    0.00%            0        610         0        0        0    0.00%            0        610        17        0        0  100.00%            0        610         0        0        0    0.00%            0        610</code></pre><h4 id="bpftrace-和-kfunc-跟踪"><a href="#bpftrace-和-kfunc-跟踪" class="headerlink" title="bpftrace 和 kfunc 跟踪"></a><code>bpftrace</code> 和 <code>kfunc</code> 跟踪</h4><p>除此之外，<code>eBPF</code> 和 <code>bpftrace</code> 最近又增加了一个很棒的新功能，名为 <a href="https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md%2315-kfunckretfunc-kernel-functions-tracing"><code>kfunc</code></a>。因此，使用它，您可以在没有安装内核调试信息的情况下跟踪一些内核函数。</p><p>它仍然接近于实验性功能，但它看起来确实很有前景。</p><h3 id="Perf-工具"><a href="#Perf-工具" class="headerlink" title="Perf 工具"></a>Perf 工具</h3><p>但是如果你想要更深入地了解，我可以为你提供一些东西。<code>perf</code> 允许你几乎在任何内核函数中设置动态跟踪内核探测器。唯一的问题是需要安装内核调试信息。不幸的是，并非所有发行版都提供它，有时你可能需要添加一些额外的标志手动重新编译内核。</p><p>但是当你获得调试信息时，你可以进行非常疯狂的调查。例如，如果我们想跟踪主要缺页中断，我们可以找到负责的内核函数（<a href="https://elixir.bootlin.com/linux/latest/source">https://elixir.bootlin.com/linux/latest/source</a> 及其帮助搜索）并安装一个探针：</p><pre><code class="hljs bash">perf probe -f <span class="hljs-string">"do_read_fault vma-&gt;vm_file-&gt;f_inode-&gt;i_ino"</span></code></pre><p>其中，<code>do_read_fault</code> 是我们的内核函数，<code>vma-&gt;vm_file-&gt;f_inode-&gt;i_ino</code> 是发生主要缺页中断的文件的 inode 编号。</p><p>现在您可以开始记录事件：</p><pre><code class="hljs bash">perf record -e probe:do_read_fault -ag -- <span class="hljs-built_in">sleep</span> 10</code></pre><p><code>perf script</code>10 秒后，我们可以用 bash 魔法来 grep 出 inode ：</p><pre><code class="hljs bash">perf script | grep i_ino | <span class="hljs-built_in">cut</span> -d <span class="hljs-string">' '</span> -f 1,8| sed <span class="hljs-string">'s#i_ino=##g'</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c | <span class="hljs-built_in">sort</span> -rn</code></pre><p><em>原文：</em> <a href="https://biriukov.dev/docs/page-cache/">Linux Page Cache mini book</a></p><blockquote><p>本文翻译仅用于学习与技术交流，版权归原作者所有，如有侵权请联系删除。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/12-11-2024/linux-page-cache-minibook-cn.html">https://www.cyningsun.com/12-11-2024/linux-page-cache-minibook-cn.html</a> <br><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;SRE-深入理解-Linux-Page-Cache&quot;&gt;&lt;a href=&quot;#SRE-深入理解-Linux-Page-Cache&quot; class=&quot;headerlink&quot; title=&quot;SRE 深入理解 Linux Page Cache&quot;&gt;&lt;/a&gt;SRE 深入理解 Li</summary>
      
    
    
    
    <category term="Linux" scheme="https://www.cyningsun.com/category/Linux/"/>
    
    
    <category term="内存" scheme="https://www.cyningsun.com/tag/%E5%86%85%E5%AD%98/"/>
    
  </entry>
  
  <entry>
    <title>Redis 延迟毛刺问题定位-软中断篇</title>
    <link href="https://www.cyningsun.com/09-17-2024/redis-latency-irqoff.html"/>
    <id>https://www.cyningsun.com/09-17-2024/redis-latency-irqoff.html</id>
    <published>2024-09-16T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>该问题发生于去年的十二月份，业务发现部分线上集群再次出现延迟毛刺。只是现象与上次不同：</p><ol><li>延迟出现的时间点不固定，逐渐发生变化</li><li>延迟较为规律的每小时出现一次</li><li>持续时间大概有差不多十分钟，不是一瞬间</li></ol><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240409133418.jpg"></p><h3 id="问题定位"><a href="#问题定位" class="headerlink" title="问题定位"></a>问题定位</h3><p>相比八月份出现类似问题的状态，整个系统的监控系统和定位能力更加完备，包含主调和被调耗时以及耗时百分位。</p><h4 id="缩小范围"><a href="#缩小范围" class="headerlink" title="缩小范围"></a>缩小范围</h4><p>通过 Redis Proxy 主调 Redis 的监控看板，可以观察到明显的耗时毛刺。</p><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240409132002.jpg"></p><p> 然后，使用 ebpf 抓取 Redis 执行耗时也并未发现慢速命令，说明并非是业务使用命令导致的。</p><p> 基于以上手段以及整体架构很容易将问题范围缩小到：Redis Proxy 调用 Redis 链路。 </p><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240409133828.png"></p><p>接下来，我们将注意力转向了网络层面。</p><h4 id="调用链路分析"><a href="#调用链路分析" class="headerlink" title="调用链路分析"></a>调用链路分析</h4><p>首先，在问题出现的时间点，使用 MTR 检查网络丢包和延迟，一切正常。</p><p>再次，检查问题集群的上层交换机，一切正常。</p><p>最后，检查某个主机的监控时，终于发现了与延迟匹配的指标。</p><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240409135042.jpg"></p><p>经网络团队检查，看到机器上 rx missed_errors 比较高</p><pre><code class="hljs sh">$&gt; ethtool -S eno2 |grep rx |grep errorIX_errors: 0Ix_over_errors: 0Ix_crc_errors: oIX_ frame_errors: 0IX_fifo_errors: 0rx missed_errors: 2071867IX_length_errors: 0Ix_long_length_errors: 0rx_short_length_errors: 0</code></pre><p>找了一台机器调高 ring buffer 大小为 4096。</p><pre><code class="hljs sh">$&gt; ethtool -G &lt;nic&gt; rx 4096  <span class="hljs-comment"># 增加 RX 队列的大小到 4096</span></code></pre><pre><code class="hljs sh">$&gt; ethtool -g eno2 <span class="hljs-comment"># 查询网卡队列长度</span>Ring parameters <span class="hljs-keyword">for</span> eno2:Pre-<span class="hljs-built_in">set</span> maximums:RX:4096RX Mini:0RX Jumbo:0TX:4096Current hardware settings:RX:4096RX Mini:0RX Jumbo:0TX:512</code></pre><p>持续观察一天，问题不再复现。</p><p>网络团队的同事判断是业务层有周期性阻塞性的任务，导致软中断线程收包阻塞，rx drop 是因为软中断线程收包慢导致的。 使用字节跳动团队的 <a href="https://github.com/bytedance/trace-irqoff">trace-irqoff</a>，可以观测到以下输出</p><pre><code class="hljs sh">$&gt; <span class="hljs-built_in">cat</span> /proc/trace_irqoff/trace_latencysoftirq: cpu: 2COMMAND: ethtool PID: 95974 LATENCY: 29+ms trace_irqoff_record+0x2a6/0x330 [trace_irqoff] trace_irqoff._hrtimer_handler+0xcb/0xd4 [trace_irqoff]__hrtimer_run_queues+0xca/0x1d0hrtimer_interrupt+0x109/0x230 __sysvec_apic_timer__interrupt+0x61/0xd0sysvec_apic_timer_interrupt+0x77/0x90asm_sysvec_apic_timer_interrupt+0x12/0x20ixgbe_read_reg+0x33/0xf0 [ixgbe]ixgbe_lower_i2c_clk+0x4a/0x60 [ixgbe]ixgbe_clock_in_i2c_byte+0xc0/0x120 [ixgbe]ixgbe_read_i2c_byte_generic_int+0x20f/0x270 [ixgbe]ixgbe_read_12c_byte_generic+0x1b/0x20 [ixgbelixgbe_read_i2c_eeprom_generic+0x21/0x30 [ixgbe]ixgbe_get_module_eeprom+0x6f/0x100 [ixgbe]ethtool_get_module_eeprom_call+0x5b/0x70ethtool_get_any_eeprom+0xf9/0x1b0dev_ethtool+0x1e9a/0x2980dev_ioctl+0x145/0x530sock_do_ioctl+0xa9/0x100sock_ioctl+0xef/0x310__x64_sys_ioctl+0x91/0xc0do_syscall_64+0x5c/Oxc0entry_SYSCALL_64_after_hwframe+0x44/OxaeCOMMAND: ksoftirqd/2 PID: 28 LATENCY: 227ms trace_irqoff_record+0x2a6/0x330 [trace_irqoff]trace_irqoff_timer_handler+0x48/0x80 [trace_irqoff]call_timer_fn+0x2e/0x110run_timer_softirq+0x36e/0x480__do_softirq+0xf0/0x33erun_ksoftirqd+0x2b/0x40smpboot_thread_fn+0xba/0x150kthread+0x12a/0x150ret_from_fork+0x22/0x30</code></pre><p>看到下面的进程 ksoftirqd&#x2F;2 的栈，延迟时间是 227ms。ksoftirqd 进程是 kernel 中处理 softirq 的进程。因此这段栈是没有意义的，因为元凶已经错过了。所以此时，可以借鉴上面的栈信息，看到当 softirq 被延迟 29+ms 的时候，当前 CPU 正在执行的进程是 ethtool。ethtool 的 lantency 提示信息 <strong>29+ms</strong> 是阈值信息，并非实际 latency（所以后面添加一个 ‘+’ 字符，表示 latency 大于 29ms）。实际的 latency 是 ksoftirqd&#x2F;2 显示的 <strong>227ms</strong>。原来是有人用 ethtool 读 eeprom 导致网卡阻塞丢包了。</p><p>团队同事使用以下命令，扫描机器上可执行程序：</p><pre><code class="hljs sh">$&gt; find /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin  -<span class="hljs-built_in">type</span> f -executable ! -path <span class="hljs-string">&quot;/usr/sbin/ethtool&quot;</span> -print0 | xargs -0 strings -f | grep -w <span class="hljs-string">&#x27;ethtool&#x27;</span>/usr/bin/node-exporter: ethtool/usr/bin/udevadm: ../src/shared/ethtool-util.c...</code></pre><p>因为问题是持续定时发生的，识别过滤出两个常驻后台的可执行程序，逐一确认。</p><p>经相关同事确认，故障出现的前一两天确实灰度了光模块监控，会调用 <code>ethtool -m</code> 读取光模块的信息。程序灰度时间与问题出现的时间一致，程序回滚之后问题恢复。原来是程序是被逐个机器遍历的远程调用完成数据抓取，并且根据上次完成的时间偏移固定的时间来启动下次数据抓取。也就解释了为何会出现背景中描述的毛刺特征。</p><h3 id="问题复盘"><a href="#问题复盘" class="headerlink" title="问题复盘"></a>问题复盘</h3><p>MTR 能探测主机丢包么？要回答这个问题首先要了解以下几个问题：</p><ol><li>MTR 是怎么探测是否有丢包的？</li><li>TCP 主机上是怎么负载均衡的 ？</li><li>主机有哪些环节可能导致丢包？</li></ol><h4 id="MTR-原理"><a href="#MTR-原理" class="headerlink" title="MTR 原理"></a>MTR 原理</h4><p>在使用 ICMP（TCP） 探测时，mtr 发送 ICMP ECHO（TCP SYN） 数据包到目标主机（的指定端口）。目标主机收到数据包后，会响应 ICMP ECHO REPLY（TCP SYN-ACK）数据包。mtr 记录下从发送数据包到接收到响应数据包之间的延迟，并将这些信息显示给用户。</p><p>当网络数据包到达网卡时，硬件中断会被触发，然后系统会调度 <code>ksoftirqd</code> 线程来处理数据包，进行协议栈的进一步处理。并在软中断上下文中完成 ICMP（TCP）协议响应（以及 TCP 的连接状态管理，如 SYN、ACK、FIN 等）。以 ICMP 为例，相关内核逻辑如下</p><pre><code class="hljs c++"><span class="hljs-comment">// https://elixir.bootlin.com/linux/v4.6/source/net/ipv4/icmp.c#L893</span><span class="hljs-comment">/*</span><span class="hljs-comment"> *Handle ICMP_ECHO (&quot;ping&quot;) requests.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *RFC 1122: 3.2.2.6 MUST have an echo server that answers ICMP echo</span><span class="hljs-comment"> *  requests.</span><span class="hljs-comment"> *RFC 1122: 3.2.2.6 Data received in the ICMP_ECHO request MUST be</span><span class="hljs-comment"> *  included in the reply.</span><span class="hljs-comment"> *RFC 1812: 4.3.3.6 SHOULD have a config option for silently ignoring</span><span class="hljs-comment"> *  echo requests, MUST have default=NOT.</span><span class="hljs-comment"> *See also WRT handling of options once they are done and working.</span><span class="hljs-comment"> */</span><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">bool</span> <span class="hljs-title">icmp_echo</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> sk_buff *skb)</span></span><span class="hljs-function"></span>&#123;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">net</span> *net;net = <span class="hljs-built_in">dev_net</span>(<span class="hljs-built_in">skb_dst</span>(skb)-&gt;dev);<span class="hljs-keyword">if</span> (!net-&gt;ipv4.sysctl_icmp_echo_ignore_all) &#123;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">icmp_bxm</span> icmp_param;icmp_param.data.icmph   = *<span class="hljs-built_in">icmp_hdr</span>(skb);icmp_param.data.icmph.type = ICMP_ECHOREPLY;icmp_param.skb   = skb;icmp_param.offset   = <span class="hljs-number">0</span>;icmp_param.data_len   = skb-&gt;len;icmp_param.head_len   = <span class="hljs-built_in">sizeof</span>(<span class="hljs-keyword">struct</span> icmphdr);<span class="hljs-built_in">icmp_reply</span>(&amp;icmp_param, skb);&#125;<span class="hljs-comment">/* should there be an ICMP stat for ignored echos? */</span><span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;&#125;</code></pre><blockquote><p>与 TCP 协议相关的定时器（例如 TCP 重传定时器），是通过 <code>kworker</code> 内核线程处理的。定时器触发时，内核线程会进行重传、ACK 处理等操作。</p></blockquote><h4 id="RSS-硬件多队列"><a href="#RSS-硬件多队列" class="headerlink" title="RSS 硬件多队列"></a>RSS 硬件多队列</h4><p>多数主机网卡都支持 RSS（Receive Packet Steering）功能，网卡会有多个接受队列，旨在根据接收到的数据包计算哈希值，并将包分配到不同的接收队列，以便多个 CPU 核心并行处理数据包。查看网卡队列数量：</p><pre><code class="hljs sh">$ ethtool -l eno1Channel parameters <span class="hljs-keyword">for</span> eno1:Pre-<span class="hljs-built_in">set</span> maximums:RX:0TX:0Other:1Combined:128Current hardware settings:RX:0TX:0Other:1Combined:48    <span class="hljs-comment"># 启用的网卡队列数</span></code></pre><p>RSS 的负载均衡通常基于数据包的 <strong>五元组</strong>，包括：</p><ul><li>源 IP 地址</li><li>目的 IP 地址</li><li>源端口（TCP&#x2F;UDP）</li><li>目的端口（TCP&#x2F;UDP）</li><li>协议类型（TCP&#x2F;UDP&#x2F;<strong>ICMP</strong>）</li></ul><p>当使用 MTR 进行探测时，可以指定所使用的协议类型 <code>ICMP</code> 或 <code>TCP</code>。RSS 在处理 ICMP 包时，只会基于三元组：</p><ul><li><strong>源 IP 地址</strong></li><li><strong>目的 IP 地址</strong></li><li><strong>协议类型（ICMP）</strong></li></ul><p>当 RSS 处理 ICMP 包时，网卡会基于这三元组计算一个哈希值，随后将该哈希值与网卡的队列数量进行取模运算，决定数据包被分配到哪个硬件队列，所以<strong>具有相同源 IP、目的 IP 和协议的 ICMP 流量通常会被固定分配到某个特定的队列</strong>。</p><p>对比来看，TCP 协议会不断更改请求包的来源端口，进而可以覆盖所有队列。</p><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240917205825-1.jpeg"></p><p>对于低概率的丢包事件，除了考虑负载均衡，还要考虑探测的频率。 MTR 默认的发包频率是 1 秒，root 用户可以通过 <code>-i</code> 参数来指定 0 到 1 之间的值以提高探测频率，并且保障一定的时长来检测丢包。抑或使用 <code>hping3</code> 直接向终点 IP 发送数据包，而不对中间的路由跳数进行探测。</p><pre><code class="hljs sh">$&gt; hping3 -S  10.129.114.203 -p 80HPING 10.129.114.203 (bond0.1000 10.129.114.203): S <span class="hljs-built_in">set</span>, 40 headers + 0 data byteslen=46 ip=10.129.114.203 ttl=61 DF <span class="hljs-built_in">id</span>=0 sport=80 flags=RA <span class="hljs-built_in">seq</span>=0 win=0 rtt=3.7 mslen=46 ip=10.129.114.203 ttl=61 DF <span class="hljs-built_in">id</span>=0 sport=80 flags=RA <span class="hljs-built_in">seq</span>=1 win=0 rtt=3.7 mslen=46 ip=10.129.114.203 ttl=61 DF <span class="hljs-built_in">id</span>=0 sport=80 flags=RA <span class="hljs-built_in">seq</span>=2 win=0 rtt=7.6 ms</code></pre><h4 id="RPS-软件多队列"><a href="#RPS-软件多队列" class="headerlink" title="RPS 软件多队列"></a>RPS 软件多队列</h4><p>对于不支持多队列或队列数显著少于 CPU 数的主机（如：基于 <a href="https://github.com/intel/ethernet-linux-ixgbe">82598</a> 网络连接的 Intel 网卡仅支持 16 个队列），需要开启软件实现的多队列，即 RPS。RPS 类似的基于数据包的<strong>五元组</strong>（源 IP、目的 IP、源端口、目的端口、协议类型），将接收队列的网络数据包分发到多个 CPU 核的 backlog 队列。再由各个 CPU 上软中断线程将数据包交给 L2、L3、L4 协议解析，最终到达 socket 缓存区。以此避免网络处理集中在单个（部分） CPU 核上，从而造成瓶颈或资源不平衡。整体流程参考：<a href="https://www.cyningsun.com/04-24-2023/monitoring-and-tuning-the-linux-networking-stack-recv-cn.html#Receive-Packet-Steering-RPS">译｜Monitoring and Tuning the Linux Networking Stack - Receiving Data</a></p><p><img src="/images/redis-latency-irqoff/Redis%20%E5%BB%B6%E8%BF%9F%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-%E8%BD%AF%E4%B8%AD%E6%96%AD%E7%AF%87-20240914205807-1.jpeg"></p><h4 id="主机丢包环节"><a href="#主机丢包环节" class="headerlink" title="主机丢包环节"></a>主机丢包环节</h4><p>对于本次故障的场景，由于没有连接异常断开，所有长连接均处于 ESTABLISHED 状态。那么，连接建立阶段的丢包的因素就可以不用考虑。因此就可以重点考虑最关键的三个队列是否溢出：RX 队列、backlog 队列、Socket 接收缓存区。</p><p>由于 TCP 是面向连接的协议，有流控机制，当接收缓冲区满时，发送方会停止发送数据，直到缓冲区有空闲空间为止，因此 TCP 丢包的概率较小。其他两个队列的丢包情况，则可以通过 ethtool 查看，即上文提及的排查命令。</p><p>推而广之，怎么覆盖上层协议栈的丢包呢？使用 <code>netstat -s</code> 命令，可以查看网络协议栈各层的详细统计信息，包括 IP、TCP、UDP、ICMP。如果具体到定位丢包原因，则需要其他可观测性的工具。</p><p>考虑到数据包处理路径的复杂度，Linux 内核从 5.15 版本开始引入了 <code>skb_drop_reason</code> 以追溯根因。它通过为丢包原因提供一组标准化的枚举值 <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/dropreason-core.h?h=v6.4-rc1#n88"><code>skb_drop_reason enum</code></a> ，让开发者能够更清楚地看到丢包的具体原因，并可以通过工具在 <code>skb:kfree_skb</code> 跟踪点上添加探测器来监控包丢弃情况。</p><pre><code class="hljs sh">$&gt; perf record -e skb:kfree_skb curl https://localhostcurl: (7) Failed to connect to localhost port 443: Connection refused[ perf record: Woken up 1 <span class="hljs-built_in">times</span> to write data ][ perf record: Captured and wrote 0.040 MB perf.data (4 samples) ]$&gt; perf script            curl 163406 [036] 7681948.959483: skb:kfree_skb: skbaddr=0xffff8a68e752cc00 protocol=0 location=0xffffffff8efced8e reason: NOT_SPECIFIED            curl 163406 [036] 7681948.959574: skb:kfree_skb: skbaddr=0xffff8a68ed61d2e0 protocol=34525 location=0xffffffff8f0177e9 reason: NOT_SPECIFI&gt;            curl 163406 [036] 7681948.959728: skb:kfree_skb: skbaddr=0xffff8a68ed61d2e0 protocol=2048 location=0xffffffff8ef64c2b reason: NO_SOCKET            curl 163406 [036] 7681948.959779: skb:kfree_skb: skbaddr=0xffff8a68ed61d2e0 protocol=2048 location=0xffffffff8ef64c2b reason: NO_SOCKET</code></pre><p>腾讯、字节等厂在此基础上进行了更加友好的封装：<a href="https://github.com/OpenCloudOS/nettrace">nettrace</a>、<a href="https://github.com/bytedance/netcap">netcap</a></p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>针对该类偶现问题，由于短期波动对整体趋势影响较小，抓取现场获取瞬时值（即时值）的难度颇高。相反，累计值能够保存历史记录，并且随着时间的推移，累计值的数据量可能变得非常大，更适合分析。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/09-17-2024/redis-latency-irqoff.html">https://www.cyningsun.com/09-17-2024/redis-latency-irqoff.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h3&gt;&lt;p&gt;该问题发生于去年的十二月份，业务发现部分线上集群再次出现延迟毛刺。只是现象与上次不同：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;延迟出现的时间点不固定，逐</summary>
      
    
    
    
    <category term="Performance" scheme="https://www.cyningsun.com/category/Performance/"/>
    
    
    <category term="Kernel space" scheme="https://www.cyningsun.com/tag/Kernel-space/"/>
    
  </entry>
  
  <entry>
    <title>译｜IOCost: Block IO Control for Containers in Datacenters</title>
    <link href="https://www.cyningsun.com/06-27-2024/iocost-block-io-control-for-containers-in-datacenters-cn.html"/>
    <id>https://www.cyningsun.com/06-27-2024/iocost-block-io-control-for-containers-in-datacenters-cn.html</id>
    <published>2024-06-26T16:00:00.000Z</published>
    <updated>2025-08-03T12:26:29.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><p>资源隔离是数据中心环境的基本需求。然而，我们在 Meta 大规模数据中心的生产实践中发现，现有的块存储 IO 控制机制在容器化环境中表现不足。IO 控制必须为容器提供按比例分配的资源，同时考虑到存储设备硬件异构性和数据中心部署工作负载的特性。现代 SSD 的速度要求 IO 控制以低开销执行。此外，IO 控制应追求工作量保持，考虑与内存管理子系统的交互，并避免优先级反转导致的隔离失败。</p><p>为应对这些挑战，本文提出 IOCost，一种专为容器化环境设计的 IO 控制方案，它为数据中心中异构存储设备和多样化的负载提供了可扩展、工作量保持和低开销的 IO 控制。<strong>IOCost 通过离线分析建立设备模型并用此模型来估计每个 IO 请求的设备占用情况。为了最小化运行时开销，IOCost 将 IO 控制分为快速的每 IO 问题路径和较慢的周期性规划路径。一个创新的工作量保持预算捐赠算法允许容器动态共享未使用的预算。</strong> IOCost 已经在 Meta 的数据中心进行了部署，覆盖了数百万台机器，向上游的 Linux 内核贡献了 IOCost，并开源了设备分析工具。IOCost 已经在生产环境中稳定运行了两年，为 Meta 的设备群提供着 IO 控制服务。我们在此文中详细阐述了 IOCost 的设计理念，并分享了将其大规模部署所积累的经验。</p><blockquote><p>译者注<br>工作量保持：尽量利用可用资源来执行任务，不让资源闲置</p></blockquote><h3 id="CCS-概念"><a href="#CCS-概念" class="headerlink" title="CCS 概念"></a>CCS 概念</h3><ul><li>软件工程 → 操作系统；输入/输出；</li><li>计算机系统组织 → 云计算。</li></ul><h3 id="关键词"><a href="#关键词" class="headerlink" title="关键词"></a>关键词</h3><p>数据中心，操作系统，I/O，容器</p><h3 id="1-引言"><a href="#1-引言" class="headerlink" title="1. 引言"></a>1. 引言</h3><p>容器正在迅速成为现代数据中心中虚拟化容量的主要机制之一。它们在操作系统层面虚拟化资源，为应用程序提供轻量级、一致的运行环境，便于跨平台部署和运行。目前市场上有众多容器解决方案，包括亚马逊 AWS、谷歌 Cloud 和微软 Azure 等主要云服务商提供的产品。容器也正在接管私有数据中心，Facebook 的整个服务器群也完全基于容器运作。随着容器使应用整合程度提高，构建有效的控制和隔离机制变得尤为重要。</p><p>以往的研究关注点主要集中在计算、内存和网络资源的隔离上，并在 Linux 中有许多改进。不过，Meta 在大规模数据中心的实际运营中发现，现有针对块存储的 IO 控制机制，例如 BFQ，无法满足容器化环境下的需求。</p><p>为容器提供健壮的 IO 控制，存在以下几项挑战：首先，IO 控制需要考虑数据中心中的硬件异构性。单个数据中心中可能同时存在多代 SSD、传统的硬盘驱动器、本地/远程存储和新型存储技术。硬件异构性因它们在延迟和吞吐量方面的性能特性大不相同而进一步加剧，不仅在不同类型的设备（如 SSD 和硬盘）之间，而且在同一类型内也是如此。有效控制还需要考虑 SSD 的特殊性，这些特性可能会在短时间内过度发挥其性能，然后急剧下降，从而对堆叠环境产生不利影响。</p><p>其次，IO 控制需要适应各种应用程序的限制。例如，一些应用程序对延迟敏感，而其他应用程序主要从增加吞吐量中受益，还有一些应用程序可能执行顺序或随机访问，这些访问可能是突发的或是持续的。不幸的是，在数据中心级别，当设备异构性和应用多样性结合在一起时，找到延迟和吞吐量之间的平衡点尤其具有挑战性。</p><p>第三，IO 隔离需要提供数据中心所需的一系列属性。工作量保持是理想的，因为它能实现高利用率，避免资源闲置。此外，一些 IO 控制机制依赖于严格的优先级排序，但这在平等优先级的应用共享同一台机器时无法提供公平性。再者，应用程序开发者常常无法准确评估每个应用和设备层面上的 IO 需求，比如 IOPS 这样的指标。因此，IO 控制机制应当易于应用程序开发者理解和配置。最后，IO 隔离与诸如页面回收和交换等内存管理操作相互作用。IO 控制必须识别这些交互，以防止优先级反转和其他隔离失败的情况发生。</p><p>过去的 IO 控制研究主要集中在基于 VM 的虚拟化环境上，提出了多种旨在增强 hypervisor 功能的方案。然而，这些方法并没有充分考虑到容器环境的复杂性，例如单一共享的操作系统、IO 与内存子系统之间的交互，以及高度堆叠的部署方式。在 Linux 内核中，最先进的解决方案要么依赖于 BFQ，要么基于最大带宽使用量，通过 IOPS 或字节数来设定限制。然而，这些方法未能实现充分的工作量保持（work-conserving），缺乏与内存子系统的整合，或者对于快速存储设备增加了过多的性能开销。这意味着，传统 IO 控制机制在容器化环境下，特别是在需要高效利用资源和与内存管理协同工作的场景中，表现不佳。</p><p>在这项研究中，我们引入了 IOCost，这是一个全面的 IO 控制解决方案，它综合地解决了异构硬件设备和应用程序带来的挑战，同时满足了数据中心规模下容器对 IO 隔离的需求，同时考虑了与内存管理的交互。IOCost 背后的关键洞察是，IO 控制中的主要难点在于对设备占用情况理解不足。当我们比较现有的 IO 控制与 CPU 调度时，这一点变得明显。CPU 调度依赖于加权公平队列等技术，通过测量 CPU 时间消耗来按比例分配 CPU 占用率。相比之下，像 IOPS 或字节数这样的指标对于衡量占用率来说并不理想，尤其是考虑到块设备种类繁多。现代块设备严重依赖内部缓冲和复杂的延后操作，如垃圾收集，这给那些依赖设备时间共享或主要基于 IOPS 或字节数来确保公平性的技术带来了难题。</p><p>IOCost 通过使用特定设备的模型来估算每个 IO 请求的设备占用量工作。例如，4KB 的读取操作在高端 SSD 上的成本与在传统机械硬盘上是不同的。有了占用模型和额外的 QoS 参数——后者用于补偿建模不准确性并决定设备负载程度——IOCost 可以在各个容器之间公平地分配设备占用。系统管理员或容器管理系统沿着容器层次结构设置权重，以确保单个容器或容器组获得一定比例的 IO 服务。IOCost 进一步引入了一种新颖的工作量保持预算捐赠算法，允许容器高效地将其多余的 IO 预算转移给其他容器。</p><p>我们已经在 Meta 的整个机群中部署了 IOCost。我们的评估显示，与其它解决方案相比，IOCost 能提供比例、工作量保持且具备内存管理感知的 IO 控制，同时开销极小。具体而言，我们证明了 IOCost 在堆叠式 ZooKeeper 部署中成功隔离了 IO 操作，而先前的解决方案未能提供可行的解决办法。为了表明 IOCost 的广泛应用性，我们还在使用远程存储如 AWS Elastic Block Store 和 Google Persistent Disk 的公共云 VM 上成功验证了它的有效性。</p><p>我们已经在 Meta 的设备群中部署了 IOCost。我们的评估表明，IOCost 优于其他解决方案，提供了比例工作保持和内存管理感知的 IO 控制，且开销极小。具体来说，我们展示了 IOCost 在一个堆叠的 ZooKeeper 部署中成功地隔离了 IO 操作，而现有的解决方案则未能提供可行的解决方案。为了证明 IOCost 的广泛适用性，我们在使用远程存储（如 AWS Elastic Block Store 和 Google Cloud Persistent Disk）的公共云 VM 中成功验证了它。</p><p>本文的贡献如下：</p><p>• IOCost 提出了一种针对现代存储设备设计的容器感知、可扩展、工作量保持且低开销的 IO 控制方案。<br>• 我们介绍了一种建模技术，用于评估不同应用和设备上的 IO 设备占用情况。为了弥补模型不精确性带来的影响，IOCost 根据实时 cgroup 使用情况和 IO 完成延迟的统计数据，在运行时调整 IO 控制策略。<br>• 我们提出了一种工作保护算法，它使得容器能够将未完全使用的 IO 预算按比例捐赠给 cgroup 层级中的其他容器。<br>• 为了减少运行时开销，我们将 IO 控制分解为快速的每 IO 问题路径和较慢的周期性规划路径。<br>• 我们对 IOCost 进行了详细的评估，并展示了现有的 IO 控制机制在功能集和性能上无法与 IOCost 相匹敌。<br>• 我们已在 Meta 公司遍布全球的数据中心（包含数百万台机器）全面部署了 IOCost，并向上游的 Linux 内核贡献了 IOCost，同时开源了我们的设备性能分析和基准测试工具。</p><h3 id="2-背景"><a href="#2-背景" class="headerlink" title="2. 背景"></a>2. 背景</h3><p>在本节中，我们首先简要介绍 cgroup，它是用来配置每个容器资源分配的关键机制。接下来，我们介绍了 Linux 块层和现有的 IO 控制解决方案。最后，我们描述了现代数据中心的背景，其中包含多种不同的块存储设备和工作负载。</p><h4 id="2-1-使用-cgroup-进行资源控制"><a href="#2-1-使用-cgroup-进行资源控制" class="headerlink" title="2.1 使用 cgroup 进行资源控制"></a>2.1 使用 cgroup 进行资源控制</h4><p>容器运行时依赖于控制组（cgroup）来实现资源控制和隔离。如今，cgroup 是容器组织进程并沿其层次结构以受控和可配置方式分配系统资源的主要机制。</p><p>cgroup 有两个主要的概念部分。首先，单个 cgroup 形成了一个层次结构，而进程属于一个 cgroup。一个 cgroup 可以包含大量进程或仅包含一个进程。其次，cgroup 控制器会根据配置，沿着这个树状结构分配具体的系统资源，比如 CPU、内存和 IO。</p><p>配置 cgroup 控制器的一种常见方法是使用权重（<code>weight</code>），即通过累加所有同级 cgroup 的权重，然后根据每个 cgroup 权重与总和的比例来分配资源。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626230940-1.jpeg"><br>图 1：Meta 生产环境 cgroup 层次结构</p><p>图 1 显示的是 Meta 使用的一个示例性 cgroup 层级结构。这个层级被划分为系统（<code>system</code>）、主机关键（<code>host critical</code>）和工作负载（<code>workload</code>）三个部分的 cgroup。<code>System</code> cgroup 包含了所有的辅助服务，比如 chef，服务通常执行定期操作以保持主机更新。<code>Host Critical</code> cgroup 则包括了维持主机运行所必需的进程，例如 sshd 和容器管理代理。<code>Workload</code> cgroup 则存放了所有应用程序的进程，为了适当地隔离不同的容器，它被进一步细分为子 cgroup。</p><h4 id="2-2-块层和-IO-控制"><a href="#2-2-块层和-IO-控制" class="headerlink" title="2.2 块层和 IO 控制"></a>2.2 块层和 IO 控制</h4><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626231020-1.jpeg"><br>图 2：IO 和块层</p><p>应用程序和文件系统通过块层来访问块设备。图 2 显示了 Linux 块层以及与之交互的其它组件。从顶部开始，用户空间通过系统调用与内核进行互动。对文件系统的读写操作会传递到块层，形成文件系统 IO（FS IO）。此外，用户空间还可以通过导致页错误、脏页回写或换出等内存操作间接达到块层。 cgroup 子系统负责资源核算，并基于 cgroup 层级结构，在所有相关组件间传递控制信息。</p><p>块层使用 <code>bio</code> 数据结构来携带信息，如请求类型（例如读或写）、大小、目标设备、设备的扇区偏移、发出请求的 cgroup 以及数据复制源或复制目的内存。在请求提交给设备驱动程序之前，块层的控制和调度逻辑可以选择限制 <code>bio</code> 的速度，将它与其他请求合并等。Linux 内核提供了多种不同的 IO 调度器，可以被启用。我们将那些与 cgroup 子系统集成的调度器称为“控制器”，以此区别于仅仅确保整机有良好性能的普通 IO 调度器。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626231435-1.jpeg"><br>表 1：Linux IO 控制机制和功能</p><p>表 1 列出了各种 Linux IO 控制机制的特点。第 4 节通过实验全面比较了这些机制。在没有 cgroup 控制的情况下，IO 调度主要有三种选择：no scheduler、mq-deadline 和 kyber。这些选项并不向容器保证 IO 资源，而是确保一些总体性能特性，例如防止异步写入影响同步读取操作。</p><p>blk-throttle 允许通过设定每秒读/写 IOPS 或字节数的形式来限定 I/O 操作。然而，这些限制并不具备工作量保持，对于数据中心内多样化的设备和应用来说，配置起来十分困难。</p><p>BFQ 提供了比例控制 I/O 的工作量保持接口，但它忽略了与内存管理的交互，这可能导致隔离失效。此外，如第 4.1 节所示，BFQ 具有较高的每次请求开销和宽泛的延迟波动。最后，BFQ 根据每个容器读/写扇区进行轮询调度，这种方法在具有复杂内部操作的现代设备上效率低下。</p><p>除了 IOCost 之外，我们还开发了 IOLatency 控制器，它可以为单独的 cgroup 设置 I/O 延迟目标。具体而言，它界定了一个 cgroup 的 I/O 操作在其它 cgroup 受到限制前所能接受的最大延迟。例如，如果另一个设置了 5 毫秒延迟目标的 cgroup 其 I/O 操作开始超过 5 毫秒，那么一个延迟目标为 10 毫秒的 cgroup 将会被限流。我们已经将 IOLatency 控制器集成到上游 Linux 内核。</p><p>在实际生产部署中，我们发现了 IOLatency 存在的一些局限性。首先，基于延迟的接口只适用于严格的优先级划分，即阻止低优先级的工作负载干扰高优先级的工作负载，但缺乏比例控制使得它不适合在同等优先级的工作负载之间确保公平性。其次，尽管从技术角度讲，IOLatency 实现了工作量保持，但在多元化的设备和工作负载中寻找既能隔离又能工作量保持的配置几乎是不可能的。</p><h4 id="2-3-硬件和工作负载异构性"><a href="#2-3-硬件和工作负载异构性" class="headerlink" title="2.3 硬件和工作负载异构性"></a>2.3 硬件和工作负载异构性</h4><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626231628-1.jpeg"><br>图 3：Meta 设备群的设备异质性。</p><p>硬件异质性。硬件的逐步更新和供应链的多样化，导致了数据中心内部存在多种类型的 SSD。在 Meta 的服务器群中，图 3 显示了不同 SSD 设备的性能特征。图的左侧 y 轴表示随机和顺序读写操作的 IOPS，右侧 y 轴则显示了读写操作的延迟。我们运用 fio 工具来测量每款设备所能持续达到的峰值性能。</p><p>八种类型的固态硬盘（标记为 A 至 H）展现出各自独特的性能特征。具体而言，SSD H 在低延迟条件下实现了高 IOPS，SSD G 虽然 IOPS 较低，但同样保持了相对低的延迟，而 SSD A 则以中等的 IOPS 水平配以较高的延迟。每一款设备通常占数据中心总设备数量的比重不超过 14%，除了设备 F，它的占比达到了 19%。大约 20% 的 SSD 容量分布于图中未列出的 18 种设备，但这些设备的特性已经被图中显示的设备所涵盖。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626231932-1.jpeg"><br>图 4：IO 工作负载异构性</p><p>工作负载异构性。Meta 的应用程序表现出其 IO 工作负载的多样性。图 4 显示了 Meta 上几个典型工作负载的 I/O 需求。通过测量一周生产数据的 P50，我们观察到每秒读、写操作与随机、顺序字节操作之间的对比。像 Web A 和 Web B 这样的工作负载最能代表 Meta 的平均状况，它们的读、写操作在随机、顺序操作上大致均衡。而 Meta 的 Serverless 工作负载则高度过载，呈现出混合的读、写比例。Cache A 和 Cache B 是内存缓存服务，它们使用高速的块设备作为内存缓存的后端存储，这两者均展现出大量的顺序 I/O。此外，Meta 的非存储服务进行的显式 I/O 操作相对较少，它们的 I/O 大多来源于页面调度和周期性的软件更新。</p><p>总而言之，有效的 I/O 控制的重大挑战在于，在不需要每工作负载配置（例如延迟、IOPS 或每秒字节数）的情况下，能够应对硬件异构和工作负载多样化的稳健性，这通常在生产环境中太脆弱且难以管理。一个理想的 I/O 控制机制应当能够满足各类工作负载的复合需求，同时避免配置的爆炸式增长。</p><h3 id="3-IOCost-设计"><a href="#3-IOCost-设计" class="headerlink" title="3. IOCost 设计"></a>3. IOCost 设计</h3><p>IOCost 的目标是实现 IO 控制，该控制需考虑到硬件设备的异构性和工作负载需求的多样性，同时为容器间提供比例分配的资源和强大的隔离性。</p><h4 id="3-1-概述"><a href="#3-1-概述" class="headerlink" title="3.1 概述"></a>3.1 概述</h4><p>IOCost 显式地将设备配置与工作负载配置解耦。对于每个设备，IOCost 引入了一个成本模型及一组服务质量（QoS）参数，它们定义并规范了设备的行为。而对于工作负载，IOCost 利用 cgroup 权重进行比例配置，这意味着工作负载的配置可以独立于设备细节，这在异构环境中大大简化并增强了大规模配置的便捷性和稳健性。</p><p>IOCost 采用多核 CPU 的分层加权公平调度概念。IOCost 通过每 IO 的成本建模来估算单次 IO 操作的占用情况，然后根据为每个 cgroup 分配的权重，使用该占用估算值来做调度决策。我们的创新设计将低延迟问题路径与周期性规划路径分开，使得 IOCost 能够扩展到每秒数百万次 IOPS 的 SSD。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626232102-1.jpeg"><br>图 5：IOCost 架构概览，显示了左侧如何评估 bio（块 I/O 请求）的成本以作出限流决策，以及右侧的离线成本模型与逻辑生成过程。</p><p>图 5 给出了 IOCost 体系结构的概览。IOCost 在逻辑上分为 <code>问题路径（Issue Path）</code> 和 <code>规划路径（Planning Path）</code> 两部分，前者是运行在微秒时间尺度上的每 bio 操作，后者则是运行在毫秒时间尺度上的周期性操作。此外，离线工作用于推导出设备的成本模型和 QoS 参数。</p><p>让我们简短地探讨一下 bio 的生命周期及其与 IOCost 的交互过程。首先，在步骤 1 中，IOCost 接收到一个描述 IO 操作的 bio。随后的步骤中，IOCost 会计算这个 bio 的 <code>cost</code>，并作出相应的限流决策。</p><p>在步骤 2 里，IOCost 从 bio 中抽取特征，并利用成本模型参数计算出 <code>𝑎𝑏𝑠𝑜𝑙𝑢𝑡𝑒 𝑐𝑜𝑠𝑡</code>。<code>cost</code> 是以时间单位表示的，但是一个 IO 操作的 <code>cost</code> 其实是一个占用率指标，而非延迟。例如，20 毫秒的代价意味着设备每秒可以处理 50 个这样的请求，但这并不说明每个操作实际耗时多久。我们将在第 3.2 节中进一步讨论特征选择和成本模型的细节。</p><p>紧接着，在步骤 3 中，绝对的 IO 成本会被除以发出请求的控制组（cgroup）的层次权重（<code>hweight</code>），以得出相对的 IO 成本。<code>hweight</code> 是通过在 cgroup 层次结构中向上递归，累计该 cgroup 相对于其同级 cgroup 所占的权重份额来计算的。<code>hweight</code> 代表着该 cgroup 有权获得的 IO 设备最终份额。例如，一个 <code>hweight</code> 为 0.2 的 cgroup 就拥有设备 20% 的份额，而一个 IO 操作的相对成本就是 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -1.108ex;" xmlns="http://www.w3.org/2000/svg" width="9.965ex" height="3.11ex" role="img" focusable="false" viewBox="0 -884.7 4404.7 1374.7"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mrow" transform="translate(220,394) scale(0.707)"><g data-mml-node="mi"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(529,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mi" transform="translate(958,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(1427,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(1912,0)"><path data-c="1D459" d="M117 59Q117 26 142 26Q179 26 205 131Q211 151 215 152Q217 153 225 153H229Q238 153 241 153T246 151T248 144Q247 138 245 128T234 90T214 43T183 6T137 -11Q101 -11 70 11T38 85Q38 97 39 102L104 360Q167 615 167 623Q167 626 166 628T162 632T157 634T149 635T141 636T132 637T122 637Q112 637 109 637T101 638T95 641T94 647Q94 649 96 661Q101 680 107 682T179 688Q194 689 213 690T243 693T254 694Q266 694 266 686Q266 675 193 386T118 83Q118 81 118 75T117 65V59Z"></path></g><g data-mml-node="mi" transform="translate(2210,0)"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(2782,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(3143,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mtext" transform="translate(3609,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(3859,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="mi" transform="translate(4292,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(4777,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(5246,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g></g><g data-mml-node="mrow" transform="translate(958.9,-345) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(576,0)"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mi" transform="translate(1292,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mi" transform="translate(1758,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(2103,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mi" transform="translate(2580,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(3156,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g></g><rect width="4164.7" height="60" x="120" y="220"></rect></g></g></g></svg></mjx-container> 。</p><p>步骤 4 显示了全局虚拟时间（<code>vtime</code>）时钟，它以虚拟时间速率（<code>vrate</code>）指定的速度与实际时间同步前进。每个 cgroup 跟踪其本地 vtime，每当发生一次 IO 操作时，本地 vtime 会根据该 IO 的相对成本向前推进。接着，在步骤 5 中，基于本地 <code>vtime</code> 与全局 <code>vtime</code> 之间的差距，IOCost 做出限流决策。这个差距代表了 cgroup 当前的 IO 预算。如果预算等于或大于某个 IO 的相对成本，该 IO 立即执行。否则，IO 必须等待直到全局 <code>vtime</code> 推进足够远。</p><p>在规划路径中，IOCost 收集 cgroup 的使用情况和完成延迟，并定期调整 IO 控制策略。在步骤 6 中，IOCost 根据设备反馈全局调整 <code>vrate</code>，进而调整总的 IO 发起量。由于模型可能过高或过低估计实际设备占用，<code>vrate</code> 的调整确保设备的良好利用。关于 <code>vrate</code> 调整和 QoS 的更多讨论见第 3.3 节。接下来，在步骤 7 中，IOCost 的捐赠算法高效地将多余的预算捐赠给其他 cgroup，实现工作量保持。第 3.6 节详细介绍了该算法。</p><p>在步骤 8 中，离线状态下，IOCost 利用部署设备上的性能剖析、基准测试和训练来构建每个设备模型的成本模型和 QoS 参数，这些参数在生产部署期间会被使用。</p><h5 id="3-1-1-问题路径"><a href="#3-1-1-问题路径" class="headerlink" title="3.1.1 问题路径"></a>3.1.1 问题路径</h5><p>问题路径决定了 IO 的 <code>cost</code>、<code>hweight</code>、基于本地和全局 <code>vtime</code> 的可用预算，并作出限流决策。</p><p>bio 的绝对成本是通过将成本模型应用于 bio 的特征来计算的。每个 cgroup 也被分配了一个权重，这个权重表示了该 cgroup 在其同级 cgroup 中所占的 IO 占用比例。为了避免在热点路径上重复递归操作，权重被合并并平展为 <code>hweight</code>，然后被缓存起来，只有当权重发生变化时才会重新计算。</p><p>一个没有发出 IO、因而没有消耗其预算的 cgroup 会导致设备利用率低下。为了解决这个问题，IOCost 区分了活跃的 cgroup。当一个 cgroup 发出 IO 时，它就变成了活跃状态；而在一个完整的规划周期过去而没有任何 IO 的情况下，它会变成非活跃状态。在计算 <code>hweight</code> 时，非活跃的 cgroup 会被忽略。这个低开销机制使得设备保持较高的利用率，因为闲置的 cgroup 隐式地将其预算捐赠给了活跃的 cgroup。当一个 cgroup 变为活跃或非活跃时，它会增加一个权重树生成号，以此指示权重已被调整。随后通过问题路径执行的 cgroup 会注意到这一点，并重新计算它们的 <code>hweight</code>。</p><h5 id="3-1-2-规划路径"><a href="#3-1-2-规划路径" class="headerlink" title="3.1.2 规划路径"></a>3.1.2 规划路径</h5><p>规划路径负责全局协调，确保每个 cgroup 仅凭本地信息就能高效运行，并且能够收敛到期望的分层加权公平 IO 分配。它基于延迟目标的倍数定期运行，这样既能包含足够数量的 IO，又能允许精细的控制。</p><p>规划路径统计每个 cgroup 正在使用的 IO 量，以此确定它们可以捐赠多少权重，并相应地调整权重。通过预算捐赠，IOCost 实现了工作量保持，同时保证问题路径操作严格局限在 cgroup 本地。与捐赠相关的问题路径操作仅限于当预算紧张时减少或取消捐赠，这也是一个本地操作。</p><p>此外，规划路径还监控设备行为，并通过调整 <code>vrate</code> 来控制全局虚拟时间相对于实际时间的快慢，从而调节所有 cgroup 能发出的 IO 总量。例如，如果 <code>vrate</code> 设置为 150%，那么全局虚拟时间将以实际时间的 1.5 倍速度运行，并产生比设备成本模型指定的多 1.5 倍的 IO 预算。<code>vrate</code> 调整的条件和范围是由系统管理员通过 QoS 参数配置的。</p><h4 id="3-2-设备成本建模"><a href="#3-2-设备成本建模" class="headerlink" title="3.2 设备成本建模"></a>3.2 设备成本建模</h4><p>IOCost 将设备成本建模与运行时的 IO 控制分离。成本模型在部署前为每个设备离线生成。为了达到最大的灵活性，IOCost 允许成本模型以任意的 eBPF 程序形式表达。此外，IOCost 原生支持线性模型，其工作原理如下。IOCost 从 bio 请求中提取以下特征：1）读取或写入、2）相对于 cgroup 的上一次 IO 是随机还是顺序，3）请求的大小。IO 成本计算如下：</p><p><mjx-container class="MathJax" jax="SVG" display="true" width="full" style="min-width: 54.894ex;"><svg style="vertical-align: -0.566ex; min-width: 54.894ex;" xmlns="http://www.w3.org/2000/svg" width="100%" height="2.262ex" role="img" focusable="false"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(0.0181,-0.0181) translate(0, -750)"><g data-mml-node="math"><g data-mml-node="mtable" transform="translate(2078,0) translate(-2078,0)"><g transform="translate(0 750) matrix(1 0 0 -1 0 0) scale(55.25)"><svg data-table="true" preserveAspectRatio="xMidYMid" viewBox="10053.5 -750 1 1000"><g transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mlabeledtr"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(345,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mtext" transform="translate(830,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(1080,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="mi" transform="translate(1513,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(1998,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(2467,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(3105.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(4161.6,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mi" transform="translate(4590.6,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(5119.6,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(5588.6,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mtext" transform="translate(6054.6,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(6304.6,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="mi" transform="translate(6737.6,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(7222.6,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(7691.6,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mtext" transform="translate(8052.6,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(8524.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mtext" transform="translate(9525,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(9775,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(10244,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(10589,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="mi" transform="translate(11054,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mtext" transform="translate(11520,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(11770,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g><g data-mml-node="mi" transform="translate(12203,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(12688,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(13157,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mtext" transform="translate(13518,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(13768,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(14219,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(14748,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(15109,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mtext" transform="translate(15575,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(15825,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mtext" transform="translate(16603,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(16853,0)"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mi" transform="translate(17282,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(17627,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mtext" transform="translate(18112,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(18362,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(18831,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(19176,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="mi" transform="translate(19641,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g></g></g></g></svg><svg data-labels="true" preserveAspectRatio="xMaxYMid" viewBox="1278 -750 1 1000"><g data-labels="true" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mtd" id="mjx-eqn:1"><g data-mml-node="mtext"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z" transform="translate(389,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(889,0)"></path></g></g></g></svg></g></g></g></g></svg></mjx-container></p><p>根据读/写和随机/顺序的组合，从四种 <code>base cost</code> 选择一种。根据读或写选择 <code>size cost rate</code>。因此，线性模型由六个参数组成：四种 <code>base cost</code> 和两种 <code>size cost rate</code>。</p><p>为了方便起见，配置以不同的格式接受这六个参数——读写每秒字节数（bps），以及读写时的每秒 4kB 顺序和随机 IO（IOPS）。这些参数在内部被转换为 <code>base cost</code> 和 <code>size cost rate</code>，转换公式如下：</p><p><mjx-container class="MathJax" jax="SVG" display="true" width="full" style="min-width: 28.59ex;"><svg style="vertical-align: -2.043ex; min-width: 28.59ex;" xmlns="http://www.w3.org/2000/svg" width="100%" height="5.217ex" role="img" focusable="false"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(0.0181,-0.0181) translate(0, -1403)"><g data-mml-node="math"><g data-mml-node="mtable" transform="translate(2078,0) translate(-2078,0)"><g transform="translate(0 1403) matrix(1 0 0 -1 0 0) scale(55.25)"><svg data-table="true" preserveAspectRatio="xMidYMid" viewBox="4240.3 -1403 1 2306"><g transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mlabeledtr" transform="translate(0,-23)"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(469,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(814,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="msub" transform="translate(1279,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mi" transform="translate(499,-150) scale(0.707)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g></g><g data-mml-node="mi" transform="translate(2134.2,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(2619.2,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="msub" transform="translate(3088.2,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mi" transform="translate(3851.1,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(4380.1,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(4741.1,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mo" transform="translate(5484.9,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mfrac" transform="translate(6540.6,0)"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mi" transform="translate(500,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">秒</text></g></g><g data-mml-node="mrow" transform="translate(269.5,-686)"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mi" transform="translate(429,0)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g><g data-mml-node="mi" transform="translate(932,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g><rect width="1700" height="60" x="120" y="220"></rect></g></g></g></g></svg><svg data-labels="true" preserveAspectRatio="xMaxYMid" viewBox="1278 -1403 1 2306"><g data-labels="true" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mtd" id="mjx-eqn:2" transform="translate(0,727)"><text data-id-align="true"></text><g data-idbox="true" transform="translate(0,-750)"><g data-mml-node="mtext"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(389,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(889,0)"></path></g></g></g></g></svg></g></g></g></g></svg></mjx-container></p><p><mjx-container class="MathJax" jax="SVG" display="true" width="full" style="min-width: 53.138ex;"><svg style="vertical-align: -2.002ex; min-width: 53.138ex;" xmlns="http://www.w3.org/2000/svg" width="100%" height="5.135ex" role="img" focusable="false"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(0.0181,-0.0181) translate(0, -1384.9)"><g data-mml-node="math"><g data-mml-node="mtable" transform="translate(2078,0) translate(-2078,0)"><g transform="translate(0 1384.9) matrix(1 0 0 -1 0 0) scale(55.25)"><svg data-table="true" preserveAspectRatio="xMidYMid" viewBox="9665.5 -1384.9 1 2269.8"><g transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mlabeledtr" transform="translate(0,-41.1)"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path></g><g data-mml-node="mi" transform="translate(429,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(958,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="msub" transform="translate(1427,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mi" transform="translate(499,-150) scale(0.707)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g></g><g data-mml-node="mi" transform="translate(2282.2,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(2767.2,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(3236.2,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(3875,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mfrac" transform="translate(4930.7,0)"><g data-mml-node="mrow" transform="translate(1456.3,676)"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mi" transform="translate(500,0)"><text data-variant="normal" transform="scale(1,-1)" font-size="884px" font-family="serif">秒</text></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="mi"><path data-c="1D43C" d="M43 1Q26 1 26 10Q26 12 29 24Q34 43 39 45Q42 46 54 46H60Q120 46 136 53Q137 53 138 54Q143 56 149 77T198 273Q210 318 216 344Q286 624 286 626Q284 630 284 631Q274 637 213 637H193Q184 643 189 662Q193 677 195 680T209 683H213Q285 681 359 681Q481 681 487 683H497Q504 676 504 672T501 655T494 639Q491 637 471 637Q440 637 407 634Q393 631 388 623Q381 609 337 432Q326 385 315 341Q245 65 245 59Q245 52 255 50T307 46H339Q345 38 345 37T342 19Q338 6 332 0H316Q279 2 179 2Q143 2 113 2T65 2T43 1Z"></path></g><g data-mml-node="mi" transform="translate(504,0)"><path data-c="1D442" d="M740 435Q740 320 676 213T511 42T304 -22Q207 -22 138 35T51 201Q50 209 50 244Q50 346 98 438T227 601Q351 704 476 704Q514 704 524 703Q621 689 680 617T740 435ZM637 476Q637 565 591 615T476 665Q396 665 322 605Q242 542 200 428T157 216Q157 126 200 73T314 19Q404 19 485 98T608 313Q637 408 637 476Z"></path></g><g data-mml-node="mi" transform="translate(1267,0)"><path data-c="1D443" d="M287 628Q287 635 230 637Q206 637 199 638T192 648Q192 649 194 659Q200 679 203 681T397 683Q587 682 600 680Q664 669 707 631T751 530Q751 453 685 389Q616 321 507 303Q500 302 402 301H307L277 182Q247 66 247 59Q247 55 248 54T255 50T272 48T305 46H336Q342 37 342 35Q342 19 335 5Q330 0 319 0Q316 0 282 1T182 2Q120 2 87 2T51 1Q33 1 33 11Q33 13 36 25Q40 41 44 43T67 46Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628ZM645 554Q645 567 643 575T634 597T609 619T560 635Q553 636 480 637Q463 637 445 637T416 636T404 636Q391 635 386 627Q384 621 367 550T332 412T314 344Q314 342 395 342H407H430Q542 342 590 392Q617 419 631 471T645 554Z"></path></g><g data-mml-node="msub" transform="translate(2018,0)"><g data-mml-node="mi"><path data-c="1D446" d="M308 24Q367 24 416 76T466 197Q466 260 414 284Q308 311 278 321T236 341Q176 383 176 462Q176 523 208 573T273 648Q302 673 343 688T407 704H418H425Q521 704 564 640Q565 640 577 653T603 682T623 704Q624 704 627 704T632 705Q645 705 645 698T617 577T585 459T569 456Q549 456 549 465Q549 471 550 475Q550 478 551 494T553 520Q553 554 544 579T526 616T501 641Q465 662 419 662Q362 662 313 616T263 510Q263 480 278 458T319 427Q323 425 389 408T456 390Q490 379 522 342T554 242Q554 216 546 186Q541 164 528 137T492 78T426 18T332 -20Q320 -22 298 -22Q199 -22 144 33L134 44L106 13Q83 -14 78 -18T65 -22Q52 -22 52 -14Q52 -11 110 221Q112 227 130 227H143Q149 221 149 216Q149 214 148 207T144 186T142 153Q144 114 160 87T203 47T255 29T308 24Z"></path></g><g data-mml-node="TeXAtom" transform="translate(646,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g><g data-mml-node="mi" transform="translate(500,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z"></path></g><g data-mml-node="mi" transform="translate(1021,0)"><path data-c="1D435" d="M231 637Q204 637 199 638T194 649Q194 676 205 682Q206 683 335 683Q594 683 608 681Q671 671 713 636T756 544Q756 480 698 429T565 360L555 357Q619 348 660 311T702 219Q702 146 630 78T453 1Q446 0 242 0Q42 0 39 2Q35 5 35 10Q35 17 37 24Q42 43 47 45Q51 46 62 46H68Q95 46 128 49Q142 52 147 61Q150 65 219 339T288 628Q288 635 231 637ZM649 544Q649 574 634 600T585 634Q578 636 493 637Q473 637 451 637T416 636H403Q388 635 384 626Q382 622 352 506Q352 503 351 500L320 374H401Q482 374 494 376Q554 386 601 434T649 544ZM595 229Q595 273 572 302T512 336Q506 337 429 337Q311 337 310 336Q310 334 293 263T258 122L240 52Q240 48 252 48T333 46Q422 46 429 47Q491 54 543 105T595 229Z"></path></g></g></g></g><rect width="4172.7" height="60" x="120" y="220"></rect></g><g data-mml-node="mtext" transform="translate(9343.4,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(9593.4,0)"><g data-mml-node="mo"><path data-c="200B" d=""></path></g></g><g data-mml-node="mo" transform="translate(9815.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(10815.8,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(11065.8,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mi" transform="translate(11534.8,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(11879.8,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="msub" transform="translate(12344.8,0)"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mi" transform="translate(499,-150) scale(0.707)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></g></g><g data-mml-node="mi" transform="translate(13200,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z"></path></g><g data-mml-node="mi" transform="translate(13685,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="msub" transform="translate(14154,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mi" transform="translate(14916.9,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path></g><g data-mml-node="mi" transform="translate(15445.9,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(15806.9,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mtext" transform="translate(16272.9,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(16522.9,0)"><path data-c="D7" d="M630 29Q630 9 609 9Q604 9 587 25T493 118L389 222L284 117Q178 13 175 11Q171 9 168 9Q160 9 154 15T147 29Q147 36 161 51T255 146L359 250L255 354Q174 435 161 449T147 471Q147 480 153 485T168 490Q173 490 175 489Q178 487 284 383L389 278L493 382Q570 459 587 475T609 491Q630 491 630 471Q630 464 620 453T522 355L418 250L522 145Q606 61 618 48T630 29Z"></path></g><g data-mml-node="mtext" transform="translate(17300.9,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mn" transform="translate(17550.9,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g><g data-mml-node="mi" transform="translate(18050.9,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z"></path></g><g data-mml-node="mi" transform="translate(18571.9,0)"><path data-c="1D435" d="M231 637Q204 637 199 638T194 649Q194 676 205 682Q206 683 335 683Q594 683 608 681Q671 671 713 636T756 544Q756 480 698 429T565 360L555 357Q619 348 660 311T702 219Q702 146 630 78T453 1Q446 0 242 0Q42 0 39 2Q35 5 35 10Q35 17 37 24Q42 43 47 45Q51 46 62 46H68Q95 46 128 49Q142 52 147 61Q150 65 219 339T288 628Q288 635 231 637ZM649 544Q649 574 634 600T585 634Q578 636 493 637Q473 637 451 637T416 636H403Q388 635 384 626Q382 622 352 506Q352 503 351 500L320 374H401Q482 374 494 376Q554 386 601 434T649 544ZM595 229Q595 273 572 302T512 336Q506 337 429 337Q311 337 310 336Q310 334 293 263T258 122L240 52Q240 48 252 48T333 46Q422 46 429 47Q491 54 543 105T595 229Z"></path></g></g></g></g></svg><svg data-labels="true" preserveAspectRatio="xMaxYMid" viewBox="1278 -1384.9 1 2269.8"><g data-labels="true" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mtd" id="mjx-eqn:3" transform="translate(0,708.9)"><text data-id-align="true"></text><g data-idbox="true" transform="translate(0,-750)"><g data-mml-node="mtext"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(389,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(889,0)"></path></g></g></g></g></svg></g></g></g></g></svg></mjx-container></p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626233918-1.jpeg"><br>图 6：IOCost 配置示例</p><p>图 6 显示了一个示例配置。对于读取操作，转化为每字节 2.05 纳秒的 <code>size cost rate</code>，顺序 <code>base cost</code> 为 104 微秒，随机 <code>base cost</code> 为 109 微秒。相应地，一个 32KB 的随机读取 bio 请求的成本将是 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="34.783ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 15373.9 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" transform="translate(1000,0)"></path></g><g data-mml-node="mi" transform="translate(1500,0)"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(2072,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(2763.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(3763.4,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(4985.7,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mn" transform="translate(5707.9,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" transform="translate(1000,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1500,0)"></path></g><g data-mml-node="mo" transform="translate(7930.1,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mn" transform="translate(8652.3,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="2E" d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" transform="translate(500,0)"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(778,0)"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(1278,0)"></path></g><g data-mml-node="mi" transform="translate(10430.3,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(11030.3,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(11777.1,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(12832.9,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(1000,0)"></path></g><g data-mml-node="mi" transform="translate(14332.9,0)"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mi" transform="translate(14904.9,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container>，并且设备每秒钟能够处理 2840 个这样的请求。</p><p>我们的工具使用 <code>fio</code> 和饱和工作负载来推断设备的线性模型参数，例如，通过尽可能多地发出 4KB 随机读取请求来确定随机读取的 <code>base cost</code>。即使在 Meta 数据中心中存在大约三十种不同的存储设备，以这种方式系统地对设备进行建模仍然是可行的。我们已经将我们的建模工具集成到了 Linux 内核源码树。</p><h4 id="3-3-QoS-和动态-vrate-调整"><a href="#3-3-QoS-和动态-vrate-调整" class="headerlink" title="3.3 QoS 和动态 vrate 调整"></a>3.3 QoS 和动态 vrate 调整</h4><p>简单的线性建模无法捕捉现代 SSD 的复杂性。这些设备有着复杂的缓存层、请求重排序、垃圾回收机制，面对不同的 I/O 混合模式时，其表现往往出乎预料。先前的研究着重强调了精确建模 SSD 行为的难度。IOCost 通过动态调整 <code>vrate</code> 来应对设备性能的波动。</p><p><code>vrate</code> 调整基于两个信号：I/O 预算不足和设备饱和。前者表明内核本可以发出更多 I/O，但由于由 <code>vtime</code> 决定的全局预算限制而无法做到。后者则表明设备无法处理更多的 I/O。如果系统能够发出更多 I/O 且设备并未饱和，<code>vrate</code> 将向上调整。反之，若设备处于饱和状态，<code>vrate</code> 则向下调整。</p><p>IOCost 通过追踪请求耗尽和延迟目标超限来识别设备是否饱和。当正在进行的 I/O 请求过多，耗尽了可用的 I/O 槽位，导致设备层出现长队列时，即发生请求耗尽。延迟目标是通过 QoS 参数设定的。例如，系统管理员可以配置，如果 90 百分位的读取完成延迟超过 10 毫秒，则认为设备处于饱和状态。</p><p>通过限制向设备发出的总 I/O 量，即便是在表现出突发行为或设备模型难以完全捕捉的其他行为的设备上，IOCost 也能实现一致的延迟。IOCost 将延迟视为设备层面的属性。它使用 QoS 参数来调控设备行为，然后分配由此产生的 I/O 占用。这种分离简化了工作负载的配置，并且对于保持 QoS 目标是必要的。理论上，如果我们放宽对批处理工作负载的设备限流，可能会失去对设备的控制，在延迟敏感型工作负载激活时，无法达到 QoS 目标。</p><h4 id="3-4-使用-ResourceControlBench-调整-QoS-参数"><a href="#3-4-使用-ResourceControlBench-调整-QoS-参数" class="headerlink" title="3.4 使用 ResourceControlBench 调整 QoS 参数"></a>3.4 使用 ResourceControlBench 调整 QoS 参数</h4><p>QoS 参数决定了设备的整体限流，这是在设备利用率与一致延迟之间的重要权衡。最终，如何做出这种权衡取决于存储的使用场景。在 Meta，主要的考量是确保在竞争情况下合理的 I/O 延迟，而原始吞吐量则作为次要考量。</p><p>为了确保设备能够得到充分的限流，我们为 Meta 机群中的每个设备开发了一套系统性的方法来确定 QoS 参数。虽然完整描述超出了本文的范畴，但我们提供了一个简化的说明。</p><p>我们开发了 ResourceControlBench，这是一个高度可配置的复合工作负载，模仿了 Meta 中延迟敏感服务的行为。我们通过观察 ResourceControlBench 在不同 <code>vrate</code> 范围内的行为，利用它来进行 QoS 调整。我们在两种场景下执行 ResourceControlBench。</p><p>首先，ResourceControlBench 独占机器运行，并调整其工作集大小，直到可用于分页和交换操作的吞吐量开始限制 ResourceControlBench 的性能。随着 <code>vrate</code> 的降低，工作集大小也会下降。其次，ResourceControlBench 与另一个容器中的内存泄露一起运行。随着 <code>vrate</code> 的降低，I/O 控制得到改善，直到 ResourceControlBench 的延迟得到了充分保护，免受内存泄露导致的抖动影响。</p><p>这两种场景确定了 <code>vrate</code> 范围的两点，低于这两点不再需要进一步的 I/O 控制改进，高于这两点吞吐量的提升对于内存超额分配并没有带来实质性的好处。我们将每个设备的 <code>vrate</code> 设定在这两点之间。这些 QoS 参数被部署到机群中的每个设备上，从而实现了所有应用程序的一致延迟控制，并将吞吐量损失降至最低。ResourceControlBench 和场景生成工具作为开源软件提供。</p><h4 id="3-5-处置优先级反转"><a href="#3-5-处置优先级反转" class="headerlink" title="3.5 处置优先级反转"></a>3.5 处置优先级反转</h4><p>考虑两个具有相同权重的 cgroup，A 和 B，在同一台机器上运行。A 持续泄露内存。当机器内存不足时，B 尝试分配内存并进入内存回收流程，此时会识别出 A 的一部分内存用于换出。由于这部分被换出的内存属于 A，因此换出 bio 的成本只能合理地归咎于 A。如果这个成本被记在 B 上，B 将因为 A 的过度内存使用而受到惩罚，破坏了资源隔离。</p><p>为使 B 完成内存分配，这个换出操作必须同步完成。如果 A 超过了其预算，对其进行限速会导致优先级反转，即 B 再次因为 A 的内存过度使用而受罚。IOCost 解决这个问题的方法是允许 A 产生“债务”，并在不进行限速的情况下发出 I/O 操作。A 的未来 bio 将按比例进行限速，直到用未来的预算还清债务为止。</p><p>然而，如果 A 泄露内存但没有发出可以被限速的 I/O，A 将获得不公平的大量“免费”换出 I/O，并且永远无法偿还债务。为解决这个问题，IOCost 在每次返回用户空间前添加了一个检查。如果累积的债务超过了阈值，线程会在返回用户空间前短暂阻塞，以此来限制“免费”I/O 的生成。结果，生成交换的内存活动被限速，而不会造成优先级反转。同样的机制也用于共享文件系统等操作，如日志记录。</p><h4 id="3-6-预算捐赠"><a href="#3-6-预算捐赠" class="headerlink" title="3.6 预算捐赠"></a>3.6 预算捐赠</h4><p>单个 cgroup 并不总是会发出达到 <code>hweight</code> 的 I/O 请求量。为了工作量保持，IOCost 通过动态降低捐赠者 cgroup 的权重，允许其他 cgroup 利用存储设备。我们研究了多种方案，包括暂时加速 <code>vrate</code>，但发现只有局部调整权重的策略能够满足以下所有要求：1）I/O 问题路径保持低开销；2）发出的 I/O 总量不会超过 <code>vrate</code> 设定的限制；3）捐赠者可随时低成本撤销捐赠。</p><p>每个规划阶段都会识别出捐赠者，并计算出它们能捐出多少 <code>hweight</code>。随后，它会计算出捐赠 <code>hweight</code> 后已降低的权重。权重计算的过程设计得让父节点的权重调整完全由子节点的权重变化决定。</p><p>由于捐赠是通过权重调整实现的，所以 I/O 问题路径不会发生变化，也不会与设备级别的行为交互，这样就满足了前两项要求。捐赠者只需更新自己的权重，并沿问题路径向上传播更新，无需任何全局操作即可撤销捐赠，从而满足了最后的要求。这会增加权重树的代数，后续的 I/O 发出者会重新计算它们的 <code>hweight</code>。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234109-1.jpeg"><br>图 7：规划阶段（a）、规划阶段之后（b）和问题路径期间（c）的预算捐赠示例</p><p><strong>高层次捐赠示例。</strong> 在图 7(a) 中，容器 A 和 B 的权重分别是 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.816ex;" xmlns="http://www.w3.org/2000/svg" width="1.795ex" height="2.773ex" role="img" focusable="false" viewBox="0 -864.9 793.6 1225.5"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mn" transform="translate(220,394) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mn" transform="translate(220,-345) scale(0.707)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><rect width="553.6" height="60" x="120" y="220"></rect></g></g></g></svg></mjx-container> 和 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.816ex;" xmlns="http://www.w3.org/2000/svg" width="1.795ex" height="2.773ex" role="img" focusable="false" viewBox="0 -864.9 793.6 1225.5"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mfrac"><g data-mml-node="mn" transform="translate(220,394) scale(0.707)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mn" transform="translate(220,-345) scale(0.707)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><rect width="553.6" height="60" x="120" y="220"></rect></g></g></g></svg></mjx-container>。在规划阶段，发现 B 未使用其一半的预算。为了避免设备利用率低下，系统将 B 原始预算的一半转移给了 A。图 7(b) 显示了这一变动对第二周期的影响。随着 <code>hweight</code> 增加，A 的 I/O 相对成本降低，可以更频繁地发出，而 B 则达到其降低后的新预算上限。在周期末尾，不再需要进一步调整。图 7(c) 显示，在第三个周期中期，B 尝试发出更多 I/O，并在问题路径中撤销捐赠，无需等待下一个规划阶段。值得注意的是，容器也可以只撤销其原始捐赠的一部分。</p><p><strong>权重树更新算法。</strong> 令 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.62ex" height="1.027ex" role="img" focusable="false" viewBox="0 -443 716 454"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g></g></g></svg></mjx-container> 表示权重，<mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.061ex" height="1.023ex" role="img" focusable="false" viewBox="0 -442 469 452"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g></g></g></svg></mjx-container> 表示兄弟节点的权重总和，<mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.303ex" height="1.595ex" role="img" focusable="false" viewBox="0 -694 576 705"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g></g></g></svg></mjx-container> 表示 <code>hweight</code>，而 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.176ex" height="1.593ex" role="img" focusable="false" viewBox="0 -694 520 704"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g></g></svg></mjx-container> 则代表子树中所有捐赠叶节点的 <code>hweight</code> 总和。下标 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="1.138ex" height="1.439ex" role="img" focusable="false" viewBox="0 -442 503 636"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g></svg></mjx-container> 标记父节点，而撇号（’）表示捐赠后的数值。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234340-1.jpeg"><br>图 8：B 和 H 捐赠了部分预算</p><p>图 8 显示了预算捐赠过程。在这个例子中，叶节点 B 和 H 的活跃使用量总共比它们设定的 <code>hweight</code> 少 0.25。过剩的部分被捐赠给其他可以按照它们的层级权重比例使用更多 I/O 的 cgroup。最重要的是，只需要局部更新，因为 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.62ex" height="1.027ex" role="img" focusable="false" viewBox="0 -443 716 454"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g></g></g></svg></mjx-container> 的值仅沿着从 B 和 H 到根节点的路径递减，之后所有其它节点都可以在问题路径中懒惰地计算出它们新的 <code>hweight</code>。</p><p>捐赠值 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.804ex" height="1.74ex" role="img" focusable="false" viewBox="0 -759 797.5 769"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 会沿着树向上传播，作为预算捐赠算法的输入。仅需沿着从根到捐赠子节点 B、D 和 H 的路径计算更新后的权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 和层级权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container>。为了确保非捐赠节点不需要更新，我们维持了两个不变性质，进而推导出更新后的层级权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container>、兄弟节点的权重总和 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.689ex" height="1.74ex" role="img" focusable="false" viewBox="0 -759 746.5 769"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 和更新后的权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 。</p><p>第一个不变性质强制规定了父节点的非捐赠权重占比在预算捐赠后不会改变。</p><p><mjx-container class="MathJax" jax="SVG" display="true" width="full" style="min-width: 32.265ex;"><svg style="vertical-align: -2.268ex; min-width: 32.265ex;" xmlns="http://www.w3.org/2000/svg" width="100%" height="5.668ex" role="img" focusable="false"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(0.0181,-0.0181) translate(0, -1502.6)"><g data-mml-node="math"><g data-mml-node="mtable" transform="translate(2078,0) translate(-2078,0)"><g transform="translate(0 1502.6) matrix(1 0 0 -1 0 0) scale(55.25)"><svg data-table="true" preserveAspectRatio="xMidYMid" viewBox="5052.6 -1502.6 1 2505.2"><g transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mlabeledtr" transform="translate(0,67.6)"><g data-mml-node="mtd"><g data-mml-node="mfrac"><g data-mml-node="mrow" transform="translate(658.7,676)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mtext" transform="translate(576,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1048.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2048.4,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(2298.4,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1486.9,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2487.1,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msub" transform="translate(2737.1,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><rect width="3895.8" height="60" x="120" y="220"></rect></g><g data-mml-node="mtext" transform="translate(4135.8,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(4663.6,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mtext" transform="translate(5719.3,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mfrac" transform="translate(5969.3,0)"><g data-mml-node="mrow" transform="translate(381.2,676)"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(853.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1325.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2325.9,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msup" transform="translate(2575.9,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g><g data-mml-node="mrow" transform="translate(220,-686)"><g data-mml-node="msubsup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1486.9,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2487.1,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msubsup" transform="translate(2737.1,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(553,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><rect width="3895.8" height="60" x="120" y="220"></rect></g></g></g></g></svg><svg data-labels="true" preserveAspectRatio="xMaxYMid" viewBox="1278 -1502.6 1 2505.2"><g data-labels="true" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mtd" id="mjx-eqn:4" transform="translate(0,817.6)"><text data-id-align="true"></text><g data-idbox="true" transform="translate(0,-750)"><g data-mml-node="mtext"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(389,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(889,0)"></path></g></g></g></g></svg></g></g></g></g></svg></mjx-container></p><p>第二个不变性质保证了所有未捐赠的兄弟节点的汇总权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.62ex" height="1.027ex" role="img" focusable="false" viewBox="0 -443 716 454"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g></g></g></svg></mjx-container> 在预算捐赠后不会发生变化。</p><p><mjx-container class="MathJax" jax="SVG" display="true" width="full" style="min-width: 46.328ex;"><svg style="vertical-align: -2.827ex; min-width: 46.328ex;" xmlns="http://www.w3.org/2000/svg" width="100%" height="6.785ex" role="img" focusable="false"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(0.0181,-0.0181) translate(0, -1749.5)"><g data-mml-node="math"><g data-mml-node="mtable" transform="translate(2078,0) translate(-2078,0)"><g transform="translate(0 1749.5) matrix(1 0 0 -1 0 0) scale(55.25)"><svg data-table="true" preserveAspectRatio="xMidYMid" viewBox="8160.5 -1749.5 1 2999"><g transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mlabeledtr"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mtext" transform="translate(469,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(941.2,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(1663.4,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1913.4,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M758 -1237T758 -1240T752 -1249H736Q718 -1249 717 -1248Q711 -1245 672 -1199Q237 -706 237 251T672 1700Q697 1730 716 1749Q718 1750 735 1750H752Q758 1744 758 1741Q758 1737 740 1713T689 1644T619 1537T540 1380T463 1176Q348 802 348 251Q348 -242 441 -599T744 -1218Q758 -1237 758 -1240Z"></path></g></g><g data-mml-node="mfrac" transform="translate(2705.4,0)"><g data-mml-node="mrow" transform="translate(220,747.2)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1486.9,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2487.1,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msub" transform="translate(2737.1,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><g data-mml-node="msub" transform="translate(1560.6,-686)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><rect width="3895.8" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6841.2,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M33 1741Q33 1750 51 1750H60H65Q73 1750 81 1743T119 1700Q554 1207 554 251Q554 -707 119 -1199Q76 -1250 66 -1250Q65 -1250 62 -1250T56 -1249Q55 -1249 53 -1249T49 -1250Q33 -1250 33 -1239Q33 -1236 50 -1214T98 -1150T163 -1052T238 -910T311 -727Q443 -335 443 251Q443 402 436 532T405 831T339 1142T224 1438T50 1716Q33 1737 33 1741Z"></path></g></g><g data-mml-node="mtext" transform="translate(7633.2,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(8161,0)"><g data-mml-node="text"><path data-c="3A" d="M78 370Q78 394 95 412T138 430Q162 430 180 414T199 371Q199 346 182 328T139 310T96 327T78 370ZM78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z"></path></g><g data-mml-node="text" transform="translate(278,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g></g><g data-mml-node="mtext" transform="translate(9494.8,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msup" transform="translate(9744.8,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,413) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(10491.2,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mtext" transform="translate(10741.2,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(10991.2,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M180 96T180 250T205 541T266 770T353 944T444 1069T527 1150H555Q561 1144 561 1141Q561 1137 545 1120T504 1072T447 995T386 878T330 721T288 513T272 251Q272 133 280 56Q293 -87 326 -209T399 -405T475 -531T536 -609T561 -640Q561 -643 555 -649H527Q483 -612 443 -568T353 -443T266 -270T205 -41Z"></path></g></g><g data-mml-node="mfrac" transform="translate(11588.2,0)"><g data-mml-node="mrow" transform="translate(220,844.2)"><g data-mml-node="msubsup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1486.9,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2487.1,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msubsup" transform="translate(2737.1,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(553,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><g data-mml-node="msubsup" transform="translate(1560.6,-686)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><rect width="3895.8" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(15724,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M35 1138Q35 1150 51 1150H56H69Q113 1113 153 1069T243 944T330 771T391 541T416 250T391 -40T330 -270T243 -443T152 -568T69 -649H56Q43 -649 39 -647T35 -637Q65 -607 110 -548Q283 -316 316 56Q324 133 324 251Q324 368 316 445Q278 877 48 1123Q36 1137 35 1138Z"></path></g></g></g></g></g></svg><svg data-labels="true" preserveAspectRatio="xMaxYMid" viewBox="1278 -1749.5 1 2999"><g data-labels="true" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="mtd" id="mjx-eqn:5"><g data-mml-node="mtext"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(389,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(889,0)"></path></g></g></g></svg></g></g></g></g></svg></mjx-container></p><p><strong>步骤</strong></p><p>(1) 使用公式 (4) 的约束从父节点层级权重 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 值计算新的 hweight: <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -2.827ex;" xmlns="http://www.w3.org/2000/svg" width="35.893ex" height="6.785ex" role="img" focusable="false" viewBox="0 -1749.5 15864.7 2999"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(853.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1381.2,0)"><g data-mml-node="text"><path data-c="3A" d="M78 370Q78 394 95 412T138 430Q162 430 180 414T199 371Q199 346 182 328T139 310T96 327T78 370ZM78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z"></path></g><g data-mml-node="text" transform="translate(278,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g></g><g data-mml-node="mtext" transform="translate(2715,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(2965,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M758 -1237T758 -1240T752 -1249H736Q718 -1249 717 -1248Q711 -1245 672 -1199Q237 -706 237 251T672 1700Q697 1730 716 1749Q718 1750 735 1750H752Q758 1744 758 1741Q758 1737 740 1713T689 1644T619 1537T540 1380T463 1176Q348 802 348 251Q348 -242 441 -599T744 -1218Q758 -1237 758 -1240Z"></path></g></g><g data-mml-node="mfrac" transform="translate(3757,0)"><g data-mml-node="mrow" transform="translate(530.2,398) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mtext" transform="translate(576,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(826,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(1604,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(1854,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g><g data-mml-node="mrow" transform="translate(220,-345) scale(0.707)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1264.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2042.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msub" transform="translate(2292.7,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><rect width="2499.1" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(6496.1,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M33 1741Q33 1750 51 1750H60H65Q73 1750 81 1743T119 1700Q554 1207 554 251Q554 -707 119 -1199Q76 -1250 66 -1250Q65 -1250 62 -1250T56 -1249Q55 -1249 53 -1249T49 -1250Q33 -1250 33 -1239Q33 -1236 50 -1214T98 -1150T163 -1052T238 -910T311 -727Q443 -335 443 251Q443 402 436 532T405 831T339 1142T224 1438T50 1716Q33 1737 33 1741Z"></path></g></g><g data-mml-node="mtext" transform="translate(7288.1,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(7760.3,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(8482.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(8732.5,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M180 96T180 250T205 541T266 770T353 944T444 1069T527 1150H555Q561 1144 561 1141Q561 1137 545 1120T504 1072T447 995T386 878T330 721T288 513T272 251Q272 133 280 56Q293 -87 326 -209T399 -405T475 -531T536 -609T561 -640Q561 -643 555 -649H527Q483 -612 443 -568T353 -443T266 -270T205 -41Z"></path></g></g><g data-mml-node="msubsup" transform="translate(9329.5,0)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(10344.2,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(10816.4,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(11816.6,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msubsup" transform="translate(12066.6,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(553,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(13025.3,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M35 1138Q35 1150 51 1150H56H69Q113 1113 153 1069T243 944T330 771T391 541T416 250T391 -40T330 -270T243 -443T152 -568T69 -649H56Q43 -649 39 -647T35 -637Q65 -607 110 -548Q283 -316 316 56Q324 133 324 251Q324 368 316 445Q278 877 48 1123Q36 1137 35 1138Z"></path></g></g><g data-mml-node="mtext" transform="translate(13622.3,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(14094.5,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mtext" transform="translate(15094.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(15344.7,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g></g></svg></mjx-container><br>(2) 基于公式 (5) 的约束计算新的兄弟节点的权重总和: <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -2.827ex;" xmlns="http://www.w3.org/2000/svg" width="32.742ex" height="6.785ex" role="img" focusable="false" viewBox="0 -1749.5 14472 2999"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(746.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1274.2,0)"><g data-mml-node="text"><path data-c="3A" d="M78 370Q78 394 95 412T138 430Q162 430 180 414T199 371Q199 346 182 328T139 310T96 327T78 370ZM78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z"></path></g><g data-mml-node="text" transform="translate(278,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g></g><g data-mml-node="mtext" transform="translate(2608,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mi" transform="translate(2858,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mtext" transform="translate(3327,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(3799.2,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(4521.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4771.5,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M758 -1237T758 -1240T752 -1249H736Q718 -1249 717 -1248Q711 -1245 672 -1199Q237 -706 237 251T672 1700Q697 1730 716 1749Q718 1750 735 1750H752Q758 1744 758 1741Q758 1737 740 1713T689 1644T619 1537T540 1380T463 1176Q348 802 348 251Q348 -242 441 -599T744 -1218Q758 -1237 758 -1240Z"></path></g></g><g data-mml-node="mfrac" transform="translate(5563.5,0)"><g data-mml-node="mrow" transform="translate(220,543.1) scale(0.707)"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1264.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2042.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msub" transform="translate(2292.7,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mi" transform="translate(553,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><g data-mml-node="msub" transform="translate(1010.8,-345) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mi" transform="translate(609,-150) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><rect width="2499.1" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(8302.5,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M33 1741Q33 1750 51 1750H60H65Q73 1750 81 1743T119 1700Q554 1207 554 251Q554 -707 119 -1199Q76 -1250 66 -1250Q65 -1250 62 -1250T56 -1249Q55 -1249 53 -1249T49 -1250Q33 -1250 33 -1239Q33 -1236 50 -1214T98 -1150T163 -1052T238 -910T311 -727Q443 -335 443 251Q443 402 436 532T405 831T339 1142T224 1438T50 1716Q33 1737 33 1741Z"></path></g></g><g data-mml-node="mtext" transform="translate(9094.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(9566.7,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(10289,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(10539,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M180 96T180 250T205 541T266 770T353 944T444 1069T527 1150H555Q561 1144 561 1141Q561 1137 545 1120T504 1072T447 995T386 878T330 721T288 513T272 251Q272 133 280 56Q293 -87 326 -209T399 -405T475 -531T536 -609T561 -640Q561 -643 555 -649H527Q483 -612 443 -568T353 -443T266 -270T205 -41Z"></path></g></g><g data-mml-node="mfrac" transform="translate(11136,0)"><g data-mml-node="msubsup" transform="translate(1010.8,611.7) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mrow" transform="translate(220,-345) scale(0.707)"><g data-mml-node="msubsup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><g data-mml-node="mtext" transform="translate(1014.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1264.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mtext" transform="translate(2042.7,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msubsup" transform="translate(2292.7,0)"><g data-mml-node="mi"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(553,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(553,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g></g><rect width="2499.1" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(13875,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M35 1138Q35 1150 51 1150H56H69Q113 1113 153 1069T243 944T330 771T391 541T416 250T391 -40T330 -270T243 -443T152 -568T69 -649H56Q43 -649 39 -647T35 -637Q65 -607 110 -548Q283 -316 316 56Q324 133 324 251Q324 368 316 445Q278 877 48 1123Q36 1137 35 1138Z"></path></g></g></g></g></svg></mjx-container><br>(3) 最终的权重由计算出的 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 和 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.689ex" height="1.74ex" role="img" focusable="false" viewBox="0 -759 746.5 769"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 得出: <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -1.469ex;" xmlns="http://www.w3.org/2000/svg" width="22.259ex" height="4.07ex" role="img" focusable="false" viewBox="0 -1149.5 9838.3 1799"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(746.5,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(1218.7,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(1940.9,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msup" transform="translate(2190.9,0)"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(3184.4,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(3712.1,0)"><g data-mml-node="text"><path data-c="3A" d="M78 370Q78 394 95 412T138 430Q162 430 180 414T199 371Q199 346 182 328T139 310T96 327T78 370ZM78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z"></path></g><g data-mml-node="text" transform="translate(278,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g></g><g data-mml-node="mtext" transform="translate(5045.9,0)"><path data-c="A0" d=""></path></g><g data-mml-node="msup" transform="translate(5295.9,0)"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="mtext" transform="translate(6042.4,0)"><path data-c="A0" d=""></path></g><g data-mml-node="mo" transform="translate(6514.6,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mtext" transform="translate(7236.8,0)"><path data-c="A0" d=""></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(7486.8,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="28" d="M180 96T180 250T205 541T266 770T353 944T444 1069T527 1150H555Q561 1144 561 1141Q561 1137 545 1120T504 1072T447 995T386 878T330 721T288 513T272 251Q272 133 280 56Q293 -87 326 -209T399 -405T475 -531T536 -609T561 -640Q561 -643 555 -649H527Q483 -612 443 -568T353 -443T266 -270T205 -41Z"></path></g></g><g data-mml-node="mfrac" transform="translate(8083.8,0)"><g data-mml-node="msup" transform="translate(277,394) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g><g data-mml-node="msubsup" transform="translate(220,-345) scale(0.707)"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,289) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g><g data-mml-node="mi" transform="translate(609,-247) scale(0.707)"><path data-c="1D45D" d="M23 287Q24 290 25 295T30 317T40 348T55 381T75 411T101 433T134 442Q209 442 230 378L240 387Q302 442 358 442Q423 442 460 395T497 281Q497 173 421 82T249 -10Q227 -10 210 -4Q199 1 187 11T168 28L161 36Q160 35 139 -51T118 -138Q118 -144 126 -145T163 -148H188Q194 -155 194 -157T191 -175Q188 -187 185 -190T172 -194Q170 -194 161 -194T127 -193T65 -192Q-5 -192 -24 -194H-32Q-39 -187 -39 -183Q-37 -156 -26 -148H-6Q28 -147 33 -136Q36 -130 94 103T155 350Q156 355 156 364Q156 405 131 405Q109 405 94 377T71 316T59 280Q57 278 43 278H29Q23 284 23 287ZM178 102Q200 26 252 26Q282 26 310 49T356 107Q374 141 392 215T411 325V331Q411 405 350 405Q339 405 328 402T306 393T286 380T269 365T254 350T243 336T235 326L232 322Q232 321 229 308T218 264T204 212Q178 106 178 102Z"></path></g></g><rect width="917.5" height="60" x="120" y="220"></rect></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(9241.3,0)"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="29" d="M35 1138Q35 1150 51 1150H56H69Q113 1113 153 1069T243 944T330 771T391 541T416 250T391 -40T330 -270T243 -443T152 -568T69 -649H56Q43 -649 39 -647T35 -637Q65 -607 110 -548Q283 -316 316 56Q324 133 324 251Q324 368 316 445Q278 877 48 1123Q36 1137 35 1138Z"></path></g></g></g></g></svg></mjx-container></p><p>在其他节点上完整展示剩余的 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container>、<mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.023ex;" xmlns="http://www.w3.org/2000/svg" width="1.689ex" height="1.74ex" role="img" focusable="false" viewBox="0 -759 746.5 769"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z"></path></g><g data-mml-node="mo" transform="translate(502,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container>、<mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 值是为了完整性，但在预算捐赠的过程中，这些值并非必要。值得注意的是，对于其他节点而言，<mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 的值并不会改变。这种效率对于庞大的 cgroup 层次结构非常重要。仅需沿着从捐赠叶子节点到根节点的路径更新 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container>，所有其他节点基于这些 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.248ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 993.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D464" d="M580 385Q580 406 599 424T641 443Q659 443 674 425T690 368Q690 339 671 253Q656 197 644 161T609 80T554 12T482 -11Q438 -11 404 5T355 48Q354 47 352 44Q311 -11 252 -11Q226 -11 202 -5T155 14T118 53T104 116Q104 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Q21 293 29 315T52 366T96 418T161 441Q204 441 227 416T250 358Q250 340 217 250T184 111Q184 65 205 46T258 26Q301 26 334 87L339 96V119Q339 122 339 128T340 136T341 143T342 152T345 165T348 182T354 206T362 238T373 281Q402 395 406 404Q419 431 449 431Q468 431 475 421T483 402Q483 389 454 274T422 142Q420 131 420 107V100Q420 85 423 71T442 42T487 26Q558 26 600 148Q609 171 620 213T632 273Q632 306 619 325T593 357T580 385Z"></path></g><g data-mml-node="mo" transform="translate(749,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 更新的新 <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.931ex" height="1.742ex" role="img" focusable="false" viewBox="0 -759 853.5 770"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(609,363) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"></path></g></g></g></g></svg></mjx-container> 值就会获得正确的数值。举例说明，在这个例子中，B 和 H 共释放了 0.25 的 <code>hweight</code>，根据 E、F 和 G 最初的 <code>hweight</code> 比例 0.16:0.04:0.35 分配，从而分别向 E、F 和 G 捐赠 0.07、0.02 和 0.16 的 <code>hweight</code>。</p><h3 id="4-评估"><a href="#4-评估" class="headerlink" title="4. 评估"></a>4. 评估</h3><p>本节表明 IOCost 提供了一种低开销、工作量保持、内存管理感知并允许进行比例 cgroup 配置的 I/O 控制。我们将 IOCost 与最新的 Linux I/O 控制机制以及我们在第 2.2 节描述的先前解决方案 IOLatency 进行了比较。显示没有一种机制能够与 IOCost 拥有的特性和性能相媲美。</p><p>在所有的实验中，除非另有说明，我们使用的是单插槽、64GB 内存的服务器，配备三种不同的 SSD：1）较早一代的商用 SSD；2）较新一代的商用 SSD；3）高端企业级 SSD。 我们安装了 5.6 版的 Linux 内核，该内核已经应用了来自 5.15 版本最新的 IOCost 变更。模型参数是通过使用 fio 饱和工作负载确定的，如第 3.2 节所述。QoS 参数是通过使用 ResourceControlBench 确定的，如第 3.4 节所述。</p><h4 id="4-1-低开销"><a href="#4-1-低开销" class="headerlink" title="4.1 低开销"></a>4.1 低开销</h4><p>在数据中心中控制高速 SSD 的 IO 操作，需要控制器具有极小的开销。本次实验采用了一款最大读取 IOPS 为 75 万次的 SSD，我们使用 fio 工具生成尽可能多的 4KB 随机读取，以测试 IO 子系统能支持的最大数量。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234439-1.jpeg"><br>图 9：IO 控制开销</p><p>图 9 测量了启用 IO 控制时，使用多种不同机制所能达到的最大 IOPS。控制器或调度程序并未设置进行实际的限速，这样我们可以测量在快速 IO 问题路径上引入的开销。我们使用企业级 SSD 进行此实验，以展示我们最快存储设备上的开销。我们禁用了所有控制器的 QoS 设置，以便单纯测量在不限制设备时，各控制器的基线开销。</p><p>none 列对应于没有运行任何软件调度器或控制器的情况，展示了该设备上块层可实现的吞吐量。mq-deadline 是 Linux 默认的调度器，具有适度的开销。kyber 的表现与没有调度器时无异。这两种 IO 调度器都不提供 cgroup 控制功能，因为它们只提供系统级的调度。bfq 则有严重的软件开销。尽管我们进行了大量调优，但始终未能找到合理性能的配置。其余的列表明，其他 IO 控制器并没有增加明显的开销。虽然 IOCost 的限速逻辑比其他控制器复杂得多，但由于其将问题拆分为快速的 IO 问题路径和较慢的规划路径，因此能够确保几乎无感的开销。</p><h4 id="4-2-比例控制和工作量保持"><a href="#4-2-比例控制和工作量保持" class="headerlink" title="4.2 比例控制和工作量保持"></a>4.2 比例控制和工作量保持</h4><p>工作量保持 IO 控制对于确保在某些消费者空闲时，存储设备的性能得到充分利用至关重要。如果没有工作量保持 IO 控制，我们就需要为诸如操作系统软件更新等不频繁的活动过度预置 IO 资源。</p><p>为了评估这些特性，我们进行了两个相关的实验，其中两个合成的工作负载同时运行。在第一次实验中，我们运行了两个延迟敏感型工作负载实例，在 p50 延迟低于 200 微秒的情况下，持续发出 4KB 的随机读取请求。这些工作负载模拟了在线服务，如果请求延迟过高可能会导致负载卸载。我们将高优先级工作负载的 IO 配置为低优先级工作负载的两倍。这个实验是在我们较旧一代的 SSD 上进行的，由于它的相对较低的延迟，对 IO 控制的要求更高。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234521-1.jpeg"><br>图 10：比例控制。高优先级和低优先级工作负载接收的 IOPS 目标比例为 2</p><p>图 10 显示了第一次实验的结果。我们仅关注 cgroup 感知的 IO 控制机制。bfq 被配置了期望 2:1 权重比例。然而，高优先级工作负载以超过 10:1 的比例占据主导地位。这是因为低优先级工作负载受到较差的延迟影响，并持续降低其 IO 发出率以保持在 200 微秒的目标之下，这反过来又让高优先级工作负载得以占据主导并接收远超其应得份额的 IO。blk-throttle 被配置为限制每个工作负载以保持 2:1 的比例。它的表现符合预期，与 IOCost 观察到的延迟匹配。IOLatency 没有提供配置此类分布的方法。相反，我们尝试通过调整每个 cgroup 的延迟目标来实现期望的分布，但最佳配置（如图所示）仍然导致大约 10:1 的分布。最后，IOCost 如同 bfq 一样配置权重，并且能够精确匹配预期的 2:1 比例。</p><p>第二次实验保持了相同的配置，只是将高优先级工作负载替换为一个顺序执行、思考时间为 100 微秒、随机 4KB 读取操作的工作负载，即在上一次 I/O 完成后 100 微秒才发起新的 I/O。这次实验所取得的吞吐量取决于读取操作的延迟，远低于之前的实验。因此，低优先级工作负载可利用的吞吐量取决于 I/O 控制器的工作量保持特性。我们预期低优先级工作负载会耗尽剩余的可用 I/O。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234630-1.jpeg"><br>图 11：工作量保持。低优先级的工作负载应该用尽所有可用容量</p><p>图 11 显示了第二次实验的结果。bfq 的工作量保持特性导致低优先级工作负载完成了大量的 I/O 操作。bfq 在这方面超越其他机制的能力，源于其较弱的延迟控制，这反而导致高优先级工作负载表现明显恶化。高优先级工作负载平均延迟为 250 微秒，标准差接近 1 毫秒，而其他所有机制都能将延迟保持在平均 200 微秒以下，标准差约为 200 微秒。这个实验也揭示了非工作量保持方法的主要缺点，例如 blk-throttle，它能很好地控制延迟，但不允许低优先级工作负载消耗比前一次实验更多的 IO。IOLatency 和 IOCost 表现相当，既能控制高优先级工作负载的延迟，又允许低优先级工作负载消耗原本可用的 I/O。这两个实验共同证明了 IOCost 独特地提供了比例和工作量保持的 I/O 控制。</p><h4 id="4-3-机械硬盘建模"><a href="#4-3-机械硬盘建模" class="headerlink" title="4.3 机械硬盘建模"></a>4.3 机械硬盘建模</h4><p>尽管 SSD 构成了 Meta 数据中心的绝大多数，IOCost 同样适用于机械硬盘。与 SSD 不同，机械硬盘具有较高的寻道延迟，这意味着随机 I/O 的吞吐量比顺序 I/O 低（或者说是更高的占用成本）。我们进行了一项实验，其中两个工作负载分别发出随机 4KB 读取或顺序 4KB 读取。其中一个工作负载（高权重）被配置为另一个工作负载（低权重）权重的两倍。我们比较了 mq-deadline，bfq 和 IOCost 在三种情况下的表现：两个工作负载都发出随机读取（rand/rand），高优先级工作负载发出随机读取而低优先级发出顺序读取（rand/seq），以及两者都发出顺序读取（seq/seq）。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234753-1.jpeg"><br>图 12：机械硬盘上随机和顺序工作负载的公平性</p><p>图 12 显示了这次实验的结果。为了清晰地展示差异，我们将随机和顺序工作负载的吞吐量分别标准化为设备能够处理的每种类型工作负载的峰值吞吐量。结果显示，mq-deadline 无法在任何工作负载上以 2:1 的比例提供公平性，因为它只是一个全局调度器。BFQ 在两个工作负载都发出顺序 I/O 时表现出色，保持了预期的 2:1 比例，但在两个工作负载都发出随机 I/O 时遇到困难，尤其是在混合了顺序工作负载时，它过分分配了设备给随机读取工作负载。相比之下，IOCost 通过建模随机 I/O 与顺序 I/O 的成本差异，并确保在设备占用方面实现公平性，在所有情况下都保持了预期的 2:1 比例。这导致了适当的隔离，即无论邻居的磁盘访问模式如何，工作负载从磁盘收到的服务都是相同的。</p><h4 id="4-4-QoS-和-Vrate-调整"><a href="#4-4-QoS-和-Vrate-调整" class="headerlink" title="4.4 QoS 和 Vrate 调整"></a>4.4 QoS 和 Vrate 调整</h4><p>正如第 3.3 节讨论的那样，现代 SSD 的复杂性使得简单的建模方法不够准确，可能会导致 IOCost 对设备的占用率估计偏低或偏高。<code>vrate</code> 通过动态调整整体的 I/O 发出速率来补偿这种建模的不准确性。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626234857-1.jpeg"><br>图 13：由于模型不准确而进行的 <code>vrate</code> 调整</p><p>图 13 显示了我们在新一代商用 SSD 上进行的一个实验结果，其中一项工作负载试图通过 4KB 的随机读取来饱和设备，而 QoS 设置被配置成使 IOCost 保持 90 百分位的读取延迟在 250 微秒。最初，vrate 保持在约 100 左右，这表明模型参数适合维持这样的 QoS。</p><p>在第一个指示的时间点，我们在线更新模型参数，将其值减半（实际上是在声称设备的占用量仅为之前的一半）。作为响应，读取速率下降。然而，<code>vrate</code> 迅速攀升至大约两倍的发送速率，同时保持着我们期望的 QoS。最后，在第二个指示的时间点，我们再次在线更新模型参数，将其设置为原来值的两倍（实际上是在声称设备的占用量是之前的两倍）。起初，发送速率过度饱和设备，导致延迟出现尖峰，但随着 <code>vrate</code> 降至大约初始值的一半，延迟开始稳定下来，以维持 QoS。这项实验表明，IOCost 中动态 <code>vrate</code> 调整功能能够处理建模不准确的问题，同时仍能保持 QoS。</p><h4 id="4-5-内存管理感知"><a href="#4-5-内存管理感知" class="headerlink" title="4.5 内存管理感知"></a>4.5 内存管理感知</h4><p>在数据中心中，资源过度分配是一种普遍用来提高利用率的方法。通常的做法是部署一个拥有保证资源的高优先级工作负载，并允许低优先级的工作负载尽力而为地消耗机器上的剩余资源。内存管理的集成对于确保资源得到恰当回收至关重要。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235019-1.jpeg"><br>图 14：延迟敏感型工作负载与内存泄漏工作负载堆叠时的每秒请求数 (RPS)</p><p>我们展示了 Meta 生产网络服务器在老一代和新一代商用固态硬盘上的结果。我们在系统切片中启动了一个内存泄漏进程（参见图 1 以了解 cgroup 层级结构），这个进程最终会被 out-of-memory（OOM）杀手终止。图 14 显示，由于内存争抢，Web 服务器的吞吐量降低了。在理想资源控制条件下，Web 服务器应该主要保持其吞吐量。mq-deadline 隔离效果不佳，因为它缺乏 cgroup 集成，但与高端 SSD 配合时稍好，仅仅是因为高端 SSD 具有更大的带宽。尽管 BFQ 具有比例控制，但它的表现最差，导致吞吐量几乎完全丧失，这是由于缺乏延迟控制和内存管理集成。IOLatency 表现中等。最后，IOCost 超越了所有其他 I/O 控制机制，Web 服务器的吞吐量不低于正常水平的 80%。</p><p>为了评估内存管理集成的特定细节，我们设计了一个实验，其中 ResourceControlBench 与 stress 同地运行，stress 是一个复合内存消费者，它不断地访问其配置的工作集。我们配置了一个 PID 控制器，逐渐将 ResourceControlBench 的负载从其峰值计算负载的 40% 增加到 80%，同时保持 95 百分位的延迟在 75 毫秒以下。随着 ResourceControlBench 负载的增加，其内存访问频率增加，推动了对其驻留内存需求的增长。相应地，为了确保高优先级的 ResourceControlBench 有足够的内存，复合内存消费者的内存必须被换出。我们测量了 ResourceControlBench 从其峰值负载的 40% 扩展到 80% 所需的时间。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235125-1.jpeg"><br>图 15：过度承诺环境中的启动时间</p><p>图 15 显示了这个实验的结果。没有 stress 的两个基准配置显示，IOCost 的加载时间大约是 BFQ 的一半。当 stress 消耗内存时，IOCost 配置能够比 BFQ 快约 5 倍地完成纵向扩展。我们还运行了 IOCost 的修改版本。在第一个配置中，所有交换出的 I/O 都被计费到根 cgroup，因此永远不会受到限制。stress 无论消耗多少交换 I/O，都能自由运行。在第二个配置中，我们根据来源的 cgroup 来限制交换 I/O，这会产生优先级反转，即在交换出 stress 的内存时，ResourceControlBench 可能受到限制。这两种配置的表现不如生产版本的 IOCost，这表明 IOCost 的债务机制（第 3.5 节）如何避免优先级反转，同时保持良好的 I/O 控制。</p><h4 id="4-6-堆叠延迟敏感的工作负载"><a href="#4-6-堆叠延迟敏感的工作负载" class="headerlink" title="4.6 堆叠延迟敏感的工作负载"></a>4.6 堆叠延迟敏感的工作负载</h4><p>IOCost 在生产环境中的一个应用是确保多个容器能够获得其应有的 I/O 服务份额。在 Meta，我们运行着类似 Zookeeper 的工作负载，它提供了一个强一致性 API，用于配置、元数据和协调原语，如监视器、锁和信号量。单个操作复制到多个参与者，以提供容错能力。在 500000 次事务后，该服务会触发内存数据库的快照，即使在正常负载下，也会导致瞬时的写入峰值。生产服务对读写操作有一秒的服务等级目标（<code>SLO</code>）。这一 SLO 使得该服务与其他服务共存变得困难，因为集合中的一个参与者遇到的减速可能导致整个操作放慢。这项服务运行在配备了我们企业级 SSD 的机器上。</p><p>我们分析了这样一个场景下的服务行为：十二个集群 (每个集群由五个参与者组成) 分布在五台机器上。同一集群中的任意两个参与者都不会共享主机。这种配置允许多个低流量集群共享机器，实现合理的总利用率。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235228-1.jpeg"><br>图 16：不同 IO 控制方法对 ZooKeeper 延迟 SLO 超限的影响</p><p>这十二个集群各自接收中等程度的流量，即每秒 3000 次读取和 100 次写入。其中十一个集群的平均有效载荷大小为 100KB，而第十二个集群作为一个“嘈杂的邻居”，其有效载荷大小为 300KB。图 16 显示了十一个表现良好的集群的 P99 延迟。SLO 超限由其频率和严重程度来表征。在六小时的实验期间，使用 blk-throttle、BFQ 和 IOLatency 时，这些集群反复超限 1 秒的 SLO。具体来说，blk-throttle 显示了 78 次超限，其中有些持续了数十秒。BFQ 显示了 13 次超限，每次持续 2-5 秒。值得注意的是，虽然图中没有显示，但由于 BFQ 限流的严重性导致系统完全无响应，我们不得不多次运行这个实验。IOLatency 无法配置为比例控制，也显示出不良行为，即 31 次超限，最长的一次持续了 7.8 秒。而使用 IOCost 时，有效地隔离了“嘈杂邻居”集群和快照的影响，仅出现了两次轻微超限，持续时间分别为 1.5 秒和 1.04 秒。</p><h4 id="4-7-远程存储和-VM-环境"><a href="#4-7-远程存储和-VM-环境" class="headerlink" title="4.7 远程存储和 VM 环境"></a>4.7 远程存储和 VM 环境</h4><p>除了本地存储之外，IOCost 还适用于为远程块存储环境提供 I/O 控制，比如公共云中常见的环境。为了评估 IOCost 的广泛适用性，我们重复了图 14 中的实验，不过这一次将 Meta 的生产 Web 服务器替换为与一个在低优先级 cgroup 中运行的、高速内存泄漏程序并行运行的 ResourceControlBench。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235333-1.jpeg"><br>图 17：AWS EBS 和 Google Cloud Persistent Storage 中延迟敏感型工作负载与内存泄漏工作负载叠加时的每秒请求数 (RPS)</p><p>我们在公共云的 VM 中运行这两个工作负载，虚拟机的客户操作系统配置了 IOCost。图 17 显示了四种配置的保护比率——两种 AWS Elastic Block Store (gp3-3000iops, io2-64000iops)，以及两种 Google Cloud Persistent Disk (balanced, SSD)。尽管不同延迟配置文件存在差异，实验清楚地表明，无论是在本地还是远程挂载的情况下，IOCost 都能够有效地隔离所有配置的 I/O。这个实验证明了 IOCost 在建模和 QoS 参数化方面的稳健性，可以成功应用于 Meta 之外的环境。</p><h4 id="4-8-包获取和容器清理"><a href="#4-8-包获取和容器清理" class="headerlink" title="4.8 包获取和容器清理"></a>4.8 包获取和容器清理</h4><p>IOCost 相较于 IOLatency 的一个重大特性是，其比例控制能力使我们能够确保系统服务和工作负载获得公平的 I/O 份额，而不是强制执行严格的优先级排序。此外，即便在极端情况下，当服务器资源被充分利用且竞争激烈时，IOCost 仍能成功地保护服务和工作负载。</p><p><strong>包获取失败。</strong> 在 Meta，一个常见的操作是为容器获取包。这一过程通过一个 <code>host critical</code> 服务（容器代理）请求系统服务来获取包。我们经常遇到由于系统服务因 I/O 资源不足而无法响应，导致两者之间的通信失败的情况。包获取失败会导致容器更新失败，进而常常导致整台机器不得不退出生产环境。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235434-1.jpeg"><br>图 18：随着区域从之前的解决方案 IOLatency 迁移到 IOCost，包获取失败率降低</p><p>图 18 显示了 IOCost 的效果，当数以十万计服务器组成的区域在两个月的时间里从 IOLatency 迁移到 IOCost。随着 IOCost 的启用，该区域内的包获取错误率显著下降，错误数量大约减少了 10 倍。</p><p><strong>容器清理失败。</strong> 在 Meta 的数据中心，定期进行的操作之一是清理老旧容器。我们依赖 btrfs 及其写时复制语义，因此这通常是一个低成本的操作，但我们仍然会遇到一些情况，这些操作可能需要几秒钟。这种情况往往是主工作负载耗尽容器代理的 I/O 资源所造成的。清理老旧容器通常是为了确保后续容器有足够的磁盘空间，而清理失败可能导致机器在功能上变得不可用。</p><p><img src="/images/iocost/%E8%AF%91%EF%BD%9CIOCost%EF%BC%9ABlock%20IO%20Control%20for%20Containers%20in%20Datacenters-20240626235535-1.jpeg"><br>图 19：随着区域从之前的解决方案 IOLatency 迁移到 IOCost，容器清理失败次数减少</p><p>图 19 显示了容器清理失败率的减少，即那些耗时超过 5 秒的清理操作，在该区域迁移至 IOCost 后的变化。IOCost 的效果立竿见影。具体来说，我们看到 IOCost 实现了 3 倍的减少，极大地降低了停滞情况。这再次表明了 IOCost 对容器编排系统成功管理主机能力的影响。</p><h3 id="5-经验教训"><a href="#5-经验教训" class="headerlink" title="5. 经验教训"></a>5. 经验教训</h3><p>Meta 拥有全球最大的 I/O 控制部署之一。最初的动机之一是解决因系统服务内存泄漏导致的隔离失效问题。单独的内存控制是不够的，因为即使设置了内存限制，仍然会导致回收过程，影响延迟敏感应用程序的 IO。只有将内存控制和 I/O 控制结合在一起，我们才能实现全面的隔离。</p><p>我们尝试了现有的 I/O 控制机制，但发现它们对 Meta 的异构设备和应用程序无效。通过 blk-throttle 为每个应用程序配置 I/O 限制效率低下，容易出错，最终难以处理。BFQ 显示了显著的开销和宽泛的延迟波动，并且在实际场景中无法实现有效的隔离。</p><p>我们首先开发了 IOLatency，它揭示了在内存管理和文件系统操作中的优先级反转导致的隔离失效问题。解决了这些优先级反转问题后，我们能够通过调整延迟目标来实现全面的隔离。然而，生产环境下的配置非常困难，因为延迟目标是异构设备属性和动态应用程序属性构成的复杂函数。针对某一场景优化的配置往往对其他场景无效。此外，它无法在具有相同优先级的多个竞争性应用程序之间进行 I/O 仲裁。</p><p>随后，我们开发了 IOCost 来解决 IOLatency 的局限性。IOCost 的配置更为简便，首先可以通过使用 fio（第 3.2 节）对设备性能进行建模，然后利用 ResourceControlBench（第 3.3 节）调整 QoS 参数，以此系统性地实现设备配置。有了每个设备的 I/O 成本模型，就可以通过简单的比例权重为各种应用程序实现有效的 I/O 控制，而无需针对每个应用程序进行离线性能剖析或配置 IOPS、字节数或延迟，这些方法通常过于脆弱和难以大规模生产使用。总体而言，IOCost 已在生产环境中稳健运行两年，有效应对了我们的设备群中异构设备和多样化的应用程序的场景。</p><p><strong>倾向于性能一致的 SSD。</strong> 在 Meta 的数据中心，我们反复遭遇不可预测的 SSD 行为，并发现迎合特定设备的行为并不现实。随着各种应用程序在异构设备群中迁移，对我们来说，针对遇到的特定 SSD 的奇异情况来调整每个应用程序是不切实际的。我们放弃第一代解决方案 IOLatency 主要是因为它需要脆弱的逐应用程序调优。我们当前的解决方案利用 IOCost 的 QoS 特性来限制 SSD，以实现对多样化应用程序可接受的延迟和一致性。</p><p>总的来说，我们的经验表明，与那些具有短时、不可预测、高峰值性能的 SSD 相比，性能更一致的 SSD 可以在高度扩展和复杂的环境中得到更有效的利用。因此，我们建议吞吐量和延迟稳定的 SSD 更适合数据中心使用。</p><h3 id="6-相关工作"><a href="#6-相关工作" class="headerlink" title="6. 相关工作"></a>6. 相关工作</h3><p>与我们的发现一致，<a href="https://www.usenix.org/conference/fast16/technical-sessions/presentation/hao">The Tail at Store</a> 对生产存储设备进行了大规模研究，发现在不同设备之间存在大量的性能差异。此外，<a href="https://ieeexplore.ieee.org/document/8416843/">FLIN</a> 发现工作负载的 I/O 请求模式在并发执行的应用程序间的不公平性中扮演了重要角色。</p><p><a href="https://dl.acm.org/doi/10.1145/3037697.3037732">ReFlex</a> 采用了一种建模方法来考虑访问远程闪存设备时读写操作之间的相互影响。<a href="https://ieeexplore.ieee.org/document/8574561">SSDcheck</a> 为现代 SSD 构建了一个性能模型，以预测每个请求的延迟，并基于预期的请求延迟进行调度。类似地，<a href="https://dl.acm.org/doi/10.1145/3317550.3321430">SSD Performance Transparency</a> 讨论了建模 SSD 性能的需求和挑战，并提倡对设备进行逆向工程，而非黑盒建模。</p><p>关于虚拟机监控器的文献致力于解决跨多个不同工作的 I/O 公平性问题。<a href="https://www.usenix.org/legacy/event/fast09/tech/full_papers/gulati/gulati.pdf">PARDA</a> 和 <a href="https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Gulati.pdf">mClock</a> 都探讨了为访问网络存储的 VM 提供粗粒度公平性的设计。<a href="https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.resmgmt.doc/GUID-D964A753-0844-4343-A96F-27A4C769F92D.html">VMWare</a> 和 <a href="https://docs.netapp.com/us-en/ontap/performance-admin/guarantee-throughput-qos-task.html#about-throughput-ceilings-qos-max">NetApp</a> 都提出了 I/O 解决方案，允许 VM 获得配置数量的 IOPS。相比之下，IOCost 应对了 I/O 子系统与内存子系统交互带来的额外挑战，并通过建模设备占用率，而非仅以 IOPS 或延迟来测量和控制，独特地实现了 I/O 公平性。我们认为，建模设备占用率可能是虚拟机监控器值得探索的一个富有成效的方法。</p><p><a href="https://dl.acm.org/doi/10.1145/277851.277871">Cello</a>、<a href="https://www.usenix.org/conference/fast-07/argon-performance-insulation-shared-storage-servers">Argon</a> 和 <a href="https://www.usenix.org/legacy/events/osdi08/tech/full_papers/yang/yang.pdf">Redline</a> 都提出了在慢速、机械硬盘时代控制 I/O 的方法，这类驱动器具有相对较低的并发性和较高的寻道延迟。最近，<a href="https://www.usenix.org/conference/hotstorage16/workshop-program/presentation/ahn">WDT</a> 描述了一个基于权重配置的、cgroup 感知的 I/O 调度器，旨在针对高速 SSD，与 IOCost 不同的是，它分配的是 I/O 带宽而非占用率。<a href="https://www.usenix.org/conference/fast17/technical-sessions/presentation/huang">FlashBlox</a> 对 SSD 通道进行了分区，这允许硬件强制隔离，但代价是租户数量灵活性的降低。</p><p>在 <a href="https://dl.acm.org/doi/10.1145/2815400.2815421">Split-level I/O scheduling</a> 中，作者指出了在调度时需要考虑 IO 栈不同层的信息。IOCost 识别了交换和日志记录的 I/O 源，并在不引起优先级反转的情况下引入了对内存管理和文件系统日志记录操作的 I/O 控制。</p><p>多篇文献 [8, 10–12,15, 24, 25, 31, 33] 聚焦于资源管理。这些解决方案旨在集中部署运行的应用程序间划分系统资源，同时不违反各自的 SLO。其他工作 [37, 38] 提出了针对容器的架构和操作系统扩展。总体而言，它们在 IOCost 之上或之下运作，并可以利用 IOCost 强大的 I/O 控制，进一步增强数据中心环境下集中部署运行的能力。</p><h3 id="7-结论"><a href="#7-结论" class="headerlink" title="7. 结论"></a>7. 结论</h3><p>我们已经识别出在容器化环境中对 I/O 控制的需求。本文介绍了 IOCost，这是一种专为容器化环境设计的 I/O 控制方案，它为数据中心中异构存储设备和多样化工作负载提供了可扩展、工作量保持且低开销的 I/O 控制。我们的方法通过离线生成的设备成本模型来估算设备占用率。此外，IOCost 的设计将 I/O 控制分为轻量级的按 I/O 问题路径和周期性的 I/O 规划路径。一种创新的 cgroup 树层次权重更新算法，确保容器能够以最小的开销动态共享未使用的 I/O 预算。最后，我们分享了使用 IOCost 的经验以及潜在的未来硬件发展方向。</p><p><em>原文：</em> <a href="https://www.cs.cmu.edu/~dskarlat/publications/iocost_asplos22.pdf">IOCost: Block IO Control for Containers in Datacenters</a></p><p><strong>本文作者</strong> ： cyningsun<br><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/06-27-2024/iocost-block-io-control-for-containers-in-datacenters-cn.html">https://www.cyningsun.com/06-27-2024/iocost-block-io-control-for-containers-in-datacenters-cn.html</a> <br><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;摘要&quot;&gt;&lt;a href=&quot;#摘要&quot; class=&quot;headerlink&quot; title=&quot;摘要&quot;&gt;&lt;/a&gt;摘要&lt;/h3&gt;&lt;p&gt;资源隔离是数据中心环境的基本需求。然而，我们在 Meta 大规模数据中心的生产实践中发现，现有的块存储 IO 控制机制在容器化环境中表现不足</summary>
      
    
    
    
    <category term="Linux" scheme="https://www.cyningsun.com/category/Linux/"/>
    
    
    <category term="I/O" scheme="https://www.cyningsun.com/tag/I-O/"/>
    
  </entry>
  
  <entry>
    <title>译｜Efficient IO with io_uring</title>
    <link href="https://www.cyningsun.com/05-18-2024/efficient-io-with-io_uring.html"/>
    <id>https://www.cyningsun.com/05-18-2024/efficient-io-with-io_uring.html</id>
    <published>2024-05-17T16:00:00.000Z</published>
    <updated>2026-04-03T14:52:49.000Z</updated>
    
    <content type="html"><![CDATA[<p>本文旨在作为最新 Linux I&#x2F;O 接口 io_uring 的入门介绍，并将其与现有技术进行比较。我们将探讨其存在的原因、内部运作机制以及用户可见的接口。文章不会深入到特定命令等细节，因为这些信息在相关的 man 手册页中已有提供。相反，我们的目标是为读者提供对 io_uring 及其工作原理的初步理解，希望能帮助读者更深入地理解这一技术的全貌。尽管如此，本文与 man 手册页之间难免会有所重叠，因为在描述 io_uring 时不可避免地要包含一些这些细节。</p><h3 id="1-0-引言"><a href="#1-0-引言" class="headerlink" title="1.0 引言"></a>1.0 引言</h3><p>在 Linux 系统中，实现基于文件的 I&#x2F;O 有多种方式。最古老且最基本的是 <code>read(2)</code> 和 <code>write(2)</code> 系统调用。后来，为了支持指定偏移量，加入了 <code>pread(2)</code> 和 <code>pwrite(2)</code>。随后，又引入了向量版本 <code>preadv(2)</code> 和 <code>pwritev(2)</code>。即使如此，API 进一步扩展，提供了 <code>preadv2(2)</code> 和 <code>pwritev2(2)</code> 系统调用，允许使用修饰符标志。尽管以上系统调用不尽相同，但它们共有的特征是同步接口，即系统调用会在数据准备就绪（或写入完成）时返回。对于某些应用场景而言，这并非最优选择，因此需要一个异步接口。POSIX 标准提供了 <code>aio_read(3)</code> 和 <code>aio_write(3)</code> 来满足这一需求，但这些实现往往不尽如人意，性能欠佳。</p><p>Linux 本身也具备一个本地异步 I&#x2F;O 接口，简称为 aio。然而，它存在多个限制：</p><ul><li>最大的限制在于仅支持 <strong>O_DIRECT</strong>（或无缓冲）访问的异步 I&#x2F;O。由于 <strong>O_DIRECT</strong> 的限制（绕过缓存和大小&#x2F;对齐约束），使得原生 aio 接口对于大多数应用场景并不适用。对于常规（缓冲）I&#x2F;O 操作，该接口的行为与同步方式相同。</li><li>即便满足了所有使 I&#x2F;O 操作异步化的条件，仍然有可能出现 I&#x2F;O 提交阻塞的情况。例如，如果执行 I&#x2F;O 操作需要元数据信息，提交过程将会阻塞直至元数据就绪。对于存储设备而言，请求槽位的数量固定，一旦槽位全部被占用，新的 I&#x2F;O 请求提交就需要等待空闲槽位出现。这些不确定性意味着依赖于始终异步提交的应用程序，实际上仍需设计额外逻辑来处理可能的阻塞情况，无法完全避免性能上的影响。</li><li>API 设计并不理想。每次 I&#x2F;O 提交操作最终都需要复制 64 + 8 字节的数据，而每次完成事件则需要复制 32 字节。这意味着即使是号称“零拷贝”的 I&#x2F;O 操作，也会产生总共 104 字节的内存复制开销。根据 I&#x2F;O 大小不同，该开销可能会相当明显。公开的完成事件环形缓冲区实际上减慢了完成过程，并且对于应用程序来说很难（或者说几乎不可能？）正确使用。I&#x2F;O 操作总是至少需要两个系统调用（提交和等待完成），在 Spectre&#x2F;Meltdown 安全漏洞出现后的时代，无疑成为严重的性能瓶颈。</li></ul><p>多年来，人们为解决上述第一个限制（即仅支持 O_DIRECT 访问的异步 I&#x2F;O）做出了多方面的努力，我本人也在 2010 年尝试过解决这个问题，但均未取得成功。随着能提供低于 10 微秒延迟和极高 IOPS 的设备的出现，现有接口开始显现出其年代感。对于这类设备，缓慢且不确定的提交延迟是非常严重的问题，同样，单个核心所能榨取的性能也显得不足。加之上述种种限制，可以说原生 Linux aio 在实际应用中并不广泛。它已被边缘化，仅在一些特定的应用场景中使用，随之而来的是长期未发现的 bug 等问题。</p><p>此外，由于“普通”应用程序无法从 aio 中获益，表明 Linux 在提供开发者期望的功能方面仍存在缺口。没有理由让应用程序或库继续创建私有的 I&#x2F;O 卸载线程池来获取合理的异步 I&#x2F;O 性能，特别是在内核可以更高效地完成这项工作的前提下。</p><h3 id="2-0-改善现状的努力"><a href="#2-0-改善现状的努力" class="headerlink" title="2.0 改善现状的努力"></a>2.0 改善现状的努力</h3><p>起初的尝试主要集中在改进 aio 接口上，且进展颇丰，但最终未能继续。选择这一初始方向的原因包括：</p><ul><li>如果能够扩展和完善现有接口，相比提供一个全新的接口更为可取。新接口的采纳需要时间，而且新接口的审查和批准过程可能既漫长又艰难。</li><li>通常来说，这样做工作量要小得多。作为开发者，总是力求以最少的工作量实现最大的成果。扩展现有接口在测试基础设施方面具有许多优势。</li></ul><p>现有的 aio 接口主要包括三个主要系统调用：用于设置 aio 上下文的 <code>io_setup(2)</code>、用于提交 I&#x2F;O 的 <code>io_submit(2)</code>，以及用于获取或等待 I&#x2F;O 完成的 <code>io_getevents(2)</code>。由于需要改变这些系统调用中的多项行为，我们必须新增系统调用来传递这些信息。这不仅导致了代码的多重入口点，还在其他地方产生了捷径。最终代码在复杂性和可维护性方面并不理想，而且只解决了前文提到的缺陷之一。更甚之，它实际上让问题变得更糟，因为现在 API 变得更加复杂，更难以理解和使用。</p><p>虽然放弃已开展的工作重新开始总是一件困难的事，但显而易见，我们需要一个全新的解决方案。这个新方案需要满足所有要求，既要性能优越且可扩展，又要易于使用，同时具备现有接口所缺乏的功能。</p><h3 id="3-0-新接口设计目标"><a href="#3-0-新接口设计目标" class="headerlink" title="3.0 新接口设计目标"></a>3.0 新接口设计目标</h3><p>从头开始虽然不易，但也赋予了我们设计上的自由。大致按重要性递增的顺序，主要设计目标包括：</p><ul><li>易于使用，难以误用。任何用户&#x2F;应用程序可见的接口都应以此为目标。接口应易于理解，直观易用。</li><li>可扩展性。虽然我的背景主要与存储相关，但我希望设计的接口能够不仅仅应用于基于块的 I&#x2F;O，还能适应未来可能出现的网络和非块存储接口。如果你正在创建一个全新的接口，它应当（至少尝试）在某种程度上具有面向未来的适应性。</li><li>功能丰富。Linux aio 仅服务于一部分（甚至更小的一部分）应用程序的需求。我不希望再创造另一个只能覆盖部分应用需求的接口，或者迫使应用程序反复实现相同的功能（例如 I&#x2F;O 线程池）。</li><li>效率。尽管存储 I&#x2F;O 很大程度上仍是基于块的，至少为 512 字节或 4KB，但对于某些应用来说，不同大小的效率仍然至关重要。此外，有些请求可能根本就不携带数据载荷。新接口在单个请求的开销上必须是高效的。</li><li>可伸缩性。虽然效率和低延迟很重要，但提供最佳的性能峰值也同样关键。特别是对于存储来说，我们已经努力构建了一个可扩展的基础架构。新的接口应该能够让我们将这种可伸缩性直接反馈给应用程序。</li></ul><p>上述某些目标看似相互矛盾。高效且可扩展的接口往往难以使用，更重要的是，难以正确使用。同时，功能丰富与效率高也很难同时达成。然而，这些就是我们设定的目标。</p><h3 id="4-0-引入-io-uring"><a href="#4-0-引入-io-uring" class="headerlink" title="4.0 引入 io_uring"></a>4.0 引入 io_uring</h3><p>尽管设计目标按照优先级进行了排序，但最初的设计焦点集中在效率上。效率不是事后可以添加的东西，而是必须从一开始就融入设计之中——一旦接口确定，就很难在之后提升效率。我明确不想在提交或完成事件中涉及任何内存复制，也不想有内存间接引用。在基于 aio 的设计末期，aio 因需要处理 I&#x2F;O 的两端而进行的多次复制，显著损害了效率和可伸缩性。</p><p>鉴于复制不可取，很显然内核与应用程序需要共享定义 I&#x2F;O 操作及完成事件的数据结构。如果将共享的概念推到极致，自然地，协调共享数据的机制也应该放在应用程序与内核共享的内存中。一旦接受了这一理念，就会意识到两者间的同步必须以某种方式管理。应用程序无法不通过系统调用而与内核共享锁，但系统调用无疑会降低与内核通信的效率，这与我们的效率目标背道而驰。能满足这一需求的数据结构就是单一生产者 - 单一消费者（SPSC）的环形缓冲区。通过使用共享的环形缓冲区，我们可以消除应用与内核间共享锁，转而巧妙利用内存排序和屏障来处理。</p><p>与异步接口相关的两个基本操作是：提交请求的行为和与该请求完成相关的事件。在提交 I&#x2F;O 操作时，应用程序充当生产者，而内核是消费者；而在处理完成事件时，角色反转，内核变为生产者，生成完成事件，应用程序成为消费者。因此，为了建立应用程序与内核之间高效通信的渠道，需要一对环形缓冲区，这对环形缓冲区构成了 io_uring 新接口的核心。它们被恰当地命名为提交队列 (SQ) 和完成队列 (CQ)，并构成新接口的基础。</p><h4 id="4-1-数据结构"><a href="#4-1-数据结构" class="headerlink" title="4.1 数据结构"></a>4.1 数据结构</h4><p>在设计了通信基础之后，接下来是定义描述请求和完成事件的数据结构。完成事件相对直接：它需要携带操作结果相关的信息，以及将完成事件关联回原始请求的方式。在 io_uring 中，完成事件的数据结构布局如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_cqe</span> &#123;</span>    __u64 user_data;  <span class="hljs-comment">// 用户定义的数据，用于关联请求和完成事件</span>    __s32 res;        <span class="hljs-comment">// 操作结果</span>    __u32 flags;      <span class="hljs-comment">// 标志位，可能包含额外信息</span>&#125;;</code></pre><p><code>io_uring</code> 的名字到现在应该为人所知了。后缀 <code>_cqe</code> 指的是 Completion Queue Event（完成队列事件），在本文剩余部分通常简称为 <code>cqe</code>。cqe 结构体中包含一个 <strong>user_data</strong> 字段，这个字段在请求初次提交时携带信息，并包含应用识别请求所需的任何数据。一个常见的用途是让它指向原始请求的指针。内核不会修改此字段，它会直接从提交阶段传递到完成事件阶段。<strong>res</strong> 字段保存请求的结果，可以将其视作来自类似 <code>read(2)</code> 或 <code>write(2)</code> 系统调用的返回值。对于正常的读写操作，它将包含传输的字节数。如果发生错误，它则会包含一个负的错误值，比如如果发生 I&#x2F;O 错误，<strong>res</strong> 就会包含 <strong>-EIO</strong>。最后，<strong>flags</strong> 成员截止目前尚未启用，可以用来承载与操作相关的元数据。</p><p>请求类型的定义更为复杂。它不仅要描述比完成事件更多的信息，而且 io_uring 在设计时就旨在为未来的请求类型留有扩展性。我们设计的结构如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_sqe</span> &#123;</span>    __u8 opcode;         <span class="hljs-comment">// 操作码，定义特定请求的类型</span>    __u8 flags;          <span class="hljs-comment">// 标志位，包含适用于多种命令类型的修饰标志</span>    __u16 ioprio;        <span class="hljs-comment">// 请求的优先级，遵循 ioprio_set(2) 系统调用定义</span>    __s32 fd;            <span class="hljs-comment">// 与请求关联的文件描述符</span>    __u64 off;           <span class="hljs-comment">// 操作应发生的偏移量</span>    __u64 addr;          <span class="hljs-comment">// 操作应执行 I/O 的地址，如果操作涉及数据传输的话。对于向量读/写操作，这是一个指向 iovec 结构数组的指针</span>    __u32 len;           <span class="hljs-comment">// 对于非向量 I/O 传输，这是字节计数；对于向量 I/O 传输，这是由 addr 描述的向量数量</span>    <span class="hljs-class"><span class="hljs-keyword">union</span> &#123;</span>        <span class="hljs-type">__kernel_rwf_t</span> rw_flags;  <span class="hljs-comment">// 读写标志，针对读/写操作</span>        __u16 fsync_flags;    <span class="hljs-comment">// fsync操作的标志</span>        __u16 poll_events;    <span class="hljs-comment">// poll操作的事件标志</span>        __u32 sync_range_flags;  <span class="hljs-comment">// 同步范围操作的标志</span>        __u32 msg_flags;       <span class="hljs-comment">// 消息传递操作的标志</span>    &#125;;    __u64 user_data;      <span class="hljs-comment">// 用户定义的数据，用于标识请求，对应 cqe 中的 user_data</span>    <span class="hljs-class"><span class="hljs-keyword">union</span> &#123;</span>        __u16 buf_index;  <span class="hljs-comment">// 缓冲区索引，具体含义取决于操作</span>        __u64 pad[<span class="hljs-number">3</span>];     <span class="hljs-comment">// 填充字段，确保结构体对齐</span>    &#125;;&#125;;</code></pre><p>类似于完成事件，提交侧的结构被称为 Submission Queue Entry（提交队列条目），简称 <code>sqe</code>。它包含一个 <strong>opcode</strong> 字段，用于描述该请求的操作码。例如，操作码 <strong>IORING_OP_READV</strong>  是一个向量读操作。<strong>flags</strong> 字段包含适用于多种命令类型的通用修饰标志。我们将在稍后的高级使用场景部分对此进行探讨。<strong>ioprio</strong> 表示请求的优先级，对于普通的读写操作，它遵循 <code>ioprio_set(2)</code> 系统调用中定义的规则。<strong>fd</strong> 是与请求关联的文件描述符，<strong>off</strong> 指定了操作应执行的偏移位置。<strong>addr</strong> 字段，如果操作涉及数据传输，包含执行 I&#x2F;O 操作的地址；对于向量读&#x2F;写操作，将是一个指向类似用于 <code>preadv(2)</code> 的 <code>iovec</code> 结构数组的指针。<code>len</code> 字段，在非向量 I&#x2F;O 传输中，表示字节长度；在向量 I&#x2F;O 传输中，则表示 <code>iovec</code> 结构的数量。</p><p>接下来的部分是一个标志的联合体 (union)，它针对操作码（<code>op-code</code>）具有特定性。例如，对于前面提到的向量读取操作（<strong>IORING_OP_READV</strong>），这些标志遵循了 <code>preadv2(2)</code> 系统调用中描述的那些标志。<strong>user_data</strong> 字段是所有操作码通用的，内核不会修改这个字段。它只是简单地从提交阶段复制到完成事件（<code>cqe</code>）中。<strong>buf_index</strong> 字段将在高级使用场景部分进行说明。结构的末尾还有一些填充，目的是确保 <code>sqe</code> 在内存中以 64 字节对齐，同时也为将来可能需要更多数据来描述请求的情况预留空间。可以想象几个这样的应用场景，比如作为一个键值存储命令集，或者是端到端数据保护场景，应用程序在其中传入预先计算的数据校验和。</p><h4 id="4-2-通信通道"><a href="#4-2-通信通道" class="headerlink" title="4.2 通信通道"></a>4.2 通信通道</h4><p>在介绍了数据结构后，接下来详细说明环是如何工作的。尽管我们有一个提交侧和完成侧，显示出一定的对称性，但两者之间的索引方式是不同的。如同之前章节那样，我们先从较为简单的完成环开始讲解。</p><p>完成队列（<code>CQ</code>）中的完成事件（<code>cqe</code>）被组织成一个数组，其内存由内核和应用程序双方可见并可修改。然而，由于 <code>cqe</code> 是由内核产生的，实际上只有内核会修改 <code>cqe</code> 条目。通信是通过环形缓冲区管理的。每当内核向 <code>CQ</code> 环中发布一个新的事件，它就会更新相应的尾部指针。当应用程序消费一个条目时，它会更新头部指针。因此，如果尾部不同于头部，应用程序就知道它有一个或多个事件可供消费。环计数器（ring counters）本质上是无界流动的 32 位整数，当完成事件数量超出环的容量时，它会自然地循环回绕。这种方法的好处在于，可以充分利用环的全部容量，而无需额外管理一个“环已满”的标志，后者会使环的管理变得复杂。因此，环的大小必须是 2 的次幂</p><p>要定位一个事件的索引，应用程序需将当前的尾部索引与环的大小掩码进行按位与运算。典型的代码流程如下：</p><pre><code class="hljs c"><span class="hljs-type">unsigned</span> head;head = cqring-&gt;head;read_barrier();  <span class="hljs-keyword">if</span> (head != cqring-&gt;tail) &#123;    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_cqe</span> *<span class="hljs-title">cqe</span>;</span>    <span class="hljs-type">unsigned</span> index;    index = head &amp; cqring-&gt;mask;    cqe = &amp;cqring-&gt;cqes[index];    <span class="hljs-comment">/* 在此处处理已完成的 cqe */</span>    ...    <span class="hljs-comment">/* 已经消费此条目 */</span>    head++;&#125;cqring-&gt;head = head;write_barrier();</code></pre><p><code>ring→cqes[]</code> 是一个共享的 <code>io_uring_cqe</code> 结构数组。接下来的部分，我们将深入了解这种共享内存（以及 io_uring 实例本身）是如何设置和管理的，以及其中神秘的读屏障（read barrier）和写屏障（write barrier）调用的作用。</p><p>在提交侧，角色则颠倒过来：应用程序负责更新尾指针，而内核负责消费条目（并更新头指针）。一个重要的区别在于，尽管 CQ 环直接索引共享的 <code>cqe</code> 数组，但在提交侧之间却存在一个间接索引数组。因此，提交侧的环形缓冲区实际上是一个索引，指向这个间接数组，而间接数组中又包含了指向 sqe（提交队列条目）的索引。这初看可能显得有些奇怪且令人困惑，但实际上这么做是有道理的。某些应用程序可能会在其内部数据结构中嵌入请求单元，而这种设计给予了它们在保持一次性提交多个 <code>sqe</code> 能力的同时，还能灵活地组织这些请求的自由。这样的设计反过来又使得这些应用程序向 io_uring 接口的迁移变得更加简便。</p><p>向内核提交一个 <code>sqe</code>（用于内核消费）基本上是与从内核收割 <code>cqe</code>（完成队列事件）相反的操作。一个典型的示例大概如下所示：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_sqe</span> *<span class="hljs-title">sqe</span>;</span><span class="hljs-type">unsigned</span> tail, index;tail = sqring-&gt;tail;index = tail &amp; sqring-&gt;ring_mask;sqe = &amp;sqring-&gt;sqes[index];<span class="hljs-comment">/* 这里通过某个函数初始化 sqe，准备 IO 操作参数 */</span>init_io_request(sqe);<span class="hljs-comment">/* 将当前 sqe 的索引存入间接数组 */</span>sqring-&gt;<span class="hljs-built_in">array</span>[index] = index;<span class="hljs-comment">/* 更新尾指针，表示新的 sqe 已准备好 */</span>tail++;write_barrier(); <span class="hljs-comment">// 确保更新对其他 CPU 可见</span>sqring-&gt;tail = tail;write_barrier(); <span class="hljs-comment">// 确保 tail 更新操作的顺序性</span></code></pre><p>如同在处理 CQ 环时一样，我们稍后会解释读屏障（read barrier）和写屏障（write barrier）的具体作用。上面是一个简化的示例，它假设 SQ 环当前是空的，或者至少还有空间容纳一个额外的条目。</p><p>一旦 sqe（提交队列条目）被内核消费，应用程序就可以自由重用该 sqe 条目。即使内核还未完全处理完某个 sqe，情况也是如此。如果内核在条目被消费后仍需访问它，那么它在此之前已创建了该 sqe 的稳定副本。为何会发生这种情况并不一定重要，但它对应用程序有着重要影响。通常，应用程序会请求一个特定大小的环，并且可能会认为这个大小直接对应着应用程序在内核中可以挂起的请求数量。然而，由于 sqe 的有效期仅限于它被实际提交的那一刻，所以应用程序实际上有可能驱动比 SQ 环大小更多的挂起请求。应用程序必须小心，不要过度利用这一点，否则可能会导致 CQ 环溢出。默认情况下，CQ 环的大小是 SQ 环的两倍，这为应用程序在管理这一方面提供了一定的灵活性，但并未完全消除管理的必要。如果应用程序违反了这一限制，将会在 CQ 环中被记录为溢出状况，关于这部分的更多信息将在后面详细介绍。</p><p>完成事件可以以任意顺序到达，请求提交与关联的完成事件之间并没有固定的顺序关系。SQ 环和 CQ 环彼此独立运行。然而，每一个完成事件都会对应于一个特定的提交请求，即每个完成事件总是与一个具体的提交请求相关联。</p><h3 id="5-0-io-uring-接口"><a href="#5-0-io-uring-接口" class="headerlink" title="5.0 io_uring 接口"></a>5.0 io_uring 接口</h3><p>与 aio 相似，io_uring 也有一系列与其操作相关的系统调用。第一个系统调用用于创建一个 io_uring 实例：</p><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">io_uring_setup</span><span class="hljs-params">(<span class="hljs-type">unsigned</span> entries, <span class="hljs-keyword">struct</span> io_uring_params *params)</span>;</code></pre><p>应用程序必须为此 io_uring 实例提供期望的条目数量，以及与之相关的一组参数。<strong>entries</strong> 表示将与此 io_uring 实例关联的 sqe（提交队列条目）数量，它必须是 2 的幂，在 1 到 4096（包括两端）的范围内。<strong>params</strong> 结构体由内核读取和写入，定义如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_params</span> &#123;</span>    __u32 sq_entries;    <span class="hljs-comment">// 提交队列（SQ）的条目数</span>    __u32 cq_entries;    <span class="hljs-comment">// 完成队列（CQ）的条目数</span>    __u32 flags;         <span class="hljs-comment">// 控制io_uring实例的标志</span>    __u32 sq_thread_cpu; <span class="hljs-comment">// 提交线程的CPU亲和力</span>    __u32 sq_thread_idle; <span class="hljs-comment">// 提交线程空闲超时（毫秒）</span>    __u32 resv[<span class="hljs-number">5</span>];       <span class="hljs-comment">// 保留字段</span>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_sqring_offsets</span> <span class="hljs-title">sq_off</span>;</span> <span class="hljs-comment">// 提交队列的偏移量信息</span>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_cqring_offsets</span> <span class="hljs-title">cq_off</span>;</span> <span class="hljs-comment">// 完成队列的偏移量信息</span>&#125;;</code></pre><p><strong>sq_entries</strong> 字段将由内核填写，以此通知应用程序此环能够支持多少个 sqe（提交队列条目）。同理，通过 <strong>cq_entries</strong> 成员告知应用程序完成队列（CQ）环的大小。关于此结构体其余部分的讨论将推迟到高级使用场景部分，但有两个例外：<strong>sq_off</strong> 和 <strong>cq_off</strong> 字段，因为它们对于通过 io_uring 建立基本通信机制是必要的。</p><p>当 <code>io_uring_setup(2)</code> 调用成功后，内核会返回一个文件描述符，该描述符用于标识 io_uring 实例。这时，<strong>sq_off</strong> 和 <strong>cq_off</strong> 结构体便发挥了作用。考虑到 sqe 和 cqe 结构体是由内核和应用程序共享的，应用程序需要一种方式来访问这块内存。这是通过使用 <code>mmap(2)</code> 系统调用将其映射到应用程序的内存空间中来实现的。应用程序利用 <strong>sq_off</strong> 成员来确定环中各元素的偏移量。<code>io_sqring_offsets</code> 结构定义如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_sqring_offsets</span> &#123;</span>    __u32 head;          <span class="hljs-comment">// 提交队列头部的偏移量</span>    __u32 tail;          <span class="hljs-comment">// 提交队列尾部的偏移量</span>    __u32 ring_mask;     <span class="hljs-comment">// 环状缓冲区掩码，用于快速索引</span>    __u32 ring_entries;  <span class="hljs-comment">// 环中条目的数量</span>    __u32 flags;         <span class="hljs-comment">// 环的标志</span>    __u32 dropped;       <span class="hljs-comment">// 未提交的sqe数量</span>    __u32 <span class="hljs-built_in">array</span>;         <span class="hljs-comment">// sqe索引数组的偏移量</span>    __u32 resv1;         <span class="hljs-comment">// 保留字段</span>    __u64 resv2;         <span class="hljs-comment">// 保留字段</span>&#125;;</code></pre><p>为了访问这块内存，应用程序必须使用 io_uring 的文件描述符以及与 SQ 环关联的内存偏移量调用 <code>mmap(2)</code>。io_uring API 为应用程序定义了以下 mmap 偏移量：</p><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> IORING_OFF_SQ_RING 0ULL</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> IORING_OFF_CQ_RING 0x8000000ULL</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> IORING_OFF_SQES 0x10000000ULL</span></code></pre><p>其中，<strong>IORING_OFF_SQ_RING</strong> 用于将 SQ 环映射到应用程序的内存空间中，<strong>IORING_OFF_CQ_RING</strong> 用于同样地映射 CQ 环，而 <strong>IORING_OFF_SQES</strong> 则是用来映射 sqe 数组的。对于 CQ 环而言，cqes 数组本身就是 CQ 环的一部分。由于 SQ 环是对 sqe 数组中的值的索引，因此 sqe 数组必须由应用程序单独映射。</p><p>应用程序将定义一个持有这些偏移量的自定义结构体。一个示例可能如下所示：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">app_sq_ring</span> &#123;</span>   <span class="hljs-type">unsigned</span> *head;   <span class="hljs-type">unsigned</span> *tail;   <span class="hljs-type">unsigned</span> *ring_mask;   <span class="hljs-type">unsigned</span> *ring_entries;   <span class="hljs-type">unsigned</span> *flags;   <span class="hljs-type">unsigned</span> *dropped;   <span class="hljs-type">unsigned</span> *<span class="hljs-built_in">array</span>;&#125;;</code></pre><p>一个典型的设置案例看起来如下：</p><pre><code class="hljs c"><span class="hljs-keyword">struct</span> app_sq_ring <span class="hljs-title function_">app_setup_sq_ring</span><span class="hljs-params">(<span class="hljs-type">int</span> ring_fd, <span class="hljs-keyword">struct</span> io_uring_params *p)</span>&#123;   <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">app_sq_ring</span> <span class="hljs-title">sqring</span>;</span>   <span class="hljs-type">void</span> *ptr;      ptr = mmap(<span class="hljs-literal">NULL</span>, p→sq_off.<span class="hljs-built_in">array</span> + p→sq_entries * <span class="hljs-keyword">sizeof</span>(__u32),               PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,               ring_fd, IORING_OFF_SQ_RING);      sring→head = ptr + p→sq_off.head;   sring→tail = ptr + p→sq_off.tail;   sring→ring_mask = ptr + p→sq_off.ring_mask;   sring→ring_entries = ptr + p→sq_off.ring_entries;   sring→flags = ptr + p→sq_off.flags;   sring→dropped = ptr + p→sq_off.dropped;   sring→<span class="hljs-built_in">array</span> = ptr + p→sq_off.<span class="hljs-built_in">array</span>;   <span class="hljs-keyword">return</span> sring;&#125;</code></pre><p>完成队列（CQ）环的映射方式与之类似，使用 <strong>IORING_OFF_CQ_RING</strong> 偏移量以及由 <code>io_cqring_offsets</code> 结构体的 <strong>cq_off</strong> 成员定义的偏移量。最终，通过 <strong>IORING_OFF_SQES</strong> 偏移量映射 sqe 数组。由于这些代码在不同应用程序之间大多是可以复用的模板代码，<code>liburing</code> 库提供了一系列助手函数，以便以简单的方式完成设置和内存映射。详情请参阅 io_uring 库部分。完成这些步骤后，应用程序就可以通过 io_uring 实例进行通信了。</p><p>应用程序还需要一种方式告诉内核它现在已经准备好了请求供内核消费。这是通过另一个系统调用来完成的：</p><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">io_uring_enter</span><span class="hljs-params">(<span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> fd, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> to_submit,</span><span class="hljs-params">                   <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> min_complete, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> flags,</span><span class="hljs-params">                   <span class="hljs-type">sigset_t</span> *sig)</span>;</code></pre><p>其中，<strong>fd</strong> 指的是由 <code>io_uring_setup(2)</code> 返回的环文件描述符；<strong>to_submit</strong> 告诉内核最多有这么多的 sqe（提交队列条目）准备消费和提交；<strong>min_complete</strong> 请求内核等待至少完成指定数量的请求。这个单一调用同时支持提交请求和等待完成事件，意味着应用程序可以用一个系统调用同时提交请求并等待它们的完成。<strong>flags</strong> 包含修改此调用行为的标志，其中最重要的是：</p><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> IORING_ENTER_GETEVENTS (1U &lt;&lt; 0)</span></code></pre><p>如果在 <code>flags</code> 中设置了 <strong>IORING_ENTER_GETEVENTS</strong>，那么内核将主动等待至少 <code>min_complete</code> 个事件变为可用。敏锐的读者可能会疑惑，既然已经有了 <strong>min_complete</strong>，为什么还需要这个标志。实际上，在某些情况下，这种区分是很重要的，这部分内容将在后面讨论。目前，如果你想等待完成事件，就必须设置 <strong>IORING_ENTER_GETEVENTS</strong>。</p><p>以上基本上涵盖了 io_uring 的基本 API。<code>io_uring_setup(2)</code> 用于创建指定大小的 io_uring 实例。创建完毕后，应用程序可以开始填充 sqe 并使用 <code>io_uring_enter(2)</code> 提交它们。完成事件既可以与提交一起通过同一个调用来等待，也可以在稍后单独处理。除非应用程序想要等待完成事件到来，否则它可以简单地检查 CQ 环的尾部指针，以了解是否有任何事件待处理。内核会直接修改 CQ 环的尾部指针，因此应用程序无需设置 <strong>IORING_ENTER_GETEVENTS</strong> 标志就可以直接消费完成事件。</p><p>关于可用命令类型及其使用方法，请查阅 <code>io_uring_enter(2)</code> 的手册页。</p><h4 id="5-1-SQE-顺序控制"><a href="#5-1-SQE-顺序控制" class="headerlink" title="5.1 SQE 顺序控制"></a>5.1 SQE 顺序控制</h4><p>通常情况下，sqe（提交队列条目）是独立使用的，意味着一个条目的执行不会影响环中后续 sqe 条目的执行顺序或排列。这提供了操作的完全灵活性，并使它们能够并行执行和完成，以达到最大的效率和性能。然而，在某些情况下，可能需要控制 sqe 的执行顺序，例如为了数据完整性保证的写入操作。一个典型的例子是一系列写操作之后跟着一个 fsync 或 fdatasync 调用。只要允许写操作以任意顺序完成，我们只需要确保当所有写操作都完成后才执行数据同步操作。应用程序常常将这转化为先写后等待的操作模式，当所有写入被底层存储确认后，再发出同步指令。</p><p>io_uring 支持清空提交队列，直到所有先前的完成事件都结束。这样，应用程序可以入队上述同步操作，并知道在所有先前的命令完成之前不会启动。这是通过在 sqe 的标志字段中设置 <strong>IOSQE_IO_DRAIN</strong> 来实现的。请注意，这会导致整个提交队列暂停。根据 io_uring 在特定应用程序中的使用方式，这可能会引入比预期更大的流水线延迟。如果这类阻塞操作频繁发生，应用程序使用一个独立的 io_uring 上下文用于保证数据完整性的写操作，以允许无关命令同时获得更好的并发性能。</p><h4 id="5-2-链式-SQE"><a href="#5-2-链式-SQE" class="headerlink" title="5.2 链式 SQE"></a>5.2 链式 SQE</h4><p>虽然 <strong>IOSQE_IO_DRAIN</strong> 提供了全管道屏障，但 io_uring 还支持对 sqe 更细粒度的序列控制。链式 sqe 提供了一种方式来描述在较大的提交队列中的 sqe 序列间的依赖关系，其中每个 sqe 的执行依赖于前一个 sqe 的成功完成。这种使用场景的例子可能包括必须按顺序执行的一系列写操作，或者像拷贝操作那样的场景，先从一个文件读取，随后将数据写入另一个文件，且这两个 sqe 共享缓冲区。为了使用这个功能，应用程序必须在 sqe 的 <strong>flag</strong> 字段中设置 <strong>IOSQE_IO_LINK</strong>。如果设置了此标志，那么在前一个 sqe 成功完成之前，下一个 sqe 不会开始执行。如果前一个 sqe 没有完全成功完成（即遇到任何错误或读&#x2F;写不完全），链接链会被打破，相关的 sqe 将以 <strong>-ECANCELED</strong> 作为错误码被取消。此时，“完全完成”指的是请求完全成功完成，任何错误或潜在的读&#x2F;写不足都将中断这个链，请求必须完整完成。</p><p>只要其 <strong>flag</strong> 字段中设置了 <strong>IOSQE_IO_LINK</strong>，链式 sqe 的链会持续。因此，链的定义始于首个设置了 <strong>IOSQE_IO_LINK</strong>  的 sqe，并终止于紧随其后的未设置该标志的第一个 sqe。理论上支持任意长度的链。</p><p>这些链独立于提交环中的其他 sqe 执行。链是独立的执行单元，多个链可以并行执行和完成，包括不属于任何链的 sqe。</p><h4 id="5-3-超时命令"><a href="#5-3-超时命令" class="headerlink" title="5.3 超时命令"></a>5.3 超时命令</h4><p>虽然 io_uring 支持的大多数命令都是直接或间接作用于数据（前者如读&#x2F;写操作，后者如 fsync 等），但超时命令（timeout command）有所不同。<strong>IORING_OP_TIMEOUT</strong> 命令不直接操作数据，而是帮助管控完成环上的等待。该超时命令支持两种不同的触发类型，并且可以在单个命令中同时使用。一种触发类型是经典的超时，调用者传递一个（变体的）<code>struct timespec</code> 结构，其中包含非零的秒或纳秒值。为了保持 32 位与 64 位应用程序及内核空间之间的兼容性，使用的类型格式应如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> __<span class="hljs-title">kernel_timespec</span> &#123;</span>    <span class="hljs-type">int64_t</span> tv_sec;   <span class="hljs-comment">// 秒</span>    <span class="hljs-type">long</span> <span class="hljs-type">long</span> tv_nsec;     <span class="hljs-comment">// 纳秒</span>&#125;;</code></pre><p>在某些时候，用户空间应当具备一个符合上述描述的 <code>struct timespec64</code> 类型。在此之前，必须使用上述类型。如果希望使用计时超时，sqe 的 <strong>addr</strong> 字段必须指向一个这样的结构体。一旦指定的时间量过去，超时命令就会完成。</p><p>第二种触发类型是完成计数。如果使用此类型，应在 sqe 的 <strong>offset</strong> 字段中填入完成计数值。一旦自从超时命令排队以来，指定数量的完成事件产生，超时命令就会完成。</p><p>可以在单个超时命令中指定两个触发器事件。如果单个超时命令同时包含两个条件，则第一个触发的条件将生成超时完成事件。当发布超时完成事件时，任何等待完成事件者都将被唤醒，无论他们要求的完成量是否已满足。</p><h3 id="6-0-内存排序"><a href="#6-0-内存排序" class="headerlink" title="6.0 内存排序"></a>6.0 内存排序</h3><p>通过 io_uring 实例进行安全且高效通信的一个关键方面是正确使用内存排序原语。详细探讨各种架构的内存排序超出了本文的范围。如果你乐于使用通过 liburing 库暴露的简化版 io_uring API，那么你可以安全地忽略本节，直接跳到 liburing 库部分。但如果你有兴趣使用原始接口，理解本节内容就很重要了。</p><p>为了简化问题，我们将它归结为两个简单的内存排序操作。以下解释为了简洁而有所简化。</p><ul><li><code>read_barrier()</code>：确保在进行后续内存读取之前，之前的写操作对其他 CPU 可见。</li><li><code>write_barrier()</code>：确保此写操作发生在之前的写操作之后。</li></ul><p>根据目标架构的不同，这两个操作之一或两者都可能是空操作（no-ops）。但在使用 io_uring 时，这一点并不重要。重要的是，某些架构确实需要它们，因此应用程序开发者需要理解如何正确使用。<code>write_barrier()</code> 是为了确保写操作的顺序。假设一个应用程序希望填写一个 sqe 并通知内核有一个新的 sqe 可供处理，这是一个两阶段的过程——首先填写 sqe 的各个成员并将 sqe 的索引放入 SQ 环数组中，然后更新 SQ 环的尾指针以显示内核有新条目可用。如果不明确指定顺序，处理器可以任意重新排序这些写操作以达到其认为最优化的顺序。让我们看看下面的例子，每个数字代表一个内存操作：</p><pre><code class="hljs c"><span class="hljs-number">1</span>: sqe→opcode = IORING_OP_READV;<span class="hljs-number">2</span>: sqe→fd = fd;<span class="hljs-number">3</span>: sqe→off = <span class="hljs-number">0</span>;<span class="hljs-number">4</span>: sqe→addr = &amp;iovec;<span class="hljs-number">5</span>: sqe→len = <span class="hljs-number">1</span>;<span class="hljs-number">6</span>: sqe→user_data = some_value;<span class="hljs-number">7</span>: sqring→tail = sqring→tail + <span class="hljs-number">1</span>;</code></pre><p>无法保证操作 7（使 sqe 对内核可见的写操作）会作为序列中的最后一个写操作执行。操作 7 之前的所有写操作，在操作 7 之前对内核可见至关重要，否则内核可能会看到一个只写了一半的 sqe。从应用程序的角度来看，在通知内核有新的 sqe 之前，你需要一个写屏障来确保写操作的正确顺序。由于只要在尾部写入之前 sqe 的存储可见，它们的实际存储顺序并不重要，我们可以在操作 6 之后、操作 7 之前使用一个排序原语就能满足要求。因此，序列看起来应该是这样的：</p><pre><code class="hljs c"><span class="hljs-number">1</span>: sqe→opcode = IORING_OP_READV;<span class="hljs-number">2</span>: sqe→fd = fd;<span class="hljs-number">3</span>: sqe→off = <span class="hljs-number">0</span>;<span class="hljs-number">4</span>: sqe→addr = &amp;iovec;<span class="hljs-number">5</span>: sqe→len = <span class="hljs-number">1</span>;<span class="hljs-number">6</span>: sqe→user_data = some_value; write_barrier(); <span class="hljs-comment">/* 确保之前的写入在尾部写入前可见 */</span><span class="hljs-number">7</span>: sqring→tail = sqring→tail + <span class="hljs-number">1</span>; write_barrier(); <span class="hljs-comment">/* 确保尾部写入对其他 CPU 可见 */</span></code></pre><p>内核在读取 SQ 环的尾部之前会包含一个 <code>read_barrier()</code>，以确保来自应用程序的尾部写入是可见的。从 CQ 环的角度来看，因为消费者和生产者的角色是相反的，应用程序只需在读取 CQ 环的尾部之前执行一个 <code>read_barrier()</code>，以确保它能看到内核所做的任何写入。</p><p>虽然内存顺序类型被简化为两种特定类型，但架构的具体实现当然会根据代码运行的机器不同而不同。即使应用程序直接使用 io_uring 接口（而不是 liburing 的帮助函数），它仍然需要特定于架构的屏障类型。liburing 库提供了这些定义，并建议应用程序使用它们。</p><p>有了对内存顺序的基本解释，以及 liburing 提供的管理它们的帮助函数，现在回过头去看前面引用了 <code>read_barrier()</code> 和 <code>write_barrier()</code> 的例子。如果之前它们看起来不太明白，现在应该能理解了。</p><h3 id="7-0-liburing-库"><a href="#7-0-liburing-库" class="headerlink" title="7.0 liburing 库"></a>7.0 liburing 库</h3><p>在了解了 io_uring 的内部细节后，你会很高兴得知有一个更简单的方法来完成上述大部分工作。liburing 库有两个主要目的：</p><ul><li>去除 io_uring 实例设置所需的模板代码</li><li>为基本使用场景提供简化的 API</li></ul><p>后者确保了应用程序完全不必担心内存屏障，也不必自己处理环缓冲区管理。这使得 API 变得更加简洁易懂，并且实际上不再需要深入理解其内部工作原理。如果仅关注提供基于 liburing 的示例，本文可以大大缩短，但了解一些内部工作原理对于从应用程序中榨取最佳性能通常是有益的。此外，尽管 liburing 当前专注于减少模板代码并为标准使用场景提供基础帮助函数，一些更高级的功能尚未通过 liburing 提供。不过，这并不意味着你不能混合使用两者。实际上，它们底层操作的是相同的结构。通常鼓励应用程序即使使用原始接口，也采用 liburing 的创建助手。</p><h4 id="7-1-liburing-的-io-uring-创建"><a href="#7-1-liburing-的-io-uring-创建" class="headerlink" title="7.1 liburing 的 io_uring 创建"></a>7.1 liburing 的 io_uring 创建</h4><p>从一个例子开始。liburing 提供了以下基本助手函数，代替手动调用 <code>io_uring_setup(2)</code> 并随后映射三个必需的区域：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring</span> <span class="hljs-title">ring</span>;</span>io_uring_queue_init(ENTRIES, &amp;ring, <span class="hljs-number">0</span>);</code></pre><p><code>io_uring</code> 结构体保存了 SQ 和 CQ 环的信息，<code>io_uring_queue_init(3)</code> 调用为你处理了所有创建逻辑。在这个特定示例中，我们向 <strong>flags</strong> 参数传入了 0。一旦应用程序结束 io_uring 实例的使用，只需调用：</p><pre><code class="hljs c">io_uring_queue_exit(&amp;ring);</code></pre><p>来清理它。类似于应用程序分配的其他资源，一旦应用程序退出，内核会自动回收它们。对于应用程序可能创建的任何 io_uring 实例，也是如此。</p><h4 id="7-2-liburing-的提交与完成"><a href="#7-2-liburing-的提交与完成" class="headerlink" title="7.2 liburing 的提交与完成"></a>7.2 liburing 的提交与完成</h4><p>一个非常基本的使用场景是提交一个请求，稍后再等待它完成。使用 liburing 的帮助函数，操作大致如下：</p><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_sqe</span> *<span class="hljs-title">sqe</span>;</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">io_uring_cqe</span> *<span class="hljs-title">cqe</span>;</span><span class="hljs-comment">/* 获取 sqe 并填写 READV 操作 */</span>sqe = io_uring_get_sqe(&amp;ring);io_uring_prep_readv(sqe, fd, &amp;iovec, <span class="hljs-number">1</span>, offset);<span class="hljs-comment">/* 告诉内核有一个可供消费的 sqe */</span>io_uring_submit(&amp;ring);<span class="hljs-comment">/* 等待 sqe 完成 */</span>io_uring_wait_cqe(&amp;ring, &amp;cqe);<span class="hljs-comment">/* 读取并处理 cqe 事件 */</span>app_handle_cqe(cqe);io_uring_cqe_seen(&amp;ring, cqe);</code></pre><p>这应该是不言自明的。对 <code>io_uring_wait_cqe(3)</code> 的最后一次调用将返回我们刚提交的 sqe 的完成事件，前提是您没有其他正在飞行中的 sqe。如果有，那么完成事件可能属于另一个 sqe。</p><p>如果应用程序只想查看完成状态而不是等待事件变为可用，<code>io_uring_peek_cqe(3)</code> 就能做到这个需求。对于这两种情况，应用程序在处理完这个完成事件后都必须调用 <code>io_uring_cqe_seen(3)</code>。否则，重复调用 <code>io_uring_peek_cqe(3)</code> 或 <code>io_uring_wait_cqe(3)</code> 会一直返回相同的事件。这种区分是必要的，以避免内核在应用程序处理完之前就可能覆盖现有的完成事件。<code>io_uring_cqe_seen(3)</code> 递增 CQ 环头，使得内核可以在同一槽位填充新的事件。</p><p>有多种辅助函数用于填充 sqe，<code>io_uring_prep_readv(3)</code> 只是一个例子。我鼓励应用程序尽可能利用 liburing 提供的辅助器。</p><p>liburing 库仍处于初期阶段，并在不断开发中以扩展支持的功能和可用的辅助工具。</p><h3 id="8-0-高级使用场景与特性"><a href="#8-0-高级使用场景与特性" class="headerlink" title="8.0 高级使用场景与特性"></a>8.0 高级使用场景与特性</h3><p>上述示例和使用场景适用于各种类型的 I&#x2F;O，无论是基于文件的 <strong>O_DIRECT</strong> I&#x2F;O、缓冲 I&#x2F;O、套接字 I&#x2F;O 等。无需特别注意就能确保它们的正确操作或异步性质。然而，io_uring 确实提供了一系列特性，应用程序需要选择启用。接下来的小节将描述其中大部分内容。</p><h4 id="8-1-固定文件和缓冲区"><a href="#8-1-固定文件和缓冲区" class="headerlink" title="8.1 固定文件和缓冲区"></a>8.1 固定文件和缓冲区</h4><p>每次将文件描述符填入 sqe 并提交给内核时，内核都必须获取对该文件的引用。一旦 I&#x2F;O 完成，该文件引用再次被释放。由于文件引用的原子性，对于高 IOPS 工作负载，这可能会成为显著的减速因素。为了解决这个问题，io_uring 提供了一种方法，可以为 io_uring 实例预先注册一个文件集。这是通过第三个系统调用来实现的：</p><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">io_uring_register</span><span class="hljs-params">(<span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> fd, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> opcode, <span class="hljs-type">void</span> *arg, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> nr_args)</span>;</code></pre><p><strong>fd</strong> 是 io_uring 实例的环文件描述符，而 <strong>opcode</strong> 指定了正在进行的注册操作类型。如果要注册一个文件集合，必须使用 <strong>IORING_REGISTER_FILES</strong> 。此时，<strong>arg</strong> 应指向一个应用程序已经打开的文件描述符数组；同时，<strong>nr_args</strong> 必须包含该数组的大小。一旦针对文件集合的 <code>io_uring_register(2)</code> 调用成功完成，应用程序就可以通过在 sqe（提交队列条目）的 <code>fd</code> 字段赋值文件描述符数组中的索引（而不是实际的文件描述符），并设置 sqe 的 <code>flags</code> 字段为 <strong>IOSQE_FIXED_FILE</strong> 来标识这是一个文件集合的 fd，进而使用这些文件。即使已注册了文件集合，应用程序仍然可以自由地使用未注册的文件，只需将 sqe 的 <code>fd</code> 设置为未注册的文件描述符，并不在 <code>flags</code> 中设置 <strong>IOSQE_FIXED_FILE</strong> 即可。当 io_uring 实例被销毁时，已注册的文件集合会自动释放；或者，也可以通过在 <code>io_uring_register(2)</code> 的 <strong>opcode</strong> 中使用 <strong>IORING_UNREGISTER_FILES</strong> 手动进行释放。</p><p>另外，应用程序还可以注册一组固定的 I&#x2F;O 缓冲区。当使用 <strong>O_DIRECT</strong> 方式进行 I&#x2F;O 操作时，内核需要在执行 I&#x2F;O 之前将应用程序的页面映射到内核空间，然后在 I&#x2F;O 完成后再解除映射，这一过程可能比较耗时。如果应用程序重复使用 I&#x2F;O 缓冲区，就可以通过一次性完成映射和解除映射来优化，而不是为每个 I&#x2F;O 操作都重复进行。为了注册这样一组固定的 I&#x2F;O 缓冲区，需要使用 <strong>IORING_REGISTER_BUFFERS</strong> 作为操作码调用 <code>io_uring_register(2)</code>，并且 <strong>args</strong> 应当指向一个 <code>struct iovec</code> 结构体数组，该数组中填入了各个缓冲区的地址和长度信息。<strong>nr_args</strong> 则应包含 <code>iovec</code> 数组的大小。一旦缓冲区注册成功，应用程序就可以使用 <strong>IORING_OP_READ_FIXED</strong> 和 <strong>IORING_OP_WRITE_FIXED</strong> 操作码来读写这些固定的缓冲区。使用这些固定操作码时，sqe 的 <code>addr</code> 字段必须指向这些缓冲区之一内的地址，而 <code>len</code> 字段则需指定请求的字节长度。应用程序可以注册大于任何单次 I&#x2F;O 操作所需的缓冲区，即一个固定的读&#x2F;写操作完全可以只使用单一固定缓冲区的一部分，这是完全合法的。</p><h4 id="8-2-轮询-I-x2F-O（POLLED-IO）"><a href="#8-2-轮询-I-x2F-O（POLLED-IO）" class="headerlink" title="8.2 轮询 I&#x2F;O（POLLED IO）"></a>8.2 轮询 I&#x2F;O（POLLED IO）</h4><p>对于追求极低延迟的应用程序，io_uring 提供了对文件轮询 I&#x2F;O 的支持。在这种情况下，轮询指的是执行 I&#x2F;O 操作时不依赖硬件中断来指示完成事件。当采用轮询 I&#x2F;O 时，应用程序会不断地向硬件驱动查询已提交 I&#x2F;O 请求的状态。这与非轮询 I&#x2F;O 不同，在非轮询模式下，应用程序通常会进入休眠状态，等待硬件中断作为唤醒源。对于极低延迟设备而言，轮询可以显著提升性能。同样，对于具有极高 IOPS（每秒输入输出操作数）的应用程序，高中断率使得非轮询方式的负载拥有更高的开销。是否采用轮询的界限，无论是从延迟还是总体 IOPS 速率来看，都依据具体的应用程序、I&#x2F;O 设备及机器能力而有所不同。</p><p>为了利用 I&#x2F;O 轮询，必须在调用 <code>io_uring_setup(2)</code> 系统调用或使用 <code>io_uring_queue_init(3)</code>liburing 库助手时，在传入的标志中设置 <strong>IORING_SETUP_IOPOLL</strong>。当启用轮询时，应用程序不能再检查 CQ（完成队列）环尾部来确认完成事件的可用性，因为不会有自动触发的异步硬件侧完成事件。相反，应用程序必须主动查找并收割这些事件，通过调用 <code>io_uring_enter(2)</code> 并设置 <strong>IORING_ENTER_GETEVENTS</strong> 以及将 <strong>min_complete</strong> 设置为期望的事件数量来实现。设置 <strong>IORING_ENTER_GETEVENTS</strong> 且将 <strong>min_complete</strong> 设为 0 也是合法的，这意味着要求内核仅在驱动端检查一次完成事件，而非持续循环检测。</p><p>只有那些适合轮询完成的操作码才可以在 <strong>IORING_SETUP_IOPOLL</strong> 注册过的 io_uring 实例上使用，包括所有读写命令：<strong>IORING_OP_READV</strong>、<strong>IORING_OP_WRITEV</strong>、<strong>IORING_OP_READ_FIXED</strong>、<strong>IORING_OP_WRITE_FIXED</strong>。在注册为轮询的 io_uring 实例上发出非轮询操作码是非法的，这样做会导致 <code>io_uring_enter(2)</code> 返回 <strong>-EINVAL</strong> 错误。背后的原因是内核无法判断带有 <strong>IORING_ENTER_GETEVENTS</strong> 标志的 <code>io_uring_enter(2)</code> 调用是否能安全地睡眠等待事件，还是应该积极地进行轮询。</p><h4 id="8-3-内核侧轮询（KERNEL-SIDE-POLLING）"><a href="#8-3-内核侧轮询（KERNEL-SIDE-POLLING）" class="headerlink" title="8.3 内核侧轮询（KERNEL SIDE POLLING）"></a>8.3 内核侧轮询（KERNEL SIDE POLLING）</h4><p>尽管 io_uring 通常在允许更多的请求通过更少的系统调用来完成发起和处理方面效率更高，但在某些情况下，我们仍可以通过进一步减少执行 I&#x2F;O 所需的系统调用数量来提高效率。其中一个功能就是内核侧轮询。启用该功能后，应用程序不再需要调用 <code>io_uring_enter(2)</code> 来提交 I&#x2F;O。当应用程序更新 SQ 环并填写新的 sqe（提交队列条目）时，内核侧会自动发现新条目并提交它们。这是通过一个特定于该 io_uring 的内核线程来完成的。</p><p>要使用此功能，io_uring 实例必须在 <code>io_uring_params</code> 的 <strong>flag</strong> 成员中使用 <strong>IORING_SETUP_SQPOLL</strong> 进行注册，或者传递给 <code>io_uring_queue_init(3)</code> 函数。此外，如果应用程序希望将此线程限制在特定 CPU 上，可以通过同时标记 <strong>IORING_SETUP_SQ_AFF</strong> 并将 <code>io_uring_params</code> 的 <strong>sq_thread_cp</strong> 设置为所需 CPU 来实现。需要注意的是，使用 <strong>IORING_SETUP_SQPOLL</strong> 设置 io_uring 实例是一个需要特权的操作。如果用户没有足够的权限，<code>io_uring_queue_init(3)</code> 将失败并返回 <strong>-EPERM</strong> 错误。</p><p>为了避免在 io_uring 实例空闲时浪费过多 CPU，内核侧线程将在闲置一段时间后自动进入休眠状态。当发生这种情况时，线程会在 SQ 环的标志成员中设置 <strong>IORING_SQ_NEED_WAKEUP</strong>。当此标志被设置时，应用程序不能依赖内核自动发现新条目，而必须随后调用带有 <strong>IORING_ENTER_SQ_WAKEUP</strong> 标志的 <code>io_uring_enter(2)</code>。应用程序侧的逻辑通常如下所示：</p><pre><code class="hljs c"><span class="hljs-comment">/* 增加新的 sqe 条目 */</span>add_more_io();<span class="hljs-comment">/*</span><span class="hljs-comment">* 如果轮询并且线程现在正在睡眠，则需要调用io_uring_enter() 以使内核注意到新的 io</span><span class="hljs-comment">*/</span><span class="hljs-keyword">if</span> ((*sqring→flags) &amp; IORING_SQ_NEED_WAKEUP) io_uring_enter(ring_fd, to_submit, to_wait, IORING_ENTER_SQ_WAKEUP);</code></pre><p>只要应用程序持续进行 I&#x2F;O 操作，就不会设置 <strong>IORING_SQ_NEED_WAKEUP</strong>，我们就可以在不执行任何系统调用的情况下有效地执行 I&#x2F;O。然而，重要的是要在应用程序中始终保持类似上述的逻辑，以防线程确实进入休眠。进入空闲状态前的具体宽限期可以通过设置 <code>io_uring_params</code> 的 <strong>sq_thread_idle</strong> 成员来配置，其值以毫秒为单位。如果不设置该成员，内核默认在使线程休眠前空闲一秒钟。</p><p>对于“常规”的 IRQ 驱动 I&#x2F;O，应用程序直接查看 CQ 环即可找到完成事件。如果 io_uring 实例配置了 <strong>IORING_SETUP_IOPOLL</strong>，则内核线程也会负责收割完成事件。因此，在这两种情况下，除非应用程序希望等待 I&#x2F;O 发生，否则它只需简单地查看 CQ 环以查找完成事件。</p><h3 id="9-0-性能表现"><a href="#9-0-性能表现" class="headerlink" title="9.0 性能表现"></a>9.0 性能表现</h3><p>最终，io_uring 达到了为其设定的设计目标。我们拥有一个非常高效的内核与应用程序之间的通信机制，表现为两个独立的环。虽然原始接口在应用程序中正确使用时需要一些注意事项，但主要的复杂之处实际上在于需要显式的内存排序原语。这些原语在事件的提交和处理的提交和完成两端都有特定的应用，且通常在不同应用程序中遵循相同模式。随着 liburing 接口的不断成熟，我预计大多数应用程序都会对提供的 API 感到相当满意。</p><p>虽然本文无意深入细节讨论 io_uring 实现的性能和可扩展性，但本节将简要涉及在此领域观察到的一些优势。更多详细信息，请参见 [1]。请注意，由于对块层进行了进一步改进，这些结果有些过时。例如，在我的测试环境中，通过 io_uring 实现的每核心峰值性能现在大约为 170 万次 4k IOPS，而非 162 万次。请注意，这些数值本身并没有太多绝对意义，它们主要用于衡量相对改进。现在，应用程序与内核之间的通信机制不再是瓶颈，我们将继续通过使用 io_uring 发现更低的延迟和更高的峰值性能。</p><h4 id="9-1-原始性能表现"><a href="#9-1-原始性能表现" class="headerlink" title="9.1 原始性能表现"></a>9.1 原始性能表现</h4><p>考察接口的原始性能有许多方法。大多数测试也将涉及内核的其他部分。一个例子是上文中的数字，我们通过随机从块设备或文件读取来衡量性能。在峰值性能下，通过轮询，io_uring 帮助我们达到了 170 万次 4k IOPS。相比之下，aio 的性能峰值远低于此，仅为 60.8 万次。这里的比较并不完全公平，因为 aio 不支持轮询 I&#x2F;O。如果我们禁用轮询，io_uring 在相同的测试案例中仍能驱动约 120 万次 IOPS。此时，aio 的局限性变得非常明显，对于相同的工作负载，io_uring 能够驱动两倍的 IOPS。</p><p>io_uring 还支持无操作命令，这对于检查接口的原始吞吐量特别有用。根据所使用的系统，每秒消息数量从我笔记本电脑上的 1200 万次到用于其他引用结果的测试盒上的 2000 万次不等。实际结果根据具体的测试案例有很大差异，主要受限于必须执行的系统调用数量。原始接口在其他方面受内存限制，由于提交和完成消息在内存中既小又线性，因此实现的消息每秒速率可以非常高。</p><h4 id="9-2-缓存异步-I-x2F-O-性能"><a href="#9-2-缓存异步-I-x2F-O-性能" class="headerlink" title="9.2 缓存异步 I&#x2F;O 性能"></a>9.2 缓存异步 I&#x2F;O 性能</h4><p>我之前提到过，内核级别的缓冲异步 I&#x2F;O 实现可能比用户空间中的实现更为高效。一个重要原因与缓存与未缓存数据有关。进行缓冲 I&#x2F;O 时，应用程序通常严重依赖内核的页缓存来获得良好性能。用户空间应用程序无法得知它接下来请求的数据是否已经被缓存。它可以查询这一信息，但这需要更多的系统调用，而且答案本质上总是存在竞争条件——此刻被缓存的数据可能在几毫秒之后就不再是缓存中的了。因此，拥有 I&#x2F;O 线程池的应用程序总是需要将请求发送到异步上下文中，导致至少两次上下文切换。如果请求的数据已经在页缓存中，将导致性能急剧下降。</p><p>io_uring 处理这种情况的方式与其他可能导致应用程序阻塞的资源相同。更重要的是，对于不会阻塞的操作，数据会直接在线提供。这使得 io_uring 在处理已位于页缓存中的 I&#x2F;O 时，与常规同步接口一样高效。一旦 I&#x2F;O 提交调用返回，应用程序就会在 CQ 环中立即有一个等待它的完成事件，数据也已被复制。</p><h3 id="10-0-进一步阅读"><a href="#10-0-进一步阅读" class="headerlink" title="10.0 进一步阅读"></a>10.0 进一步阅读</h3><p>鉴于这是一个全新的接口，目前还没有广泛采用。截至撰写时，包含此接口的内核正处于候选发布阶段（-rc）。即使有了相当完整的接口描述，研究使用 io_uring 的程序对于完全理解如何最好地使用它也是有益的。</p><p>一个例子是随 fio[2] 提供的 io_uring 引擎，它能够使用所有描述过的高级特性，除了注册文件集之外。</p><p>另一个例子是随 fio 一起提供的 t&#x2F;io_uring.c 示例基准测试应用程序，它只是对文件或设备进行随机读取，具有可配置的设置，可以探索高级使用场景的整个特性集。</p><p>liburing 库 [3] 有一整套针对系统调用接口的手册页，值得一读。它还附带了一些测试程序，包括开发过程中发现的问题的单元测试，以及技术演示。</p><p>LWN 还撰写了一篇关于 io_uring 早期阶段的优秀文章 [4]。请注意，在这篇文章发表后，io_uring 做了一些改动，因此在两者之间存在差异的情况下，我建议参考本文。</p><h3 id="11-0-参考文献"><a href="#11-0-参考文献" class="headerlink" title="11.0 参考文献"></a>11.0 参考文献</h3><p>[1] <a href="https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/">https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/</a><br>[2] git:&#x2F;&#x2F;git.kernel.dk&#x2F;fio<br>[3] git:&#x2F;&#x2F;git.kernel.dk&#x2F;liburing<br>[4] <a href="https://lwn.net/Articles/776703/">https://lwn.net/Articles/776703/</a></p><p><em>原文：</em> <a href="https://kernel.dk/io_uring.pdf">Efficient IO with io_uring</a></p><blockquote><p>本文翻译仅用于学习与技术交流，版权归原作者所有，如有侵权请联系删除。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/05-18-2024/efficient-io-with-io_uring.html">https://www.cyningsun.com/05-18-2024/efficient-io-with-io_uring.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;本文旨在作为最新 Linux I&amp;#x2F;O 接口 io_uring 的入门介绍，并将其与现有技术进行比较。我们将探讨其存在的原因、内部运作机制以及用户可见的接口。文章不会深入到特定命令等细节，因为这些信息在相关的 man 手册页中已有提供。相反，我们的目标是为读者提供对</summary>
      
    
    
    
    <category term="Linux" scheme="https://www.cyningsun.com/category/Linux/"/>
    
    
    <category term="I/O" scheme="https://www.cyningsun.com/tag/I-O/"/>
    
  </entry>
  
  <entry>
    <title>记一次 Redis 延时毛刺问题定位</title>
    <link href="https://www.cyningsun.com/12-22-2023/redis-latency-spike.html"/>
    <id>https://www.cyningsun.com/12-22-2023/redis-latency-spike.html</id>
    <published>2023-12-21T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>该问题发生于八月份，业务发现部分线上集群出现 10 分钟一次的耗时毛刺。整个系统的架构很简单：</p><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441.jpeg"></p><p>在 Redis Proxy  可以观察到明显的请求耗时毛刺，因此可以确定问题确实出现在 Redis Proxy 调用 Redis 的某个环节</p><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441-1.png"></p><p>然而，为了定位该问题，仍然花费了很长的时间：</p><ul><li>该问题非必现，且不固定于某台机器</li><li>问题发现时，相同&#x2F;类似毛刺现象涉及众多集群</li><li>在线的 Redis 版本缺少 P99 指标（耗时指标仅包括执行耗时，不包括等待耗时）耗时毛刺被平均之后无法观察到</li></ul><h3 id="问题定位"><a href="#问题定位" class="headerlink" title="问题定位"></a>问题定位</h3><p>由于无法利用现有指标缩小问题的范围，只能按照可能性从高到底排查：业务请求 &gt; 网络 &gt; 系统 &gt; 应用。</p><ul><li>业务层面：部分集群发现少量 LUA script 相关的慢速日志</li><li>网络层面：使用 mtr 观测出现问题的时间点网络状态，并排查上层交换机之后未见异常</li><li>系统层面：根据业务反馈之前有类似故障出现，原因是 <a href="https://github.com/Atoptool/atop/commit/604b563a223130d9bcce3d3358537a6c5ce05e7a">atop</a> 采集进程 PSS 导致延迟增加。该 case 可以稳定复现，现象略有不同；抽查有异常机器检查未发现有安装 atop。</li><li>应用层：相关集群已经较长时间没有版本更新，使用 perf record 很难发现毛刺类型问题</li></ul><p>在针对某一个集群的 master failover 到其他节点，请求延迟毛刺消失。对比前后两台机器发现 atop 进程的差异。</p><pre><code class="hljs sh">$&gt; ps aux|grep atoproot       2442  0.0  0.0   2500  1628 ?        S&lt;    2022  42:21 /usr/sbin/atopacctdroot      11530  0.0  0.0  18024  2068 pts/0    S+   22:08   0:00 grep --color=auto atoproot     182181  1.5  0.0  33784 33184 ?        S&lt;Ls 00:00  20:51 /usr/bin/atop -R -w /var/log/atop/atop_20230807 600$&gt; ps aux|grep atoproot     403334  0.0  0.0  16572  2016 pts/0    S+   22:09   0:00 grep --color=auto atop</code></pre><p>停止所有 atop 之后，请求延迟消失</p><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441-2.jpeg"></p><p>原来，线上部分机器部署的 <a href="https://github.com/Atoptool/atop/commit/604b563a223130d9bcce3d3358537a6c5ce05e7a">atop 版本</a> 默认启用了 -R 选项。在 atop 读 &#x2F;proc&#x2F;${pid}&#x2F;smaps 时，会遍历整个进程的页表，期间会持有内存页表的锁。如果在此期间进程发生虚拟内存地址分配，也需要获取锁，就需要等待锁释放。具体到应用层面就是请求耗时毛刺。</p><p>除了 atop，<a href="https://github.com/google/cadvisor/commit/7ab5e27909c5b72363641ed6e683be42488e0365#diff-28d7cc5aa0eb19db1d3a6d3d16080b4a1940e691ec501ccbd2af273de8034508R366">cadvisor</a> 等应用也会读取 &#x2F;proc&#x2F;${pid}&#x2F;smaps，虽然默认关闭。由于关闭的方式是通过 <a href="https://github.com/google/cadvisor/commit/7ab5e27909c5b72363641ed6e683be42488e0365#diff-cec39746c40e86227962aa52ed9ac22cf95c1504cef42cb16c0dd5c16a8cf6ca">disable_metrics</a> 来指定关闭。如果自定义参数时遗漏相关参数，还是会打开该功能触发耗时毛刺</p><h3 id="根因分析"><a href="#根因分析" class="headerlink" title="根因分析"></a>根因分析</h3><p>当读取 &#x2F;proc&#x2F;${pid}&#x2F;smaps 获得某个进程虚拟内存区间信息时，究竟发生了什么？</p><h4 id="seq-file"><a href="#seq-file" class="headerlink" title="seq_file"></a>seq_file</h4><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441-3.jpeg"></p><p>Linux 使用文件将内核里面数据结构通过文件导出到用户空间， smaps 使用到的文件类型就是 <code>seq_file</code> 文件。</p><pre><code class="hljs c"><span class="hljs-comment">// linux/include/linux/seq_file.h</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">seq_file</span> &#123;</span>    <span class="hljs-type">char</span> *buf;    <span class="hljs-comment">// 指向包含要读取或写入的数据的缓冲区</span>    <span class="hljs-type">size_t</span> size;  <span class="hljs-comment">// 缓冲区的大小</span>    <span class="hljs-type">size_t</span> from;  <span class="hljs-comment">// 缓冲区中读取或写入的起始位置</span>    <span class="hljs-type">size_t</span> count; <span class="hljs-comment">// 读取或写入的字节数</span>    <span class="hljs-type">size_t</span> pad_until;  <span class="hljs-comment">// 将输出填充到某个位置</span>    <span class="hljs-type">loff_t</span> index; <span class="hljs-comment">// 序列中的当前位置</span>    <span class="hljs-type">loff_t</span> read_pos;   <span class="hljs-comment">// 当前的读取位置</span>    u64 version;  <span class="hljs-comment">// 文件版本</span>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mutex</span> <span class="hljs-title">lock</span>;</span> <span class="hljs-comment">// 锁，确保对 seq_file 操作是线程安全的</span>    <span class="hljs-type">const</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">seq_operations</span> *<span class="hljs-title">op</span>;</span> <span class="hljs-comment">// 该结构定义了可以对 proc 执行的操作</span>    <span class="hljs-type">int</span> poll_event;    <span class="hljs-comment">// 用于 poll 和 select 系统调用</span>    <span class="hljs-type">const</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">file</span>;</span> <span class="hljs-comment">// 指向文件结构的指针，即 seq_file 关联的 proc</span>    <span class="hljs-type">void</span> *private; <span class="hljs-comment">// 私有数据字段，存储特定于文件的数据</span>&#125;;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">seq_operations</span> &#123;</span>    <span class="hljs-comment">// 开始读数据项，通常需要加锁，以防止并行访问数据</span><span class="hljs-type">void</span> * (*start) (<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">loff_t</span> *pos);<span class="hljs-comment">// 停止读数据项，通常需要解锁</span><span class="hljs-type">void</span> (*stop) (<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v);        <span class="hljs-comment">// 找到下一个要处理的数据项</span><span class="hljs-type">void</span> * (*next) (<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v, <span class="hljs-type">loff_t</span> *pos);        <span class="hljs-comment">// 打印数据项到临时缓冲区</span><span class="hljs-type">int</span> (*show) (<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v);&#125;;</code></pre><p><code>seq_file</code> 使用 file 存储需要关联的进程，<code>seq_operations</code> 定义读取进程数据的操作。使用全局函数 <code>seq_open</code> 把进程与 <code>seq_operations</code> 关联起来</p><blockquote><p>用户态： open(“&#x2F;proc&#x2F;pid&#x2F;smaps”) –&gt;  内核态： proc_pid_smaps_operations.open()<br>用户态： read(fd)                             –&gt;  内核态：  proc_pid_smaps_operations.read()</p></blockquote><h4 id="smaps"><a href="#smaps" class="headerlink" title="smaps"></a>smaps</h4><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441-4.jpeg"></p><p>具体到 smaps，也是一样的实现 file 相关的方法，在内核中是定义在 <strong>proc_pid_smaps_operations</strong> 结构：</p><pre><code class="hljs c"><span class="hljs-comment">// linux/fs/proc/base.c</span>REG(<span class="hljs-string">&quot;smaps&quot;</span>,      S_IRUGO, proc_pid_smaps_operations)<span class="hljs-comment">// linux/fs/proc/task_mmu.c</span><span class="hljs-comment">// `file_operations` 结构的一个实例，定义 `/proc/PID/smaps` 文件的操作，当操作`/proc/PID/smaps` 文件时被调用</span><span class="hljs-type">const</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file_operations</span> <span class="hljs-title">proc_pid_smaps_operations</span> =</span> &#123;.open= pid_smaps_open, <span class="hljs-comment">// 打开文件的函数</span>.read= seq_read,       <span class="hljs-comment">// 读取文件的函数</span>.llseek= seq_lseek,      <span class="hljs-comment">// 定位文件的函数</span>.release= proc_map_release, <span class="hljs-comment">// 释放文件的函数</span>&#125;;</code></pre><p>其中 open() 函数最终会返回一个文件描述符 fd 供后续 read(fd) 函数使用。</p><pre><code class="hljs c++"><span class="hljs-comment">// linux/fs/proc/task_mmu.c    pid_smaps_open()</span><span class="hljs-comment">//     ---&gt;linux/fs/proc/task_mmu.c    do_maps_open()</span><span class="hljs-comment">//         ---&gt;linux/fs/proc/task_mmu.c    proc_maps_open()</span><span class="hljs-comment">// `seq_operations`结构的实例，定义了一系列的操作函数，在处理`/proc/PID/smaps`文件时被调用</span><span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">seq_operations</span> proc_pid_smaps_op = &#123;.start= m_start,  <span class="hljs-comment">// 开始操作的函数</span>.next= m_next,   <span class="hljs-comment">// 下一步操作的函数</span>.stop= m_stop,   <span class="hljs-comment">// 停止操作的函数</span>.show= show_smap <span class="hljs-comment">// 显示操作的函数</span>&#125;;<span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title">pid_smaps_open</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-keyword">struct</span> file *file)</span></span><span class="hljs-function"></span>&#123;<span class="hljs-keyword">return</span> <span class="hljs-built_in">do_maps_open</span>(inode, file, &amp;proc_pid_smaps_op);&#125;<span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title">do_maps_open</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-keyword">struct</span> file *file,</span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> seq_operations *ops)</span></span><span class="hljs-function"></span>&#123;<span class="hljs-keyword">return</span> <span class="hljs-built_in">proc_maps_open</span>(inode, file, ops,<span class="hljs-built_in">sizeof</span>(<span class="hljs-keyword">struct</span> proc_maps_private));&#125;<span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title">proc_maps_open</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-keyword">struct</span> file *file,</span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> seq_operations *ops, <span class="hljs-type">int</span> psize)</span></span><span class="hljs-function"></span>&#123;    <span class="hljs-comment">// 调用`__seq_open_private`函数来打开一个序列文件，并返回一个指向`proc_maps_private`结构的指针。该结构包含了处理`/proc/PID/maps`文件所需的私有数据</span><span class="hljs-keyword">struct</span> <span class="hljs-title class_">proc_maps_private</span> *priv = __seq_open_private(file, ops, psize);<span class="hljs-keyword">if</span> (!priv)<span class="hljs-keyword">return</span> -ENOMEM;        priv-&gt;inode = inode; <span class="hljs-comment">// 将输入参数`inode`赋值给`priv-&gt;inode`</span><span class="hljs-comment">// 调用`proc_mem_open`函数以读取模式打开`inode`指向的内存对象，并将返回的内存描述符赋值给`priv-&gt;mm`</span>priv-&gt;mm = <span class="hljs-built_in">proc_mem_open</span>(inode, PTRACE_MODE_READ);<span class="hljs-keyword">if</span> (<span class="hljs-built_in">IS_ERR</span>(priv-&gt;mm)) &#123;<span class="hljs-type">int</span> err = <span class="hljs-built_in">PTR_ERR</span>(priv-&gt;mm);<span class="hljs-built_in">seq_release_private</span>(inode, file);<span class="hljs-keyword">return</span> err;&#125;<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;&#125;<span class="hljs-comment">// 打开序列文件并分配私有数据所需的基本操作</span><span class="hljs-type">void</span> *__seq_open_private(<span class="hljs-keyword">struct</span> file *f, <span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> seq_operations *ops,<span class="hljs-type">int</span> psize)&#123;<span class="hljs-type">int</span> rc;<span class="hljs-type">void</span> *<span class="hljs-keyword">private</span>;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">seq_file</span> *seq;<span class="hljs-keyword">private</span> = <span class="hljs-built_in">kzalloc</span>(psize, GFP_KERNEL);<span class="hljs-keyword">if</span> (<span class="hljs-keyword">private</span> == <span class="hljs-literal">NULL</span>)<span class="hljs-keyword">goto</span> out;rc = <span class="hljs-built_in">seq_open</span>(f, ops); <span class="hljs-comment">// 调用`seq_open`函数打开一个序列文件</span><span class="hljs-keyword">if</span> (rc &lt; <span class="hljs-number">0</span>)<span class="hljs-keyword">goto</span> out_free;、seq = f-&gt;private_data; <span class="hljs-comment">// 获取文件的私有数据，并将其转换为`seq_file`结构的指针</span>seq-&gt;<span class="hljs-keyword">private</span> = <span class="hljs-keyword">private</span>;<span class="hljs-keyword">return</span> <span class="hljs-keyword">private</span>;out_free:<span class="hljs-built_in">kfree</span>(<span class="hljs-keyword">private</span>);out:<span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;&#125;<span class="hljs-comment">/**</span><span class="hljs-comment"> *seq_open -initialize sequential file</span><span class="hljs-comment"> *@file: file we initialize</span><span class="hljs-comment"> *@op: method table describing the sequence</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *seq_open() sets @file, associating it with a sequence described</span><span class="hljs-comment"> *by @op.  @op-&gt;start() sets the iterator up and returns the first</span><span class="hljs-comment"> *element of sequence. @op-&gt;stop() shuts it down.  @op-&gt;next()</span><span class="hljs-comment"> *returns the next element of sequence.  @op-&gt;show() prints element</span><span class="hljs-comment"> *into the buffer.  In case of error -&gt;start() and -&gt;next() return</span><span class="hljs-comment"> *ERR_PTR(error).  In the end of sequence they return %NULL. -&gt;show()</span><span class="hljs-comment"> *returns 0 in case of success and negative number in case of error.</span><span class="hljs-comment"> *Returning SEQ_SKIP means &quot;discard this element and move on&quot;.</span><span class="hljs-comment"> */</span><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">seq_open</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> file *file, <span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> seq_operations *op)</span></span><span class="hljs-function"></span>&#123;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">seq_file</span> *p = file-&gt;private_data;<span class="hljs-keyword">if</span> (!p) &#123;p = <span class="hljs-built_in">kmalloc</span>(<span class="hljs-built_in">sizeof</span>(*p), GFP_KERNEL);<span class="hljs-keyword">if</span> (!p)<span class="hljs-keyword">return</span> -ENOMEM;file-&gt;private_data = p;&#125;<span class="hljs-built_in">memset</span>(p, <span class="hljs-number">0</span>, <span class="hljs-built_in">sizeof</span>(*p));<span class="hljs-built_in">mutex_init</span>(&amp;p-&gt;lock); <span class="hljs-comment">// 初始化`seq_file`结构的锁</span>p-&gt;op = op; <span class="hljs-comment">// 将输入参数`op`赋值给`seq_file`结构的`op`成员</span>    <span class="hljs-comment">// ... </span>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;&#125;<span class="hljs-keyword">struct</span> <span class="hljs-title class_">mm_struct</span> *<span class="hljs-built_in">proc_mem_open</span>(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> mode)&#123;<span class="hljs-comment">// 调用`get_proc_task`函数获取`inode`对应的进程的任务结构</span><span class="hljs-keyword">struct</span> <span class="hljs-title class_">task_struct</span> *task = <span class="hljs-built_in">get_proc_task</span>(inode);<span class="hljs-keyword">struct</span> <span class="hljs-title class_">mm_struct</span> *mm = <span class="hljs-built_in">ERR_PTR</span>(-ESRCH);    <span class="hljs-comment">// ... </span>    <span class="hljs-keyword">return</span> mm;&#125;</code></pre><p><code>pid_smaps_open</code> 函数通过参数 inode 找到进程相关的结构并放到 file 的私有数据结构。</p><p>当 <code>read</code> 时，调用 <code>seq_read()</code> 函数，它是内核的一个通用架构的函数，特定的 proc 文件（如：smaps）需要提供自己特有的操作方法供通用的 <code>seq_read()</code> 调用。smaps 即是 <code>pid_smaps_open()</code> 函数的 <code>file_operations</code> 参数 <code>&amp;proc_pid_smaps_op</code>，专门为读取进程虚拟内存区(vma)信息的方法。</p><pre><code class="hljs c"><span class="hljs-comment">/**</span><span class="hljs-comment"> *seq_read --&gt;read() method for sequential files.</span><span class="hljs-comment"> *@file: the file to read from</span><span class="hljs-comment"> *@buf: the buffer to read to</span><span class="hljs-comment"> *@size: the maximum number of bytes to read</span><span class="hljs-comment"> *@ppos: the current position in the file</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *Ready-made -&gt;f_op-&gt;read()</span><span class="hljs-comment"> */</span><span class="hljs-type">ssize_t</span> <span class="hljs-title function_">seq_read</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> file *file, <span class="hljs-type">char</span> __user *buf, <span class="hljs-type">size_t</span> size, <span class="hljs-type">loff_t</span> *ppos)</span>&#123;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">seq_file</span> *<span class="hljs-title">m</span> =</span> file-&gt;private_data;<span class="hljs-type">size_t</span> copied = <span class="hljs-number">0</span>;<span class="hljs-type">loff_t</span> pos;<span class="hljs-type">size_t</span> n;<span class="hljs-type">void</span> *p;<span class="hljs-type">int</span> err = <span class="hljs-number">0</span>;mutex_lock(&amp;m-&gt;lock); <span class="hljs-comment">// 锁定`seq_file`结构，以确保线程安全</span><span class="hljs-comment">/*</span><span class="hljs-comment"> * seq_file-&gt;op-&gt;..m_start/m_stop/m_next may do special actions</span><span class="hljs-comment"> * or optimisations based on the file-&gt;f_version, so we want to</span><span class="hljs-comment"> * pass the file-&gt;f_version to those methods.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * seq_file-&gt;version is just copy of f_version, and seq_file</span><span class="hljs-comment"> * methods can treat it simply as file version.</span><span class="hljs-comment"> * It is copied in first and copied out after all operations.</span><span class="hljs-comment"> * It is convenient to have it as  part of structure to avoid the</span><span class="hljs-comment"> * need of passing another argument to all the seq_file methods.</span><span class="hljs-comment"> */</span>m-&gt;version = file-&gt;f_version;<span class="hljs-comment">/* Don&#x27;t assume *ppos is where we left it */</span><span class="hljs-keyword">if</span> (unlikely(*ppos != m-&gt;read_pos)) &#123;<span class="hljs-keyword">while</span> ((err = traverse(m, *ppos)) == -EAGAIN);<span class="hljs-keyword">if</span> (err) &#123;<span class="hljs-comment">/* With prejudice... */</span>m-&gt;read_pos = <span class="hljs-number">0</span>;m-&gt;version = <span class="hljs-number">0</span>;m-&gt;index = <span class="hljs-number">0</span>;m-&gt;count = <span class="hljs-number">0</span>;<span class="hljs-keyword">goto</span> Done;&#125; <span class="hljs-keyword">else</span> &#123;m-&gt;read_pos = *ppos;&#125;&#125;<span class="hljs-comment">/* grab buffer if we didn&#x27;t have one */</span><span class="hljs-comment">// 如果`seq_file`结构没有缓冲区，需要分配一个</span><span class="hljs-keyword">if</span> (!m-&gt;buf) &#123;m-&gt;buf = seq_buf_alloc(m-&gt;size = PAGE_SIZE);<span class="hljs-keyword">if</span> (!m-&gt;buf)<span class="hljs-keyword">goto</span> Enomem;&#125;<span class="hljs-comment">/* if not empty - flush it first */</span><span class="hljs-comment">// 如果`seq_file`结构的缓冲区不为空，需要先将其内容复制到用户空间</span><span class="hljs-keyword">if</span> (m-&gt;count) &#123;n = min(m-&gt;count, size);err = copy_to_user(buf, m-&gt;buf + m-&gt;from, n);<span class="hljs-keyword">if</span> (err)<span class="hljs-keyword">goto</span> Efault;m-&gt;count -= n;m-&gt;from += n;size -= n;buf += n;copied += n;<span class="hljs-keyword">if</span> (!m-&gt;count)m-&gt;index++;<span class="hljs-keyword">if</span> (!size)<span class="hljs-keyword">goto</span> Done;&#125;<span class="hljs-comment">/* we need at least one record in buffer */</span>pos = m-&gt;index;p = m-&gt;op-&gt;start(m, &amp;pos);<span class="hljs-comment">// 从序列文件中读取记录，直到出错或缓冲区满</span><span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) &#123;err = PTR_ERR(p);<span class="hljs-keyword">if</span> (!p || IS_ERR(p))<span class="hljs-keyword">break</span>;err = m-&gt;op-&gt;show(m, p);<span class="hljs-keyword">if</span> (err &lt; <span class="hljs-number">0</span>)<span class="hljs-keyword">break</span>;<span class="hljs-keyword">if</span> (unlikely(err))m-&gt;count = <span class="hljs-number">0</span>;<span class="hljs-keyword">if</span> (unlikely(!m-&gt;count)) &#123;p = m-&gt;op-&gt;next(m, p, &amp;pos);m-&gt;index = pos;<span class="hljs-keyword">continue</span>;&#125;<span class="hljs-keyword">if</span> (m-&gt;count &lt; m-&gt;size)<span class="hljs-keyword">goto</span> Fill;m-&gt;op-&gt;stop(m, p);kvfree(m-&gt;buf);m-&gt;count = <span class="hljs-number">0</span>;m-&gt;buf = seq_buf_alloc(m-&gt;size &lt;&lt;= <span class="hljs-number">1</span>);<span class="hljs-keyword">if</span> (!m-&gt;buf)<span class="hljs-keyword">goto</span> Enomem;m-&gt;version = <span class="hljs-number">0</span>;pos = m-&gt;index;p = m-&gt;op-&gt;start(m, &amp;pos);&#125;m-&gt;op-&gt;stop(m, p);m-&gt;count = <span class="hljs-number">0</span>;<span class="hljs-keyword">goto</span> Done;Fill:<span class="hljs-comment">/* they want more? let&#x27;s try to get some more */</span><span class="hljs-comment">// 尝试获取更多的记录，直到出错、缓冲区溢出或缓冲区满</span><span class="hljs-keyword">while</span> (m-&gt;count &lt; size) &#123;<span class="hljs-type">size_t</span> offs = m-&gt;count;<span class="hljs-type">loff_t</span> next = pos;p = m-&gt;op-&gt;next(m, p, &amp;next);<span class="hljs-keyword">if</span> (!p || IS_ERR(p)) &#123;err = PTR_ERR(p);<span class="hljs-keyword">break</span>;&#125;err = m-&gt;op-&gt;show(m, p);<span class="hljs-keyword">if</span> (seq_has_overflowed(m) || err) &#123;m-&gt;count = offs;<span class="hljs-keyword">if</span> (likely(err &lt;= <span class="hljs-number">0</span>))<span class="hljs-keyword">break</span>;&#125;pos = next;&#125;m-&gt;op-&gt;stop(m, p);n = min(m-&gt;count, size);err = copy_to_user(buf, m-&gt;buf, n);<span class="hljs-keyword">if</span> (err)<span class="hljs-keyword">goto</span> Efault;copied += n;m-&gt;count -= n;<span class="hljs-keyword">if</span> (m-&gt;count)m-&gt;from = n;<span class="hljs-keyword">else</span>pos++;m-&gt;index = pos;Done:<span class="hljs-keyword">if</span> (!copied)copied = err;<span class="hljs-keyword">else</span> &#123;*ppos += copied;m-&gt;read_pos += copied;&#125;file-&gt;f_version = m-&gt;version;mutex_unlock(&amp;m-&gt;lock); <span class="hljs-comment">// 解锁`seq_file`结构</span><span class="hljs-keyword">return</span> copied;Enomem:err = -ENOMEM;<span class="hljs-keyword">goto</span> Done;Efault:err = -EFAULT;<span class="hljs-keyword">goto</span> Done;&#125;</code></pre><p>seq_read() 函数的参数：文件对应的内核数据结构 file，用户态 buf 用于存放读取到的信息，size 和ppos 分别是大小和偏移。通用的 seq_read() 函数要将进程的 vma 信息读取给用户的 buf</p><p>在开始读取时，<code>m_start</code> 会调用 <code>mmap_read_lock_killable</code> 给整个 mm 结构体加锁；在读取结束时， <code>m_stop</code> 会调用 <code>mmap_read_unlock</code> 解锁。通过 <code>m_next</code> 和 <code>show_smap</code> 每次读取一个 VMA，最终完成所有所有区域的打印。</p><pre><code class="hljs c"><span class="hljs-comment">// linux/fs/proc/task_mmu.c</span><span class="hljs-type">static</span> <span class="hljs-type">void</span> *<span class="hljs-title function_">m_start</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">loff_t</span> *ppos)</span>&#123;<span class="hljs-comment">// 获取`seq_file`结构的私有数据，并将其转换为`proc_maps_private`结构的指针</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc_maps_private</span> *<span class="hljs-title">priv</span> =</span> m-&gt;private;<span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> last_addr = *ppos;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mm_struct</span> *<span class="hljs-title">mm</span>;</span><span class="hljs-comment">/* See m_next(). Zero at the start or after lseek. */</span><span class="hljs-keyword">if</span> (last_addr == <span class="hljs-number">-1UL</span>)<span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;<span class="hljs-comment">// 调用`get_proc_task`函数来获取`inode`对应的进程的任务结构</span>priv-&gt;task = get_proc_task(priv-&gt;inode);<span class="hljs-keyword">if</span> (!priv-&gt;task)<span class="hljs-keyword">return</span> ERR_PTR(-ESRCH);mm = priv-&gt;mm;<span class="hljs-keyword">if</span> (!mm || !mmget_not_zero(mm)) &#123;put_task_struct(priv-&gt;task);priv-&gt;task = <span class="hljs-literal">NULL</span>;<span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;&#125;<span class="hljs-comment">// 尝试获取内存描述符的读锁。如果无法获取，函数释放内存描述符和任务结构并返回错误指针</span><span class="hljs-keyword">if</span> (mmap_read_lock_killable(mm)) &#123;mmput(mm);put_task_struct(priv-&gt;task);priv-&gt;task = <span class="hljs-literal">NULL</span>;<span class="hljs-keyword">return</span> ERR_PTR(-EINTR);&#125;<span class="hljs-comment">// 初始化虚拟内存区域的迭代器</span>vma_iter_init(&amp;priv-&gt;iter, mm, last_addr);hold_task_mempolicy(priv); <span class="hljs-comment">// 获取任务的内存策略</span><span class="hljs-keyword">if</span> (last_addr == <span class="hljs-number">-2UL</span>)<span class="hljs-keyword">return</span> get_gate_vma(mm);<span class="hljs-comment">// 获取虚拟内存区域</span><span class="hljs-keyword">return</span> proc_get_vma(priv, ppos);&#125;<span class="hljs-type">static</span> <span class="hljs-type">void</span> *<span class="hljs-title function_">m_next</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v, <span class="hljs-type">loff_t</span> *ppos)</span>&#123;<span class="hljs-keyword">if</span> (*ppos == <span class="hljs-number">-2UL</span>) &#123;*ppos = <span class="hljs-number">-1UL</span>;<span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;&#125;<span class="hljs-keyword">return</span> proc_get_vma(m-&gt;private, ppos);&#125;<span class="hljs-type">static</span> <span class="hljs-type">void</span> <span class="hljs-title function_">m_stop</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v)</span>&#123;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc_maps_private</span> *<span class="hljs-title">priv</span> =</span> m-&gt;private;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mm_struct</span> *<span class="hljs-title">mm</span> =</span> priv-&gt;mm;<span class="hljs-keyword">if</span> (!priv-&gt;task)<span class="hljs-keyword">return</span>;release_task_mempolicy(priv); <span class="hljs-comment">// 释放任务的内存策略</span>mmap_read_unlock(mm); <span class="hljs-comment">// 解锁内存描述符的读锁</span>mmput(mm); <span class="hljs-comment">// 减少内存描述符的引用计数，如果引用计数为零，释放内存描述符</span>put_task_struct(priv-&gt;task); <span class="hljs-comment">// 减少任务结构的引用计数，如果引用计数为零，释放任务结构</span>priv-&gt;task = <span class="hljs-literal">NULL</span>;&#125;<span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">show_smap</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v)</span>&#123;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vm_area_struct</span> *<span class="hljs-title">vma</span> =</span> v;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mem_size_stats</span> <span class="hljs-title">mss</span>;</span><span class="hljs-built_in">memset</span>(&amp;mss, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(mss));smap_gather_stats(vma, &amp;mss, <span class="hljs-number">0</span>);show_map_vma(m, vma);SEQ_PUT_DEC(<span class="hljs-string">&quot;Size:           &quot;</span>, vma-&gt;vm_end - vma-&gt;vm_start);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nKernelPageSize: &quot;</span>, vma_kernel_pagesize(vma));SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nMMUPageSize:    &quot;</span>, vma_mmu_pagesize(vma));seq_puts(m, <span class="hljs-string">&quot; kB\n&quot;</span>);__show_smap(m, &amp;mss, <span class="hljs-literal">false</span>);seq_printf(m, <span class="hljs-string">&quot;THPeligible:    %8u\n&quot;</span>,   hugepage_vma_check(vma, vma-&gt;vm_flags, <span class="hljs-literal">true</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>));<span class="hljs-keyword">if</span> (arch_pkeys_enabled())seq_printf(m, <span class="hljs-string">&quot;ProtectionKey:  %8u\n&quot;</span>, vma_pkey(vma));show_smap_vma_flags(m, vma);<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;&#125;<span class="hljs-comment">/* Show the contents common for smaps and smaps_rollup */</span><span class="hljs-type">static</span> <span class="hljs-type">void</span> __show_smap(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> mem_size_stats *mss,<span class="hljs-type">bool</span> rollup_mode)&#123;SEQ_PUT_DEC(<span class="hljs-string">&quot;Rss:            &quot;</span>, mss-&gt;resident);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPss:            &quot;</span>, mss-&gt;pss &gt;&gt; PSS_SHIFT);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPss_Dirty:      &quot;</span>, mss-&gt;pss_dirty &gt;&gt; PSS_SHIFT);<span class="hljs-keyword">if</span> (rollup_mode) &#123;<span class="hljs-comment">/*</span><span class="hljs-comment"> * These are meaningful only for smaps_rollup, otherwise two of</span><span class="hljs-comment"> * them are zero, and the other one is the same as Pss.</span><span class="hljs-comment"> */</span>SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPss_Anon:       &quot;</span>,mss-&gt;pss_anon &gt;&gt; PSS_SHIFT);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPss_File:       &quot;</span>,mss-&gt;pss_file &gt;&gt; PSS_SHIFT);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPss_Shmem:      &quot;</span>,mss-&gt;pss_shmem &gt;&gt; PSS_SHIFT);&#125;SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nShared_Clean:   &quot;</span>, mss-&gt;shared_clean);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nShared_Dirty:   &quot;</span>, mss-&gt;shared_dirty);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPrivate_Clean:  &quot;</span>, mss-&gt;private_clean);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nPrivate_Dirty:  &quot;</span>, mss-&gt;private_dirty);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nReferenced:     &quot;</span>, mss-&gt;referenced);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nAnonymous:      &quot;</span>, mss-&gt;anonymous);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nKSM:            &quot;</span>, mss-&gt;ksm);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nLazyFree:       &quot;</span>, mss-&gt;lazyfree);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nAnonHugePages:  &quot;</span>, mss-&gt;anonymous_thp);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nShmemPmdMapped: &quot;</span>, mss-&gt;shmem_thp);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nFilePmdMapped:  &quot;</span>, mss-&gt;file_thp);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nShared_Hugetlb: &quot;</span>, mss-&gt;shared_hugetlb);seq_put_decimal_ull_width(m, <span class="hljs-string">&quot; kB\nPrivate_Hugetlb: &quot;</span>,  mss-&gt;private_hugetlb &gt;&gt; <span class="hljs-number">10</span>, <span class="hljs-number">7</span>);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nSwap:           &quot;</span>, mss-&gt;swap);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nSwapPss:        &quot;</span>,mss-&gt;swap_pss &gt;&gt; PSS_SHIFT);SEQ_PUT_DEC(<span class="hljs-string">&quot; kB\nLocked:         &quot;</span>,mss-&gt;pss_locked &gt;&gt; PSS_SHIFT);seq_puts(m, <span class="hljs-string">&quot; kB\n&quot;</span>);&#125;<span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> vm_area_struct *<span class="hljs-title function_">proc_get_vma</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> proc_maps_private *priv,</span><span class="hljs-params"><span class="hljs-type">loff_t</span> *ppos)</span>&#123;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vm_area_struct</span> *<span class="hljs-title">vma</span> =</span> vma_next(&amp;priv-&gt;iter);<span class="hljs-keyword">if</span> (vma) &#123;*ppos = vma-&gt;vm_start;&#125; <span class="hljs-keyword">else</span> &#123;*ppos = <span class="hljs-number">-2UL</span>;vma = get_gate_vma(priv-&gt;mm);&#125;<span class="hljs-keyword">return</span> vma;&#125;<span class="hljs-comment">// linux/include/linux/mmap_lock.h</span><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">int</span> <span class="hljs-title function_">mmap_read_lock_killable</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> mm_struct *mm)</span>&#123;<span class="hljs-type">int</span> ret;__mmap_lock_trace_start_locking(mm, <span class="hljs-literal">false</span>);ret = down_read_killable(&amp;mm-&gt;mmap_lock);__mmap_lock_trace_acquire_returned(mm, <span class="hljs-literal">false</span>, ret == <span class="hljs-number">0</span>);<span class="hljs-keyword">return</span> ret;&#125;<span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">void</span> <span class="hljs-title function_">mmap_read_unlock</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> mm_struct *mm)</span>&#123;__mmap_lock_trace_released(mm, <span class="hljs-literal">false</span>);up_read(&amp;mm-&gt;mmap_lock);&#125;</code></pre><p>smaps 读取的重点在于:</p><ul><li>mmap_lock 锁粒度： 该锁的粒度很大，当进程发生 vma 操作都需要持有该锁，如内存分配和释放。</li><li>遍历 VMA 耗时：如果进程的内存比较大，就会长时间持有该锁，影响进程的内存管理。</li></ul><h4 id="smaps-rollup"><a href="#smaps-rollup" class="headerlink" title="smaps_rollup"></a>smaps_rollup</h4><p>有时只是想获取一下进程的 PSS 占用，是不是可以省去遍历 VMA 的部分呢？   google 的优化是增加 <a href="https://patchwork.kernel.org/project/linux-fsdevel/patch/20170810001557.147285-1-dancol@google.com/#20801969">&#x2F;proc&#x2F;pid&#x2F;smaps_rollup</a>，据 Patch 描述性能改善了 12 倍，节省几百毫秒。</p><blockquote><p>By using smaps_rollup instead of smaps, a caller can avoid the<br>significant overhead of formatting, reading, and parsing each of a<br>large process’s potentially very numerous memory mappings. For<br>sampling system_server’s PSS in Android, we measured a 12x speedup,<br>representing a savings of several hundred milliseconds.</p></blockquote><p><code>smaps_rollup</code> 的具体实现如下，可以看到持锁的粒度和时长都大大降低，当有写入请求等待锁时，还会临时释放锁。</p><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">show_smaps_rollup</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> seq_file *m, <span class="hljs-type">void</span> *v)</span>&#123;<span class="hljs-comment">// 获取`seq_file`结构的私有数据，并将其转换为`proc_maps_private`结构的指针</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc_maps_private</span> *<span class="hljs-title">priv</span> =</span> m-&gt;private;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mem_size_stats</span> <span class="hljs-title">mss</span> =</span> &#123;&#125;;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">mm_struct</span> *<span class="hljs-title">mm</span> =</span> priv-&gt;mm;<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vm_area_struct</span> *<span class="hljs-title">vma</span>;</span><span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> vma_start = <span class="hljs-number">0</span>, last_vma_end = <span class="hljs-number">0</span>;<span class="hljs-type">int</span> ret = <span class="hljs-number">0</span>;VMA_ITERATOR(vmi, mm, <span class="hljs-number">0</span>);<span class="hljs-comment">// 调用`get_proc_task`函数来获取`inode`对应的进程的任务结构</span>priv-&gt;task = get_proc_task(priv-&gt;inode);<span class="hljs-keyword">if</span> (!priv-&gt;task)<span class="hljs-keyword">return</span> -ESRCH;<span class="hljs-keyword">if</span> (!mm || !mmget_not_zero(mm)) &#123;ret = -ESRCH;<span class="hljs-keyword">goto</span> out_put_task;&#125;<span class="hljs-comment">// 尝试获取内存描述符的读锁。如果无法获取，函数返回错误码</span>ret = mmap_read_lock_killable(mm);<span class="hljs-keyword">if</span> (ret)<span class="hljs-keyword">goto</span> out_put_mm;hold_task_mempolicy(priv); <span class="hljs-comment">// 获取任务的内存策略</span>vma = vma_next(&amp;vmi); <span class="hljs-comment">// 获取下一个虚拟内存区域</span><span class="hljs-keyword">if</span> (unlikely(!vma))<span class="hljs-keyword">goto</span> empty_set;vma_start = vma-&gt;vm_start;<span class="hljs-comment">// 遍历所有的虚拟内存区域，并收集统计信息</span><span class="hljs-keyword">do</span> &#123;<span class="hljs-comment">// 调用`smap_gather_stats`函数来收集当前VMA的统计信息</span>smap_gather_stats(vma, &amp;mss, <span class="hljs-number">0</span>);last_vma_end = vma-&gt;vm_end;<span class="hljs-comment">/*</span><span class="hljs-comment"> * Release mmap_lock temporarily if someone wants to</span><span class="hljs-comment"> * access it for write request.</span><span class="hljs-comment"> */</span> <span class="hljs-comment">// 如果内存映射的锁存在争用，需要暂时释放锁以允许其他线程进行写操作</span><span class="hljs-keyword">if</span> (mmap_lock_is_contended(mm)) &#123;vma_iter_invalidate(&amp;vmi);mmap_read_unlock(mm);ret = mmap_read_lock_killable(mm);<span class="hljs-keyword">if</span> (ret) &#123;release_task_mempolicy(priv);<span class="hljs-keyword">goto</span> out_put_mm;&#125;<span class="hljs-comment">/*</span><span class="hljs-comment"> * After dropping the lock, there are four cases to</span><span class="hljs-comment"> * consider. See the following example for explanation.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *   +------+------+-----------+</span><span class="hljs-comment"> *   | VMA1 | VMA2 | VMA3      |</span><span class="hljs-comment"> *   +------+------+-----------+</span><span class="hljs-comment"> *   |      |      |           |</span><span class="hljs-comment"> *  4k     8k     16k         400k</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * Suppose we drop the lock after reading VMA2 due to</span><span class="hljs-comment"> * contention, then we get:</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *last_vma_end = 16k</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * 1) VMA2 is freed, but VMA3 exists:</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *    vma_next(vmi) will return VMA3.</span><span class="hljs-comment"> *    In this case, just continue from VMA3.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * 2) VMA2 still exists:</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *    vma_next(vmi) will return VMA3.</span><span class="hljs-comment"> *    In this case, just continue from VMA3.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * 3) No more VMAs can be found:</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *    vma_next(vmi) will return NULL.</span><span class="hljs-comment"> *    No more things to do, just break.</span><span class="hljs-comment"> *</span><span class="hljs-comment"> * 4) (last_vma_end - 1) is the middle of a vma (VMA&#x27;):</span><span class="hljs-comment"> *</span><span class="hljs-comment"> *    vma_next(vmi) will return VMA&#x27; whose range</span><span class="hljs-comment"> *    contains last_vma_end.</span><span class="hljs-comment"> *    Iterate VMA&#x27; from last_vma_end.</span><span class="hljs-comment"> */</span>vma = vma_next(&amp;vmi); <span class="hljs-comment">// 获取下一个VMA</span><span class="hljs-comment">/* Case 3 above */</span><span class="hljs-keyword">if</span> (!vma) <span class="hljs-comment">// 如果没有更多的VMA，跳出循环</span><span class="hljs-keyword">break</span>;<span class="hljs-comment">/* Case 1 and 2 above */</span><span class="hljs-keyword">if</span> (vma-&gt;vm_start &gt;= last_vma_end) <span class="hljs-comment">// 如果下一个 VMA 的开始地址大于或等于上一个 VMA 的结束地址，跳过当前迭代</span><span class="hljs-keyword">continue</span>;<span class="hljs-comment">/* Case 4 above */</span><span class="hljs-keyword">if</span> (vma-&gt;vm_end &gt; last_vma_end) <span class="hljs-comment">// 如果下一个 VMA 的结束地址大于上一个 VMA 的结束地址，从上一个 VMA 的结束地址开始收集下一个 VMA 的统计信息</span>smap_gather_stats(vma, &amp;mss, last_vma_end);&#125;&#125; for_each_vma(vmi, vma);empty_set:<span class="hljs-comment">// 显示虚拟内存区域的头部前缀</span>show_vma_header_prefix(m, vma_start, last_vma_end, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);seq_pad(m, <span class="hljs-string">&#x27; &#x27;</span>);seq_puts(m, <span class="hljs-string">&quot;[rollup]\n&quot;</span>);<span class="hljs-comment">// 显示内存映射的统计信息</span>__show_smap(m, &amp;mss, <span class="hljs-literal">true</span>);release_task_mempolicy(priv); <span class="hljs-comment">// 释放任务的内存策略</span>mmap_read_unlock(mm); <span class="hljs-comment">// 解锁内存描述符的读锁</span>out_put_mm:<span class="hljs-comment">// 减少内存描述符的引用计数，如果引用计数为零，释放内存描述符</span>mmput(mm); out_put_task:<span class="hljs-comment">// 减少任务结构的引用计数，如果引用计数为零，释放任务结构</span>put_task_struct(priv-&gt;task);priv-&gt;task = <span class="hljs-literal">NULL</span>;<span class="hljs-keyword">return</span> ret;&#125;</code></pre><h3 id="定位策略-x2F-工具"><a href="#定位策略-x2F-工具" class="headerlink" title="定位策略&#x2F;工具"></a>定位策略&#x2F;工具</h3><p>正如前面提到，整个故障定位过程耗时较长，定位方式也不具备普适性。针对延迟毛刺性问题，是否有什么普适的定位方法呢？</p><p>首先，定位非必现的问题，首要条件就是获取问题发生的现场快照，获取更多的问题细节。针对非必现的问题最好的方式，就是在可能出现问题的现场部署合适的脚本获取现场快照。</p><p>其次，最重要的是定位工具。本问题之所以定位耗时较长，是因为没有使用合适的工具缩小故障的范围。就进程的调用耗时而言，由两部分耗时组成：<strong>用户空间</strong>和<strong>内核空间</strong>。</p><p><img src="/images/redis-latency-spike/%E8%AE%B0%E4%B8%80%E6%AC%A1%20Redis%20%E5%BB%B6%E6%97%B6%E6%AF%9B%E5%88%BA%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D-20231222123441-7.png"></p><h4 id="用户空间耗时"><a href="#用户空间耗时" class="headerlink" title="用户空间耗时"></a>用户空间耗时</h4><p>由于在线的 Redis 版本缺少 P99 指标，可以使用 <a href="https://github.com/iovisor/bcc/blob/master/tools/funcslower.py">funcslower(bcc)</a> 可以定位或排除 Redis 执行毛刺，将范围缩小到网络或者单机问题。</p><pre><code class="hljs sh">$&gt; funcslower -UK -u 5000 -p 324568 <span class="hljs-string">&#x27;/var/lib/docker/overlay2/69e6c3d262a1aed8db1a8b16ddfc34c7c78999f527e028857dc2e5248ae5704a/merged/usr/local/bin/redis-server:processCommand&#x27;</span></code></pre><h4 id="内核空间耗时"><a href="#内核空间耗时" class="headerlink" title="内核空间耗时"></a>内核空间耗时</h4><p>使用系统调用性能测试工具，通过查看系统调用的长尾延迟，可以确定系统层面是否存在问题。满足要求的工具可能有：</p><p><strong>syscount(bcc)</strong></p><p>syscount  并不能直接查看 outliner，但可以通过对比不同时间区间的延迟变化发现问题。使用它在问题现场，抓取到延迟前后 <code>mmap</code> 系统调用前后变化，问题出现前耗时为 11 us，问题发生时耗时为 177 ms，如下所示：</p><pre><code class="hljs sh"><span class="hljs-comment"># ebpf 抓取故障前后 mmap 耗时</span>$&gt; syscount -L -i 30  -p <span class="hljs-variable">$PID</span>[21:39:27]SYSCALL                   COUNT        TIME (us)epoll_pwait               24952      4322184.374write                     34458       331600.262<span class="hljs-built_in">read</span>                      26400        59001.053open                         50          527.602epoll_ctl                    70           93.506getpid                       50           39.793close                        50           35.262munmap                        1           26.372getpeername                  12           15.252mmap                          1           11.003[21:40:14]SYSCALL                   COUNT        TIME (us)epoll_pwait               24371      4189948.513write                     34110       296551.821mmap                          1       177477.938<span class="hljs-built_in">read</span>                      25878        57099.880open                         48          504.271epoll_ctl                    68          104.834getpid                       49           45.939close                        49           37.919getpeername                   8           13.127accept                        2            7.896</code></pre><p><strong>perf trace</strong></p><p>另外一个更好用的工具是 perf trace，相较于 syscount 提供了 histogram 图，可以直观的发现长尾问题，使用示例如下所示（非问题现场）：</p><pre><code class="hljs sh"><span class="hljs-comment"># perf trace 示例</span>$&gt; perf trace -p <span class="hljs-variable">$PID</span> -s   syscall            calls    total       min       avg       max      stddev                               (msec)    (msec)    (msec)    (msec)        (%)   --------------- -------- --------- --------- --------- ---------     ------   epoll_pwait        53841 14561.545     0.000     0.270     4.538      0.53%   write              56177   757.799     0.005     0.013     0.047      0.09%   <span class="hljs-built_in">read</span>               55591   219.250     0.001     0.004     0.702      0.67%   open                 170     2.468     0.012     0.015     0.043      1.69%   getpid               171     1.668     0.002     0.010     1.069     63.91%   mmap                  76     0.795     0.007     0.010     0.018      2.14%   munmap                77     0.643     0.003     0.008     0.030      7.91%   epoll_ctl            151     0.533     0.001     0.004     0.014      4.26%   close                173     0.291     0.001     0.002     0.012      3.87%   getpeername           24     0.064     0.002     0.003     0.004      4.76%   accept                 8     0.045     0.003     0.006     0.011     18.34%   setsockopt            20     0.040     0.001     0.002     0.003      5.50%   fcntl                 16     0.029     0.001     0.002     0.006     15.83%   getrusage              3     0.008     0.001     0.003     0.006     48.77%   getcwd                 1     0.006     0.006     0.006     0.006      0.00%</code></pre><p>定位到 mmap 耗时异常之后，其实相关工作就可以交给内核同事处理了，毕竟术业有专攻。要想查看慢在哪里，可以通过 <code>func_graph</code> 工具定位到耗时异常的函数</p><pre><code class="hljs sh"><span class="hljs-comment"># tracer: function_graph</span><span class="hljs-comment">#</span><span class="hljs-comment"># CPU  DURATION                  FUNCTION CALLS</span><span class="hljs-comment"># |     |   |                     |   |   |   |</span> 0)               |  <span class="hljs-function"><span class="hljs-title">sys_open</span></span>() &#123; 0)               |    <span class="hljs-function"><span class="hljs-title">do_sys_open</span></span>() &#123; 0)               |      <span class="hljs-function"><span class="hljs-title">getname</span></span>() &#123; 0)               |        <span class="hljs-function"><span class="hljs-title">kmem_cache_alloc</span></span>() &#123; 0)   1.382 us    |          __might_sleep(); 0)   2.478 us    |        &#125; 0)               |        <span class="hljs-function"><span class="hljs-title">strncpy_from_user</span></span>() &#123; 0)               |          <span class="hljs-function"><span class="hljs-title">might_fault</span></span>() &#123; 0)   1.389 us    |            __might_sleep(); 0)   2.553 us    |          &#125; 0)   3.807 us    |        &#125; 0)   7.876 us    |      &#125; 0)               |      <span class="hljs-function"><span class="hljs-title">alloc_fd</span></span>() &#123; 0)   0.668 us    |        _spin_lock(); 0)   0.570 us    |        expand_files(); 0)   0.586 us    |        _spin_unlock();</code></pre><p>针对于 mmap_lock 的锁占用，要想排查持有该锁的进程列表。在内核高版本中封装了 mmap_lock 相关函数，并在其中增加了 tracepoint，可以使用 bpftrace 等工具统计持有写锁的进程、调用栈等</p><pre><code class="hljs sh">$&gt; perf list |grep mmap  mmap:vm_unmapped_area                              [Tracepoint event]  mmap_lock:mmap_lock_acquire_returned               [Tracepoint event]  mmap_lock:mmap_lock_released                       [Tracepoint event]  mmap_lock:mmap_lock_start_locking                  [Tracepoint event]  syscalls:sys_enter_mmap                            [Tracepoint event]  syscalls:sys_exit_mmap                             [Tracepoint event]$&gt; bpftrace -e <span class="hljs-string">&#x27;tracepoint:mmap_lock:mmap_lock_start_locking /args-&gt;write == true/&#123; @[comm, kstack] = count();&#125;&#x27;</span></code></pre><p>相关 perf 命令来自 <a href="https://mp.weixin.qq.com/s/S0sc2aysc6aZ5kZCcpMVTw">字节跳动SYSTech</a> 分享，遗憾的是由于发生问题的内核版本较旧，并未实操相关该定位过程。</p><p>当然，从 <a href="https://mp.weixin.qq.com/s/ZRzESgyyAL06-d8MZSjxMQ">持锁</a>这个更宽泛的观测纬度来看，可以找出有相关动作的进程，如下所示：</p><pre><code class="hljs sh">$&gt; trace <span class="hljs-string">&#x27;rwsem_down_read_slowpath(struct rw_semaphore *sem, int state) &quot;count=0x%lx owner=%s&quot;, sem-&gt;count.counter, ((struct task_struct *)((sem-&gt;owner.counter)&amp;~0x7))-&gt;comm&#x27;</span>/virtual/main.c:44:66: warning: comparison of array <span class="hljs-string">&#x27;((struct task_struct *)((sem-&gt;owner.counter) &amp; ~7))-&gt;comm&#x27;</span> not equal to a null pointer is always <span class="hljs-literal">true</span> [-Wtautological-pointer-compare]        <span class="hljs-keyword">if</span> (((struct task_struct *)((sem-&gt;owner.counter)&amp;~<span class="hljs-number">0</span>x7))-&gt;<span class="hljs-built_in">comm</span> != 0) &#123;            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~    ~1 warning generated.PID     TID     COMM            FUNC             -195453  195458  monitor         rwsem_down_read_slowpath count=0x100 owner=195453  195458  monitor         rwsem_down_read_slowpath count=0x101 owner=ip195453  195756  monitor         rwsem_down_read_slowpath count=0x101 owner=sh195453  195458  monitor         rwsem_down_read_slowpath count=0x101 owner=python195453  195458  monitor         rwsem_down_read_slowpath count=0x101 owner=python195453  195458  monitor         rwsem_down_read_slowpath count=0x101 owner=python212360  212360  runc            rwsem_down_read_slowpath count=0x100 owner=212360  212360  runc            rwsem_down_read_slowpath count=0x101 owner=runc...</code></pre><p>然而，加锁解锁耗时跟持锁耗时是两个完全不同的概念，因此并不能直接定位到持锁耗时较长的进程，所以仍需额外的工作进一步排查。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>下次遇到同步调用场景下的延迟毛刺，就可以选择合适的工具根据函数执行耗时快速定位。然而采用 streaming 模式的异步请求&#x2F;响应的延迟问题，仍然需要再深入学习探索。</p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/12-22-2023/redis-latency-spike.html">https://www.cyningsun.com/12-22-2023/redis-latency-spike.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h3&gt;&lt;p&gt;该问题发生于八月份，业务发现部分线上集群出现 10 分钟一次的耗时毛刺。整个系统的架构很简单：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imag</summary>
      
    
    
    
    <category term="Performance" scheme="https://www.cyningsun.com/category/Performance/"/>
    
    
    <category term="Kernel space" scheme="https://www.cyningsun.com/tag/Kernel-space/"/>
    
  </entry>
  
  <entry>
    <title>深入理解 DNS 解析</title>
    <link href="https://www.cyningsun.com/10-08-2023/dive-into-dns-resolution.html"/>
    <id>https://www.cyningsun.com/10-08-2023/dive-into-dns-resolution.html</id>
    <published>2023-10-07T16:00:00.000Z</published>
    <updated>2026-03-23T11:56:45.000Z</updated>
    
    <content type="html"><![CDATA[<p>作为互联网的基本设施，DNS 通过将域名转换为一组 IP 地址，在不同的连接尝试中，客户端将接收来自不同 IP 的服务器的服务，从而将整体负载分配到不同服务器之间。</p><p>在一些对响应延迟极度敏感的场景下，服务端负载不均会显著增加 P99&#x2F;P999 延迟，例如：Redis 服务接入。假如后端服务能力一致，使用 DNS 作为服务发现的情况下，怎样才能让负载均衡到不同的服务器（注意：不仅仅是<strong>负载分配</strong>，而是<strong>负载均衡</strong>）。通常意义上，我们倾向于认为 <strong>DNS 解析</strong>返回的结果是 Round-robin 的，然而实际上并非如此。</p><h3 id="DNS-查询"><a href="#DNS-查询" class="headerlink" title="DNS 查询"></a>DNS 查询</h3><h4 id="迭代查询"><a href="#迭代查询" class="headerlink" title="迭代查询"></a>迭代查询</h4><p>所有 DNS 服务器都属于以下四个类别之一：</p><ul><li>递归解析器（Local DNS）</li><li>根域名服务器(Root Nameserver)</li><li>TLD 域名服务器(TLD Nameserver)</li><li>权威性域名服务器(Authoritative Nameserver)</li></ul><p>在典型 DNS 查找中，四种 DNS 服务器协同工作来完成客户端发起的域名到 IP 地址的解析任务。</p><p><img src="/images/dive-into-dns-resolution/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20DNS%20%E8%A7%A3%E6%9E%90-20231008175440.png"></p><p><strong>客户端不会直接与 DNS 域名服务器通信，递归解析器（也称为 DNS 解析器）作为客户端与 DNS 域名服务器的中间人，是 DNS 查询中的第一站</strong>。从客户端收到 DNS 查询后，递归解析器将使用缓存的数据进行响应，或向 Root 域名服务器发送请求，接着向 TLD 域名服务器发送另一个请求，然后向权威性域名服务器发送最后一个请求。收到来自权威性域名服务器的响应后，递归解析器将向客户端发送响应。</p><h4 id="递归查询"><a href="#递归查询" class="headerlink" title="递归查询"></a>递归查询</h4><p>为了满足访问加速、私有（内部）域名、防止 DNS 劫持、智能路由等需求，实际生产环境中会有多级的递归解析器。递归解析器会缓存上游 DNS 服务的查询记录，并根据配置转发未命中缓存的 DNS 查询请求给上游 DNS 服务。</p><p>以公有云 VPC 为例，可以在主机部署 node-local-dns，在 Kubernetes 集群部署 CoreDNS，在 VPC 内使用 <code>AWS Route 53</code> 等 DNS 服务。整体效果，如下图：</p><p><img src="/images/dive-into-dns-resolution/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20DNS%20%E8%A7%A3%E6%9E%90-20231008175446.png"></p><p>当 Kubernetes 集群内的容器进行 DNS 解析时，请求首先被转发给主机的 DNS 服务，在未命中缓存时逐级转发给上游的递归解析器。最后一级递归解析器，通过迭代查询返回解析结果。</p><h3 id="递归解析器"><a href="#递归解析器" class="headerlink" title="递归解析器"></a>递归解析器</h3><p>使用 CoreDNS 搭建<a href="https://github.com/cyningsun/kubetest/tree/main/coredns">域名服务</a>，配置如下：</p><pre><code class="hljs corefile"># Corefile.:53 &#123;    log    errors    forward . 192.168.65.7    # 未命中私有域名、缓存的请求转发给主机 DNS    file /etc/coredns/db/example.com example.com # 私有域名    cache 30  # 缓存 30 秒    loop    reload    loadbalance round_robin # 充当循环DNS负载均衡器，随机响应中 A、AAAA 和 MX 记录的顺序。&#125;# /etc/coredns/db/example.com# www.example.com 两条 A 记录， 两 IP 均为 mock IP# www.cname.example.com CNAME 到 www.example.com...www    IN A     192.168.8.7 www    IN A     192.168.8.8www.cname    IN CNAME  www...</code></pre><p>CoreDNS 通过 forward 插件实现递归查询；loadbalance 插件实现轮询 DNS；cache 插件根据域名和记录进行缓存。</p><p>使用 dig 验证私有域名 <code>www.example.com</code>、<code>www.cname.example.com</code> 和 <code>serverfault.com</code>，可以看到正常解析，响应中 IP 顺序随机：</p><pre><code class="hljs sh">$&gt; dig -p 53 @127.0.0.1 +noall +answer  www.example.com      www.example.com.21INA192.168.8.8www.example.com.21INA192.168.8.7$&gt; dig -p 53 @192.168.3.2 +noall +answer  www.cname.example.comwww.cname.example.com.18INCNAMEwww.example.com.www.example.com.18INA192.168.8.7www.example.com.18INA192.168.8.8$&gt; dig -p 53 @127.0.0.1 +noall +answer  serverfault.comserverfault.com.30INA104.18.23.101serverfault.com.30INA104.18.22.101</code></pre><h4 id="轮询-DNS"><a href="#轮询-DNS" class="headerlink" title="轮询 DNS"></a>轮询 DNS</h4><p>统计 <code>serverfault.com</code> 返回记录的首位结果：</p><pre><code class="hljs sh">$&gt; <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> $(<span class="hljs-built_in">seq</span> 1 10); <span class="hljs-keyword">do</span>  dig +short serverfault.com | <span class="hljs-built_in">head</span> -n 1; <span class="hljs-keyword">done</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c      4 104.18.22.101      6 104.18.23.101</code></pre><p>即使<strong>排除缓存失效再缓存</strong>的干扰，CoreDNS 结果也并不总是 5:5，看起来与想象的 <code>round-robin</code> 不同。</p><p>深入 CoreDNS loadbalance 插件的源代码，可以看到:</p><ul><li>仅对 MX 和 A、AAAA 记录 <code>round-robin</code> shuffle </li><li>A、AAAA 记录会合并到一起  <code>round-robin</code> shuffle</li></ul><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">roundRobin</span><span class="hljs-params">(in []dns.RR)</span></span> []dns.RR &#123;cname := []dns.RR&#123;&#125;address := []dns.RR&#123;&#125;mx := []dns.RR&#123;&#125;rest := []dns.RR&#123;&#125;<span class="hljs-keyword">for</span> _, r := <span class="hljs-keyword">range</span> in &#123;<span class="hljs-keyword">switch</span> r.Header().Rrtype &#123;<span class="hljs-keyword">case</span> dns.TypeCNAME:cname = <span class="hljs-built_in">append</span>(cname, r)<span class="hljs-keyword">case</span> dns.TypeA, dns.TypeAAAA: <span class="hljs-comment">// IPv4, IPv6</span>address = <span class="hljs-built_in">append</span>(address, r)<span class="hljs-keyword">case</span> dns.TypeMX:mx = <span class="hljs-built_in">append</span>(mx, r)<span class="hljs-keyword">default</span>:rest = <span class="hljs-built_in">append</span>(rest, r)&#125;&#125;roundRobinShuffle(address)roundRobinShuffle(mx)out := <span class="hljs-built_in">append</span>(cname, rest...)out = <span class="hljs-built_in">append</span>(out, address...)out = <span class="hljs-built_in">append</span>(out, mx...)<span class="hljs-keyword">return</span> out&#125;</code></pre><p>再看 <code>roundRobinShuffle</code> 的实现，可以看到排序规则：<strong>根据随机的消息 ID 做 random_shuffle（随机排列组合），而非像击球队伍中的运动员一样：每个人都轮到一次，然后移到队伍的后面</strong>。</p><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">roundRobinShuffle</span><span class="hljs-params">(records []dns.RR)</span></span> &#123;<span class="hljs-keyword">switch</span> l := <span class="hljs-built_in">len</span>(records); l &#123;<span class="hljs-keyword">case</span> <span class="hljs-number">0</span>, <span class="hljs-number">1</span>:<span class="hljs-keyword">break</span><span class="hljs-keyword">case</span> <span class="hljs-number">2</span>:<span class="hljs-keyword">if</span> dns.Id()%<span class="hljs-number">2</span> == <span class="hljs-number">0</span> &#123;records[<span class="hljs-number">0</span>], records[<span class="hljs-number">1</span>] = records[<span class="hljs-number">1</span>], records[<span class="hljs-number">0</span>]&#125;<span class="hljs-keyword">default</span>:<span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span>; j &lt; l; j++ &#123;p := j + (<span class="hljs-type">int</span>(dns.Id()) % (l - j))<span class="hljs-keyword">if</span> j == p &#123;<span class="hljs-keyword">continue</span>&#125;records[j], records[p] = records[p], records[j]&#125;&#125;&#125;<span class="hljs-comment">// Id by default returns a 16-bit random number to be used as a message id. The</span><span class="hljs-comment">// number is drawn from a cryptographically secure random number generator.</span><span class="hljs-comment">// This being a variable the function can be reassigned to a custom function.</span><span class="hljs-comment">// For instance, to make it return a static value for testing:</span><span class="hljs-comment">//</span><span class="hljs-comment">//dns.Id = func() uint16 &#123; return 3 &#125;</span><span class="hljs-keyword">var</span> Id = id<span class="hljs-comment">// id returns a 16 bits random number to be used as a</span><span class="hljs-comment">// message id. The random provided should be good enough.</span><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">id</span><span class="hljs-params">()</span></span> <span class="hljs-type">uint16</span> &#123;<span class="hljs-keyword">var</span> output <span class="hljs-type">uint16</span>err := binary.Read(rand.Reader, binary.BigEndian, &amp;output)<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<span class="hljs-built_in">panic</span>(<span class="hljs-string">&quot;dns: reading random id failed: &quot;</span> + err.Error())&#125;<span class="hljs-keyword">return</span> output&#125;</code></pre><p>从 Wiki 的解释可以看出来，<strong>此 Round-robin 是指排列组合，更类似于 Random</strong>：</p><blockquote><p>The order in which IP addresses from the list are returned is the basis for the term <em><a href="https://en.wiktionary.org/wiki/round_robin" title="wikt:round robin">round robin</a></em>. With each DNS response, the IP address sequence in the list is <a href="https://en.wikipedia.org/wiki/Permutation" title="Permutation">permuted</a>.  – <a href="https://en.wikipedia.org/wiki/Round-robin_DNS">Round-robin DNS</a></p></blockquote><h4 id="缓存插件"><a href="#缓存插件" class="headerlink" title="缓存插件"></a>缓存插件</h4><pre><code class="hljs sh">cache [TTL] [ZONES...]</code></pre><ul><li>TTL：最大TTL（秒）。如果未指定，将使用最大 TTL，对于 NOERROR 响应为 3600，对于拒绝存在的响应为 1800。将 TTL 设置为 300 : cache 300 将缓存最多 300 秒的记录。</li><li>ZONE：它应该缓存的区域。如果为空，则使用配置块中的区域。</li></ul><p>缓存中的每个元素都根据其 TTL 进行缓存（TTL为最大值）。缓存有 256 个 Shard，默认情况下每 Shard 最多保存 39 条数据，总大小为 256*39&#x3D;9984 条数据。</p><h3 id="域名服务"><a href="#域名服务" class="headerlink" title="域名服务"></a>域名服务</h3><p>如果一个域名有多条 A 记录，当发送 DNS 请求时：</p><ol><li>DNS 服务是否会返回全部记录？</li><li>DNS 服务会以什么顺序返回记录？</li></ol><p>由于 RFC 缺少相关的规定，在传输协议的范围内，不同的名称服务器有不同的路由策略。两者共同决定了返回的记录和顺序</p><h4 id="传输协议"><a href="#传输协议" class="headerlink" title="传输协议"></a>传输协议</h4><p>大多数 DNS <a href="https://www.rfc-editor.org/rfc/rfc1034">RFC1034</a> 请求通过 UDP <a href="https://www.rfc-editor.org/rfc/rfc768">RFC 768</a> 进行。<a href="https://www.rfc-editor.org/rfc/rfc791">IPv4</a>规定主机必须能够重组 少于等于 576 字节的数据包，包含 IPv4 报头和 8 字节 UDP报头。</p><p>因此基于 UDP 的 DNS ，有效载荷限制为小于 512 字节，保证了如果 DNS 数据包在传输中被分段，可以重新组装，降低数据包被随机丢弃的可能性。超过 512 字节的响应将被截断，解析器必须通过 <a href="https://www.rfc-editor.org/rfc/rfc5966.html">TCP</a> 重新发出请求。</p><p>如果解析器支持 <a href="https://tools.ietf.org/html/rfc2671">EDNS0</a>，也可以通过 UDP 响应最多 4096 字节，且不会被截断。</p><h4 id="路由策略"><a href="#路由策略" class="headerlink" title="路由策略"></a>路由策略</h4><p>常见的一种路由策略设置是：轮询 DNS</p><blockquote><p>当查询有多条记录时，名称服务器执行循环 DNS。在一个请求和下一个请求时，发送响应的顺序会有所不同。大多数客户端将连接到第条记录，因此可以实现负载平衡。</p></blockquote><p>分别使用 <code>8.8.8.8</code> 和 CoreDNS 分别作为名称服务器。前者直接解析返回，后者配置 <code>loadbalance round_robin</code> shuffle 返回。</p><pre><code class="hljs yaml"><span class="hljs-string">loadbalance</span> [<span class="hljs-string">round_robin</span> <span class="hljs-string">|</span> <span class="hljs-string">weighted</span> <span class="hljs-string">WEIGHTFILE</span>] &#123; <span class="hljs-string">reload</span> <span class="hljs-string">DURATION</span> &#125;</code></pre><p>查看 <code>serverfault.com</code> 返回记录的顺序，可以看到响应首位的结果差异</p><pre><code class="hljs sh">$&gt; dig +short serverfault.com104.18.23.101104.18.22.101<span class="hljs-comment"># 8.8.8.8</span>$&gt; <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> $(<span class="hljs-built_in">seq</span> 1 10); <span class="hljs-keyword">do</span>  dig +short serverfault.com | <span class="hljs-built_in">head</span> -n 1; <span class="hljs-keyword">done</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c     10 104.18.23.101<span class="hljs-comment"># CoreDNS: round-robin</span> $&gt; <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> $(<span class="hljs-built_in">seq</span> 1 10); <span class="hljs-keyword">do</span>  dig +short serverfault.com | <span class="hljs-built_in">head</span> -n 1; <span class="hljs-keyword">done</span> | <span class="hljs-built_in">sort</span> | <span class="hljs-built_in">uniq</span> -c      4 104.18.22.101      6 104.18.23.101</code></pre><p>除了 CoreDNS 的 <code>round-robin</code>，<a href="https://docs.aws.amazon.com/zh_cn/Route53/latest/DeveloperGuide/routing-policy.html">AWS route 53</a> 之类的 DNS 服务提供了更多路由策略，常见：</p><ul><li>Geolocation routing policy</li><li>IP-based routing policy</li><li>Weighted routing policy</li><li>…</li></ul><p><strong>值得注意的是，由于 CoreDNS 等下游递归解析器，在启用缓存时，并不感知上游的路由策略，因此会导致上游策略失效，甚至导致缺陷。</strong> </p><blockquote><p>假设，上游域名服务随机返回部分 IP，该部分 IP 会持续缓存直至缓存失效。在缓存失效前所有请求都会集中到该部分 IP，导致较为严重的访问倾斜。</p></blockquote><h3 id="Resolver-库"><a href="#Resolver-库" class="headerlink" title="Resolver 库"></a>Resolver 库</h3><p>在 Linux 上并不存在一个 <code>syscall</code> 用于域名解析，实际上<strong>大多数</strong>程序是通过一个 C 标准库调用 <a href="http://man7.org/linux/man-pages/man3/getaddrinfo.3.html">getaddrinfo</a> 完成的。</p><blockquote><p><code>dig</code> 、<code>nslookup</code> 等，是查询 DNS 域名服务的工具，因此没有调用 <code>resolver</code> 库</p></blockquote><p><img src="/images/dive-into-dns-resolution/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20DNS%20%E8%A7%A3%E6%9E%90-20231008175446-1.png"></p><p>通过 strace 命令可以看到执行的部分细节：</p><pre><code class="hljs bash">$&gt; strace -e trace=openat -f ping -c1 serverfault.comopenat(AT_FDCWD, <span class="hljs-string">&quot;/etc/ld.so.cache&quot;</span>, O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, <span class="hljs-string">&quot;/lib/x86_64-linux-gnu/libcap.so.2&quot;</span>, O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, <span class="hljs-string">&quot;/lib/x86_64-linux-gnu/libidn2.so.0&quot;</span>, O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, <span class="hljs-string">&quot;/lib/x86_64-linux-gnu/libc.so.6&quot;</span>, O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, <span class="hljs-string">&quot;/lib/x86_64-linux-gnu/libunistring.so.2&quot;</span>, O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/nsswitch.conf&quot;</span>, O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/host.conf&quot;</span>, O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/resolv.conf&quot;</span>, O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/hosts&quot;</span>, O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/gai.conf&quot;</span>, O_RDONLY|O_CLOEXEC) = 5PING serverfault.com (104.18.22.101) 56(84) bytes of data.openat(AT_FDCWD, <span class="hljs-string">&quot;/etc/hosts&quot;</span>, O_RDONLY|O_CLOEXEC) = 564 bytes from 104.18.22.101 (104.18.22.101): icmp_seq=1 ttl=62 time=68.6 ms</code></pre><p><img src="/images/dive-into-dns-resolution/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20DNS%20%E8%A7%A3%E6%9E%90-20231008175446-2.png"></p><p>可以看到依次读取了 <code>/etc/nsswitch.conf</code>，<code>/etc/host.conf</code>，<code>/etc/resolv.conf</code> <code>/etc/gai.conf</code> 四个配置文件， DNS 解析的策略也跟他们相关。通过 POSIX 文档，可以了解四个配置文件的作用</p><h4 id="nsswitch-conf"><a href="#nsswitch-conf" class="headerlink" title="nsswitch.conf"></a>nsswitch.conf</h4><p>Name Service Switch (NSS) 配置文件，管理了各种信息来源的类别和顺序。每一行可以当做是一个数据库，冒号前面的是信息类型，冒号后面是数据来源或服务。 举例：</p><pre><code class="hljs sh">...hosts:          files dnsnetworks:       files...</code></pre><p>域名解析时，<code>gethostbyname</code> 会读取 hosts 一行，并从 files 和 dns 两个来源依次获取数据：</p><ul><li><code>/lib/libnss_files.so.X</code>：实现了 “files” 数据源，读取本地文件：<code>/etc/hosts</code></li><li><code>/lib/libnss_dns.so.X</code>：实现 “dns” 数据源，访问远端 DNS 服务。</li></ul><p>相比于固定搜索顺序的硬编码， NSS 提供了一种更灵活的方法可以动态更新搜索顺序，插件化的增减来源。</p><h4 id="host-conf"><a href="#host-conf" class="headerlink" title="host.conf"></a>host.conf</h4><p><code>host.conf</code> 包含了为解析库声明的配置信息. 每行含一个配置关键字，其后跟着合适的配置信息.。举例：</p><pre><code class="hljs sh"><span class="hljs-comment"># The &quot;order&quot; line is only used by old versions of the C library.</span>order hosts,<span class="hljs-built_in">bind</span>multi on</code></pre><ul><li>order：管理解析顺序。表示先使用 <code>/etc/hosts</code> 文件，再使用 name server 解析。bind(Berkeley Internet Name Domain)，一种开源 DNS 协议实现。（仅 glibc 2.4及更早版本生效，更新版本见 NSS</li><li>multi on：允许主机名对应多个 IP 地址，如果机器有多张网卡，就设置为 on</li></ul><h4 id="resolv-conf"><a href="#resolv-conf" class="headerlink" title="resolv.conf"></a>resolv.conf</h4><p><code>resolv.conf</code> 是解析器的核心配置，举例：</p><pre><code class="hljs sh">$&gt; <span class="hljs-built_in">cat</span> /etc/resolv.confoptions rotate     options <span class="hljs-built_in">timeout</span>:2  options attempts:3  options single-request-reopennameserver 8.8.4.4nameserver 8.8.8.8</code></pre><p>其配置项既要满足解析的基本要求：</p><ol><li>首先，在发起查询前要填补 local domain 得到 <strong>FQDN</strong> (Fully Qualified Domain Name 全限定域名): <a href="https://access.redhat.com/solutions/58028"><code>search</code></a>、<code>ndots:n</code></li><li>其次，有多个 nameserver 时，需要定义查询选择的 nameserver 策略: <code>nameserver</code>、<a href="https://access.redhat.com/solutions/1426263"><code>rotate</code></a></li></ol><blockquote><p> <strong>配置 <code>rotate</code> 时</strong><br>    - 以 <strong>Round Robin</strong> 的形式挑选 <code>nameserver</code>，而非每次都选择第一个，起到<strong>负载均衡</strong>的的作用。一次性请求的工具不生效，因为只有一次请求。</p><p><strong>不配置 <code>rotate</code> 时</strong><br>    - 首先使用第一个 nameserver<br>    - 如果请求成功，永远不会继续尝试后续的 nameserver<br>    - 如果请求失败且尚未超时，则继续使用后续 nameserver，直至成功</p></blockquote><ol start="3"><li>再次，既然是远程调用，更要控制好请求超时时间，以及出错时的重试次数: <code>timeout</code>、<code>attempts</code></li><li>最后，支持对返回的多个结果排序:  <code>sortlist</code></li></ol><p>也要兼容历史变迁的沧桑：</p><ol><li>首先，要兼容 IPv4 和 IPv6</li><li>其次，数据包过大时，可以 TCP 解析: <code>use-vc</code></li><li>最后，兼容种种历史缺陷: <a href="https://tencentcloudcontainerteam.github.io/2018/10/26/DNS-5-seconds-delay/"><code>single-request-reopen</code>、<code>single-request</code></a></li></ol><h4 id="gai-conf"><a href="#gai-conf" class="headerlink" title="gai.conf"></a>gai.conf</h4><p>调用 <code>getaddrinfo</code> 可能会返回多个结果。根据 <a href="https://www.ietf.org/rfc/rfc3484.txt">rfc3484</a> &#x2F; <a href="https://www.ietf.org/rfc/rfc6724.txt">rfc6724</a> 的要求，需要根据<strong>根据来源 IP 与结果 IP 进行最长匹配排序，以便相同子网里的 IP 在列表中排在首位，以得到成功率最高的结果</strong>。当然相关排序机制也可以通过 <code>/etc/gai.conf</code> 配置控制。</p><blockquote><p>示例：</p><p><a href="https://kanochan.net/archives/3249.html">IPv4&#x2F;IPv6双栈网络下配置IPv4链路优先</a></p></blockquote><p>换句话说，按照最新规范，<strong>DNS 解析返回的结果应当是固定顺序的，而非 round-robin</strong>，那么当 DNS server 返回 <a href="/09-07-2023/getaddrinfo-with-round-robin-dns-and-happy-eyeballs-cn.html">round-robin</a> 的结果时，就会因为解析器的排序而不生效，导致新旧版本 library 之间行为不一。</p><ul><li><a href="https://engineering.grab.com/dns-resolution-in-go-and-cgo">DNS Resolution in Go and Cgo</a></li><li><a href="https://github.com/golang/go/issues/18518"># net: replicate DNS resolution behaviour of getaddrinfo(glibc) in the go dns resolver</a></li></ul><p>最新的规范的前提都是 IPv6，然而 IPv6 到目前位置支持的并不理想，并且考虑基于兼容性的考虑：<strong>当返回结果中仅有 IPv4 时，不适用最长匹配相关的规则，也就不会调整结果的相对顺序（稳定排序）</strong>。</p><h3 id="Dial：连接创建"><a href="#Dial：连接创建" class="headerlink" title="Dial：连接创建"></a>Dial：连接创建</h3><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Dial</span><span class="hljs-params">(network, address <span class="hljs-type">string</span>)</span></span> (Conn, <span class="hljs-type">error</span>)</code></pre><p>Golang 创建连接时，使用 Dial 连接到 <code>named network</code> 的地址。</p><p>已知 <code>network</code> 类型有：</p><ul><li>TCP：”tcp”、”tcp4” (IPv4-only)、”tcp6” (IPv6-only)</li><li>UDP：”udp”、”udp4” (IPv4-only)、”udp6” (IPv6-only)</li><li>IP：”ip”、”ip4” (IPv4-only)、”ip6” (IPv6-only)</li><li>Unix domain socket：”unix”, “unixgram” and “unixpacket”.</li></ul><p>Golang 默认使用双栈（IPv4&amp;IPv6）DNS 解析，当 IPV6 不能访问时，支持 IPv6 的程序需要延迟几秒钟才能正常切换到 IPv4，为了不影响用户体验可以指定 <code>network</code> 为 <code>tcp4</code>，直接禁用 IPv6。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>综述，一次 DNS 解析，如果指定 network 为 TCP，在启用 IPv6 时：</p><ol><li>Golang Resolver 会并发发出 IPv4 和 IPv6 DNS 查询请求。查询的域名服务节点是 &#x2F;etc&#x2F;resolv.conf 指定的递归解析器，策略：详见 resolv.conf 节</li><li>递归解析器如果从缓存中发现结果，则直接使用，否则递归查询上游的域名服务，并将结果缓存。得到结果之后，再根据路由策略返回。每一级域名服务均如是</li><li>Golang net.Dial 选择 IP 列表中的第一个 IP 建立连接</li></ol><p>DNS 本身作为服务发现，通过轮询 DNS 提供了最基本的负载分配功能，而不能保证完美的负载均衡。对负载有极致需求的业务，建议自行负载均衡，策略参考：</p><blockquote><ol><li><strong>动态（定时）更新</strong> DNS 对应的 IP 列表</li><li>根据<strong>负载均衡策略</strong>从 IP 列表中选择合适的 IP</li><li><strong>根据 IP 从连接池中获取连接</strong>，发起请求</li></ol></blockquote><p><strong>备注：由于 Linux 发行版本众多，也有多种 Resolver 库、DNS 递归解析器，再叠加复杂的版本历史。因此本文中的众多细节仅供参考，实际情况建议使用 strace、tcpdump、ebpf tools 等工具确认</strong></p><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/10-08-2023/dive-into-dns-resolution.html">https://www.cyningsun.com/10-08-2023/dive-into-dns-resolution.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;作为互联网的基本设施，DNS 通过将域名转换为一组 IP 地址，在不同的连接尝试中，客户端将接收来自不同 IP 的服务器的服务，从而将整体负载分配到不同服务器之间。&lt;/p&gt;
&lt;p&gt;在一些对响应延迟极度敏感的场景下，服务端负载不均会显著增加 P99&amp;#x2F;P999 延迟，</summary>
      
    
    
    
    <category term="Network" scheme="https://www.cyningsun.com/category/Network/"/>
    
    
    <category term="DNS" scheme="https://www.cyningsun.com/tag/DNS/"/>
    
  </entry>
  
  <entry>
    <title>译｜getaddrinfo with round robin DNS and happy eyeballs</title>
    <link href="https://www.cyningsun.com/09-07-2023/getaddrinfo-with-round-robin-dns-and-happy-eyeballs-cn.html"/>
    <id>https://www.cyningsun.com/09-07-2023/getaddrinfo-with-round-robin-dns-and-happy-eyeballs-cn.html</id>
    <published>2023-09-06T16:00:00.000Z</published>
    <updated>2026-04-03T14:52:49.000Z</updated>
    
    <content type="html"><![CDATA[<p>这不是新闻。这只是一些事实，但似乎仍然有许多人不知道，所以我想帮助记录这些内容，以帮助教育世界。我首先会通过提供完整的背景信息来绕着主题转一转……</p><h2 id="轮询基础"><a href="#轮询基础" class="headerlink" title="轮询基础"></a>轮询基础</h2><p><a href="http://en.wikipedia.org/wiki/Round-robin_DNS">轮询 DNS</a>一直以来都是实现粗略且廉价的负载均衡和将访问者分散到多个主机上的方法，当他们尝试使用具有静态内容的单个主机&#x2F;服务时。通过在 DNS 区域中设置<a href="http://rscott.org/dns/a.html">一条 A  记录</a>来解析为多个 IP 地址，客户端将以半随机的方式获得不同的结果，从而在不同时间访问不同服务器：</p><pre><code class="hljs sh">server  IN  A  192.168.0.1server  IN  A  10.0.0.1server  IN  A  127.0.0.1</code></pre><p>例如，如果是一个小型开源项目，那么它是一种完美的方式来提供分布式服务，该服务以单一名称出现，但由互联网上的多个分布式独立服务器托管。它也被高端网络服务器使用，例如 <a href="http://www.google.com/">www.google.com</a> 和 <a href="http://www.yahoo.com/">www.yahoo.com</a> 。</p><h2 id="主机名解析"><a href="#主机名解析" class="headerlink" title="主机名解析"></a>主机名解析</h2><p>如果您是一名老派黑客，如果您从 Stevens 的原著中学习了套接字和 TCP&#x2F;IP 编程，如果您是在 BSD unix 环境长大，您就会知道可以使用 <a href="http://pubs.opengroup.org/onlinepubs/009695399/functions/gethostbyname.html">gethostbyname()</a>等方法来解析主机名。这是一个 POSIX 和单一 UNIX 规范，基本上一直存在。当对给定的循环主机名调用 <em>gethostbyname()</em> 时，该函数返回一个地址数组。该地址列表将以看似随机的顺序排列。如果应用程序只是按照接收到的顺序遍历列表并连接它们，则轮询概念非常有效。</p><h2 id="但-gethostbyname-不够好"><a href="#但-gethostbyname-不够好" class="headerlink" title="但 gethostbyname 不够好"></a>但 gethostbyname 不够好</h2><p>gethostbyname() 只适用 IPv4，涉及 IPv6 就崩溃了。它必须被更好的东西取代。<a href="http://pubs.opengroup.org/onlinepubs/009604499/functions/getaddrinfo.html">getaddrinfo ()</a> 加入，也是 POSIX（在 <a href="http://www.ietf.org/rfc/rfc3493.txt">RFC 3943</a>定义，并在 <a href="https://www.ietf.org/rfc/rfc5014.txt">RFC 5014</a>再次更新）。支持 IPv6 和更多功能的现代函数。这是世界所需要的闪亮之物！</p><h2 id="不是直接替代品"><a href="#不是直接替代品" class="headerlink" title="不是直接替代品"></a>不是直接替代品</h2><p>因此，（世界好的部分）将所有调用 gethostbyname() 替换为调用 getaddrinfo() ，现在一切都支持 IPv6，一切都很好？不完全如此。因为其中涉及微妙之处。比如函数返回地址的顺序。2003 年，IETF 人员发布了 <a href="http://www.ietf.org/rfc/rfc3484.txt">RFC 3484</a>，详细说明了 _Internet 协议版本 6 的默认地址选择_，并以此为指导，大多数（全部？）实现现在已改为按该顺序返回地址列表。然后它将成为按“首选”顺序排列的主机​​列表。突然间，应用程序将按照“从 IPv6 升级路径的角度来看很聪明的顺序”，同时遍历 IPv4 和 IPv6 地址，。</p><h2 id="getaddrinfo-没有轮询"><a href="#getaddrinfo-没有轮询" class="headerlink" title="getaddrinfo 没有轮询"></a>getaddrinfo 没有轮询</h2><p>因此，相比旧的轮询 DNS 的方法：多个地址（无论是 IPv4 或 IPv6 或两者）。随着如何返回地址的新想法，这种负载平衡方式不再有效。现在 getaddrinfo() 每次调用基本上都返回相同的顺序。我在 2005 年注意到这一点，并在 glibc 黑客邮件列表上发布了一个问题：<a href="http://www.cygwin.com/ml/libc-alpha/2005-11/msg00028.html">http://www.cygwin.com/ml/libc-alpha/2005-11/msg00028.html</a>正如您所看到的，我的问题被愉快地忽略了，并没有人回应过。顺序似乎主要由上述 RFC 和本地 <a href="https://linux.die.net/man/5/gai.conf">&#x2F;etc&#x2F;gai.conf</a> 文件决定，但如果您的目标是获得良好的轮询，两者都无济于事。其他人<a href="http://www.mail-archive.com/wget@sunsite.dk/msg09237.html">也</a><a href="https://lists.debian.org/debian-ctte/2007/09/msg00035.html">注意到了这个缺陷</a> 有些人激烈争辩说这是一件坏事，当然也有相反的人声称这是正确的行为，并且无论如何，像这样做轮询 DNS 一开始就是一个坏主意。对大量常见实用程序的影响很简单，<strong>当它们启用 IPv6 时，也会同时禁用循环 DNS</strong>。</p><h2 id="没有合适的方案"><a href="#没有合适的方案" class="headerlink" title="没有合适的方案"></a>没有合适的方案</h2><p>由于 getaddrinfo() 现在已经这样工作了近十年，我们可以忘掉“修复”它。。由于 gai.conf 需要本地编辑来提供不同的函数响应，因此它不是答案。但也许更糟糕的是，由于 getaddrinfo() 现在以某种优先顺序返回地址，，因此很难在顶部“粘贴”一个简单洗牌返回结果的层。洗牌需要考虑 IP 版本等因素。而且它将变得特定于应用程序，因此必须一次作用于一个程序。流行的浏览器似乎不太受到 getaddrinfo 的影响。。我的猜测是，因为他们致力于进行异步名称解析，以便名称解析不会阻塞进程，它们采取了不同的方法，因此拥有自己的代码。在 <a href="http://curl.haxx.se/">curl</a> 情况下，即使支持IPv6，它也可以使用 <a href="http://c-ares.haxx.se/">c-ares</a> 作为解析器后端构建，并且 c-ares 不提供 getaddrinfo的排序功能，因此在这些情况下，curl 将更像使用 gethostbyname 时那样与轮询 DNS 一起工作。</p><h2 id="替代方案"><a href="#替代方案" class="headerlink" title="替代方案"></a>替代方案</h2><p>我所知道的所有替代方案的缺点是它们并没有充分利用朴素 DNS。为了避免我提到的问题，您可以调整 DNS 服务器以对不同的用户做出不同的响应。这样，您既可以随机以轮询的方式响应不同的地址，也可以尝试通过 <a href="http://www.powerdns.com/content/home-powerdns.html">PowerDNS</a> 的 geobackend 功能等使其变得更加智能。当然，我们都知道 A) <a href="http://en.wikipedia.org/wiki/Geotargeting">geoip</a>粗糙且经常错误，B) 现实世界地理位置与网络拓扑并不匹配。</p><h2 id="happy-eyeballs"><a href="#happy-eyeballs" class="headerlink" title="happy eyeballs"></a>happy eyeballs</h2><p>在此期间，另一个与连接相关的问题出现了。事实上，IPv6 连接通常作为双栈计算机的第二个选项，而且事实上 IPv6 如今主要出现在双栈中。这可悲地惩罚了 IPv6 的早期采用者（是的，不幸的是，IPv6 仍然必须被视为早期），因为这些服务将比旧的纯 IPv4 服务慢。</p><p>对于克服这个问题的方法似乎有一个普遍的共识：<a href="http://tools.ietf.org/html/draft-ietf-v6ops-happy-eyeballs-07">happy eyeballs 方法</a>。简而言之，它建议同时尝试两个（或所有）选项，响应最快的获胜并被使用。这就需要同时解析 A 和 AAAA 名称，如果两者都得到响应，就连接到 IPv4 和 IPv6 地址，看看哪一个连接速度最快。</p><p>这当然不仅仅是替换一两个函数的问题。要实施这种方法，您需要做一些全新的事情。例如，仅执行 getaddrinfo() + 循环地址并尝试 connect() 根本不起作用。您基本上要么启动两个线程，并在一个线程中执行 IPv4-only 路由，并在另一个线程中执行 IPv6 路由，_或者 _您必须发出非阻塞解析器调用以在同一线程中并行执行 A 和 AAAA 解析，并且当第一个响应到达时，您会触发非阻塞 connect() …</p><p>我的观点是，无论如何，在您良好的旧套接字应用程序中引入 Happy Eyeballs 都需要进行一些相当大的改造。这样做很可能还会影响您的应用程序处理轮询  DNS 的方式，因此现在您有机会重新考虑您的选择和代码！</p><p><em>原文：</em> <a href="https://daniel.haxx.se/blog/2012/01/03/getaddrinfo-with-round-robin-dns-and-happy-eyeballs/">getaddrinfo with round robin DNS and happy eyeballs</a></p><blockquote><p>原文采用 <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> 许可，本译文以相同许可发布。</p></blockquote><p><strong>本文作者</strong> ： cyningsun<br /><strong>本文地址</strong> ： <a href="https://www.cyningsun.com/09-07-2023/getaddrinfo-with-round-robin-dns-and-happy-eyeballs-cn.html">https://www.cyningsun.com/09-07-2023/getaddrinfo-with-round-robin-dns-and-happy-eyeballs-cn.html</a> <br /><strong>版权声明</strong> ：本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/cn/">CC BY-NC-ND 3.0 CN</a> 许可协议。转载请注明出处！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这不是新闻。这只是一些事实，但似乎仍然有许多人不知道，所以我想帮助记录这些内容，以帮助教育世界。我首先会通过提供完整的背景信息来绕着主题转一转……&lt;/p&gt;
&lt;h2 id=&quot;轮询基础&quot;&gt;&lt;a href=&quot;#轮询基础&quot; class=&quot;headerlink&quot; title=&quot;轮询基</summary>
      
    
    
    
    
    <category term="dns" scheme="https://www.cyningsun.com/tag/dns/"/>
    
  </entry>
  
</feed>
