JavaScript monorepo’muzun Git boyutunu nasıl %94 küçülttük?
(jonathancreamer.com)- Microsoft’un büyük JavaScript monorepo’su 1JS, aylık 1.000 aktif kullanıcı, yaklaşık 2.500 paket ve yaklaşık 20 milyon satır koda ulaştı; yakın zamanda klon boyutu 178 GB’a çıktığı için bazı geliştiriciler depoyu klonlamakta bile zorlandı
- Başta ortaya çıkan darboğaz, Beachball değişiklik dosyalarının tek bir klasörde on binlerce dosya biriktiren yapısıydı; dosya sayısı 40.000’e ulaşınca her yeni dosya eklendiğinde büyük bir tree object oluşuyordu
- Daha büyük neden ise
CHANGELOG.mdveCHANGELOG.jsongibi uzun dosya adlarının tekrar tekrar güncellendiği durumlarda, Git’in mevcut paketleme kodunun delta hedefini seçerken dosya adının yalnızca son 16 karakterine bakmasıydı git repack -adf --window=250de etkili oldu, ancak yol tabanlı paketleme olangit repack -adf --path-walkMicrosoft Git fork’u2.47.0.vfs.0.2üzerinde çalıştırıldığında yerel depo 178 GB’tan 5 GB’a düştü- Büyük monorepo’larda uzun adlı dosyalar sık değişiyorsa
pack.usePathWalk trueve yenigit surveykomutuna bakmak gerekiyor; ilgili değişiklikler Git upstream’e de yansıtılacak
1JS monorepo’sunun ölçeği ve darboğazı
- Microsoft içindeki büyük JavaScript monorepo’su 1JS, yalnızca depo boyutu açısından değil, kod ve katkı ölçeği açısından da büyük
- Aylık 1.000’den fazla aktif kullanıcı
- Yaklaşık 2.500 paket
- Yaklaşık 20 milyon satır kod
- Yakın zamanda klonlanan deponun boyutu 178 GB idi ve bazı Avrupalı geliştiriciler boyutu nedeniyle depoyu klonlayamıyordu
- İlk klonlandığında 1–2 GB düzeyindeydi, ancak birkaç ay sonra yaklaşık 4 GB’a çıktı ve sonrasında da büyümeye devam etti
Ders #1: Tek bir klasöre binlerce dosya yığmamak
git-sizer, büyük blob’ları ve Beachball değişiklik dosyaları sorununu ortaya çıkardı- Büyük blob’lar genellikle binary dosyaların yanlışlıkla commit edilmesiyle oluşur; Azure DevOps’un check-in boyutu sınırı gibi özelliklerle engellenebilir
- Geçmişe zaten girmiş dosyaları sonradan kaldırmak kolay değildir
- 1JS, paketlerin semver aralığı artışını otomatikleştirmek için Beachball change files kullanıyordu
- Kullanım amacı Changesets ile benzerdir
- Amaç, semantic-release gibi paket sürüm artışını otomatikleştirmektir
- Değişiklik dosyaları silinmediği için tek bir klasörde 40.000’e kadar birikti; bu durumda yeni bir dosya eklendiğinde büyük bir tree object oluşturuluyordu
- İki önlem alındı
- Beachball PR ile paket başına bir dosya yerine birden çok değişikliğin tek bir değişiklik dosyasında tutulması sağlandı
- Değişiklik dosyaları klasörünü periyodik olarak otomatik temizleyen bir pipeline yazıldı
Ders #2: versioned branch ve changelog paketleme sorunu
- 1JS’in sürüm yönetimi akışı,
main’in aynası olanversionedbranch’ini korur- Gerçek paket sürümlerini saklayarak
mainüzerinde Git çakışmalarını önler - NPM paketi olarak yayınlanan semver sürümleri ile Git commit’leri arasındaki eşleşmeyi doğru biçimde görmeyi sağlar
- Gerçek paket sürümlerini saklayarak
versionedbranch’i çoğunluklaCHANGELOG.mdveCHANGELOG.jsondosyalarına içerik ekleyen commit’lerden oluşuyordu, ancak klonlamayı giderek zorlaştıracak kadar büyüdü- Depo, 2021’den bu yana her yıl yüz binlerce satır kod ve yüzlerce geliştirici eklenmesiyle doğal olarak büyüdü; ancak büyüme oranı Microsoft’un Office monorepo’sundan daha yüksek hale gelince ayrı bir nedenden şüphelenildi
- Git’in
shallow checkout,sparse indexgibi büyük monorepo’lara yönelik özelliklerini geliştiren bir geliştirici incelediğinde,versionedbranch’i alınırken 125 GB ek Git verisi fetch edildiğini gördü
Git paketleme kodu yanlış deltaları nasıl oluşturuyordu?
- Neden, eski Git paketleme kodunun dosyaları sıkıştırmadan önce karşılaştırma yaparken dosya adının yalnızca son 16 karakterini kontrol etmesiydi
- Normalde Git yalnızca değişen dosyanın diff’ini push eder; ancak bu paketleme sorunu nedeniyle farklı paketlere ait
CHANGELOG.mddosyaları birbirleriyle karşılaştırılıyordu - Örneğin
repo/packages/foo/CHANGELOG.jsondeğiştirildiğinde, Git push hazırlığı sırasındarepo/packages/bar/CHANGELOG.jsonile bir diff oluşturabiliyordu - Bu yüzden birçok durumda tüm dosya tekrar tekrar push ediliyordu; bazı dosyaların boyutu onlarca MB olabiliyordu
- Sorunun ayrıntıları Stolee’nin Git mailing list yazısında özetleniyor
repack ve path-walk sonuçları
- Önce
git repack -adf --window=250ile daha büyük bir window kullanılarak pack dosyası sıkıştırması iyileştirildi ve depo boyutu belirgin biçimde küçüldü - Ardından git-for-windows PR #5171, commit’ler üzerinde yürüyen varsayılan yöntem yerine Git yolları üzerinde yürüyen bir paketleme yöntemi ekledi
- Microsoft’un Git fork’u olan Microsoft git
git version 2.47.0.vfs.0.2ile yeni yöntem kullanılarak klonlandıktan sonra repack çalıştırıldı- Çalıştırılan komut:
git repack -adf --path-walk - Sonuç: depo boyutu 178 GB’tan 5 GB’a düştü
- Çalıştırılan komut:
- Yeni yapılandırma seçeneği, push sırasında da doğru deltaların oluşturulmasını sağlar
git config --global pack.usePathWalk true
git version 2.47.0.vfs.0.2kullanan geliştiriciler, yerel klondan sonra repack çalıştırıp yeni path-walk push algoritmasıyla büyüme oranını düşürebilir
GitHub, Azure DevOps ve upstream Git’e etkisi
- GitHub periyodik olarak repack ve Git garbage collection gerçekleştirir, ancak GitHub’ın mevcut paketleme yöntemi
CHANGELOG.md,CHANGELOG.jsongibi dosyalar için deltaları doğru hesaplayamıyor - Zaman içinde sık değişen ve son 16 karakterden fazlası aynı olan uzun dosya adları da etkilenebilir
- Örnek olarak i18n için kullanılan büyük string dosyaları gösteriliyor
- 1JS’in kullandığı Azure DevOps henüz bu tür bir repack yapmıyor; sunucu tarafındaki depo boyutunu da küçültmek için ilgili çalışmalar yürütülüyor
- İlgili değişiklikler Git upstream’e de yansıtılacak
Büyük monorepo’larda kontrol edilmesi gerekenler
- Büyük ölçekli monorepo’larda
CHANGELOG.mdveya nispeten uzun adlı dosyalar tekrar tekrar güncelleniyorsa path-walk ile ilgili değişiklikleri takip etmek gerekiyor - Yeni
git surveykomutu, depo durumunu değerlendirebilecek heuristics sağlar- Top Files By Disk Size
- Top Directories By Inflated Size
- Top Files By Inflated Size
- Bu heuristics, path-walk çalışmasının ilgili depo boyutu üzerinde de etkili olup olmayacağını tahmin etmeye yardımcı olur
1 yorum
Hacker News yorumları
Yeni
git-surveykomutu nerede diye sorarsanız, henüz git.git içine girmiş değilYazar Microsoft'un Git fork'unu kullanıyor ve bu komut bu yaz eklenmiş: https://github.com/microsoft/git/pull/667
full-name-hashvepath-walkda yalnızca fork'ta ya da Git HEAD'de var gibi görünüyorKılavuz sayfasında ya da 2.47 değişiklik günlüğünde bulunamıyor
“Avrupa'daki insanlar depo boyutu yüzünden clone bile edemiyor” ifadesinin ne anlama geldiğini merak ediyorum
Büyük bir deponun Avrupa'da indirilemeyip başka yerlerde indirilebilmesi tuhaf; ayrıca sunucu tarafındaki değişiklik uygulanana kadar hâlâ clone edilemeyecekmiş gibi duyuluyor
“Dosyanın tamamını sürekli yeniden push etti” cümlesi sanki ortasından kesilmiş gibi; yazıyı okurken GIF de çok dikkat dağıtıyor ve okuma modunda bile aynen görünüyor
Daha eksiksiz yazılsa muhtemelen “Avrupa'da seyahat ederken kullanmak için tüm depoyu dizüstü bilgisayarına clone etmeye çalışan biri vardı; o sırada diski dolu olduğu için yapamadı. Sonra diski temizleyip clone edebildi” gibi olurdu
Bunun Avrupa'nın 180 GB'lık dosyayı kaldıramadığına dair büyük bir sorun anlamına geldiğini sanmıyorum
Özellikle kişi VPN, hele de berbat “SSL VPN” numaralarından birini kullanıyorsa daha da öyle; gecikmeyle kötü başa çıkan yazılımlar veya bozuk window scaling'e sahip sunucular hâlâ şaşırtıcı derecede yaygın
Geliştiricilerin çoğu sunucuya yakınsa ve iyi bant genişliğine sahip bağlantılar kullanıyorsa bu tür sorunlar pek ortaya çıkmaz
Azure DevOps clone işlemi kablolu gigabit bağlantıyı hiç doyuramıyor; o kadar uzun sürüyor ki sonunda ya benim tarafımda ya da Azure DevOps tarafında duruyor ve vazgeçmek zorunda kalıyorum
Neyse ki işlerin neredeyse tamamını Codespaces içinde shallow clone ile yaptığım için büyük bir sorun değil; bu yazıdaki 1JS deposu sorununa benzer bir boyut patlaması bizim depoda da varsa düzeltilmesini umuyorum
Güncelleme: Saçma bir hataydı; dosya adları tam yolu içermiyor
Açıklama GIF’lerin arasında kaybolmuş gibi, ama burada son 16 karakter birbirinden farklı:
repo/packages/foo/CHANGELOG.mddeğiştirilmişken Git’in push’a hazırlanırkenrepo/packages/bar/CHANGELOG.mdile karşılaştırıp fark oluşturduğu bir örnek bupath-walkAPI kapak mektubuna da bakmaya değer: https://lore.kernel.org/all/pull.1786.git.1725935335.gitgitg...Blogdaki örnek çok net değil, ama Git aslında depodaki tüm dosyaların tüm sürümlerini alıp yolun son 16 baytını bir hash tablosuna koyuyor ve aynı dosyanın farklı sürümleri olmasını beklediği şeyleri delta sıkıştırma için grupluyordu
Yazıdaki
foo/CHANGELOG.mdvebar/CHANGELOG.mdyalnızca 13 karakter olduğundan örnek hâliyle doğru değil; daha uzun ortak soneki olan yollar hayal etmek gerekiyorBu kısım
--full-name-hashseçeneğiyle düzeltildi; artık 16 bayt yerine tüm yol karşılaştırılıyorSonrasında bahsedilen pencere boyutunu artırmak, kötü dosya gruplamasını aşmaya yönelik bir hack’e daha yakın; gerçek bir düzeltme değil
Sıkıştırıcıya hâlâ berbat bir girdi verip bunu muazzam bellek kullanarak aşıyorsunuz; bu yüzden bunun çözüm gibi sunulması kafa karıştırıcıydı
Asıl ilginç kısım path-walk API ve
--full-name-hashBurada dosya adının yol anlamına mı geldiğini, yoksa Git’in aynı ada sahip herhangi bir dosyayı seçip fark mı oluşturduğunu merak ediyorum
Başka dosyanın nasıl seçildiğinde bir örüntü olup olmadığını da bilmek isterdim
--full-name-hashPR’ının ilk commit mesajına bakılabilir: https://github.com/git-for-windows/git/pull/5157/commits/d5c...Alıntılarsak,
"/CHANGELOG.json"15 karakter ve beachball aracı tarafından oluşturuluyor; bu dosyanın farklı sürümlerini ayırt edebilen tek şey üst dizinin son harfi, o da yalnızca üstteki 2 bit üzerinden yansıyorO harf bir karakterse her zaman çakışıyor;
"/CHANGELOG.md"yolu da benzer bir sorun taşıyor ama üst dizinde farklılaşma payı biraz daha varGruplama algoritması, adın sağ tarafından uzaklaştıkça her karaktere daha düşük ağırlık veriyor:
hash = (hash >> 2) + (c << 24)Hash 32 bit; tüm yolun her 8 bitlik karakteri sırayla hash’in en yüksek 8 bitine ekleniyor, ancak mevcut hash bitleri 2 bit sağa kaydırıldığı için nihai hash’i yalnızca son 16 karakter etkiliyor
Gerçekte ne olduğunu burada görebilirsiniz: https://go.dev/play/p/JQpdUGXdQs7
Go’ya taşıyıp
"aaa/CHANGELOG.md"ile"zzz/CHANGELOG.md"için nihai değerleri karşılaştırdım;aaavezzzyerine çeşitli değerler koyarsanız nihai değeri ne kadar az etkilediklerini görürsünüzCHANGELOG.md’nin son 16 karakteri dosya adının tamamı ve böyle yorumlandığındafilepathwalkçözümünün sorunu neden çözdüğünü de açıklıyorAma gerçekten dosya adının yalnızca son 16 karakterine dayanıyorsa ve tam yol değilse, özellikle monorepo’larda bu sorunun çok daha yaygın olması gerekirdi gibi geliyor
Muhtemelen
foovebarmetad değişkenleri kafa karıştırmış; gerçek paket adlarınınfirstPkg,secondPkggibi ortak bir soneki olmuş olabilirYalnızca
"/CHANGELOG.md"zaten 13 karakter, yani 3 karakterlik ortak bir sonek bile 16 karakter çakışması yaratıyorHemen
nixpkgsüzerinde denedim; GitHub’dan olduğu gibi klonlayınca yaklaşık 5 GBYazıda bahsedilen ilk seçenek olan
--window 250boyutu 1,7 GB’a düşürdü; Microsoft’un Git fork’undaki yeni--path-walkseçeneği ise daha az etkiliydi ve toplam 1,9 GB olduİkisi de başlangıç boyutunun yarısından küçük; keşke GitHub’ın bu tür işlemleri çalıştırmasını sağlayan bir yol olsa, hatta insanlar böyle şeyleri doğrudan kontrol edebilecekleri şekilde barındırmaya başlasa daha da iyi olurdu
Yazıda gerekli değişikliği araştırıp yayımlayan Derrick Stolee geçiyor
Git’in iç yapısı, yerelde ve CI’da Git klon boyutunu küçültme gibi konular ilginizi çekiyorsa Derrick’in GitHub blogunda çok sayıda harika yazısı var: https://github.blog/author/dstolee/
Kişisel sitesi de bakmaya değer: https://stolee.dev/
Yanlışlıkla büyük bir ikili dosyayı commit ettiyseniz, düzeltme yolu
filter-branchçalıştırıp sorunlu ikili dosyayı kaldırmak, herkese ikili dosyalar için git-lfs kullanmayı öğretip bunu yapılandırmak, ardından force push yapıp herkesin çalışma ortamını normal hâline döndürmek olarak görülebilir.İdeal değil, ama kullanılmayan büyük bir dosyayı Git’te tutmaya devam etmekten daha iyi.
filter-branchgözünü korkutanlar için BFG de var: https://rtyley.github.io/bfg-repo-cleaner/Başkasının da belirttiği gibi bu sorun küçük ama sık değişen dosyalarla ilgili olduğundan, yerden tasarruf etmek için geçmişteki eski sürümleri kaldırıp bundan sonra LFS kullanmak yeterli.
Git’i hacklemek eğlenceli olabilir, ama en başta monorepo’ya 2.500 paket koymamanın bir yolu yok mu diye düşünüyorum.
Kod tabanı büyüdükçe mevcut tekerleği yeniden icat etmemeyi beklemek zorlaşıyor; çünkü ya kodun varlığından haberdar olunmuyor ya da kendi kullanım senaryosunu karşılayacak şekilde değiştirirken mevcut kullanımları bozma korkusu oluyor.
Bana kalırsa 50 modülden 100 modüle çıkmak, 100 modülden 200 modüle çıkmaktan daha uzun sürüyor.
Tamamen kendi kendilerine yarattıkları bir sorun gibi görünüyor.
Son dönemde GitButler’ın yayımladığı GitMerge sunumlarını izleyerek monorepo ve ölçeklenebilirlik alanındaki gelişmeleri takip ediyorum; Microsoft, GitHub ve GitLab’dan çok iyi şeyler geliyor.
Son 16 karakter denetimi ve tam yol denetimi sorunlarının delta sıkıştırma, pack index ve multi-pack index ile nasıl etkileştiğini daha iyi anlamak isterim.
Microsoft’un Azure DevOps’u bizzat kullandığını görmek güzel.
Giderek daha fazla Azure hizmetinin yalnızca GitHub’a native connector sunduğunu gördüğüm için Azure DevOps’un ihmal edilmeye doğru gittiğini sanmıştım.
Git’in iç yapısını bu kadar iyi bilen birinin elinizin altında olup yardım edebilmesi, bu ölçekte bir şirkette böyle bir proje yaparken gerçekten güzel bir ayrıcalık olmalı.