IlmHamroh
JavaScript Full-stack/30-qism. NestJS41/65-dars21 daqiqa
Mundarija (30)

Health check'lar NestJS'da: @nestjs/terminus — /health/live va /health/ready

Qisqacha: Health check — "ilova sog'mi?" savoliga javob beradigan endpoint. Ikki xil savol bor. Liveness (/health/live) — jarayon tirikmi, qotib qolmaganmi. Readiness (/health/ready) — so'rov qabul qilishga tayyormi (baza javob beryaptimi). @nestjs/terminus buni HealthCheckService.check([...]) va tayyor indikatorlar (xotira, disk, Prisma, TypeORM) bilan qiladi: hammasi yaxshi — 200, birortasi down — 503. Kanon tuzoqni topdi: umumiy xato filtri terminus'ning 503 ini 500 ga aylantirib yuborardi.

Bu darsda

  • Liveness va readiness farqini va nega live bazaga bog'lanmasligini tushuntirasiz.
  • TerminusModule, HealthCheckService va @HealthCheck() bilan endpoint yozasiz.
  • HealthIndicatorService bilan o'z indikatoringizni yasab, up, down va degraded holatlarini qaytarasiz.
  • Osilib qolgan tekshiruvni timeout bilan cheklaysiz.
  • vazifalar-api ga /health/live va /health/ready qo'shib, xato filtrini 503 uchun tuzatasiz.

Oldin bilishingiz kerak: Exception filter'lar, Prisma bilan NestJS, Hodisalar: @nestjs/event-emitter, Node xotira limitlari.

Oldingi darsda domen hodisalarini qo'shdik. Bu darsda loyiha bosqichining uchinchi qismi: salomatlik tekshiruvi. Bu endpoint'lardan hozir faqat biz foydalanamiz (curl). 33-qismda ular Docker va orkestratorga kerak bo'ladi — o'shanda tayyor turadi.

1. Nega bu kerak?

Serverda vazifalar-api ishlayapti. Bir kuni Postgres qayta ishga tushdi va 30 soniya javob bermadi. Nima bo'ladi? Node jarayoni tirik, port ochiq, curl javob oladi — lekin har GET /v1/vazifalar 500 qaytaradi. Kuzatuvchi dastur "server ishlayapti" deydi, foydalanuvchilar esa xato ko'radi.

Yoki boshqa holat: jarayon tirik, lekin event loop bloklangan — cheksiz sikl. Port ochiq, ammo hech qanday so'rovga javob yo'q.

Bu ikki holatda kerakli harakat har xil:

  • Event loop qotdi — jarayonni qayta ishga tushirish kerak. U o'zi tuzalmaydi.
  • Baza uzildi — jarayonni qayta ishga tushirish yordam bermaydi. Baza qaytguncha bu nusxaga yangi so'rov yubormaslik kerak.

Shuning uchun ikki xil savol, ikki xil endpoint.

O'xshatish: navbatchi shifokor bemorni ikki savol bilan tekshiradi. "Nafas olyaptimi?" — yo'q bo'lsa, darhol reanimatsiya. "Bugun ishga chiqa oladimi?" — isitmasi bor bo'lsa, uyda qoladi, lekin reanimatsiya kerak emas. Ikkinchi savolga "yo'q" degan javob birinchi savoldagi harakatni talab qilmaydi.

2. Liveness va readiness

2.1 Farqi

Liveness (/health/live) Readiness (/health/ready)
Savol jarayon tirikmi? so'rov qabul qila oladimi?
Nimani tekshiradi hech narsa — javob berdi baza, kesh, xotira
Yiqilsa orkestrator jarayonni qayta ishga tushiradi trafikni boshqa nusxaga yuboradi
Tez-tez har 10–30 s har 5–10 s

Liveness (tiriklik tekshiruvi) — jarayon javob berdimi, yetarli. Javob kelmasa yoki uzoq kechiksa, jarayon qotgan.

Readiness (tayyorlik tekshiruvi) — bog'liqliklar javob beryaptimi. Yo'q bo'lsa, nusxa vaqtincha "navbatdan chiqadi", lekin o'ldirilmaydi. Baza qaytgach, tekshiruv yana 200 beradi va trafik qaytadi.

Uchinchi turi ham bor — startup (ishga tushish tekshiruvi): sekin ishga tushadigan ilovaga "hali tayyorlanyapman" deyish imkoni. Kubernetes uni alohida probe sifatida biladi. Bizning loyihada readiness shu vazifani ham bajaradi.

2.2 Eng ko'p uchraydigan xato: live da baza

