Skip to content
Health check-uri care mint: șase moduri în care serviciul tău zice „ready” când nu e
← ← Înapoi la Idei Cloud

Health check-uri care mint: șase moduri în care serviciul tău zice „ready” când nu e

Un health check e un contract între aplicația ta și lucrul care îi rutează traficul. Router-ul întreabă „poți lua un request?” și aplicația răspunde. Când răspunsul e greșit, router-ul face exact ce i s-a spus: trimite utilizatori la o instanță care îi va lăsa baltă, sau încetează să trimită utilizatori la instanțe care erau în regulă. Ambele sunt pene, și ambele au fost cauzate de un health check care, tehnic, funcționa.

Am fost mușcați de șase varietăți distincte ale acestui lucru pe App Runner, ECS, Lambda în spatele unui API gateway și un cluster Kubernetes. Ăsta e catalogul, cum arată fiecare din exterior, și ruta de health la care am ajuns.

Două întrebări, nu una

Rădăcina majorității acestor probleme e că „sănătos” comprimă două întrebări diferite într-un singur endpoint.

Liveness: procesul e viu și ar trebui lăsat în pace? Răspunsul e „nu” doar când procesul e înțepenit: deadlock, fără memorie, în buclă. Răspunsul corect la un eșec de liveness e să omori și să înlocuiești instanța.

Readiness: instanța asta poate lua trafic chiar acum? Răspunsul e „nu” cât timp încă pornește, încălzește cache-uri, așteaptă un pool de conexiuni sau, temporar, când e supraîncărcată. Răspunsul corect e să încetezi să rutezi spre ea și să verifici din nou în curând. Nu s-o omori.

Kubernetes face cele două probe explicite. App Runner și ECS îți dau un singur health check și îl folosesc pentru ambele scopuri, ceea ce e în regulă atâta timp cât știi că un check picat duce la înlocuirea instanței, și proiectezi check-ul astfel încât să pice doar când înlocuirea e răspunsul corect.

Cele șase minciuni

1. TCP e deschis, deci suntem ready. Implicitul pe App Runner și pe target group-urile ECS e un check TCP: dacă portul acceptă o conexiune, instanța e sănătoasă. Node deschide portul în prima secundă de pornire, înainte ca framework-ul să încarce rutele, înainte să existe pool-ul de bază de date, înainte ca cache-urile să fie calde. Instanța intră în rotație și servește primele treizeci de secunde de trafic lent sau cu erori. Am scris despre asta pe App Runner: rezolvarea e un check HTTP pe o rută care întoarce 503 până când pornirea e chiar completă.

2. Un 200 static. Greșeala opusă. app.get('/health', (req, res) => res.send('ok')). Nu pică niciodată. O instanță al cărei pool de bază de date e epuizat, al cărei event loop e blocat secunde întregi, al cărei disc e plin, raportează sănătos la nesfârșit, iar router-ul continuă să-i trimită trafic în timp ce instanța de lângă ea, care e în regulă, își primește și ea partea. Ăsta e health check-ul pe care îl au majoritatea codebase-urilor, pentru că a fost primul pe care l-a scris cineva.

3. Check-ul profund care pică la pana altcuiva. Supracorectarea lui 2: ruta de health pinguie baza de date, cache-ul, coada și un API terț, și întoarce 503 dacă oricare pică. Apoi API-ul terț are o oră proastă, fiecare instanță raportează nesănătos, platforma le înlocuiește pe toate, înlocuirile raportează și ele nesănătos, și ai transformat o funcție degradată într-o pană totală a unui serviciu care ar fi putut continua să servească fiecare request care n-avea nevoie de acel API. Readiness-ul nu trebuie să depindă de lucruri pe care instanța nu le poate repara repornind.

4. Check-ul lovește altă cale de cod decât traficul. Ruta de health e înregistrată pe un port intern separat, sau înaintea stivei de middleware, sau pe o cale exclusă din router-ul prin care trec request-urile reale. Trece în timp ce calea reală e stricată: un deploy prost de middleware, un router care nu se încarcă, o configurație TLS greșită pe portul public. Check-ul trebuie să treacă prin cât mai mult din calea reală fără să facă muncă reală.

5. Rezultatul cache-uit. Ca să facă check-ul ieftin, cineva îi cache-uiește rezultatul șaizeci de secunde. O instanță care s-a stricat în secunda unu raportează sănătos încă cincizeci și nouă, iar o problemă la nivel de cluster e invizibilă un minut, timp în care fiecare instanță minte la unison. Cache-uiește sub-check-urile scumpe dacă trebuie, cu TTL scurt, dar niciodată agregatul.

6. Check-ul care e blocat. Ruta de health n-are User-Agent, vine dintr-o plajă de IP-uri interne și nu poartă header-ul de auth, așa că regula NoUserAgent_HEADER din WAF, sau rate limiter-ul, sau middleware-ul de auth o respinge, iar platforma vede un 403 ca nesănătos. Fiecare instanță e înlocuită în buclă până observă cineva metricile WAF. Health check-urile au nevoie de un bypass explicit în orice stă în fața aplicației, restrâns la sursa platformei și doar la calea de health.

1 · TCP deschis ≠ readyportul se deschide în secunda 1rutele și pool-urile gata în secunda 40instanța rece servește 30 s de erori 2 · un 200 staticres.send('ok'), mereupool epuizat, loop blocat: tot 200instanța stricată își păstrează partea de trafic 3 · check profund pe un vendor503 dacă API-ul terț e josfiecare instanță pică deodatăo funcție degradată devine pană totală 4 · altă cale de codport intern, înainte de middlewarecalea reală stricată, check-ul tot trecesănătos pe hârtie, picat în producție 5 · un rezultat cache-uitagregatul cache-uit 60 sse strică în secunda 1, minte 59toată flota minte la unison 6 · blocat de WAF sau authfără User-Agent, fără header de auth → 403platforma citește 403 ca nesănătosinstanțe sănătoase înlocuite în buclă

