IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya30/34-dars25 daqiqa
Mundarija (34)

Backend unumdorligini oshirish: o'lchov, profil, gipoteza va qayta o'lchov

Qisqacha: Unumdorlik ishi bitta sikl: o'lcha → eng sekin joyni top → faqat o'shani tuzat → qayta o'lcha. Kanon shu siklni vazifalar-api da halol o'tdi va uch saboq oldi. Birinchi yuklama testi API'ni emas, tezlik cheklovini o'lchagan ekan — hamma javob 429. Qisqa yugurishdagi CPU profili asosan ishga tushishni ko'rsatdi. "Issiq qator" gipotezasi mikro o'lchovda tasdiqlandi — Redis cheklagichi 2.4 baravar tez. Lekin butun tizimda farq shovqin ichida qoldi: cheklagich tor joy emas edi. Bu — Amdahl qonuni amalda.

Bu darsda

  • Unumdorlik siklini va nega "o'lchovsiz tuzatish" xavfli ekanini tushuntirasiz.
  • p50, p95, p99 ni hisoblaysiz va o'rtacha nega aldashini ko'rasiz.
  • Yuklama testi aslida nimani o'lchaganini tekshira olasiz.
  • Little va Amdahl qonunlari bilan natijani oldindan baholaysiz.
  • vazifalar-api ga Redis tezlik cheklagichini qo'shib, loyihaning shu bosqichini commit qilasiz.

Oldin bilishingiz kerak: CPU profiling va flamegraph, Sekin so'rovlarni topish, Rate limiting, Observability: metrikalar, trace va xatolarni kuzatish, N+1 muammosi.

O'tgan darslarda ilova to'g'ri to'xtaydigan, o'z sog'lig'ini aytadigan, ichini ko'rsatadigan va tashqi xatolarga chidaydigan bo'ldi. Endi oxirgi savol: u qanchalik tez, va uni qanday tezlashtiramiz — bir haftani behuda o'tkazmasdan?

1. Nega bu kerak?

Dasturchi Sardor "API sekin" degan shikoyatni eshitdi. U kodga qaradi va o'yladi: "Har so'rovda tezlik cheklagichi bazaga yozadi — aniq shu sekinlashtiryapti". Ikki kun ishlab, cheklagichni Redis'ga ko'chirdi. Natija? Foydalanuvchilar farqni sezmadi.

Bu — eng ko'p uchraydigan unumdorlik xatosi: gipotezani o'lchovsiz tuzatish. Sardorning gipotezasi noto'g'ri emas edi — Redis haqiqatan tezroq. Lekin cheklagich so'rov vaqtining kichik bir qismi edi. Kichik qismni ikki baravar tezlashtirish butunni deyarli o'zgartirmaydi.

Bu darsda aynan shu hikoyani kanon o'lchovlari bilan qadamma-qadam ko'ramiz — va kanonning o'zi ham Sardor kabi xulosaga keldi.

O'xshatish: avtobus yo'nalishi. Yo'l 40 daqiqa, shundan 30 daqiqasi bitta chorrahadagi tirbandlik. Avtobusga kuchliroq dvigatel qo'yish (ochiq yo'lda 10 % tezroq) deyarli hech narsa bermaydi. Avval qayerda turib qolganingizni o'lchang.

2. Unumdorlik sikli

flowchart LR
  A["1. O'lcha"] --> B["2. Profil"]
  B --> C["3. Gipoteza"]
  C --> D["4. Mikro o'lchov"]
  D --> E["5. Tuzat"]
  E --> F["6. Tizimda qayta o'lcha"]
  F --> A

Diagrammadagi eng ko'p tashlab ketiladigan qadam — oxirgisi. Mikro o'lchovda "2.4× tez" chiqdi — xursand bo'lib, ish tugadi deyish oson. Lekin foydalanuvchi mikro o'lchovni emas, butun so'rovni his qiladi.

3. O'lchash

3.1 O'rtacha aldaydi

Javob vaqtini o'rtacha bilan aytish — eng katta xato. Uning o'rniga persentil (percentile) ishlatiladi. Persentil — barcha javob vaqtlarini o'sish tartibida tizganda, p foizi undan tez yoki teng bo'lgan chegara. Masalan, p95 = 250 ms: 100 so'rovdan 95 tasi 250 ms dan tez yoki teng. Xuddi maktab navbatidagi bo'y kabi: "sinfning 95 foizi 170 sm dan past" — eng baland bir-ikki o'quvchini ham ko'rsatadi.

20 ta so'rovni ko'raylik: ko'pi 12–16 ms, ikkitasi sekin:

js
const kechikishlar = [12, 15, 14, 13, 16, 15, 14, 13, 250, 15,
  14, 16, 13, 15, 14, 12, 15, 14, 13, 900];

function persentil(qiymatlar, p) {
  const t = qiymatlar.toSorted((a, b) => a - b);
  // "eng yaqin o'rin" usuli
  const orin = Math.ceil((p / 100) * t.length) - 1;
  return t[Math.max(0, orin)];
}

const ortacha = kechikishlar.reduce((s, x) => s + x, 0)
  / kechikishlar.length;
console.log(`o'rtacha: ${ortacha} ms`);
for (const p of [50, 90, 95, 99]) {
  console.log(`p${p}: ${persentil(kechikishlar, p)} ms`);
}

Konsolda:

text
o'rtacha: 70.15 ms
p50: 14 ms
p90: 16 ms
p95: 250 ms
p99: 900 ms

O'rtacha — 70 ms. Lekin birorta so'rov 70 ms bo'lmagan: 18 tasi 16 ms dan tez, ikkitasi juda sekin. O'rtacha ikkala guruhni ham noto'g'ri tasvirlaydi. Persentil esa aniq: p50 (mediana) — tipik foydalanuvchi 14 ms; p95 va p99 — eng omadsiz foydalanuvchilar. Bir sahifa 20 ta API so'rovi qilsa, deyarli har sahifa ochilishida p95 dagi sekin so'rovlardan biri uchraydi — shuning uchun "dum" (tail latency) muhim. toSorted — nusxa bilan o'zgartirish darsidan: asl massiv buzilmaydi.

3.2 Kanon yuklama testi: nimani o'lchadik?

autocannon — Node'da yozilgan yuklama vositasi: u N ta ulanish ochadi va berilgan vaqt davomida serverga to'xtovsiz so'rov yuboradi. Natijada o'tkazuvchanlik (throughput — soniyasiga bajarilgan so'rovlar, req/s) va kechikish persentillari chiqadi. Vositaning o'zini Yuklama va stress testlari darsida batafsil ko'ramiz.

Kanon jonli serverni (Postgres, haqiqiy rollar, Aziz sessiyasi bilan) autocannon bilan yukladi: GET /v1/vazifalar?limit=20, 20 ulanish, 15 soniya. Har qatorda avval kodlar ustuniga qarang — keyin req/s ga. Kanon chiqishi (sana va ichki yorliqlar qisqartirilgan):

text
asos      | LOG_LEVEL=info | 838 req/s, p50 22 ms, p99 38 ms, kodlar 429×12569, xatolar 0
log warn  | LOG_LEVEL=warn | 786 req/s, p50 22 ms, p99 82 ms, kodlar 429×11797, xatolar 0

kodlar 429×12569 — hamma javob 429 (Too Many Requests). Birorta ham 200 yo'q! Sabab: 28-qismdagi API cheklovi — bitta foydalanuvchiga daqiqasiga 600 so'rov (Rate limiting). Yuklama testi bitta foydalanuvchi bilan soniyasiga ~800 so'rov yubordi — birinchi soniyadayoq limit tugadi. Test API'ning tezligini emas, 429 javobining tezligini o'lchagan.

Saboq: yuklama testining natijasini o'qishdan oldin status kodlarini tekshiring. Uchta to'g'ri yo'l bor: test muhitida limitni oshirish, ko'p foydalanuvchi bilan yuklash yoki atayin 429 yo'lini o'lchash (bu ham foydali — hujum paytida server 429 ni qanchalik tez qaytaradi). Bularni Yuklama va stress testlari darsida qo'llaymiz.

Ikkinchi qatorga ham qarang: LOG_LEVEL=warn (kam log) — sekinroq chiqdi (786 < 838), p99 esa ikki baravar. Mantiqan kam log tezroq bo'lishi kerak edi. Bu — shovqin: bitta mashina, boshqa jarayonlar bilan band, har rejim bir marta. Bunday farqqa xulosa qurilmaydi.

3.3 Little qonuni bilan tekshirish

Raqamlar o'zaro mosmi? Little qonuni: tizimdagi so'rovlar soni = o'tkazuvchanlik × bitta so'rov vaqti (L = λ × W). autocannon 20 ulanishni doim band ushlaydi (L = 20). Redis cheklagichli yugurishda λ = 1 246 req/s. Demak:

js
const L = 20; // bir vaqtda ochiq so'rovlar (ulanishlar)
const lam = 1246; // soniyasiga so'rovlar
console.log(`W = ${(L / lam * 1000).toFixed(1)} ms`);

Konsolda:

text
W = 16.1 ms

autocannon o'sha yugurishda p50 = 15 ms ko'rsatgan — formula bilan mos. Bu oddiy tekshiruv o'lchov to'g'ri yig'ilganini tasdiqlaydi: agar W kutilgandan juda farq qilsa, o'lchovda xato bor (masalan, mijozning o'zi tor joy).

4. Profil

4.1 Kanon profili

Tor joyni taxmin qilmaslik uchun — CPU profil (CPU profiling va flamegraph): node --cpu-prof jarayon vaqtini funksiyalar bo'yicha yozib oladi. Kanon server'ni profil bilan yukladi va natijani paketlar bo'yicha yig'di. Self time — funksiyaning o'zida o'tgan vaqt (u chaqirgan boshqa funksiyalarsiz). Ro'yxatning pastki qismidagi funksiya nomlariga qarang — ular nima ish qilyapti? Kanon chiqishi (qisqartirilgan):

text
jami namuna vaqti: 14.0 s
paketlar bo'yicha (self time):
   52.5 %  node ichki
    9.7 %  (idle)
    3.8 %  ilova (dist)
    3.3 %  @nestjs/core
    2.8 %  pg
    2.2 %  pino
funksiyalar (self time, top 12):
    9.7 %  (idle) :0
    6.6 %  writev :0
    4.6 %  internalModuleStat :0
    3.7 %  wrapSafe node:internal/modules/cjs/loader:1819
    2.4 %  compileSourceTextModule node:internal/modules/esm/utils:317
    2.3 %  readFileUtf8 :0

4.2 Natijani halol o'qish

Funksiyalar ro'yxatiga qarang: internalModuleStat, wrapSafe, compileSourceTextModule, readFileUtf8 — bular modul yuklash (fayllarni topish, o'qish, kompilyatsiya). Ya'ni profilning katta qismi so'rovlarga emas, ilovaning ishga tushishiga ketgan. 14 soniyalik yozuvning ~3 soniyasi start (Health check endpoint'lari darsidagi o'lchov: odatda ~2.6 s, band mashinada ko'proq), isitish 2 soniya, qolgani yuklama — va profil o'rtachasi hammasini aralashtirib yubordi.

Kanon yozuvi ham buni tasdiqlaydi: profil skriptida jarayon taymer bilan process.exit qiladi (yuklama vaqti + 6 s). Kanonning bitta profil yugurishi esa buzildi — 129 req/s, p50 117 ms, xatolar 1541. Bizning o'qishimiz: o'sha yugurishda chiqish taymeri yuklama tugamasdan ishlagan yoki profil yozish serverni haddan tashqari sekinlashtirgan; aniq sababi kanonda yozilmagan. Bu raqam xulosalarga kiritilmaydi.

To'g'ri usul: avval serverni isitish, keyin profilni faqat yuklama oynasida yoqish. Node buni node --cpu-prof bilan emas, inspector moduli (Profiler.start/stop) yoki Chrome DevTools orqali qila oladi (CPU profiling darsida ko'rgansiz). Profil bergan yagona foydali signal: pg 2.8 % — baza drayveri sezilarli joyda emas.

Tekshirib ko'ring: Profilda writev 6.6 %. Bu nima bo'lishi mumkin?

Javob

writev — bir nechta bufer'ni bitta tizim chaqiruvi bilan yozish. Serverda bu ikki joydan keladi: HTTP javoblarni soketga yozish va pino loglarini stdout'ga yozish. Har so'rovda log qatori bor (LOG_LEVEL=info) — shuning uchun log hajmini kamaytirish gipotezasi tug'iladi. Lekin «Kanon yuklama testi» bo'limidagi o'lchovda warn tezroq chiqmadi. Gipoteza — tekshirish uchun, xulosa emas.

5. Gipoteza: issiq qator

5.1 Nima bu

Profil aniq javob bermadi. Kanon gipoteza qurdi: tezlik cheklagichi (Rate limiting, 28-qism) har so'rovda Postgres'ga bitta INSERT … ON CONFLICT DO UPDATE qiladi. Bitta foydalanuvchining hamma so'rovlari bitta qatorni yangilaydi. Parallel so'rovlar shu qator qulfi uchun navbatda turadi — bu issiq qator (hot row).

Har yangilash Postgres'da yangi qator versiyasini yaratadi (MVCC darsidagi g'oya). O'z PG 17 bazamizda ko'ramiz. Avval jadval — kanondagidek UNLOGGED:

sql
CREATE UNLOGGED TABLE tezlik_chegaralari (
  kalit  text        NOT NULL PRIMARY KEY,
  son    int         NOT NULL,
  tugash timestamptz NOT NULL
);
SELECT relname, relpersistence
FROM pg_class WHERE relname = 'tezlik_chegaralari';
text
CREATE TABLE
      relname       | relpersistence
--------------------+----------------
 tezlik_chegaralari | u
(1 row)

relpersistence = u — unlogged: jadval WAL'ga yozilmaydi (tez, lekin server qulasa — bo'shaydi; hisoblagich uchun maqbul). Endi kanon cheklagichining so'rovi, ctid (qatorning jismoniy manzili) bilan:

sql
PREPARE ol(text) AS
INSERT INTO tezlik_chegaralari AS t (kalit, son, tugash)
VALUES ($1, 1, now() + interval '60 seconds')
ON CONFLICT (kalit) DO UPDATE SET
  son = CASE WHEN t.tugash <= now() THEN 1 ELSE t.son + 1 END,
  tugash = CASE WHEN t.tugash <= now()
    THEN now() + interval '60 seconds' ELSE t.tugash END
RETURNING son, ctid;
EXECUTE ol('api:42');
EXECUTE ol('api:42');
EXECUTE ol('api:42');
EXECUTE ol('api:7');
text
PREPARE
 son | ctid
-----+-------
   1 | (0,1)
(1 row)

INSERT 0 1
 son | ctid
-----+-------
   2 | (0,2)
(1 row)

INSERT 0 1
 son | ctid
-----+-------
   3 | (0,3)
(1 row)

INSERT 0 1
 son | ctid
-----+-------
   1 | (0,4)
(1 row)

INSERT 0 1

Biz ishga tushirdik (PostgreSQL 17.6). Bitta foydalanuvchi (api:42) uch so'rov qildi — jadvalda uchta jismoniy qator paydo bo'ldi: (0,1), (0,2), (0,3). Ikkitasi endi "o'lik" (dead tuple) — ularni keyinchalik autovacuum tozalaydi (VACUUM va autovacuum). Har API so'rovi = bitta baza yozuvi + bitta o'lik qator.

Kanon kodidagi izohda "WAL yoziladi" deyilgan — bu jadval uchun aniq emas: UNLOGGED jadval WAL'ga yozmaydi, buni yuqoridagi u ko'rsatdi. Qator qulfi va o'lik qatorlar esa haqiqatan bor.

5.2 Mikro o'lchov

Gipotezani alohida tekshirish uchun kanon uchta cheklagichni bir xil sharoitda o'lchadi: 5 000 chaqiruv, 20 parallel. Mikro o'lchov — butun tizimni emas, faqat bitta qismni (bu yerda cheklagichni) yolg'iz o'lchash. "Bitta kalit" (hamma chaqiruv bitta foydalanuvchidan) va "5000 kalit" (har chaqiruv boshqa foydalanuvchidan) qatorlarini juft-juft solishtiring. Kanon chiqishi:

text
Postgres   bitta kalit |   4594 ops/s | p50 4.06 ms, p99 9.03 ms
Redis      bitta kalit |  11147 ops/s | p50 1.52 ms, p99 5.06 ms
xotira     bitta kalit | 1244307 ops/s | p50 0.01 ms, p99 0.07 ms
Postgres   5000 kalit  |   5062 ops/s | p50 3.72 ms, p99 8.65 ms
Redis      5000 kalit  |  12260 ops/s | p50 1.48 ms, p99 2.84 ms
xotira     5000 kalit  | 1205023 ops/s | p50 0.01 ms, p99 0.09 ms
tezlik_chegaralari: 5000 qator, 744 kB (har chaqiruv — UPDATE: dead tuple, autovacuum)
Tezlik cheklagichi: soniyasiga chaqiruvlar (bitta kalit)
  • Postgres (UPSERT)4 594 ops/s
  • Redis (MULTI)11 147 ops/s

Manba: IlmHamroh kanon o'lchovi, 2026-11-03: kanon cheklov.ts, 5 000 chaqiruv, 20 parallel, PostgreSQL 18.6 va Redis 8 — Docker'da, bitta mashina; xotira varianti 1.2 mln ops/s (grafikka sig'maydi); bitta yugurish

Uch xulosa:

  1. Redis 2.4 baravar tez — gipotezaning bir qismi tasdiqlandi.
  2. Lekin Postgres'da bitta kalit (4 594) va 5 000 kalit (5 062) deyarli teng. Issiq qatorning qulf raqobati kichik ekan — sekinlik qatordan emas, har chaqiruvdagi tarmoq aylanishidan (ilova → baza → ilova).
  3. Xotira — millionlab: tarmoq yo'q. Lekin u bitta jarayonda ishlaydi — ikki nusxa bo'lsa, har biri o'z hisobini yuritadi va limit ikki baravar "kengayadi".

6. Butun tizimda: Amdahl qonuni

6.1 Kanon natijasi

Kanon Redis cheklagichini ulab, butun tizimni qayta o'lchadi (kanon chiqishi, sana va ichki yorliqlar qisqartirilgan):

text
Pg cheklagich          | 1169 req/s, p50 16 ms, p99 30 ms, kodlar 429×11687, xatolar 0
Redis cheklagich       | 1246 req/s, p50 15 ms, p99 29 ms, kodlar 429×12457, xatolar 0
Pg cheklagich (takror) | 1239 req/s, p50 15 ms, p99 27 ms, kodlar 429×12385, xatolar 0

Postgres 1 169 va 1 239, Redis 1 246. Takroriy Postgres yugurishi Redis bilan deyarli teng — farq shovqin ichida. Mikro o'lchovdagi 2.4× tizimda ko'rinmadi.

6.2 Nega: Amdahl qonuni

Amdahl qonuni: agar tizim vaqtining f ulushini s baravar tezlashtirsangiz, umumiy tezlashuv = 1 / ((1 − f) + f / s). Cheklagich so'rov vaqtining qancha qismi bo'lishi mumkinligini sinab ko'ramiz:

js
const tezlashuv = (f, s) => 1 / ((1 - f) + f / s);

for (const f of [0.05, 0.2, 0.5]) {
  const k = tezlashuv(f, 2.4).toFixed(2);
  console.log(`ulush ${f * 100} % → ${k}×`);
}

Konsolda:

text
ulush 5 % → 1.03×
ulush 20 % → 1.13×
ulush 50 % → 1.41×

Cheklagich so'rov vaqtining 5 % i bo'lsa, uni 2.4 baravar tezlashtirish butunni 3 % ga tezlashtiradi — bitta mashinadagi shovqindan kichik. Taqqoslang: kanonning ikki Postgres yugurishi bir-biridan 6 % farq qildi (1 169 va 1 239). 3 % yaxshilanishni bunday o'lchovda ko'rib bo'lmaydi.

Bu qonunning amaliy ma'nosi: avval ulushni o'lchang. Kichik ulushni qancha tezlashtirsangiz ham, umumiy foyda kichik. 3-mashqda uchta variant orasidan eng foydalisini Amdahl bilan tanlaysiz.

6.3 Unda nega Redis cheklagichi qoldi?

Kanon Redis cheklagichini baribir loyihaga qo'shdi — lekin boshqa sabab bilan, va buni halol yozdi:

  • har so'rovda baza yozuvi yo'q — bazaning kuchi haqiqiy ma'lumotga qoladi;
  • o'lik qatorlar va autovacuum ishi yo'q (5 000 kalit — 744 kB jadval);
  • Redis allaqachon bor (kesh, navbat) — yangi bog'liqlik emas;
  • bir nechta nusxada hisob umumiy (xotira cheklagichidan farqli).

Ya'ni qaror "tezlik uchun" emas, "resurs va soddalik uchun". REDIS_URL bo'lmasa — Postgres cheklagichi ishlayveradi.

Tekshirib ko'ring: Kanon Redis cheklagichi tizimni tezlashtirmaganini bilgan holda uni qo'shdi. Bu "o'lchovsiz tuzatish" xatosimi?

Javob

Yo'q. Xato — tuzatishni tezlik uchun qilib, natijani o'lchamaslik. Kanon esa o'lchadi, tezlik foydasi yo'qligini tan oldi va qarorni boshqa, tekshirilgan sabablarga (baza yuki, o'lik qatorlar) asosladi. Sardor ham shunday qilganda, xatosi bo'lmasdi — u faqat "tezlashadi" deb o'ylagan va tekshirmagan.

7. Boshqa tezlashtirish vositalari

Kanonda o'lchanmagan, lekin ish joyida doim uchraydigan vositalar. Hammasiga bir xil qoida: avval profil, keyin tuzatish.

Vosita Qachon foyda Izoh
N+1 ni bitta so'rovga ro'yxat har element uchun so'rov qilsa N+1 muammosi; ko'pincha eng katta yutuq
Indeks va EXPLAIN sekin SQL Sekin so'rovlarni topish
Kesh qatlamlari bir xil ma'lumot ko'p o'qilsa HTTP kesh (ETag), Redis (kesh naqshlari), jarayon xotirasi
Pool hajmi so'rovlar ulanish kutsa Connection pooling; katta pool — har doim ham yaxshi emas
JSON serializatsiya katta javoblar, CPU'da stringify fast-json-stringify — sxema bilan 2–5× (hujjat bo'yicha, sinalmagan, 2026-11)
Siqish (gzip, brotli) katta javob, sekin tarmoq odatda reverse proxy'da (zlib); kanonda ilovada yo'q

Siqish haqida alohida: kanonda compression middleware yo'q — 25-qismda qaror qilingan va 30-qismda saqlangan: siqishni nginx bajaradi (CPU ishini Node event loop'idan olib tashlash). JSON javoblarimiz kichik (ro'yxat — 20 ta vazifa), siqish foydasi ham kichik.

8. Ko'p uchraydigan xatolar

Xato Oqibat To'g'risi
O'rtacha vaqtga qarash sekin "dum" ko'rinmaydi p50, p95, p99
Status kodlarini tekshirmaslik 429 yoki 500 ni o'lchaysiz avval kodlar, keyin req/s
Profilni startdan boshlash modul yuklash profilni to'ldiradi isitib, keyin profil
Faqat mikro o'lchov tizimda farq yo'q tizimda qayta o'lchash
Kichik ulushni optimallashtirish Amdahl: deyarli foydasiz avval ulushni o'lchash
Bir marta o'lchab xulosa shovqinni natija deb o'qish bir necha yugurish, nisbat
Bitta foydalanuvchi bilan yuklama limit o'lchanadi ko'p foydalanuvchi yoki test limiti

9. Mashqlar

Mashqlar kurs/mashqlar/31/30-unumdorlik/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

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

  • 100 so'rovning eng sekin 1 tasidan tashqari hammasi tezroq bo'lgan chegara — p
  • Kanon yuklama testida hamma javoblarning status kodi —
  • L = λ × W formulasi — qonuni
  • Kanon mikro o'lchovida Redis cheklagichi Postgres'dan necha baravar tez —
  • Kichik ulushni tezlashtirish umumiy foyda bermasligini tushuntiradigan qonun —
Yechim
  • 99 — p99: 100 tadan 99 tasi shu qiymatdan tez yoki teng.
  • 429 — bitta foydalanuvchining 600/daqiqa limiti tugagan.
  • Little — tizimdagi so'rovlar = o'tkazuvchanlik × so'rov vaqti.
  • 2.4 — 4 594 ga qarshi 11 147 ops/s (bitta kalit).
  • Amdahl — 1 / ((1 − f) + f / s).

2-mashq (o'rta): autocannon natijasini tekshirish

tekshir(natija) funksiyasini yozing. Kirish — autocannon'ga o'xshash obyekt: { reqS, ulanishlar, p50, kodlar } (kodlar — { "200": 9000, "429": 1000 }). Funksiya ogohlantirishlar ro'yxatini qaytarsin. Ikki qoida: (1) 2xx ulushi 95 % dan kam bo'lsa — "2xx atigi N %"; (2) Little bo'yicha kutilgan W (ulanishlar / reqS, ms) p50 dan 2 baravardan ko'p farq qilsa — "Little mos emas". Kanonning ikki natijasini tekshiring. Ishora: Object.entries va filter.

Yechim
js
function tekshir({ reqS, ulanishlar, p50, kodlar }) {
  const ogoh = [];
  const jami = Object.values(kodlar).reduce((s, n) => s + n, 0);
  const ok = Object.entries(kodlar)
    .filter(([kod]) => kod.startsWith("2"))
    .reduce((s, [, n]) => s + n, 0);
  const ulush = (ok / jami) * 100;
  if (ulush < 95) ogoh.push(`2xx atigi ${ulush.toFixed(1)} %`);
  const w = (ulanishlar / reqS) * 1000;
  const nisbat = Math.max(w, p50) / Math.min(w, p50);
  if (nisbat > 2) {
    ogoh.push(`Little mos emas: W ${w.toFixed(1)} ms, p50 ${p50} ms`);
  }
  return ogoh;
}

const kanon = { reqS: 1246, ulanishlar: 20, p50: 15,
  kodlar: { 429: 12457 } };
const yaxshi = { reqS: 2224, ulanishlar: 20, p50: 8,
  kodlar: { 200: 33000, 404: 360 } };
const shubhali = { reqS: 900, ulanishlar: 50, p50: 5,
  kodlar: { 200: 9000 } };
console.log(tekshir(kanon));
console.log(tekshir(yaxshi));
console.log(tekshir(shubhali));

Konsolda:

text
[ '2xx atigi 0.0 %' ]
[]
[ 'Little mos emas: W 55.6 ms, p50 5 ms' ]

Kanon natijasi birinchi tekshiruvdan o'tmadi — testni qayta qurish kerak. Uchinchi natijada 50 ulanish bilan 900 req/s bo'lsa, har so'rov ~56 ms bo'lishi kerak edi, autocannon esa 5 ms deydi. Bunday nomuvofiqlik odatda mijoz tomonini (autocannon'ning o'zi CPU'ni to'ldirgan) yoki xato ulanishlarni bildiradi. yaxshi dagi 404 lar — 1 % dan kam, ruxsat etilgan. yaxshi va shubhali — namuna uchun o'ylab topilgan natijalar; faqat kanon — haqiqiy o'lchov.

3-mashq (qiyin): qaysi joyni tuzatish kerak

Profil uch nomzodni ko'rsatdi: JSON serializatsiya (so'rov vaqtining 8 %, 2× tezlashtirish mumkin), cheklagich Postgres → Redis (5 %, 2.4×) va N+1 ni bitta JOIN ga (45 %, 5×). Amdahl qonuni bilan har birining umumiy tezlashuvini hisoblang va eng foydalisidan boshlab tartiblang. Ishora: map va toSorted (nusxa bilan o'zgartirish).

Yechim
js
const tezlashuv = (f, s) => 1 / ((1 - f) + f / s);

const variantlar = [
  { nom: "JSON serializatsiya", ulush: 0.08, tez: 2 },
  { nom: "cheklagich: Pg → Redis", ulush: 0.05, tez: 2.4 },
  { nom: "N+1 → bitta JOIN", ulush: 0.45, tez: 5 },
];
const tartib = variantlar
  .map((v) => ({ nom: v.nom, umumiy: tezlashuv(v.ulush, v.tez) }))
  .toSorted((a, b) => b.umumiy - a.umumiy);
for (const v of tartib) {
  console.log(`${v.nom}: ${v.umumiy.toFixed(2)}×`);
}

Konsolda:

text
N+1 → bitta JOIN: 1.56×
JSON serializatsiya: 1.04×
cheklagich: Pg → Redis: 1.03×

N+1 tuzatishi qolgan ikkitasidan o'n baravardan ko'p foyda beradi — chunki u vaqtning katta ulushida. Cheklagich esa oxirgi o'rinda: «Butun tizimda» bo'limidagi kanon natijasi bilan mos. Real ishda ulushlar taxmin emas, profil va trace'dan olinadi.

10. Loyiha qadami: Redis cheklagichi va bosqich commit'i

10.1 RedisCheklagich

src/umumiy/cheklov.ts — PgCheklagich klassidan keyin. Redis'ning INCR (sonni 1 ga oshirish) va PEXPIRE … NX (muddat — faqat birinchi marta) bitta MULTI ichida — atomik, bitta tarmoq aylanishi:

ts
/** Redis klienti (node-redis) — faqat kerakli metodlar */
type RedisMulti = {
  incr(k: string): RedisMulti;
  pExpire(k: string, ms: number, rejim?: "NX"): RedisMulti;
  pTTL(k: string): RedisMulti;
  exec(): Promise<unknown[]>;
};
export type CheklovRedis = {
  multi(): RedisMulti;
  del(k: string): Promise<unknown>;
};

/**
 * 31 #30: Postgres cheklagichi har so'rovda bitta UPSERT — bitta
 * foydalanuvchining parallel so'rovlari BITTA qatorni yangilaydi
 * ("issiq qator"). REDIS_URL bo'lsa — Redis: INCR + PEXPIRE NX bitta
 * MULTI'da (atomik, xotirada). Oyna mantig'i Pg bilan bir xil.
 */
export class RedisCheklagich implements Cheklagich {
  constructor(
    private readonly r: CheklovRedis,
    private readonly prefiks = "vz:cheklov:",
  ) {}

  async ol(kalit: string, limit: number, oynaMs: number)
    : Promise<Natija> {
    const k = this.prefiks + kalit.slice(0, 300);
    const javob = await this.r.multi()
      .incr(k).pExpire(k, oynaMs, "NX").pTTL(k).exec();
    const [son, , ttl] = javob as [number, unknown, number];
    const hozir = Date.now();
    return natija(son, limit, hozir + Math.max(0, ttl), hozir);
  }

  async tozala(kalit: string): Promise<void> {
    await this.r.del(this.prefiks + kalit.slice(0, 300));
  }
}

Uch nozik joy:

  • NX — muddat faqat kalitda muddat yo'q bo'lsa qo'yiladi. Busiz har so'rov oynani qaytadan 60 soniyaga cho'zardi va faol foydalanuvchi hech qachon tiklanmasdi.
  • CheklovRedis turi — butun node-redis klienti emas, faqat 4 metod. Test uchun soxta obyekt yasash oson, va klient o'zgarsa, faqat shu tur tekshiriladi.
  • Oyna — qat'iy (fixed window), Postgres variantidagi kabi. Ikki cheklagich bir xil shartnomani bajaradi — Cheklagich interfeysi.

Kanonda turlar va konstruktor bir qatorda edi — telefon uchun bo'ldik, mantiq aynan.

10.2 Ulash

src/auth/auth-yarat.ts — import va oxirgi parametr:

ts
import { PgCheklagich, RedisCheklagich } from "../umumiy/cheklov.ts";
import type { CheklovRedis } from "../umumiy/cheklov.ts";

export function authYarat(
  pool: pg.Pool, config: Config, sirlar: Sirlar, log: Log,
  pochta: Pochta | null = null, redis?: CheklovRedis,
): AuthBogliqliklari {
  // …
    // 31 #30: REDIS_URL bo'lsa — Redis cheklagichi (issiq qator
    // yo'q); aks holda Postgres (28-qism)
    cheklagich: redis === undefined
      ? new PgCheklagich(pool)
      : new RedisCheklagich(redis),

src/auth/auth.module.ts — fabrika saqlash qatlamidagi Redis klientini uzatadi (u kesh uchun allaqachon bor):

ts
          return authYarat(pool, c, sirlar, log, pochta, s.redis);

10.3 Test

test/cheklov-redis.test.ts — Postgres cheklagichi testlari bilan bir xil shartnoma. Redis kerak: TEST_REDIS_URL bo'lmasa — o'tkaziladi. Kanon faylidagi uzun qatorlarni telefon uchun bo'ldik, tekshiruvlar aynan:

ts
// cheklov-redis.test.ts — 31-qism #30: RedisCheklagich — Pg
// cheklagichi bilan bir xil shartnoma (qat'iy oyna, limit,
// tiklanish, tozalash). TEST_REDIS_URL kerak.
import { afterAll, test } from "vitest";
import assert from "node:assert/strict";
import { createClient } from "redis";
import { RedisCheklagich } from "../src/umumiy/cheklov.ts";

const URL = process.env["TEST_REDIS_URL"];
if (URL === undefined) {
  test.skip("RedisCheklagich — TEST_REDIS_URL yo'q", () => {});
} else {
  const r = createClient({ url: URL });
  await r.connect();
  afterAll(() => r.quit());
  test("limit 3: 4-chi rad; tiklanish 1..60 s; oyna tugagach yana "
    + "1 dan; tozala() — darhol", async () => {
    const prefiks = `vz:test:cheklov:${String(process.pid)}:`;
    const c = new RedisCheklagich(r, prefiks);
    await c.tozala("k");
    const n = [];
    for (let i = 0; i < 4; i++) n.push(await c.ol("k", 3, 60_000));
    assert.deepEqual(n.map((x) => [x.son, x.ruxsat, x.qoldi]), [
      [1, true, 2], [2, true, 1], [3, true, 0], [4, false, 0],
    ]);
    const tiklanish = n[3]?.tiklanishS ?? 0;
    assert.ok(tiklanish >= 1 && tiklanish <= 60);
    await c.tozala("k");
    assert.equal((await c.ol("k", 3, 60_000)).son, 1);
    const q = await c.ol("q", 1, 150);
    await new Promise((ok) => setTimeout(ok, 250));
    // oyna tugadi — PEXPIRE NX bilan kalit o'chdi
    assert.equal((await c.ol("q", 1, 150)).son, 1);
    assert.equal(q.son, 1);
    await c.tozala("k");
  });
}

Limit 3 da to'rtinchi so'rov rad (ruxsat: false), tiklanish 1–60 s orasida, tozala() hisobni darhol nolga qaytaradi. Oxirgi qism eng muhimi: 150 ms lik oyna tugagach hisob yana 1 dan boshlandi — demak PEXPIRE NX kalitni o'z vaqtida o'chirdi. Kanon testni Redis 8 bilan yurgizdi; bizda Redis yo'q, test o'tkazildi (↓ … skipped).

10.4 30-qismdan qolgan xato: Problem title

30-qismda 404 javobining title i inglizcha qolgan edi ("Not Found"), API esa o'zbekcha. RFC 9457 about:blank turida title sifatida HTTP holat iborasini beradi, lekin uni mahalliylashtirishga ruxsat beradi ("MAY be localized"). Mashina status va type ga qaraydi, title — odam uchun. src/umumiy/problem.ts:

ts
/**
 * 31 (30-qism ochiq xatosi: 404 `title` inglizcha edi). RFC 9457
 * §4.2.1: about:blank da title — HTTP holat iborasi, lekin uni
 * MAHALLIYLASHTIRISH mumkin ("MAY be localized"). Mashina `status` va
 * `type` ga qaraydi (title'ga emas).
 */
export const HOLAT_SARLAVHALARI: Readonly<Record<number, string>> = {
  400: "Noto'g'ri so'rov", 401: "Kirish talab qilinadi",
  403: "Ruxsat yo'q", 404: "Topilmadi",
  405: "Metod ruxsat etilmagan", 406: "Javob formati qo'llanmaydi",
  409: "Ziddiyat", 410: "O'chirilgan", 412: "Shart bajarilmadi",
  413: "So'rov juda katta", 415: "Tana formati qo'llanmaydi",
  422: "Ma'lumot noto'g'ri", 428: "Shart talab qilinadi",
  429: "So'rovlar juda ko'p", 500: "Ichki xato",
  502: "Tashqi xizmat xatosi", 503: "Xizmat vaqtincha mavjud emas",
  504: "Tashqi xizmat javob bermadi",
};

problemYasa ichida birinchi qator:

ts
  const umumiy = HOLAT_SARLAVHALARI[holat]
    ?? STATUS_CODES[holat] ?? "Xato";

Ro'yxatda yo'q kod — avvalgidek Node'ning inglizcha iborasi. O'zgarish haqida: 25-qismdagi Problem Details darslarida va testlarida "Bad Request", "Not Found", "Method Not Allowed" ko'rgansiz — endi ular "Noto'g'ri so'rov", "Topilmadi", "Metod ruxsat etilmagan". Shuning uchun uchta eski test kutuvi yangilanadi: test/api.test.ts da title: "Noto'g'ri so'rov", test/xato.test.ts da "Topilmadi" va "Metod ruxsat etilmagan". Frontend title ni ko'rsatsa, endi o'zbekcha matn ko'rinadi — status bo'yicha ishlaydigan kod esa o'zgarmaydi.

10.5 Versiya va hujjat

package.json — "version": "4.0.0-alfa.4". API.md ga yangi bo'lim:

text
## Kuzatuv (31-qism)
- `GET /health/live` — jarayon tirik; `GET /health/ready` — baza javob beradi va yopilish boshlanmagan (SIGTERM dan keyin 503); `GET /health/startup` — ishga tushish tugadi.
- `GET /metrics` — Prometheus (RED: `http_sorovlar_jami{metod,yol,status}`, `http_sorov_davomiyligi_soniya`; yo'l — marshrut shabloni); `METRIKA_TOKEN` bo'lsa Bearer.
- Problem Details `title` — o'zbekcha (RFC 9457 §4.2.1 mahalliylashtirishga ruxsat beradi): `Topilmadi`, `Ma'lumot noto'g'ri` …

O'zgarishlar tarixi dagi v4.0.0-alfa qatoriga: "/metrics, /health/startup, graceful drain, Problem title o'zbekcha, AI chidamliligi (retry, circuit breaker, bulkhead), Redis tezlik cheklagichi".

10.6 Bosqich tekshiruvi

Besh darsning hammasi joyida. To'liq tekshiruv:

bash
npm run check

Biz loyiha nusxasida (Node 24.21.0, bazasiz — baza va Redis talab qiladigan testlar o'tkaziladi): tsc --noEmit va eslint --max-warnings=0 xatosiz, nest build muvaffaqiyatli, testlar:

text
 Test Files  42 passed | 24 skipped (66)
      Tests  133 passed | 38 skipped (171)

Bosqich boshida (feature flag'lar darsidan keyin) xuddi shu sharoitda 128 o'tgan, 37 o'tkazilgan edi: +5 test (kuzatuv 2, chidamlilik 3) va +1 o'tkazilgan (cheklov-redis). Kanon esa to'liq muhitda tekshirdi: PostgreSQL 18.6, Redis, MinIO, Mailpit — Docker'da. Natija: tip, lint, build — 0 xato; Vitest 333 o'tdi, 1 o'tkazildi; migratsiyalar 26, drift 0; jonli API uch saqlash rejimida — status kodlari oldingi bosqich bilan aynan.

Jonli tekshiruv — biz ishga tushirdik (xotira rejimi, METRIKA_TOKEN bilan; sorovId har safar boshqa):

text
$ curl http://127.0.0.1:5595/yoq-manzil
{"type":"about:blank","title":"Topilmadi","status":404,"detail":"GET /yoq-manzil — bunday manzil yo'q","instance":"/yoq-manzil","sorovId":"3fea25af-116f-4d4f-8e03-72d5747822b4"}
$ curl -X DELETE http://127.0.0.1:5595/v1/teglar
{"type":"about:blank","title":"Metod ruxsat etilmagan","status":405,"detail":"DELETE bu manzilda yo'q","instance":"/v1/teglar","sorovId":"55a82dd3-7eed-4558-91d1-d8afd9e8d65c"}
$ curl -i http://127.0.0.1:5595/metrics
HTTP/1.1 401 Unauthorized

10.7 Commit

bash
git add -A
M="feat: graceful drain (readiness 503 + kechikish), /health/startup,"
M+=" Prometheus /metrics, AI chidamliligi (retry, circuit breaker,"
M+=" bulkhead), Redis tezlik cheklagichi; fix: buyruq CLS konteksti"
M+=" va logi stderr'ga, Problem title o'zbekcha"
git commit -m "$M"

git add -A dan oldin git status ga qarang: .env, dist/, coverage/ kirmasin. Commit besh darsning hammasini qamraydi: graceful shutdown, health, kuzatuv, chidamlilik va unumdorlik.

11. Real ishda

  • Qayerda uchraydi: "API sekin" shikoyati, bulut hisobining o'sishi, Black Friday oldidan sig'imni rejalash. Har holatda birinchi savol — "qayerda o'lchadingiz?".
  • Vositalar: autocannon, k6, Artillery (yuklama); --cpu-prof, Clinic.js, 0x, Chrome DevTools (profil); EXPLAIN ANALYZE, pg_stat_statements (baza); o'tgan darsdagi /metrics va trace'lar (prodakshnda).
  • Intervyu savollari: "p99 nima va nega o'rtacha emas?", "Unumdorlik muammosini qanday qidirasiz?", "Amdahl qonuni nima deydi?", "Yuklama testi natijasiga qachon ishonmaysiz?"
  • Jamoada: optimallashtirish PR'iga "oldin/keyin" o'lchovi, mashina va yugurishlar soni bilan qo'shiladi. O'lchovsiz "tezlashtirdim" — qabul qilinmaydi.

Xulosa

  • Sikl: o'lcha → profil → gipoteza → mikro o'lchov → tuzat → tizimda qayta o'lcha.
  • O'rtacha aldaydi — p50, p95, p99; Little qonuni o'lchovni tekshiradi.
  • Yuklama natijasidan oldin status kodlariga qarang: kanon testi 429 ni o'lchagan.
  • Qisqa profil ishga tushishni ko'rsatadi — isitib, keyin profil.
  • Amdahl: 5 % ulushni 2.4× tezlashtirish — butun tizimga ~3 %; avval ulushni o'lchang.
  • Redis cheklagichi tezlik uchun emas, baza yuki va o'lik qatorlar uchun qoldi — qaror o'lchov bilan asoslandi.

Keyingi dars: Asinxron komponentlarni testlash — kesh muddati, retry kutishi va breaker vaqtini soxta soat bilan millisekundgacha aniq sinaymiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Backend unumdorligini oshirish: o'lchov, profil, gipoteza va qayta o'lchov — IlmHamroh