症状: Git LFS 下载时间上升,整条 iOS CI 变慢。
最快解法: 先拆分 Git、LFS 对象、工作区恢复和 Xcode 编译耗时;只有编译阶段仍持续饱和时,才增加 Mac 构建节点。
这套判断适用于大型 iOS 仓库、共享 Mac 打包机和远程 Mac CI 集群。不要因为 checkout 阶段变长,就直接采购更多 Mac。你需要先证明瓶颈位于数据路径,还是位于 CPU、内存和构建并发。
适用团队与判断边界
这篇文章适合维护大量 Git LFS 资源、且 iOS CI 检出阶段明显变慢的研发效能负责人。
如果你负责 Mac 构建机扩容、迁移或弹性租赁,也可以用文中的指标决定固定节点与远程 Mac 节点的比例。
安全与平台团队则应重点关注凭证继承、缓存污染、持久化工作区和构建结果可重复性。
Git LFS 的核心机制是:仓库中保存指针文件,真实大文件存放在 LFS 服务端。普通 Git fetch 成功,只能说明 Git 对象和引用已经更新,并不代表 LFS 真实文件已经进入工作区。Git LFS 官方机制说明
阶段耗时拆分
五类时间不要合并
一次 iOS CI checkout 至少要拆成以下指标:
- Git 数据传输:提交、树对象、普通文件内容和引用更新。
- LFS 对象传输:根据指针文件下载真实资源。
- 工作区恢复:把本地对象填充为工作区文件,或替换仍存在的指针文件。
- 依赖恢复:Swift Package、CocoaPods、构建工具和其它依赖缓存。
- Xcode 构建:编译、链接、资源处理、签名和归档。
其中,LFS fetch 不会更新工作区;git lfs checkout 也不会下载缺失对象,只会使用本地已有对象填充工作区。Git LFS fetch 手册
建议在同一提交、同一 Mac 节点和同一流水线参数下,分别记录冷启动与重复运行。至少保留以下日志:
date
git lfs env
git lfs fetch --dry-run --json origin "$GIT_REF"
git lfs ls-files
git status --short
--dry-run 可以帮助你观察将要传输的 LFS 对象;git lfs ls-files 用于确认仓库中哪些路径由 LFS 管理。不要只看 CI 平台显示的 checkout 总时长。
判断规则: 如果网络等待、LFS 下载和工作区恢复占主要时间,先改数据路径;如果这些阶段稳定后,Xcode 编译持续占满 CPU 或内存,再评估 Mac 扩容。
冷启动与热缓存
冷启动要回答:没有本地 LFS 对象时,流水线需要下载多少内容?
热缓存要回答:对象已经存在时,重复任务能否跳过传输,并且工作区仍然完整?
两者必须分开记录。缓存目录有文件,不等于本次任务真正命中了缓存。你至少要记录下载字节量、重复对象比例、缓存命中后的 LFS 阶段耗时,以及磁盘增长趋势。
场景案例:某团队发现重复 PR 验证仍然很慢,于是增加了 Mac 节点。进一步拆分后发现,新节点每次都使用干净工作区,LFS 对象从未复用;而原有节点虽然有缓存,却遗留了上一条任务的 sparse-checkout 状态。最终问题不是 Mac 编译能力,而是缓存和工作区生命周期没有分离。
拉取范围与工作区状态
指针、对象和文件
Git LFS 指针是仓库中的小型文本文件,真实资源由 LFS 对象提供。启用 --skip-smudge 后,克隆或切换提交不会自动下载对象,需要显式执行 git lfs pull。Git LFS install 手册
因此,以下三个状态不能混为一谈:
- Git 引用已经更新。
- LFS 对象已经存在于本地对象存储。
- 工作区已经被真实文件填充。
在验收时,应对关键资源执行校验:
git lfs fsck
git lfs checkout
test -f path/to/required-resource
如果构建脚本能在指针文件存在时继续运行,问题可能直到资源打包或运行测试阶段才暴露。对发布任务,应增加资源完整性检查,而不是只检查 checkout 命令返回成功。
按路径筛选
Git LFS 支持 lfs.fetchinclude 和 lfs.fetchexclude,可按路径减少不需要的对象下载。路径匹配使用类似 gitignore 的通配规则。Git LFS fetch 官方命令手册
诊断环境可以使用:
git config lfs.fetchinclude "Assets/Required,Resources/CI"
git config lfs.fetchexclude "Assets/Unused"
git lfs fetch --dry-run origin "$GIT_REF"
但不要把路径过滤直接推广到所有任务。PR 验证、单元测试、UI 测试和发布归档需要的资源集可能不同。建议至少拆成:
- PR 快速验证:只拉取编译和基础测试必需资源。
- 集成测试:增加运行时资源和测试素材。
- 发布归档:使用完整资源集,并执行完整性校验。
Git 的 sparse-checkout 和 partial clone 也能减少普通文件范围,但它们不会自动替你定义正确的 LFS 资源边界。Git clone 官方文档 说明了 --filter=blob:none 和 sparse checkout 的作用范围。
缓存复用与隔离
三种节点形态
| 方案 | LFS 对象复用 | 主要优点 | 主要风险 | 适用条件 |
|---|---|---|---|---|
| 一次性环境 | 低 | 隔离清晰,残留少 | 每次承担冷启动传输 | 低频任务、低信任代码 |
| 长期在线专用 Mac | 高 | 适合受控缓存和稳定构建 | 工作区、凭证、磁盘会长期积累 | 固定项目、可信分支 |
| 共享工作区 | 不稳定 | 设备利用率高 | 容易出现状态污染和交叉访问 | 只有在强清理、强隔离下采用 |
长期在线节点只有在对象缓存真正被重复任务命中时才有优势。共享工作区如果没有按提交重置,可能把旧资源、旧构建设置或 sparse-checkout 配置带入下一条任务。
对于自托管 Mac,建议采用“对象缓存持久化、工作区短生命周期”的方式:
- 每个任务使用独立工作目录。
- LFS 对象缓存按项目隔离。
- 任务开始时执行干净 checkout。
- 任务结束时删除工作区和临时文件。
- 定期检查磁盘增长并执行
git lfs prune。 - 对缓存设置容量门槛和回收策略。
依赖缓存也不能等同于可信制品。官方文档提醒,缓存内容不会被签名验证,能够读取缓存的工作流可能提取其中的内容;缓存中不应放令牌、密钥或其它敏感信息。依赖缓存安全说明
经验: LFS 对象缓存可以作为“可重新下载的数据层”,不能作为签名身份层。代码签名证书、发布令牌和生产凭证应使用独立的权限边界。
凭证与信任等级
下载权限不等于签名权限
CI 至少要区分两类权限:
- 仓库与 LFS 下载权限:用于读取代码、指针和真实资源。
- 代码签名与发布权限:用于签名、上传和发布。
把两者放在同一长期在线 Mac 上,会放大工作区污染和不可信分支执行带来的影响。非可信分支不应继承生产发布节点的长期凭证。
如果使用 checkout 组件,要复核当前版本的 persist-credentials 行为、凭证保存位置、清理时机,以及后续脚本是否仍能访问仓库。相关项目文档也说明了 sparse-checkout、fetch-depth、LFS 和凭证持久化等参数之间并非完全独立。checkout 参数说明
安全上尤其要注意两种情况:
- PR 任务可以执行任意构建脚本。
- 长期在线自托管 Mac 中残留了令牌、工作区或钥匙串凭证。
官方安全指南指出,自托管 Runner 不具备一次性干净虚拟机的天然保证;不可信代码可能持续影响节点并接触凭证。自托管 Runner 安全说明
建议将节点划分为:
- 普通验证节点:只允许低权限下载令牌。
- 可信集成节点:可以复用项目级缓存,但不保存生产签名凭证。
- 发布节点:固定项目、固定分支、最小化人员和任务范围。
节点吞吐与扩容判断
三个容量指标
优化 Git LFS 后,你需要重新观察:
- 有效构建时间:从资源准备完成到归档完成的实际时间。
- 峰值到达率:单位时间内进入队列的任务数量。
- 排队时间:任务等待可用 Mac 节点的时间。
不要根据开发者人数或硬件宣传规格直接估算节点数量。一个节点可能在编译阶段很快,却因为 LFS 传输、依赖恢复或签名互斥而产生长队列。
可使用下面的 TCO 公式进行内部比较:
年度 TCO =
网络流量成本
+ LFS 与依赖存储成本
+ Mac 节点占用时间
+ 运维工时
+ 故障与等待造成的影响成本
公式不应预填未经核实的价格。你需要从流水线记录中取实际下载量、节点占用时长、排队时长、故障恢复时间和维护工时。
条件式选择
只优化 Git LFS,适用于:
- LFS 下载和工作区恢复占主要时间。
- 编译阶段没有持续 CPU 或内存饱和。
- 热缓存命中后,构建节点仍有余量。
- 当前任务经常拉取无关资源。
增加固定 Mac 节点,适用于:
- 拉取范围已经收敛。
- 冷启动与热缓存行为稳定。
- Xcode 编译持续占满节点资源。
- 峰值排队来自真实构建并发,而非网络等待。
采用固定节点加弹性远程 Mac,适用于:
- 发布任务需要可信、固定、可审计的节点。
- PR 和测试任务具有明显峰值。
- 团队不希望为全年最高并发长期购买 Mac。
- 你可以用同一仓库、同一流水线验证远程节点的冷启动、缓存复用、构建和排队表现。
如果你正在做大型 iOS 仓库的节点规划,可以先参考 Mac 构建节点租赁与采购的决策入口,再把真实流水线记录带入 TCO 模型,而不是先按设备数量采购。
验收矩阵与落地步骤
建议按以下步骤实施,避免优化后无法证明结果。
第 1 步:建立两组基线
固定提交、分支、Xcode 工具链、依赖锁定文件和 Mac 节点类型。分别执行一次冷启动和多次重复运行,记录 Git、LFS、工作区、依赖和构建阶段。
第 2 步:核对对象范围
生成仓库实际 LFS 路径清单,检查 PR 任务是否下载了发布资源、历史分支资源或其它无关目录。重点核对 fetch-depth、分支范围和 checkout 组件参数。
第 3 步:实施路径过滤
先在非发布任务中启用 fetchinclude 或 fetchexclude。使用 git lfs fetch --dry-run 比较预计对象范围,再执行真实下载。发布任务暂时保持完整资源集。
第 4 步:分离缓存与工作区
保留可重新下载的 LFS 对象缓存,任务工作区按提交或任务独立创建。任务结束后清理 sparse-checkout 状态、临时凭证、构建目录和残留文件。
第 5 步:加入完整性验证
在编译前检查关键资源不是 LFS 指针文件。对发布任务执行 git lfs fsck、资源存在性检查和可重复构建验证,确认加速没有改变产物内容。
第 6 步:验证安全边界
确认普通验证节点无法读取发布凭证。检查缓存中没有令牌、证书和钥匙串导出文件。非可信分支不得复用生产发布节点的长期身份。
第 7 步:做远程 Mac 对照试点
使用同一仓库和同一流水线,在短期远程 Mac 试点中分别记录冷启动、热缓存、实际构建、峰值排队和故障恢复。若下载优化后瓶颈仍在编译阶段,再决定扩充固定容量还是增加弹性容量。
验收矩阵可以采用以下结构:
- 冷启动:记录 LFS 下载量和恢复耗时;不通过则回到拉取范围。
- 热缓存:记录重复对象比例和缓存后耗时;不通过则检查缓存路径与键。
- 资源完整性:检查关键文件和归档结果;不通过则禁止进入发布。
- 凭证隔离:检查下载令牌、签名权限和缓存内容;不通过则拆分节点。
- 磁盘回收:记录对象目录和工作区增长;不通过则设置清理门槛。
- 排队时间:记录峰值等待;不通过则评估弹性节点。
- 故障恢复:模拟节点失效或缓存不可用;不通过则保留可重新下载路径。
常见问题
Git LFS 会让 iOS CI 越来越慢吗?
可能会,但原因通常不是单一的。仓库资源持续增加、任务拉取范围过宽、缓存未命中、分支范围扩大,都会让传输或工作区恢复时间上升。你应先比较对象下载量和阶段耗时,再判断是否需要改变 Mac 构建容量。
CI 可以只下载当前任务需要的 LFS 文件吗?
可以使用路径过滤,但必须先定义任务资源集。fetchinclude 和 fetchexclude 只解决对象下载范围,不自动保证 sparse-checkout 后的工作区完整。编译、测试和发布应分别设计资源清单。
自托管 Mac 如何避免缓存污染?
不要把长期工作区当作缓存。保留按项目隔离的对象缓存,为每条任务建立独立工作区,并在任务结束后清理 Git 状态、稀疏检出设置和临时凭证。对共享节点尤其要验证下一条任务不会继承上一条任务的配置。
下载慢时应该优化网络还是增加 Mac?
先看网络等待是否主导总耗时。增加 Mac 只能提升并发或编译吞吐,不能直接降低每条任务需要下载的 LFS 字节量。只有当传输和缓存问题处理后,Xcode 编译仍然饱和且队列持续增长,扩容才有依据。
完成分阶段计时后,你可以用同一仓库申请一次短期远程 Mac 试点,对比冷启动、缓存复用、实际编译和峰值排队。如果当前方案是一次性 Mac 或固定自托管节点,常见缺点是冷启动成本难以摊薄、闲时资源闲置、硬件故障需要自行处理,以及峰值并发只能提前采购容量;而弹性远程 Mac 更适合先验证真实负载,再决定固定节点与弹性节点的组合。需要进一步核对区域与租赁方式时,可查看 Mac mini M4 远程租赁方案。
如果测量结果显示编译阶段长期饱和,优先把可信发布节点固定下来,再让 PR 验证和峰值任务使用弹性远程 Mac。若主要瓶颈仍是 LFS 传输,就不要用增加 Mac 的方式掩盖数据路径问题。