↓快轉到主要內容
  1. 教學文章/

Git maintenance 實戰:用 Commit-Graph、MIDX 與排程加速大型 Repo

·10 分鐘· loading · loading · ·
Git Git Maintenance Commit-Graph Multi-Pack-Index Performance Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 18: 本文

大型 Repository 用久了,常會出現一些很難描述的「鈍」:

  • git status 還好,但 git log -- path/to/file 越來越慢;
  • git fetch 完成後,偶爾會卡在整理物件;
  • .git/objects/pack/ 裡累積一大串 pack;
  • 同事說「跑一下 git gc --aggressive 就好」,卻沒人先量過;
  • CI 快取整包 .git,每次只是把歷史包袱搬到下一台機器。

這些症狀不一定都靠 GC 解決。

Git 的效能不只取決於工作目錄大小,還跟 commit 拓樸、物件數量、pack 數量、refs 數量,以及維護工作何時執行有關。

這篇跟著拍拍君從「先量測」開始,拆解 git maintenance、commit-graph 與 multi-pack-index(MIDX),最後建立可驗證、可停用、不會半夜亂刪資料的背景維護流程。

本文聚焦本機 Repository 的資料結構維護。若問題是 Monorepo checkout 太大,請先看 Git sparse-checkout 實戰;兩者解的是不同瓶頸。

一. 先別急著 GC:你要修的是哪一層?
#

Git Repository 大致可以從四層觀察:

層次 常見現象 對應工具
Working tree / index status、checkout 慢 sparse-checkout、fsmonitor、index 設定
Commit 拓樸 log、merge-base、可達性查詢慢 commit-graph
Object lookup pack 很多、找物件成本增加 multi-pack-index
儲存與過期資料 loose objects、多餘 pack、舊 reflog maintenance task、審慎的 GC

git gc 是一組整理工作的集合,不是萬用加速按鈕。

更重要的是,效能問題要有基準:

  1. 記錄 Git 版本與檔案系統環境;
  2. 記錄 .git 大小、loose object 與 pack 數量;
  3. 選三到五個真正常用的 Git 指令;
  4. 先跑暖機,再重複量測;
  5. 一次只改一個維護策略;
  6. 維護後做完整性驗證,再比較結果。

沒有 before / after 的「感覺變快」,只能算心情很好,不能算效能工程。

二. 建立 Repository 健康快照
#

2.1 先確認版本與 Repository 根目錄
#

git --version
git rev-parse --show-toplevel
git rev-parse --git-dir

不同版本的 Git 可能提供不同 maintenance task 或排程器。

如果你要把腳本交給整個團隊,先寫下最低支援版本,不要假設每台開發機的 Git 都一樣新。

2.2 看物件與 pack 的基本統計
#

git count-objects -vH

常見欄位包括:

  • count:目前 loose objects 數量;
  • size:loose objects 佔用空間;
  • in-pack:已放進 pack 的物件數量;
  • packs:packfile 數量;
  • size-pack:packfile 總大小;
  • prune-packable:已有 pack 版本、可被整理的 loose objects;
  • garbage:Git 不認得或需要人工檢查的檔案數量。

可以把快照留成純文字,方便日後比較:

mkdir -p .git/maintenance-metrics
git count-objects -vH > .git/maintenance-metrics/before.txt

這份檔案放在 .git 裡,不會被 commit;若 Repository 本身損壞,診斷資料也可能一起不見,所以正式監控應送到外部系統。

2.3 數 pack、commit-graph 與 MIDX
#

find "$(git rev-parse --git-dir)/objects/pack" \
  -name '*.pack' -type f | wc -l

find "$(git rev-parse --git-dir)/objects/info" \
  -name 'commit-graph*' -o -name 'commit-graphs' 2>/dev/null

find "$(git rev-parse --git-dir)/objects/pack" \
  -name 'multi-pack-index*' -o -name 'multi-pack-index.d' 2>/dev/null

這些命令只是盤點,不代表檔案越少一定越好。

大型 Repository 可能刻意保留多個 pack,再用 MIDX 提供統一索引;看到多個 pack 就立刻全量 repack,反而可能製造很長的停頓與大量暫存空間。

三. 量測真實操作,不要只量 git gc
#

3.1 用 /usr/bin/time 做最小量測
#

先挑團隊真的常用、而且輸出可丟棄的指令:

/usr/bin/time -p git rev-list --count --all >/dev/null
/usr/bin/time -p git log --all --oneline >/dev/null
/usr/bin/time -p git log --all -- src >/dev/null
/usr/bin/time -p git merge-base main HEAD >/dev/null

