4. 依赖缓存:npm/pip/maven等包管理器的缓存机制与配置

依赖缓存机制与 CI 配置总览 npm 缓存 缓存路径 ~/.npm 缓存策略 lockfile + tarball CI 配置 cache: npm --prefer-offline 参数 pip 缓存 缓存路径 ~/.cache/pip 缓存策略 wheel + http cache CI 配置 cache: pip pip cache 命令管理 Maven 缓存 缓存路径 ~/.m2/repository 缓存策略 pom + artifact CI 配置 cache: maven settings.xml 配置 通用最佳实践 🔑 缓存键设计 ⏳ 失效策略 🧹 定期清理 多语言项目统一缓存策略 · 避免缓存膨胀

4.1 为什么依赖缓存是CI的命门

你想想看,一个项目从零开始构建,最耗时的环节是什么?

我个人经验,编译和测试反而好预测,真正让人抓狂的是依赖下载。尤其遇到网络抖动,npm install 跑个三分钟,pip 卡在某个源上超时,Maven 把中央仓库拉个底朝天——整个流水线的时间瞬间翻倍。

我在项目中遇到过好几次:业务团队急着发布,CI 却因为依赖下载失败而重试,一拖就是半小时。后来我们算了笔账,依赖下载占 CI 总耗时的 40%~70%。你品,你细品。

说白了,依赖缓存就是给 CI 装了个"本地仓库"。第一次下载后把包存起来,下次直接取,网络不通也不怕。但这东西配置起来有讲究,不是简单设个路径就能跑起来的。

4.2 npm:从 .npm 缓存到 CI 配置

npm 的缓存机制,核心在于 ~/.npm 目录。它把下载过的 tarball 存成 .cache 文件,安装时先看本地有没有。

但这有个前提——你得有 package-lock.json。没这个锁文件,npm 会重新解析版本范围,缓存命中的概率大打折扣。

npm CI 配置要点:

  • 缓存路径:~/.npm(Linux/Mac)或 %AppData%/npm-cache(Windows)
  • 关键参数:--prefer-offline 优先用缓存,实在没有才去网络拉
  • 缓存键:建议用 package-lock.json 的哈希值

我习惯在 GitHub Actions 里这样配:

# .github/workflows/ci.yml
- name: Cache npm
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

- name: Install dependencies
  run: npm ci --prefer-offline --no-audit --no-fund

npm ci 比 npm install 快不少,它严格走 lockfile,不会去解析版本范围。配上 --prefer-offline,只要缓存里有就直接拿,基本不碰网络。

小技巧: 如果项目里同时有 package-lock.json 和 yarn.lock,建议统一用一种包管理器,不然缓存键会打架。我踩过这个坑……嗯,后来老老实实只留一个。

4.3 pip:wheel 缓存与 CI 实践

pip 的缓存藏在 ~/.cache/pip 里,存的是 wheel 包和 HTTP 响应缓存。wheel 是 Python 的二进制分发格式,pip 会优先尝试从缓存里拿 wheel,省去编译步骤。

为什么会快?因为 wheel 是编译好的,不用从源码 build。我第一次意识到这个差距,是在一个依赖了 numpy、pandas、scikit-learn 的项目里——不开缓存,pip install 跑了 8 分钟;开了缓存,20 秒结束。

# .gitlab-ci.yml
variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
  paths:
    - .cache/pip

before_script:
  - pip install --upgrade pip
  - pip install --cache-dir $PIP_CACHE_DIR -r requirements.txt

注意: 别把 --cache-dir 指向临时目录,否则每次 CI 运行都白缓存了。我见过有人把路径写成了 /tmp/pip-cache……结果缓存永远用不上。

pip 也支持 lock 文件,推荐用 pip freeze > requirements.lock 锁定版本。虽然没有 npm 那么严格的 resolver,但至少版本不会乱跳。

4.4 Maven:本地仓库与 CI 集成

Maven 的缓存就是 ~/.m2/repository,按 groupId:artifactId:version 的坐标结构组织。第一次拉取后,jar 包和 pom 文件就留在本地了。

Maven 的缓存策略比较"朴实"——它相信本地仓库里的东西是可靠的,只要坐标对上,就直接用,不会去远程校验 checksum。除非你显式执行 mvn dependency:purge-local-repository。

<!-- settings.xml 片段 -->
<settings>
  <localRepository>${CI_PROJECT_DIR}/.m2/repository</localRepository>
  <profiles>
    <profile>
      <id>ci</id>
      <repositories>
        <repository>
          <id>central</id>
          <url>https://repo.maven.apache.org/maven2</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>false</enabled></snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>
</settings>

我建议在 CI 里把 localRepository 指向项目目录下的 .m2 文件夹,方便缓存系统统一管理。同时关掉 snapshot 仓库,避免每次构建都去拉快照版本。

加速技巧: 用 mvn dependency:go-offline 提前把所有依赖拉下来,后面跑 mvn install -o 就可以完全离线了。对于网络不稳定的 CI 环境,这招很管用。

4.5 缓存策略对比与最佳实践

三种包管理器的缓存机制各有侧重,我整理了一张对比表,方便你对照配置:

对比维度 npm pip Maven
缓存路径 ~/.npm ~/.cache/pip ~/.m2/repository
缓存内容 .cache tarball wheel + HTTP 缓存 jar + pom
锁文件 package-lock.json requirements.lock pom.xml + maven-dependency-plugin
CI 缓存键建议 lockfile 哈希 requirements 哈希 + Python 版本 pom.xml + settings.xml 哈希
离线模式 --prefer-offline --no-index (配合 wheelhouse) -o (offline 模式)

缓存键设计是核心中的核心。我用三个原则来把控:

  • 精确匹配:锁文件变了,缓存必须失效。用哈希值做 primary key。
  • 兜底恢复:如果 primary key 没命中,用 restore-keys 回退到最近一次缓存,至少能省一部分。
  • 区分运行环境:操作系统、Python/Node/Java 版本不同,缓存不能混用。把 runner.os 和语言版本拼进 key。

4.6 避坑指南

这些年我在依赖缓存上交过的学费,分享给你:

坑一:缓存过期策略没配好

我曾经在 GitLab CI 里只配了 key: "$CI_JOB_NAME",结果所有分支共用一个缓存。开发分支改了依赖,master 的构建居然用了开发分支的缓存包……版本对不上,编译直接挂。后来我强制把锁文件哈希加进了 key。

坑二:缓存膨胀失控

npm 的 ~/.npm 会随着时间越积越大。有个项目跑了大半年,缓存飙到 2GB,每次 CI 恢复缓存比直接下载还慢。现在我定期用 npm cache clean --force 做清理,或者干脆在 CI 里设置缓存保留天数。

坑三:pip 的 wheel 缓存命中率低

如果你每次都用 pip install --no-cache-dir,那 pip 根本不会写缓存。有人为了省磁盘空间这么干,结果每次 CI 都从源码编译 numpy…… 我建议用 --cache-dir 显式指定,并且确保路径在缓存范围内。

我的三个"铁律":

  1. 锁文件必须进版本管理,而且作为缓存键的唯一输入。
  2. 缓存路径不要用默认的 home 目录,统一归到项目目录下,方便 CI 系统识别。
  3. 每季度或每半年清一次缓存,避免"缓存垃圾"拖慢恢复速度。

依赖缓存这件事,配好了是加速器,配不好就是隐形炸弹。我建议你配完之后,手动触发一次"缓存 miss"和一次"缓存 hit",对比一下时间差异。看到构建时间从 8 分钟降到 1 分钟,那种感觉……嗯,挺爽的。