Asosiy qismga o'tish

Kechikish, to'plam va ishchilar

29-bob 9 kod bloki 2033 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

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.

python10 qator
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**"},
]))
KechikishO'tkazuvchanlik
Nimani o'lchaydiBitta so'rov vaqtiSekundiga so'rovlar
Kim sezadiFoydalanuvchiServer hisobi
Yaxshilash usuliTezroq model, kamroq ishTo'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.

bash28 qator
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 ""
Chiqish (qaytish kodi 0, 4372 ms)
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.

python23 qator
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')
"
Chiqish (qaytish kodi 0, 1387 ms)
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.

python27 qator
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}')
"
Chiqish (qaytish kodi 0, 1915 ms)
 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:

  1. Nechta parallel so'rov bilan?
  2. p99, o'rtacha emas (27-bob)
  3. 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

python19 qator
"""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:

python29 qator
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')
"
Chiqish (qaytish kodi 0, 1127 ms)
500 ta elon:
  bittalab :    590.5 ms  (1.181 ms/elon)
  toplam   :      3.1 ms  (0.006 ms/elon)
  toplam 193x tez
2026-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"

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.

python18 qator
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}')
"
Chiqish (qaytish kodi 0, 52 ms)
  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.

bash35 qator
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}')
"
Chiqish (qaytish kodi 0, 3667 ms)
  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

bash2 qator
docker rm -f kitob-servis kitob-servis4 > /dev/null 2>&1 && echo "konteynerlar o'chirildi"
docker ps --format '{{.Names}}' | grep -v k3d
Chiqish (qaytish kodi 0, 880 ms)
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

  1. Parallellikni 100 ga oshiring. p99 qanday o'zgardi? Qaysi nuqtada server "sinadi"?
  2. /bashorat-toplam yo'lini servisga qo'shing va 100 ta e'lonni bitta so'rovda yuboring. p99 va so'rov/s qanday o'zgardi?
  3. Ishchi sonini 8 va 16 qiling. Qaysi nuqtadan keyin yomonlashadi va nega?
  4. Vaqt chegarasi bilan to'plam yozing (50 ta yoki 20 ms). Bo'sh paytda kechikish oshdimi?
  5. docker stats bilan 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.

Telegramda yozish Narxlar