macOS 與 GNU time 的詳細輸出不同,-p 可以保留較可攜的 real、user、sys。

不要只跑一次。第一次可能包含磁碟 cache 暖機,後續執行也可能受到其他程式影響。

3.2 用 hyperfine 做重複比較
#

如果有安裝 hyperfine,可以固定暖機與次數:

hyperfine --warmup 3 --runs 10 \
  'git rev-list --count --all' \
  'git log --all -- src >/dev/null'

請在同一台機器、同一份 Repository、相近負載下比較。

維護前後若剛好有新的 fetch、branch 或大量檔案變更,兩組資料就不是同一個實驗。

3.3 需要拆解時使用 Trace2
#

Git Trace2 的 PERF target 可以把巢狀區段與計時事件寫成欄位化紀錄:

trace_file="$(mktemp)"
GIT_TRACE2_PERF="$trace_file" git log --all -- src >/dev/null
sed -n '1,30p' "$trace_file"
rm -f "$trace_file"

這適合回答「時間花在讀 commit-graph、走 revision graph,還是其他階段」,而不是只看到一個總秒數。

Trace 檔可能含路徑、參數與 Repository 資訊,不要直接貼進公開 issue。

四. Commit-Graph:替 Commit 拓樸建立捷徑
#

4.1 它加速的是什麼?
#

Commit 本身會記錄 parent,但每次從大量 commit 物件追溯拓樸都很昂貴。

commit-graph 把常用的拓樸資訊與 generation number 序列化,讓 revision walk、可達性判斷、merge-base 等操作少讀很多散落物件。

它不會修改 commit ID,也不會重寫歷史。

刪除 commit-graph 後,Git 仍可回到直接讀取 object database;這是一份可重建的加速資料。

4.2 手動建立與驗證
#

git commit-graph write --reachable
git commit-graph verify

大型而且持續成長的 Repository,通常更適合 split chain:

git commit-graph write --reachable --split
git commit-graph verify

新 commit 可以先寫成較小的一層,之後再依大小策略合併,不必每次重寫整張圖。

日常使用不必自己反覆執行這些底層命令;後面會讓 git maintenance 管理 incremental write 與 verify。

4.3 Path history 慢時,再考慮 Bloom filters
#

如果瓶頸是這種查詢:

git log --all -- path/to/component

可以評估 changed-path Bloom filters:

git commit-graph write --reachable --split --changed-paths
git commit-graph verify

它能幫 Git 快速排除「大概沒改到這條路徑」的 commit,但建立 filter 本身需要時間與空間。

因此先量測 path-limited history,確認這真的是常見瓶頸,再決定是否啟用:

git config commitGraph.changedPaths true

團隊若混用不同世代的 Git,還要測試相容性,別只在一台最新版本機器上宣告勝利。

五. Multi-Pack-Index:多個 Pack 共用一張索引
#

5.1 為什麼不永遠 repack 成一包?
#

每個 pack 都有自己的 .idx。

當 pack 數量增加,物件查找可能需要檢查多份 index;可是把所有物件重打成單一巨型 pack,可能消耗大量 CPU、I/O、時間與額外磁碟空間。

MIDX 的想法是:

保留多個 packfile,但建立一份跨 pack 的物件位置索引。

Git 因此可以用同一份索引定位物件,不必先把整座倉庫重包一次。

5.2 建立與驗證 MIDX
#

git multi-pack-index write
git multi-pack-index verify

底層檔案通常位於:

.git/objects/pack/multi-pack-index

MIDX 是衍生索引;各 pack 的 .idx 仍保留,所以它不是把唯一可讀資料鎖進一個神秘格式。

5.3 Incremental repack 的兩階段節奏
#

git maintenance 的 incremental-repack 會利用 MIDX:

  1. expire 清掉已不再被 MIDX 引用的舊 pack;
  2. repack 挑數個較小 pack 合成較大的 pack;
  3. 更新 MIDX,讓物件位置指向新 pack;
  4. 被取代的 pack 留到後續週期再安全清理。

這種漸進節奏,通常比大型 Repository 動不動就 full GC 更適合背景執行。

若要手動診斷,可以分開驗證:

git multi-pack-index verify
git fsck --connectivity-only

不要在不理解目前 object database 狀態時,直接把 expire、repack 和手動刪檔混成一串 shell 指令。

六. git maintenance:把小工作放到背景
#

