Kechikish, to'plam va ishchilar
Kechki soat 19:00, restoran to'la
Restoran ishlayapti. Ofitsiant buyurtma oladi va oshxonaga olib boradi.
Bitta buyurtma — bitta yurish. Yana bitta — yana yurish.
Kunduzi bu normal: mijoz kam, ofitsiant bo'sh. Lekin kechqurun zal to'ladi va endi ofitsiant faqat yurib yuradi. Oshpaz ham bo'sh turadi — unga bir vaqtda bitta taom keladi.
Tajribali ofitsiant nima qiladi? To'planib boradi: uch-to'rt stoldan buyurtma yig'adi, keyin bir marta oshxonaga boradi.
Yurish soni to'rt barobar kamaydi. Oshpaz esa to'rt taomni birga qo'yadi — pechka bir marta qiziydi.
27 va 28-bobda servis qurdik va u bitta so'rovda 2-3 millisekundda javob berdi. Yaxshi raqam.
Endi savol: ming kishi bir vaqtda so'rasa-chi?
Ikki xil "tez"
Bu bobning butun mavzusi ikki so'zning farqida.
Kechikish (latency) — bitta so'rov qancha kutadi. Foydalanuvchi shuni sezadi.
O'tkazuvchanlik (throughput) — sekundiga nechta so'rov bajariladi. Server shuni ko'taradi.
Ular bir xil narsa emas, va ko'pincha bir-biriga qarshi turadi.
Ofitsiantni eslang: to'plab olib borsa, o'tkazuvchanlik oshadi (ko'proq stol xizmat oladi), lekin birinchi buyurtma bergan mijozning kechikishi oshadi — u endi qolganlarni kutadi.
import pandas as pd show(pd.DataFrame([ {"": "Nimani o'lchaydi", "Kechikish": "Bitta so'rov vaqti", "O'tkazuvchanlik": "Sekundiga so'rovlar"}, {"": "Kim sezadi", "Kechikish": "Foydalanuvchi", "O'tkazuvchanlik": "Server hisobi"}, {"": "Yaxshilash usuli", "Kechikish": "Tezroq model, kamroq ish", "O'tkazuvchanlik": "To'plam, ko'proq ishchi"}, {"": "To'plam ta'siri", "Kechikish": "**Yomonlashadi**", "O'tkazuvchanlik": "**Yaxshilanadi**"}, ]))
| Kechikish | O'tkazuvchanlik | |
|---|---|---|
| Nimani o'lchaydi | Bitta so'rov vaqti | Sekundiga so'rovlar |
| Kim sezadi | Foydalanuvchi | Server hisobi |
| Yaxshilash usuli | Tezroq model, kamroq ish | To'plam, ko'proq ishchi |
| To'plam ta'siri | **Yomonlashadi** | **Yaxshilanadi** |
Oxirgi qatorni yodda tuting — bobning eng muhim jumlasi shu.
Servisni ko'taramiz
28-bobdagi rasmdan foydalanamiz.
python3 -c " import sys, pathlib; sys.path.insert(0, 'src') if not pathlib.Path('model.onnx').exists(): import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from quvur.xususiyat import XUSUSIYATLAR, tayyorla from quvur.format import onnx_ga d = pd.read_csv('../datasets/ijara.csv') x, y = tayyorla(d), d['broker'] Xtr, _, ytr, _ = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) onnx_ga(m, len(XUSUSIYATLAR), 'model.onnx') print('model.onnx qayta yaratildi (28-bobdagidek)') else: print('model.onnx bor') " docker rm -f kitob-servis > /dev/null 2>&1 if ! docker images mlops-servis:v1 -q | grep -q .; then docker build -q -f Dockerfile.servis -t mlops-servis:v1 . > /dev/null 2>&1 fi docker run -d --name kitob-servis -p 18100:8000 mlops-servis:v1 > /dev/null 2>&1 for i in $(seq 1 40); do curl -s http://127.0.0.1:18100/healthz > /dev/null 2>&1 && { echo "servis tayyor (${i} urinish)"; break; } sleep 0.5 done curl -s http://127.0.0.1:18100/ready; echo ""
model.onnx qayta yaratildi (28-bobdagidek)
servis tayyor (3 urinish)
{"holat":"tayyor","sorovlar":0,"chegara":0.5}
Bitta foydalanuvchi: baza chizig'i
Avval eng yaxshi holatni o'lchaymiz — navbat yo'q, faqat bitta so'rovchi.
python3 -c " import json, time, urllib.request import numpy as np ELON = {'xonalar': 2, 'maydon': 55, 'qavat': 3, 'jami_qavat': 9, 'narx_mln': 4.5, 'tavsif_uzunligi': 300, 'rasmlar_soni': 8, 'telefon_takrorlanishi': 4, 'kunlik_elonlar': 2, 'egasi_tasdiqlangan': 0} xom = json.dumps(ELON).encode() def sorov(): t0 = time.perf_counter() r = urllib.request.Request('http://127.0.0.1:18100/bashorat', data=xom, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(r, timeout=10) as j: j.read() return (time.perf_counter() - t0) * 1000 for _ in range(20): sorov() v = np.array([sorov() for _ in range(300)]) print(f'BITTA FOYDALANUVCHI ({len(v)} sorov)') print(f' p50 : {np.percentile(v,50):6.2f} ms') print(f' p99 : {np.percentile(v,99):6.2f} ms') print(f' otkazuvchanlik: {1000/np.mean(v):6.0f} sorov/s') "
BITTA FOYDALANUVCHI (300 sorov) p50 : 2.83 ms p99 : 17.18 ms otkazuvchanlik: 254 sorov/s
Bu raqam chiroyli, lekin yolg'on. Chunki haqiqiy saytda bitta foydalanuvchi bo'lmaydi.
Ko'p foydalanuvchi: haqiqat
Endi 50 kishi bir vaqtda so'raydi.
python3 -c " import json, time, urllib.request from concurrent.futures import ThreadPoolExecutor import numpy as np ELON = {'xonalar': 2, 'maydon': 55, 'qavat': 3, 'jami_qavat': 9, 'narx_mln': 4.5, 'tavsif_uzunligi': 300, 'rasmlar_soni': 8, 'telefon_takrorlanishi': 4, 'kunlik_elonlar': 2, 'egasi_tasdiqlangan': 0} xom = json.dumps(ELON).encode() def sorov(_=None): t0 = time.perf_counter() r = urllib.request.Request('http://127.0.0.1:18100/bashorat', data=xom, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(r, timeout=30) as j: j.read() return (time.perf_counter() - t0) * 1000 print(f'{\"parallel\":>9s} {\"p50\":>9s} {\"p99\":>9s} {\"sorov/s\":>10s}') print('-' * 42) for n in (1, 10, 50): with ThreadPoolExecutor(max_workers=n) as ex: list(ex.map(sorov, range(n * 3))) # isitish t0 = time.perf_counter() v = np.array(list(ex.map(sorov, range(n * 10)))) dt = time.perf_counter() - t0 print(f'{n:>9d} {np.percentile(v,50):>8.1f}ms {np.percentile(v,99):>8.1f}ms {len(v)/dt:>10.0f}') "
parallel p50 p99 sorov/s
------------------------------------------
1 2.4ms 3.6ms 372
10 21.9ms 49.0ms 433
50 104.4ms 158.5ms 456
Jadvalni diqqat bilan o'qing — natija men kutganimdan boshqacha chiqdi.
Men "parallellik oshsa, o'tkazuvchanlik ham sezilarli oshadi" deb kutgandim. Aslida u deyarli o'zgarmadi: bir necha yuz so'rov/s da qotib qoldi.
Kechikish esa o'nlab barobar yomonlashdi.
Ya'ni server allaqachon bitta parallel so'rovda to'la band edi. Qo'shilgan yuk yangi ish bajarmadi — u faqat navbat yasadi.
Ofitsiant misolini eslang: u allaqachon to'xtamasdan yugurayapti. Zalga yana o'ttiz mijoz kirsa, u tezroq yugurmaydi — mijozlar shunchaki uzoqroq kutadi.
Mana amaliy xulosa, va u ko'p jamoada noto'g'ri tushuniladi: o'tkazuvchanlik tekislangan bo'lsa, yuk qo'shish faqat kechikishni oshiradi. Bunday holatda yechim — kod emas, ko'proq ishchi yoki ko'proq server.
"Bitta so'rov 3 ms" — bu marketing raqami
Model tezligini o'lchab "3 ms" deb hisobotga yozish keng tarqalgan.
Lekin bu raqam bo'sh serverda, bitta so'rov bilan olingan. Foydalanuvchi esa bo'sh serverga tushmaydi — u boshqa 500 kishi bilan birga keladi.
To'g'ri o'lchov har doim uch qismdan iborat:
- Nechta parallel so'rov bilan?
- p99, o'rtacha emas (27-bob)
- Qancha vaqt davomida? (birinchi soniyalar aldamchi)
Shu uchtasiz aytilgan latency raqami — hech narsa demaydi.
To'plam: ofitsiantning hiylasi
Endi to'plamni sinab ko'ramiz. Servisga ro'yxat qabul qiladigan yo'l qo'shamiz.
26-bobda buni allaqachon o'lchagandik: ONNX bitta qatorni 0.007 ms da, 2000 qatorni esa 4.30 ms da bajargan. Ya'ni bitta qatorga 0.002 ms — uch barobar arzon.
Sabab: har chaqiruvda tayyorgarlik xarajati bor va u butun to'plamga taqsimlanadi.
shu faylni yaratingsrc/xizmat/toplam.py
"""To'plamli bashorat: bitta so'rovda ko'p e'lon.""" from typing import Any import numpy as np import pandas as pd from quvur.format import onnx_ehtimol from quvur.xususiyat import XOM_USTUNLAR, tayyorla def toplam_ehtimol(sessiya, elonlar: list[dict[str, Any]]) -> list[float]: """Ro'yxatni BITTA marta modelga beradi. Diqqat: 3-bobdagi qoida — xususiyat kodi o'sha `tayyorla()`. Bu yerda yangi hisob yozilmaydi. """ d = pd.DataFrame(elonlar, columns=XOM_USTUNLAR) x = tayyorla(d) return [float(p) for p in onnx_ehtimol(sessiya, x.values)]
O'lchaymiz — bir xil 500 ta e'lon, ikki xil usulda:
python3 -c " import sys, time; sys.path.insert(0, 'src') import numpy as np from quvur.format import onnx_sessiya, onnx_ehtimol from quvur.xususiyat import XOM_USTUNLAR, tayyorla from xizmat.toplam import toplam_ehtimol import pandas as pd s = onnx_sessiya('model.onnx') d = pd.read_csv('../datasets/ijara.csv').head(500) elonlar = d[XOM_USTUNLAR].to_dict('records') # 1) bittalab t0 = time.perf_counter() for e in elonlar: x = tayyorla(pd.DataFrame([e], columns=XOM_USTUNLAR)) onnx_ehtimol(s, x.values) bitta = (time.perf_counter() - t0) * 1000 # 2) toplam bilan t0 = time.perf_counter() toplam_ehtimol(s, elonlar) toplam = (time.perf_counter() - t0) * 1000 print(f'{len(elonlar)} ta elon:') print(f' bittalab : {bitta:8.1f} ms ({bitta/len(elonlar):.3f} ms/elon)') print(f' toplam : {toplam:8.1f} ms ({toplam/len(elonlar):.3f} ms/elon)') print(f' toplam {bitta/toplam:.0f}x tez') "
500 ta elon: bittalab : 590.5 ms (1.181 ms/elon) toplam : 3.1 ms (0.006 ms/elon) toplam 193x tez [0;93m2026-09-29 19:36:21.255535628 [W:onnxruntime:Default, device_discovery.cc:164 DiscoverDevicesForPlatform] GPU device discovery failed: device_discovery.cc:89 ReadFileContents Failed to open file: "/sys/class/drm/card0/device/vendor"[m
Farq katta. Lekin bu tezlik bepul emas, va narxini tushunish muhim.
To'plamning narxi
To'plam ishlashi uchun so'rovlar to'planishi kerak. Ya'ni birinchi kelgan so'rov qolganlarini kutadi.
python3 -c " import numpy as np # oddiy model: toplam hajmi va kutish vaqti BITTA_MS = 2.6 # bitta sorovni alohida bajarish TOPLAM_ASOS = 1.5 # toplam uchun tayyorgarlik TOPLAM_QATOR = 0.02 # har qoshimcha qator KELISH_MS = 5.0 # sorovlar orasidagi ortacha vaqt print(f'{\"toplam\":>8s} {\"kutish\":>9s} {\"hisob\":>9s} {\"JAMI p50\":>10s} {\"sorov/s\":>9s}') print('-' * 50) for n in (1, 4, 16, 64): kutish = (n - 1) * KELISH_MS / 2 # ortacha kutish hisob = TOPLAM_ASOS + n * TOPLAM_QATOR if n > 1 else BITTA_MS jami = kutish + hisob otkaz = n / (hisob / 1000) print(f'{n:>8d} {kutish:>8.1f}ms {hisob:>8.1f}ms {jami:>9.1f}ms {otkaz:>9.0f}') "
toplam kutish hisob JAMI p50 sorov/s
--------------------------------------------------
1 0.0ms 2.6ms 2.6ms 385
4 7.5ms 1.6ms 9.1ms 2532
16 37.5ms 1.8ms 39.3ms 8791
64 157.5ms 2.8ms 160.3ms 23022
Jadval bobning asosiy fikrini raqam bilan ko'rsatadi:
- To'plam kattalashgani sari o'tkazuvchanlik oshadi;
- lekin kechikish ham oshadi, chunki kutish vaqti qo'shiladi.
Ya'ni to'plam — tezlashtirish emas, almashtirish. Siz kechikishni berib, o'tkazuvchanlik olasiz.
Qancha to'plam kerak
Savol texnik emas: foydalanuvchi qancha kutishga tayyor?
- Sayt filtri (foydalanuvchi kutib turibdi) → to'plam kichik yoki umuman yo'q
- Tungi hisobot (hech kim kutmaydi) → to'plam katta
- Mobil bildirishnoma (bir necha soniya mayli) → o'rtacha
Amaliy yondashuv — vaqt chegarasi bilan to'plam: "50 ta yig'il, yoki 20 ms kut — qaysi biri oldin bo'lsa". Shunda bo'sh paytda kechikish oshmaydi, band paytda esa to'plam to'ladi.
Ishchilar soni
Ikkinchi vosita — ishchi jarayonlar (worker). Hozir servisda bitta jarayon ishlayapti.
Python da bitta jarayon bir vaqtda bitta Python kodini bajaradi (GIL). Demak ko'p yadro bo'lsa ham, bitta ishchi ularni ishlatmaydi.
docker rm -f kitob-servis4 > /dev/null 2>&1 docker run -d --name kitob-servis4 -p 18102:8000 mlops-servis:v1 \ python3 -m uvicorn xizmat.servis:app --host 0.0.0.0 --port 8000 --workers 4 > /dev/null 2>&1 for i in $(seq 1 40); do curl -s http://127.0.0.1:18102/healthz > /dev/null 2>&1 && break; sleep 0.5; done python3 -c " import json, time, urllib.request from concurrent.futures import ThreadPoolExecutor import numpy as np ELON = {'xonalar': 2, 'maydon': 55, 'qavat': 3, 'jami_qavat': 9, 'narx_mln': 4.5, 'tavsif_uzunligi': 300, 'rasmlar_soni': 8, 'telefon_takrorlanishi': 4, 'kunlik_elonlar': 2, 'egasi_tasdiqlangan': 0} xom = json.dumps(ELON).encode() def qil(port): def s(_=None): t0 = time.perf_counter() r = urllib.request.Request(f'http://127.0.0.1:{port}/bashorat', data=xom, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(r, timeout=30) as j: j.read() return (time.perf_counter() - t0) * 1000 return s print(f'{\"ishchi\":>8s} {\"p50\":>9s} {\"p99\":>9s} {\"sorov/s\":>10s}') print('-' * 40) for nom, port in (('1', 18100), ('4', 18102)): f = qil(port) with ThreadPoolExecutor(max_workers=30) as ex: list(ex.map(f, range(60))) t0 = time.perf_counter() v = np.array(list(ex.map(f, range(300)))) dt = time.perf_counter() - t0 print(f'{nom:>8s} {np.percentile(v,50):>8.1f}ms {np.percentile(v,99):>8.1f}ms {len(v)/dt:>10.0f}') "
ishchi p50 p99 sorov/s
----------------------------------------
1 86.9ms 170.1ms 306
4 27.7ms 103.5ms 847
Natija mashinangizdagi yadro soniga bog'liq. Agar farq kichik bo'lsa, sabab odatda bittadan: yadro yetarli emas yoki ish protsessorda emas, kutishda o'tmoqda.
Ishchi sonini cheksiz oshirib bo'lmaydi
"Ko'proq ishchi — tezroq" degan mantiq ma'lum chegaragacha ishlaydi.
Har ishchi alohida jarayon va u modelni o'z xotirasiga yuklaydi. To'rt ishchi — model xotirada to'rt marta. Bizning model kichik, lekin 2 GB lik model bilan bu 8 GB degani.
Bundan tashqari yadrodan ko'p ishchi qo'ysangiz, ular bir-biri bilan navbat talashadi va p99 yomonlashadi.
Boshlang'ich qoida: ishchi soni = yadro soni. Keyin o'lchang — 12-bobdagi partition masalasi bilan bir xil naqsh.
Tozalash
docker rm -f kitob-servis kitob-servis4 > /dev/null 2>&1 && echo "konteynerlar o'chirildi" docker ps --format '{{.Names}}' | grep -v k3d
konteynerlar o'chirildi novavivo-postgres novavivo-redis stratix-med-frontend-1
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Kechikish | Bitta so'rov vaqti — foydalanuvchi shuni sezadi |
| O'tkazuvchanlik | Sekundiga so'rovlar — server hisobi |
| Ular teskari | To'plam birini yaxshilab, ikkinchisini yomonlashtiradi |
| Bo'sh serverdagi o'lchov | Yolg'on — parallel yuk bilan o'lchang |
| To'g'ri o'lchov | Parallellik + p99 + yetarli davomiylik |
| To'plam foydasi | Tayyorgarlik xarajati bo'linadi |
| To'plam narxi | Birinchi so'rov qolganlarini kutadi |
| Vaqt chegarasi | "N ta yig'il YOKI T ms kut" — ikkalasini muvozanatlaydi |
| Ishchilar | GIL tufayli bitta jarayon bitta yadro ishlatadi |
| Ishchi chegarasi | Har biri modelni xotiraga yuklaydi; yadrodan oshirmang |
Talaba ishi
- Parallellikni 100 ga oshiring. p99 qanday o'zgardi? Qaysi nuqtada server "sinadi"?
/bashorat-toplamyo'lini servisga qo'shing va 100 ta e'lonni bitta so'rovda yuboring. p99 va so'rov/s qanday o'zgardi?- Ishchi sonini 8 va 16 qiling. Qaysi nuqtadan keyin yomonlashadi va nega?
- Vaqt chegarasi bilan to'plam yozing (50 ta yoki 20 ms). Bo'sh paytda kechikish oshdimi?
docker statsbilan 1 va 4 ishchili konteyner xotirasini solishtiring. Farq modelning hajmiga mosmi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.