Asosiy qismga o'tish

Inference servis: modelni HTTP orqali berish

27-bob 11 kod bloki 2387 so'z

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

Eshiksiz restoran

Shahar chekkasida ajoyib oshpaz bor. U chindan ham zo'r pishiradi — buni hamma tan oladi.

Faqat bir muammo: uning restorani yo'q. Eshik yo'q, ofitsiant yo'q, menyu yo'q, telefon yo'q.

Ovqat yemoqchi bo'lsangiz, uning uyiga borib, oshxonasiga kirib, o'zingiz idish olib, o'zingiz solib yeyishingiz kerak.

Oshpaz zo'r. Lekin restoran emas.

Hozirgacha bizda aynan shunday holat: model.onnx fayli bor, u to'g'ri bashorat qiladi. Lekin undan foydalanish uchun Python bilish, kutubxonalarni o'rnatish va kod yozish kerak.

Mobil ilova buni qila olmaydi. Veb-sayt qila olmaydi. Hamkasbingizning PHP dagi tizimi ham.

Eshik, ofitsiant va menyu

Modelni foydalanishga yaroqli qilish uchun uch narsa kerak, va ular restorandagidek:

  • Eshik — kirish nuqtasi. Dasturda bu HTTP manzil: POST /bashorat.
  • Menyu — nima so'rash mumkinligi. Dasturda bu sxema: qaysi maydonlar, qanday turda.
  • Ofitsiant — buyurtmani tekshiradi va oshxonaga uzatadi. Dasturda bu validatsiya: noto'g'ri so'rovni oshpazga umuman yetkazmaydi.

Oxirgisi eng muhim. Yaxshi ofitsiant "bizda bu taom yo'q" deb darhol aytadi. Yomon ofitsiant buyurtmani oshxonaga uzatadi, oshpaz nimadir pishiradi, va mijoz kutilmagan narsa oladi.

6-bobni eslang: o'sha yerda yomon ma'lumot modelga yetib borgan va jim noto'g'ri javob bergan edi. Bu bobda uni eshikda to'xtatamiz.

Modeldan xizmatga

Model fayli o'zi hech kimga foyda bermaydi. Foydalanuvchi uni so'rov yuborib ishlatishi kerak.

python17 qator
diagramma(
    {
        "Mijoz\n(veb, mobil)": (0, 1.4, 2.4, 1.1, "manba"),
        "FastAPI\nvalidatsiya": (3.2, 1.4, 2.4, 1.1, "qayta"),
        "Xususiyat\n(3-bob)": (6.2, 1.4, 2.2, 1.1, "qayta"),
        "ONNX\n(26-bob)": (9.0, 1.4, 2.2, 1.1, "saqlash"),
        "Xato 422\n(yomon kirish)": (3.2, 3.4, 2.4, 1.0, "xizmat"),
    },
    [
        ("Mijoz\n(veb, mobil)", "FastAPI\nvalidatsiya", "JSON"),
        ("FastAPI\nvalidatsiya", "Xususiyat\n(3-bob)", "to'g'ri"),
        ("FastAPI\nvalidatsiya", "Xato 422\n(yomon kirish)", "noto'g'ri"),
        ("Xususiyat\n(3-bob)", "ONNX\n(26-bob)"),
    ],
    nom="inference_servis.png", kenglik=13,
    izoh="Validatsiya chegarada: yomon so'rov modelga umuman yetib bormaydi.",
)

Validatsiya chegarada: yomon so'rov modelga umuman yetib bormaydi.

Validatsiya chegarada: yomon so'rov modelga umuman yetib bormaydi.

Diqqat: 6-bobdagi qoida shu yerda ham amal qiladi — tekshiruv chegarada. Farqi shundaki, bu yerda javob darhol qaytariladi: 422.

Servis kodi

shu faylni yaratingsrc/xizmat/servis.py