6.1 先看預設,不要先改全域設定
#

在 Repository 內執行:

git config --show-origin --get maintenance.strategy || true
git config --show-origin --get-all maintenance.repo || true
git config --show-origin --get maintenance.auto || true

若只是想做一次 commit-graph 維護:

git maintenance run --task=commit-graph

若要依目前啟用的 task 與門檻判斷是否需要執行:

git maintenance run --auto

--task 明確指定工作,很適合先在測試 Repository 驗證;不要一開始就用最高權限把未知設定灑到所有專案。

6.2 註冊單一 Repository
#

git maintenance register

這會把目前 Repository 加進使用者層級的 maintenance.repo 清單。

若原本沒有設定 strategy,Git 會採用適合背景工作的 incremental 策略,典型節奏包括:

  • commit-graph:hourly;
  • prefetch:hourly;
  • loose-objects:daily;
  • incremental-repack:daily;
  • GC:不放進這組背景排程。

實際 task 與頻率仍要以你安裝版本的 git help maintenance 為準。

6.3 啟動平台排程器
#

git maintenance start

Git 會選擇平台排程器;也可以明確指定:

# macOS
git maintenance start --scheduler=launchctl

# Linux(可用時)
git maintenance start --scheduler=systemd-timer

# Windows
git maintenance start --scheduler=schtasks

背景排程會透過註冊清單逐一處理 Repository,不需要為每個專案手刻一份 cron。

6.4 確認真的註冊成功
#

git config --global --get-all maintenance.repo
git config --local --get maintenance.strategy
git config --local --get maintenance.auto

macOS 可再檢查 launchd:

launchctl list | grep -i git || true

Linux systemd user timer 可檢查:

systemctl --user list-timers | grep -i git || true

不要只看到 git maintenance start exit code 為 0 就收工;排程器、註冊清單與 Repository 設定三層都要確認。

七. GC 的安全邊界:哪些指令不要順手貼?
#

7.1 git gc --aggressive 不是定期保養
#

--aggressive 會用更昂貴的方式重新計算 delta,可能花很久,卻不保證改善你的真實 workload。

把它排進每日 cron,通常只是穩定地浪費資源。

如果診斷後確實要執行 GC,優先透過 maintenance 的鎖定與工作模型:

git maintenance run --task=gc

仍應安排維護窗口,並避免跟其他寫入 object database 的作業重疊。

7.2 避免立即 prune
#

這類指令風險很高:

# 不建議當成一般清理命令
git gc --prune=now
git prune --expire=now

不可達物件有時正是 reflog、未完成操作或救援流程的最後保險。

立即刪除也可能跟同時寫入 Repository 的程序產生競態;節省的那點空間,不一定值得拿復原能力交換。

如果你的目的其實是找回誤刪 commit,應先停止清理並閱讀 Git reflog 救援實戰。

