in großen Teams gezielt steuern
Wer in einem größeren Team mit GitLab CI/CD arbeitet, kennt das Phänomen: Jobs landen in der Queue, obwohl die Pipeline schon längst abgeschickt wurde. Die Ursache liegt fast immer in zu niedrig konfigurierten concurrent-Limits, fehlenden Runner-Tags oder unausgewogener Lastverteilung zwischen Build-, Test- und Deploy-Jobs.
Inhaltsverzeichnis
- 1. Das Queue-Problem in wachsenden Teams
- 2. concurrent und limit: das Herzstück der Kapazitätssteuerung
- 3. Runner-Tags als Routing-Mechanismus
- 4. job_policy und fair scheduling zwischen Projekten
- 5. Horizontale Skalierung: wann ein zweiter Runner sinnvoll ist
- 6. Vollständige config.toml für Magento-Build-Runner
- 7. Queue-Zeiten beobachten und auswerten
- 8. Vergleich der Strategien
- 9. Typische Fehler beim Kapazitätsmanagement
- 10. Zusammenfassung
- 11. FAQ
1. Das Queue-Problem in wachsenden Teams
Wenn ein Team wächst und mehr Entwickler gleichzeitig Merge Requests öffnen, treffen Pipeline-Starts aufeinander. GitLab verteilt Jobs an verfügbare Runner — sind alle Runner ausgelastet, landen neue Jobs in der Queue. Die Queue-Zeit ist die Zeitspanne zwischen dem Trigger eines Jobs und seinem tatsächlichen Start. In kleinen Teams mit wenigen gleichzeitigen Pipelines bleibt diese Zeit gering. In Teams mit zehn oder mehr aktiven Entwicklern kann die Queue-Zeit die eigentliche Ausführungszeit eines Jobs überschreiten, was Feedback-Zyklen empfindlich verlängert.
Das Problem ist in Magento-Projekten besonders ausgeprägt: Build-Jobs mit composer install, setup:di:compile und Tailwind-Build laufen häufig vier bis acht Minuten. Wenn drei Teams gleichzeitig auf denselben Runner angewiesen sind, warten die letzten Jobs nicht Sekunden, sondern Minuten, bevor sie überhaupt starten. Das Ergebnis ist ein künstlich verlängerter Deployment-Zyklus, obwohl die Server-Hardware theoretisch ausreichen würde. Die Lösung liegt nicht immer in mehr Hardware, sondern zunächst in der richtigen Kapazitätskonfiguration des bestehenden Runners.
Die zwei wichtigsten Stellschrauben sind concurrent auf Runner-Ebene und limit auf Executor-Ebene in der config.toml. Daneben spielen Tags, fair scheduling und die Struktur der Pipelines selbst eine Rolle. Dieser Artikel zeigt, wie man diese Stellschrauben gezielt einsetzt, um Queue-Zeiten für ein wachsendes Magento-Team messbar zu reduzieren.
2. concurrent und limit: das Herzstück der Kapazitätssteuerung
Die globale concurrent-Einstellung in /etc/gitlab-runner/config.toml gibt an, wie viele Jobs der Runner-Prozess gleichzeitig ausführen darf — über alle registrierten Runner-Instanzen hinweg. Der Standardwert nach der Installation ist meistens 1, was bedeutet, dass der Runner jeden Job sequenziell abarbeitet. Für einen Server mit acht CPU-Kernen und 32 GB RAM ist dieser Wert drastisch zu niedrig. Ein realistischer Ausgangspunkt für einen dedizierten Build-Server ist concurrent = 4, also vier parallel laufende Jobs, da Magento-Builds durch composer install und setup:di:compile speicherintensiv sind.
Der pro-Runner-Wert limit im [[runners]]-Block begrenzt, wie viele Jobs eine einzelne Runner-Instanz gleichzeitig übernehmen darf. Wenn concurrent = 4 und zwei Runner mit limit = 2 registriert sind, können in der Summe vier Jobs laufen, je zwei pro Runner-Instanz. Dieses Modell erlaubt es, verschiedene Runner-Instanzen für verschiedene Aufgaben zu konfigurieren — etwa einen Runner mit hohem Limit für schnelle Test-Jobs und einen Runner mit niedrigem Limit für ressourcenintensive Build-Jobs — ohne den globalen concurrent-Wert übermäßig zu erhöhen.
# /etc/gitlab-runner/config.toml
# Global concurrent: maximum simultaneous jobs across all runners on this host
concurrent = 6
check_interval = 3
shutdown_timeout = 30
[[runners]]
name = "magento-build-runner"
url = "https://gitlab.example.com"
token = "RUNNER_TOKEN_BUILD"
executor = "docker"
# Limit for this runner instance only
limit = 3
[runners.docker]
image = "php:8.4-cli"
volumes = ["/cache:/cache", "/composer-cache:/root/.composer"]
memory = "4g"
cpus = "2"
[[runners]]
name = "magento-deploy-runner"
url = "https://gitlab.example.com"
token = "RUNNER_TOKEN_DEPLOY"
executor = "shell"
# Deploy runner: only one job at a time to avoid concurrent symlink switches
limit = 1
[runners.custom]
run_exec = ""
Ein häufiger Fehler ist es, concurrent zu erhöhen, ohne die verfügbaren Systemressourcen zu berücksichtigen. Wenn fünf parallele composer install-Prozesse gleichzeitig laufen und jeder davon 2 GB RAM benötigt, gerät ein Server mit 8 GB RAM in Swap-Bereiche, was alle Jobs verlangsamt statt sie zu beschleunigen. Die richtige Formel: concurrent = verfügbarer RAM / durchschnittlicher RAM-Bedarf pro Job, abzüglich eines Puffers für das Betriebssystem.
3. Runner-Tags als Routing-Mechanismus
Runner-Tags sind in GitLab CI/CD das primäre Mittel, um Jobs gezielt an bestimmte Runner zu routen. Ohne Tags übernimmt jeder ungetaggte Runner jeden ungetaggten Job — was in einer gemischten Umgebung aus Build-, Test- und Deploy-Runnern zu einer unerwünschten Durchmischung führt. Ein Deploy-Runner, der versehentlich einen ressourcenintensiven Build-Job übernimmt, bindet seine limit-Kapazität und verzögert Deploy-Jobs, die auf denselben Runner angewiesen sind.
Das empfohlene Muster für große Teams ist eine klare Tag-Hierarchie: build für Compiler und Asset-Build-Jobs, test für PHPUnit und PHPStan, deploy für SSH-basierte Deployments, docker für Jobs, die einen Docker-Executor benötigen, und shell für Jobs mit direktem Serverzugriff. Jeder Runner wird mit einem oder mehreren dieser Tags registriert, und jeder Job in der .gitlab-ci.yml deklariert seinen tags-Block. So landet jeder Job garantiert beim richtigen Runner-Typ, und die Queue-Zeiten für kritische Deploy-Jobs sind von der Last der Build-Jobs entkoppelt.
4. job_policy und fair scheduling zwischen Projekten
In Umgebungen, in denen mehrere Magento-Projekte denselben GitLab-Runner teilen, entsteht das Problem der Job-Fairness: Ein Projekt, das viele parallele Pipelines auslöst, kann den Runner für andere Projekte dauerhaft blockieren. GitLab bietet mit dem fair scheduling-Algorithmus auf Instanz-Ebene eine automatische Ausbalancierung: Der GitLab-Server verteilt Jobs nicht in strikter FIFO-Reihenfolge, sondern bevorzugt Projekte mit weniger aktuell laufenden Jobs. Das reduziert Starvation, bei der ein Projekt lange wartet, weil ein anderes Projekt die Runner-Kapazität dominiert.
Auf Runner-Ebene kann man zusätzlich mit dem maximum_timeout-Wert im [[runners]]-Block dafür sorgen, dass hängende Jobs automatisch abgebrochen werden und die Kapazität freigeben. Ein typischer Wert für Magento-Build-Jobs beträgt 1800 Sekunden (30 Minuten). Jobs, die länger laufen, sind fast immer ein Symptom eines Problems — etwa ein blockierter composer install oder ein fehlgeschlagener Frontend-Build, der auf Benutzereingabe wartet und nie abbricht. Mit einem definierten Timeout gibt der Runner die Ressource kontrolliert frei, statt sie dauerhaft zu belegen.
5. Horizontale Skalierung: wann ein zweiter Runner sinnvoll ist
Horizontale Skalierung — also das Hinzufügen weiterer Runner-Hosts — ist die richtige Maßnahme, wenn die vertikale Kapazität des bestehenden Servers ausgeschöpft ist. Der Indikator dafür ist nicht allein die hohe CPU- oder RAM-Auslastung, sondern eine stabil hohe Queue-Zeit trotz eines optimierten concurrent-Werts. Wenn Jobs im Schnitt zwei Minuten in der Queue warten, bevor sie starten, und alle verfügbaren Ressourcen des Runners ausgelastet sind, ist ein zweiter Runner-Host sinnvoller als weiteres Tuning.
GitLab unterstützt dabei mehrere Muster: Mehrere physische oder virtuelle Hosts, auf denen jeweils ein gitlab-runner-Prozess läuft und mit denselben Tags registriert ist. GitLab verteilt Jobs automatisch zwischen diesen Runnern. Alternativ bieten Cloud-Autoscaler (AWS EC2 Autoscaler, Google Cloud Autoscaler) die Möglichkeit, Runner-Instanzen dynamisch nach Bedarf zu starten und zu beenden. Für Magento-Teams mit vorhersehbarer Arbeitslast — zum Beispiel mit definierten Sprint-Zyklen — ist eine statische zweite Runner-Maschine oft die einfachere und kostengünstigere Lösung.
6. Vollständige config.toml für Magento-Build-Runner
Die folgende Konfiguration zeigt eine produktionsnahe config.toml für einen dedizierten Magento-Build-Runner mit Docker-Executor. Sie berücksichtigt Concurrency, Ressourcenlimits, Cache-Volumes, Netzwerkkonfiguration und einen separaten Deploy-Runner ohne Concurrency-Problem. Die Kommentare erklären die Entscheidungen hinter jedem Parameter.
# /etc/gitlab-runner/config.toml
# Global limit: maximum jobs across all [[runners]] on this host
concurrent = 5
check_interval = 5
shutdown_timeout = 60
log_level = "info"
log_format = "json"
[[runners]]
name = "magento-build-php84"
url = "https://gitlab.example.com"
token = "${GITLAB_RUNNER_TOKEN_BUILD}"
executor = "docker"
# Max 3 concurrent build jobs on this runner instance
limit = 3
# Job timeout: abort after 25 minutes
maximum_timeout = 1500
# Route only jobs tagged with 'build' and 'php84'
tag_list = ["build", "php84", "docker"]
[runners.cache]
Type = "local"
Path = "/runner-cache"
Shared = false
[runners.docker]
tls_verify = false
image = "php:8.4-cli"
privileged = false
# Bind-mount Composer cache to speed up repeated builds
volumes = [
"/srv/composer-cache:/root/.composer:rw",
"/srv/npm-cache:/root/.npm:rw",
"/runner-cache:/cache"
]
shm_size = 0
memory = "4096m"
memory_swap = "4096m"
cpus = "2.0"
network_mode = "bridge"
[[runners]]
name = "magento-deploy-ssh"
url = "https://gitlab.example.com"
token = "${GITLAB_RUNNER_TOKEN_DEPLOY}"
executor = "shell"
# Deploy: only one job at a time to prevent concurrent symlink switches
limit = 1
maximum_timeout = 900
tag_list = ["deploy", "production", "shell"]
# Shell executor uses the gitlab-runner user's environment
shell = "bash"
Wichtig bei dieser Konfiguration: Der Build-Runner verwendet Docker-Executor mit Ressourcenlimits pro Container, um sicherzustellen, dass einzelne Jobs nicht die gesamte Host-Kapazität in Beschlag nehmen. Der Deploy-Runner läuft mit Shell-Executor und strikt limit = 1, weil parallele Deployments zu Race Conditions beim Symlink-Wechsel führen würden. Das log_format = "json" auf globaler Ebene erlaubt das strukturierte Parsing der Runner-Logs in Monitoring-Systemen wie Grafana Loki oder Elasticsearch.
7. Queue-Zeiten beobachten und auswerten
GitLab selbst bietet in der Instanz-Administration unter Admin Area → Monitoring → GitLab Runner eine Übersicht über aktiv laufende Jobs und wartende Jobs. Diese Ansicht reicht für Troubleshooting aus, ist aber für kontinuierliches Monitoring nicht geeignet, weil sie keine historischen Daten speichert. Für dauerhaftes Kapazitäts-Monitoring gibt es zwei praxiserprobte Ansätze: den GitLab Prometheus-Endpunkt und die GitLab API.
GitLab exportiert Runner-Metriken unter /metrics, darunter gitlab_runner_jobs_total, gitlab_runner_jobs{state="running"} und gitlab_runner_jobs{state="waiting"}. Diese Metriken lassen sich in Prometheus scrapen und in Grafana visualisieren. Ein einfaches Alerting-Regel könnte so aussehen: Wenn gitlab_runner_jobs{state="waiting"} > 5 für mehr als fünf Minuten, sendet Alertmanager eine Benachrichtigung. Das ist das Frühwarnsignal dafür, dass die Kapazität nicht mit der Last Schritt hält und eine Anpassung des concurrent-Werts oder ein weiterer Runner-Host erforderlich ist.
8. Vergleich der Strategien zur Queue-Reduzierung
Es gibt mehrere Stellschrauben für kürzere Queue-Zeiten, und die richtige Wahl hängt vom konkreten Engpass ab. Eine pauschale Erhöhung von concurrent hilft nur, wenn Systemressourcen noch verfügbar sind. Tag-Routing hilft nur, wenn Jobs falsch verteilt werden. Horizontale Skalierung hilft nur, wenn der Engpass wirklich in der Kapazität liegt und nicht in der Pipeline-Struktur selbst.
| Strategie | Wann sinnvoll | Risiko | Aufwand |
|---|---|---|---|
| concurrent erhöhen | Ressourcen noch frei, Standardwert zu niedrig | OOM bei zu aggressiver Erhöhung | Gering |
| Runner-Tags einführen | Deploy-Runner übernimmt Build-Jobs | Fehlkonfiguration blockiert Jobs komplett | Mittel |
| maximum_timeout setzen | Hängende Jobs blockieren Kapazität | Legitime lange Jobs werden abgebrochen | Gering |
| Zweiter Runner-Host | Erster Host voll ausgelastet | Infrastrukturkosten, Wartungsaufwand | Hoch |
| Pipeline-Struktur optimieren | Jobs laufen länger als nötig | Erfordert tiefes Pipeline-Verständnis | Mittel–Hoch |
In der Praxis ist der erste Schritt immer die Messung: Wie lang ist die durchschnittliche Queue-Zeit pro Job-Typ? Wo liegt der Engpass — beim Build-Runner, beim Deploy-Runner oder beim Test-Runner? Erst wenn diese Fragen datenbasiert beantwortet sind, lohnt es sich, eine der obigen Strategien gezielt anzuwenden. Blindes Erhöhen von concurrent ohne Monitoring führt regelmäßig zu OOM-Situationen, die alle laufenden Jobs abbrechen und die Queue-Zeit für den gesamten Abend verlängern.
9. Typische Fehler beim Kapazitätsmanagement
Der häufigste Fehler ist ein zu hoher concurrent-Wert ohne angepasste Docker-Ressourcenlimits. Wenn zehn parallele Jobs ohne Memory-Limit laufen und einer davon einen Speicherleck hat, wächst der Speicherbedarf unkontrolliert, bis der OOM-Killer des Kernels Jobs beendet. Die Lösung: immer memory und memory_swap im [runners.docker]-Block setzen, sodass ein einzelner Job nur das ihm zugewiesene RAM nutzen kann.
Ein zweiter Fehler ist das Vergessen von Tags beim Registrieren eines neuen Runners. Ein Runner ohne Tags übernimmt alle ungetaggten Jobs — auch Jobs aus Projekten, für die dieser Runner eigentlich nicht gedacht war. In einer Umgebung mit mehreren Projekten führt das dazu, dass ein Deploy-Runner für Projekt A versehentlich Build-Jobs von Projekt B übernimmt. Durch konsequente Tag-Strategie und die Runner-Einstellung run_untagged = false im [[runners]]-Block wird dieses Problem strukturell ausgeschlossen. Ein dritter Fehler ist das Ignorieren des maximum_timeout: Ohne Timeout kann ein hängender Job die gesamte limit-Kapazität eines Runners dauerhaft blockieren, bis jemand manuell eingreift.
10. Zusammenfassung
Queue-Zeiten in großen Teams sind kein unvermeidliches Schicksal, sondern ein Konfigurationsproblem mit bekannten Lösungen. Der erste Schritt ist immer das Messen: GitLab-Metriken und Prometheus zeigen, welcher Runner-Typ der Engpass ist. Der zweite Schritt ist die Anpassung von concurrent und limit auf der Basis realer Ressourcenverfügbarkeit — nicht auf der Basis von Wunschdenken. Tag-Routing entkoppelt Build-, Test- und Deploy-Runner voneinander, sodass ein voller Build-Queue nicht den Deploy-Runner blockiert. maximum_timeout verhindert, dass einzelne hängende Jobs die Kapazität dauerhaft belegen.
Für Magento-Projekte ist die Empfehlung klar: ein dedizierter Build-Runner mit Docker-Executor und limit = 2–4, ein separater Deploy-Runner mit Shell-Executor und limit = 1, und Prometheus-basiertes Monitoring für Queue-Zeiten und Job-Durchsatz. Erst wenn diese Grundkonfiguration ausgeschöpft ist, lohnt sich der Aufwand eines zweiten Runner-Hosts oder eines Cloud-Autoscalers.
Runner-Kapazität und Queue-Zeiten — Das Wichtigste auf einen Blick
concurrent & limit
Global-Wert und pro-Runner-Limit müssen auf reale RAM-Verfügbarkeit abgestimmt sein. OOM durch zu hohe Werte verlangsamt alle Jobs.
Tag-Routing
Build-, Test- und Deploy-Runner mit dedizierten Tags entkoppeln. run_untagged = false schützt vor ungewollter Job-Übernahme.
maximum_timeout
Hängende Jobs ohne Timeout blockieren Kapazität dauerhaft. Für Magento-Builds: 1500–1800 Sekunden als obere Grenze setzen.
Monitoring
Prometheus-Metriken von GitLab scrapen, Queue-Zeit und waiting-Jobs in Grafana visualisieren — erst messen, dann skalieren.