用 uv 替代 pip + virtualenv:一次 Python 依赖安装链路的提速实践
为什么要换
我们一个 Django 服务有 46 个直接依赖、间接依赖 180 多个。原来 CI 里固定跑四步:创建 venv、升级 pip、装 requirements、再装一遍本地包。在 2 核 4G 的构建机上,这段稳定在 92 秒左右,占了整条流水线三分之一的时间。换成 uv 之后同样的依赖树降到 4 秒上下(热缓存 1.2 秒),镜像体积也小了将近 80MB。
需要说明的是,uv 不是"pip 加速器"这么简单:它同时承担了 venv、pip、pip-tools、virtualenv 四件事,并且内置了跨平台锁文件。所以这次迁移的真正收益不在省下的那几十秒,而在依赖解析变得可复现。
三行改造
先把工具装上,然后把 pipeline 里三条命令替换掉即可,不需要保留 virtualenv:
# 安装(CI 里可 curl 到本地后执行)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 建环境:--python 会自动下载对应版本解释器
uv venv --python 3.11
# 装依赖(-r 语义与 pip 一致)
uv pip install -r requirements.txt
第 2 行是关键:构建机不需要预装 Python,uv 会把解释器拉到 ~/.local/share/uv/python 下自管。我们原来靠一个自建基础镜像预装 3.10 和 3.11,现在镜像里只剩系统工具,体积和升级成本都下来了。
锁文件才是重点
uv pip install 只是提速,真正解决问题的是锁文件工作流:
uv init --app # 生成 pyproject.toml
uv add django gunicorn # 写依赖并立即解析
uv lock # 生成跨平台 uv.lock
uv sync --frozen # CI 严格按锁安装,不允许漂移
--frozen 会拒绝任何不在锁文件里的变更,效果接近 pip install --require-hashes,但不用自己维护 hash 列表。我们把它加进 CI 的第一周就抓到两个问题:一个同事本地手动 pip install 了没登记的包,另一个是某个依赖在 Linux 和 macOS 上解析出了不同的小版本。
踩到的四个坑
- 私有源配置不通用。uv 不读
pip.conf,要写[[tool.uv.index]],或者用环境变量UV_INDEX_URL。我们的 Nexus 带账号密码,改成https://user:pass@nexus.internal/simple放进 CI 密钥里。 - 源码编译不是默认行为。
psycopg2这类包默认也拿 wheel,如果本地编译选项和生产不一致,必须显式写--no-binary psycopg2。 - 可编辑安装要开开关。
uv pip install -e .能用,但uv sync需要在[tool.uv]里加package = true,否则只装第三方依赖、不装项目本身,容器里直接 import 失败。 - 缓存目录得自己挂。uv 的缓存默认在
~/.cache/uv,容器每次都全新的话第一次仍然要拉完 180 个包,实测 26 秒。把这一层挂成 CI 缓存,第二次才会掉到秒级。
实测结果
| 步骤 | 改造前 | 改造后 |
| 创建虚拟环境 | 3s | 0.1s |
| 装依赖(冷缓存) | 89s | 26s |
| 装依赖(热缓存) | 64s | 1.2s |
| 运行镜像体积 | 1.02GB | 0.94GB |
流水线总时长从 6 分 20 秒降到 4 分 05 秒。改动只集中在一行 CI 配置和一个新增的 pyproject.toml,回滚成本几乎为零——把命令换回 pip 就能跑。
一点建议
如果你现在用的是 pip-tools + virtualenv 的组合,uv 是性价比最高的替换点:它兼容 requirements.txt,可以先只换安装命令、不引入锁文件,跑顺了再迁 uv.lock。反过来,如果项目依赖里有大量需要打补丁的私有包,建议先在一个边缘服务上试点,确认私有源和源码编译两条路都通了再铺开。
- 点赞
- 收藏
- 关注作者
评论(0)