Health check eșuează, dar aplicația funcționează
Când un health check eșuează la deployment, deși aplicația funcționează corect local, cauza este de obicei una dintre cele cinci. Verifică binding-ul, calea, portul, timpul de pornire și dependențele în ordinea care duce cel mai rapid la soluție.
Există un anumit tip de blocaj în care un health check care eșuează la deployment blochează fiecare release, deși aplicația este, după toate măsurătorile pe care le poți face, perfect funcțională. Rulează local. Rulează în Docker local. Logurile arată că ascultă conexiuni. Iar platforma raportează eșec după eșec, uneori chiar douăzeci la rând, fără ca vreo cerere să apară vreodată în access log.
Ultimul detaliu este important și restrânge imediat aria problemei. Dacă aplicația nu a înregistrat niciodată cererea, înseamnă că verificarea nu a ajuns la aplicație — deci nimic din codul aplicației nu va explica problema.
Iată cele cinci cauze, în ordinea care duce cel mai rapid la identificarea problemei.
1. Aplicația ascultă doar pe localhost
Aceasta este de departe cea mai frecventă cauză și explică exact simptomul „nu ajunge niciodată trafic”.
Într-un container, 127.0.0.1 înseamnă loopback-ul propriului container. Un health check venit din afara containerului nu poate ajunge la el. Procesul ascultă conexiuni, logurile confirmă acest lucru, dar socket-ul nu poate fi accesat din locul care contează.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
Framework-urile au valori implicite diferite, iar mai multe dintre ele au schimbat valoarea implicită între versiunile majore. Verifică adresa pe care framework-ul tău o folosește efectiv, nu pe cea pe care îți amintești că o folosește.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Dacă adresa pe care procesul ascultă este 127.0.0.1:3000 în loc de 0.0.0.0:3000, ai găsit problema și nimic altceva din această listă nu mai contează.
2. Portul verificat de platformă nu este portul pe care servești aplicația
Sunt implicate două porturi și este ușor să le confunzi: portul pe care procesul ascultă în interiorul containerului și portul către care face platforma routing. Dacă aplicația citește PORT din environment și ai hardcodat 3000 undeva în Dockerfile, cele două porturi pot să nu coincidă fără ca acest lucru să fie evident.
Abordarea sigură este să lași platforma să îți spună ce port să folosești:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Apoi configurează portul serviciului o singură dată, în platformă, și nu mai întreține aceeași valoare în două locuri.
3. Calea returnează altceva decât un răspuns de succes
O cale de health check este potrivită exact, iar un număr surprinzător de mare de eșecuri este cauzat de un redirect. Dacă aplicația redirecționează /healthz către /healthz/ sau forțează HTTPS printr-un 301, un checker care consideră drept succes doar răspunsurile 2xx va eșua de fiecare dată, în timp ce browserul urmărește redirectul și îți afișează o pagină funcțională.
Trei capcane concrete:
- Redirecturi cauzate de slash-ul final.
/healthz→/healthz/este un 301. - HTTPS forțat. Verificarea internă ajunge de obicei prin HTTP simplu, pe loopback. Un redirect necondiționat către HTTPS o face să eșueze.
- Middleware de autentificare. Un guard global de autentificare care rulează înainte de routing va returna 401 și pentru calea de health check.
Exclude explicit calea de health check atât din autentificare, cât și din forțarea HTTPS. Este ruta care ar trebui să fie cât mai simplă.
4. Verificarea se execută mai repede decât pornirea la rece
Dacă verificarea eșuează de câteva ori și apoi reușește sau eșuează la deploy, dar reușește când o reiei, problema ține de timing, nu de configurare.
Bugetul de care ai nevoie nu este durata unei singure încercări — este intervalul × numărul de retry-uri. O aplicație care are nevoie de douăsprezece secunde pentru a se conecta la baza de date și a încălzi un cache are nevoie de un buget total mai mare de douăsprezece secunde; altfel vei eșua la fiecare release și, în cele din urmă, vei dezactiva gate-ul, eliminând singurul lucru care protejează utilizatorii de un build defect.
dockup info my-project/my-api --json | grep -A6 healthCheck
Setează timeout-ul peste durata celei mai lente încercări legitime și configurează numărul de retry-uri astfel încât intervalul × numărul de retry-uri să depășească confortabil cea mai lentă pornire legitimă. Măsoară durata pornirii în loc să o estimezi — logurile au timestamp-uri.
5. Aplicația chiar nu este pregătită
Ultimul caz este motivul pentru care există health check-ul: aplicația a pornit, nu a putut ajunge la o dependență și încearcă din nou. Nu s-a oprit, deci nimic nu o repornește. Nu poate răspunde, deci verificarea eșuează. Sistemul funcționează exact așa cum a fost proiectat și îți spune că acest release nu ar trebui să primească trafic.
Diferența față de celelalte patru cazuri o vezi prin faptul că aplicația ta a înregistrat cererea și a răspuns cu un status non-2xx. Dacă cererea apare în loguri, cauzele 1–3 sunt eliminate.
Ordinea de diagnosticare care economisește timp
# 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
Pasul 3 este cel care rezolvă majoritatea acestor probleme. Un curl executat din interiorul containerului elimină dintr-o dată toate variabilele de rețea: dacă returnează 200 acolo, iar platforma continuă să eșueze, problema este adresa sau portul, nu aplicația. Dacă returnează un 301 sau un 401, ai găsit cauza fără să modifici deloc platforma.
De ce merită să păstrezi gate-ul
După al patrulea deploy eșuat, este tentant să dezactivezi health check-ul și să livrezi release-ul. Merită să îți amintești ce dezactivezi.
În Dockup, health gate-ul este mecanismul care împiedică un release defect să ajungă la utilizatori. Noua versiune este construită și pornită în timp ce versiunea curentă continuă să servească trafic; traficul este mutat doar după ce noua versiune răspunde. Dacă dezactivezi gate-ul, reactivezi scenariul în care un container care pornește, dar nu poate funcționa, îl înlocuiește pe unul care funcționa corect.
O verificare care eșuează la patru release-uri consecutive este enervantă. O verificare care trece necondiționat este o verificare care nu va opri deployment-ul care contează.
Întrebări frecvente
De ce eșuează health check-ul când aplicația funcționează local?
Aproape întotdeauna deoarece containerul ascultă pe 127.0.0.1 în loc de 0.0.0.0. Local te conectezi prin același loopback; din afara containerului, adresa nu poate fi accesată.
Endpoint-ul de health ar trebui să necesite autentificare? Nu. Exclude-l din middleware-ul global de autentificare, altfel checker-ul primește un 401, iar deploy-ul eșuează, deși aplicația funcționează.
Ce timeout ar trebui să folosesc? Un timeout mai mare decât durata celei mai lente încercări legitime, cu suficiente retry-uri pentru a acoperi cea mai lentă pornire la rece legitimă. Citește durata pornirii din loguri în loc să o estimezi.
Este sigur să dezactivez health check-ul pentru a debloca un release? Deblochează release-ul și elimină protecția care împiedică o versiune defectă să primească trafic. Repară verificarea în schimb — în majoritatea cazurilor, cauza este o adresă de binding sau un redirect, iar remedierea durează câteva minute.
