fix: Video-Upload startet den Pod nicht mehr neu
Ein Video ueber 50 MB im Admin hochzuladen, liess den Pod mitten im Upload neu starten. Ursache war nicht die Dateigroesse, sondern dass ffmpeg sich nach den Kernen des KNOTENS bemessen hat statt nach dem CPU-Anteil des Pods. x264 waehlt seine Threadzahl standardmaessig als das 1,5-fache der vom Betriebssystem gemeldeten Kerne. In einem Container sind das die Kerne des Knotens: auf einer 16-Kern-Maschine rund 24 Encoder-Threads mit je eigenen Bildpuffern, bei 4K-Material rund 12 MB je Bild. Das hatte zwei Folgen, die von aussen beide gleich aussehen: 1. Der Speicherbedarf sprengte das Speicherlimit des Pods, der Kernel beendete den Container (OOMKilled). 2. ffmpeg belegte alle Kerne, der Node-Prozess kam nicht mehr zum Zug und beantwortete die Liveness-Probe nicht rechtzeitig. Kubelet wertete das als haengenden Prozess und startete den Container neu. Dekoder-, Filter- und Encoder-Threads sind jetzt gedeckelt (Vorgabe 2, ueber VIDEO_TRANSCODE_THREADS auf bis zu 8 anhebbar). Der Verbrauch ist damit konstant und unabhaengig davon, auf welchem Knoten der Pod laeuft. Der "-threads"-Deckel steht bewusst auch VOR "-i": ffmpeg ordnet Optionen der naechsten Datei zu, ein Deckel erst hinter "-i" wuerde nur den Encoder begrenzen und das Dekodieren eines 4K-Videos unbegrenzt lassen. Zusaetzlich "-nostdin" und stdio ["ignore","ignore","pipe"]: der Kindprozess bekam von spawn() eine Standardeingabe, die niemand bedient, und eine unbeachtete stdout-Pipe, die volllaufen kann. Beides konnte ffmpeg bis zum Zeitlimit blockieren. Die Argumentliste ist als buildFfmpegArgs() ausgelagert und getestet, damit die Begrenzungen pruefbar sind, ohne ffmpeg wirklich zu starten. Beide Probes bekommen ausserdem timeoutSeconds: 5. Ohne den Eintrag gilt Kubernetes' Vorgabe von einer Sekunde — zu knapp, solange sich der Node-Prozess die CPU mit einem laufenden Transcode teilt. k8s/README.md beschreibt die Diagnose fuer den Fall, dass im Cluster noch eine aeltere, von Hand gepflegte Fassung des Deployments laeuft. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
78c19f1b73
commit
b1384c3695
5 files changed
+270
-19
No files matched your search
@@ -86,6 +86,11 @@ spec:
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
# Ohne diesen Eintrag gilt Kubernetes' Vorgabe von EINER Sekunde.
|
||||
# Waehrend ein Admin-Video umgerechnet wird, teilt sich der
|
||||
# Node-Prozess die CPU mit ffmpeg — eine Antwort kann dann laenger
|
||||
# als eine Sekunde brauchen, ohne dass irgendetwas kaputt waere.
|
||||
timeoutSeconds: 5
|
||||
# Bewusst der DB-FREIE Endpunkt (siehe app/api/health/route.ts):
|
||||
# eine Liveness-Probe an der Datenbank wuerde aus einer
|
||||
# voruebergehenden DB-Stoerung eine Neustart-Schleife machen, weil
|
||||
@@ -106,6 +111,14 @@ spec:
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 5
|
||||
# Siehe Begruendung an der Readiness-Probe oben. Hier wiegt es
|
||||
# schwerer: eine fehlgeschlagene LIVENESS-Probe laesst Kubernetes
|
||||
# den Container neu starten. Genau das passierte beim
|
||||
# Admin-Video-Upload, solange die Probe mit der Vorgabe von einer
|
||||
# Sekunde gegen "/" lief — also gegen die Startseite samt
|
||||
# Datenbankabfragen und vollem React-Rendering, waehrend ffmpeg
|
||||
# gleichzeitig die CPU belegte.
|
||||
timeoutSeconds: 5
|
||||
# Bemessen nach dem teuersten Vorgang der Anwendung, dem
|
||||
# Admin-Video-Upload: Node laedt den multipart-Body beim
|
||||
# request.formData() vollstaendig in den Speicher, und die Datei
|
||||
|
||||
@@ -165,6 +165,60 @@ vorliegen, und zeigt sonst eine Warnung mit Anzahl und Beispielpfaden
|
||||
(`lib/upload-health.ts`). Dieser Fall faellt damit sofort auf, statt erst
|
||||
dann, wenn Kundschaft im Shop leere Flaechen sieht.
|
||||
|
||||
## Server startet beim Video-Upload neu
|
||||
|
||||
Symptom: Ein Video im Admin hochladen, und der Pod startet mittendrin neu. Kleine
|
||||
Bilder gehen problemlos durch.
|
||||
|
||||
Ursache ist fast immer, dass im Cluster noch eine ältere, von Hand gepflegte
|
||||
Fassung des Deployments läuft und nicht die aus diesem Verzeichnis. Zuerst den
|
||||
Ist-Zustand feststellen:
|
||||
|
||||
```bash
|
||||
# Abbruchgrund des letzten Containerlaufs:
|
||||
kubectl -n wabenhain get pods -l app=wabenhain-app \
|
||||
-o jsonpath='{.items[0].status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
|
||||
|
||||
# Die drei Werte, auf die es ankommt:
|
||||
kubectl -n wabenhain get deploy wabenhain-app \
|
||||
-o jsonpath='limit={.spec.template.spec.containers[0].resources.limits.memory}{"\n"}liveness={.spec.template.spec.containers[0].livenessProbe.httpGet.path}{"\n"}timeout={.spec.template.spec.containers[0].livenessProbe.timeoutSeconds}{"\n"}'
|
||||
```
|
||||
|
||||
Erwartet wird `limit=1Gi`, `liveness=/api/health`, `timeout=5`. Steht dort
|
||||
`512Mi`, `/` oder ein leerer Timeout, ist genau das die Ursache — es gibt zwei
|
||||
Wege, wie das den Container umbringt:
|
||||
|
||||
- **`liveness=/`**: Die Startseite ist `force-dynamic`, fragt die Datenbank ab
|
||||
und rendert React vollständig. Ohne eigenen `timeoutSeconds` gilt Kubernetes'
|
||||
Vorgabe von **einer Sekunde**. Während ffmpeg das Video umrechnet, teilt sich
|
||||
der Node-Prozess die CPU — die Seite antwortet nicht rechtzeitig, die Probe
|
||||
schlägt fehl, und nach `failureThreshold × periodSeconds` startet kubelet den
|
||||
Container neu. `/api/health` ist stattdessen datenbankfrei und billig.
|
||||
- **`limit=512Mi`**: Node-Server und ffmpeg teilen sich dasselbe Speicherbudget.
|
||||
Bei einem 4K-Video reicht das nicht, der Kernel beendet den Container
|
||||
(`reason=OOMKilled`).
|
||||
|
||||
Behoben wird beides, indem die Fassung aus diesem Verzeichnis ausgerollt wird:
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/05-app-deployment.yaml
|
||||
kubectl -n wabenhain rollout status deploy/wabenhain-app
|
||||
```
|
||||
|
||||
Wird das Deployment über die Rancher-Oberfläche gepflegt, überschreibt das
|
||||
nächste Speichern dort ein `kubectl apply` wieder. Dann die Werte in Rancher
|
||||
selbst nachziehen (Speicherlimit, beide Probe-Pfade, `timeoutSeconds`) oder das
|
||||
Manifest als Quelle übernehmen.
|
||||
|
||||
Die Anwendungsseite ist unabhängig davon abgesichert: ffmpeg läuft mit
|
||||
gedeckelter Threadzahl (`VIDEO_TRANSCODE_THREADS`, Vorgabe 2), damit sein
|
||||
Speicher- und CPU-Bedarf nicht mehr von der Größe des Knotens abhängt — siehe
|
||||
`src/lib/video-transcode.ts`.
|
||||
|
||||
**Zusätzlich prüfen:** Hat das Deployment gar kein `uploads`-Volume, ist ein
|
||||
erfolgreicher Upload beim nächsten Neustart trotzdem weg. Siehe
|
||||
„Bilder/Videos verschwinden nach jedem Redeploy" oben.
|
||||
|
||||
## Sicherung und Wiederherstellung
|
||||
|
||||
`07-backup-cronjob.yaml` legt jede Nacht um 03:15 Uhr eine
|
||||
|
||||
Reference in new issue
Block a user