Wenn Git bei Millionen Dateien in die Knie geht
Ein Monorepo mit mehreren Millionen Dateien und einer jahrelangen Historie bringt Git-Befehle, die in einem kleinen Repository sofort reagieren, spürbar ins Schwitzen. Dieser Artikel zeigt, mit welchen konkreten Funktionen sich Clone-Zeit, Checkout-Größe und die Reaktionszeit von Status-Abfragen auch bei extremer Repository-Größe wieder in den Griff bekommen lassen.
Inhaltsverzeichnis
- 1. Wo Git bei sehr großen Repositories an Grenzen stößt
- 2. Partial Clone: nicht jede Datei sofort herunterladen
- 3. Sparse Checkout: nur den relevanten Teilbaum auschecken
- 4. Commit-Graph und Bloom-Filter für schnellere Historien-Abfragen
- 5. File System Monitor: Statusabfragen ohne vollständigen Scan
- 6. Multi-Pack-Index und automatisierte Wartung
- 7. Protocol v2 und serverseitige Faktoren
- 8. Shallow Clone in der CI: Trade-offs bewusst abwägen
- 9. Verbesserungen messen statt nur vermuten
- 10. Zusammenfassung
- 11. FAQ
1. Wo Git bei sehr großen Repositories an Grenzen stößt
Git wurde ursprünglich für den Linux-Kernel entworfen und skaliert von Haus aus erstaunlich gut, doch ein Monorepo mit mehreren Millionen Dateien, Zehntausenden Branches und einer Historie über viele Jahre stellt andere Anforderungen als ein klassisches Open-Source-Projekt. Drei Symptome tauchen dabei typischerweise zuerst auf: ein initialer Clone, der Stunden statt Sekunden dauert, ein Arbeitsverzeichnis, das mehr Speicherplatz belegt als jeder Entwickler tatsächlich braucht, und ein git status, der spürbar länger braucht als ein einzelner Tastendruck erwarten lässt.
Alle drei Symptome haben eine gemeinsame Ursache: Git ist von seinem Grundmodell her darauf ausgelegt, dass jeder Klon die komplette Historie und jeder Checkout den kompletten Dateibaum enthält. Bei einem Monorepo, in dem ein einzelnes Team nur einen kleinen Ausschnitt des Codes tatsächlich benötigt, wird dieses Modell zum Performance-Problem, das sich nicht durch schnellere Hardware allein lösen lässt, sondern gezielte Konfiguration erfordert.
2. Partial Clone: nicht jede Datei sofort herunterladen
Partial Clone erlaubt es, beim Klonen bestimmte Objekte auszulassen und erst bei tatsächlichem Bedarf vom Server nachzuladen. Mit dem Filter blob:none werden zunächst nur die Commit- und Tree-Objekte heruntergeladen, während die eigentlichen Dateiinhalte, die Blobs, erst abgerufen werden, wenn ein Checkout, ein Diff oder ein Blame sie tatsächlich benötigt. Das reduziert die initiale Clone-Größe drastisch, weil die Historie über Jahre meist um ein Vielfaches größer ist als der aktuelle Arbeitsstand.
Der Filter blob:limit=1m ist eine Variante, die nur Blobs oberhalb einer bestimmten Größe auslässt, was sich anbietet, wenn vor allem einzelne große Binärdateien das Problem verursachen und der restliche Quellcode überschaubar bleibt. Wichtig ist, dass der Server, meist über uploadpack.allowFilter, Partial Clone explizit unterstützen muss, was bei modernen Hosting-Lösungen wie GitHub, GitLab und Gerrit standardmäßig der Fall ist.
# Nur Commits und Trees initial laden, Blobs on demand
git clone --filter=blob:none https://example.com/monorepo.git
# Nur Blobs unter 1 MB initial laden
git clone --filter=blob:limit=1m https://example.com/monorepo.git
# Fehlende Objekte werden bei Bedarf automatisch nachgeladen
git checkout feature/payment-service
3. Sparse Checkout: nur den relevanten Teilbaum auschecken
Während Partial Clone die Historie reduziert, sorgt Sparse Checkout dafür, dass nur ein bestimmter Teilbaum des Dateisystems tatsächlich im Arbeitsverzeichnis materialisiert wird. Im modernen cone-Modus wird eine Liste von Verzeichnissen angegeben, und Git checkt ausschließlich diese Pfade sowie alle Dateien im Wurzelverzeichnis aus, während der Rest im internen Index verwaltet, aber nicht auf die Festplatte geschrieben wird.
Für ein Team, das nur an einem Microservice innerhalb eines riesigen Monorepos arbeitet, reduziert das die Anzahl der Dateien im Arbeitsverzeichnis von mehreren Millionen auf wenige Tausend, was sich direkt auf die Geschwindigkeit von git status, git add und der Datei-Indizierung durch den Editor auswirkt. Der Cone-Modus ist dabei deutlich performanter als der ältere, musterbasierte Sparse-Checkout-Modus, weil Git im Cone-Modus mit einfachen Verzeichnispräfixen statt mit beliebigen Glob-Mustern arbeitet.
git sparse-checkout init --cone
git sparse-checkout set services/payment services/shared-libs
# Arbeitsverzeichnis enthält jetzt nur diese Pfade plus Root-Dateien
ls
4. Commit-Graph und Bloom-Filter für schnellere Historien-Abfragen
Befehle wie git log, git merge-base oder git blame müssen die Beziehungen zwischen Commits nachvollziehen, was bei Millionen von Commit-Objekten bedeutet, dass Git für jede Abfrage viele einzelne Objekte vom Datenträger lesen muss. Die Datei commit-graph speichert diese Beziehungen in einem kompakten, vorberechneten Format, sodass Git die Historie durchlaufen kann, ohne jedes Commit-Objekt einzeln zu deserialisieren.
Bloom-Filter erweitern den Commit-Graph um eine schnelle Antwort auf die Frage, ob ein bestimmter Pfad sich in einem Commit überhaupt geändert hat, bevor Git den tatsächlichen Diff berechnet. Das beschleunigt vor allem eine pfadgefilterte Historienabfrage wie git log mit einem angehängten Pfad in einem Monorepo massiv, weil Git ohne Bloom-Filter für jeden einzelnen Commit in der gesamten Historie prüfen müsste, ob der Pfad betroffen war.
git commit-graph write --reachable --changed-paths
# Automatisch bei jedem Fetch aktuell halten
git config fetch.writeCommitGraph true
5. File System Monitor: Statusabfragen ohne vollständigen Scan
Ohne weitere Hilfe muss git status jede Datei im Arbeitsverzeichnis einzeln auf Änderungen prüfen, was bei Millionen Dateien selbst auf schneller SSD spürbar dauert. Der eingebaute File System Monitor, fsmonitor, lauscht stattdessen kontinuierlich auf Dateisystem-Ereignisse des Betriebssystems und liefert Git eine bereits vorgefilterte Liste geänderter Dateien, wodurch der teure vollständige Scan entfällt.
Seit Git 2.37 ist ein plattformübergreifender fsmonitor als eingebautes Daemon-Feature verfügbar, das ohne externe Abhängigkeit auf macOS, Linux und Windows funktioniert und sich pro Repository mit einer einzigen Konfigurationszeile aktivieren lässt. In Kombination mit Sparse Checkout, das die Anzahl der zu überwachenden Dateien ohnehin reduziert, ergibt sich der größte Effekt.
git config core.fsmonitor true
# Status des Daemons pruefen
git fsmonitor--daemon status
6. Multi-Pack-Index und automatisierte Wartung
Ein aktives Repository sammelt über die Zeit viele einzelne Pack-Dateien an, weil Git bei jedem fetch ein neues Pack anlegt. Ohne Wartung muss Git bei einer Objektsuche potenziell jede dieser Pack-Dateien einzeln durchsuchen. Der Multi-Pack-Index, kurz MIDX, fasst die Indizes mehrerer Pack-Dateien in einer einzigen, kompakten Struktur zusammen, sodass eine Objektsuche unabhängig von der Anzahl der Pack-Dateien schnell bleibt.
Der Befehl git maintenance bündelt Commit-Graph-Aktualisierung, Multi-Pack-Index-Pflege, Objekt-Komprimierung und weitere Aufräumarbeiten in einem planbaren Hintergrunddienst, der über git maintenance start als geplante Aufgabe im Betriebssystem eingerichtet wird. Statt eines manuellen, gelegentlichen git gc, das bei einem großen Repository Minuten oder Stunden blockieren kann, laufen die Wartungsaufgaben dann in kleinen, unauffälligen Intervallen im Hintergrund.
# Wartung als Hintergrunddienst einrichten (cron/launchd/Task Scheduler)
git maintenance start
# Aufgaben, die dabei automatisch geplant werden
git maintenance run --task=commit-graph
git maintenance run --task=pack-refs
git maintenance run --task=incremental-repack
7. Protocol v2 und serverseitige Faktoren
Neben der Client-Konfiguration spielt auch das verwendete Git-Protokoll eine Rolle. Protocol v2 verhandelt Referenzen effizienter als das ältere Protocol v0, weil der Server bei einem Fetch nicht mehr zwangsläufig alle Referenzen des Repositories auflisten muss, sondern gezielt nach angeforderten Referenzen filtern kann. Bei einem Monorepo mit Zehntausenden Branches und Tags reduziert das den Overhead jeder einzelnen Fetch-Operation spürbar.
Serverseitig lohnt es sich zusätzlich, die Bandbreite und CPU-Last für uploadpack Operationen zu überwachen, weil ein Monorepo mit vielen gleichzeitigen CI-Klonen die Servergrenzen deutlich schneller erreicht als ein kleines Repository. Ein dedizierter Cache-Server oder ein Geo-Replikat, wie es GitHub Enterprise und GitLab Geo anbieten, entlastet den primären Server zusätzlich bei global verteilten Teams.
git config protocol.version 2
8. Shallow Clone in der CI: Trade-offs bewusst abwägen
In CI-Pipelines, die nur den aktuellen Stand eines Branches bauen und testen müssen, ist ein flacher Klon mit --depth=1 oft die schnellste Option, weil keine Historie übertragen wird. Der Nachteil zeigt sich, sobald ein Job tatsächlich Historie benötigt, etwa für git describe, einen Vergleich gegen den letzten erfolgreichen Build oder ein Blame in einem Analyse-Schritt, weil ein flacher Klon diese Operationen entweder verweigert oder falsche Ergebnisse liefert.
Eine pragmatische Zwischenlösung kombiniert einen flachen Klon mit einem gezielten git fetch --deepen=50 oder --unshallow genau dann, wenn ein bestimmter Job mehr Historie braucht, statt pauschal für jeden Job die volle Tiefe zu übertragen. Für Monorepos mit vielen parallelen CI-Jobs macht diese Differenzierung einen messbaren Unterschied bei der Gesamt-Pipeline-Laufzeit.
9. Verbesserungen messen statt nur vermuten
Jede der genannten Maßnahmen sollte vor und nach der Einführung mit konkreten Zeitmessungen belegt werden, weil sich der Nutzen je nach Repository-Struktur stark unterscheidet. Der eingebaute Trace2-Mechanismus von Git, aktiviert über GIT_TRACE2_PERF=1, liefert detaillierte Zeitangaben für einzelne interne Operationen und zeigt genau, welcher Schritt eines Befehls tatsächlich die Zeit kostet.
In der Praxis empfiehlt sich ein kleines Set an Referenzbefehlen, etwa ein frischer Clone, ein git status nach einem größeren Merge und eine pfadgefilterte git log Abfrage auf ein häufig geändertes Verzeichnis, die regelmäßig mit derselben Methode gemessen werden. Nur so lässt sich objektiv beurteilen, ob eine neue Git-Version oder eine geänderte Konfiguration tatsächlich eine Verbesserung bringt oder nur subjektiv schneller wirkt.
GIT_TRACE2_PERF=1 git status 2> trace-status.log
tail -n 40 trace-status.log
| Maßnahme | Löst primär | Client oder Server | Aufwand |
|---|---|---|---|
| Partial Clone (blob:none) | Große initiale Clone-Zeit | Beide, Server muss Filter erlauben | Gering |
| Sparse Checkout (cone) | Zu viele Dateien im Arbeitsverzeichnis | Client | Gering |
| Commit-Graph + Bloom-Filter | Langsame Log/Blame-Abfragen | Client, automatisierbar | Gering |
| fsmonitor | Langsames git status | Client | Gering |
| git maintenance | Viele Pack-Dateien, langsame Objektsuche | Client, im Hintergrund | Mittel |
Mironsoft
Git-Workflows, Branching-Strategien und CI-Hooks
Chaotische Git-Historie und unklare Branching-Regeln im Team?
Wir richten saubere Git-Workflows ein, klären Branching-Strategien fürs Team und automatisieren Qualitätschecks über Git-Hooks und CI-Pipelines, damit die Historie nachvollziehbar bleibt.
Workflow-Audit
Bestehende Branching-Strategie und Merge-Praxis auf Schwachstellen prüfen.
Hook-Automatisierung
Pre-Commit- und Pre-Push-Hooks für Linting, Tests und Commit-Konventionen einrichten.
Team-Schulung
Rebase, Cherry-Pick und Konfliktauflösung im Team praxisnah vermitteln.
10. Zusammenfassung
Monorepo-Performance
Clone-Zeit
Partial Clone mit --filter=blob:none reduziert initiale Übertragung
Arbeitsverzeichnis
Sparse Checkout im cone-Modus checkt nur benötigte Pfade aus
Historien-Abfragen
Commit-Graph mit Bloom-Filtern beschleunigt log und blame
Wartung
git maintenance start automatisiert Pflege im Hintergrund