JournalindeksDockup / feltnote
Note / health-check-failing-deployment

Health check fejler, men appen virker

Et health check, der fejler under deployment, mens appen kører fint lokalt, skyldes som regel én af fem ting. Gennemgå binding, sti, port, timing og dependencies i den rækkefølge, der hurtigst finder problemet.

Der findes en helt særlig form for fastlåsning, hvor et health check, der fejler under deployment, blokerer alle releases, selv om applikationen efter alle målbare kriterier, du kan nå, fungerer helt fint. Den kører lokalt. Den kører i Docker lokalt. Loggene viser, at den lytter. Og platformen rapporterer fejl efter fejl, nogle gange tyve i træk, uden at der nogensinde dukker en request op i din access-log.

Den sidste detalje er vigtig, fordi den straks indsnævrer feltet. Hvis din applikation aldrig loggede requesten, nåede checket aldrig frem til applikationen — så der er ikke noget i din applikationskode, der kan forklare det.

Her er de fem årsager i den rækkefølge, der hurtigst finder problemet.

1. Du lytter kun på localhost

Det er den klart mest almindelige årsag, og den forklarer præcis symptomet "der kommer aldrig trafik frem".

I en container betyder 127.0.0.1 denne containers eget loopback-interface. Et health check, der kommer udefra containeren, kan ikke nå det. Processen lytter, dine logs bekræfter det, og socketen er utilgængelig fra alle steder, der betyder noget.

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

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

Frameworks har forskellige defaults, og flere af dem har ændret defaulten mellem større versioner. Kontrollér, hvad dit framework faktisk binder til, i stedet for hvad du husker, at det binder til.

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

Hvis lytteadressen er 127.0.0.1:3000 i stedet for 0.0.0.0:3000, har du fundet problemet, og intet andet på denne liste er relevant.

2. Den port, platformen checker, er ikke den port, du serverer på

Der er to porte involveret, og de er nemme at forveksle: den port, processen lytter på inde i containeren, og den port, platformen router til. Hvis din app læser PORT fra environment, mens du har hardcodet 3000 et sted i en Dockerfile, kan de to porte være forskellige uden at det er tydeligt.

Det pålidelige mønster er at lade platformen fortælle dig det:

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

Indstil derefter servicens port ét sted i platformen, og stop med at vedligeholde tallet to steder.

3. Stien returnerer noget andet end en successtatus

En health check-sti matches præcist, og overraskende mange fejl skyldes en redirect. Hvis din app redirecter /healthz til /healthz/ eller tvinger HTTPS med en 301, vil en checker, der kun betragter 2xx som succes, fejle hver gang, mens en browser følger redirecten og viser dig en side, der virker.

Tre konkrete fælder:

  • Redirects på grund af trailing slash. /healthz/healthz/ er en 301.
  • Tvungen HTTPS. Det interne check kommer normalt via almindelig HTTP på loopback. En ubetinget HTTPS-redirect får det til at fejle.
  • Auth-middleware. En global authentication guard, der kører før routing, returnerer også 401 for health-stien.

Udeluk health-stien eksplicit fra auth og håndhævelse af HTTPS. Det er den ene route, der bør være kedelig.

4. Checket er hurtigere end din cold start

Hvis checket fejler nogle gange og derefter lykkes, eller fejler under deploy og lykkes, når du prøver igen, er det timing og ikke konfiguration.

Det budget, du har brug for, er ikke ét forsøg — det er interval × retries. En applikation, der bruger tolv sekunder på at oprette forbindelse til sin database og varme en cache op, har brug for et samlet budget på over tolv sekunder. Ellers fejler du ved hver release og ender med at slå gaten fra, hvilket fjerner det eneste, der står mellem et ødelagt build og dine brugere.

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

Sæt timeout højere end dit langsomste legitime enkeltforsøg, og indstil retries, så interval × retries med god margin overstiger din langsomste legitime boot. Mål boot-tiden i stedet for at gætte — loggene har timestamps.

5. Applikationen er reelt ikke klar

Det sidste tilfælde er det, checket findes til: Din app startede, kunne ikke få forbindelse til en dependency og prøver igen. Den er ikke crashet, så intet genstarter den. Den kan ikke håndtere requests, så checket fejler. Systemet fungerer præcis som designet og fortæller dig, at denne release ikke bør modtage trafik.

Måden at skelne dette fra de andre fire på er, at din applikation loggede requesten og svarede med en status, der ikke er 2xx. Hvis requesten optræder i dine logs, er årsag 1 til 3 udelukket.

Den rækkefølge for diagnosticering, der sparer 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

Trin 3 løser de fleste af disse problemer. En curl fra inde i containeren fjerner alle netværksvariabler på én gang: Hvis den returnerer 200 dér, mens platformen stadig fejler, er problemet adressen eller porten — ikke appen. Hvis den returnerer en 301 eller en 401, har du fundet årsagen uden overhovedet at røre platformen.

Hvorfor det er værd at beholde gaten

Efter det fjerde fejlede deploy kan det være fristende at deaktivere health checket og få releasen ud. Det er værd at huske, hvad du slår fra.

På Dockup er health gaten det, der holder en ødelagt release væk fra dine brugere. Den nye version bliver built og startet, mens den aktuelle fortsætter med at levere trafik; trafikken flyttes først, når den nye version svarer. Slår du gaten fra, genaktiverer du den fejltilstand, hvor en container, der starter men ikke kan fungere, erstatter en version, der fungerede.

Et check, der fejler ved fire releases i træk, er irriterende. Et check, der altid lykkes, er et check, der ikke stopper det deploy, der betyder noget.

Ofte stillede spørgsmål

Hvorfor fejler health checket, når appen virker lokalt? Næsten altid fordi containeren binder til 127.0.0.1 i stedet for 0.0.0.0. Lokalt opretter du forbindelse via det samme loopback-interface; udefra containeren er adressen utilgængelig.

Bør health-endpointet kræve authentication? Nej. Udeluk det fra global auth-middleware, ellers får checkeren en 401, og deployet fejler, selv om appen fungerer.

Hvilken timeout bør jeg bruge? Længere end dit langsomste legitime enkeltforsøg, med retries, der dækker din langsomste legitime cold start. Aflæs boot-tiden i dine logs i stedet for at gætte.

Er det sikkert at deaktivere health checket for at få en release igennem? Det får releasen igennem og fjerner den beskyttelse, der forhindrer en ødelagt version i at modtage trafik. Ret checket i stedet — i de fleste tilfælde skyldes problemet en bind-adresse eller en redirect, og det tager få minutter.