JournalindexDockup / fältanteckning
Note / health-check-failing-deployment

Health check misslyckas trots att appen fungerar

En health check som misslyckas vid en deployment trots att appen fungerar lokalt beror vanligtvis på en av fem orsaker. Gå igenom binding, sökväg, port, timing och beroenden i den ordning som snabbast hittar felet.

Det finns en särskild sorts låst läge där en health check som misslyckas vid deployment stoppar varje release, trots att applikationen enligt alla mätvärden du kan nå fungerar helt utan problem. Den kör lokalt. Den kör i Docker lokalt. Loggarna visar att den lyssnar. Ändå rapporterar plattformen misslyckande efter misslyckande, ibland tjugo gånger i rad, utan att någon request någonsin syns i din accesslogg.

Den sista detaljen är viktig och begränsar felsökningen direkt. Om din applikation aldrig loggade requesten nådde kontrollen aldrig fram till applikationen — så det finns inget i applikationskoden som kan förklara det.

Här är de fem orsakerna, i den ordning som snabbast hittar problemet.

1. Du har bundit till localhost

Det här är den absolut vanligaste orsaken och förklarar exakt symptomet "ingen trafik kommer någonsin fram".

Inuti en container betyder 127.0.0.1 den här containerns eget loopback-gränssnitt. En health check som kommer utifrån containern kan inte nå det. Processen lyssnar, loggarna bekräftar det och socketen går inte att nå från någon plats som spelar roll.

// Unreachable from outside the container
app.listen(3000, '127.0.0.1')

// Correct
app.listen(3000, '0.0.0.0')

Ramverk har olika standardinställningar, och flera av dem ändrade standardvärdet mellan större versioner. Kontrollera vad ditt ramverk faktiskt binder till i stället för vad du minns att det binder till.

# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

Om lyssningsadressen är 127.0.0.1:3000 i stället för 0.0.0.0:3000 har du hittat felet, och inget annat på den här listan spelar någon roll.

2. Porten som plattformen kontrollerar är inte porten där du kör tjänsten

Två portar är inblandade och de blandas lätt ihop: porten som processen lyssnar på inuti containern och porten som plattformen routar till. Om appen läser PORT från miljön och du har hårdkodat 3000 någonstans i en Dockerfile kan de två värdena skilja sig åt utan att det märks.

Det tillförlitliga mönstret är att låta plattformen tala om för dig vilken port som ska användas:

const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')

Ange sedan tjänstens port en gång, i plattformen, och sluta underhålla numret på två ställen.

3. Sökvägen returnerar något annat än en lyckad statuskod

En health check-sökväg matchas exakt, och förvånansvärt många fel beror på en redirect. Om appen omdirigerar /healthz till /healthz/ eller tvingar HTTPS med en 301:a kommer en kontroll som bara behandlar 2xx som lyckat att misslyckas varje gång, medan en webbläsare följer omdirigeringen och visar en fungerande sida.

Tre specifika fallgropar:

  • Omdirigeringar med avslutande snedstreck. /healthz/healthz/ är en 301:a.
  • Tvingad HTTPS. Den interna kontrollen kommer vanligtvis via vanlig HTTP på loopback. En ovillkorlig HTTPS-omdirigering gör att den misslyckas.
  • Auth-middleware. En global autentiseringskontroll som körs före routingen returnerar 401 även för health-sökvägen.

Undanta health-sökvägen uttryckligen från auth och från tvingad HTTPS. Det är den enda routen som ska vara tråkig.

4. Kontrollen körs snabbare än din cold start

Om kontrollen misslyckas några gånger och sedan lyckas, eller misslyckas vid deployment men fungerar när du försöker igen, handlar det om timing snarare än konfiguration.

Budgeten du behöver är inte ett enda försök — den är intervall × antal försök. En applikation som behöver tolv sekunder för att ansluta till sin databas och värma upp en cache behöver en total budget på mer än tolv sekunder. Annars kommer varje release att misslyckas och du stänger till slut av spärren, vilket tar bort det enda som står mellan en trasig build och dina användare.

dockup info my-project/my-api --json | grep -A6 healthCheck

Sätt timeout-värdet över den långsammaste legitima enskilda körningen och ange så många försök att intervall × antal försök med god marginal överstiger den långsammaste legitima uppstarten. Mät uppstarten i stället för att gissa — loggarna innehåller tidsstämplar.

5. Applikationen är faktiskt inte redo

Det sista fallet är det som kontrollen finns till för: appen startade, kunde inte nå ett beroende och försöker igen. Den har inte kraschat, så inget startas om. Den kan inte hantera trafik, så kontrollen misslyckas. Systemet fungerar exakt som det ska och talar om för dig att den här releasen inte bör ta emot trafik.

Sättet att skilja detta från de fyra andra fallen är att applikationen loggade requesten och svarade med en statuskod som inte är 2xx. Om requesten syns i loggarna är orsakerna 1 till 3 uteslutna.

Felsökningsordningen som sparar tid

# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow

# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api

# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api

# 4. What is the gate configured to expect?
dockup info my-project/my-api --json

Steg 3 löser de flesta av de här fallen. En curl inifrån containern tar bort alla nätverksvariabler på en gång: om den returnerar 200 där men plattformen fortfarande misslyckas är problemet adressen eller porten, inte appen. Om den returnerar en 301:a eller en 401:a har du hittat orsaken utan att behöva röra plattformen alls.

Varför det är värt att behålla spärren

Efter den fjärde misslyckade deploymenten är det frestande att stänga av health checken och få ut releasen. Det är värt att komma ihåg vad du stänger av.

I Dockup är health gate den funktion som håller en trasig release borta från dina användare. Den nya versionen byggs och startas medan den nuvarande fortsätter att leverera trafik; trafiken flyttas först när den nya versionen svarar. Stäng av spärren och du återaktiverar scenariot där en container som startar men inte fungerar ersätter en som fungerade.

En kontroll som misslyckas fyra releaser i rad är irriterande. En kontroll som alltid lyckas är en kontroll som inte kommer att stoppa den deployment som verkligen spelar roll.

Vanliga frågor

Varför misslyckas health checken när appen fungerar lokalt? Nästan alltid för att containern binder till 127.0.0.1 i stället för 0.0.0.0. Lokalt ansluter du via samma loopback; utifrån containern går den adressen inte att nå.

Bör health-endpointen kräva autentisering? Nej. Undanta den från global auth-middleware, annars får kontrollen en 401:a och deploymenten misslyckas trots att appen fungerar.

Vilken timeout bör jag använda? Längre än din långsammaste legitima enskilda körning, med tillräckligt många försök för att täcka din långsammaste legitima cold start. Läs ut uppstartstiden ur loggarna i stället för att gissa.

Är det säkert att stänga av health checken för att få igenom en release? Det släpper igenom releasen och tar bort skyddet som hindrar en trasig version från att ta emot trafik. Fixa kontrollen i stället — i de flesta fall beror problemet på en bind-adress eller en redirect, och det tar bara några minuter.