货物缓存清理

2023 年 12 月 11 日 · Eric Huss 代表 Cargo 团队

Cargo 最近在 nightly 通道(从 nightly-2023-11-17 开始)获得了一个不稳定的功能,用于自动清理 Cargo 主目录内的缓存内容。这篇文章包括

简而言之,我们要求使用 nightly 通道的用户启用此功能,并在 Cargo 问题跟踪器 上报告您遇到的任何问题。要启用它,请将以下内容放在您的 Cargo 配置文件中(通常位于 ~/.cargo/config.toml 或 Windows 上的 %USERPROFILE%\.cargo\config.toml 中)

[unstable]
gc = true

或者设置 CARGO_UNSTABLE_GC=true 环境变量,或使用 -Zgc CLI 标志为单个命令打开它。

我们特别希望使用非标准文件系统或环境的用户尝试一下,因为实现中有一些敏感部分,需要在为所有人启用之前进行实战测试。

什么是这个功能?

Cargo 在 Cargo 主目录中保留各种缓存数据。此缓存可以无限增长,并且可以变得非常大(很容易达到数 GB)。社区成员开发了管理此缓存的工具,例如 cargo-cache,但 cargo 本身从未公开过任何管理它的能力。

此缓存包括

  • 注册索引数据,例如来自 crates.io 的包依赖项元数据。
  • 从注册表下载的压缩 .crate 文件。
  • 这些 .crate 文件的解压缩内容,rustc 使用它们来读取源代码并编译依赖项。
  • git 依赖项使用的 git 存储库的克隆。

新的垃圾回收(“GC”)功能添加了对该缓存数据的跟踪,以便 cargo 可以自动或手动删除未使用的文件。它保留了一个 SQLite 数据库,用于跟踪各种缓存元素的最后使用时间。每次运行读取或写入任何这些缓存数据的 cargo 命令时,它都会使用该数据最后使用的时间戳更新数据库。

尚未包含的是目标目录的清理,请参阅 未来计划

自动清理

运行 cargo 时,它每天都会检查最后使用缓存跟踪器,并确定是否有任何缓存元素在一段时间内未使用。如果没有,它们将被自动删除。这发生在大多数通常会执行大量工作的命令中,例如 cargo buildcargo fetch

默认情况下,如果数据未被使用 1 个月,则删除可以本地重新创建的数据,如果数据未被使用 3 个月,则删除必须重新下载的数据。

如果 cargo 处于脱机状态,例如使用 --offline--frozen,则自动删除将被禁用,以避免删除可能需要在长时间脱机时使用的工件。

