kiln-app: liveness-probe startar om podden under last (självförvärrande) #119

Open
opened 2026-09-26 18:43:25 +00:00 by znibb · 0 comments
Owner

Bakgrund

Under lasttest av kiln.kobbo.se (2026-09-26) upptäcktes att kiln-app-poddarna
blir liveness-dödade och startar om när appen blir överlastad — innan den
egentligen är död. Det är ett självförvärrande fel: kubelet tar bort kapacitet
mitt under en lasttopp, vilket gör överlasten värre.

Berörda manifest: kubernetes/apps/kiln/app/deployment.yaml (branch feature/kiln).

Vad som händer

Både readiness- och liveness-proben pekar på samma endpoint, /api/health/live:

Probe Endpoint timeout period failureThreshold
readiness /api/health/live 1s (default) 15s 3
liveness /api/health/live 1s (default) 30s 3

/api/health/live gör SELECT 1 och serveras av samma Node-process och
event-loop
som SSR-sidan. När appen mättas (CPU-bunden SSR) blockerar
event-loopen även health-endpointen, och då:

  1. readiness faller → Traefik drar podden ur rotation. (Detta är önskat.)
  2. liveness faller 3× i rad → kubelet dödar och startar om containern.
  3. Under omstarten (~30s+, imagen är ~690 MiB) försvinner kapacitet → mer last
    på kvarvarande pod → fler probe-failures. Ond spiral.

Bevis (från lasttest inifrån klustret)

Appens egna probes är klientoberoende bevis. Vid en trapptest — konstanta
RPS-nivåer mot kiln-app (mix: 50 % startsida, 30 % /login, 20 % /api/health/live):

RPS avg p95 fel
200 43ms 249ms 0.00%
300 79ms 268ms 0.00%
400 4411ms 8379ms 0.00%
500 5264ms 19530ms 19.76%

Appens health-endpoint började svara >1s (probe-timeout) precis när 400-nivån
startade, och liveness dödade podden:

18:37:21  health-probe börjar svara >1s (L400 startar)
18:38:21  liveness failat 3x → "Container app failed liveness probe, will be restarted"
18:38:46  readiness-probe failed på andra podden

kubectl -n kiln get events:
Container app failed liveness probe, will be restarted

Omstarter under testerna: 0 → 5 resp. 0 → 3 (båda poddarna), medan
kiln-postgres stod helt orörd (0 omstarter, 8/100 anslutningar, inga fel i
loggen). Flaskhalsen är alltså Node-processen, inte databasen.

Not: vid 400 RPS nådde även lastgeneratorn sitt maxVUs (belastningen är
därför ett minimum), men appens egen probe-failure är oberoende av klienten
och bekräftar att det är servern som ger upp.

Varför liveness är fel verktyg här

Liveness ska svara på frågan "är processen död eller permanent hängd?" — inte
"är den upptagen just nu?". En CPU-mättad men fullt fungerande app ska
dräneras av readiness, inte dödas av liveness. Att lägga liveness på en
endpoint som delar event-loop med den dyra SSR-koden gör att en tillfällig
överlast leder till onödiga omstarter och längre avbrott än nödvändigt.

Förslag på åtgärd

  1. Liveness: använd en signal som inte delar event-loop/CPU med SSR, t.ex.
    en tcpSocket-probe på port 3000, eller höj timeoutSeconds (3–5s) och
    failureThreshold (5–6). Liveness får inte fälla på tillfällig latens.
  2. Readiness: behåll /api/health/live — den ska dra ur trafik, vilket är
    precis vad man vill när appen är upptagen.
  3. (Separat) Appens tak är ~300 RPS för denna mix; överväg HPA på CPU +
    fler replicas om högre kapacitet behövs.

Tasks

  • Byt/justera livenessProbe i kubernetes/apps/kiln/app/deployment.yaml
    (tcpSocket eller höjd timeout/failureThreshold)
  • Verifiera med nytt lasttest att inga omstarter sker vid 400 RPS
  • (valfritt) Lägg till HPA på kiln-app

Referenser

  • Lasttest-verktyg: scripts/loadtest/ (kiln-brytpunkt.js, kiln-trapp.js, README.md)
  • Deploy-underlag: docs/kiln-deploy-plan.md
  • Upptäckt vid lasttest initierat för att hitta appens brytpunkt
## Bakgrund Under lasttest av `kiln.kobbo.se` (2026-09-26) upptäcktes att `kiln-app`-poddarna blir **liveness-dödade och startar om när appen blir överlastad** — innan den egentligen är död. Det är ett självförvärrande fel: kubelet tar bort kapacitet mitt under en lasttopp, vilket gör överlasten värre. Berörda manifest: `kubernetes/apps/kiln/app/deployment.yaml` (branch `feature/kiln`). ## Vad som händer Både readiness- och liveness-proben pekar på **samma** endpoint, `/api/health/live`: | Probe | Endpoint | timeout | period | failureThreshold | |---|---|---|---|---| | readiness | `/api/health/live` | 1s (default) | 15s | 3 | | liveness | `/api/health/live` | 1s (default) | 30s | 3 | `/api/health/live` gör `SELECT 1` och serveras av **samma Node-process och event-loop** som SSR-sidan. När appen mättas (CPU-bunden SSR) blockerar event-loopen även health-endpointen, och då: 1. **readiness** faller → Traefik drar podden ur rotation. *(Detta är önskat.)* 2. **liveness** faller 3× i rad → kubelet **dödar och startar om** containern. 3. Under omstarten (~30s+, imagen är ~690 MiB) försvinner kapacitet → mer last på kvarvarande pod → fler probe-failures. Ond spiral. ## Bevis (från lasttest inifrån klustret) Appens **egna** probes är klientoberoende bevis. Vid en trapptest — konstanta RPS-nivåer mot `kiln-app` (mix: 50 % startsida, 30 % `/login`, 20 % `/api/health/live`): | RPS | avg | p95 | fel | |----:|---:|---:|---:| | 200 | 43ms | 249ms | 0.00% | | 300 | 79ms | 268ms | 0.00% | | 400 | 4411ms | **8379ms** | 0.00% | | 500 | 5264ms | **19530ms** | 19.76% | Appens health-endpoint började svara >1s (probe-timeout) precis när 400-nivån startade, och liveness dödade podden: ```text 18:37:21 health-probe börjar svara >1s (L400 startar) 18:38:21 liveness failat 3x → "Container app failed liveness probe, will be restarted" 18:38:46 readiness-probe failed på andra podden ``` `kubectl -n kiln get events`: `Container app failed liveness probe, will be restarted` Omstarter under testerna: `0 → 5` resp. `0 → 3` (båda poddarna), medan `kiln-postgres` stod **helt orörd** (0 omstarter, 8/100 anslutningar, inga fel i loggen). Flaskhalsen är alltså **Node-processen, inte databasen**. > Not: vid 400 RPS nådde även lastgeneratorn sitt `maxVUs` (belastningen är > därför ett minimum), men appens egen probe-failure är oberoende av klienten > och bekräftar att det är servern som ger upp. ## Varför liveness är fel verktyg här Liveness ska svara på frågan *"är processen död eller permanent hängd?"* — inte *"är den upptagen just nu?"*. En CPU-mättad men fullt fungerande app ska **dräneras av readiness**, inte **dödas av liveness**. Att lägga liveness på en endpoint som delar event-loop med den dyra SSR-koden gör att en tillfällig överlast leder till onödiga omstarter och längre avbrott än nödvändigt. ## Förslag på åtgärd 1. **Liveness:** använd en signal som inte delar event-loop/CPU med SSR, t.ex. en `tcpSocket`-probe på port `3000`, *eller* höj `timeoutSeconds` (3–5s) och `failureThreshold` (5–6). Liveness får inte fälla på tillfällig latens. 2. **Readiness:** behåll `/api/health/live` — den ska dra ur trafik, vilket är precis vad man vill när appen är upptagen. 3. **(Separat)** Appens tak är ~300 RPS för denna mix; överväg HPA på CPU + fler replicas om högre kapacitet behövs. ## Tasks - [ ] Byt/justera `livenessProbe` i `kubernetes/apps/kiln/app/deployment.yaml` (tcpSocket **eller** höjd timeout/failureThreshold) - [ ] Verifiera med nytt lasttest att **inga omstarter** sker vid 400 RPS - [ ] (valfritt) Lägg till HPA på `kiln-app` ## Referenser - Lasttest-verktyg: `scripts/loadtest/` (`kiln-brytpunkt.js`, `kiln-trapp.js`, `README.md`) - Deploy-underlag: `docs/kiln-deploy-plan.md` - Upptäckt vid lasttest initierat för att hitta appens brytpunkt
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
znibb/kobbo-homelab#119
No description provided.