Lab 09: Caching-Strategien¶
Hintergrund¶
Pipelines, die bei jedem Lauf alle Abhängigkeiten erneut herunterladen und
installieren (npm ci, pip install, nuget restore), verschwenden Zeit und
Bandbreite. Bei einem typischen Node.js-Projekt mit vielen Abhängigkeiten dauert
npm ci 20–40 Sekunden - bei jedem einzelnen Lauf.
Der Cache@2-Task in Azure Pipelines speichert Verzeichnisse zwischen
Pipeline-Läufen. Beim nächsten Lauf wird der Cache wiederhergestellt, sofern
sich der Cache-Key nicht geändert hat. Der Cache-Key ist ein
zusammengesetzter Schlüssel, der typischerweise aus einem festen Präfix, dem
Betriebssystem des Agents und dem Hash einer Lock-Datei besteht. Ändert sich die
Lock-Datei (weil z.B. eine neue Abhängigkeit hinzukommt), ändert sich auch der
Key - und der Cache wird neu aufgebaut.
Zusätzlich zum exakten Key unterstützt der Task sogenannte Restore-Keys: Falls der exakte Key nicht gefunden wird, sucht Azure Pipelines nach dem neuesten Cache, dessen Key mit einem der Restore-Keys beginnt. So bekommst du zumindest einen teilweisen Cache, selbst wenn sich die Abhängigkeiten geändert haben - die meisten Pakete stammen ja noch aus dem alten Cache.
Was cachen?¶
Es gibt zwei Strategien:
- Download-Cache (
~/.npm,~/.cache/pip): Der Paketmanager lädt Pakete nicht erneut herunter, muss sie aber trotzdem extrahieren und installieren. Die Zeitersparnis ist gering, weil die Installation (Entpacken, Linking, Dateioperationen) den Großteil der Zeit ausmacht. - Installationsverzeichnis (
node_modules,venv): Das gesamte Ergebnis der Installation wird gecacht. Bei einem Cache-Hit wird die Installation komplett übersprungen. Die Zeitersparnis ist drastisch - von 20–40 Sekunden auf unter 1 Sekunde.
Wir verwenden in diesem Lab Strategie 2, weil sie den Caching-Effekt deutlich
sichtbar macht. Der Cache@2-Task bietet dafür den Parameter cacheHitVar:
Er setzt eine Variable auf true, wenn der exakte Cache-Key gefunden wurde.
Mit einer condition auf dem Install-Step können wir die Installation bei
Cache-Hit überspringen.
Aufgabenstellung¶
Schritt 1: Projekt mit Abhängigkeiten vorbereiten¶
Um Caching sinnvoll zu demonstrieren, brauchen wir ein Projekt mit echten
npm-Abhängigkeiten. Bisher hat unser hello-pipeline-Projekt keine externen
Pakete verwendet. Jetzt fügen wir einige gängige Abhängigkeiten hinzu, damit
npm ci messbar Zeit benötigt.
Ersetze den Inhalt von package.json mit folgender Version, die mehrere Abhängigkeiten mit vielen transitiven Paketen enthält:
{
"name": "hello-pipeline",
"version": "1.0.0",
"description": "Demo-App für Caching-Lab",
"main": "src/app.js",
"scripts": {
"build": "echo 'Building...' && mkdir -p dist && cp src/*.js dist/ && cp index.html dist/",
"test": "node test/test.js",
"lint": "echo 'Lint OK'"
},
"dependencies": {
"express": "^4.18.2",
"dotenv": "^16.3.1",
"axios": "^1.6.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"winston": "^3.11.0"
},
"devDependencies": {
"jest": "^29.7.0",
"typescript": "^5.3.3",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"eslint": "^8.56.0"
}
}
Diese Kombination bringt mehrere hundert transitive Abhängigkeiten mit. webpack,
typescript und eslint sind besonders schwergewichtige Pakete.
Erzeuge nun die package-lock.json, die als Grundlage für den Cache-Key
dient. Die Lock-Datei fixiert die exakten Versionen aller transitiven
Abhängigkeiten. Führe dazu lokal npm install aus:
npm install
Der Cache@2-Task verwendet den Hash dieser Datei als Teil des Cache-Keys.
Ändert sich die Lock-Datei, ändert sich der Key - und der Cache wird neu
aufgebaut.
Erstelle außerdem eine requirements.txt für das Python-Caching-Beispiel im zweiten Job. Diese Datei listet Python-Pakete mit festen Versionen:
requests==2.31.0
flask==3.0.0
pytest==7.4.3
pandas==2.1.4
boto3==1.34.0
pydantic==2.5.3
Schritt 2: Pipeline mit Caching¶
Jetzt erstellen wir eine Pipeline mit zwei parallelen Jobs, die jeweils den
Cache@2-Task verwenden: einen für Node.js und einen für Python. Beide
verwenden dieselbe Strategie: das Installationsverzeichnis cachen und die
Installation bei Cache-Hit überspringen.
Ersetze den Inhalt von azure-pipelines.yml:
trigger:
branches:
include:
- master
stages:
- stage: BuildWithCache
displayName: 'Build mit Caching'
jobs:
# ===== Job 1: Node.js mit node_modules-Cache =====
- job: NodeBuild
displayName: 'Node.js Build (cached)'
pool:
vmImage: 'ubuntu-latest'
variables:
cacheHit: 'false'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Node.js installieren'
# node_modules aus dem Cache wiederherstellen
- task: Cache@2
displayName: 'node_modules Cache'
inputs:
key: 'node_modules | "$(Agent.OS)" | package-lock.json'
path: 'node_modules'
cacheHitVar: 'cacheHit'
- script: |
echo "=== Cache-Status ==="
echo "cacheHitVar = $(cacheHit)"
if [ -d node_modules ]; then
echo "node_modules existiert"
echo "Pakete: $(ls node_modules | wc -l)"
echo "Größe: $(du -sh node_modules | cut -f1)"
else
echo "Kein Cache vorhanden (erster Lauf)"
fi
displayName: 'Cache-Status prüfen'
# npm ci nur ausführen, wenn KEIN exakter Cache-Hit
- script: |
echo "=== npm ci (kein Cache-Hit) ==="
START=$(date +%s%N)
npm ci 2>&1
END=$(date +%s%N)
DURATION=$(( (END - START) / 1000000 ))
echo "Dauer: ${DURATION} ms"
displayName: 'npm ci'
condition: ne(variables.cacheHit, 'true')
- script: |
echo "=== Cache-Hit: npm ci übersprungen ==="
echo "node_modules wurde aus dem Cache wiederhergestellt."
echo "Pakete: $(ls node_modules | wc -l)"
displayName: 'Cache-Hit bestätigen'
condition: eq(variables.cacheHit, 'true')
- script: npm run build
displayName: 'Build ausführen'
# ===== Job 2: Python mit venv-Cache =====
- job: PythonBuild
displayName: 'Python Build (cached)'
pool:
vmImage: 'ubuntu-latest'
variables:
cacheHit: 'false'
steps:
# Virtualenv aus dem Cache wiederherstellen
- task: Cache@2
displayName: 'venv Cache'
inputs:
key: 'venv | "$(Agent.OS)" | requirements.txt'
path: '.venv'
cacheHitVar: 'cacheHit'
- script: |
echo "=== pip install (kein Cache-Hit) ==="
python -m venv .venv
source .venv/bin/activate
START=$(date +%s%N)
pip install -r requirements.txt 2>&1
END=$(date +%s%N)
DURATION=$(( (END - START) / 1000000 ))
echo "Dauer: ${DURATION} ms"
displayName: 'pip install'
condition: ne(variables.cacheHit, 'true')
- script: |
echo "=== Cache-Hit: pip install übersprungen ==="
source .venv/bin/activate
echo "Installierte Pakete:"
pip list --format=columns | head -20
displayName: 'Cache-Hit bestätigen'
condition: eq(variables.cacheHit, 'true')
- stage: ComparePerformance
displayName: 'Cache-Vergleich'
dependsOn: BuildWithCache
jobs:
- job: ShowCacheInfo
displayName: 'Cache-Informationen'
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
echo "=== Caching-Erklärung ==="
echo ""
echo "Beim ERSTEN Pipeline-Lauf:"
echo " - Kein Cache vorhanden (cacheHitVar = false)"
echo " - npm ci / pip install laufen normal"
echo " - node_modules / .venv werden am Ende gecacht"
echo ""
echo "Bei FOLGENDEN Läufen (gleiche Lock-Datei):"
echo " - Cache wird wiederhergestellt (cacheHitVar = true)"
echo " - npm ci / pip install werden ÜBERSPRUNGEN"
echo " - Zeitersparnis: 20-40 Sekunden pro Job"
echo ""
echo "Bei GEÄNDERTEN Abhängigkeiten:"
echo " - Cache-Key ändert sich (Lock-Datei-Hash anders)"
echo " - npm ci / pip install laufen erneut"
echo " - Neuer Cache wird gespeichert"
displayName: 'Cache-Strategie erklären'
Gehe die Pipeline Abschnitt für Abschnitt durch:
- Cache-Key (
key: 'node_modules | "$(Agent.OS)" | package-lock.json'): Der Key besteht aus drei Teilen, getrennt durch|: einem festen Präfix (node_modules), dem Betriebssystem des Agents ($(Agent.OS)) und dem Hash der Dateipackage-lock.json. Azure Pipelines berechnet den SHA-256-Hash der Datei automatisch. Der OS-Teil stellt sicher, dass Linux- und Windows-Agents getrennte Caches verwenden, da native Module plattformspezifisch kompiliert werden. cacheHitVar: Der ParametercacheHitVar: 'cacheHit'weist denCache@2-Task an, eine Pipeline-VariablecacheHitzu setzen. Der Wert isttrue, wenn der exakte Cache-Key gefunden wurde, sonstfalse(auch bei Restore-Key-Treffern). So können wir die Installation konditionell überspringen.condition: Der Stepnpm cihat die Bedingungcondition: ne(variables.cacheHit, 'true')- er läuft nur, wenn kein exakter Cache-Hit vorliegt. Bei Cache-Hit wird stattdessen der Bestätigungs-Step ausgeführt, der anzeigt, dassnode_modulesaus dem Cache stammt.- Warum
node_modulesstatt~/.npm? Der npm-Download-Cache (~/.npm) speichert nur die heruntergeladenen Tarballs.npm cimuss trotzdem jedes Paket extrahieren und innode_modulesinstallieren - das dauert fast genauso lang wie der Download selbst. Durch Caching vonnode_modulesdirekt wird die gesamte Installation übersprungen. - Python-Job: Zeigt dasselbe Muster für pip mit einem gecachten virtualenv.
Bei Cache-Hit ist
.venvsofort verfügbar, ohnepip install. - ComparePerformance-Stage: Eine rein informative Stage, die das Caching-Verhalten zusammenfasst.
Schritt 3: Committen und zweimal ausführen¶
Der Caching-Effekt zeigt sich erst beim zweiten Lauf: Beim ersten Lauf wird der Cache aufgebaut, beim zweiten Lauf wird er wiederhergestellt. Daher führen wir die Pipeline bewusst zweimal aus.
git add package.json package-lock.json requirements.txt azure-pipelines.yml
git commit -m "Add caching to pipeline"
git push origin master
Warte, bis der erste Build vollständig abgeschlossen ist. Der Cache wird erst nach Abschluss aller Jobs gespeichert. Ein zweiter Lauf während des ersten findet also noch keinen Cache.
Starte dann einen zweiten Build manuell:
# Pipeline-ID herausfinden
az pipelines list --output table
# Zweiten Build starten (ersetze <pipeline-id> durch die ID
# aus der vorherigen Ausgabe)
az pipelines run --id <pipeline-id> --branch master
Schritt 4: Build-Zeiten vergleichen¶
Öffne beide Pipeline-Runs im Browser und vergleiche die Logs:
- Erster Lauf: Im Step "node_modules Cache" siehst du die Meldung
Cache not found for input keys. Der Step "npm ci" läuft und dauert typischerweise 20–40 Sekunden. - Zweiter Lauf: Im Step "node_modules Cache" siehst du
Cache restored from key: node_modules | "Linux" | <hash>. Der Step "npm ci" wird komplett übersprungen (als "skipped" markiert). Stattdessen läuft der Step "Cache-Hit bestätigen", der die Anzahl der wiederhergestellten Pakete anzeigt.
Die gleiche Beobachtung kannst du für den Python-Job machen: pip install
wird beim zweiten Lauf übersprungen, .venv stammt aus dem Cache.
Validierung¶
Prüfe per CLI, dass beide Runs erfolgreich waren:
# Beide Runs vergleichen
az pipelines runs list --top 2 --output table
Öffne im Browser die Logs beider Runs und prüfe:
- Step "node_modules Cache": Zeigt beim ersten Lauf
Cache not found, beim zweiten LaufCache restored. - Step "npm ci": Beim ersten Lauf ausgeführt (20–40 Sekunden), beim zweiten Lauf als skipped markiert.
- Step "Cache-Hit bestätigen": Beim zweiten Lauf ausgeführt, zeigt
Anzahl der Pakete in
node_modules.
Aufräumen¶
Kein Aufräumen nötig. Pipeline-Caches werden automatisch nach 7 Tagen ohne
Verwendung gelöscht. Du kannst die Cache-Nutzung einer Pipeline unter
Pipelines >