CI/CD-Pipeline-Infrastruktur auf dedizierten Servern

CI/CD-Pipeline-Infrastruktur auf dedizierten Servern

Gemeinsam genutzte CI-Dienste bremsen immer dann ab, wenn es am meisten nervt. GitHub Actions stellt Jobs in die Warteschlange, wenn es zu vielen gleichzeitigen Zugriffen kommt. GitLab Shared Runners brechen bei Builds ab, die länger als eine Stunde dauern. Die Kosten für kostenpflichtige Tarife summieren sich schnell: GitHub Actions kostet 0,008 US-Dollar pro Minute für Linux-Runners, was bedeutet, dass ein 20-minütiger Build, der 50 Mal pro Tag läuft, 240 US-Dollar pro Monat kostet, bevor größere parallele Testsuiten ins Spiel kommen.

Ein dedizierter Server, auf dem Jenkins oder selbst gehostete GitLab-Runner laufen, verändert die Wirtschaftlichkeit komplett. Feste monatliche Kosten. Keine Abrechnung pro Minute. Keine Wartezeiten in der Warteschlange zu Spitzenzeiten. Und auf NVMe Speicher laufen Docker-Layer-Caching und das Lesen von Testartefakten mit Geschwindigkeiten, die einen messbaren Unterschied in der Gesamtdauer der Pipeline ausmachen.

Warum eine eigene CI-Infrastruktur bei großem Umfang sinnvoll ist

Das Warteschlangenproblem

Gemeinsam genutzte CI-Dienste funktionieren nach dem Fair-Use-Prinzip. Wenn dein Team vor einer Veröffentlichung 15 Commits gleichzeitig schickt, werden diese Jobs hintereinander in die Warteschlange gestellt. Auf einem dedizierten Server mit 16 Kernen, auf dem Jenkins mit 16 parallelen Executoren läuft, starten alle 15 Jobs gleichzeitig. Die Zeit vom letzten Commit bis zum endgültigen Build-Ergebnis wird dadurch deutlich verkürzt.

Diese Komprimierung ist vor allem während Code-Review-Zyklen wichtig, wo die Wartezeit der Entwickler direkt beeinflusst, wie viele Review-Durchläufe pro Tag stattfinden. Teams, die die CI-Wartezeit von 15 Minuten auf 3 Minuten verkürzen, haben in der Regel einen höheren PR-Durchsatz und schnellere Merge-Zyklen.

Abrechnung pro Minute vs. Fixkosten

Ein mittelgroßes Entwicklerteam, das täglich 200 Builds mit einer durchschnittlichen Dauer von jeweils 15 Minuten durchführt, kommt auf 3.000 Build-Minuten pro Tag. Bei GitHub Actions kostet das 0,008 $ pro Minute für Linux, also 24 $ pro Tag oder ungefähr 720 $ pro Monat. Bei GitLab Premium mit zusätzlich gekauften Runner-Minuten fallen ähnliche Kosten an.

Ein InMotion Hosting Dedicated Server für 99,99 $/Monat schafft die gleichen 3.000 täglichen Build-Minuten und hat noch Kapazitäten übrig. Auf der Advanced-Stufe kann die Parallelisierung über 64 GB RAM umfassende Testsuiten ohne Speicherbelastung abwickeln.

Jenkins: Master/Agent-Topologie auf dedizierter Hardware

Einzel-Server-Einrichtung

Für Teams, die weniger als 500 Builds pro Tag machen, ist ein einziger dedizierter Server, auf dem sowohl der Jenkins-Master als auch die Build-Agenten laufen, eine gute Lösung. Der Jenkins-Master-Prozess ist leichtgewichtig: Er kümmert sich um die Planung, die Plugin-Verwaltung und die Web-Benutzeroberfläche. Die Build-Agenten machen die eigentliche Arbeit.

Richte Jenkins mit 12 bis 14 Build-Executoren auf einem Server mit 16 Kernen ein, wobei 2 Kerne für den Master-Prozess und den Overhead des Betriebssystems reserviert bleiben. Jeder Executor führt einen Build-Job aus. Mit 14 parallelen Executoren wird eine Warteschlange mit 14 Jobs in der Zeit abgearbeitet, die ein einzelner Job benötigt.

Multi-Server für größere Teams

Wenn das Build-Volumen über 500 Jobs pro Tag geht oder wenn verschiedene Build-Umgebungen getrennt werden müssen (Python 3.9 vs. 3.12, verschiedene Docker-Daemon-Versionen), sorgt eine Master-Agent-Topologie über mehrere dedizierte Server für eine klarere Trennung. Der Jenkins-Master läuft auf einem kleineren Server (Essential-Tier reicht aus). Build-Agenten laufen auf leistungsstärkeren Servern, die auf die Anforderungen der Arbeitslast abgestimmt sind.

