Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. To'liq yopilish tartibi
- 2.1 Yetti qadam
- 2.2 Haqiqiy server bilan ko'ramiz
- 3. Poyga: kechikish qancha bo'lishi kerak
- 3.1 Model
- 3.2 Kubernetes'da boshqacha yo'l
- 4. Grace period byudjeti
- 4.1 Orkestrator abadiy kutmaydi
- 4.2 Hisob
- 4.3 Majburiy chiqish
- 5. Server'dan tashqari resurslar
- 5.1 API jarayoni: Nest lifecycle
- 5.2 Ishchi jarayoni: BullMQ
- 5.3 Nima yopilmasa nima bo'ladi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): grace period byudjeti
- 3-mashq (qiyin): yopilish jurnalini tekshirish
- 8. Loyiha qadami: readiness drain va kechikish
- 8.1 Umumiy holat: IlovaHolati
- 8.2 Sozlama
- 8.3 jarayon.ts: kechikish
- 8.4 main.ts: holatni ulash
- 8.5 Readiness 503
- 8.6 Test
- 8.7 Tekshiruv
- 9. Real ishda
- Xulosa
- Manbalar
Graceful shutdown to'liq: SIGTERM, readiness drain va grace period
Qisqacha: Graceful shutdown (ehtiyotkor to'xtash) — deploy paytida birorta so'rov va fon ishi yo'qolmasligi. 25-qismda eshikni to'g'ri yopishni o'rgangan edik: yangi ulanish yo'q, boshlangan so'rovlar tugaydi. Lekin balanser SIGTERM'dan keyin ham bir necha soniya so'rov yuborishda davom etadi — ular yopiq eshikka urilib, foydalanuvchiga 502 bo'lib qaytadi. To'liq tartib: avval
/health/ready503 qaytaradi, keyin balanser sezishi uchun kechikish, keyinserver.close(), keyin ishchilar, navbat va pool yopiladi. Hammasi orkestrator bergan grace period ichiga sig'ishi shart, aks holda SIGKILL keladi.
Bu darsda
- SIGTERM'dan keyingi poygani (race) tushuntirasiz: nega
server.close()yolg'iz yetmaydi. - Yopilishning to'liq tartibini — readiness'dan pool'gacha — kanon kodida kuzatasiz.
- Kechikishning narxini model bilan hisoblaysiz.
- Docker va Kubernetes grace period byudjetini tekshira olasiz.
vazifalar-apiga readiness drain vaYOPISH_KECHIKISH_MSni qo'shasiz.
Oldin bilishingiz kerak: Server timeout'lari va keep-alive, Process darajasidagi xatolar va crash strategiyasi, Lifecycle hook'lar, Health check'lar: @nestjs/terminus, BullMQ ishonchlilik.
O'tgan darsda feature flag bilan xususiyatni kodni o'zgartirmasdan yoqib-o'chirishni o'rgandik. Endi kod o'zgaradigan lahzaga qaytamiz — deploy. Yangi versiya chiqadi, eskisi to'xtatiladi. Shu to'xtash paytida foydalanuvchi nimani his qiladi — bu darsning mavzusi.
1. Nega bu kerak?
Juma, 18:00. Jamoa vazifalar-api ning yangi versiyasini chiqaryapti. Orkestrator (Docker, Kubernetes yoki oddiy skript) eski jarayonga SIGTERM yuboradi. 25-qismdagi yop() ishlaydi: yangi ulanish qabul qilinmaydi, boshlangan so'rovlar oxirigacha bajariladi. Hammasi to'g'ri ko'rinadi.
Lekin oldida balanser (load balancer) turibdi. U trafikni bir nechta nusxaga taqsimlaydi va qaysi nusxa sog'ligini o'zi tekshirib turadi — masalan, har soniyada. SIGTERM kelgan lahzada balanser buni bilmaydi. U eski nusxaga so'rov yuborishda davom etadi. Eshik esa allaqachon yopiq: ulanish rad etiladi (ECONNREFUSED), balanser foydalanuvchiga 502 Bad Gateway qaytaradi.
Natija: har deployda bir necha soniya xatolar. Kuniga uch marta deploy qilsangiz — kuniga uch marta "ilova ishlamayapti" degan shikoyat.
O'xshatish: kechki soat 21:00 da yopiladigan do'kon. Yomon sotuvchi soat 21:00 da eshikni qulflaydi — tashqarida eshikni tortayotganlar hayron qoladi. Yaxshi sotuvchi 20:50 da eshikka "yopiq" yozuvini osadi. Yangi xaridor kirmaydi, ichkaridagilar esa xaridini tugatadi. Faqat oxirgisi chiqqach eshik qulflanadi. "Yopiq" yozuvi — bu readiness 503, 10 daqiqa — kechikish.
2. To'liq yopilish tartibi
2.1 Yetti qadam
To'liq graceful shutdown — yettita qadam, aynan shu tartibda:
| # | Qadam | Nega aynan shu yerda |
|---|---|---|
| 1 | SIGTERM keldi |
orkestrator "to'xta" dedi |
| 2 | /health/ready → 503 |
balanser yangi trafikni boshqa nusxaga bursin |
| 3 | kechikish (masalan 5 s) | balanser 503 ni sezishi uchun vaqt |
| 4 | uzoq ulanishlar (SSE) yopiladi | ular o'zi tugamaydi |
| 5 | server.close() + bo'sh keep-alive'lar |
yangi ulanish yo'q, boshlanganlari tugaydi |
| 6 | app.close(): navbat, Redis, pool |
so'rovlar bazani ishlatib bo'lgach |
| 7 | process.exitCode = 0 |
jarayon o'zi chiqadi |
Tartibni buzish — xato. Pool'ni server'dan oldin yopsangiz, hali bajarilayotgan so'rov bazaga yetolmay 500 oladi (Lifecycle hook'lar darsida aynan shuni ko'rgandik). Kechikishni olib tashlasangiz — darsning boshidagi 502 lar qaytadi.
Diagrammada vaqt yuqoridan pastga oqadi:
sequenceDiagram
participant O as Orkestrator
participant B as Balanser
participant I as Ilova
participant D as Baza
O->>I: SIGTERM
Note over I: ready = 503
B->>I: GET /health/ready
I-->>B: 503
Note over B: nusxani chiqaradi
Note over I: kechikish tugadi
Note over I: server.close()
I->>D: oxirgi so'rovlar
Note over I: pool.end()
I-->>O: exit 0Diagrammaning o'rtasiga qarang: balanser 503 ni ko'rib, nusxani chiqargandan keyin server yopiladi. Shu tartib bo'lsa, birorta so'rov yopiq eshikka urilmaydi.
2.2 Haqiqiy server bilan ko'ramiz
Gapdan ko'ra kod ishonchliroq. Quyidagi blokda oddiy node:http server bor: readiness, sekin so'rov (0.8 s) va toxta() — SIGTERM o'rniga uni o'zimiz chaqiramiz. Windows'da boshqa jarayondan signal yuborib bo'lmaydi (process obyekti darsidagi topilma), shuning uchun mantiqni to'g'ridan sinaymiz. yop() — Server timeout'lari darsidagi funksiyaning qisqa nusxasi.
import { createServer } from "node:http";
const holat = { yopilmoqda: false };
const server = createServer(async (req, res) => {
if (req.url === "/health/ready") {
res.writeHead(holat.yopilmoqda ? 503 : 200).end();
return;
}
await new Promise((ok) => setTimeout(ok, 800)); // sekin so'rov
res.end("vazifa saqlandi");
});
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
const url = `http://127.0.0.1:${server.address().port}`;
const tayyormi = async () =>
(await fetch(`${url}/health/ready`)).status;
function yop(server, kutishMs) {
return new Promise((ok) => {
const iv = setInterval(() => server.closeIdleConnections(), 50);
const t = setTimeout(() => {
server.closeAllConnections();
ok(false);
}, kutishMs);
server.close(() => {
clearInterval(iv);
clearTimeout(t);
ok(true);
});
server.closeIdleConnections();
});
}
async function toxta(kechikishMs) {
console.log("SIGTERM keldi (soxta)");
holat.yopilmoqda = true; // 1) readiness 503
await new Promise((ok) => setTimeout(ok, kechikishMs)); // 2) kutish
console.log("server.close()");
return yop(server, 2000); // 3) eshik yopiladi
}
console.log("ready:", await tayyormi());
const sekin = fetch(`${url}/v1/vazifalar`, { method: "POST" })
.then(async (r) => console.log("sekin so'rov:", r.status,
await r.text()));
await new Promise((ok) => setTimeout(ok, 50));
const yopildi = toxta(150);
console.log("ready:", await tayyormi());
await new Promise((ok) => setTimeout(ok, 250));
try {
await fetch(`${url}/health/ready`);
} catch (x) {
console.log("yangi ulanish:", x.cause.code);
}
await sekin;
console.log("o'z vaqtida yopildi:", await yopildi);Konsolda:
ready: 200
SIGTERM keldi (soxta)
ready: 503
server.close()
yangi ulanish: ECONNREFUSED
sekin so'rov: 200 vazifa saqlandi
o'z vaqtida yopildi: trueChiqishni qatorma-qator o'qing:
- Oddiy holatda readiness 200.
- "SIGTERM" kelgach, kechikish davomida server hali tinglaydi, lekin readiness 503. Balanser shu oraliqda nusxani chiqaradi.
- Kechikishdan keyin
server.close()— endi yangi ulanishECONNREFUSEDoladi. Balanser bu paytda boshqa nusxaga yuboryapti, shuning uchun hech kim zarar ko'rmaydi. - Sekin so'rov yopilishdan oldin boshlangan edi — u 200 bilan tugadi.
yop()trueqaytardi: hamma ulanish o'z vaqtida yopildi, majburiy uzish kerak bo'lmadi.
Tekshirib ko'ring: Kechikish paytida readiness 503 qaytaradi, lekin oddiy
POST /v1/vazifalarga kelgan so'rov-chi? U ham rad etiladimi?
Javob
Yo'q. Kechikish paytida faqat readiness "tayyor emasman" deydi. Oddiy so'rovlar odatdagidek bajariladi — ataylab shunday. Balanser hali nusxani chiqarmagan bo'lsa, u yuborgan so'rov xizmat oladi. Agar oddiy so'rovlarni ham 503 bilan rad etsangiz, kechikishning butun ma'nosi yo'qoladi: foydalanuvchi 502 o'rniga 503 ko'radi, xolos.
3. Poyga: kechikish qancha bo'lishi kerak
3.1 Model
Balanser nusxani darhol chiqarmaydi. Odatda u readiness'ni muayyan davrda so'raydi va bir necha marta ketma-ket muvaffaqiyatsizlikdan (chegara) keyingina "bu nusxa kasal" deydi. Bitta tasodifiy xato uchun nusxani chiqarib yubormaslik uchun shunday.
Eng yomon holatni hisoblaymiz: SIGTERM aynan tekshiruvdan keyin keldi. Balanser har 10 ms da bitta so'rov yuboradi:
// Model: SIGTERM — 0 ms da. Balanser readiness'ni har `davr` ms da
// so'raydi va 503 ni `chegara` marta ketma-ket ko'rgach, nusxani
// chiqaradi. Ungacha shu nusxaga har 10 ms da bitta so'rov keladi.
function yopilish({ davr, chegara, kechikish }) {
const chiqarildi = davr * chegara; // eng yomon holat
let xizmat = 0;
let rad = 0;
for (let t = 0; t < chiqarildi; t += 10) {
if (t < kechikish) xizmat++; // server hali tinglayapti
else rad++; // eshik yopiq — foydalanuvchiga 502
}
return { chiqarildi, xizmat, rad };
}
for (const kechikish of [0, 2000, 5000]) {
const r = yopilish({ davr: 1000, chegara: 3, kechikish });
console.log(`kechikish ${kechikish} ms →`, r);
}Konsolda:
kechikish 0 ms → { chiqarildi: 3000, xizmat: 0, rad: 300 }
kechikish 2000 ms → { chiqarildi: 3000, xizmat: 200, rad: 100 }
kechikish 5000 ms → { chiqarildi: 3000, xizmat: 300, rad: 0 }Kechikishsiz — 300 ta so'rov yopiq eshikka urildi. 2 soniya — hali 100 ta. 5 soniya — birortasi ham. Qoida bitta: kechikish ≥ davr × chegara (va ustiga biroz ortiqcha vaqt). Model soddalashtirilgan: haqiqiy balanserda tekshiruvlar va so'rovlar bir tekis kelmaydi. Lekin raqamlarning tartibi aynan shunday.
Formulani o'z sozlamangizga qo'llab ko'ring. Masalan, nginx yoki bulut balanseri readiness'ni har 2 soniyada so'raydi va 2 marta ketma-ket 503 ni ko'rgach nusxani chiqaradi. 2 × 2 = 4 soniya. Kechikish 5 soniya bo'lsa — 1 soniya ortiqcha vaqt qoladi, yetarli. Balanser sozlamasini bilmasangiz, uning hujjatidagi sukut qiymatlarini oling va shu hisobni yozib qo'ying: keyin kimdir davrni oshirsa, kechikishni ham oshirish kerakligini biladi.
3.2 Kubernetes'da boshqacha yo'l
Kubernetes'da pod o'chirilayotganda uni Service ro'yxatidan chiqarish va SIGTERM yuborish parallel boshlanadi — hujjatda ular orasida kafolatlangan tartib yo'q. Ro'yxat o'zgarishi esa klasterdagi hamma proksi va balanserlarga biroz kechikib yetib boradi. Shu oraliqda eski pod'ga trafik kelishda davom etadi.
Shuning uchun Kubernetes'da ko'p jamoalar preStop ilgagiga 5 soniyalik kutish yozadi: SIGTERM o'sha 5 soniyadan keyin keladi. Yangi versiyalarda buning uchun alohida sleep amali bor — konteyner ichida sleep dasturi bo'lishi shart emas. Muhim detal: preStop vaqti grace period'ning ichida hisoblanadi. Hujjatdagi misol: grace 60 s, ilgak 55 s, ilova to'xtashiga 10 s — jarayon o'z vaqtida to'xtay olmaydi va SIGKILL oladi. (Kubernetes hujjatiga ko'ra, 2026-11-03 da tekshirildi; klasterda sinalmagan.)
Kanon boshqacha yo'l tanladi: kechikish ilovaning o'zida — YOPISH_KECHIKISH_MS. Afzalligi — orkestratorga bog'liq emas: nginx, Docker Compose yoki boshqa balanser bilan ham ishlaydi. Readiness 503 esa sog'likni o'zi tekshiradigan balanserlarga (nginx, HAProxy, bulut balanserlari) aniq signal beradi.
Diqqat: Lokal ishlashda kechikish kerak emas — balanser yo'q, Ctrl+C dan keyin 5 soniya kutish faqat jonga tegadi. Shuning uchun kanonda sukut 0, prodakshnda esa
.envda 5000.
4. Grace period byudjeti
4.1 Orkestrator abadiy kutmaydi
SIGTERM — iltimos. Orkestrator ma'lum vaqt kutadi, keyin SIGKILL yuboradi. SIGKILL'ni ushlab bo'lmaydi: jarayon o'sha zahoti o'ladi, bajarilayotgan so'rovlar uziladi, tranzaksiya yarim qoladi (baza uni ROLLBACK qiladi, lekin foydalanuvchi xato ko'radi). Bu kutish vaqti — grace period (imtiyozli muddat).
Rasmiy hujjatlar bo'yicha sukut qiymatlari (2026-11-03 da hujjatda tekshirildi):
| Orkestrator | Sukut grace period | Qayerda o'zgartiriladi |
|---|---|---|
docker stop |
10 s (Linux konteyner) | docker stop -t, Compose'da stop_grace_period |
| Docker Compose | 10 s | stop_grace_period |
| Kubernetes | 30 s | terminationGracePeriodSeconds |
| systemd | 90 s (odatda) | TimeoutStopSec |
systemd'da sukut tizim sozlamasidan (DefaultTimeoutStopSec) olinadi — ko'p distributivlarda 90 s. Windows konteynerlarida Docker sukuti 30 s.
4.2 Hisob
Butun yopilish shu muddatga sig'ishi kerak:
kechikish + so'rovlarni kutish + app.close() ≤ grace periodVaqt chizig'ida ko'raylik. 0-soniya — SIGTERM, readiness 503. 5-soniya — kechikish tugadi, server.close(). Endi eng sekin so'rov tugashini ko'pi bilan 10 soniya kutamiz — 15-soniya. Keyin app.close(): navbat, Redis, pool — yana bir soniyacha. Jarayon taxminan 16-soniyada chiqadi.
Kanon qiymatlari: kutishMs = 10 000 (25-qismdan beri), prodakshn uchun YOPISH_KECHIKISH_MS = 5000. Jami — 15 soniya, yana pool va navbat yopilishi ustiga. Kubernetes'ning 30 soniyasiga bemalol sig'adi. Docker'ning sukut 10 soniyasiga esa sig'maydi: sekin so'rov kelib qolsa, 10-soniyada SIGKILL uni uzadi. 33-qismda Compose fayl yozganda stop_grace_period: 30s ni unutmang. Byudjetni hisoblash — 2-mashqda.
4.3 Majburiy chiqish
Ba'zi narsa o'z vaqtida tugamaydi: osilib qolgan so'rov, javob bermayotgan tashqi xizmat. Shuning uchun kanonda ikki sug'urta bor:
yop(server, kutishMs)—kutishMsdan keyin qolgan ulanishlarnicloseAllConnections()bilan majburan uzadi vafalseqaytaradi. Jarayon shundaexitCode = 1bilan chiqadi — log va monitoring "nimadir uzildi" deb biladi.- Halokatda (
uncaughtException) —setTimeout(() => process.exit(1), …).unref(). Yopish o'zi osilib qolsa ham jarayon albatta o'ladi (Process darajasidagi xatolar darsidan).
Muhim detal: halokatda kechikish yo'q. Jarayon holati noma'lum — undan iloji boricha tez chiqish kerak. Kanon kodidagi shart: kechikishMs > 0 && kod === 0.
Tekshirib ko'ring: Nega majburiy chiqish taymeriga
.unref()qo'shilgan?
Javob
unref() qilingan taymer jarayonni tirik ushlab turmaydi (timers chuqur darsi). Yopish o'z vaqtida tugasa, jarayon taymerni kutmasdan darhol chiqadi. Taymer faqat yopish osilib qolsa ishga tushadi. unref bo'lmasa, har to'xtash halokatKutishMs + 1000 ms kechikardi.
5. Server'dan tashqari resurslar
HTTP server — yopiladigan narsalardan faqat bittasi. vazifalar-api da uchta jarayon bor, har birining o'z yopilish tartibi.
5.1 API jarayoni: Nest lifecycle
app.close() Nest'ning lifecycle hook'larini chaqiradi (Lifecycle hook'lar). Kanonda ular quyidagi ishni qiladi:
| Hook | Kanon klassi | Nima yopiladi |
|---|---|---|
beforeApplicationShutdown |
AuthYopuvchi |
fonda ketayotgan xatlar tugashi kutiladi |
beforeApplicationShutdown |
IntegratsiyaYopuvchi |
jonli (Pub/Sub) obunachi |
onApplicationShutdown |
IntegratsiyaYopuvchi |
navbat (BullMQ producer) ulanishi |
onApplicationShutdown |
AuthYopuvchi |
alohida auth pool, pochta |
onApplicationShutdown |
SaqlashYopuvchi |
asosiy pool (pool.end()), Redis |
pool.end() — Connection pooling darsidan: yangi so'rov olmaydi, bo'sh ulanishlarni yopadi va band ulanishlar bo'shashini kutadi. Server allaqachon yopilgan, demak yangi so'rov kelmaydi.
5.2 Ishchi jarayoni: BullMQ
Fon ishlarini bajaradigan ishchi — alohida jarayon, HTTP serveri yo'q. Uning SIGTERM ishlovchisi (kanon kodidan, src/ishchi.ts):
async function chiq(signal: string): Promise<void> {
if (toxtamoqda) return;
toxtamoqda = true;
relayHolati.faol = false; // outbox relay yangi partiya olmaydi
await relayTugadi; // joriy partiya tugaydi
await Promise.all(workerlar.map((w) => w.close()));
redis.destroy();
await navbatlar.yop();
pochta?.yop();
await agent.close(); // HTTP mijoz (undici)
await pool.end();
log.info({ signal }, "ishchi to'xtadi");
process.exit(0);
}Mantiq API bilan bir xil: avval yangi ish olishni to'xtatish, keyin boshlanganini tugatish, oxirida ulanishlar. worker.close() — BullMQ ishonchlilik darsidan: worker navbatdan yangi ish olmaydi va faol ishlar tugashini kutadi. Agar ish grace period ichida tugamasa va SIGKILL kelsa — ish "stalled" bo'ladi va boshqa worker uni qayta oladi. Shuning uchun fon ishlari idempotent bo'lishi shart.
5.3 Nima yopilmasa nima bo'ladi
| Yopilmagan resurs | Belgisi |
|---|---|
server.close() chaqirilmadi |
jarayon chiqmaydi, SIGKILL kutadi |
| bo'sh keep-alive'lar | close() callback 5–6 s kechikadi |
| SSE ulanishlari | close() kutishMs gacha osiladi |
| pool | jarayon chiqmaydi yoki baza logida "unexpected EOF" |
| BullMQ worker | faol ish "stalled", boshqa worker qayta bajaradi |
| Redis klient | jarayon chiqmaydi (ochiq soket) |
Jadvaldagi ikkinchi qator — 25-qismdagi o'lchovimiz: 1.5 soniyalik so'rov tugagach, bo'sh qolgan ulanishni Node yana 5 + 1 soniya ushlab turdi. Faqat close() bilan callback jami 7.4 soniya kutgan edi.
Tekshirib ko'ring: Ishchi jarayonda fon ishi 40 soniya davom etadi, Kubernetes grace period esa 30 soniya. Deploy paytida bu ish bilan nima bo'ladi?
Javob
worker.close() ishni kutadi, lekin 30-soniyada SIGKILL keladi va ish yarim qoladi. BullMQ uni keyinroq "stalled" deb topadi va boshqa worker boshidan bajaradi. Shuning uchun ish idempotent bo'lishi shart: ikki marta bajarilsa ham natija bir xil bo'lsin. Ikkinchi yo'l — uzoq ishni kichik bo'laklarga ajratish yoki grace period'ni oshirish.
6. Ko'p uchraydigan xatolar
| Xato | Oqibat | To'g'risi |
|---|---|---|
SIGTERM'da darhol server.close() |
balanser bilmagan so'rovlar 502 | avval readiness 503, keyin kechikish |
| Kechikish paytida hamma so'rovni 503 qilish | kechikish foydasiz | faqat readiness 503 |
process.exit(0) SIGTERM ishlovchisida |
boshlangan so'rovlar uziladi | process.exitCode va o'zi chiqsin |
| Pool'ni server'dan oldin yopish | oxirgi so'rovlar 500 | app.close() — server'dan keyin |
| Halokatda ham kechikish | noma'lum holatda 5 s ishlash | kechikish faqat kod === 0 da |
| Grace period hisoblanmagan | SIGKILL o'rtada uzadi | kechikish + kutish + yopish ≤ grace |
| Liveness'ni ham 503 qilish | orkestrator jarayonni o'ldiradi | liveness 200 qoladi |
Oxirgi qatorga alohida e'tibor. Yopilish paytida jarayon tirik — u ishini tugatyapti. Liveness 503 bo'lsa, Kubernetes "jarayon o'ldi" deb uni SIGKILL bilan qayta ishga tushirishi mumkin. Kanonda /health/live yopilishda ham 200.
7. Mashqlar
Mashqlar kurs/mashqlar/31/26-shutdown/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
- SIGTERM'dan keyin birinchi bo'lib 503 qaytaradigan endpoint —
/health/ - Docker
stopning sukut grace period'i, soniyada — - Grace period tugagach orkestrator yuboradigan, ushlab bo'lmaydigan signal —
- Bo'sh keep-alive ulanishlarni darhol yopadigan metod —
server.() - Kanonda kechikishni beradigan muhit o'zgaruvchisi —
Yechim
- ready —
/health/liveesa 200 qoladi. - 10 — Kubernetes'da 30, systemd'da 90.
- SIGKILL — jarayon darhol o'ladi, hech qanday ishlovchi chaqirilmaydi.
- closeIdleConnections — ishlayotgan so'rovlarga tegmaydi.
- YOPISH_KECHIKISH_MS — sukut 0, prodakshnda ~5000.
2-mashq (o'rta): grace period byudjeti
byudjet(sozlama) funksiyasini yozing. Kirish — kechikish, kutish, yopish (app.close() ga ketadigan vaqt) va grace (hammasi ms). Natija: jami, zaxira (grace − jami) va holat — zaxira 0 dan kichik bo'lsa "SIGKILL xavfi", aks holda "sig'adi". Kanon qiymatlarini (5000 + 10 000 + 1000) Docker va Kubernetes sukutida tekshiring. Ishora: parametrda destructuring va spread bilan obyekt nusxasi.
Yechim
function byudjet({ kechikish, kutish, yopish, grace }) {
const jami = kechikish + kutish + yopish;
const zaxira = grace - jami;
return {
jami,
zaxira,
holat: zaxira < 0 ? "SIGKILL xavfi" : "sig'adi",
};
}
const kanon = { kechikish: 5000, kutish: 10_000, yopish: 1000 };
console.log("docker:", byudjet({ ...kanon, grace: 10_000 }));
console.log("k8s:", byudjet({ ...kanon, grace: 30_000 }));Konsolda:
docker: { jami: 16000, zaxira: -6000, holat: 'SIGKILL xavfi' }
k8s: { jami: 16000, zaxira: 14000, holat: "sig'adi" }zaxira bu yerda — grace period'dan ortib qolgan vaqt (ma'lumot zaxirasi emas). Docker sukutida 6 soniya yetmaydi — eng sekin so'rov SIGKILL bilan uzilishi mumkin. Ikki yo'l bor: stop_grace_period ni oshirish yoki kutishMs ni kamaytirish. Ikkinchi qatordagi qo'shtirnoq — satr ichida apostrof bor.
3-mashq (qiyin): yopilish jurnalini tekshirish
Yopilish paytida log yoziladi: har hodisa — { nom, ms } (SIGTERM'dan beri o'tgan vaqt). tekshir(jurnal, { kechikish, grace }) funksiyasini yozing. U buzilgan qoidalar ro'yxatini qaytarsin:
"ready503"hodisasi"server.close"dan oldin bo'lsin."server.close"kechikishdan oldin bo'lmasin (ms ≥ kechikish)."pool.end""server.close"dan keyin bo'lsin."exit"grace period'dan oshmasin.
Ishora: find bilan hodisani nomi bo'yicha toping (massivning HOF metodlari).
Yechim
function tekshir(jurnal, { kechikish, grace }) {
const vaqt = (nom) => jurnal.find((h) => h.nom === nom)?.ms;
const xatolar = [];
const ready = vaqt("ready503");
const close = vaqt("server.close");
const pool = vaqt("pool.end");
const exit = vaqt("exit");
if (ready === undefined || ready > close) {
xatolar.push("readiness 503 server.close dan keyin");
}
if (close < kechikish) {
xatolar.push(`server.close ${close} ms da — kechikishdan erta`);
}
if (pool < close) xatolar.push("pool server'dan oldin yopildi");
if (exit > grace) xatolar.push(`exit ${exit} ms — grace'dan keyin`);
return xatolar;
}
const yaxshi = [
{ nom: "ready503", ms: 0 },
{ nom: "server.close", ms: 5000 },
{ nom: "pool.end", ms: 5400 },
{ nom: "exit", ms: 5420 },
];
const yomon = [
{ nom: "server.close", ms: 0 },
{ nom: "pool.end", ms: 0 },
{ nom: "ready503", ms: 10 },
{ nom: "exit", ms: 12_500 },
];
console.log(tekshir(yaxshi, { kechikish: 5000, grace: 10_000 }));
console.log(tekshir(yomon, { kechikish: 5000, grace: 10_000 }));Konsolda:
[]
[
'readiness 503 server.close dan keyin',
'server.close 0 ms da — kechikishdan erta',
"exit 12500 ms — grace'dan keyin"
]Ikkinchi jurnal — "yomon sotuvchi". Eshik darhol qulflandi, yozuv keyin osildi. Jarayon 12.5 soniyada chiqdi — Docker uni 10-soniyada o'ldirgan bo'lardi. pool.end va server.close bir vaqtda (0 ms) — qoida "oldin emas" deydi, shuning uchun bu qator o'tdi; haqiqatda esa ikkalasining tartibi muhim, vaqti emas. Yechimni kengaytirish mumkin: hodisalarning massivdagi o'rnini ham solishtiring.
8. Loyiha qadami: readiness drain va kechikish
Kanon shu darsda besh faylni o'zgartirdi. Hammasi "yopilish boshlandi" degan bitta holat atrofida.
8.1 Umumiy holat: IlovaHolati
Readiness controller'i va main.ts dagi signal ishlovchisi turli joyda. Ular bitta obyekt orqali gaplashadi. src/tokenlar.ts oxiriga:
/** 31 #26: jarayon holati — yopilishni readiness biladi */
export const ILOVA_HOLATI = Symbol("ILOVA_HOLATI");
export class IlovaHolati {
/** SIGTERM keldi — /health/ready 503, yangi trafik kelmasin */
yopilmoqda = false;
}Bu — oddiy klass, Nest'da provider sifatida bitta nusxa bo'ladi (Custom provider'lar).
8.2 Sozlama
src/config.ts — HSTS qatoridan keyin:
/** 31 #26: SIGTERM dan keyin server.close() gacha kutish (ms) —
* shu orada /health/ready 503. Prod (k8s, balanser): readiness
* tekshiruvi oralig'idan katta, masalan 5000 */
YOPISH_KECHIKISH_MS: Butun(0).pipe(z.number().max(60_000)),Butun(0) — 30-qismdagi yordamchi: bo'sh qiymat 0 ga aylanmaydi, xato beradi. Maksimum 60 soniya — tasodifan 5000000 yozib qo'yilsa, ilova ishga tushmaydi. .env.example oxiriga:
# ─── 31-qism: prodakshn (#26–#30) ───
# SIGTERM → /health/ready 503 → shuncha kutib server.close()
# YOPISH_KECHIKISH_MS=50008.3 jarayon.ts: kechikish
jarayonniBoshqar ga yangi parametr — kechikishMs. Kanonda parametrlar bitta uzun qatorda edi; telefon uchun ustunga bo'ldik, ma'nosi aynan:
/** kechikishMs (31 #26) — boshida() (readiness 503) dan keyin
* server.close() gacha: balanser tekshiruv oralig'idan uzunroq */
export function jarayonniBoshqar(
server: Server,
log: JarayonLogi,
{
kutishMs = 10_000,
halokatKutishMs = 3_000,
kechikishMs = 0,
boshida = (): void => undefined,
oxirida = (): Promise<void> => Promise.resolve(),
} = {},
): void {toxta() ichida boshida() va yop() orasiga uch qator:
boshida();
// halokatda (kod 1) kutilmaydi
if (kechikishMs > 0 && kod === 0) {
await new Promise((ok) => setTimeout(ok, kechikishMs));
}
const ozVaqtida = await yop(server, kutish);8.4 main.ts: holatni ulash
Importlar:
import { CONFIG, ILOVA_HOLATI, JONLI, LOG } from "./tokenlar.ts";
import type { IlovaHolati } from "./tokenlar.ts";log dan keyin holatni olamiz va jarayonniBoshqar ning uchinchi argumentini yangilaymiz:
const holat = app.get<IlovaHolati>(ILOVA_HOLATI);
// …
jarayonniBoshqar(server, { /* info, fatal — o'zgarmadi */ }, {
// 31 #26: avval readiness 503, YOPISH_KECHIKISH_MS kutiladi
// (balanser sezsin), keyin server.close
kechikishMs: config.YOPISH_KECHIKISH_MS,
boshida: () => {
holat.yopilmoqda = true;
app.get<SseHub | null>(JONLI)?.yop();
},
oxirida: () => app.close(),
});boshida endi ikki ish qiladi: readiness'ni 503 ga o'tkazadi va SSE ulanishlarini yopadi (29-qismdan beri shu yerda edi).
8.5 Readiness 503
src/salomatlik/salomatlik.module.ts — uchta o'zgarish. Importga Global va ServiceUnavailableException, tokenlardan ILOVA_HOLATI, IlovaHolati:
import {
Controller, Get, Global, Inject, Injectable, Module,
ServiceUnavailableException, VERSION_NEUTRAL,
} from "@nestjs/common";
import {
ILOVA_HOLATI, IlovaHolati, VAZIFA_OMBORI,
} from "../tokenlar.ts";Controller holatni oladi va tayyor() boshida tekshiradi:
constructor(
private readonly salomatlik: HealthCheckService,
private readonly ombor: OmborIndikatori,
private readonly xotira: MemoryHealthIndicator,
@Inject(ILOVA_HOLATI) private readonly holat: IlovaHolati,
) {}
// …tirik() o'zgarmadi
@Get("ready")
@HealthCheck()
tayyor(): Promise<HealthCheckResult> {
// 31 #26: SIGTERM kelgan — tayyor EMAS (balanser trafikni
// boshqa nusxaga buradi), boshlangan so'rovlar esa tugaydi
if (this.holat.yopilmoqda) {
throw new ServiceUnavailableException({
status: "error", info: {},
error: { yopilish: { status: "down" } }, details: {},
});
}
return this.salomatlik.check([
() => this.ombor.tekshir("ombor"),
() => this.xotira.checkHeap("xotira", 512 * 1024 * 1024),
]);
}Javob shakli terminus natijasi bilan bir xil (status, info, error, details) — monitoring vositasi farqni sezmaydi, faqat error da yangi kalit yopilish. Modul esa global bo'ladi va holatni bitta nusxa sifatida beradi:
@Global()
@Module({
imports: [TerminusModule],
controllers: [SalomatlikController],
providers: [
OmborIndikatori,
{ provide: ILOVA_HOLATI, useValue: new IlovaHolati() },
],
exports: [ILOVA_HOLATI],
})
export class SalomatlikModule {}8.6 Test
Signalni Windows'da yuborib bo'lmaydi, lekin natijasini sinash mumkin: holatni qo'lda yopilmoqda = true qilamiz. Yangi fayl test/kuzatuv.test.ts (keyingi ikki darsda unga yana testlar qo'shiladi):
// kuzatuv.test.ts — 31-qism #26 graceful shutdown: yopilish
// boshlansa readiness 503, liveness 200. Xotira ombori — bazasiz.
import { test } from "vitest";
import assert from "node:assert/strict";
import request from "supertest";
import { ilovaYarat } from "../src/ilova.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { configYukla } from "../src/config.ts";
import { ILOVA_HOLATI, IlovaHolati } from "../src/tokenlar.ts";
const ilova = async (env: Record<string, string> = {}) => {
const app = await ilovaYarat({
ombor: new XotiraOmbori(),
config: configYukla({
LOG_LEVEL: "silent", NODE_ENV: "test", ...env,
}),
});
await app.init();
return app;
};
test("#26 yopilmoqda — /health/ready 503, /health/live 200",
async () => {
const app = await ilova();
const h = app.get<IlovaHolati>(ILOVA_HOLATI);
const http = app.getHttpServer();
await request(http).get("/health/ready").expect(200);
h.yopilmoqda = true;
const r = await request(http).get("/health/ready").expect(503);
assert.match(JSON.stringify(r.body), /yopilish/);
await request(http).get("/health/live").expect(200);
await app.close();
});yop() ning o'zi (boshlangan so'rov tugaydi, yangisi rad, keep-alive kutilmaydi) — 25-qismdan beri test/yopish.test.ts da.
Test haqiqiy signal yubormaydi — u faqat signal natijasini (holat o'zgarishini) sinaydi. Bu ataylab: signalni yuborish operatsion tizimga bog'liq, holatga qarab javob berish esa bizning kodimiz. Signal ishlovchisining o'zini esa Linux'da (masalan, konteynerda docker stop bilan) tekshirish mumkin — Windows'da emas.
8.7 Tekshiruv
npm run tip
npx vitest run test/kuzatuv.test.ts test/yopish.test.tsBiz loyiha nusxasida ishga tushirdik (Node 24.21.0, bazasiz): tsc --noEmit xatosiz, testlar:
✓ |asosiy| test/yopish.test.ts (2 tests) 750ms
✓ yop(): boshlangan so'rov tugaydi, yangisi rad etiladi, keep-alive kutilmaydi 470ms
✓ |asosiy| test/kuzatuv.test.ts (1 test) 302ms
✓ #26 yopilmoqda — /health/ready 503, /health/live 200 300ms
Test Files 2 passed (2)
Tests 3 passed (3)Nima bo'ldi? Ikki fayl, uch test — hammasi yashil. Eski yopish.test.ts buzilmadi, yangi test esa readiness 503 va liveness 200 ni tasdiqladi.
Yopilish paytidagi readiness javobi qanday ko'rinadi? Testga vaqtincha console.log qo'shib ko'rdik (sorovId har safar boshqa):
503 application/problem+json; charset=utf-8
{"type":"about:blank","title":"Service Unavailable","status":503,"detail":"Xizmat vaqtincha tayyor emas","instance":"/health/ready","sorovId":"fc640cd6-0cbf-4a78-a2c4-b04e8d130096","tekshiruvlar":{"status":"error","info":{},"error":{"yopilish":{"status":"down"}},"details":{}}}503 ni Problem Details'ga aylantirgan — 30-qismdagi xato filtrining 503 tarmog'i. title hozircha inglizcha — buni Backend unumdorligini oshirish darsida, loyihaning shu bosqichini yopayotganda o'zbekchaga o'tkazamiz.
Hali commit qilmang — loyihaning shu bosqichi to'rtta keyingi dars bilan birga yopiladi.
9. Real ishda
- Qayerda uchraydi: har deploy, har avtomatik masshtablash (nusxa kamayganda), har Kubernetes node almashinuvi. "Deploy paytida 502" — prodakshndagi eng ko'p uchraydigan shikoyatlardan biri.
- Vositalar: Kubernetes
preStopvaterminationGracePeriodSeconds, Docker Composestop_grace_period, nginx/HAProxy health check'lari. 33–34-qismlarda ularni ulaymiz. - Intervyu savollari: "Graceful shutdown qanday yoziladi?", "SIGTERM va SIGKILL farqi?", "Nega deploy paytida 502 chiqadi va uni qanday yo'qotasiz?", "Readiness va liveness yopilish paytida nima qaytaradi?"
- Monitoring:
exitCode = 1bilan chiqqan yopilishlar soni — "deploy toza o'tdimi" degan savolga javob.
Xulosa
server.close()yolg'iz yetmaydi: balanser SIGTERM'ni bilmaydi va yopiq eshikka so'rov yuboradi.- To'liq tartib: readiness 503 → kechikish → SSE →
server.close()→app.close()(navbat, Redis, pool) →exitCode. - Kechikish ≥ balanser davri × chegara; lokalda 0, prodakshnda ~5 s.
- Hammasi grace period ichiga sig'sin: Docker 10 s, Kubernetes 30 s — kanon 15+ s Docker sukutiga sig'maydi.
- Halokatda kechikish yo'q; liveness yopilishda ham 200.
- Ishchi jarayonda ham bir xil g'oya: yangi ish olmaslik → boshlanganini tugatish → ulanishlarni yopish.
Keyingi dars: Health check endpoint'lari — liveness va readiness'ga uchinchi tekshiruv, startup qo'shiladi va orkestrator sozlamalarini hisoblashni o'rganamiz.
Manbalar
- Kubernetes hujjati — Pod Lifecycle, Termination of Pods: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination (ochib tekshirildi, 2026-11-03)
- Kubernetes hujjati — Container Lifecycle Hooks (
preStop): https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/ (ochib tekshirildi, 2026-11-03) - Docker hujjati —
docker container stop: https://docs.docker.com/reference/cli/docker/container/stop/ (ochib tekshirildi, 2026-11-03) - Docker Compose hujjati —
stop_grace_period: https://docs.docker.com/reference/compose-file/services/#stop_grace_period (ochib tekshirildi, 2026-11-03) - systemd hujjati —
TimeoutStopSec: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html (ochib tekshirildi, 2026-11-03) - Node.js 24 hujjati —
server.close(),closeIdleConnections(),closeAllConnections(): https://nodejs.org/docs/latest-v24.x/api/http.html#serverclosecallback - BullMQ hujjati — Graceful shutdown: https://docs.bullmq.io/guide/workers/graceful-shutdown (ochib tekshirildi, 2026-11-03)
- Kanon (IlmHamroh, 2026-11-03):
vazifalar-apiloyihasining shu bosqichi —jarayon.ts,main.ts,salomatlik.module.ts,ishchi.ts; haqiqiy SIGTERM Linux/Docker'da sinalmagan. Drain bloki, model, mashq yechimlari va loyiha nusxasidagi testlar — biz ishga tushirdik (2026-11-03, Node 24.21.0, NestJS 12.2.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!