IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi29/31-dars21 daqiqa
Mundarija (34)

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-api da 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 oddiy GET so'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-api dagi 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-api ga Semafor klassini, 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

  1. Regex'ni to'g'ri yozish. Ichma-ich kvantifikatorlar ((a+)+, (a|a)*) — xavf belgisi. Loyiha regex'lari chiziqli.
  2. Kirish uzunligini cheklash. Regex'ga 16 KB emas, kerakli 200 belgi bersangiz, zaif regex ham kamroq zarar qiladi.
  3. Chiziqli dvigatel. re2 npm paketi — Google'ning RE2 kutubxonasi, orqaga qaytishsiz. Kanonda sinalmagan.
  4. V8 bayrog'i. Node'ning V8 dvigatelida --enable-experimental-regexp-engine-on-excessive-backtracks bayrog'i bor: orqaga qaytish juda ko'payganda regex chiziqli dvigatelga o'tadi. Biz Node 24.21.0 da sinadik — o'sha /^(a+)+$/:
text
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 ms

Natija (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:

text
  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 ms

p99 — 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 GET ulanish 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:

  1. Bir vaqtda nechta — kanon: 4 ta (thread pool hajmi; ko'prog'i baribir pool navbatida turardi).
  2. Navbat qancha — kanon: 64 ta.
  3. Navbat to'lsa — darhol 503 Service Unavailable va Retry-After: 1 sarlavhasi: "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:

js
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:

text
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: 401

Qatorma-qator:

  • 1 va 2 — darhol boshlandi, #band 2 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 finally navbatdagi 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:

text
  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
Login toshqini paytida oddiy GET p99
  • 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:

  1. p99 4.5 baravar yaxshilandi (3 283 → 733 ms). Toshqin ham ikki baravar tez tugadi (4.25 → 2.10 s).
  2. 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.
  3. 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?

  1. Tanada 5 MB JSON.
  2. GET /v1/vazifalar?limit=999999.
  3. 10 000 xil IP'dan har biri bitta login.
  4. Bitta IP'dan minutiga 300 ta login.
  5. Server sarlavhani 15 soniya kutdi.
Yechim
  1. Tana chegarasi — 16 KB, 413.
  2. Zod sahifa limiti — 400.
  3. Argon2 semafori — navbat to'lsa 503 (har IP tezlik chegarasi ichida).
  4. Tezlik cheklovi — 429.
  5. 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
js
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:

text
p50: 14
p99: 2900
o'rtacha: 302

p50 "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
js
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:

text
1 bajarildi
3 503 (kutish tugadi)
2 bajarildi

1 — 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):

ts
// 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:

diff
+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:

ts
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):

bash
node --test test/semafor.test.ts
text
✔ 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.3378

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

bash
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-After ni 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-api chegaralari: 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
DoS va resurs tugashidan himoya: chegaralar, ReDoS va login toshqiniga semafor — IlmHamroh