Wie wir die Git-Größe eines JavaScript-Monorepos um 94 % reduziert haben
(jonathancreamer.com)- Das große JavaScript-Monorepo 1JS von Microsoft wuchs auf 1.000 monatlich aktive Nutzer, etwa 2.500 Pakete und rund 20 Millionen Zeilen Code an; zuletzt erreichte die Clone-Größe 178 GB, sodass einige Entwickler das Repository kaum noch klonen konnten
- Ein früher Engpass war die Struktur, bei der sich Beachball-Änderungsdateien zu Zehntausenden in einem Ordner ansammelten; als die Zahl auf 40.000 Dateien stieg, entstand bei jedem neuen Datei-Add ein großes tree object
- Die größere Ursache war, dass der bisherige Packing-Code von Git in Situationen mit wiederholt aktualisierten langen Dateinamen wie
CHANGELOG.mdundCHANGELOG.jsonnur die letzten 16 Zeichen des Dateinamens zur Auswahl von Delta-Zielen betrachtete git repack -adf --window=250half zwar, aber als Microsoft im Git-Fork2.47.0.vfs.0.2git repack -adf --path-walkausführte, schrumpfte das lokale Repository von 178 GB auf 5 GB- Wenn sich in großen Monorepos Dateien mit langen Namen häufig ändern, sollte man
pack.usePathWalk trueund den neuen Befehlgit surveyprüfen; die zugehörigen Änderungen sollen auch in Git upstream einfließen
Größe und Engpässe des 1JS-Monorepos
- Das große interne Monorepo 1JS von Microsoft ist nicht nur beim Speicherbedarf groß, sondern auch beim Umfang von Code und Beiträgen
- mehr als 1.000 monatlich aktive Nutzer
- etwa 2.500 Pakete
- rund 20 Millionen Zeilen Code
- Ein kürzlich geklontes Repository hatte eine Größe von 178 GB, und einige Entwickler in Europa konnten das Repository wegen seiner Größe nicht klonen
- Beim ersten Clone lag es noch bei etwa 1 bis 2 GB, einige Monate später bei rund 4 GB, und danach wuchs es weiter
Lesson #1: Nicht Tausende Dateien in einem Ordner ansammeln
git-sizermachte große Blobs und das Problem mit Beachball-Änderungsdateien sichtbar- Große Blobs entstehen, wenn versehentlich Binärdateien committet werden; mit Funktionen wie Check-in-Größenlimits in Azure DevOps lässt sich das verhindern
- Dateien, die bereits in der Historie gelandet sind, lassen sich nachträglich nicht leicht entfernen
- 1JS nutzte Beachball change files, um semver-Bereichserhöhungen von Paketen zu automatisieren
- Der Einsatzzweck ist ähnlich wie bei Changesets
- Das Ziel ist, Versionsanhebungen von Paketen ähnlich wie bei semantic-release zu automatisieren
- Weil die Änderungsdateien nicht gelöscht wurden, sammelten sich bis zu 40.000 Dateien in einem Ordner; in diesem Zustand erzeugte das Hinzufügen neuer Dateien große tree objects
- Es gab zwei Gegenmaßnahmen
- Ein Beachball PR änderte das Verhalten so, dass statt einer Datei pro Paket mehrere Änderungen in einer einzigen Änderungsdatei gesammelt werden
- Es wurde eine Pipeline geschrieben, die den Ordner mit den Änderungsdateien regelmäßig automatisch bereinigt
Lesson #2: Das Problem mit dem versioned-Branch und dem Packing von Changelogs
- Der Versionsverwaltungs-Workflow von 1JS hält einen
versioned-Branch als Spiegel vonmain- Er speichert die tatsächlichen Paketversionen und vermeidet so Git-Konflikte in
main - Außerdem lässt sich die Zuordnung zwischen als NPM-Paket veröffentlichten semver-Versionen und Git-Commits exakt nachvollziehen
- Er speichert die tatsächlichen Paketversionen und vermeidet so Git-Konflikte in
- Der
versioned-Branch enthielt hauptsächlich Commits, die nur Inhalte inCHANGELOG.md- undCHANGELOG.json-Dateien ergänzten, wuchs aber so stark an, dass Clones immer schwieriger wurden - Das Repository wuchs seit 2021 zwar natürlich jedes Jahr um Hunderttausende Codezeilen und Hunderte Entwickler, doch als die Wachstumsrate die des Office-Monorepos von Microsoft überstieg, wurde eine gesonderte Ursache vermutet
- Entwickler, die an Git-Funktionen für große Monorepos wie
shallow checkoutundsparse indexgearbeitet hatten, stellten fest, dass beim Abrufen desversioned-Branchs 125 GB zusätzliche Git-Daten gefetcht wurden
Wie der Packing-Code von Git die falschen Deltas erzeugte
- Die Ursache war, dass alter Packing-Code in Git vor dem Komprimieren beim Vergleichen von Dateien nur die letzten 16 Zeichen des Dateinamens betrachtete
- Normalerweise pusht Git nur den Diff geänderter Dateien, aber durch dieses Packing-Problem wurden
CHANGELOG.md-Dateien aus unterschiedlichen Paketen miteinander verglichen - Wenn zum Beispiel
repo/packages/foo/CHANGELOG.jsongeändert wurde, konnte Git bei der Vorbereitung des Pushs ein Diff gegenrepo/packages/bar/CHANGELOG.jsonerzeugen - Dadurch wurden in vielen Fällen immer wieder ganze Dateien erneut gepusht, und manche dieser Dateien konnten mehrere Dutzend MB groß sein
- Details zum Problem sind in Stolees Beitrag auf der Git-Mailingliste zusammengefasst
Ergebnisse mit repack und path-walk
- Zunächst verbesserte
git repack -adf --window=250die Komprimierung der Pack-Dateien durch ein größeres Fenster, wodurch die Repository-Größe deutlich sank - Danach ergänzte git-for-windows PR #5171 statt des bisherigen standardmäßigen Commit-Durchlaufs eine Packing-Methode, die Git-Pfade durchläuft
- Im Git-Fork von Microsoft, Microsoft git
git version 2.47.0.vfs.0.2, wurde mit der neuen Methode geklont und anschließend repack ausgeführt- Ausgeführter Befehl:
git repack -adf --path-walk - Ergebnis: Die Repository-Größe sank von 178 GB auf 5 GB
- Ausgeführter Befehl:
- Die neue Konfigurationsoption sorgt auch beim Push dafür, dass die richtigen Deltas erzeugt werden
git config --global pack.usePathWalk true
- Entwickler, die
git version 2.47.0.vfs.0.2verwenden, können nach dem lokalen Clone repack ausführen und mit dem neuen path-walk-Push-Algorithmus die Wachstumsrate senken
Auswirkungen auf GitHub, Azure DevOps und Git upstream
- GitHub führt regelmäßig repack und Git Garbage Collection aus, aber die aktuelle Packing-Methode von GitHub kann Deltas für Dateien wie
CHANGELOG.mdundCHANGELOG.jsonderzeit nicht korrekt berechnen - Betroffen sein können auch Fälle, in denen sich lange Dateinamen mit denselben letzten 16 Zeichen über längere Zeit häufig ändern
- Als Beispiel werden große String-Dateien für i18n genannt
- Das von 1JS genutzte Azure DevOps führt ein solches repack bislang noch nicht aus; es wird aber an Maßnahmen gearbeitet, um auch die Größe serverseitiger Repositories zu reduzieren
- Die zugehörigen Änderungen sollen auch in Git upstream einfließen
Was man in großen Monorepos prüfen sollte
- Wenn in großen Monorepos
CHANGELOG.mdoder andere Dateien mit relativ langen Namen wiederholt aktualisiert werden, sollte man die Änderungen rund um path-walk im Blick behalten - Der neue Befehl
git surveybietet Heuristiken, um den Zustand eines Repositorys zu beurteilen- Top Files By Disk Size
- Top Directories By Inflated Size
- Top Files By Inflated Size
- Diese Heuristiken helfen einzuschätzen, ob die path-walk-Arbeit auch die Größe dieses Repositorys beeinflussen kann
1 Kommentare
Meinungen auf Hacker News
Wo der neue Befehl
git-surveyzu finden ist: Er ist noch nicht in git.git enthalten.Der Autor verwendet Microsofts Git-Fork, und dieser Befehl wurde in diesem Sommer hinzugefügt: https://github.com/microsoft/git/pull/667
full-name-hashundpath-walkscheinen ebenfalls nur im Fork oder nur in Git HEAD vorhanden zu sein.In den Manpages oder im 2.47-Changelog sind sie nicht zu finden.
Ich frage mich, was mit „Leute in Europa können das Repository wegen seiner Größe nicht einmal klonen“ gemeint ist.
Es wirkt seltsam, dass man ein großes Repository in Europa nicht herunterladen kann, anderswo aber schon, und es klingt so, als könne man es bis zur serverseitigen Änderung weiterhin nicht klonen.
Der Satz „die gesamte Datei immer wieder neu gepusht“ scheint mittendrin abgeschnitten zu sein, und beim Lesen ist das GIF sehr ablenkend; selbst im Lesemodus bleibt es sichtbar.
Vollständiger formuliert wäre es wohl etwa: „Jemand wollte das gesamte Repository auf einen Laptop klonen, um es auf Reisen in Europa zu nutzen, konnte es damals aber nicht, weil die Festplatte voll war. Später wurde die Festplatte aufgeräumt, und das Klonen war möglich.“
Ich glaube nicht, dass damit ein großes Problem gemeint ist, dass Europa keine 180-GB-Dateien bewältigen könne.
Besonders dann, wenn die betroffene Person ein VPN nutzt, insbesondere einen dieser furchtbaren „SSL-VPN“-Workarounds; Software mit schlechtem Latenzverhalten oder Server mit kaputtem Window Scaling sind immer noch überraschend verbreitet.
Wenn die meisten Entwickler nahe am Server sitzen und Verbindungen mit guter Bandbreite nutzen, fallen solche Probleme kaum auf.
Das Klonen aus Azure DevOps lastet eine kabelgebundene Gigabit-Verbindung bei Weitem nicht aus, und weil es so lange dauert, bricht es am Ende entweder bei mir oder auf der Azure-DevOps-Seite ab, sodass ich aufgebe.
Zum Glück erledige ich fast alles mit einem Shallow Clone in Codespaces, daher ist es kein großes Problem; falls es in unserem Repository eine ähnliche Größenexplosion gibt wie beim 1JS-Repository in diesem Artikel, hoffe ich, dass sie behoben wird.
Update: Das war ein dummer Fehler, und der Dateiname enthält nicht den vollständigen Pfad
Die Erklärung scheint zwischen den GIFs untergegangen zu sein, aber die letzten 16 Zeichen unterscheiden sich hier: Gemeint ist ein Beispiel, in dem
repo/packages/foo/CHANGELOG.mdgeändert wurde, Git beim Vorbereiten des Pushs aber mitrepo/packages/bar/CHANGELOG.mdverglichen und daraus ein Diff erzeugt hatAuch der Cover Letter zur
path-walk-API ist lesenswert: https://lore.kernel.org/all/pull.1786.git.1725935335.gitgitg...Das Beispiel im Blog ist nicht besonders eindeutig, aber Git holte sich im Grunde alle Versionen aller Dateien im Repository, legte die letzten 16 Bytes des Pfads in eine Hash-Tabelle und gruppierte Dinge, von denen es annahm, dass es unterschiedliche Versionen derselben Datei seien, für die Delta-Kompression
foo/CHANGELOG.mdundbar/CHANGELOG.mdim Artikel haben nur 13 Zeichen, daher passt das Beispiel so nicht; man muss sich Pfade mit einem längeren gemeinsamen Suffix vorstellenDieser Teil wurde mit der Option
--full-name-hashbehoben, sodass nun nicht mehr 16 Bytes, sondern der gesamte Pfad verglichen wirdDie anschließend erwähnte Vergrößerung der Window-Size ist eher ein Hack, um eine schlechte Dateigruppierung zu umgehen, und kein echter Fix
Man gibt dem Kompressor weiterhin furchtbare Eingaben und umgeht das Problem mit enormem Speicherverbrauch; deshalb war es verwirrend, dass das als Lösung dargestellt wurde
Der wirklich interessante Teil sind die path-walk API und
--full-name-hashIch frage mich, ob „Dateiname“ hier den Pfad meint oder ob Git beliebige Dateien mit demselben Namen auswählt und daraus ein Diff erstellt
Außerdem würde ich gern wissen, ob es bei der Auswahl der anderen Datei ein Muster gibt
Siehe die erste Commit-Message des PR zu
--full-name-hash: https://github.com/git-for-windows/git/pull/5157/commits/d5c...Auszug:
"/CHANGELOG.json"hat 15 Zeichen und wird vom beachball-Tool erzeugt; nur der letzte Buchstabe des Elternverzeichnisses kann die unterschiedlichen Versionen dieser Datei unterscheiden, und selbst davon gehen nur die oberen 2 Bits einWenn dieses Zeichen ein Buchstabe ist, kollidiert es immer; der Pfad
"/CHANGELOG.md"hat ein ähnliches Problem, lässt aber etwas mehr Spielraum für Unterschiede im ElternverzeichnisDer Gruppierungsalgorithmus gewichtet jedes Zeichen umso geringer, je weiter es von rechts im Namen entfernt ist:
hash = (hash >> 2) + (c << 24)Der Hash ist 32 Bit breit; jedes 8-Bit-Zeichen des vollständigen Pfads wird nacheinander zu den obersten 8 Bits des Hashes addiert, während die vorhandenen Hash-Bits um 2 Bit nach rechts geschoben werden, sodass am Ende nur die letzten 16 Zeichen den Hash beeinflussen
Was tatsächlich passiert, sieht man hier: https://go.dev/play/p/JQpdUGXdQs7
Ich habe es nach Go übertragen und die Endwerte von
"aaa/CHANGELOG.md"und"zzz/CHANGELOG.md"verglichen; wenn man füraaaundzzzverschiedene Werte einsetzt, sieht man, wie wenig sie den Endwert beeinflussenDie letzten 16 Zeichen von
CHANGELOG.mdsind der gesamte Dateiname, und so interpretiert erklärt das auch, warum diefilepathwalk-Lösung das Problem behebtWenn es aber wirklich nur auf den letzten 16 Zeichen des Dateinamens basiert und nicht auf dem vollständigen Pfad, hätte dieses Problem gerade in Monorepos viel häufiger auftreten müssen
Vermutlich haben die Metavariablen
fooundbarverwirrt; die tatsächlichen Paketnamen hatten wahrscheinlich ein gemeinsames Suffix, etwafirstPkg,secondPkg"/CHANGELOG.md"allein hat schon 13 Zeichen, daher reichen bereits 3 Zeichen gemeinsames Suffix für eine 16-Zeichen-KollisionIch habe es direkt mit
nixpkgsausprobiert; wenn man es unverändert von GitHub klont, sind es etwa 5 GBDie erste im Artikel erwähnte Option,
--window 250, reduzierte die Größe auf 1,7 GB; die neue Option--path-walkaus Microsofts Git-Fork war weniger effektiv und kam insgesamt auf 1,9 GBBeides ist weniger als die Hälfte der ursprünglichen Größe, daher wäre es schön, wenn GitHub solche Vorgänge ausführen könnte; noch besser wäre es, wenn Leute Hosting so anbieten würden, dass man so etwas selbst steuern kann
Im Artikel kommt Derrick Stolee vor, der die nötigen Änderungen untersucht und veröffentlicht hat
Wenn dich Git-Interna sowie die Reduktion der Git-Klongröße lokal und in CI interessieren, hat Derrick viele hervorragende Beiträge im GitHub-Blog geschrieben: https://github.blog/author/dstolee/
Auch seine persönliche Website ist einen Blick wert: https://stolee.dev/
Wenn man versehentlich eine große Binärdatei committet hat, kann man als Lösung
filter-branchlaufen lassen, um die problematische Binärdatei zu entfernen, allen beibringen und einrichten, für Binärdateien git-lfs zu verwenden, anschließend einen Force-Push machen und die Arbeitsumgebungen aller wieder in einen sauberen Zustand bringen.Ideal ist das nicht, aber besser, als eine große Datei, die niemand nutzt, weiter in Git zu behalten.
filter-branchAngst macht, gibt es auch BFG: https://rtyley.github.io/bfg-repo-cleaner/Wie jemand anderes angemerkt hat, geht es bei diesem Problem um kleine, aber häufig geänderte Dateien. Um Speicherplatz zu sparen, sollte man also alte Versionen aus der Historie entfernen und künftig LFS verwenden.
An Git herumzuhacken mag Spaß machen, aber vielleicht gäbe es ja eine Möglichkeit, von Anfang an keine 2.500 Pakete in ein Monorepo zu packen.
Je größer eine Codebase wird, desto schwerer kann man erwarten, dass vorhandene Räder nicht neu erfunden werden, weil man entweder nicht weiß, dass der Code existiert, oder Angst hat, beim Anpassen an den eigenen Anwendungsfall bestehende Nutzer zu brechen.
Gefühlt dauert es weniger lange, von 100 auf 200 Module zu kommen, als von 50 auf 100.
Sieht komplett nach einem selbst geschaffenen Problem aus.
Ich verfolge die Entwicklungen rund um Monorepos und Skalierbarkeit, nachdem ich mir kürzlich die GitMerge-Vorträge von GitButler angesehen habe; von Microsoft, GitHub und GitLab kommt da eine Menge Gutes.
Ich würde gern besser verstehen, wie das Problem mit der Prüfung der letzten 16 Zeichen und der Prüfung des vollständigen Pfads mit Delta-Kompression, Pack-Index und Multi-Pack-Index zusammenspielt.
Schön zu sehen, dass Microsoft Azure DevOps selbst nutzt.
Es wirkt zunehmend so, als würden immer mehr Azure-Dienste nur noch native Connectoren für GitHub anbieten, daher dachte ich, Azure DevOps werde eher liegen gelassen.
Bei einem Projekt in einem Unternehmen dieser Größenordnung ist es sicher ein großer Vorteil, jemanden in Reichweite zu haben, der die internen Strukturen von Git so gut kennt und helfen kann.