A health check sikertelen, pedig az alkalmazás működik
Ha deployment közben sikertelen a health check, miközben az alkalmazás lokálisan hibátlanul fut, annak általában öt oka lehet. Ellenőrizd a bindingot, az útvonalat, a portot, az időzítést és a függőségeket abban a sorrendben, amely a leggyorsabban megtalálja a hibát.
Különösen frusztráló helyzet, amikor egy deployment közben sikertelen health check minden release-t blokkol, miközben az alkalmazás minden elérhető szempontból teljesen rendben van. Lokálisan fut. Lokálisan Dockerben is fut. A logok szerint figyel a kapcsolatokra. A platform mégis újra és újra hibát jelez, néha egymás után húsz alkalommal is, miközben egyetlen kérés sem jelenik meg az access logban.
Ez az utolsó részlet a kulcs, és azonnal jelentősen leszűkíti a lehetséges okokat. Ha az alkalmazásod soha nem logolta a kérést, akkor a check nem jutott el az alkalmazásodhoz — vagyis az alkalmazáskódod semmilyen hibája nem magyarázza a problémát.
Az alábbi öt okot abban a sorrendben mutatjuk be, amely a leggyorsabban vezet el a megoldáshoz.
1. A localhosthoz bindolsz
Ez a leggyakoribb ok, és pontosan megmagyarázza azt a tünetet, amikor „soha nem érkezik forgalom”.
Egy konténeren belül a 127.0.0.1 a konténer saját loopback interfészét jelenti. A konténeren kívülről érkező health check nem éri el. A process figyel, a logok ezt igazolják, a socket azonban nem érhető el onnan, ahonnan számít.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
A frameworkök defaultjai eltérhetnek, és több framework esetében a major verziók között is változott a default beállítás. Ellenőrizd, hogy a framework valójában mire bindol, ne azt használd alapul, amire emlékszel.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Ha a figyelő cím 0.0.0.0:3000 helyett 127.0.0.1:3000, akkor megtaláltad a hibát, és a lista többi pontja már nem számít.
2. A platform által vizsgált port nem az a port, amelyen szolgálsz ki
Két port érintett, és könnyű összekeverni őket: az a port, amelyen a process a konténeren belül figyel, illetve az a port, amelyre a platform irányítja a forgalmat. Ha az alkalmazásod az environmentből olvassa a PORT értékét, miközben valahol egy Dockerfile-ban 3000 van hardcode-olva, a két érték észrevétlenül eltérhet.
A megbízható minta az, ha hagyod, hogy a platform adja meg az értéket:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Ezután állítsd be a service portját egyszer, a platformon, és ne tartsd karban ugyanazt a számot két külön helyen.
3. Az útvonal nem sikeres státuszkódot ad vissza
A health check útvonalát a rendszer pontosan egyezteti, és meglepően sok hibát egy redirect okoz. Ha az alkalmazásod a /healthz útvonalat /healthz/ címre irányítja át, vagy 301-es válasszal HTTPS-re kényszerít, akkor az a checker, amely csak a 2xx válaszokat tekinti sikeresnek, minden alkalommal hibát jelez. A böngésző viszont követi a redirectet, és egy működő oldalt mutat.
Három konkrét buktató:
- Trailing slash miatti redirect. A
/healthz→/healthz/egy 301-es redirect. - Kényszerített HTTPS. A belső check általában sima HTTP-kérésként érkezik loopbacken. A feltétel nélküli HTTPS-redirect meghiúsítja.
- Auth middleware. A routing előtt futó globális authentication guard a health útvonalra is 401-es választ ad.
Zárd ki a health útvonalat explicit módon az auth és a HTTPS-enforcement alól. Ez az egyetlen route, amelynek unalmasnak kell lennie.
4. A check gyorsabban fut le, mint a cold start
Ha a check néhányszor sikertelen, majd átmegy, vagy deploymentkor hibázik, újrapróbáláskor viszont sikeres, akkor nem konfigurációs, hanem időzítési problémáról van szó.
A szükséges keret nem egyetlen próbálkozás ideje — hanem az interval × retries. Egy olyan alkalmazásnak, amelynek tizenkét másodpercre van szüksége az adatbázishoz való kapcsolódáshoz és egy cache felmelegítéséhez, tizenkét másodpercnél hosszabb teljes keretre van szüksége. Ellenkező esetben minden release meghiúsul, végül pedig kikapcsolod a gate-et, így megszünteted az egyetlen védelmet egy hibás build és a felhasználóid között.
dockup info my-project/my-api --json | grep -A6 healthCheck
Állítsd a timeoutot a leglassabb, még normális egyedi próbálkozásnál hosszabbra, a retries értékét pedig úgy, hogy az interval × retries kényelmesen meghaladja a leglassabb, még normális indulási időt. Ne találgass: mérd meg az indulási időt — a logok tartalmazzák az időbélyegeket.
5. Az alkalmazás valóban nem áll készen
Ez az utolsó eset az, amely miatt a check egyáltalán létezik: az alkalmazásod elindult, nem tudott kapcsolódni egy függőséghez, és újrapróbálkozik. Nem állt le, ezért semmi nem indítja újra. Kiszolgálni sem tud, ezért a check sikertelen. A rendszer pontosan a tervezett módon működik, és azt jelzi, hogy ez a release még nem kaphat forgalmat.
Ezt úgy különböztetheted meg a másik négy esettől, hogy az alkalmazásod logolta a kérést, és nem 2xx választ adott rá. Ha a kérés megjelenik a logokban, akkor az 1–3. ok kizárható.
A diagnosztikai sorrend, amellyel időt takaríthatsz meg
# 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
A legtöbb ilyen esetet a 3. lépés oldja meg. A konténeren belülről indított curl egyszerre zár ki minden hálózati változót: ha ott 200-as választ kapsz, a platform viszont továbbra is hibázik, akkor a probléma a cím vagy a port, nem pedig az alkalmazás. Ha 301-es vagy 401-es választ kapsz, platformmódosítás nélkül megtaláltad az okot.
Miért érdemes megtartani a gate-et?
A negyedik sikertelen deployment után csábító lehet kikapcsolni a health checket, hogy végre kimenjen a release. Érdemes azonban átgondolni, mit kapcsolsz ki ezzel.
A Dockupon a health gate tartja távol a hibás release-t a felhasználóidtól. Az új verzió buildelése és indítása közben a jelenlegi továbbra is kiszolgál; a forgalom csak akkor kerül át, amikor az új verzió válaszol. Ha kikapcsolod a gate-et, újra engedélyezed azt a hibamódot, amelyben egy elinduló, de működésképtelen konténer lecserél egy korábban hibátlan verziót.
Négy egymást követő release-en át sikertelen check bosszantó. A feltétel nélkül sikeres check viszont nem fogja megállítani azt a deploymentet, amelyiknél igazán szükség lenne rá.
Gyakran ismételt kérdések
Miért sikertelen a health check, ha az alkalmazás lokálisan működik?
Szinte mindig azért, mert a konténer a 127.0.0.1 címre bindol a 0.0.0.0 helyett. Lokálisan ugyanazon a loopbacken keresztül csatlakozol; a konténeren kívülről ez a cím nem érhető el.
Szükséges authentication az health endpointhoz? Nem. Zárd ki a globális auth middleware alól, különben a checker 401-es választ kap, és a deployment meghiúsul, miközben az alkalmazás egyébként rendben van.
Milyen timeoutot használjak? Legyen hosszabb a leglassabb, még normális egyedi próbálkozásnál, a retries pedig fedje le a leglassabb, még normális cold startot. Ne találgass: olvasd ki az indulási időt a logokból.
Biztonságos kikapcsolni a health checket egy release feloldásához? Feloldja a release-et, de megszünteti azt a védelmet is, amely megakadályozza, hogy egy hibás verzió forgalmat kapjon. Inkább javítsd meg a checket — az ok legtöbbször a bind cím vagy egy redirect, és a javítás csak néhány percet vesz igénybe.