Tasavvur qiling: /health/live bazani tekshiradi. Baza 30 soniya uzildi. Orkestrator "jarayon o'lgan" deb hamma nusxalarni birdaniga qayta ishga tushiradi. Baza qaytdi — lekin hozir hamma nusxa sovuq startda (NestJS nima va nega darsida o'lchagandik: ~5 s). Natija: 30 soniyalik baza uzilishi o'rniga butun tizim bir daqiqa ishlamadi.

Qoida: live — hech qanday tashqi bog'liqliksiz. Kanon izohida ham shunday yozilgan: "bazaga bog'lamang — baza uzilsa, qayta ishga tushirish yordam bermaydi".

Tekshirib ko'ring: Redis kesh uzildi, lekin ilova keshsiz ham ishlay oladi (sekinroq). Redis tekshiruvini /health/ready ga qo'shish kerakmi?

Javob

Odatda yo'q. Readiness "so'rov qabul qila olamanmi?" degan savol. Kesh yo'q bo'lsa ham ilova javob beradi — demak tayyor. Redis'ni ready'ga qo'shsangiz, kesh uzilganda hamma nusxa trafikdan chiqadi va ilova umuman ishlamay qoladi — keshsiz ishlash imkoniyati behuda ketadi. Kesh holatini alohida monitoring kuzatsin yoki uni "degraded" deb ko'rsating (keyingi bo'limlarda).

3. @nestjs/terminus

3.1 O'rnatish va eng kichik endpoint

bash
npm i --save-exact @nestjs/terminus@12.1.0

Biz demo loyihada (NestJS 12.2.0, @nestjs/terminus 12.1.0) controller yozdik:

ts
import { Controller, Get, Module } from "@nestjs/common";
import {
  HealthCheck, HealthCheckService, TerminusModule,
} from "@nestjs/terminus";

@Controller("health")
class SalomatlikController {
  constructor(private readonly salomatlik: HealthCheckService) {}

  @Get("live")
  @HealthCheck()
  tirik() {
    return this.salomatlik.check([]);
  }
}

@Module({
  imports: [TerminusModule],
  controllers: [SalomatlikController],
})
class Ilova {}

check([]) — bo'sh ro'yxat: hech narsa tekshirilmaydi. Liveness uchun aynan shu kerak. So'rov (biz ishga tushirdik):

text
GET /health/live
  → 200 {"status":"ok","info":{},"error":{},"details":{}}

Javob formati terminus'ning standarti. To'rt maydon:

Maydon Ichida
status "ok", "degraded" yoki "error"
info up va degraded indikatorlar
error down indikatorlar
details hammasi birga

3.2 @HealthCheck() nima qiladi

Bu dekorator ikkita ish qiladi (paket manbasini ochib ko'rdik). Birinchisi — javobga Cache-Control: no-cache, no-store, must-revalidate sarlavhasini qo'yadi (@Header orqali). Salomatlik javobi hech qachon keshlanmasligi kerak: oraliq proxy eski "ok" ni qaytarsa, tekshiruv ma'nosini yo'qotadi. Ikkinchisi — @nestjs/swagger o'rnatilgan bo'lsa, OpenAPI'ga 200 va 503 javob sxemalarini yozadi (Swagger).

3.3 Indikatorlar

Indikator (health indicator) — bitta bog'liqlikni tekshiradigan funksiya. check() ga ularning ro'yxati beriladi. Terminus tayyor indikatorlar bilan keladi:

Indikator Nimani tekshiradi
MemoryHealthIndicator heap yoki RSS chegaradan kichikmi
DiskHealthIndicator disk to'lmaganmi (foiz yoki bayt)
PrismaHealthIndicator Prisma bazasi SELECT 1 ga javob beradimi
TypeOrm…, Mongoose…, Sequelize…, MikroOrm… o'sha ORM ulanishi
HttpHealthIndicator tashqi URL (@nestjs/axios kerak)
MicroserviceHealthIndicator, GRPCHealthIndicator mikroservis transporti

Biz xotira va diskni sinadik:

ts
() => this.xotira.checkHeap("xotira", 512 * 1024 * 1024),
() => this.disk.checkStorage("disk", {
  path: "C:\\", thresholdPercent: 0.999,
}),
text
GET /health/disk
  → 200 {"status":"ok","info":{"disk":{"status":"up"}},"error":{},"details":{"disk":{"status":"up"}}}

checkHeap("xotira", 512 MB) — V8 heap'ida ishlatilgan xotira 512 MB dan oshsa down (Node xotira limitlari). Xotira sizib (leak) borayotgan nusxa shunda trafikdan chiqadi. HttpHealthIndicator ni sinamadik — u @nestjs/axios paketini talab qiladi va loyihada yo'q.

4. O'z indikatoringiz

4.1 HealthIndicatorService

Tayyor indikator har doim ham mos kelmaydi. Loyihada ombor besh xil bo'lishi mumkin: xotira, fayl, Postgres, Prisma, kesh. PrismaHealthIndicator faqat Prisma'ni biladi. Bizga "ombor javob beradimi?" degan umumiy savol kerak.

Terminus'ning yangi API'si — HealthIndicatorService. U check(kalit) bilan seans ochadi, seans esa uch holatdan birini qaytaradi: up(...), down(...) yoki degraded(...). Qavs ichidagi obyekt javobga qo'shiladi. Demoda ombor holatini o'zimiz boshqardik:

ts
@Injectable()
class OmborIndikatori {
  constructor(private readonly indikator: HealthIndicatorService) {}
  async tekshir(kalit: string) {
    const s = this.indikator.check(kalit);
    if (holat.ombor === "osilgan") await new Promise(() => {});
    if (holat.ombor === "down") {
      return s.down({ xato: "ulanish uzildi" });
    }
    if (holat.ombor === "sekin") return s.degraded({ ms: 1800 });
    return s.up({ ms: 3 });
  }
}

/health/ready ga ikki indikator berdik — ombor va xotira — va omborni to'rt holatda so'radik:

text
GET /health/ready (ombor: up)
  → 200 {"status":"ok","info":{"ombor":{"ms":3,"status":"up"},"xotira":{"status":"up"}},"error":{},"details":{"ombor":{"ms":3,"status":"up"},"xotira":{"status":"up"}}}
GET /health/ready (ombor: sekin)
  → 200 {"status":"degraded","info":{"ombor":{"ms":1800,"status":"degraded"},"xotira":{"status":"up"}},"error":{},"details":{"ombor":{"ms":1800,"status":"degraded"},"xotira":{"status":"up"}}}
GET /health/ready (ombor: down)
ERROR Health Check has failed! {"xotira":{"status":"up"},"ombor":{"xato":"ulanish uzildi","status":"down"}}
  → 503 {"status":"error","info":{"xotira":{"status":"up"}},"error":{"ombor":{"xato":"ulanish uzildi","status":"down"}},"details":{"xotira":{"status":"up"},"ombor":{"xato":"ulanish uzildi","status":"down"}}}
GET /health/ready (ombor: osilgan)
  → TimeoutError

To'rt qatorni ketma-ket o'qing:

  1. up — 200, status: "ok". Qo'shgan ms maydonimiz javobda.
  2. degraded — 200, lekin status: "degraded". Bu "ishlayapman, lekin yomon": trafik qabul qilinadi, monitoring esa ogohlantiradi. Kesh uzilgan holat uchun aynan mos.
  3. down — 503, status: "error", ombor error ga tushdi. Xotira esa info da qoldi — qaysi qism buzilgani aniq ko'rinadi. Terminus logga Health Check has failed! deb yozdi.
  4. osilgan — javob umuman kelmadi. Mijoz 500 ms kutib, TimeoutError oldi.

4.2 Osilgan tekshiruv

To'rtinchi holat eng xavfli. Indikator ichidagi await hech qachon tugamasa, check() ham tugamaydi. Terminus o'zi timeout qo'ymaydi. Orkestrator 5 soniyadan keyin o'zi uzadi — lekin har tekshiruv serverda osilgan so'rov va ochiq ulanish qoldiradi. Bazaga ulanish hovuzi (pool) to'lsa, aynan shunday bo'ladi.

Yechim — har indikatorga muddat. Ikki yo'l bor. Birinchisi — Promise.race bilan qo'lda (loyihada shunday). Ikkinchisi — terminus 12 ning attempt(...).withTimeout(ms) zanjiri:

ts
urinish(kalit: string) {
  return this.indikator
    .check(kalit)
    .attempt(async ({ signal }) => {
      // kut — 1000 ms li ish; signal bekor qilinsa, u ham to'xtaydi
      await kut(1000, signal);
      return { ms: 1000 };
    })
    .withTimeout(200);
}
text
GET /health/urinish (attempt + withTimeout(200))
ERROR Health Check has failed! {"ombor":{"message":"timeout of 200ms exceeded","responseTime":211,"status":"down"}}
  → 503 {"status":"error","info":{},"error":{"ombor":{"message":"timeout of 200ms exceeded","responseTime":211,"status":"down"}},"details":{"ombor":{"message":"timeout of 200ms exceeded","responseTime":211,"status":"down"}}}

200 ms dan keyin indikator down bo'ldi, message va responseTime ni terminus o'zi qo'shdi (211 — o'lchangan ms, sizda boshqacha bo'ladi). signal — AbortController signali: muddat tugaganda u bekor qilinadi, tekshiruv ichidagi ishni to'xtatish mumkin. Zanjirda yana cacheFor(ms) bor — natijani bir muddat keshlash (3-mashqda o'zimiz yozamiz).

Ikkinchi yo'l chiroyliroq. Kanon esa birinchisini tanlagan: withTimeout terminus 12 da paydo bo'lgan va kanon yozilgan paytda bizning ombor indikatori allaqachon Promise.race bilan yozilgan edi. Ikkalasi ham to'g'ri. Tayyor indikatorlarning eski timeout sozlamasi esa paket ichida @deprecated deb belgilangan — "o'rniga .withTimeout(ms) ni zanjirlang".

Tekshirib ko'ring: Readiness indikatorining muddati 2 soniya, orkestrator esa tekshiruvni 1 soniyadan keyin uzadi. Nima bo'ladi?

Javob

Baza sekinlashsa (masalan 1.5 s), orkestrator 1 soniyada "javob yo'q" deb hisoblaydi, garchi indikator hali up qaytarishi mumkin bo'lsa ham. Bundan ham yomoni — serverda tekshiruv yana bir soniya ishlaydi, hech kim natijani kutmaydi. Qoida: indikator muddati orkestrator muddatidan qisqa bo'lsin. Shunda server o'zi aniq down va sababini qaytaradi.

5. 503 va xato filtri — kanon topgan xato

Terminus down holatida ServiceUnavailableException tashlaydi — Nest'ning 503 li HttpException i. Nest'ning sukut filtri uni to'g'ri qaytaradi: 503 va terminus formati.

Lekin vazifalar-api da o'zimizning global filtr bor (Exception filter'lar). U hamma xatoni Problem Details'ga aylantiradi. Qoidasi: IlovaXatosi — o'z statusi bilan; 500 dan kichik HttpException — statusi bilan; qolgan hammasi — "kutilmagan" 500. 503 esa 500 dan kichik emas. Natija: terminus "tayyor emas" dedi, filtr esa uni "server buzildi" ga aylantirdi.

Demoda shunday filtr bilan tekshirdik:

ts
@Catch()
class HammasiFiltr implements ExceptionFilter {
  catch(_x: unknown, host: ArgumentsHost) {
    host.switchToHttp().getResponse<Response>().status(500)
      .json({ status: 500, detail: "Kutilmagan xato" });
  }
}
text
umumiy @Catch() filtri bilan: GET /health/ready (down)
ERROR Health Check has failed! {"xotira":{"status":"up"},"ombor":{"xato":"ulanish uzildi","status":"down"}}
  → 500 {"status":500,"detail":"Kutilmagan xato"}

Ikki zarar bor. Orkestrator uchun 500 ham "tayyor emas" (200 emas) — bu yerda omad. Lekin monitoring 500 ni "dasturchi xatosi" deb ko'radi va navbatchini tunda uyg'otadi. Bundan ham yomoni — tekshiruvlar ma'lumoti yo'qoldi: qaysi bog'liqlik buzilgani javobda yo'q. Loyiha qadamida filtrga 503 uchun alohida tarmoq qo'shamiz va shu xatoni test bilan qotiramiz.

flowchart LR
  T["terminus: ombor down"] --> X["ServiceUnavailableException 503"]
  X --> F{"Problem filtri"}
  F -->|"eski: 500 emasmi?"| K["500 Kutilmagan xato"]
  F -->|"yangi: 503 tarmog'i"| P["503 + tekshiruvlar"]

Diagrammadagi ikki yo'lni solishtiring: bitta if farqi — javob butunlay boshqa ma'noga ega.

6. JS modelida terminus

Terminus'ning asosiy ishi — indikatorlarni parallel yurgizish, har biriga muddat qo'yish va natijani to'rt maydonga yig'ish. Paketsiz modelda yozamiz — blok brauzerda ishlaydi:

js
const kut = (ms, qiymat) =>
  new Promise((ok) => setTimeout(() => ok(qiymat), ms));

function muddat(ms) {
  let t;
  const p = new Promise((_, rad) => {
    const xato = new Error(`${ms} ms ichida javob yo'q`);
    t = setTimeout(() => rad(xato), ms);
  });
  return { p, bekor: () => clearTimeout(t) };
}

async function tekshir(tekshiruvlar, kutishMs) {
  const info = {};
  const error = {};
  const details = {};
  const ishlar = Object.entries(tekshiruvlar).map(async ([k, fn]) => {
    const m = muddat(kutishMs);
    try {
      const q = await Promise.race([fn(), m.p]);
      details[k] = info[k] = { ...q, status: "up" };
    } catch (e) {
      details[k] = error[k] = { xato: e.message, status: "down" };
    } finally {
      m.bekor();
    }
  });
  await Promise.all(ishlar); // hammasi parallel
  const ok = Object.keys(error).length === 0;
  const tana = { status: ok ? "ok" : "error", info, error, details };
  return `${ok ? 200 : 503} ${JSON.stringify(tana)}`;
}

console.log(await tekshir({}, 50)); // live
console.log(await tekshir({ ombor: () => kut(5, { ms: 5 }) }, 50));
console.log(await tekshir({
  ombor: () => Promise.reject(new Error("ulanish uzildi")),
}, 50));
const osilgan = () => new Promise(() => {});
console.log(await tekshir({ ombor: osilgan }, 50));

Konsolda:

text
200 {"status":"ok","info":{},"error":{},"details":{}}
200 {"status":"ok","info":{"ombor":{"ms":5,"status":"up"}},"error":{},"details":{"ombor":{"ms":5,"status":"up"}}}
503 {"status":"error","info":{},"error":{"ombor":{"xato":"ulanish uzildi","status":"down"}},"details":{"ombor":{"xato":"ulanish uzildi","status":"down"}}}
503 {"status":"error","info":{},"error":{"ombor":{"xato":"50 ms ichida javob yo'q","status":"down"}},"details":{"ombor":{"xato":"50 ms ichida javob yo'q","status":"down"}}}

Birinchi uch qator Nest chiqishi bilan bir xil shaklda. To'rtinchisi esa Nest'dagi TimeoutError dan yaxshiroq: osilgan tekshiruv o'rniga aniq 503 va sabab. Loyihadagi OmborIndikatori aynan shu Promise.race naqshini ishlatadi. finally dagi bekor() muhim: usiz har tekshiruvdan keyin keraksiz taymer 50 ms osilib turadi.

Soddalashtirilgan joyi: degraded holati yo'q (2-mashqda qo'shamiz) va xato logga yozilmaydi.

7. Express'da qanday edi → Nest'da qanday

25–27-qismlardagi vazifalar-api da alohida health endpoint yo'q edi. Express'da yozganimizda u oddiy marshrut bo'lardi: app.get("/health/ready", ...) ichida await ombor.soni({}) va try/catch. Nest'da terminus beradigan narsa:

Express (qo'lda) Nest + terminus
Javob formati o'zingiz o'ylab topasiz standart: status/info/error/details
Bir nechta tekshiruv Promise.all qo'lda check([...])
Tayyor tekshiruvlar yo'q xotira, disk, ORM'lar
Kesh sarlavhasi unutish oson @HealthCheck() o'zi

Nima yutdik: format standart — monitoring vositalari uni taniydi; bir qatorda yangi indikator qo'shiladi. Nima yo'qotdik: yana bitta prod bog'liqlik (jami 22 ta) va terminus'ning 503 xatosini filtrimiz bilan kelishtirish ishi.

8. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
live bazani tekshiradi baza uzilsa hamma nusxa qayta ishga tushadi live — tekshiruvsiz
Indikatorda muddat yo'q so'rov osiladi Promise.race yoki withTimeout
Umumiy filtr 503 ni 500 qiladi monitoring yolg'on, sabab yo'qoladi filtrda 503 tarmog'i
/health ga auth guard tushdi orkestrator 401 oladi guard'dan ozod qiling
/health versiyalandi (/v1/health) orkestrator sozlamasi API versiyasiga bog'lanadi VERSION_NEUTRAL
Har tekshiruvda og'ir so'rov baza probe'lardan charchaydi yengil so'rov, kesh

To'rtinchi qatorni 30-qismning auth darslarida uchratamiz: global guard yoqilganda kanon aynan shu xatoni topgan.

9. Mashqlar

Mashqlar kurs/mashqlar/30/41-salomatlik/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Jarayon tirikligini tekshiradigan endpoint —
  • Baza uzilganda qaysi endpoint 503 qaytarishi kerak? —
  • Terminus down holatida qaytaradigan status kod —
  • "Ishlayapman, lekin sekin" holati —
  • @HealthCheck() qo'yadigan sarlavha —
Yechim
  • /health/live — tekshiruvsiz, check([]).
  • /health/ready — ombor va xotira; yiqilsa trafik boshqa nusxaga ketadi.
  • 503 — Service Unavailable; filtrimiz uni saqlashi kerak.
  • degraded — 200 bilan qaytadi, status: "degraded".
  • Cache-Control — no-cache, no-store, must-revalidate.

2-mashq (o'rta): degraded holati

Model ketma-ket ishlaydigan soddaroq tekshir(tekshiruvlar, sekinMs) funksiyasini yozing. Har tekshiruv vaqtini o'lchang (performance.now()). sekinMs dan uzoq bo'lsa — indikator degraded, aks holda up, xato bo'lsa — down. Umumiy holat: birorta down — 503 va "error"; down yo'q, lekin degraded bor — 200 va "degraded"; aks holda 200 va "ok". Soddalik uchun muddat va details yo'q. Ishora: ichma-ich ternary yoki if.

Yechim
js
const kut = (ms, qiymat) =>
  new Promise((ok) => setTimeout(() => ok(qiymat), ms));

async function tekshir(tekshiruvlar, sekinMs) {
  const info = {};
  const error = {};
  let degraded = false;
  for (const [k, fn] of Object.entries(tekshiruvlar)) {
    const t = performance.now();
    try {
      await fn();
      const ms = performance.now() - t;
      const status = ms > sekinMs ? "degraded" : "up";
      if (status === "degraded") degraded = true;
      info[k] = { status };
    } catch (e) {
      error[k] = { xato: e.message, status: "down" };
    }
  }
  const bor = Object.keys(error).length > 0;
  const status = bor ? "error" : degraded ? "degraded" : "ok";
  const tana = JSON.stringify({ status, info, error });
  return `${bor ? 503 : 200} ${tana}`;
}

console.log(await tekshir({ ombor: () => kut(5) }, 30));
console.log(await tekshir({ ombor: () => kut(60) }, 30));
console.log(await tekshir({
  ombor: () => kut(60),
  kesh: () => Promise.reject(new Error("Redis uzildi")),
}, 30));

Konsolda:

text
200 {"status":"ok","info":{"ombor":{"status":"up"}},"error":{}}
200 {"status":"degraded","info":{"ombor":{"status":"degraded"}},"error":{}}
503 {"status":"error","info":{"ombor":{"status":"degraded"}},"error":{"kesh":{"xato":"Redis uzildi","status":"down"}}}

Uchinchi qatorda down degraded dan ustun: umumiy holat "error". Terminus'da ham degraded indikator info ga tushadi (demo chiqishidagidek). ms 60 ga yaqin, lekin sekin kompyuterda 5 ms li ham 30 dan oshishi mumkin — bunday chegaralarni zaxira bilan tanlang.

3-mashq (qiyin): tekshiruv natijasini keshlash

Orkestrator har soniyada /health/ready ni so'raydi. 10 ta nusxa bo'lsa — soniyada 10 ta baza so'rovi, faqat "tayyormisan?" uchun. keshli(fn, ttl, soat) funksiyasini yozing: u fn ni o'raydi va natijani ttl ms davomida saqlaydi. soat — vaqtni qaytaradigan funksiya (test uchun soxta soat). 0, 1000, 2000, 4999, 5000, 6000, 11000 ms dagi 7 tekshiruvda, ttl = 5000 bo'lsa, baza necha marta so'ralishini chiqaring. Ishora: closure ichida { natija, tugash } saqlang; t < tugash — kesh yaroqli.

Yechim
js
// Natijani ttl ms davomida keshlaydigan tekshiruv
function keshli(fn, ttl, soat) {
  let saqlangan = null; // { natija, tugash }
  return async () => {
    const t = soat();
    if (saqlangan && t < saqlangan.tugash) return saqlangan.natija;
    const natija = await fn();
    saqlangan = { natija, tugash: t + ttl };
    return natija;
  };
}

let vaqt = 0;
let bazaSorovi = 0;
const ombor = keshli(async () => {
  bazaSorovi++;
  return { status: "up" };
}, 5000, () => vaqt);

for (const t of [0, 1000, 2000, 4999, 5000, 6000, 11000]) {
  vaqt = t;
  await ombor(); // orkestrator har soniya so'raydi
}
console.log(`7 ta tekshiruv, bazaga ${bazaSorovi} ta so'rov`);

Konsolda:

text
7 ta tekshiruv, bazaga 3 ta so'rov

So'rovlar 0, 5000 va 11000 da ketdi. 4999 — kesh hali yaroqli (4999 < 5000), 5000 — muddat tugadi. Terminus'dagi cacheFor(ms) shu g'oya. Narxi: baza uzilganini 5 soniyagacha kech bilasiz. Readiness uchun 1–5 soniya kesh odatda qabul qilinadi; liveness'ni esa keshlash shart emas — u baribir hech narsa tekshirmaydi.

10. Loyiha qadami: /health/live va /health/ready

10.1 Paket

bash
npm i --save-exact @nestjs/terminus@12.1.0

10.2 salomatlik.module.ts

Yangi fayl src/salomatlik/salomatlik.module.ts — modul, controller va o'z indikatorimiz bitta faylda (kichik, bir-biriga bog'liq):

ts
// salomatlik.module.ts — orkestrator (Docker healthcheck, Kubernetes
// probe, 33–34-qism) uchun endpoint'lar — @nestjs/terminus.
//   GET /health/live  — jarayon tirikmi. Tekshiruvsiz: javob berdi —
//     yetarli. Bazaga bog'lamang: baza uzilsa, qayta ishga tushirish
//     yordam bermaydi
//   GET /health/ready — so'rov qabul qilishga tayyormi: ombor (baza)
//     2 s ichida javob beradi, heap 512 MB dan kam. Yo'q bo'lsa — 503
// Versiyasiz (VERSION_NEUTRAL): /v1 ga bog'liq emas
import {
  Controller, Get, Inject, Injectable, Module, VERSION_NEUTRAL,
} from "@nestjs/common";
import {
  HealthCheck, HealthCheckService, HealthIndicatorService,
  MemoryHealthIndicator, TerminusModule,
} from "@nestjs/terminus";
import type {
  HealthCheckResult, HealthIndicatorResult,
} from "@nestjs/terminus";
import { VAZIFA_OMBORI } from "../tokenlar.ts";
import type { VazifaOmbori } from "../vazifalar/ombor.ts";

/**
 * O'z indikatorimiz: ombor turi muhim emas (xotira, fayl, Postgres,
 * Prisma, kesh) — VazifaOmbori.soni() javob beradimi
 */
@Injectable()
export class OmborIndikatori {
  constructor(
    private readonly indikator: HealthIndicatorService,
    @Inject(VAZIFA_OMBORI) private readonly ombor: VazifaOmbori,
  ) {}

  async tekshir<K extends string>(
    kalit: K,
    kutishMs = 2000,
  ): Promise<HealthIndicatorResult<K>> {
    const seans = this.indikator.check(kalit);
    const t = performance.now();
    try {
      await Promise.race([
        this.ombor.soni({}),
        new Promise((_, rad) => {
          setTimeout(() => {
            const ms = String(kutishMs);
            rad(new Error(`${ms} ms ichida javob yo'q`));
          }, kutishMs).unref();
        }),
      ]);
      return seans.up({ ms: Math.round(performance.now() - t) });
    } catch (x) {
      return seans.down({
        xato: x instanceof Error ? x.message : String(x),
      });
    }
  }
}

@Controller({ path: "health", version: VERSION_NEUTRAL })
export class SalomatlikController {
  constructor(
    private readonly salomatlik: HealthCheckService,
    private readonly ombor: OmborIndikatori,
    private readonly xotira: MemoryHealthIndicator,
  ) {}

  @Get("live")
  @HealthCheck()
  tirik(): Promise<HealthCheckResult> {
    return this.salomatlik.check([]);
  }

  @Get("ready")
  @HealthCheck()
  tayyor(): Promise<HealthCheckResult> {
    return this.salomatlik.check([
      () => this.ombor.tekshir("ombor"),
      () => this.xotira.checkHeap("xotira", 512 * 1024 * 1024),
    ]);
  }
}

@Module({
  imports: [TerminusModule],
  controllers: [SalomatlikController],
  providers: [OmborIndikatori],
})
export class SalomatlikModule {}

Faylning to'rt qarori:

  • VAZIFA_OMBORI tokeni — indikator omborning qaysi turi ekanini bilmaydi (Custom provider'lar). Xotira rejimida ham, Postgres'da ham bir xil ishlaydi. soni({}) — eng arzon so'rovlardan biri.
  • .unref() — taymer jarayonni tirik ushlamasin (timers chuqur). Ombor 3 ms da javob bersa ham taymer 2 soniya turadi; unref bo'lmasa, test tugagandan keyin jarayon 2 soniya kutardi.
  • VERSION_NEUTRAL — API /v1 bilan versiyalangan, lekin health yo'li versiyasiz: /health/ready. Orkestrator sozlamasi API versiyasiga bog'lanmasin. Yo'l nomlari inglizcha — bu Docker va Kubernetes konventsiyasi.
  • TerminusModule — HealthCheckService, HealthIndicatorService va MemoryHealthIndicator ni beradi. Uni faqat shu modul import qiladi.

Kanon faylida qatorlar uzun edi — telefon uchun bo'ldik, izohlar qayta taqsimlandi, kod ma'nosi aynan.

10.3 Xato filtri: 503 tarmog'i

src/umumiy/problem.filter.ts — importlarga import type { Problem } from "./problem.ts"; qo'shing. "Kutilmagan" 500 tarmog'idan oldin yangi tarmoq:

ts
    // 503 — "tayyor emas" (masalan @nestjs/terminus /health/ready):
    // kutilgan holat — status va tekshiruv natijalari saqlanadi
    // (kutilmagan 500 EMAS)
    if (xato instanceof HttpException && xato.getStatus() === 503) {
      const tekshiruvlar = xato.getResponse();
      joriyLog(this.log).warn(
        { tekshiruvlar }, "xizmat tayyor emas (503)",
      );
      const p = problemYasa(503, {
        ...umumiy, detail: "Xizmat vaqtincha tayyor emas",
      });
      problemYubor(res, { ...p, tekshiruvlar } as Problem);
      return;
    }

Uch narsa: status 503 saqlanadi; terminus natijasi Problem Details'ning tekshiruvlar kengaytmasiga tushadi (Problem Details kengaytma maydonlarga ruxsat beradi); log darajasi — warn, error emas: bu dasturchi xatosi emas, kutilgan holat. Kanonda shu tarmoq ikki qator bilan yozilgan; biz uni o'qishga qulay bo'lishi uchun tekshiruvlar va p o'zgaruvchilariga ajratdik — xulq aynan bir xil.

10.4 Ulash va marshrutlar testi

src/app.module.ts — import { SalomatlikModule } from "./salomatlik/salomatlik.module.ts"; va imports da VazifalarModule dan oldin SalomatlikModule,.

test/marshrutlar.test.ts — butun marshrutlar jadvalini tekshiradi (Reflector va DiscoveryService darsidan). Yangi ikki yo'l jadvalga qo'shilmasa, test yiqiladi. "/eksportlar/:id/fayl": "GET, HEAD", qatoridan keyin:

ts
    "/health/live": "GET, HEAD",
    "/health/ready": "GET, HEAD",

10.5 Test

test/hodisa.test.ts ga ikkinchi server qo'shiladi — ombori ataylab buzilgan:

ts
class BuzuqOmbor extends XotiraOmbori {
  override soni(): Promise<number> {
    return Promise.reject(new Error("ulanish uzildi"));
  }
}

let i: Ishlayotgan;
let buzuq: Ishlayotgan;
beforeAll(async () => {
  i = await ishga({
    port: 0,
    bogliqliklar: { ombor: new XotiraOmbori() },
  });
  buzuq = await ishga({
    port: 0,
    bogliqliklar: { ombor: new BuzuqOmbor() },
  });
});
afterAll(async () => {
  for (const x of [i, buzuq]) {
    await yop(x.server, 1000);
    await x.app.close(); // @Interval taymerlari to'xtasin
  }
});

Va yangi test (avvalgi @Interval testidan oldin):

ts
test("salomatlik: /health/live va /health/ready — 200; ombor javob "
  + "bermasa ready 503 (Problem Details + tekshiruvlar), live "
  + "baribir 200", async () => {
  const live = await fetch(`${i.manzil}/health/live`);
  expect([live.status, await live.json()]).toEqual([
    200, { status: "ok", info: {}, error: {}, details: {} },
  ]);
  const ready = (await (
    await fetch(`${i.manzil}/health/ready`)
  ).json()) as {
    status: string;
    info: { ombor: { status: string } };
  };
  expect(ready.status).toBe("ok");
  expect(ready.info.ombor.status).toBe("up");
  const yomon = await fetch(`${buzuq.manzil}/health/ready`);
  expect(yomon.status).toBe(503);
  expect(yomon.headers.get("content-type")).toMatch(/problem\+json/);
  const p = (await yomon.json()) as {
    status: number;
    tekshiruvlar: {
      error: { ombor: { status: string; xato: string } };
    };
  };
  expect(p.tekshiruvlar.error.ombor).toEqual({
    status: "down", xato: "ulanish uzildi",
  });
  expect((await fetch(`${buzuq.manzil}/health/live`)).status)
    .toBe(200);
});

Oxirgi qator darsning asosiy qoidasini qotiradi: ombor buzilgan serverda ham live 200. Fayl boshidagi izohni ham yangilang: "salomatlik endpoint'lari, rejalashtirilgan tozalash (@Interval)".

10.6 Tekshiruv

bash
npm run tip
npm run lint
npm test

Kanon nusxasida (Node 24.21.0, bazasiz):

text
> tsc --noEmit
> eslint . --max-warnings=0
 Test Files  20 passed | 5 skipped (25)
      Tests  79 passed | 6 skipped (85)

Endi kanon topgan xatoni o'z ko'zingiz bilan ko'ring: filtrdagi 503 tarmog'ini vaqtincha olib tashlab, testni qayta yurgizdik:

text
 ❯ test/hodisa.test.ts (2 tests | 1 failed) 428ms
   × salomatlik: /health/live va /health/ready — 200; ombor javob bermasa ready 503 (Problem Details + tekshiruvlar), live baribir 200 154ms
   ✓ @Interval: eksport-tozalash ro'yxatda; muddati o'tgan ish tozalanadi va 404 27ms
AssertionError: expected 500 to be 503 // Object.is equality

Tarjimasi: "500 ning 503 bo'lishi kutilgan edi (Object.is tengligi)". Aynan «503 va xato filtri» bo'limidagi demo: filtr 503 ni 500 ga aylantirdi. Tarmoqni qaytaring — test yashil.

Kanonning jonli tekshiruvi (Prisma rejimi) — sarlavhalardan keraklilarini qoldirdik:

text
$ curl /health/ready
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: application/json; charset=utf-8

{"status":"ok","info":{"ombor":{"ms":4,"status":"up"},"xotira":{"status":"up"}},"error":{},"details":{"ombor":{"ms":4,"status":"up"},"xotira":{"status":"up"}}}

Prisma bilan ham ombor 4 ms da javob berdi. Cache-Control — @HealthCheck() ishi.

Hali commit qilmang — oxirgi qism, SSE oqimi, keyingi darsda.

11. Real ishda

  • Docker va Kubernetes: HEALTHCHECK (Dockerfile), livenessProbe va readinessProbe (Kubernetes) aynan shu endpoint'larni so'raydi. 33–34-qismlarda ularni ulaymiz; endpoint'lar tayyor.
  • Load balancer: nginx, AWS ALB, Yandex Cloud balanser ham "sog'lom nusxa"ni health endpoint orqali aniqlaydi.
  • Monitoring: Uptime Kuma, Grafana kabi vositalar /health/ready ni kuzatib, degraded yoki error da xabar beradi.
  • Intervyu savollari: "Liveness va readiness farqi nima?", "Liveness'da bazani tekshirish nega xavfli?", "Health endpoint'ga auth kerakmi?" (javob: yo'q, lekin ichki ma'lumotni — versiya, xato matni — ochiq bermang).

Xulosa

  • Liveness — "tirikmisan?" (tekshiruvsiz); readiness — "so'rov qabul qila olasanmi?" (baza, xotira).
  • TerminusModule + HealthCheckService.check([...]) + @HealthCheck() — format status/info/error/details, kesh sarlavhasi bilan.
  • O'z indikatori — HealthIndicatorService.check(kalit) → up, down, degraded; degraded 200 bilan qaytadi.
  • Terminus o'zi muddat qo'ymaydi: Promise.race yoki attempt().withTimeout(ms).
  • Umumiy xato filtri 503 ni 500 ga aylantirmasin — loyihada alohida tarmoq va test.
  • /health/* versiyasiz (VERSION_NEUTRAL), yo'l nomlari inglizcha.

Keyingi dars: Server-Sent Events NestJS'da — @Sse() va Observable bilan vazifa hodisalarini brauzerga jonli oqim qilib yuboramiz va loyiha bosqichini commit qilamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Health check'lar NestJS'da: @nestjs/terminus — /health/live va /health/ready — IlmHamroh