python79 qator
"""Broker aniqlash servisi. FastAPI + ONNX.

Diqqat: bu fayl `oqitish/` dan hech narsa import QILMAYDI (3-bob qoidasi).
Faqat `quvur/` dagi umumiy kodni ishlatadi.
"""
import os
import time
from pathlib import Path
from typing import Annotated, Any

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

from quvur.format import onnx_ehtimol, onnx_sessiya
from quvur.xususiyat import bitta_qator

MODEL_YOLI = os.environ.get("MODEL_YOLI", "model.onnx")
CHEGARA = float(os.environ.get("BROKER_CHEGARASI", "0.5"))

app = FastAPI(title="Broker aniqlash", version="1.0.0")
HOLAT: dict[str, Any] = {"sessiya": None, "yuklandi": None, "sorovlar": 0}


class Elon(BaseModel):
    """Kirish sxemasi. Chegaralar 6-bobdagi IJARA_SXEMASI bilan mos."""
    xonalar: Annotated[int, Field(ge=1, le=10)]
    maydon: Annotated[float, Field(ge=10, le=500)]
    qavat: Annotated[int, Field(ge=1, le=60)]
    jami_qavat: Annotated[int, Field(ge=1, le=60)]
    narx_mln: Annotated[float, Field(ge=0.5, le=200)]
    tavsif_uzunligi: Annotated[int, Field(ge=0, le=5000)]
    rasmlar_soni: Annotated[int, Field(ge=0, le=100)]
    telefon_takrorlanishi: Annotated[int, Field(ge=1, le=500)]
    kunlik_elonlar: Annotated[int, Field(ge=1, le=200)]
    egasi_tasdiqlangan: Annotated[int, Field(ge=0, le=1)]


class Javob(BaseModel):
    broker_ehtimoli: float
    broker: bool
    chegara: float
    ms: float


def yukla() -> None:
    """Modelni bir marta yuklaydi. Fayl yo'q bo'lsa — xizmat baribir ishga tushadi."""
    p = Path(MODEL_YOLI)
    if p.exists():
        HOLAT["sessiya"] = onnx_sessiya(p)
        HOLAT["yuklandi"] = time.time()


yukla()


@app.get("/healthz")
def tirikmi() -> dict[str, str]:
    """Jarayon ishlayaptimi. Model kerak EMAS."""
    return {"holat": "tirik"}


@app.get("/ready")
def tayyormi() -> dict[str, Any]:
    """Xizmat ko'rsatishga tayyormi. Model yuklanmagan bo'lsa — 503."""
    if HOLAT["sessiya"] is None:
        raise HTTPException(status_code=503, detail="model yuklanmagan")
    return {"holat": "tayyor", "sorovlar": HOLAT["sorovlar"], "chegara": CHEGARA}


@app.post("/bashorat", response_model=Javob)
def bashorat(elon: Elon) -> Javob:
    if HOLAT["sessiya"] is None:
        raise HTTPException(status_code=503, detail="model yuklanmagan")
    t0 = time.perf_counter()
    x = bitta_qator(elon.model_dump())
    e = float(onnx_ehtimol(HOLAT["sessiya"], x.values)[0])
    HOLAT["sorovlar"] += 1
    return Javob(broker_ehtimoli=round(e, 4), broker=e >= CHEGARA,
                 chegara=CHEGARA, ms=round((time.perf_counter() - t0) * 1000, 2))

Tayyorgarlik va ishga tushirish

python18 qator
python3 -c "
import sys, pathlib; sys.path.insert(0, 'src')
if not pathlib.Path('model.onnx').exists():
    import joblib, 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)
    joblib.dump(m, 'sinov_model.joblib')
    onnx_ga(m, len(XUSUSIYATLAR), 'model.onnx')
    print('model va ONNX yaratildi')
else:
    print('model.onnx allaqachon bor')
"
Chiqish (qaytish kodi 0, 3379 ms)
model va ONNX yaratildi

Serverni 18000-portda ishga tushiramiz (boshqa xizmatlarga xalaqit bermaslik uchun):