Ruta de health la care am ajuns

O singură rută, /api/health, cu un parametru de query care selectează adâncimea, pentru că check-ul platformei și check-ul unui om vor lucruri diferite.

// app/api/health/route.ts
const startedAt = Date.now();
let warm = false;                       // setat true de task-ul de warm-up după ce se încarcă cache-urile

export async function GET(req: Request) {
  const deep = new URL(req.url).searchParams.get('deep') === '1';

  // readiness: doar lucruri pe care le-ar repara o repornire
  if (!warm) return json({ status: 'starting', uptimeMs: Date.now() - startedAt }, 503);
  if (eventLoopLagMs() > 1000) return json({ status: 'wedged' }, 503);

  const checks: Record<string, string> = { app: 'ok', release: process.env.RELEASE_SHA ?? 'dev' };

  if (deep) {
    // doar informativ: nu transformă niciodată răspunsul într-un 503
    checks.db = await timed(() => db.query('select 1'), 500);
    checks.cache = await timed(() => cache.ping(), 200);
    checks.payments = await timed(() => payments.ping(), 800);
  }
  return json({ status: 'ok', ...checks }, 200);
}

Proprietățile care contează:

  • Întoarce 503 din exact două motive, ambele reparate de o repornire: instanța n-a terminat de încălzit, sau event loop-ul e înțepenit. Asta e readiness și liveness într-o singură rută, cu condițiile de eșec alese astfel încât „înlocuiește instanța asta” să fie mereu reacția corectă la un 503.
  • Dependențele sunt raportate, niciodată impuse. Cu ?deep=1, ruta verifică baza de date, cache-ul și procesatorul de plăți, fiecare cu timeout, și pune rezultatul în body. Un om sau un dashboard îl citește. Platforma nu apelează varianta profundă, deci o pană a vendorului nu poate dărâma flota. Dacă baza de date e inaccesibilă, instanța tot zice 200, pentru că repornirea ei nu va repara baza de date, iar request-urile care n-au nevoie de ea (pagini statice, citiri din cache) merg în continuare.
  • Rulează prin calea reală. Același port, aceeași stivă de middleware, același router ca traficul utilizatorilor. Middleware-ul are un early return explicit pentru calea asta care sare peste auth și rate limiter, dar nimic altceva, iar WAF-ul are un scope-down care o exceptează de la regula de User-Agent pentru sursa de health check a platformei.
  • Nimic nu e cache-uit. Ruta e ieftină: două citiri din memorie pentru versiunea superficială. Versiunea profundă face I/O real și e apelată de oameni, rar.
  • Raportează release-ul. Body-ul poartă SHA-ul git, adică ce verifică scriptul de deploy după un rollout și ce lipești în canalul de incident.
Ce răspunde ruta, și de ce condițierăspunsde ce încă încălzește cache-uri și pool-uri503 · startingplatforma așteaptă înainte să ruteze; nimic de înlocuit lag pe event loop peste 1 s503 · wedgedo repornire e rezolvarea; lași platforma s-o facă baza de date inaccesibilă200 · db: failedo repornire n-o repară; citirile din cache tot servesc procesatorul de plăți jos200 · payments: failedo funcție degradată, nu tot serviciul check-ul sosește fără auth sau User-Agentexceptat explicitîn middleware și în WAF, restrâns la calea asta

Platformă cu platformă

Platformă Ce suportă Ce configurezi
App Runner Un singur check, TCP sau HTTP, eșec = înlocuire HTTP pe /api/health, interval 10 s, prag de nesănătos 3, timeout 5 s
ECS pe Fargate în spatele unui ALB Health check de container (task definition) + health check de target group Ambele HTTP pe aceeași rută; target group-ul e ce controlează traficul, check-ul de container e ce declanșează înlocuirea
Lambda în spatele API Gateway Fără health check; fiecare invocare e propria instanță Provisioned concurrency pentru problema de warm-up; un canary sintetic care apelează /api/health?deep=1 pentru problema de vizibilitate
Kubernetes Probe separate de liveness și readiness, plus o probă de startup Readiness pe /api/health, liveness pe aceeași cu perioadă mai lungă, probă de startup cu prag de eșec generos ca pornirile lente să nu fie omorâte

Pe fiecare dintre ele, numărul care contează cel mai mult e pragul de nesănătos înmulțit cu intervalul: atât continuă o instanță stricată să servească înainte să fie scoasă. Zece secunde ori trei înseamnă treizeci de secunde de request-uri proaste. Preferăm 5 × 2 și plătim apelurile de check în plus, care sunt gratuite.

Versiunea scurtă

Un health check e o promisiune. Fă-l să pice doar când o repornire e leacul, raportează tot restul în body pentru oameni, rulează-l prin calea reală de request, exceptează-l explicit de la orice păzește calea aia și nu cache-ui niciodată răspunsul. Apoi setează intervalul și pragul astfel încât o minciună, când tot se întâmplă, să dureze zece secunde, nu un minut.

Dacă ruta ta de health e un res.send('ok'), te ajutăm s-o înlocuiești. E o după-amiază, și de obicei e după-amiaza care închide tichetul „502-uri aleatorii după deploy”.