Parallele Jobs und Caching
Eine zehn Minuten lange Pipeline, die fuenfzigmal am Tag laeuft, kostet ein Team ueber acht Stunden Wartezeit taeglich. Systematische Optimierung beginnt nicht mit Raten, sondern mit Messen.
Inhaltsverzeichnis
- 1. Warum sequenzielle Stages selten die tatsaechlichen Abhaengigkeiten widerspiegeln
- 2. needs: Von linearen Stages zum echten Abhaengigkeitsgraphen
- 3. parallel: Einen einzelnen Job in mehrere gleichzeitige Instanzen aufteilen
- 4. parallel:matrix: Parallelisierung ueber unterschiedliche Konfigurationen
- 5. Cache-Trefferquote messbar machen statt zu vermuten
- 6. Welche Schritte sich fuer Parallelisierung eignen und welche nicht
- 7. Fail-fast-Strategien: Teure Jobs erst nach billigen Checks starten
- 8. Messung vor und nach der Optimierung
- 9. Eine priorisierte Gesamtstrategie fuer die Praxis
- 10. Zusammenfassung
- 11. FAQ
1. Warum sequenzielle Stages selten die tatsaechlichen Abhaengigkeiten widerspiegeln
Das klassische GitLab-CI-Modell mit stages, in dem alle Jobs einer Stufe abgeschlossen sein muessen, bevor die naechste Stufe beginnt, ist einfach zu verstehen, bildet aber selten die echten Abhaengigkeiten eines Projekts ab. Ein Unit-Test-Job, der nur vom Build-Ergebnis des Backends abhaengt, wartet in diesem Modell trotzdem auf den kompletten Abschluss der Build-Stage, selbst wenn dort auch ein voellig unabhaengiger Frontend-Build laeuft, der noch fuenf Minuten laenger braucht.
Diese kuenstliche Synchronisation an Stage-Grenzen ist die haeufigste Ursache fuer unnoetig lange Pipeline-Laufzeiten. Jede Stage wartet auf den langsamsten Job innerhalb dieser Stage, selbst wenn die meisten nachfolgenden Jobs nur einen kleinen Teil der Ergebnisse dieser Stage tatsaechlich brauchen. Der erste und wirkungsvollste Optimierungsschritt besteht daher fast immer darin, die tatsaechlichen Datenabhaengigkeiten zwischen Jobs explizit zu machen, statt sich auf die implizite Stage-Reihenfolge zu verlassen.
2. needs: Von linearen Stages zum echten Abhaengigkeitsgraphen
Die needs-Direktive erlaubt es, einem Job explizit mitzuteilen, von welchen anderen Jobs er tatsaechlich abhaengt, unabhaengig davon, in welcher Stage diese liegen. GitLab startet einen Job mit needs, sobald alle dort aufgefuehrten Jobs abgeschlossen sind, statt auf das Ende der gesamten vorherigen Stage zu warten. Das verwandelt die Pipeline effektiv von einer linearen Kette in einen gerichteten azyklischen Graphen, kurz DAG, in dem unabhaengige Zweige tatsaechlich parallel statt nur scheinbar parallel innerhalb derselben Stage laufen.
Ein typisches Beispiel ist ein Projekt mit getrenntem Backend- und Frontend-Build: Mit needs kann der Backend-Test-Job direkt nach dem Backend-Build starten, ohne auf den laenger laufenden Frontend-Build zu warten, obwohl beide formal in derselben build-Stage liegen. In der Praxis reduziert das die Gesamtlaufzeit oft um mehrere Minuten, insbesondere in Pipelines mit vielen kleinen, aber ungleich langen Jobs innerhalb derselben Stage.
stages:
- build
- test
- deploy
build_backend:
stage: build
script:
- composer install --no-dev
build_frontend:
stage: build
script:
- npm ci && npm run build
test_backend:
stage: test
needs: ["build_backend"]
script:
- vendor/bin/phpunit
test_frontend:
stage: test
needs: ["build_frontend"]
script:
- npm run test
3. parallel: Einen einzelnen Job in mehrere gleichzeitige Instanzen aufteilen
Waehrend needs die Reihenfolge zwischen verschiedenen Jobs optimiert, adressiert das parallel-Keyword ein anderes Problem: einen einzelnen, langlaufenden Job in mehrere gleichzeitig laufende Instanzen aufzuteilen. Eine grosse Test-Suite mit mehreren tausend Tests, die sequenziell zwoelf Minuten braucht, kann mit parallel: 4 auf vier gleichzeitig laufende Jobs verteilt werden, von denen jeder nur ein Viertel der Tests ausfuehrt und die Gesamtlaufzeit im besten Fall auf ein Viertel reduziert, begrenzt durch die verfuegbare Runner-Kapazitaet.
Die eigentliche Herausforderung liegt in der Aufteilung der Testfaelle auf die parallelen Instanzen. Viele Test-Runner wie PHPUnit mit einem entsprechenden Plugin oder Jest unterstuetzen eine automatische Aufteilung basierend auf den CI_NODE_INDEX- und CI_NODE_TOTAL-Variablen, die GitLab jeder parallelen Instanz automatisch mitgibt. Ohne eine sinnvolle Aufteilungsstrategie, etwa gleichmaessig nach Testdauer statt nur nach Dateianzahl, kann ein einzelner besonders langsamer Testfaelle-Block eine ganze parallele Gruppe ausbremsen, waehrend die anderen drei laengst fertig sind.
test_suite:
stage: test
parallel: 4
script:
- vendor/bin/phpunit
--testsuite=integration
--order-by=random
$(vendor/bin/phpunit-split --index="$CI_NODE_INDEX" --total="$CI_NODE_TOTAL")
4. parallel:matrix: Parallelisierung ueber unterschiedliche Konfigurationen
Eine verwandte, aber konzeptionell andere Form der Parallelisierung ist parallel:matrix, bei der nicht dieselbe Aufgabe in gleiche Teile aufgeteilt wird, sondern dieselbe Aufgabe mit unterschiedlichen Parameterkombinationen gleichzeitig ausgefuehrt wird, etwa Tests gegen mehrere PHP-Versionen oder mehrere Datenbank-Engines. GitLab erzeugt dabei automatisch einen Job pro Kombination der angegebenen Variablenwerte, was die manuelle Pflege vieler fast identischer Job-Definitionen ueberfluessig macht.
Der Performance-Gewinn von parallel:matrix liegt weniger in der reinen Geschwindigkeit eines einzelnen Test-Laufs als vielmehr darin, dass Kompatibilitaetspruefungen gegen mehrere Umgebungen nicht mehr sequenziell nacheinander, sondern gleichzeitig ablaufen. Eine Pipeline, die frueher PHP 8.2, 8.3 und 8.4 nacheinander in drei getrennten, manuell gepflegten Jobs pruefte, kann mit einer einzigen matrix-Definition alle drei Versionen gleichzeitig testen und braucht dafuer im Idealfall nicht laenger als der langsamste einzelne Durchlauf.
test_php_versions:
stage: test
parallel:
matrix:
- PHP_VERSION: ["8.2", "8.3", "8.4"]
image: php:${PHP_VERSION}-cli
script:
- composer install --no-progress
- vendor/bin/phpunit
5. Cache-Trefferquote messbar machen statt zu vermuten
Bevor man Caching als Optimierungshebel einsetzt, sollte man messen, wie oft der bestehende Cache tatsaechlich greift. GitLab protokolliert im Job-Log explizit, ob ein Cache heruntergeladen wurde und unter welchem Key gesucht wurde, was sich ueber die GitLab-API auch programmatisch auswerten laesst, indem man ueber die Job-Traces mehrerer Pipeline-Laeufe iteriert und nach den charakteristischen Cache-Log-Zeilen sucht. Ohne diese Messung basiert jede Cache-Optimierung auf reiner Vermutung.
Ein einfacher, aber wirkungsvoller Ansatz ist ein woechentliches Skript, das ueber die letzten hundert Pipeline-Laeufe die Cache-Hit-Rate pro Job aggregiert und als einfache Kennzahl protokolliert. Faellt die Trefferquote fuer einen bestimmten Job deutlich unter neunzig Prozent, deutet das fast immer auf einen zu granularen oder falsch konfigurierten cache:key hin, etwa einen Key, der unnoetig auch Umgebungsvariablen einbezieht, die sich staendig aendern, obwohl der eigentliche Cache-Inhalt stabil bleiben koennte.
6. Welche Schritte sich fuer Parallelisierung eignen und welche nicht
Nicht jeder Pipeline-Schritt profitiert von Parallelisierung. Gut geeignet sind Schritte, die voneinander unabhaengige Datensaetze verarbeiten, etwa Testfaelle ohne geteilten Zustand, unabhaengige Linter fuer verschiedene Sprachen im selben Repository, oder Kompatibilitaetstests gegen mehrere Versionen einer Abhaengigkeit. Diese Schritte lassen sich mechanisch aufteilen, ohne dass die Teilergebnisse sich gegenseitig beeinflussen oder eine bestimmte Reihenfolge einhalten muessen.
Schlecht geeignet sind dagegen Schritte mit gemeinsamem, veraenderlichem Zustand, etwa Integrationstests, die alle gegen dieselbe Datenbank schreiben, ohne dass diese pro paralleler Instanz isoliert wird, oder Deploy-Schritte, die zwingend eine bestimmte Reihenfolge einhalten muessen, etwa erst Datenbank-Migration, dann Anwendungs-Deploy. Ein haeufiger Parallelisierungsfehler ist, solche Schritte trotzdem naiv mit parallel zu versehen, was zu inkonsistenten, schwer reproduzierbaren Fehlschlaegen fuehrt, wenn zwei parallele Instanzen gleichzeitig dieselbe Ressource manipulieren.
7. Fail-fast-Strategien: Teure Jobs erst nach billigen Checks starten
Ein oft uebersehener Hebel fuer kuerzere Pipeline-Laufzeit im Durchschnitt ist nicht mehr Parallelisierung, sondern eine bewusste Reihenfolge, die schnelle, billige Pruefungen vor langsame, teure Jobs stellt. Ein Syntax-Check oder Linter, der in wenigen Sekunden laeuft, sollte vor einem zehnminuetigen Integrationstest liegen und diesen ueber needs blockieren, damit ein offensichtlicher Syntaxfehler die Pipeline sofort abbricht, statt erst nach zehn Minuten teurer Testlaufzeit sichtbar zu werden.
Diese Strategie reduziert nicht die Laufzeit erfolgreicher Pipelines, wohl aber die durchschnittliche Laufzeit ueber alle Pipeline-Laeufe hinweg, da fehlerhafte Commits, die ohnehin einen erheblichen Anteil aller Pipeline-Ausloesungen ausmachen, viel frueher abgebrochen werden. Kombiniert mit fail_fast auf Ebene der parallel-Jobs, das bei einem fehlgeschlagenen parallelen Job die uebrigen sofort abbricht statt sie zu Ende laufen zu lassen, summiert sich das ueber viele Pipeline-Laeufe zu spuerbar geringerem Gesamt-Runner-Verbrauch.
lint:
stage: check
script:
- php -l src/
- vendor/bin/phpcs
test_suite:
stage: test
needs: ["lint"]
parallel: 4
script:
- vendor/bin/phpunit --order-by=random
8. Messung vor und nach der Optimierung
Jede Optimierung sollte an einer klaren Kennzahl gemessen werden, typischerweise der durchschnittlichen Pipeline-Laufzeit ueber einen repraesentativen Zeitraum von mindestens zwei Wochen, nicht an einem einzelnen guenstigen oder unguenstigen Lauf. GitLab bietet dafuer unter Analytics, CI/CD Analytics, eine Grundauswertung, die sich fuer tiefere Analysen ueber die GraphQL- oder REST-API zusaetzlich nach Job statt nur nach Gesamtpipeline aufschluesseln laesst.
Ein methodisches Vorgehen aendert immer nur eine Variable gleichzeitig, etwa zuerst nur needs einfuehren und die Laufzeit ueber zwei Wochen beobachten, bevor zusaetzlich parallel fuer die Test-Suite aktiviert wird. Werden mehrere Aenderungen gleichzeitig vorgenommen, laesst sich im Nachhinein kaum noch zuverlaessig zuordnen, welche Massnahme den groessten Beitrag zur Verbesserung geleistet hat, was zukuenftige Optimierungsentscheidungen erschwert.
9. Eine priorisierte Gesamtstrategie fuer die Praxis
Fuer die meisten Projekte lohnt sich eine feste Reihenfolge bei der Optimierung: zuerst needs einfuehren, um kuenstliche Stage-Synchronisation zu beseitigen, danach die Cache-Trefferquote messen und cache:key gezielt verbessern, erst danach parallel oder parallel:matrix fuer tatsaechlich rechenintensive, unabhaengige Schritte einsetzen, und zum Schluss Fail-fast-Reihenfolgen fuer den Durchschnittsfall ueber alle Pipeline-Laeufe optimieren. Diese Reihenfolge liefert die groessten Verbesserungen mit dem geringsten Konfigurationsaufwand zuerst.
Die folgende Tabelle fasst die vorgestellten Mechanismen zusammen und ordnet sie danach ein, wie gross der typische Laufzeitgewinn im Verhaeltnis zum Konfigurationsaufwand ausfaellt, um bei begrenzter Zeit die wirkungsvollsten Massnahmen zuerst umzusetzen.
| Mechanismus | Loest welches Problem | Typischer Laufzeitgewinn | Aufwand |
|---|---|---|---|
| needs (DAG) | Kuenstliche Stage-Wartezeit | Mehrere Minuten pro Pipeline | Gering |
| parallel | Ein langsamer Job | Bis zu Faktor N bei N Instanzen | Mittel (Testaufteilung noetig) |
| parallel:matrix | Sequenzielle Kompatibilitaetstests | Faktor Anzahl Kombinationen | Gering bis mittel |
| cache:key-Tuning | Unnoetige Reinstalls | Abhaengig von Trefferquote | Gering, aber Messung noetig |
Mironsoft
CI/CD-Pipelines, Zero-Downtime-Deployments und Release-Automatisierung
Deployments, die ohne Ausfallzeit und ohne Nervenkitzel laufen?
Wir prüfen bestehende GitLab-Pipelines auf fragile Deployment-Schritte und fehlende Absicherung und bauen daraus einen Release-Prozess mit Zero-Downtime-Deployments, automatisierten Checks und einem Rollback, dem ihr im Ernstfall vertrauen könnt.
Pipeline-Review
Bestehende .gitlab-ci.yml auf Fragilität, fehlende Stages und Sicherheitslücken prüfen.
Zero-Downtime-Deployment
Symlink-Releases, Health-Checks und Rollback-Strategien für Magento-Shops aufbauen.
CI/CD-Automatisierung
Tests, Security-Scans und Deployments zu einer zuverlässigen Pipeline verbinden.
10. Zusammenfassung
Pipeline-Laufzeit optimieren: Das Wichtigste auf einen Blick
needs
Verwandelt lineare Stages in einen echten Abhaengigkeitsgraphen, unabhaengige Jobs laufen parallel.
parallel
Teilt einen langsamen Job in mehrere gleichzeitige Instanzen, Testaufteilung entscheidet ueber Wirkung.
Cache-Messung
Trefferquote ueber Job-Logs auswerten statt Cache-Wirkung nur zu vermuten.
Fail-fast
Billige Checks vor teure Jobs stellen senkt die durchschnittliche Laufzeit ueber alle Laeufe.