Cargo 最近在 nightly 通道上新增了一个不稳定的特性(从 nightly-2023-11-17 开始),用于自动清理 Cargo 主目录中的缓存内容。本文包括
简而言之,我们呼吁使用 nightly 通道的人们启用此特性,并将遇到的任何问题报告到Cargo 问题追踪器。要启用它,请将以下内容放入您的 Cargo 配置文件中(通常位于 ~/.cargo/config.toml
或 Windows 上的 %USERPROFILE%\.cargo\config.toml
)
[]
= true
或者设置 CARGO_UNSTABLE_GC=true
环境变量,或使用 -Zgc
命令行标志为单个命令开启它。
我们特别希望使用非标准文件系统或环境的用户进行尝试,因为实现中有一些敏感的部分,在全面开启之前需要进行实战测试。
这个特性是什么?
Cargo 在其主目录中保留了各种缓存数据。这个缓存可以无限增长,并且会变得非常大(轻松达到许多吉字节)。社区成员开发了一些工具来管理这个缓存,例如 cargo-cache
,但 Cargo 本身从未提供任何管理能力。
这个缓存包括
- 注册表索引数据,例如来自 crates.io 的包依赖元数据。
- 从注册表下载的压缩
.crate
文件。 - 这些
.crate
文件的解压缩内容,rustc
使用它来读取源代码并编译依赖项。 - Git 依赖项使用的 Git 仓库克隆。
新的垃圾回收(“GC”)特性增加了对这些缓存数据的跟踪,以便 Cargo 可以自动或手动删除未使用的文件。它维护一个 SQLite 数据库,用于跟踪各种缓存元素的最后使用时间。每当您运行读取或写入任何此类缓存数据的 Cargo 命令时,它将更新数据库,记录该数据最后被使用的时间戳。
目前尚未包含对 target 目录的清理,请参见未来计划。
自动清理
运行 Cargo 时,它每天会检查一次上次使用缓存追踪器,并确定是否有任何缓存元素已有一段时间未使用。如果未使用,则会自动删除。这发生在大多数通常会执行大量工作的命令中,例如 cargo build
或 cargo fetch
。
默认设置是:如果本地可重新创建的数据在 1 个月内未使用,则删除;如果必须重新下载的数据在 3 个月后仍未使用,则删除。
如果 Cargo 处于离线状态(例如使用 --offline
或 --frozen
),则自动删除会禁用,以避免删除您长时间离线时可能需要使用的文件。
初始实现暴露了多种配置选项来控制自动清理的工作方式。然而,当它稳定下来时,我们不太可能暴露过多的底层细节,因此未来可能会有所变化(参见 issue #13061)。有关此配置的更多详细信息,请参见自动垃圾回收部分。
手动清理
如果您想手动删除缓存中的数据,可以在 cargo clean gc
子命令下找到几个选项。这个子命令可以用于执行正常的自动每日清理,或指定要删除的数据的不同选项。有一些选项可以指定要删除数据的年龄(例如 --max-download-age=3days
)或指定缓存的最大大小(例如 --max-download-size=1GiB
)。有关支持哪些选项的更多详细信息,请参见手动垃圾回收部分或运行 cargo clean gc --help
。
这个命令行设计只是初步的,我们正在研究它稳定后的最终设计会是什么样子,参见 issue #13060。
需要注意的事项
启用 GC 特性后,照常使用 Cargo 即可。您应该能在 Cargo 主目录的 ~/.cargo/.global-cache
中看到存储的 SQLite 数据库。
您第一次使用 Cargo 后,它会填充数据库,追踪您 Cargo 主目录中已存在的所有数据。然后,1 个月后,Cargo 应该开始删除旧数据,3 个月后会删除更多数据。
最终结果是,一段时间后,您应该会注意到主目录占用的空间整体减少了。
您也可以尝试使用 cargo clean gc
命令并探索其一些选项,如果您想手动删除一些数据。
如果您遇到问题,可以禁用 GC 特性,Cargo 应该会恢复到之前的行为。如果发生这种情况,请在问题追踪器上告知我们。
征求反馈意见
我们希望了解您使用此特性的经验。我们感兴趣的一些事项包括
- 您是否遇到了任何 bug、错误、问题或令人困惑的情况?请在 https://github.com/rust-lang/cargo/issues/ 提交一个 issue。
- 您第一次启用 GC 后使用 Cargo 时,是否有异常长的延迟?Cargo 可能需要扫描您现有的缓存数据一次,以检测之前版本中已存在的内容。
- 每天执行自动清理时,您是否注意到异常长的延迟?
- 您是否有需要根据缓存大小进行清理的使用场景?如果是,请在#13062上分享。
- 如果您认为会使用手动删除缓存数据的功能,您的使用场景是什么?在#13060上分享关于命令行界面的讨论可能有助于指导我们进行整体设计。
- 将 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 来说,这种情况很少发生。我们也可能需要考虑更细粒度的时间戳跟踪或某种机制来定期清除这些数据。
Target 目录变更追踪和清理
我们考虑引入 SQLite 的另一个地方是管理 target 目录。在 Cargo 的 target 目录中,Cargo 使用所谓的 指纹 (fingerprint) 来跟踪每个已构建 crate 的信息。这些指纹帮助 Cargo 判断是否需要重新编译某个东西。每个构建产物都通过一组 4 个文件进行跟踪,使用了混合的自定义格式。
我们正在考虑用 SQLite 替换这个系统,这有望带来几项改进。一个主要的重点是清理 target 目录中的陈旧数据,这些数据往往占用大量磁盘空间。此外,我们正在寻求实现其他改进,例如更精确的指纹跟踪、提供关于 Cargo 认为某个东西需要重新编译的原因的信息,并希望提高性能。这对于 script 特性将很重要,该特性使用全局缓存存储构建产物,以及未来实现全局共享构建缓存。