7.3 不要手動刪 .git/objects/pack/*
#

pack、index、bitmap、MIDX 彼此有引用關係。

用檔名大小或修改時間猜「這包看起來很舊」,是讓 Repository 從慢變成壞的最快方式之一。

正確做法是:

git multi-pack-index verify
git fsck --connectivity-only
git maintenance run --task=incremental-repack

讓 Git 自己更新索引與選擇可清理的 pack。

八. 建立可重跑的維護檢查腳本
#

下面腳本只做量測、明確 task、驗證與再量測,不碰 --prune=now:

#!/usr/bin/env bash
set -euo pipefail

git rev-parse --is-inside-work-tree >/dev/null

metrics_dir="$(git rev-parse --git-dir)/maintenance-metrics"
mkdir -p "$metrics_dir"

timestamp="$(date +%Y%m%dT%H%M%S)"
before="$metrics_dir/${timestamp}-before.txt"
after="$metrics_dir/${timestamp}-after.txt"

{
  git --version
  git count-objects -vH
  /usr/bin/time -p git rev-list --count --all >/dev/null
} >"$before" 2>&1

git maintenance run --task=commit-graph
git commit-graph verify

{
  git --version
  git count-objects -vH
  /usr/bin/time -p git rev-list --count --all >/dev/null
} >"$after" 2>&1

printf 'before: %s\nafter:  %s\n' "$before" "$after"

這份範例刻意只執行 commit-graph task,因為一次只改一個變因才容易判讀。

若你接著要測 incremental repack,另開一次實驗、保留新的 before / after,不要把兩種效果混在一起。

8.1 在 CI 中只做驗證,不一定做重維護
#

短命 CI runner 通常沒有必要排背景 maintenance。

比較實用的是在持久 cache 或 self-hosted runner 上監控,並讓一般 CI 做輕量檢查:

git fsck --connectivity-only
git commit-graph verify 2>/dev/null || true
git multi-pack-index verify 2>/dev/null || true

最後兩項可能因為尚未建立對應索引而失敗,所以範例把它們當成可選診斷;若你的環境宣告「一定存在」,就應移除 || true,讓缺失真的使 job 失敗。

九. 變更後如何驗收與回復?
#

9.1 驗收清單
#

完成 maintenance 後,至少檢查:

git fsck --connectivity-only
git commit-graph verify
git multi-pack-index verify
git count-objects -vH

再重跑相同量測:

/usr/bin/time -p git rev-list --count --all >/dev/null
/usr/bin/time -p git log --all -- src >/dev/null

同時觀察:

  • 常用指令的中位數是否改善;
  • 最慢一次是否仍出現長尾;
  • .git 空間是下降、持平,還是換取速度而略增;
  • maintenance 執行時間是否超過排程間隔;
  • fetch / checkout 是否跟背景工作互相搶 I/O。

9.2 停止排程與取消註冊
#

停止整個使用者的 Git maintenance 排程:

git maintenance stop

只把目前 Repository 從清單移除:

git maintenance unregister

取消註冊不等於恢復所有 config;請重新查看設定來源:

git config --show-origin --get maintenance.auto || true
git config --show-origin --get maintenance.strategy || true
git config --global --get-all maintenance.repo || true

commit-graph 與 MIDX 是可重建的衍生資料,但別把「可重建」誤會成「應該直接用 rm 清掉」。先停用策略、驗證行為,再透過 Git 提供的命令處理。

十. 一套務實的導入順序
#

如果拍拍君要替團隊導入,會照這個順序:

  1. 盤點:Git 版本、Repository 大小、object / pack / ref 數量;
  2. 量測:挑出 revision walk、path history、fetch 等真實 workload;
  3. 低風險試跑:先做 commit-graph task 與 verify;
  4. 觀察 MIDX:pack 多時測試 incremental repack,不急著 full GC;
  5. 單一 Repo 註冊:確認 config 與排程器真的生效;
  6. 擴大範圍:一批一批加入,不要一次註冊所有巨型專案;
  7. 保留回復路徑:知道怎麼 stop、unregister、查看 config source;
  8. 定期檢討:若維護時間逼近排程間隔,就降低工作複雜度或頻率。

這套流程的核心不是「把 .git 壓到最小」,而是讓前景操作延遲穩定,並把昂貴整理拆成可控的小工作。

結語
#

大型 Repository 的維護,真正麻煩的不是缺少一條神奇指令,而是我們太容易把不同瓶頸都叫做「Git 很慢」。

記住三個角色就好:

  • commit-graph 加速 commit 拓樸與 revision walk;
  • MIDX 讓多個 pack 共用物件索引,支援漸進式 repack;
  • git maintenance 把可背景執行的整理工作排程化、可驗證化。

先量測、再執行一個 task、接著 verify,最後用相同 workload 比較。

這比每週五下班前盲跑一次 git gc --aggressive 科學多了,也比較不會讓週末變成 Repository 救援實戰。🔧

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 18: 本文

相關文章

Git sparse-checkout 實戰:大型 Monorepo 的部分取出與安全切換
·7 分鐘· loading · loading
Git Sparse-Checkout Monorepo Cone Mode Partial Clone Worktree Developer-Tools
Git Changelog 自動化:從 Commit、PR 到 Release Notes
·5 分鐘· loading · loading
Git GitHub Changelog Release Notes Automation GitHub Actions Developer-Tools
Git tag 與 Release 實戰:版本標記、SemVer 與安全發佈流程
·8 分鐘· loading · loading
Git Tag Release SemVer Versioning GitHub Developer-Tools
Git rerere 實戰:記住衝突解法,讓 Rebase 與 Merge 不再重做
·8 分鐘· loading · loading
Git Rerere Merge-Conflict Rebase Merge Version-Control Developer-Tools
Git reset、restore、revert 實戰:選對復原工具
·9 分鐘· loading · loading
Git Reset Restore Revert Undo Version-Control Developer-Tools
Git bisect 實戰:快速定位壞 commit 與除錯流程完全攻略
·9 分鐘· loading · loading
Git Bisect Debugging Regression Version-Control