初始实现公开了一系列配置旋钮来控制自动清理的工作方式。但是,当它稳定后,我们不太可能公开太多底层细节,因此这在将来可能会改变(请参阅问题 #13061)。有关此配置的更多详细信息,请参阅 自动垃圾回收 部分。

手动清理

如果您想手动从缓存中删除数据,则在 cargo clean gc 子命令下添加了几个选项。此子命令可用于执行正常的每日自动清理,或指定要删除哪些数据的不同选项。有几个选项可用于指定要删除的数据的年龄(例如 --max-download-age=3days)或指定缓存的最大大小(例如 --max-download-size=1GiB)。有关支持哪些选项的更多详细信息,请参阅 手动垃圾回收 部分或运行 cargo clean gc --help

此 CLI 设计仅是初步的,我们正在研究确定它稳定后的最终设计,请参阅问题 #13060

需要注意什么

启用 gc 功能后,只需继续正常使用 cargo。您应该能够观察到存储在您的 cargo 主目录 ~/.cargo/.global-cache 中的 SQLite 数据库。

第一次使用 cargo 时,它将填充跟踪您 cargo 主目录中已存在的所有数据的数据库。然后,在 1 个月后,cargo 应该开始删除旧数据,并在 3 个月后删除更多数据。

最终结果是在这段时间后,您应该开始注意到主目录使用的空间总体减少了。

您也可以尝试 cargo clean gc 命令并探索一些选项,如果您想尝试手动删除一些数据。

如果您遇到问题,您可以禁用 gc 功能,cargo 应该恢复到之前的行为。如果发生这种情况,请在 问题跟踪器 上告知我们。

反馈请求

我们想听听您使用此功能的体验。我们感兴趣的一些事情是

  • 您是否遇到过任何错误、错误、问题或令人困惑的问题?请在 https://github.com/rust-lang/cargo/issues/ 上提交问题。
  • 第一次启用 GC 使用 cargo 时,是否出现了过长的延迟?Cargo 可能需要扫描您现有的缓存数据一次,以检测以前版本中已存在的内容。
  • 您是否注意到它每天执行自动清理时出现不合理的延迟?
  • 您是否有根据缓存大小进行清理的用例?如果有,请在 #13062 上分享。
  • 如果您认为您会使用手动删除缓存数据,那么您这样做的用例是什么?在 #13060 上分享有关 CLI 接口的信息,可能会帮助我们指导整体设计。
  • 删除 3 个月前的数据的默认值是否适合您的用例?

或者,如果您想在 Zulip 上分享您的体验,请前往 #t-cargo 流。

设计考虑和实现细节

(这些部分仅供您中那些非常好奇的人。)

此功能的实现必须考虑几个约束条件,以确保它在几乎所有环境中都能正常工作,并且不会给用户带来负面体验。

性能

一个重点是确保每次调用 cargo 的性能不会受到显著影响。Cargo 需要在每次运行时可能保存大量数据。性能影响在很大程度上取决于依赖项的数量和您的文件系统。初步测试表明,影响可能在 0 到大约 50 毫秒之间。

为了最大程度地减少实际删除文件的性能影响,自动 GC 每天只运行一次。这旨在平衡保持缓存清洁,而不影响日常使用的性能。

锁定

另一个重点是处理缓存锁定。以前,cargo 在包缓存上有一个单一锁,cargo 在下载注册表数据和执行依赖项解析时会持有该锁。当 cargo 实际运行 rustc 时,它以前没有持有锁,假设现有的缓存数据不会被修改。

但是,现在 cargo 可以修改或删除现有的缓存数据,因此它需要谨慎地与可能正在从缓存中读取的任何内容进行协调,例如,如果同时运行多个 cargo 命令。为了处理这个问题,cargo 现在有两个独立的锁,它们一起使用以提供三种独立的锁定状态。有一个共享读锁,它允许多个构建并行运行并从缓存中读取。有一个写锁在下载注册表数据时持有,它独立于读锁,允许并发构建在下载新包时仍然运行。第三种状态是一个写锁,它阻止前两个锁中的任何一个被持有,并确保在清理缓存时独占访问。

1.75 之前的 Cargo 版本不知道独占写锁。我们希望在实践中,同时运行旧版和新版 Cargo 的情况很少见,并且自动 GC 也不太可能需要删除旧版本正在使用的并发数据。

错误处理和文件系统

由于我们不希望 GC 问题影响用户,因此如果无法获取包缓存的独占锁,实现将静默跳过 GC。类似地,当 Cargo 在每个命令上保存时间戳数据时,如果无法打开数据库(例如,如果它位于只读文件系统上,或者无法获取写锁),它将静默忽略错误。这可能会导致最后使用时间戳变得陈旧,但希望这不会影响大多数使用场景。对于锁定,我们特别关注 Docker 容器挂载和具有可疑锁定支持的网络文件系统等场景。

向后兼容性

由于缓存被任何版本的 Cargo 使用,因此我们必须密切关注向前和向后兼容性。我们得益于 SQLite 特别稳定的磁盘数据格式,该格式自 2004 年以来一直保持稳定。Cargo 支持在数据库中进行保持向后兼容的模式迁移。

未来计划

这项工作的重点是积累在各种环境中使用 SQLite 的经验,并计划将其应用于 Cargo 的其他几个部分。

注册表索引元数据

我们希望将 SQLite 引入的一个地方是注册表索引缓存。当 Cargo 下载注册表索引数据时,它会将其存储在自定义设计的二进制文件格式中,以提高查找性能。但是,此索引缓存使用许多小文件,这在某些文件系统上可能无法很好地执行。

此外,索引缓存无限增长。目前,自动缓存清理只会删除整个索引缓存,如果索引本身没有被使用,这种情况很少发生在 crates.io 上。我们可能还需要考虑更细粒度的 timestamps 跟踪或一些定期清除此数据的机制。

目标目录更改跟踪和清理

我们希望将 SQLite 引入的另一个地方是管理目标目录。在 Cargo 的目标目录中,Cargo 使用称为“指纹”的信息跟踪每个已构建的板条箱。这些指纹帮助 Cargo 知道它是否需要重新编译某些内容。每个工件都使用一组 4 个文件进行跟踪,使用自定义格式的混合。

我们希望用 SQLite 替换此系统,这将带来一些改进。一个主要重点是提供对目标目录中陈旧数据的清理,该目录往往会占用大量的磁盘空间。此外,我们希望实现其他改进,例如更准确的指纹跟踪,提供有关 Cargo 认为需要重新编译的原因的信息,并希望提高性能。这对于 脚本功能 非常重要,该功能使用全局缓存来构建工件,以及未来实现全局共享的构建缓存。