# 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](/ro/blog/aws-app-runner-review-six-months): 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](/ro/blog/waf-for-a-nextjs-app-managed-rules-that-block-legit-traffic), 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.

<div class="article-figure">
<svg viewBox="0 0 900 260" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Șase moduri de eșec ale health check-ului, în grilă. TCP deschis, dar nu ready: instanța rece servește erori. 200 static: instanța stricată păstrează traficul. Check profund pe un API terț: o pană a vendorului scoate toată flota. Altă cale de cod: trece în timp ce request-urile reale pică. Rezultat cache-uit: un minut de minciună la unison. Blocat de WAF sau auth: instanțe sănătoase înlocuite în buclă.">
<g font-family="Inter,system-ui,sans-serif" font-size="11">
<rect x="20" y="20" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="155" y="44" text-anchor="middle" fill="#ff6b8a" font-weight="700">1 · TCP deschis ≠ ready</text><text x="155" y="66" text-anchor="middle" fill="#f1f3ff">portul se deschide în secunda 1</text><text x="155" y="84" text-anchor="middle" fill="#f1f3ff">rutele și pool-urile gata în secunda 40</text><text x="155" y="108" text-anchor="middle" fill="#9aa3c7">instanța rece servește 30 s de erori</text>
<rect x="315" y="20" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="450" y="44" text-anchor="middle" fill="#ff6b8a" font-weight="700">2 · un 200 static</text><text x="450" y="66" text-anchor="middle" fill="#f1f3ff">res.send('ok'), mereu</text><text x="450" y="84" text-anchor="middle" fill="#f1f3ff">pool epuizat, loop blocat: tot 200</text><text x="450" y="108" text-anchor="middle" fill="#9aa3c7">instanța stricată își păstrează partea de trafic</text>
<rect x="610" y="20" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="745" y="44" text-anchor="middle" fill="#ff6b8a" font-weight="700">3 · check profund pe un vendor</text><text x="745" y="66" text-anchor="middle" fill="#f1f3ff">503 dacă API-ul terț e jos</text><text x="745" y="84" text-anchor="middle" fill="#f1f3ff">fiecare instanță pică deodată</text><text x="745" y="108" text-anchor="middle" fill="#9aa3c7">o funcție degradată devine pană totală</text>
<rect x="20" y="145" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="155" y="169" text-anchor="middle" fill="#ff6b8a" font-weight="700">4 · altă cale de cod</text><text x="155" y="191" text-anchor="middle" fill="#f1f3ff">port intern, înainte de middleware</text><text x="155" y="209" text-anchor="middle" fill="#f1f3ff">calea reală stricată, check-ul tot trece</text><text x="155" y="233" text-anchor="middle" fill="#9aa3c7">sănătos pe hârtie, picat în producție</text>
<rect x="315" y="145" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="450" y="169" text-anchor="middle" fill="#ff6b8a" font-weight="700">5 · un rezultat cache-uit</text><text x="450" y="191" text-anchor="middle" fill="#f1f3ff">agregatul cache-uit 60 s</text><text x="450" y="209" text-anchor="middle" fill="#f1f3ff">se strică în secunda 1, minte 59</text><text x="450" y="233" text-anchor="middle" fill="#9aa3c7">toată flota minte la unison</text>
<rect x="610" y="145" width="270" height="105" rx="12" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="745" y="169" text-anchor="middle" fill="#ff6b8a" font-weight="700">6 · blocat de WAF sau auth</text><text x="745" y="191" text-anchor="middle" fill="#f1f3ff">fără User-Agent, fără header de auth → 403</text><text x="745" y="209" text-anchor="middle" fill="#f1f3ff">platforma citește 403 ca nesănătos</text><text x="745" y="233" text-anchor="middle" fill="#9aa3c7">instanțe sănătoase înlocuite în buclă</text>
</g>
</svg>
</div>

## 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.

```ts
// 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](/ro/blog/same-tag-deploy-and-the-deploy-script-without-ci) după un rollout și ce lipești în canalul de incident.

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Tabel de decizie pentru ce ar trebui să facă un health check. Instanța încă se încălzește: 503, platforma așteaptă. Event loop înțepenit: 503, platforma înlocuiește. Baza de date inaccesibilă: 200 cu db failed în body, pentru că o repornire n-o va repara și alte request-uri merg în continuare. API terț jos: 200 cu payments failed în body. Check blocat de WAF sau auth: trebuie exceptat explicit.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">Ce răspunde ruta, și de ce</text>
<g fill="#9aa3c7"><text x="20" y="54" font-weight="700" fill="#f1f3ff">condiție</text><text x="330" y="54" font-weight="700" fill="#f1f3ff">răspuns</text><text x="520" y="54" font-weight="700" fill="#f1f3ff">de ce</text></g>
<line x1="20" y1="62" x2="880" y2="62" stroke="#2a3150"/>
<text x="20" y="86" fill="#f1f3ff">încă încălzește cache-uri și pool-uri</text><text x="330" y="86" fill="#ffd166" font-weight="700">503 · starting</text><text x="520" y="86" fill="#9aa3c7">platforma așteaptă înainte să ruteze; nimic de înlocuit</text>
<text x="20" y="112" fill="#f1f3ff">lag pe event loop peste 1 s</text><text x="330" y="112" fill="#ff6b8a" font-weight="700">503 · wedged</text><text x="520" y="112" fill="#9aa3c7">o repornire e rezolvarea; lași platforma s-o facă</text>
<text x="20" y="138" fill="#f1f3ff">baza de date inaccesibilă</text><text x="330" y="138" fill="#4fffb0" font-weight="700">200 · db: failed</text><text x="520" y="138" fill="#9aa3c7">o repornire n-o repară; citirile din cache tot servesc</text>
<text x="20" y="164" fill="#f1f3ff">procesatorul de plăți jos</text><text x="330" y="164" fill="#4fffb0" font-weight="700">200 · payments: failed</text><text x="520" y="164" fill="#9aa3c7">o funcție degradată, nu tot serviciul</text>
<text x="20" y="190" fill="#f1f3ff">check-ul sosește fără auth sau User-Agent</text><text x="330" y="190" fill="#7b8cff" font-weight="700">exceptat explicit</text><text x="520" y="190" fill="#9aa3c7">în middleware și în WAF, restrâns la calea asta</text>
<line x1="20" y1="200" x2="880" y2="200" stroke="#2a3150"/>
</g>
</svg>
</div>

## 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](/contact). E o după-amiază, și de obicei e după-amiaza care închide tichetul „502-uri aleatorii după deploy”.