Agentenserver verbinden sich über SSH oder JNLP mit dem Master. So kann man die Kapazität nach und nach erhöhen: Ein zweiter dedizierter Advanced-Server verdoppelt den Build-Durchsatz, ohne dass man den Master neu konfigurieren muss.

GitLab-Runner auf dedizierter Hardware

Selbst gehostete GitLab-Runner melden sich bei einer GitLab-Instanz an (entweder gitlab.com oder selbst gehostet) und führen Pipeline-Jobs aus. Jeder Runner-Prozess kann jeweils einen Job bearbeiten; durch die Ausführung mehrerer Runner-Prozesse auf einem Server mit 16 Kernen wird Parallelität erreicht.

Die GitLab-Runner-Konfiguration für maximale Parallelität auf einem Extreme-Server:

  • concurrent = 16 ( in /etc/gitlab-runner/config.toml; legt die maximale Anzahl paralleler Jobs für alle registrierten Runner fest)
  • Ausführer = Docker ( Der Docker-Ausführer packt jeden Job in einen eigenen Container, damit die Jobs sich nicht gegenseitig beeinflussen)
  • pull_policy = if-not-present ( nutzt lokal zwischengespeicherte Docker-Images, anstatt sie bei jedem Job neu zu laden; wichtig für die Leistung NVMe )

Mit dem Docker-Executor und dem lokalen Image-Caching auf NVMe wird beim nächsten Mal, wenn man dieselbe Pipeline ausführt, das Image komplett übersprungen. Ein Python 3.12-Image, das normalerweise 45 Sekunden braucht, um von Docker Hub geholt zu werden, läuft vom lokalen NVMe in weniger als 2 Sekunden.

NVMe : Wo sich die CI-Leistung am meisten verbessert

Docker-Layer-Caching

Docker-Builds sind in Schichten aufgebaut. Wenn bei einem Build nur der Anwendungscode geändert wird, aber nicht die Abhängigkeiten, nutzt Docker die zwischengespeicherten Schichten für die Installation der Abhängigkeiten wieder. Dieser Cache ist auf dem lokalen Speicher des Runners. Auf SSD dauert das Lesen einer 2 GB großen zwischengespeicherten Schicht ungefähr 4 Sekunden. Auf NVMe 5 GB/s dauert das gleiche Lesen weniger als eine halbe Sekunde.

Bei einem Build, der täglich 20 Pipeline-Jobs mit zwischengespeicherten Docker-Layern durchführt, summiert sich der Unterschied auf mehrere Minuten eingesparte Zeit pro Tag. Bei einem Team von 20 Entwicklern ist das echt wichtig.

Testartefakt-Speicher

Testsuiten machen eine Menge Artefakte: Berichte zur Abdeckung, Screenshots von Browsertests, kompilierte Binärdateien, XML-Dateien mit Testergebnissen. Auf einem vielbeschäftigten CI-Server landen hunderte von Artefakt-Schreibvorgängen pro Stunde auf der Speicherebene. NVMe diese Schreiblast, ohne dass sich I/O-Wartezeiten in den Build-Protokollen ansammeln.

Mach Jenkins oder GitLab so, dass sie während des Builds die Artefakte auf dem lokalen NVMe speichern und dann die fertigen Artefakte nach Abschluss der Pipeline in den Objektspeicher oder ein gemeinsames Repository hochladen. Mit diesem zweistufigen Ansatz bleibt der Build schnell und die Artefakte werden über die lokale Kapazität des Servers hinaus aufbewahrt.

Testparallelisierung und Arbeitsspeicher

Moderne Test-Frameworks verteilen Tests auf mehrere Prozesse. pytest-xdist, das Flag –maxWorkers von Jest und das Gem parallel_tests von RSpec schreiben während der parallelen Testausführung temporäre Dateien in den lokalen Speicher. Auf NVMe kommt es bei 16 parallelen Test-Workern, die gleichzeitig temporäre Dateien schreiben, nicht zu I/O-Konflikten. Auf SSD Netzwerkspeichern passiert das aber oft.

Richte temporäre Testverzeichnisse so ein, dass sie direkt auf den NVMe zeigen:nvme für Shell-basierte Testläufer oder eine frameworkspezifische Konfiguration des temporären Verzeichnisses. Das ist eine Änderung in einer Zeile, die eine häufige Ursache für unzuverlässige parallele Testfehler beseitigt.

Caching-Strategien entwickeln

Abhängigkeits-Caches

