了解我们如何追踪到一个存在15年的Git函数性能瓶颈并修复它,从而提升效率以支持更健壮的备份策略并降低风险。
仓库备份是任何健壮灾难恢复策略的关键组成部分。然而随着仓库规模增长,创建可靠备份的过程变得越来越具有挑战性。我们的 Rails 仓库曾需要48小时才能完成备份——这迫使团队在备份频率和系统性能之间做出艰难抉择。我们决心为内部用户和客户解决这个问题。
最终,我们发现问题根源在于一个具有O(N²)时间复杂度、存在15年的Git函数,并通过算法优化实现了指数级提速。成果包括:成本降低、风险减少,以及能随代码库规模同步扩展的备份策略。
这实际上是一个影响所有大型仓库的Git可扩展性问题。以下是我们的排查和修复历程。
首先分析问题本质。当组织扩展仓库规模时,备份工作会面临以下挑战:
耗时过长:大型仓库备份可能需要数小时,影响定期备份计划的可行性
资源密集:长时间备份会消耗大量服务器资源,可能干扰其他操作
维护窗口紧张:对于7×24小时运行的团队,难以安排足够长的维护时段
失败风险增加:长时间进程更容易受网络问题、服务器重启等因素中断,导致必须重头开始备份
竞态条件:由于备份耗时过长,仓库可能在过程中发生大量变更,导致备份无效或中断
这些挑战可能迫使团队在备份频率或完整性上妥协——这在数据保护领域是不可接受的。漫长的备份窗口会迫使客户寻求替代方案,有些采用外部工具,有些则降低备份频率,最终导致组织内部数据保护策略不一致。
GitLab的仓库备份功能依赖git bundle create
命令,该命令会捕获仓库的完整快照(包括所有对象和分支/标签等引用)。这个打包文件可作为仓库的精确还原点。
但该命令的实现存在与引用数量相关的可扩展性问题:随着引用数量增长,处理时间呈指数级上升。在我们的超大型仓库(含数百万引用)中,备份操作可能超过48小时。
通过分析命令执行时的火焰图,我们发现:
在包含1万引用的仓库中,约80%的执行时间消耗在object_array_remove_duplicates()
函数上。这个2009年引入的函数(提交b2a6d1c686)原本用于解决用户重复指定引用时(如git bundle create main.bundle main main
)的打包问题。其通过嵌套循环去重的O(N²)算法在引用量大的仓库中形成了性能瓶颈。
我们向Git上游提交的修复方案用哈希映射替代了嵌套循环:
每个引用被添加到自动去重的映射结构中
基准测试显示,在10万引用的仓库中性能提升6倍
Benchmark 1: bundle (refcount = 100000, revision = master)
Time (mean ± σ): 14.653 s ± 0.203 s [User: 13.940 s, System: 0.762 s]
Range (min … max): 14.237 s … 14.920 s 10 runs
Benchmark 2: bundle (refcount = 100000, revision = HEAD)
Time (mean ± σ): 2.394 s ± 0.023 s [User: 1.684 s, System: 0.798 s]
Range (min … max): 2.364 s … 2.425 s 10 runs
Summary
bundle (refcount = 100000, revision = HEAD) ran
6.12 ± 0.10 times faster than bundle (refcount = 100000, revision = master)
该补丁已被Git上游合并。GitLab通过反向移植使客户能立即受益,无需等待新版Git发布。
48小时→41分钟:最大仓库(gitlab-org/gitlab)的备份时间降至原1.4%
稳定扩展:优化效果在不同规模仓库中保持一致
资源高效:显著降低备份时的服务器负载
广泛适用:所有基于打包的多引用操作均受益
企业团队可建立全面的夜间备份计划,不影响开发流程
备份能在夜间后台静默完成,无需专用长时间窗口
RPO(恢复点目标)从数天级降至分钟级,灾难场景下最多损失数小时工作而非数天
减少服务器资源占用和运维窗口
更短的备份时间直接降低云环境计算成本
仓库增长不再迫使团队在备份频率与性能间取舍
备份策略可与代码库同步扩展
自GitLab 18.0起,所有客户无需额外配置即可享受这些优化成果。
此次突破是我们构建可扩展企业级Git基础设施的持续承诺的一部分。我们仍在持续识别和解决全栈性能瓶颈。特别自豪的是,这项改进已惠及整个Git社区,体现了开源协作的价值。
微信订阅号
扫一扫关注微信订阅号,
获取最前沿的极狐GitLab资讯