Cargo 缓存清理

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

默认情况下,如果本地可以重新创建的数据在一个月内未使用,则会删除这些数据;如果必须重新下载的数据在 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/.global-cache 中的 cargo 主目录中观察到存储的 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 上。我们可能还需要考虑更精细的时间戳跟踪或某些机制来定期清除此数据。

目标目录更改跟踪和清理

我们正在考虑引入 SQLite 的另一个地方是管理目标目录。在 cargo 的目标目录中,cargo 会使用一个称为 *指纹* 的信息来跟踪每个已构建 crate 的信息。这些指纹帮助 cargo 知道它是否需要重新编译某些内容。每个工件都使用一组 4 个文件进行跟踪,混合使用了自定义格式。

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