Der teuerste Schritt in den meisten CI-Pipelines ist die Installation von Abhängigkeiten: npm install, pip install, Maven-Abhängigkeitsauflösung. Diese Schritte holen Pakete aus dem Internet und speichern sie in lokalen Cache-Verzeichnissen.

  • npm: Speichere node_modules und das npm-Cache-Verzeichnis zwischen Builds mit Jenkins' Stash oder GitLabs Cache-Schlüssel.
  • pip: Den pip-Download-Cache (~/.cache/pip) zwischenspeichern und mit –find-links vom lokalen NVMe aus bedienen.
  • Maven: Zwischenspeichere ~/.m2/repository zwischen den Builds, um das erneute Herunterladen von JAR-Abhängigkeiten zu vermeiden.
  • Gradle: Zwischenspeichern von ~/.gradle zwischen Builds; Der Build-Cache von Gradle speichert zusätzlich die Ergebnisse von Aufgaben.

Auf einem dedizierten Server mit NVMe bleiben diese Caches zwischen den Jobs einfach so erhalten. Bei gemeinsam genutzten CI-Diensten ist es so, dass die Caches zwischen jedem Job hoch- und runtergeladen werden müssen, was zusätzlichen Aufwand bedeutet. Auf deinem eigenen Server ist der Cache immer lokal.

Automatisierte Testumgebungen

Docker Compose für Integrationstests

Integrationstests brauchen oft externe Dienste wie Datenbanken, Nachrichtenwarteschlangen oder Mock-APIs. Docker Compose startet diese Dienstabhängigkeiten bei jedem Testlauf. Auf einem dedizierten Server mit 192 GB RAM und 16 Kernen, auf dem PostgreSQL, Redis und ein Mock-API-Server in Docker neben der eigentlichen Testsuite laufen, entsteht nur minimaler Overhead.

Mach Docker Compose so, dass es benannte Volumes nutzt, die auf dem NVMe für Servicedaten basieren. PostgreSQL einem benannten Volume auf NVMe eine Testdatenbank in weniger als einer Sekunde, während es auf langsameren Speichern 5 bis 8 Sekunden dauert.

Browser-Tests

Playwright- und Cypress-Browser-Tests brauchen echt viele Ressourcen: Jeder Browser-Kontext braucht 200 bis 400 MB RAM und viel CPU-Zeit für das Rendering. Auf einem gemeinsam genutzten CI-Runner laufen Browser-Tests oft ins Leere oder liefern unter Speicherbelastung unzuverlässige Ergebnisse. Auf einem dedizierten Server mit 192 GB RAM, auf dem 8 parallele Browser-Test-Worker laufen, hat jeder Worker genug Speicher und es gibt keinen externen Druck auf die Ressourcenzuweisung.

Vergleich: Gemeinsame CI vs. dedizierter Server

KonfigurationParallele JobsWarteschlangeMinutenlimit erstellen
GitHub Actions Team (unbegrenzte Minuten)20 gleichzeitigJa, während der SpitzenzeitenDrosselung bei großem Umfang
GitLab Premium + zusätzliche Runner-MinutenVariabelJa, gemeinsame Läufer2.000 Minuten inklusive
InMotion Essential + Jenkins14 gleichzeitigKeineUnbegrenzt
InMotion Advanced + Jenkins16 gleichzeitigKeineUnbegrenzt
InMotion Extreme + GitLab-Runner16 gleichzeitigKeineUnbegrenzt

Die richtige InMotion-Stufe für CI/CD auswählen

  • Aspire: Kleine Teams mit weniger als 50 Builds pro Tag, einfache Pipeline-Validierung. Auf 4 bis 6 parallele Ausführungen beschränkt.
  • Unverzichtbar: Teams , die täglich 50 bis 200 Builds machen. 64 GB RAM schaffen Docker-basierte Builds mit Abhängigkeits-Caches locker.
  • Fortgeschritten: Teams , die täglich 200 bis 500 Builds oder umfassende Integrationstestsuiten ausführen und dafür große Service-Container brauchen.
  • Extrem: Ingenieurbüros , die täglich mehr als 500 Builds, umfangreiche parallele Browsertests oder ML-Modelltrainings als Teil von CI-Pipelines durchführen.

Erste Schritte

Die meisten Teams merken schon im ersten Abrechnungszeitraum, dass ihre bisherigen Kosten pro Minute für CI die Kosten für dedizierte Server überstiegen haben. Die Leistungssteigerung bei den Erstellungszeiten ist oft genauso beeindruckend: Pipeline-Dauer, die auf gemeinsam genutzter Infrastruktur 20 Minuten dauerte, ist auf dedizierter NVMe Hardware mit echter paralleler Ausführung oft schon in 4 bis 6 Minuten erledigt.

Diesen Artikel teilen

Eine Antwort hinterlassen

Deine E-Mail Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert