Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Chegaralar xaritasi
- 2.1 Qatlamlar
- 2.2 vazifalar-api dagi chegaralar
- 3. ReDoS: bitta regex — butun server
- 3.1 O'lchov
- 3.2 Himoya
- 4. Bombalar va sekin ulanishlar
- 4.1 Zip va piksel bomba
- 4.2 Slowloris
- 5. Login toshqini
- 5.1 Kanon o'lchovi
- 5.2 Nega GET ham sekinlashdi?
- 5.3 Tezlik cheklovi yetmaydimi?
- 6. Semafor
- 6.1 G'oya
- 6.2 Brauzerda sinaymiz
- 6.3 Semafordan keyin
- 7. Qimmat ishni navbatga olish
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi chegara?
- 2-mashq (o'rta): p50 va p99
- 3-mashq (qiyin): vaqt chegarali semafor
- 11. Loyiha qadami: semafor.ts va parol hash'i
- 11.1 src/umumiy/semafor.ts
- 11.2 src/auth/parol.ts — hash semafor orqali
- 11.3 Testlar
- 11.4 Tekshiruv va commit
- 12. Real ishda
- Xulosa
- Manbalar
DoS va resurs tugashidan himoya: chegaralar, ReDoS va login toshqiniga semafor
Qisqacha: DoS (Denial of Service — xizmatdan mahrum qilish) — serverni shunchalik band qilishki, oddiy foydalanuvchilar javob ololmay qoladi. Eng xavflisi — nosimmetrik hujum: hujumchiga arzon, serverga qimmat so'rov.
vazifalar-apida bunday so'rov — login: har biri 19 MiB xotira va ~70 ms argon2 ishi. Kanon o'lchovida 200 ta parallel noto'g'ri login paytida boshqa foydalanuvchining oddiyGETso'rovi p99 bo'yicha 42.5 ms dan 3 283 ms ga sekinlashdi. Parol hash'iga semafor (bir vaqtda 4 ta, navbatda 64 ta, ortig'i — darhol 503) qo'yilgach — 733 ms. Qolgan chegaralar (tana, sahifa, timeout'lar) 25–27-qismlardan beri bor — bu darsda ularni bitta xaritaga yig'amiz.
Bu darsda
- DoS va DDoS farqini hamda nosimmetrik hujum g'oyasini tushuntirasiz.
vazifalar-apidagi barcha resurs chegaralarini (tana, sahifa, timeout, regex) bitta jadvalda ko'rsatasiz.- ReDoS'ni o'lchov bilan tushuntirasiz va V8 dagi himoya bayrog'ini bilasiz.
- Login toshqini nega butun serverni sekinlashtirishini thread pool va ulanishlar hovuzi orqali tushuntirasiz.
vazifalar-apigaSemaforklassini, uning testlarini qo'shib, parol hash'ini shu orqali o'tkazasiz.
Oldin bilishingiz kerak: Parolni xavfsiz saqlash (thread pool navbati), Rate limiting, Server timeout'lari va keep-alive, Regex amaliyotda va ReDoS, libuv thread pool va OS asinxron I/O.
Oldingi darsda hujumchi serverni boshqa joyga so'rov yuborishga majburladi. Bugun maqsad oddiyroq va qo'polroq: serverni hech qayerga yubormasdan, o'zini bo'g'ib qo'yish.
1. Nega bu kerak?
Choyxonani tasavvur qiling: oshpaz bitta, qozon ikkita. Kimdir kirib, 200 kishilik osh buyurtma qiladi va pul to'lamay chiqib ketadi. Oshpaz uni pishirguncha oddiy mijozlar choy ham ololmaydi. Buyurtma berish — bir og'iz so'z, pishirish — soatlab ish. Mana shu farq — nosimmetriya.
Serverda ham shunday. Hujumchi uchun so'rov yuborish deyarli bepul: bir qator curl, bir necha bayt. Server uchun esa ba'zi so'rovlar qimmat:
| So'rov | Hujumchi narxi | Server narxi |
|---|---|---|
| noto'g'ri login | ~80 bayt | argon2: 19 MiB, ~70 ms |
| ReDoS regex'ga matn | 30 belgi | soniyalab CPU |
| zip bomba | 50 KB | 50 MB+ xotira |
| sekin ulanish | 1 ulanish | soket + timeout'gacha joy |
?limit=100000 |
20 bayt | butun jadval |
DoS — bitta manbadan shunday so'rovlar. DDoS (Distributed DoS) — minglab manbadan, ko'pincha zararlangan qurilmalar to'ridan (botnet). DDoS'ga qarshi asosiy himoya tarmoq darajasida: CDN va DDoS himoya xizmatlari (masalan Cloudflare), provayder filtrlari. Bu sizning Node kodingiz qila oladigan ish emas. Kod qila oladigani — har bir qimmat ishni chegaralash: hujumchi bitta so'rov bilan juda ko'p resurs ololmasin.
OWASP API ro'yxatida bu toifa — API4 Unrestricted Resource Consumption (cheklovsiz resurs sarfi). Bu nom yaxshi: muammo "hujum" emas, chegara yo'qligi.
2. Chegaralar xaritasi
2.1 Qatlamlar
So'rov serverga yetguncha va server ichida bir necha "darvoza"dan o'tadi. Har birida bitta chegara:
flowchart TD
T[Tarmoq: CDN, DDoS himoya] --> N[nginx: ulanishlar]
N --> H[Node http: headersTimeout, requestTimeout]
H --> B[Tana: 16 KB, 413]
B --> Z[Zod: uzunlik, sahifa limiti]
Z --> R[Tezlik cheklovi: 429]
R --> S[Semafor: 4 + 64, 503]
S --> D[(Postgres: statement_timeout 5 s)]Yuqoridagi ikki qatlam — infratuzilma (33-qismda deploy bilan). Qolganlari vazifalar-api kodida.
2.2 vazifalar-api dagi chegaralar
Ko'pchiligini oldingi qismlarda qo'yganmiz. Bu yerda faqat bitta jadvalga yig'amiz:
| Chegara | Qiymat | Qayerdan |
|---|---|---|
| so'rov tanasi | 16 KB → 413 | 25-qism, express.json |
| sarlavhalar kutish (Slowloris) | headersTimeout 10 s |
25-qism |
| butun so'rov | requestTimeout 30 s |
25-qism |
| sahifa | limit ≤ 100 → 400 |
25-qism, Zod |
PUT ommaviy |
≤ 1 000 vazifa | 25-qism |
| SQL so'rov | statement_timeout 5 s |
26-qism |
| tezlik | hisob + IP → 429 | Rate limiting |
| parol (ro'yxat) | 129 belgi → 422 | Ro'yxat va login oqimi |
| parol (login) | 512 belgidan uzun → 422, hash yo'q | o'sha |
| argon2 bir vaqtda | 4 + navbat 64 → 503 | bu dars |
Kanon tajribasida chegaralar qayta sinaldi: ?limit=100000 — 400, ro'yxatda 129 belgili parol — 422, loginda 512 belgidan uzun parol — 422. Oxirgisi muhim: bunday parol hash qilinmaydi ham. Uzun kirishni argon2 ga berish — hujumchiga bepul CPU sovg'a qilish.
Oldingi darsda yana bitta chegara sinalgan edi: 16 KB ichiga sig'adigan 2 500 darajali ichma-ich JSON — 422, server ishlashda davom etdi.
Tekshirib ko'ring: Nega login'da parol uzunligi tekshiruvi argon2 dan oldin turishi kerak?
Javob
argon2 ishining narxi parol uzunligiga ham bog'liq, va asosiysi — u qimmat. Tekshiruv keyin tursa, 1 MB parol avval hash qilinadi, keyin rad etiladi. Arzon tekshiruvlar doim qimmatlaridan oldin: uzunlik, format, tezlik cheklovi — keyin argon2 va baza.
3. ReDoS: bitta regex — butun server
3.1 O'lchov
Regex amaliyotda va ReDoS darsida halokatli orqaga qaytishni (catastrophic backtracking) ko'rgansiz. Serverda bu ayniqsa xavfli: JavaScript bitta oqimda ishlaydi, regex ishlayotganda event loop to'xtaydi (Event loop'ni bloklash) — boshqa hech kimning so'rovi javob olmaydi.
Kanon vazifalar-api regex'larini 16 000 belgilik kirishda (tana chegarasi atrofida) o'lchadi va klassik zaif regex bilan solishtirdi:
| Regex va kirish | Vaqt |
|---|---|
teg regex'i, # + 16 000 harf |
0.94 ms |
teg regex'i, 8 000 ta #a |
0.08 ms |
telefon ^\+[1-9]\d{7,14}$, 16 000 raqam |
0.09 ms |
/^(a+)+$/, a × 20 + ! |
57.72 ms |
/^(a+)+$/, a × 24 + ! |
120.55 ms |
/^(a+)+$/, a × 26 + ! |
483.19 ms |
Loyiha regex'lari 16 KB da ham bir millisekunddan kam — ular chiziqli. Zaif regex esa 26 belgida yarim soniya. Har qo'shimcha belgi vaqtni taxminan ikki baravar oshiradi: 24 → 26 da to'rt baravar. (20 va 24 orasidagi farq kichikroq — birinchi o'lchovga regex kompilyatsiyasi ham kiradi.)
Biz bir qadam oldinga bordik va 30 belgini sinadik (biz ishga tushirdik, Node 24.21.0, o'sha mashina): 10 517.9 ms. 30 belgilik matn — o'n soniya to'xtagan server.
3.2 Himoya
- Regex'ni to'g'ri yozish. Ichma-ich kvantifikatorlar (
(a+)+,(a|a)*) — xavf belgisi. Loyiha regex'lari chiziqli. - Kirish uzunligini cheklash. Regex'ga 16 KB emas, kerakli 200 belgi bersangiz, zaif regex ham kamroq zarar qiladi.
- Chiziqli dvigatel.
re2npm paketi — Google'ning RE2 kutubxonasi, orqaga qaytishsiz. Kanonda sinalmagan. - V8 bayrog'i. Node'ning V8 dvigatelida
--enable-experimental-regexp-engine-on-excessive-backtracksbayrog'i bor: orqaga qaytish juda ko'payganda regex chiziqli dvigatelga o'tadi. Biz Node 24.21.0 da sinadik — o'sha/^(a+)+$/:
bayroqsiz: 20 → 70.1 ms 24 → 148.6 ms 26 → 645.8 ms 30 → 10517.9 ms
bayroq bilan: 20 → 2.3 ms 24 → 0.4 ms 26 → 0.3 ms 30 → 0.3 msNatija (false) ikkala holatda bir xil. Lekin nomida "experimental" so'zi bor va u sukut bo'yicha o'chiq. Prodakshnda unga tayanmang — birinchi uch himoya asosiy. Bu raqamlar kanon o'lchovidan biroz farq qiladi: vaqt mashina yuklamasiga bog'liq.
4. Bombalar va sekin ulanishlar
4.1 Zip va piksel bomba
Zip bomba — kichik siqilgan fayl ichida ulkan ma'lumot. zlib: siqish darsida maxOutputLength bilan ochishni cheklashni ko'rgansiz: ochish chegaraga yetganda to'xtaydi.
Piksel bomba — kichik rasm fayli, lekin sarlavhasida 50 000 × 50 000 piksel yozilgan. Uni ochish uchun gigabaytlab xotira kerak. Rasm kutubxonalarida piksel chegarasi bor (sharp'da limitInputPixels) — 29-qismdagi rasm darsida.
vazifalar-api da hozircha fayl yuklash yo'q, shuning uchun bu ikkisi kanon kodiga tegmadi. Lekin qoida bir xil: begona ma'lumotni ochishdan oldin — natija hajmiga chegara.
4.2 Slowloris
Server timeout'lari va keep-alive darsida ko'rgansiz: hujumchi ulanish ochadi va sarlavhalarni har 10 soniyada bitta baytdan yuboradi. Server har ulanishni kutib, soketlarini band qiladi. Node 24 da headersTimeout (kanon: 10 s) va requestTimeout (30 s) bunga qarshi. Bu qiymatlar 25-qismdan beri server.ts da — 28-qismda o'zgartirilmadi.
5. Login toshqini
5.1 Kanon o'lchovi
Endi asosiy tajriba. Kanon vazifalar-api ni haqiqiy Postgres bilan ishga tushirdi, tezlik cheklovini ataylab o'chirdi (ya'ni Rate limiting bo'lmasa nima bo'lishini ko'rish uchun) va bir vaqtda ko'p noto'g'ri login yubordi. Shu paytda boshqa foydalanuvchi oddiy GET /v1/vazifalar so'rovlarini yuborib turdi — uning javob vaqtini o'lchadi.
O'lchov natijasi (oldin, semaforsiz). Har qatorda GET p99 ga qarang — bu boshqa foydalanuvchining eng sekin javoblari:
tinch: GET p50 14.9 ms, p99 42.5 ms
50 parallel login: hammasi 1.21 s (50 × 401); shu paytda GET p50 14.6 ms, p99 647.5 ms; RSS +85 MiB
200 parallel login: hammasi 4.25 s (200 × 401); shu paytda GET p50 13.3 ms, p99 3283.3 ms; RSS +94 MiB
tinch paytda bitta login: 71.0 msp99 — so'rovlarning 99 foizi shu vaqtdan tezroq; eng sekin 1 foiz bundan ham sekin. Foydalanuvchi aynan shu "eng sekin" holatni sezadi. p50 (median) deyarli o'zgarmadi — ko'p GET lar baribir tez o'tdi. Lekin p99 77 baravar oshdi: ba'zi so'rovlar 3 soniyadan ko'p kutdi.
5.2 Nega GET ham sekinlashdi?
GET /v1/vazifalar argon2 ishlatmaydi-ku? Sabab — umumiy resurslar:
- libuv thread pool — 4 ta ish oqimi (Parolni xavfsiz saqlash darsida o'lchagansiz: 16 login — 4 navbat). 200 ta argon2 navbatda turadi.
- Postgres ulanishlar hovuzi (pool) — Connection pooling: login so'rovlari ham bazaga boradi (foydalanuvchini topish, jurnal), hovuzdagi ulanishlar band bo'ladi va
GETulanish kutadi. - xotira — har argon2 19 MiB; RSS +94 MiB.
Ya'ni muammo bitta endpoint emas, umumiy navbat: qimmat so'rovlar arzonlarini ham orqaga suradi.
5.3 Tezlik cheklovi yetmaydimi?
Rate limiting darsidagi cheklagich "hisob + IP" bo'yicha 5 ta xato urinishdan keyin 429 beradi. Kanon izohi aynan shu: cheklagich yoniq bo'lsa, bitta IP'dan 200 ta login bo'lmasdi. Lekin:
- DDoS'da IP'lar minglab — har biri o'z chegarasi ichida qoladi;
- har xil email'lar bilan "hisob + IP" kaliti ham har safar yangi;
- cheklagichning o'zi ham bazaga murojaat qiladi.
Tezlik cheklovi — har mijozga chegara. Bizga yana bitta kerak: butun server bo'yicha qimmat ish soniga chegara. Bu — semafor.
6. Semafor
6.1 G'oya
Semafor — bir vaqtda nechta ish bajarilishini cheklaydigan hisoblagich. Choyxona misolida: qozon ikkita (bir vaqtda 2 ta osh), kutish joyi uchta. To'rtinchi mijoz kelsa, eshik oldida "hozir joy yo'q, keyinroq keling" deyiladi — u soatlab kutib, eshikni to'sib turmaydi.
Uchta qaror bor:
- Bir vaqtda nechta — kanon: 4 ta (thread pool hajmi; ko'prog'i baribir pool navbatida turardi).
- Navbat qancha — kanon: 64 ta.
- Navbat to'lsa — darhol 503 Service Unavailable va
Retry-After: 1sarlavhasi: "bir soniyadan keyin qayta urinib ko'ring".
Nega kutdirib qo'ymaymiz? Kutayotgan har so'rov ham resurs: ochiq soket, xotira, ba'zan baza ulanishi. Cheksiz navbat — cheksiz xotira. Tez rad etish (fail fast) — server "sog'" qismini saqlab qoladi.
6.2 Brauzerda sinaymiz
Kanon Semafor ining soddalashtirilgan nusxasi (TypeScript tiplarisiz). Bir vaqtda 2, navbat 3; oltita "login" bir vaqtda keladi, har biri 30 ms "hash"lanadi:
class Semafor {
#band = 0;
#navbat = [];
constructor(maks, navbatMaks) {
this.maks = maks;
this.navbatMaks = navbatMaks;
}
async bajar(fn) {
if (this.#band >= this.maks) {
if (this.#navbat.length >= this.navbatMaks) {
throw new Error("503");
}
await new Promise((ok) => this.#navbat.push(ok));
} else {
this.#band++;
}
try {
return await fn();
} finally {
const keyingi = this.#navbat.shift();
if (keyingi === undefined) this.#band--;
else keyingi();
}
}
}
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
const s = new Semafor(2, 3);
let birVaqtda = 0;
async function login(n) {
try {
await s.bajar(async () => {
birVaqtda++;
console.log(`${n}: hash boshlandi (bir vaqtda ${birVaqtda})`);
await kut(30);
birVaqtda--;
});
console.log(`${n}: 401`);
} catch (e) {
console.log(`${n}: ${e.message} — navbat to'la`);
}
}
await Promise.all([1, 2, 3, 4, 5, 6].map(login));Konsolda:
1: hash boshlandi (bir vaqtda 1)
2: hash boshlandi (bir vaqtda 2)
6: 503 — navbat to'la
3: hash boshlandi (bir vaqtda 2)
1: 401
4: hash boshlandi (bir vaqtda 2)
2: 401
5: hash boshlandi (bir vaqtda 2)
3: 401
4: 401
5: 401Qatorma-qator:
- 1 va 2 — darhol boshlandi,
#band2 ga yetdi. - 3, 4, 5 — navbatga turdi (navbat 3/3).
- 6 — joy ham, navbat ham yo'q: darhol 503. U 30 ms kutmadi.
- 1 tugashi bilan
finallynavbatdagi birinchisini — 3 ni — uyg'otdi. "Bir vaqtda" hech qachon 2 dan oshmadi.
Eng nozik qator — finally ichidagisi. Ish tugaganda navbatda kimdir bo'lsa, #band kamaytirilmaydi: joy to'g'ridan-to'g'ri keyingisiga o'tadi. Agar avval kamaytirib, keyin uyg'otsak, shu orada boshqa yangi so'rov "joy bor" deb kirib qolishi va navbatdagini chetlab o'tishi mumkin edi.
finally yana bir narsani kafolatlaydi: fn() xato tashlasa ham joy bo'shaydi. Aks holda har xato bitta joyni abadiy "yeb" qo'yardi va bir necha xatodan keyin semafor butunlay yopilardi.
6.3 Semafordan keyin
Kanon semaforni qo'shib, o'sha tajribani takrorladi. Yana GET p99 ni va qavs ichidagi × 401 sonini oldingi natija bilan solishtiring:
tinch: GET p50 13.5 ms, p99 34.3 ms
50 parallel login: hammasi 1.21 s (50 × 401); shu paytda GET p50 28.1 ms, p99 177.6 ms; RSS +81 MiB
200 parallel login: hammasi 2.10 s (84 × 401); shu paytda GET p50 44.3 ms, p99 733.5 ms; RSS +102 MiB
tinch paytda bitta login: 74.8 ms- 50 login, semaforsiz647,5 ms
- 50 login, semafor bilan177,6 ms
- 200 login, semaforsiz3 283,3 ms
- 200 login, semafor bilan733,5 ms
Manba: IlmHamroh kanon tajribasi (DoS o'lchovi): vazifalar-api, Node 24.21.0, PostgreSQL 18.6, i5-12500H, Windows 11, tezlik cheklovi o'chiq, 2026-10-28
Uch kuzatuv — halol, yaxshisi ham, yomoni ham:
- p99 4.5 baravar yaxshilandi (3 283 → 733 ms). Toshqin ham ikki baravar tez tugadi (4.25 → 2.10 s).
- 200 tadan faqat 84 tasi 401 oldi. O'lchov skripti faqat 401 javoblarni sanagan; qolgan 116 tasi 401 emas edi. Semafor ishlagan holatda bu — tez qaytgan 503 javoblar (ortiqcha so'rov aynan 503 olishini semafor testi alohida tasdiqlaydi). Ya'ni hujumchining yarmidan ko'p so'rovi argon2 ga umuman yetmadi.
- p50 yomonlashdi (13 → 44 ms) va xotira kamaymadi (+94 → +102 MiB). Semafor navbatni cheklaydi, xotirani emas: 4 ta argon2 × 19 MiB baribir ishlaydi, ustiga ko'p so'rov obyekti. p50 ning o'sishi — tez rad etilgan so'rovlar oqimi serverni boshqacha band qilgani. Mukammal yechim yo'q — p99 yaxshilanishi asosiy maqsad edi.
Tekshirib ko'ring: Semafor va tezlik cheklovi — bir xil narsami? Biri ikkinchisini almashtira oladimi?
Javob
Yo'q. Tezlik cheklovi kim so'rayotganiga qaraydi: bitta hisob yoki IP vaqt oynasida N tadan ko'p so'rov qilmasin. Semafor qancha ish bir vaqtda bajarilayotganiga qaraydi — kim so'raganidan qat'i nazar. Ming xil IP'dan bittadan so'rov tezlik cheklovidan o'tadi, lekin semaforda to'xtaydi. Bitta IP'dan sekin-asta mingta so'rov semafordan o'tadi, lekin tezlik cheklovida to'xtaydi. Ikkalasi kerak.
7. Qimmat ishni navbatga olish
Ba'zi ishlarni umuman so'rov ichida bajarmaslik kerak: katta eksport, hisobot, video. Ular navbatga (queue) qo'yiladi: so'rov darhol 202 Accepted qaytaradi, ish esa fonda, alohida jarayonda, o'z tezligida bajariladi. Bu 29-qismning mavzusi (BullMQ). vazifalar-api dagi eksport ishlari 25-qismdan beri shu g'oyada — so'rov ish yaratadi, natija keyin olinadi.
8. Hujumchi nigohi
| Hujumchi sinaydi | Maqsadi | vazifalar-api |
|---|---|---|
| 200 parallel login, har xil email | CPU va xotira | semafor: ortig'i 503 |
| 1 MB parol | uzun argon2 | 422, hash yo'q |
?limit=100000 |
butun jadval | 400 |
| 20 KB tana | xotira | 413 |
| 2 500 darajali JSON | rekursiya | 422 |
| sarlavhani sekin yuborish | soketlar | headersTimeout 10 s |
| og'ir SQL (qidiruv) | baza | statement_timeout 5 s |
Hujumchi "eng qimmat endpoint"ni qidiradi: javob vaqtlarini solishtiradi va eng sekinini toshqin bilan uradi. Sizning vazifangiz — eng qimmatini birinchi topish. Kanon buni o'lchov bilan qildi: login eng qimmati bo'lib chiqdi.
9. Ko'p uchraydigan xatolar
| Xato | Nega noto'g'ri | To'g'risi |
|---|---|---|
| "bizda rate limit bor — DoS'dan himoyalanganmiz" | ko'p IP chetlab o'tadi | rate limit + global semafor |
cheksiz navbat (masalan, p-limit da) |
kutayotganlar xotira yeydi | navbat chegarasi + 503 |
finally siz semafor |
xato joyni abadiy band qiladi | try/finally |
| uzunlikni hash'dan keyin tekshirish | hujumchiga bepul CPU | arzon tekshiruv oldin |
foydalanuvchi regex'iga new RegExp(kirish) |
ReDoS eshigi | ekranlash yoki RE2 |
503 da Retry-After yo'q |
mijoz darhol qayta uradi | Retry-After |
| faqat o'rtacha vaqtga qarash | p50 yaxshi, p99 halokat | p99 o'lchash |
10. Mashqlar
Mashqlar kurs/mashqlar/28/29-dos/ da.
1-mashq (oson): qaysi chegara?
Har hujumga vazifalar-api dagi qaysi chegara javob beradi?
- Tanada 5 MB JSON.
GET /v1/vazifalar?limit=999999.- 10 000 xil IP'dan har biri bitta login.
- Bitta IP'dan minutiga 300 ta login.
- Server sarlavhani 15 soniya kutdi.
Yechim
- Tana chegarasi — 16 KB, 413.
- Zod sahifa limiti — 400.
- Argon2 semafori — navbat to'lsa 503 (har IP tezlik chegarasi ichida).
- Tezlik cheklovi — 429.
headersTimeout(10 s) — ulanish yopiladi.
2-mashq (o'rta): p50 va p99
foiz(qiymatlar, q) funksiyasini yozing: massivni saralab, q (0–1) foizdagi qiymatni qaytarsin (kanon skriptidagidek: indeks Math.floor(n * q), oxirgisidan oshmasin). Quyidagi 10 ta javob vaqtida p50 va p99 ni toping va o'rtacha bilan solishtiring: [12, 14, 13, 15, 11, 14, 13, 12, 2900, 16].
Yechim
function foiz(qiymatlar, q) {
const s = qiymatlar.toSorted((a, b) => a - b);
return s[Math.min(s.length - 1, Math.floor(s.length * q))];
}
const vaqtlar = [12, 14, 13, 15, 11, 14, 13, 12, 2900, 16];
const ortacha = vaqtlar.reduce((a, b) => a + b, 0) / vaqtlar.length;
console.log("p50:", foiz(vaqtlar, 0.5));
console.log("p99:", foiz(vaqtlar, 0.99));
console.log("o'rtacha:", ortacha);Konsolda:
p50: 14
p99: 2900
o'rtacha: 302p50 "hammasi yaxshi" deydi, o'rtacha 302 ms — hech bir haqiqiy so'rovga o'xshamaydi. p99 esa bitta foydalanuvchi 2.9 soniya kutganini ko'rsatadi. Shuning uchun DoS o'lchovlarida p99 qaraladi. toSorted — asl massivni o'zgartirmaydi (toSorted).
3-mashq (qiyin): vaqt chegarali semafor
Semaforga yangi imkoniyat qo'shing: navbatda kutishMs dan ko'p turgan so'rov ham 503 olsin (navbatdan chiqarilsin). Ishora: navbatga { ok, rad } juftini qo'ying, setTimeout bilan rad chaqiring va elementni navbatdan o'chiring; vaqtida joy kelsa — taymerni clearTimeout bilan to'xtating. Sinov: bir vaqtda 1, navbat 5, kutishMs 50; uchta ish 40 ms dan.
Yechim
class Semafor {
#band = 0;
#navbat = [];
constructor(maks, navbatMaks, kutishMs) {
Object.assign(this, { maks, navbatMaks, kutishMs });
}
async bajar(fn) {
if (this.#band >= this.maks) {
if (this.#navbat.length >= this.navbatMaks) {
throw new Error("503");
}
await new Promise((ok, rad) => {
const joy = { ok: () => { clearTimeout(t); ok(); } };
const t = setTimeout(() => {
this.#navbat.splice(this.#navbat.indexOf(joy), 1);
rad(new Error("503 (kutish tugadi)"));
}, this.kutishMs);
this.#navbat.push(joy);
});
} else this.#band++;
try {
return await fn();
} finally {
const keyingi = this.#navbat.shift();
if (keyingi === undefined) this.#band--;
else keyingi.ok();
}
}
}
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
const s = new Semafor(1, 5, 50);
const ish = (n) => s.bajar(() => kut(40)).then(
() => console.log(n, "bajarildi"),
(e) => console.log(n, e.message),
);
await Promise.all([1, 2, 3].map(ish));Konsolda:
1 bajarildi
3 503 (kutish tugadi)
2 bajarildi1 — 0–40 ms. 2 — 40 ms da joy oldi (50 ms dan oldin), 80 ms da tugadi. 3 — 50 ms gacha kutdi, joy kelmadi — 503. Bu naqsh "eskirgan" so'rovlarni tashlaydi: foydalanuvchi baribir allaqachon ketgan bo'lishi mumkin. joy obyektining o'zini navbatdan topib o'chiramiz — indeks emas, chunki boshqalar orada siljigan bo'lishi mumkin.
11. Loyiha qadami: semafor.ts va parol hash'i
11.1 src/umumiy/semafor.ts
Yangi modul — to'liq. Telefon uchun qatorlar bo'lingan, boshidagi izoh qisqartirilgan; mantiq kanondagidek (bo'lingan nusxani kanon loyihasi nusxasida tsc, ESLint va testlar bilan tekshirdik):
// semafor.ts — qimmat ish (argon2 — ~30 ms CPU + 19 MiB) uchun bir
// vaqtda N ta va navbat chegarasi. Navbat to'lsa — darhol 503 (kutib
// turgan so'rovlar pool ulanishlarini va xotirani band qilmasin).
// O'lchov: 200 parallel login paytida oddiy GET p99 42 ms → 3.3 s edi
import { IlovaXatosi } from "./xatolar.ts";
export class BandXatosi extends IlovaXatosi {
constructor() {
super(503, "Server band — bir oz kutib qayta urinib ko'ring", {
kod: "server-band",
sarlavha: "Server band",
httpSarlavhalar: { "Retry-After": "1" },
});
}
}
export class Semafor {
readonly #maks: number;
readonly #navbatMaks: number;
#band = 0;
readonly #navbat: (() => void)[] = [];
constructor(maks: number, navbatMaks: number) {
this.#maks = maks;
this.#navbatMaks = navbatMaks;
}
get holat(): { band: number; navbat: number } {
return { band: this.#band, navbat: this.#navbat.length };
}
async bajar<T>(fn: () => Promise<T>): Promise<T> {
if (this.#band >= this.#maks) {
if (this.#navbat.length >= this.#navbatMaks) {
throw new BandXatosi();
}
await new Promise<void>((ok) => this.#navbat.push(ok));
} else {
this.#band++;
}
try {
return await fn();
} finally {
const keyingi = this.#navbat.shift();
if (keyingi === undefined) this.#band--;
else keyingi(); // joy keyingisiga o'tadi (band — o'zgarmaydi)
}
}
}BandXatosi — 25-qismdagi IlovaXatosi ning vorisi: markaziy xato ishlovchi uni Problem Details formatidagi 503 javobga aylantiradi va Retry-After: 1 sarlavhasini qo'shadi. holat getter'i — faqat testlar va kuzatuv uchun.
11.2 src/auth/parol.ts — hash semafor orqali
Parol modulida ikki o'zgarish:
+import { Semafor } from "../umumiy/semafor.ts";
+
+/** Bir vaqtda 4 ta hash (libuv threadpool sukut 4), navbatda
+ * 64 tagacha; ortig'i — 503 server-band */
+export const HASH_SEMAFORI = new Semafor(4, 64);
- return argon2("argon2id", { message: parol, nonce: tuz, … });
+ return HASH_SEMAFORI.bajar(() =>
+ argon2("argon2id", { message: parol, nonce: tuz, … }));(… — memory, passes, parallelism, tagLength parametrlari, Parolni xavfsiz saqlash dagidek.) Semafor bitta joyda — argon2 ni chaqiradigan ichki hisobla funksiyasida. Ro'yxat, login, parol almashtirish va soxtaHash — hammasi shu funksiya orqali o'tadi. Shuning uchun hech biri semaforni chetlab o'ta olmaydi.
Nega 4? Node'ning libuv thread pool'i sukut bo'yicha 4 ta oqim. 4 dan ortiq argon2 baribir pool navbatida turardi, faqat bu navbat ko'rinmas va chegarasiz edi. Semafor uni ko'rinadigan va chegarali qildi.
11.3 Testlar
test/semafor.test.ts — 2 test:
test("maks 2 parallel, navbat 3: 5 tasi bajariladi, 6-si darhol " +
"503; bir vaqtda hech qachon 2 dan ko'p emas", async () => {
const s = new Semafor(2, 3);
let bir = 0, eng = 0;
const ish = () => s.bajar(async () => {
bir++; eng = Math.max(eng, bir);
await kut(20); bir--; return "ok";
});
const vazifalar = Array.from({ length: 5 }, ish);
await assert.rejects(ish(), (x: unknown) =>
x instanceof BandXatosi && x.holat === 503 &&
x.httpSarlavhalar["Retry-After"] === "1");
assert.deepEqual(await Promise.all(vazifalar),
["ok", "ok", "ok", "ok", "ok"]);
assert.equal(eng, 2);
assert.deepEqual(s.holat, { band: 0, navbat: 0 });
});Ikkinchi test: ish xato bersa ham joy bo'shaydi (Semafor(1, 1) — xatodan keyingi ish bajariladi) va HASH_SEMAFORI bo'sh holatda (band: 0, navbat: 0). Ishga tushiramiz (biz ishga tushirdik, kanon nusxasi va bo'lingan semafor.ts bilan, Node 24.21.0):
node --test test/semafor.test.ts✔ maks 2 parallel, navbat 3: 5 tasi bajariladi, 6-si darhol 503; bir vaqtda hech qachon 2 dan ko'p emas (90.5245ms)
✔ ish xato bersa ham joy bo'shaydi; parol hash semafori — 4 / 64 (0.6244ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 346.337811.4 Tekshiruv va commit
Butun loyiha tekshiruvi (kanon, 2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 229 (228 + 1 o'tkazilgan) — ikkita yangi test. Brauzer testlari 10/10, migratsiya yo'q.
M="feat(auth): parol hash'i uchun semafor — bir vaqtda 4,"
M+=" navbat 64, ortig'i 503 Retry-After"
git add src/umumiy/semafor.ts src/auth/parol.ts test/semafor.test.ts
git commit -m "$M"12. Real ishda
- Barcha katta xizmatlar API'larida "bir vaqtdagi so'rovlar" chegarasi bor va ortig'iga 429 yoki 503 qaytaradi. Mijoz kutubxonalari
Retry-Afterni o'qib, qayta urinadi. - npm paketlar:
p-limit,p-queue— bir vaqtdagi ishlar chegarasi. Navbat chegarasi va 503 ni ularning ustiga o'zingiz qurasiz. Kanon o'z 40 qatorini yozdi: bog'liqlik qo'shmaslik va mantiqni ko'rish uchun. - Kubernetes va yuk taqsimlagichlar ham "ortiqcha yukda tez rad etish" (load shedding) tamoyilida ishlaydi.
- Kuzatuv: p99 — standart ko'rsatkich. Grafana dashboard'larida p50/p95/p99 yonma-yon turadi.
- Intervyuda: "DoS va DDoS farqi?", "ReDoS nima?", "Nega p99, o'rtacha emas?", "Rate limit va semafor farqi?".
Xulosa
- DoS — nosimmetrik so'rovlar bilan serverni band qilish; DDoS — tarmoq darajasida, kod esa har qimmat ishni chegaralaydi (API4).
vazifalar-apichegaralari: tana 16 KB, sahifa ≤ 100, timeout'lar,statement_timeout, parol uzunligi hash'dan oldin.- ReDoS: zaif regex 30 belgida 10 soniya; himoya — chiziqli regex, uzunlik chegarasi, RE2.
- Login toshqini umumiy resurslarni (thread pool, ulanishlar hovuzi, xotira) band qilib, boshqa so'rovlarni ham sekinlashtiradi: p99 3 283 ms.
- Semafor (4 + 64 → 503) — butun server bo'yicha chegara: p99 733 ms; tezlik cheklovining o'rnini bosmaydi, uni to'ldiradi.
Keyingi dars: CAPTCHA tokenini serverda tekshirish, honeypot va SMS pumping — botlarni odamdan ajratish: Turnstile tokeni serverda, rasmiy test kalitining kutilmagan hostname tuzog'i, asal idish va pulga tushiradigan SMS hujumiga kunlik byudjet.
Manbalar
- OWASP API Security Top 10:2023 — API4 Unrestricted Resource Consumption: https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/ (2026-10-28 da o'qildi)
- OWASP Denial of Service Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
- RFC 9110, 503 va
Retry-After: https://www.rfc-editor.org/rfc/rfc9110#name-503-service-unavailable - Node.js 24
http.Servertimeout'lari: https://nodejs.org/docs/latest-v24.x/api/http.html#serverheaderstimeout - V8 bayroqlari:
node --v8-options(Node 24.21.0 da tekshirildi); RE2: https://github.com/google/re2 - Kanon tajribasi (IlmHamroh, 2026-10-28): DoS o'lchovi (oldin va keyin; vazifalar-api, Node 24.21.0, PostgreSQL 18.6, i5-12500H, Windows 11),
semafor.ts, holat tekshiruvi. Regex bayrog'i sinovi, brauzer misollari vasemafor.test.tsnatijasi — biz ishga tushirdik (2026-10-28, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!