Büyük Git Depolarında Performans Optimizasyonu
Beş yıl önce, onlarca geliştiricinin üzerinde çalıştığı bir monorepo’nun git status komutuna 45 saniye yanıt verdiğini gördüğümde hayatımın o ana kadarki en çaresiz sysadmin anlarından birini yaşadım. Geliştirme ekibi neredeyse Git kullanmayı bırakmak üzereydi. O günden bu yana büyük depolarla boğuşmak biraz hobiye, biraz da zorunluluğa dönüştü. Bu yazıda öğrendiklerimi aktaracağım.
Sorunun Kökenini Anlamak
Git, tasarım gereği her şeyi .git dizini altında tutar. Küçük projelerde bu muhteşem bir basitlik sağlar. Ama depo büyüdükçe, özellikle şu üç faktör bir araya geldiğinde işler çığırından çıkar:
- Geçmiş derinliği: Binlerce commit, her biri kendi ağaç yapısıyla
- Binary dosyalar: Görseller, PDF’ler, derleme çıktıları yanlışlıkla commit’lenmiş
- Submodule karmaşası: Onlarca submodule’ün senkronize edilmesi gereken büyük projeler
Sorunun gerçek boyutunu anlamak için önce mevcut durumu ölçmek gerekir.
# Deponun toplam boyutunu ve pack dosyalarını incele
git count-objects -vH
# En büyük nesneleri bul
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
sed -n 's/^blob //p' |
sort --numeric-sort --key=2 |
tail -20 |
cut -f1,3 -d' '
# Depo boyut analizini daha okunabilir şekilde yap
git gc --dry-run 2>&1 | head -5
Bu komutların çıktısına bakarak hangi nesnelerin yer kapladığını görebilirsiniz. Çoğu zaman insanlar şoke olur: 2GB’lık bir depoda 1.4GB’ı, iki yıl önce yanlışlıkla commit’lenmiş bir ISO dosyasından geliyor olabilir.
git gc ve Repacking Stratejileri
Git’in çöp toplama mekanizması çoğu ortamda ya hiç yapılandırılmamış ya da varsayılan değerlerle bırakılmış olur. Varsayılanlar küçük projeler için yeterli, ama binlerce commit’lik bir depo için yetersiz kalır.
# Agresif bir gc işlemi çalıştır
# --aggressive bazen zarar verebilir, dikkatli kullanın
git gc --aggressive --prune=now
# Daha güvenli ve kontrollü bir yaklaşım
git repack -a -d -f --depth=250 --window=250
# Paralel işlem için thread sayısını ayarla
git repack -a -d --threads=4
# Pack boyutunu sınırla (CI/CD ortamları için faydalı)
git repack -a -d --max-pack-size=2g
--depth ve --window parametrelerine özellikle dikkat edin. Depth, delta zincirinin uzunluğunu; window ise delta hesaplamasında kaç nesnenin aynı anda karşılaştırılacağını belirler. Büyük değerler daha iyi sıkıştırma sağlar ama işlem süresi artar. Gece çalışan bir cron job için 250/250 kullanabilirsiniz, geliştirici makinelerinde 50/50 daha makul.
Partial Clone ve Shallow Clone Kullanımı
Bu ikisi birbirine karıştırılan ama tamamen farklı ihtiyaçlara hizmet eden özellikler.
Shallow clone, commit geçmişini keser. CI/CD pipeline’larınızda altın değerinde:
# Sadece son 1 commit'i çek
git clone --depth=1 https://github.com/ornek/buyuk-proje.git
# Belirli bir branch'i sığ klonla
git clone --depth=1 --single-branch --branch=main https://github.com/ornek/buyuk-proje.git
# Mevcut bir depoda geçmişi kırp
git fetch --depth=10 origin main
# Sığ klonu daha sonra derinleştirebilirsin
git fetch --deepen=100
Partial clone ise farklı bir şey: nesneleri ihtiyaç duyulduğunda çeker. Git 2.22 ile gelen bu özellik gerçekten oyun değiştirici:
# Blob'lar olmadan klonla (dosya içerikleri dahil edilmez başlangıçta)
git clone --filter=blob:none https://github.com/ornek/buyuk-proje.git
# Bloblar ve ağaçlar olmadan klonla (en agresif)
git clone --filter=tree:0 https://github.com/ornek/buyuk-proje.git
# Belirli boyutun üzerindeki blob'ları hariç tut
git clone --filter=blob:limit=1m https://github.com/ornek/buyuk-proje.git
# Klonladıktan sonra eksik nesneleri manuel çek
git rev-list --objects --filter=object:type=blob HEAD | head -20
Partial clone’un gerçek hayattaki etkisine dair somut bir örnek: 8GB’lık bir depoyu CI’da full clone ile çekmek 12 dakika alıyordu. --filter=blob:none ile bu süre 90 saniyeye indi. Pipeline maliyetleri ciddi oranda düştü.
Sparse Checkout ile Monorepo’larda Çalışmak
Monorepo dünyasına girdiyseniz, sparse checkout olmadan yaşayamazsınız. Bir geliştirici neden tüm depoyu yerel diskine çeksin ki, sadece frontend/ dizininde çalışıyorsa?
# Sparse checkout'u etkinleştir
git clone --no-checkout https://github.com/ornek/monorepo.git
cd monorepo
git sparse-checkout init --cone
# Sadece belirli dizinleri dahil et
git sparse-checkout set frontend/ shared/utils/ docs/
# Mevcut sparse checkout listesini görüntüle
git sparse-checkout list
# Yeni bir dizin ekle mevcut listeye
git sparse-checkout add backend/api/
# Cone modu dışında daha granüler kontrol (pattern tabanlı)
git sparse-checkout init
git sparse-checkout set '/*' '!/backend/' '!/mobile/'
Cone modu, dizin bazlı seçim yaparken çok daha hızlı çalışır çünkü Git’in iç veri yapılarıyla uyumlu şekilde optimize edilmiş. Mümkün olduğunca cone modunu tercih edin.
# Ekip için standart sparse checkout yapılandırması
# Bu scripti yeni geliştirici onboarding'ine ekleyebilirsiniz
#!/bin/bash
TEAM=$1 # frontend, backend, mobile, devops
git sparse-checkout init --cone
case $TEAM in
frontend)
git sparse-checkout set frontend/ shared/ docs/design/
;;
backend)
git sparse-checkout set backend/ shared/ docs/api/
;;
devops)
git sparse-checkout set infrastructure/ scripts/ .github/
;;
*)
echo "Bilinmeyen ekip: $TEAM"
exit 1
;;
esac
git checkout main
echo "Sparse checkout $TEAM ekibi için yapılandırıldı."
Git LFS: Binary Dosyaların Doğru Yönetimi
Geçmişi kirletmiş büyük binary dosyalar en yaygın performans katili. Git LFS (Large File Storage) bu sorunu kökten çözer, ama dikkat: LFS’yi sonradan eklemek, baştan eklemekten çok daha zahmetli.
# LFS'yi kur
git lfs install
# Belirli dosya tiplerini LFS'e yönlendir
git lfs track "*.psd"
git lfs track "*.pdf"
git lfs track "*.mp4"
git lfs track "*.zip"
git lfs track "*.tar.gz"
# .gitattributes dosyasını commit'le
git add .gitattributes
git commit -m "chore: LFS tracking kuralları eklendi"
# Mevcut LFS izlenen dosyaları listele
git lfs ls-files
# LFS durumunu kontrol et
git lfs status
# LFS'in boyut etkisini ölç
git lfs env
Peki ya zaten depo geçmişine girmiş büyük dosyalar? git-filter-repo burada devreye girer. Eski filter-branch komutunu artık kullanmayın, hem yavaş hem de tehlikeli:
# git-filter-repo'yu kur (pip veya package manager ile)
pip install git-filter-repo
# Belirli bir dosyayı tüm geçmişten sil
git filter-repo --path buyuk-dosya.iso --invert-paths
# Belirli bir dizini geçmişten temizle
git filter-repo --path dizin/eski-binary/ --invert-paths
# Boyut bazlı filtreleme (5MB üzeri tüm blob'ları kaldır)
# Bu bir script gerektiriyor, doğrudan filter-repo ile yapılır:
git filter-repo --strip-blobs-bigger-than 5M
# İşlem sonrası zorla push (bu noktadan sonra geri dönüş yok)
git push origin --force --all
git push origin --force --tags
filter-repo işlemi öncesinde mutlaka tam bir yedek alın ve ekipteki herkesi bilgilendirin. Bu komut çalıştırıldıktan sonra tüm ekibin repoyu yeniden klonlaması gerekecek.
Yapılandırma Optimizasyonları
Git’in yapılandırma dosyaları küçük tweaklerle büyük farklar yaratabilir. Bunları global veya proje bazlı olarak uygulayabilirsiniz:
# Dosya sistemi önbelleğini etkinleştir (Linux/Mac için)
git config core.fscache true
# Commit graph dosyasını kullan (traversal hızını dramatik artırır)
git config fetch.writeCommitGraph true
git commit-graph write --reachable --changed-paths
# Multipack index kullan
git multi-pack-index write
# Paralel fetch için
git config submodule.fetchJobs 8
# Preload index (büyük dizinler için fark yaratır)
git config core.preloadindex true
# Uzun süren işlemler için progress göster
git config core.checkStat minimal
# Windows'ta performans için (cross-platform ekipler için önemli)
git config core.autocrlf false
git config core.safecrlf false
# Delta cache boyutunu artır
git config pack.deltaCacheSize 2047m
git config pack.packSizeLimit 2g
git config pack.windowMemory 100m
Commit graph özelliği genellikle göz ardı edilir ama git log --graph ve git merge-base gibi komutların performansına etkisi çok ciddi. 100.000 commit’lik bir depoda commit graph olmadan git log 8 saniye alıyorken, commit graph ile 0.3 saniyeye düştüğünü bizzat ölçtüm.
# Commit graph'ı cron ile otomatik güncelle
# /etc/cron.daily/git-commit-graph dosyası olarak kaydet
#!/bin/bash
for repo in /opt/git-repos/*/; do
if [ -d "$repo/.git" ]; then
echo "Commit graph güncelleniyor: $repo"
git -C "$repo" commit-graph write --reachable --changed-paths 2>/dev/null
git -C "$repo" multi-pack-index write 2>/dev/null
fi
done
Submodule Performansı
Submodule’ler performans açısından ayrı bir başlık hak ediyor. Her submodule ek ağ isteği demek, bunu paralel yapmak kritik:
# Submodule'leri paralel güncelle
git submodule update --init --recursive --jobs=8
# Sadece belirli bir submodule'ü güncelle
git submodule update --init libs/ortak-kutuphane
# Submodule'leri sığ klonla
git submodule update --init --depth=1 --recursive
# Submodule durumunu hızlıca kontrol et
git submodule status --recursive
# Foreach ile tüm submodule'lerde komut çalıştır
git submodule foreach 'git fetch --depth=1'
Onlarca submodule varsa ve her biri ayrı bir repodan geliyorsa, bunu bir kez doğru yapılandırmak aylarca zaman kazandırır. CI/CD pipeline’larında --depth=1 ile submodule çekmek neredeyse zorunlu.
Maintenance Modu ve Otomatik Bakım
Git 2.30 ile gelen git maintenance komutu, tüm bu optimizasyon görevlerini otomatize etmek için tasarlanmış:
# Maintenance'ı etkinleştir
git maintenance start
# Arka planda çalışacak görevleri yapılandır
git maintenance run --task=gc
git maintenance run --task=commit-graph
git maintenance run --task=prefetch
git maintenance run --task=loose-objects
git maintenance run --task=incremental-repack
# Maintenance zamanlamasını görüntüle
git maintenance list
# Belirli bir görevin ne zaman çalışacağını ayarla
git config maintenance.commit-graph.schedule hourly
git config maintenance.gc.schedule weekly
git config maintenance.prefetch.schedule daily
Bu özelliği özellikle sunucu tarafında veya geliştirici makinelerinde arka plan görevi olarak kurmanızı öneririm. git maintenance start komutu, sisteminizin cron/launchd/systemd mekanizmasına otomatik olarak kayıt ekler.
Gerçek Dünya: Monorepo Performans Senaryosu
Bir e-ticaret şirketinde 200 geliştiricinin kullandığı, 5 yıllık geçmişe sahip bir monorepo’yu optimize etmek durumunda kaldım. Başlangıç durumu şuydu:
- Toplam depo boyutu: 12GB (pack dosyaları dahil)
git statussüresi: 23 saniyegit log --oneline -100süresi: 11 saniye- CI’da full clone süresi: 18 dakika
Uyguladığım adımlar sırasıyla:
# 1. Durum analizi
git count-objects -vH
git rev-list --count HEAD
# 2. Büyük nesneleri tespit et ve LFS'e taşı
git filter-repo --strip-blobs-bigger-than 10M --dry-run
# 3. Commit graph oluştur
git commit-graph write --reachable --changed-paths
# 4. Agresif repack
git repack -a -d -f --depth=250 --window=250 --threads=8
# 5. CI için partial clone yapılandırması
# .github/workflows/ci.yml içinde:
# git clone --filter=blob:none --depth=1 $REPO_URL
# 6. Sparse checkout politikası oluştur
git sparse-checkout init --cone
Sonuçlar bir hafta içinde netleşti:
- Toplam boyut: 12GB’den 3.2GB’ye düştü
git status: 23 saniyeden 1.2 saniyeyegit log: 11 saniyeden 0.4 saniyeye- CI clone süresi: 18 dakikadan 2.5 dakikaya
Ekibin moral durumu da buna paralel iyileşti, ki bu ölçülmesi zor ama gerçek bir metrik.
Sunucu Tarafı Optimizasyonları
Self-hosted GitLab veya Gitea çalıştırıyorsanız, sunucu tarafında da yapılabilecekler var:
# GitLab için git gc zamanlamasını ayarla
# /etc/gitlab/gitlab.rb içinde:
# gitlab_rails['git_gc_period'] = 86400 # 24 saat
# Gitea için repository maintenance
# app.ini içinde [cron.resync_all_hooks] ve [cron.run_task] ayarları
# Sunucu tarafında alternates kullanımı (disk alanı tasarrufu için)
git clone --shared /opt/git-repos/buyuk-repo.git /tmp/test-clone
# Reference advertisements'ı sınırla (büyük sunucularda)
git config uploadpack.allowFilter true
git config uploadpack.allowAnySHA1InWant true
Sonuç
Büyük Git depolarındaki performans sorunları çoğunlukla birkaç temel nedene dayanır: şişirilmiş geçmiş, yanlış yönetilen binary dosyalar ve yetersiz bakım rutinleri. Bunların hepsinin çözümü mevcut, ama sihirli tek bir komut yok.
Önce ölçün, sonra müdahale edin. git count-objects, git rev-list ve commit sayım araçları size gerçek tabloyu gösterir. Sorunu anlamadan önce agresif gc veya filter-repo gibi geri dönüşü zor işlemlere girişmeyin.
Kısa vadede en yüksek etkiyi genellikle şu üç şey sağlar: commit graph etkinleştirme, CI pipeline’larında partial/shallow clone kullanımı ve büyük binary dosyaları LFS’e taşımak. Bunlar görece düşük riskli ve etkisi hemen hissedilen adımlar.
Uzun vadede ise git maintenance ile otomatik bakım rutini kurmak, sparse checkout politikası oluşturmak ve yeni büyük dosyaların depoya girmesini engelleyen pre-receive hook’ları yazmak sürdürülebilirlik açısından kritik. Yangını söndürmek değil, yangın çıkmaması için sistemi kurmak asıl hedef olmalı.