bash11 qator
export PYTHONPATH=src
python3 -m uvicorn xizmat.servis:app --host 127.0.0.1 --port 18000 \
  --log-level warning > servis.log 2>&1 < /dev/null &
echo $! > servis.pid
for i in $(seq 1 40); do
  curl -s http://127.0.0.1:18000/healthz > /dev/null 2>&1 && break
  sleep 0.5
done
curl -s http://127.0.0.1:18000/healthz
echo ""
curl -s http://127.0.0.1:18000/ready
Chiqish (qaytish kodi 0, 1029 ms)
{"holat":"tirik"}
{"holat":"tayyor","sorovlar":0,"chegara":0.5}

Serverni orqa fonda ishga tushirishning ikki tuzog'i

Men shu bobni yozayotganda ikkalasiga ham tushdim.

1. & && ro'yxatiga yopishib qoladi. Men avval shunday yozgan edim:

bash1 qator
cd "$(pwd)" && PYTHONPATH=src python3 -m uvicorn ... &

Bu yerda & faqat python3 ga emas, butun cd && python3 ro'yxatiga tegishli. Bash uni alohida qobiqda bajaradi va o'sha qobiqni kutib qoladi. Skript qotib qoldi. O'lchadim: oddiy cmd & — 0.00 s, cd ... && cmd & — 25.00 s (ya'ni sleep 25 tugaguncha). Yechim: cd va export ni alohida qatorga chiqarish.

2. pkill -f skriptning o'zini o'ldiradi. Keyingi urinishim:

matn1 qator
pkill -f "port 18000"

pkill -f butun buyruq qatorini qidiradi. Skriptni bajarayotgan bash -c jarayonining buyruq qatori esa — skriptning o'z matni, ichida port 18000 bor. Ya'ni bu buyruq o'zini o'ldiradi. Yechim: PID ni saqlab qo'yish (echo $! > servis.pid) va aynan shuni o'ldirish.

Ikkala xato ham "server ishlamayapti" kabi ko'rinadi, lekin server aybdor emas edi.

/healthz va /ready — ikki boshqa savol:

Nuqta Savol Kim so'raydi
/healthz Jarayon tirikmi? Konteyner boshqaruvchi (qayta ishga tushirsinmi)
/ready Ish qabul qila oladimi? Yuk muvozanatlagich (so'rov yuborsinmi)

Farqi muhim: model yuklanayotgan paytda jarayon tirik, lekin tayyor emas. /healthz ni ikkalasiga ishlatsangiz, yuk muvozanatlagich tayyor bo'lmagan xizmatga so'rov yuboradi.

Bashorat so'raymiz

bash11 qator
echo "=== aniq EGA ==="
curl -s -X POST http://127.0.0.1:18000/bashorat -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.5,
  "tavsif_uzunligi": 600, "rasmlar_soni": 15, "telefon_takrorlanishi": 1,
  "kunlik_elonlar": 1, "egasi_tasdiqlangan": 1}'
echo ""
echo "=== aniq BROKER ==="
curl -s -X POST http://127.0.0.1:18000/bashorat -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.2,
  "tavsif_uzunligi": 60, "rasmlar_soni": 2, "telefon_takrorlanishi": 18,
  "kunlik_elonlar": 9, "egasi_tasdiqlangan": 0}'
Chiqish (qaytish kodi 0, 12 ms)
=== aniq EGA ===
{"broker_ehtimoli":0.0563,"broker":false,"chegara":0.5,"ms":2.83}
=== aniq BROKER ===
{"broker_ehtimoli":0.9179,"broker":true,"chegara":0.5,"ms":1.5}

ms maydoniga qarang — bu faqat model vaqti, tarmoq emas. 26-bobdagi ONNX tanlovi shu yerda o'zini oqlaydi.

Validatsiya ishlaydimi

Endi ataylab yomon so'rov yuboramiz:

bash11 qator
echo "=== maydon = 0 (mumkin emas) ==="
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://127.0.0.1:18000/bashorat \
  -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 0, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.5,
  "tavsif_uzunligi": 300, "rasmlar_soni": 8, "telefon_takrorlanishi": 1,
  "kunlik_elonlar": 1, "egasi_tasdiqlangan": 1}'

echo "=== maydon yetishmaydi ==="
curl -s -X POST http://127.0.0.1:18000/bashorat -H "Content-Type: application/json" \
  -d '{"xonalar": 2}' | head -c 220
echo ""
Chiqish (qaytish kodi 0, 6 ms)
=== maydon = 0 (mumkin emas) ===
HTTP 422
=== maydon yetishmaydi ===
{"detail":[{"type":"missing","loc":["body","maydon"],"msg":"Field required","input":{"xonalar":2}},{"type":"missing","loc":["body","qavat"],"msg":"Field required","input":{"xonalar":2}},{"type":"missing","loc":["body","j

422 qaytdi va qaysi maydon noto'g'ri ekani aytildi. Model bu so'rovni umuman ko'rmadi.

6-bobdagi tajribani eslang: o'sha yerda maydon = 0 modelga yetib borgan va jim yomon natija bergan edi. Bu yerda u chegarada to'xtatildi.

Latency o'lchash

python24 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:18000/bashorat', data=xom,
                               headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(r, timeout=5) as j:
        j.read()
    return (time.perf_counter() - t0) * 1000

for _ in range(20): sorov()          # isitish
v = np.array([sorov() for _ in range(300)])
print(f'  p50 : {np.percentile(v, 50):6.2f} ms')
print(f'  p95 : {np.percentile(v, 95):6.2f} ms')
print(f'  p99 : {np.percentile(v, 99):6.2f} ms')
print(f'  max : {v.max():6.2f} ms')
"
Chiqish (qaytish kodi 0, 818 ms)
  p50 :   2.07 ms
  p95 :   2.52 ms
  p99 :   3.12 ms
  max :  22.28 ms

O'rtacha latency yolg'on gapiradi

Ko'p hisobotda "o'rtacha javob vaqti 8 ms" deb yoziladi. Lekin foydalanuvchi o'rtachani sezmaydi — u o'zining so'rovini sezadi.

Agar 100 ta so'rovdan 99 tasi 5 ms, bittasi 500 ms bo'lsa, o'rtacha 10 ms chiqadi va "yaxshi" ko'rinadi. Lekin har yuzinchi foydalanuvchi yarim soniya kutadi.

Shuning uchun p95 va p99 o'lchanadi. SLA ham shularga qo'yiladi: "p99 < 100 ms".

Model yo'q bo'lsa nima bo'ladi

Bu bo'lim ahamiyatsiz tuyuladi — model doim bo'ladi-ku. Lekin u aynan eng yomon kunda bo'lmaydi.

Bir nechta haqiqiy holat:

  • Yangi server ko'tarildi, lekin model fayli hali ko'chirilmagan.
  • Registrdagi model o'chirilgan yoki nomi o'zgargan (23-bob).
  • Diskda joy tugagan va yuklash yarmida to'xtagan.

Har uchalasida savol bitta: xizmat nima qilishi kerak?

Ikki yo'l bor va farqi katta.

Birinchi yo'l: yiqilib tushish. Model yo'q bo'lsa, jarayon ishga tushmaydi. Ko'rinishi mantiqiy — "ishlay olmasam, ishlamayman".

Lekin natija yomon: konteyner qayta-qayta ko'tarilib yiqiladi, yuk muvozanatlagich nima bo'layotganini bilmaydi, log da shunchaki "crash" yoziladi. Sabab noma'lum.

Ikkinchi yo'l: ko'tarilish, lekin rostini aytish. Jarayon ishga tushadi, /healthz "tirikman" deydi, /ready esa 503 qaytaradi va sababini yozadi.

Endi yuk muvozanatlagich bu serverga so'rov yubormaydi, monitoring aniq sababni ko'radi, va siz log o'qimasdan nima yetishmayotganini bilasiz.

Ikkinchisi to'g'ri, va uni tekshiramiz:

bash16 qator
kill "$(cat servis.pid)" 2>/dev/null; sleep 1
export PYTHONPATH=src MODEL_YOLI=yoq_fayl.onnx
python3 -m uvicorn xizmat.servis:app --host 127.0.0.1 --port 18001 \
  --log-level warning > servis2.log 2>&1 < /dev/null &
echo $! > servis2.pid
for i in $(seq 1 40); do curl -s http://127.0.0.1:18001/healthz >/dev/null 2>&1 && break; sleep 0.5; done

echo -n "healthz : "; curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18001/healthz
echo -n "ready   : "; curl -s -o /dev/null -w "HTTP %{http_code}" http://127.0.0.1:18001/ready; echo ""
echo -n "bashorat: "; curl -s -X POST http://127.0.0.1:18001/bashorat \
  -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.5,
  "tavsif_uzunligi": 300, "rasmlar_soni": 8, "telefon_takrorlanishi": 1,
  "kunlik_elonlar": 1, "egasi_tasdiqlangan": 1}'
echo ""
kill "$(cat servis2.pid)" 2>/dev/null; echo "(sinov serveri to'xtatildi)"
Chiqish (qaytish kodi 0, 2032 ms)
healthz : HTTP 200
ready   : HTTP 503
bashorat: {"detail":"model yuklanmagan"}
(sinov serveri to'xtatildi)

/healthz 200, /ready 503. Aynan shu kerak: jarayon tirik, lekin so'rov yubormang.

HTTP kodlari

python8 qator
import pandas as pd
show(pd.DataFrame([
    {"kod": 200, "ma'nosi": "Bashorat tayyor", "kim aybdor": "-"},
    {"kod": 422, "ma'nosi": "Kirish sxemaga mos emas", "kim aybdor": "Mijoz"},
    {"kod": 503, "ma'nosi": "Model yuklanmagan / tayyor emas", "kim aybdor": "Server"},
    {"kod": 500, "ma'nosi": "Kutilmagan xato", "kim aybdor": "Server (bug)"},
    {"kod": 429, "ma'nosi": "So'rov juda ko'p", "kim aybdor": "Mijoz (yoki cheklov)"},
]))
kodma'nosikim aybdor
200Bashorat tayyor-
422Kirish sxemaga mos emasMijoz
503Model yuklanmagan / tayyor emasServer
500Kutilmagan xatoServer (bug)
429So'rov juda ko'pMijoz (yoki cheklov)

4xx — mijoz xatosi, 5xx — server xatosi. Bu chegarani to'g'ri qo'yish muhim: 422 ni 500 qilib qaytarsangiz, monitoring soxta signal beradi va siz yo'q muammoni qidirasiz.

Xulosa

Tushuncha Asosiy fikr
Validatsiya chegarada pydantic yomon so'rovni modelga yetkazmaydi
422 Qaysi maydon noto'g'ri ekani aytiladi
/healthz va /ready Tirikmi va tayyormi — boshqa savollar
Model yo'q Xizmat ishga tushadi, /ready 503 qaytaradi
ONNX Bitta qator uchun eng tez yo'l (26-bob)
p95 / p99 O'rtacha yolg'on gapiradi — persentil o'lchang
4xx va 5xx Aybdor kim — mijozmi yoki serverni ajrating
Import qoidasi xizmat/ oqitish/ ni import qilmaydi (3-bob)

Talaba ishi

  1. /bashorat ga ro'yxat qabul qiladigan /bashorat-toplam qo'shing. 100 ta e'lon uchun p99 qanday o'zgaradi?
  2. Javobga model versiyasini qo'shing (23-bob registridan). Nega bu muhim?
  3. CHEGARA ni 0.3 qilib qayta ishga tushiring. Qaysi e'lonlar endi broker deb belgilanadi?
  4. Bir vaqtda 50 ta so'rov yuboring (ThreadPoolExecutor). p99 qanday o'zgardi va nega?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar