Mundarija (27)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Model API uchun testlar xaritasi
- 2.2. Kontrakt testlari va orqaga moslik
- 2.3. Golden testlar
- 2.4. Chegara holatlar va NaN tuzog'i
- 2.5. Kiruvchi so'rovni cheklash
- 2.6. Timeout va zaxira javob (fallback)
- 2.7. Kesh
- 2.8. Idempotentlik kaliti
- 2.9. Strukturali JSON loglar
- 2.10. API kaliti bilan autentifikatsiya
- 2.11. Rate limiting: token chelak
- 2.12. Yuklama sinovi: navbat modeli
- 2.13. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — API test to'plami: kontrakt, golden, chegara holatlar
- Misol 2 — Ishonchlilik: timeout, zaxira, kesh, idempotentlik, cheklovlar
- Misol 3 — Strukturali loglar va API kaliti
- Misol 4 — Rate limiting va yuklama simulyatsiyasi
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
27.8-dars: API ni mustahkamlash va testlash
27-QISM — MLOPS VA DEPLOY · 8-dars
1. Kirish va motivatsiya
Oldingi darsda (27.7) Predictor ni FastAPI orqali taqdim etdik: sxemalar, to'rt endpoint, lifespan, xato kodlari. Ishlab chiqish muhitida u a'lo ishlaydi. Ishlab chiqarish esa boshqa dunyo:
- mijoz bitta so'rovda 50 000 qator yuboradi va xotira tugaydi;
- model ba'zan 300 ms "o'ylab qoladi" (disk, xotira tozalash), mijoz esa 100 ms dan ortiq kuta olmaydi;
- mobil ilova tarmoq uzilganda so'rovni qayta yuboradi — va mijozga ikki marta SMS ketadi;
- tunda xato bo'ldi, lekin loglarda faqat
ERROR: something went wrong— qaysi so'rov, qaysi mijoz, noma'lum; yoki aksincha, loglarda mijozlarning telefon raqamlari ochiq turibdi; - API ga internetdan istalgan odam murojaat qila oladi;
- bitta mijoz skripti sekundiga 1000 so'rov yuboradi va boshqalar uchun xizmat qotib qoladi.
Bundan tashqari, API ning har o'zgarishi (yangi model, yangi maydon, kutubxona yangilanishi) biror narsani jimgina buzishi mumkin. Buni faqat testlar ushlaydi — va model API uchun oddiy birlik testlaridan tashqari maxsus testlar kerak: kontrakt (sxema buzilmadimi?), golden (ma'lum kirish — ma'lum chiqish), chegara holatlar (bo'sh, NaN, cheksiz, noma'lum kategoriya), batch va yakka mosligi.
Real vaziyat. Jamoa FastAPI xizmatini chiqardi va testlarida hamma narsa yashil edi. Bir hafta o'tib monitoringda 500 xatolar paydo bo'ldi. Sabab: bir mijoz tizimi bo'sh maydonlarni NaN sifatida yuborardi. Pydantic bu qiymatni to'g'ri rad etdi, lekin FastAPI ning standart 422 javobi xato tafsilotida kiruvchi qiymatni (NaN) qaytarishga urindi — NaN esa standart JSON da yo'q, javobni yasashning o'zi yiqildi va mijoz 422 o'rniga 500 oldi. Chegara holatlar testi bu tuzoqni birinchi kunning o'zida ko'rsatgan bo'lardi. Bu darsning 1-misolida biz uni aynan shunday topamiz.
Bu darsda API uchun testlar to'plamini yozamiz va uni ishonchlilik, kuzatuvchanlik va xavfsizlik qatlamlari bilan mustahkamlaymiz — hammasi deterministik, virtual soat bilan.
Bu darsda:
- API testlari: birlik, kontrakt, golden, chegara holatlar, batch/yakka mosligi
NaNtuzog'i va o'z validatsiya xatosi ishlovchisi- Kiruvchi so'rovni cheklash: batch hajmi va tana hajmi
- Timeout va zaxira javob (fallback), kesh — virtual soat bilan
- Idempotentlik kaliti
- Strukturali JSON loglar: so'rov id, shaxsiy ma'lumotni niqoblash
- API kaliti bilan autentifikatsiya: muhit o'zgaruvchisi,
401va403 - Rate limiting: token chelak (token bucket) noldan
- Yuklama sinovi g'oyasi: navbat modeli, throughput va kechikish persentillari
- Tuzoqlar
ℹ Misollar real FastAPI 0.141, pydantic 2.13, sklearn bilan (Python 3.14). Server ishga tushirilmaydi —
TestClient. Barcha kechikishlar virtual soat bilan simulyatsiya qilinadi:time.sleepyo'q, natijalar har safar bir xil.
2. Nazariya — chuqur tushuntirish
2.1. Model API uchun testlar xaritasi
TUR NIMA TEKSHIRADI MISOL
BIRLIK alohida funksiya/klass, HTTP siz ehtimol [0, 1] da; sinf = p >= chegara
KONTRAKT API shartnomasi buzilmadimi endpointlar ro'yxati; javob kalitlari
(sxema, javob shakli, orqaga moslik) va turlari; eski majburiy maydonlar
GOLDEN ma'lum kirish -> ma'lum chiqish 5 mijoz -> saqlangan ehtimollar (tol 1e-6)
(nazorat namunalarining HTTP versiyasi)
CHEGARA HOLATLAR "g'alati" kirishlar bo'sh batch, NaN, cheksiz, 1e308,
noma'lum kategoriya, maks batch
MOSLIK bir xil narsa ikki yo'lda bir xilmi batch vs yakka; API vs batch skript
YUKLAMA ko'p so'rov ostida kechikish va p50/p95/p99, throughput, xatolar
o'tkazuvchanlik ulushi 2.12-bob
PIRAMIDA: birlik va kontrakt - ko'p va tez; golden va chegara - har PR da;
yuklama - reliz oldidan va muntazamModel API testining o'ziga xosligi: kod o'zgarmasa ham model o'zgaradi (qayta o'qitish, yangi versiya). Shuning uchun golden testlar ikki xil ma'noga ega: kod o'zgarganda — "bashorat o'zgarmasligi kerak edi, o'zgardi — regressiya"; model almashganda — "bashorat o'zgarishi kutilgan, golden qiymatlar ongli ravishda yangilanadi va sharh bilan commit qilinadi".
Har test turi o'z xatosini ushlaydi. Kontrakt — mijozlar bilan kelishuvni, golden — bashoratlarni, chegara holatlar — "hech kim o'ylamagan" kirishlarni.
2.2. Kontrakt testlari va orqaga moslik
ORQAGA MOS (mijozlar buzilmaydi): ORQAGA MOS EMAS (MAJOR - 27.5):
yangi IXTIYORIY kirish maydoni yangi MAJBURIY kirish maydoni
javobga yangi maydon qo'shish maydonni o'chirish yoki qayta nomlash
chegara kengaytirish (le=600 -> 1000) chegara toraytirish (le=600 -> 100)
enum ga yangi qiymat (kirishda) maydon turini o'zgartirish (int -> str)
KONTRAKT TESTI (snapshot bilan):
reliz paytida: sxemaning qisqa "surati" saqlanadi (maydonlar, turlar, majburiylar)
har PR da: yangi sxema <-> surat:
eski maydonlar hammasi bormi, turlari o'shami?
yangi majburiy maydon yo'qmi?
-> buzilish bo'lsa - test YIQILADI, versiya MAJOR ga ko'tariladi yoki o'zgarish qaytariladiSurat bilan to'liq tenglikni emas, orqaga moslik qoidalarini tekshiring — aks holda har ixtiyoriy maydon qo'shilishi testni buzadi va jamoa surat faylini o'ylamasdan yangilashga o'rganib qoladi.
2.3. Golden testlar
GOLDEN FAYL (reliz paytida yoziladi, git da saqlanadi):
[{"kirish": {...}, "ehtimol": 0.826812}, ...] 5-20 ta xilma-xil mijoz
TEST:
har kirish -> POST /predict -> |ehtimol - golden| <= 1e-6
TOLERANTLIK:
API javobi yaxlitlangan (4 xona) -> golden ham shunday aniqlikda solishtiriladi
aynan tenglik (==) - platformalar arasi mayda farqlar (27.6 dagi 2.2e-16) tufayli mo'rt
juda keng tolerantlik (1e-2) - haqiqiy regressiyani yashiradi2.4. Chegara holatlar va NaN tuzog'i
RO'YXAT (model API uchun minimal):
bo'sh batch [] -> 422
maksimal batch (100) -> 200 ; 101 -> 422
NaN, Infinity (JSON da nostandart) -> 422, 500 EMAS
juda katta son (1e308) -> 422 (domen chegarasi)
noma'lum kategoriya -> siyosatga ko'ra: 422 yoki 200 + ogohlantirish
ixtiyoriy maydon yo'q / null -> 200 (sukut / imputatsiya)
noma'lum maydon -> 422 (extra="forbid")
noto'g'ri JSON -> 422 (json_invalid)
tana juda katta (MB lar) -> 413 (modelga yetib bormasdan)
NaN TUZOG'I (bu mashinadagi FastAPI da tekshirilgan):
Python json NaN ni o'qiydi -> pydantic to'g'ri rad etadi
standart 422 ishlovchisi xatoga "input": NaN ni qo'shadi
javobni JSON ga o'girish NaN da YIQILADI -> mijoz 500 oladi
YECHIM: o'z RequestValidationError ishlovchingiz - faqat loc, type, msg
(+ Field(allow_inf_nan=False) - xato turi aniqroq bo'ladi)Kiruvchi qiymatni xato javobida qaytarmaslikning ikkinchi foydasi — maxfiylik: xato javobi loglarga va mijoz tizimlariga tushadi, unda esa shaxsiy ma'lumot bo'lishi mumkin.
2.5. Kiruvchi so'rovni cheklash
QATLAM CHEGARA JAVOB NEGA
tana hajmi content-length <= 256 KB 413 JSON ni o'qish/parslashdan OLDIN
batch hajmi 1 <= len(mijozlar) <= 100 422 xotira va kechikish bashorat qilinadi
maydon uzunligi max_length=40 422 "10 MB lik ism" hujumi
so'rov tezligi token chelak 2.11-bob 429 bitta mijoz hammani bosib qolmasinKatta batch kerak bo'lsa — bu allaqachon batch vazifa (27.7, 2.1): fayl yoki navbat orqali, API orqali emas.
2.6. Timeout va zaxira javob (fallback)
MUAMMO: model odatda 10 ms, lekin ba'zan 300 ms (disk, GC, qo'shni jarayon)
mijozning byudjeti 100 ms; kutish - uning ham ishini to'xtatadi
YECHIM:
model chaqiruvi BYUDJET bilan: 100 ms ichida javob bo'lmasa -> bekor
bekor bo'lsa -> ZAXIRA javob:
oddiy qoida modeli ("oylik tarif va shikoyat >= 2 -> 0.7, aks holda 0.2")
yoki oxirgi ma'lum natija (keshdan)
yoki "baholab bo'lmadi" + standart qaror
javobda: "manba": "model" | "zaxira" | "kesh" -> mijoz va monitoring biladi
HAQIQIY KOD (g'oya):
natija = await asyncio.wait_for(model_chaqir(x), timeout=0.1)
except TimeoutError: natija = zaxira(x)
VIRTUAL SOAT (darsda): haqiqiy kutish o'rniga kechikish hisoblagichi -
natijalar deterministik, test bir necha millisekundda o'tadiZaxira javobning sifati past bo'lishi tabiiy — muhimi, u tez va oldindan ma'lum. Zaxira ulushini monitoring qiling 27.11-bob: u o'sib ketsa — asosiy model bilan muammo bor.
2.7. Kesh
KALIT: sha256(kanonik JSON(kirish) + model versiyasi)
kanonik: sort_keys=True - maydonlar tartibi kalitga ta'sir qilmaydi
versiya kalitda - yangi model chiqqanda eski javoblar avtomatik "eskiradi"
SIYOSAT: LRU (eng uzoq ishlatilmagan chiqariladi), hajm chegarasi, TTL
QACHON FOYDALI: bir xil kirishlar ko'p takrorlanadi (mashhur mahsulotlar, qayta so'rovlar)
QACHON ZARARLI: kirishda vaqt/tasodif bo'lsa; model real vaqt belgilaridan foydalansa2.8. Idempotentlik kaliti
MUAMMO: mijoz so'rov yubordi, javob tarmoqda yo'qoldi, mijoz QAYTA yubordi
"chegirma SMS yubor" endpointi -> mijozga ikki SMS
YECHIM: mijoz har mantiqiy amal uchun noyob kalit yuboradi:
Idempotency-Key: 7f3a-...
server: kalit -> (tana xeshi, javob) ni saqlaydi
kalit yangi -> bajaradi, saqlaydi
kalit bor, tana o'sha -> SAQLANGAN javobni qaytaradi (qayta bajarmaydi)
kalit bor, tana boshqa -> 409 Conflict (mijoz xatosi)
SOF BASHORAT (/predict) - tabiatan idempotent (yon ta'sir yo'q)
YON TA'SIRLI (yozish, SMS, to'lov) - idempotentlik kaliti SHART2.9. Strukturali JSON loglar
YOMON: "Prediction done for Ali +998901234567 p=0.83"
- qidirib bo'lmaydi, shaxsiy ma'lumot ochiq
YAXSHI: {"daraja": "INFO", "xabar": "bashorat", "sorov_id": "req-0003",
"yol": "/predict", "status": 200, "kechikish_ms": 12.0,
"mijoz": "crm", "telefon": "+998*****4567", "versiya": "1.2.0"}
QOIDALAR:
har qator - bitta JSON obyekt (mashina o'qiydi: Loki, ELK, CloudWatch)
sorov_id - har so'rovga (kelgan X-Request-ID yoki yangi), javob sarlavhasida ham
shaxsiy ma'lumot NIQOBLANADI: telefon, email, ism, pasport; kalitlar UMUMAN yozilmaydi
daraja statusga ko'ra: 5xx -> ERROR, 4xx -> WARNING, qolgani -> INFO
stdout ga - konteyner muhiti 27.9-bob loglarni o'zi yig'adi2.10. API kaliti bilan autentifikatsiya
KALIT QAYERDA:
kodda / git da - HECH QACHON
muhit o'zgaruvchisi - MODEL_API_KALITLARI (sir boshqaruvchisidan keladi: 27.10)
ishga tushishda o'qiladi; yo'q bo'lsa - ilova ISHGA TUSHMAYDI
TEKSHIRISH:
X-API-Key sarlavhasi yo'q -> 401 Unauthorized (+ WWW-Authenticate sarlavhasi)
kalit noto'g'ri -> 403 Forbidden
kalit to'g'ri, lekin roli yetmaydi (crm -> /admin) -> 403
taqqoslash: hmac.compare_digest (vaqt bo'yicha sizib chiqmaydi)
LOGDA: kalitning o'zi emas, egasining NOMI ("crm")
AYLANTIRISH (rotation): bir vaqtda ikki kalit amal qiladi -> mijoz yangisiga o'tadi -> eskisi o'chiriladi401 va 403 farqi: 401 — "siz kimsiz? o'zingizni tanishtiring"; 403 — "kim ekaningizni bilaman (yoki kalitingiz yaroqsiz), lekin bunga ruxsat yo'q". Kengroq tizimlarda API kaliti o'rniga OAuth2/JWT tokenlari ishlatiladi — g'oya o'sha.
2.11. Rate limiting: token chelak
CHELAK: sig'im S token, sekundiga R token bilan to'ladi (S dan oshmaydi)
so'rov keldi: token >= 1 -> token -= 1, RUXSAT
token < 1 -> 429 Too Many Requests,
Retry-After = (1 - token) / R sekund
XOSSALAR:
qisqa portlash (burst) S tagacha ruxsat - foydalanuvchi tajribasi yaxshi
uzoq muddatda o'rtacha tezlik R dan oshmaydi
har mijoz (kalit) uchun ALOHIDA chelak - biri boshqasini bosib qolmaydi
TAQSIMLANGAN: bir necha server bo'lsa - chelaklar umumiy omborda (Redis)2.12. Yuklama sinovi: navbat modeli
BELGILAR: lambda - so'rovlar tezligi (so'rov/s), E[S] - o'rtacha xizmat vaqti,
c - parallel ishchilar soni
YUKLANISH: rho = lambda * E[S] / c
rho < 1 -> barqaror, navbat cheklangan
rho -> 1 -> kechikish KESKIN o'sadi (ayniqsa p99)
rho > 1 -> navbat cheksiz o'sadi, throughput = c / E[S] (sig'im) da to'xtaydi
QOIDA: o'rtachaga emas, PERSENTILLARGA qarang - p95, p99
sig'imni rho ~ 0.6-0.7 ga mo'ljallang (zaxira bilan)
HAQIQIY VOSITALAR (bu mashinada yo'q):
locust, k6, wrk - haqiqiy serverga haqiqiy so'rovlar
darsda: diskret hodisali SIMULYATSIYA - xuddi shu savollar, virtual vaqtdaHaqiqiy yuklama sinovi shunday ko'rinadi (faqat tanishish uchun):
# k6 bilan: 50 virtual foydalanuvchi, 2 daqiqa
k6 run --vus 50 --duration 2m yuklama.js
# locust bilan: veb-interfeyssiz, 200 foydalanuvchi, sekundiga 20 tadan qo'shiladi
locust -f yuklama.py --headless -u 200 -r 20 --run-time 3m --host http://localhost:8000 Kechikish yuklanishga chiziqli emas. rho = 0.5 dan 0.9 ga o'tish kechikishni ikki emas, o'n barobar oshirishi mumkin — shuning uchun sig'im zaxira bilan rejalashtiriladi va rate limiting ortiqcha yuklamani kesadi.
2.13. Tuzoqlar
Asosiy tuzoqlar: faqat "baxtli yo'l" testlari; golden testni aynan tenglik bilan yozish; kontrakt suratini har safar o'ylamasdan yangilash; standart 422 ishlovchisi bilan NaN -> 500; xato javobida kiruvchi qiymatni qaytarish; batch va tana hajmini cheklamaslik; timeout siz model chaqiruvi; zaxira javobni belgilamaslik (manba yo'q); kesh kalitida model versiyasi yo'qligi; yon ta'sirli endpointda idempotentlik yo'qligi; matnli, sorov_id siz loglar; logda telefon, email yoki API kaliti; kalitni kodda yozish; 401 va 403 ni aralashtirish; == bilan kalit taqqoslash; umumiy (hamma uchun bitta) rate limit; o'rtacha kechikishga qarab sig'im rejalash.
3. Tez ma'lumotnoma
import hashlib
import hmac
import json
import logging
import os
import sys
from fastapi import FastAPI, Header, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validatsiya_xatosi(request, exc):
# "input" ni qaytarmaymiz: NaN -> 500 tuzog'i va maxfiylik
return JSONResponse(status_code=422, content={"detail": [
{"loc": list(e["loc"]), "type": e["type"], "msg": e["msg"]}
for e in exc.errors()]})
def kalit_tekshir(x_api_key: str | None = Header(default=None)):
kalit = os.environ.get("MODEL_API_KALITI", "")
if x_api_key is None:
raise HTTPException(status_code=401, detail="API kaliti kerak",
headers={"WWW-Authenticate": "ApiKey"})
if not kalit or not hmac.compare_digest(x_api_key, kalit):
raise HTTPException(status_code=403, detail="kalit yaroqsiz")
def kesh_kaliti(kirish, versiya):
matn = json.dumps(kirish, sort_keys=True) + versiya
return hashlib.sha256(matn.encode("utf-8")).hexdigest()
class TokenChelak:
def __init__(self, sigim, tezlik):
self.sigim, self.tezlik = sigim, tezlik
self.tokenlar, self.oxirgi = sigim, 0.0
def ol(self, hozir):
self.tokenlar = min(self.sigim,
self.tokenlar + (hozir - self.oxirgi) * self.tezlik)
self.oxirgi = hozir
if self.tokenlar >= 1:
self.tokenlar -= 1
return True, 0.0
return False, (1 - self.tokenlar) / self.tezlik
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")Tuzilma xulosasi
testlar: birlik, kontrakt (orqaga moslik), golden (tol), chegara, batch=yakka, yuklama
422 ishlovchisi: loc/type/msg, input siz (NaN -> 500 tuzog'i)
cheklovlar: 413 tana, 422 batch, 429 tezlik
timeout + zaxira ("manba"), kesh (kalitda versiya), idempotentlik kaliti (409)
JSON loglar: sorov_id, niqoblash, kalit yo'q; stdout
API kaliti muhitdan: 401 yo'q, 403 yaroqsiz/ruxsatsiz; compare_digest
token chelak: sig'im + tezlik, har mijozga alohida; rho -> 1 da p99 portlaydi4. Batafsil misollar
Misollar real FastAPI/pydantic/sklearn bilan (Python 3.14). Server ishga tushirilmaydi —
TestClient. Vaqt — virtual soat, hamma natija deterministik.
Misol 1 — API test to'plami: kontrakt, golden, chegara holatlar
"""Model API test to'plami: uch variant ilovada 15 test va natija jadvali."""
import json
import warnings
from contextlib import asynccontextmanager
from enum import Enum
import numpy as np
import pandas as pd
from fastapi import Depends, FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ConfigDict, Field, create_model
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from fastapi.testclient import TestClient
SONLI, KATEGORIAL = ["oylar", "tolov"], ["tarif", "hudud"]
def malumot(n, seed):
rng = np.random.default_rng(seed)
df = pd.DataFrame({
"oylar": rng.integers(1, 72, n), "tolov": rng.normal(60, 20, n).clip(10, 150),
"tarif": rng.choice(["oylik", "yillik"], n),
"hudud": rng.choice(["Toshkent", "Buxoro", "Andijon"], n)})
logit = -1 - 0.04 * df["oylar"] + 0.03 * (df["tolov"] - 60) + np.where(
df["tarif"] == "oylik", 1.0, -0.5)
df["ketdi"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
return df
class Predictor:
versiya, chegara = "1.2.0", 0.35
def __init__(self, C):
df = malumot(3000, seed=0)
pre = ColumnTransformer([("son", StandardScaler(), SONLI),
("kat", OneHotEncoder(handle_unknown="ignore"),
KATEGORIAL)])
self.model = Pipeline([("pre", pre), ("clf", LogisticRegression(C=C))])
self.model.fit(df[SONLI + KATEGORIAL], df["ketdi"])
def predict_proba(self, df):
return self.model.predict_proba(df[SONLI + KATEGORIAL])[:, 1]
class Tarif(str, Enum):
oylik = "oylik"
yillik = "yillik"
class Mijoz(BaseModel):
model_config = ConfigDict(extra="forbid")
oylar: int = Field(ge=0, le=600)
tolov: float = Field(gt=0, le=1000)
tarif: Tarif
hudud: str = Field(default="nomalum", min_length=2, max_length=40)
class MijozV2(Mijoz):
kanal: str | None = None # yangi IXTIYORIY maydon
class Bashorat(BaseModel):
ehtimol: float
sinf: int
versiya: str
def ilova_yasa(tuzatilgan, C=1.0, sxema=Mijoz):
Partiya = create_model("Partiya", mijozlar=(list[sxema],
Field(min_length=1, max_length=100)))
@asynccontextmanager
async def lifespan(app):
app.state.predictor = Predictor(C)
yield
app = FastAPI(lifespan=lifespan)
if tuzatilgan:
@app.exception_handler(RequestValidationError)
async def validatsiya_xatosi(request, exc):
return JSONResponse(status_code=422, content={"detail": [
{"loc": list(e["loc"]), "type": e["type"], "msg": e["msg"]}
for e in exc.errors()]})
def pr_ol(request: Request):
return request.app.state.predictor
def hisobla(pr, mijozlar):
df = pd.DataFrame([m.model_dump(mode="json") for m in mijozlar])
return [Bashorat(ehtimol=round(float(p), 4), sinf=int(p >= pr.chegara),
versiya=pr.versiya) for p in pr.predict_proba(df)]
@app.post("/predict", response_model=Bashorat)
def predict(mijoz: sxema, pr=Depends(pr_ol)):
return hisobla(pr, [mijoz])[0]
@app.post("/predict_batch")
def predict_batch(partiya: Partiya, pr=Depends(pr_ol)):
return {"natijalar": hisobla(pr, partiya.mijozlar)}
return app
MIJOZ = {"oylar": 3, "tolov": 95.5, "tarif": "oylik", "hudud": "Buxoro"}
GOLDEN_KIRISH = [MIJOZ, {"oylar": 60, "tolov": 30, "tarif": "yillik"},
{"oylar": 12, "tolov": 70, "tarif": "oylik", "hudud": "Andijon"},
{"oylar": 0, "tolov": 150, "tarif": "oylik"},
{"oylar": 36, "tolov": 55.5, "tarif": "yillik", "hudud": "Toshkent"}]
def kontrakt_surati(app):
s = app.openapi()["components"]["schemas"]["Mijoz" if "Mijoz" in
app.openapi()["components"]["schemas"]
else "MijozV2"]
return {"majburiy": sorted(s["required"]),
"turlar": {k: v.get("type", "ref") for k, v in s["properties"].items()}}
def json_post(k, yol, matn):
return k.post(yol, content=matn, headers={"content-type": "application/json"})
# ---------------- TESTLAR ----------------
def test_ehtimol_oraligi(k, ctx):
p = k.app.state.predictor.predict_proba(malumot(200, seed=5))
assert ((p >= 0) & (p <= 1)).all(), "ehtimol [0, 1] dan tashqarida"
def test_sinf_chegarasi(k, ctx):
j = k.post("/predict", json=MIJOZ).json()
assert j["sinf"] == int(j["ehtimol"] >= 0.35), "sinf chegaraga mos emas"
def test_endpointlar(k, ctx):
yollar = {(y, m) for y, a in k.app.openapi()["paths"].items() for m in a}
assert yollar == {("/predict", "post"), ("/predict_batch", "post")}
def test_javob_shakli(k, ctx):
j = k.post("/predict", json=MIJOZ).json()
assert sorted(j) == ["ehtimol", "sinf", "versiya"], f"kalitlar: {sorted(j)}"
assert isinstance(j["ehtimol"], float) and isinstance(j["sinf"], int)
def test_orqaga_moslik(k, ctx):
eski, yangi = ctx["surat"], kontrakt_surati(k.app)
for maydon, tur in eski["turlar"].items():
assert yangi["turlar"].get(maydon) == tur, f"{maydon} o'zgardi"
qoshilgan = set(yangi["majburiy"]) - set(eski["majburiy"])
assert not qoshilgan, f"yangi majburiy: {qoshilgan}"
def test_golden(k, ctx):
for kirish, kutilgan in zip(GOLDEN_KIRISH, ctx["golden"]):
p = k.post("/predict", json=kirish).json()["ehtimol"]
assert abs(p - kutilgan) <= 1e-6, f"{p} != {kutilgan}"
def test_bosh_batch(k, ctx):
assert k.post("/predict_batch", json={"mijozlar": []}).status_code == 422
def test_maks_batch(k, ctx):
r100 = k.post("/predict_batch", json={"mijozlar": [MIJOZ] * 100})
r101 = k.post("/predict_batch", json={"mijozlar": [MIJOZ] * 101})
assert (r100.status_code, r101.status_code) == (200, 422), \
f"{r100.status_code}, {r101.status_code}"
def test_nan(k, ctx):
r = json_post(k, "/predict", '{"oylar": 3, "tolov": NaN, "tarif": "oylik"}')
assert r.status_code == 422, f"status {r.status_code}"
def test_cheksiz(k, ctx):
r = json_post(k, "/predict", '{"oylar": 3, "tolov": 1e400, "tarif": "oylik"}')
assert r.status_code == 422, f"status {r.status_code}"
def test_juda_katta(k, ctx):
r = k.post("/predict", json={**MIJOZ, "tolov": 1e308})
assert r.status_code == 422, f"status {r.status_code}"
def test_nomalum_tarif(k, ctx):
assert k.post("/predict", json={**MIJOZ, "tarif": "haftalik"}).status_code == 422
def test_nomalum_hudud(k, ctx):
r = k.post("/predict", json={**MIJOZ, "hudud": "Xiva"})
assert r.status_code == 200 and 0 <= r.json()["ehtimol"] <= 1
def test_nomalum_maydon(k, ctx):
assert k.post("/predict", json={**MIJOZ, "region": "X"}).status_code == 422
def test_batch_yakka_mos(k, ctx):
rng = np.random.default_rng(3)
ms = [{"oylar": int(rng.integers(0, 72)), "tolov": float(rng.uniform(10, 150)),
"tarif": str(rng.choice(["oylik", "yillik"]))} for _ in range(30)]
b = k.post("/predict_batch", json={"mijozlar": ms}).json()["natijalar"]
y = [k.post("/predict", json=m).json() for m in ms]
assert all(a == c for a, c in zip(b, y)), "batch va yakka farq qiladi"
TESTLAR = [("birlik", test_ehtimol_oraligi), ("birlik", test_sinf_chegarasi),
("kontrakt", test_endpointlar), ("kontrakt", test_javob_shakli),
("kontrakt", test_orqaga_moslik), ("golden", test_golden),
("chegara", test_bosh_batch), ("chegara", test_maks_batch),
("chegara", test_nan), ("chegara", test_cheksiz),
("chegara", test_juda_katta), ("chegara", test_nomalum_tarif),
("chegara", test_nomalum_hudud), ("chegara", test_nomalum_maydon),
("moslik", test_batch_yakka_mos)]
def toplamni_ishlat(app, ctx):
natija = {}
with TestClient(app, raise_server_exceptions=False) as k:
for _, test in TESTLAR:
try:
test(k, ctx)
natija[test.__name__] = ("ok", "")
except AssertionError as xato:
natija[test.__name__] = ("YIQ", str(xato)[:22])
return natija
def main() -> None:
reliz = ilova_yasa(tuzatilgan=True)
with TestClient(reliz) as k:
ctx = {"surat": kontrakt_surati(reliz),
"golden": [k.post("/predict", json=m).json()["ehtimol"]
for m in GOLDEN_KIRISH]}
print("=== 1. Reliz paytida saqlangan artefaktlar ===")
print(f" kontrakt surati: majburiy {ctx['surat']['majburiy']}")
print(f" golden ehtimollar: {ctx['golden']}")
variantlar = {
"A: standart 422": ilova_yasa(tuzatilgan=False),
"B: tuzatilgan": ilova_yasa(tuzatilgan=True),
"C: B + yangi model": ilova_yasa(tuzatilgan=True, C=0.001, sxema=MijozV2),
}
jadval = {nom: toplamni_ishlat(app, ctx) for nom, app in variantlar.items()}
print("\n=== 2. Natija jadvali ===")
print(f" {'tur':<9}{'test':<23}{'A':<5}{'B':<5}{'C':<5}izoh")
for tur, test in TESTLAR:
n = test.__name__
holatlar = [jadval[v][n] for v in variantlar]
izoh = next((h[1] for h in holatlar if h[1]), "")
print(f" {tur:<9}{n[5:]:<23}" + "".join(f"{h[0]:<5}" for h in holatlar)
+ izoh)
print("\n=== 3. Xulosa ===")
for nom in variantlar:
yiq = [n[5:] for n, (h, _) in jadval[nom].items() if h == "YIQ"]
print(f" {nom:<20} o'tdi {len(TESTLAR) - len(yiq)}/{len(TESTLAR)}"
f" yiqilgan: {yiq}")
with TestClient(variantlar["A: standart 422"],
raise_server_exceptions=False) as k:
r = json_post(k, "/predict", '{"oylar": 3, "tolov": NaN, "tarif": "oylik"}')
print(f" A da NaN javobi: {r.status_code} {r.text}")
print(" ⭐ Chegara holatlar testi 'hech kim o'ylamagan' xatoni topdi")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Reliz paytida saqlangan artefaktlar ===
kontrakt surati: majburiy ['oylar', 'tarif', 'tolov']
golden ehtimollar: [0.7148, 0.0085, 0.4767, 0.9367, 0.044]
=== 2. Natija jadvali ===
tur test A B C izoh
birlik ehtimol_oraligi ok ok ok
birlik sinf_chegarasi ok ok ok
kontrakt endpointlar ok ok ok
kontrakt javob_shakli ok ok ok
kontrakt orqaga_moslik ok ok ok
golden golden ok ok YIQ 0.2529 != 0.7148
chegara bosh_batch ok ok ok
chegara maks_batch ok ok ok
chegara nan YIQ ok ok status 500
chegara cheksiz YIQ ok ok status 500
chegara juda_katta ok ok ok
chegara nomalum_tarif ok ok ok
chegara nomalum_hudud ok ok ok
chegara nomalum_maydon ok ok ok
moslik batch_yakka_mos ok ok ok
=== 3. Xulosa ===
A: standart 422 o'tdi 13/15 yiqilgan: ['nan', 'cheksiz']
B: tuzatilgan o'tdi 15/15 yiqilgan: []
C: B + yangi model o'tdi 14/15 yiqilgan: ['golden']
A da NaN javobi: 500 Internal Server Error
⭐ Chegara holatlar testi 'hech kim o'ylamagan' xatoni topdiNatija tahlili. 1-bo'limda reliz paytida ikkita artefakt saqlandi: kontrakt surati (uchta majburiy maydon) va beshta golden ehtimol. Keyin bir xil 15 test uchta ilova variantida ishlatildi. 2-bo'lim jadvali darsning asosiy natijasi. A varianti (FastAPI ning standart validatsiya ishlovchisi) 15 testdan 13 tasidan o'tdi. Yiqilgan ikkitasi — NaN va cheksiz (1e400) — 422 o'rniga 500 qaytardi: pydantic qiymatni to'g'ri rad etdi, lekin standart ishlovchi uni xato javobiga input sifatida qo'shdi va NaN/inf ni JSON ga o'girib bo'lmagani uchun javob yasash yiqildi. Bu kirish qismidagi real vaziyatning aynan o'zi — va uni faqat chegara holatlar testi topdi: "baxtli yo'l" testlarining hammasi yashil edi. 1e308 esa chekli son, u xato javobiga sig'adi va A da ham to'g'ri 422 oldi. B varianti (loc, type, msg qaytaradigan o'z ishlovchimiz) 15/15. C varianti — B + boshqacha o'qitilgan model (C=0.001) + yangi ixtiyoriy kanal maydoni. Kontrakt testlari o'tdi: ixtiyoriy maydon qo'shilishi orqaga mos, eski maydonlar va majburiylar o'sha. Faqat golden test yiqildi: birinchi mijoz uchun 0.2529 va saqlangan 0.7148. Boshqa testlar (ehtimol oralig'i, sinf chegarasi, batch/yakka mosligi) "yangi model" da ham o'tdi — ular modelning ichki izchilligini tekshiradi, golden esa natijaning o'zini. Kod o'zgarmagan bo'lsa-yu golden yiqilsa — regressiya; model ongli ravishda almashtirilgan bo'lsa — golden fayl sharh bilan yangilanadi.
Misol 2 — Ishonchlilik: timeout, zaxira, kesh, idempotentlik, cheklovlar
"""Virtual soat bilan timeout + zaxira + kesh; FastAPI da idempotentlik va 413."""
import hashlib
import json
import warnings
from collections import OrderedDict
import numpy as np
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from fastapi.testclient import TestClient
BYUDJET_MS = 100.0
VERSIYA = "1.2.0"
class VirtualSoat:
def __init__(self):
self.ms = 0.0
def otkaz(self, ms):
self.ms += ms
def asosiy_model(m):
z = -1 - 0.04 * m["oylar"] + 0.03 * (m["tolov"] - 60) + (
1.0 if m["tarif"] == "oylik" else -0.5)
return 1 / (1 + np.exp(-z))
def zaxira_model(m):
"""Oddiy qoida: tez, oldindan ma'lum, sifati pastroq."""
return 0.6 if (m["tarif"] == "oylik" and m["oylar"] < 12) else 0.2
class Kesh:
def __init__(self, hajm):
self.hajm, self.d = hajm, OrderedDict()
self.topildi = self.topilmadi = 0
@staticmethod
def kalit(m):
return hashlib.sha256((json.dumps(m, sort_keys=True) + VERSIYA)
.encode("utf-8")).hexdigest()
def ol(self, m):
k = self.kalit(m)
if k in self.d:
self.d.move_to_end(k)
self.topildi += 1
return self.d[k]
self.topilmadi += 1
return None
def qoy(self, m, qiymat):
self.d[self.kalit(m)] = qiymat
self.d.move_to_end(self.kalit(m))
if len(self.d) > self.hajm:
self.d.popitem(last=False)
def xizmat(sorovlar, sekin, timeout, kesh):
"""Har so'rov uchun (ehtimol, manba, kechikish_ms) - virtual vaqtda."""
natijalar = []
for i, m in enumerate(sorovlar):
soat = VirtualSoat()
soat.otkaz(1.0) # parslash, validatsiya
if kesh is not None and (p := kesh.ol(m)) is not None:
soat.otkaz(0.5)
natijalar.append((p, "kesh", soat.ms))
continue
model_vaqti = 8.0 + (280.0 if sekin[i] else 0.0)
if timeout and model_vaqti > BYUDJET_MS:
soat.otkaz(BYUDJET_MS + 0.2) # byudjet tugadi -> zaxira
natijalar.append((zaxira_model(m), "zaxira", soat.ms))
continue
soat.otkaz(model_vaqti)
p = asosiy_model(m)
if kesh is not None:
kesh.qoy(m, p)
natijalar.append((p, "model", soat.ms))
return natijalar
class Mijoz(BaseModel):
oylar: int = Field(ge=0, le=600)
tolov: float = Field(gt=0, le=1000)
tarif: str
class Kampaniya(BaseModel):
mijozlar: list[Mijoz] = Field(min_length=1, max_length=100)
MAKS_BAYT = 8192
app = FastAPI()
app.state.yuborilgan_sms = 0
app.state.idempotent = {}
@app.middleware("http")
async def tana_chegarasi(request: Request, call_next):
if int(request.headers.get("content-length") or 0) > MAKS_BAYT:
return JSONResponse(status_code=413,
content={"detail": f"tana {MAKS_BAYT} baytdan katta"})
return await call_next(request)
@app.post("/kampaniya")
def kampaniya(k: Kampaniya, idempotency_key: str | None = Header(default=None)):
tana_xeshi = hashlib.sha256(k.model_dump_json().encode()).hexdigest()
if idempotency_key is not None and idempotency_key in app.state.idempotent:
eski_xesh, eski_javob = app.state.idempotent[idempotency_key]
if eski_xesh != tana_xeshi:
raise HTTPException(status_code=409,
detail="kalit boshqa tana bilan ishlatilgan")
return {**eski_javob, "takror": True}
tanlangan = sum(asosiy_model(m.model_dump()) >= 0.35 for m in k.mijozlar)
app.state.yuborilgan_sms += int(tanlangan) # YON TA'SIR
javob = {"sms_yuborildi": int(tanlangan), "takror": False}
if idempotency_key is not None:
app.state.idempotent[idempotency_key] = (tana_xeshi, javob)
return javob
def persentil(qiymatlar, q):
return float(np.percentile(qiymatlar, q))
def main() -> None:
rng = np.random.default_rng(7)
hovuz = [{"oylar": int(rng.integers(0, 72)),
"tolov": round(float(rng.uniform(10, 150)), 1),
"tarif": str(rng.choice(["oylik", "yillik"]))} for _ in range(150)]
ogirlik = 1 / np.arange(1, 151) # mashhurlar ko'p takrorlanadi
tanlov = rng.choice(150, size=600, p=ogirlik / ogirlik.sum())
sorovlar = [hovuz[i] for i in tanlov]
sekin = rng.random(600) < 0.06 # 6% so'rovda model "o'ylab qoladi"
print("=== 1. Uch konfiguratsiya: bir xil 600 so'rov ===")
konfiglar = {"oddiy": (False, None), "timeout+zaxira": (True, None),
"timeout+zaxira+kesh": (True, Kesh(64))}
print(f" {'konfiguratsiya':<21}{'p50':>6}{'p95':>7}{'p99':>7}{'max':>7}"
f" manbalar")
natijalar = {}
for nom, (timeout, kesh) in konfiglar.items():
n = xizmat(sorovlar, sekin, timeout, kesh)
natijalar[nom] = n
kech = [x[2] for x in n]
manba = {m: sum(x[1] == m for x in n) for m in ["model", "zaxira", "kesh"]}
print(f" {nom:<21}{persentil(kech, 50):6.1f}{persentil(kech, 95):7.1f}"
f"{persentil(kech, 99):7.1f}{max(kech):7.1f} {manba}")
print("\n=== 2. Zaxiraning narxi: sifat ===")
asl = np.array([asosiy_model(m) for m in sorovlar])
for nom in ["timeout+zaxira", "timeout+zaxira+kesh"]:
p = np.array([x[0] for x in natijalar[nom]])
zaxira = np.array([x[1] == "zaxira" for x in natijalar[nom]])
print(f" {nom:<21} zaxira javoblarda |p - asl| o'rtacha "
f"{np.abs(p - asl)[zaxira].mean():.3f}, qolganlarida "
f"{np.abs(p - asl)[~zaxira].max():.1e}")
kesh = konfiglar["timeout+zaxira+kesh"][1]
print(f" kesh: topildi {kesh.topildi}, topilmadi {kesh.topilmadi}, "
f"ulush {kesh.topildi / (kesh.topildi + kesh.topilmadi):.1%}")
print("\n=== 3. Kesh kaliti: tartib emas, mazmun va versiya ===")
a = {"oylar": 3, "tolov": 95.5, "tarif": "oylik"}
b = {"tarif": "oylik", "tolov": 95.5, "oylar": 3}
print(f" tartib boshqa, mazmun o'sha: kalitlar teng {Kesh.kalit(a) == Kesh.kalit(b)}")
k = TestClient(app)
tana = {"mijozlar": [{"oylar": 2, "tolov": 120, "tarif": "oylik"},
{"oylar": 50, "tolov": 20, "tarif": "yillik"},
{"oylar": 5, "tolov": 90, "tarif": "oylik"}]}
print("\n=== 4. Idempotentlik: tarmoq uzildi, mijoz qayta yubordi ===")
for kalit in [None, "kamp-001"]:
app.state.yuborilgan_sms = 0
sarlavha = {"Idempotency-Key": kalit} if kalit else {}
javoblar = [k.post("/kampaniya", json=tana, headers=sarlavha).json()
for _ in range(3)]
print(f" kalit={str(kalit):<9} 3 urinish -> jami SMS: "
f"{app.state.yuborilgan_sms}, takror: {[j['takror'] for j in javoblar]}")
boshqa = {"mijozlar": tana["mijozlar"][:1]}
r = k.post("/kampaniya", json=boshqa, headers={"Idempotency-Key": "kamp-001"})
print(f" o'sha kalit, boshqa tana: {r.status_code} {r.json()['detail']}")
print("\n=== 5. Kiruvchi so'rov chegaralari ===")
bitta = tana["mijozlar"][0]
for soni in [3, 100, 101, 300]:
r = k.post("/kampaniya", json={"mijozlar": [bitta] * soni})
d = r.json().get("detail", "")
izoh = d[0]["type"] if isinstance(d, list) else d
print(f" {soni:>3} mijoz, tana {len(r.request.content):>5} bayt -> "
f"{r.status_code} {izoh}")
print(" ⭐ Byudjet + zaxira dumni kesadi; idempotentlik - bitta SMS")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Uch konfiguratsiya: bir xil 600 so'rov ===
konfiguratsiya p50 p95 p99 max manbalar
oddiy 9.0 289.0 289.0 289.0 {'model': 600, 'zaxira': 0, 'kesh': 0}
timeout+zaxira 9.0 101.2 101.2 101.2 {'model': 567, 'zaxira': 33, 'kesh': 0}
timeout+zaxira+kesh 1.5 9.0 101.2 101.2 {'model': 140, 'zaxira': 13, 'kesh': 447}
=== 2. Zaxiraning narxi: sifat ===
timeout+zaxira zaxira javoblarda |p - asl| o'rtacha 0.133, qolganlarida 0.0e+00
timeout+zaxira+kesh zaxira javoblarda |p - asl| o'rtacha 0.124, qolganlarida 0.0e+00
kesh: topildi 447, topilmadi 153, ulush 74.5%
=== 3. Kesh kaliti: tartib emas, mazmun va versiya ===
tartib boshqa, mazmun o'sha: kalitlar teng True
=== 4. Idempotentlik: tarmoq uzildi, mijoz qayta yubordi ===
kalit=None 3 urinish -> jami SMS: 6, takror: [False, False, False]
kalit=kamp-001 3 urinish -> jami SMS: 2, takror: [False, True, True]
o'sha kalit, boshqa tana: 409 kalit boshqa tana bilan ishlatilgan
=== 5. Kiruvchi so'rov chegaralari ===
3 mijoz, tana 134 bayt -> 200
100 mijoz, tana 4014 bayt -> 200
101 mijoz, tana 4054 bayt -> 422 too_long
300 mijoz, tana 12014 bayt -> 413 tana 8192 baytdan katta
⭐ Byudjet + zaxira dumni kesadi; idempotentlik - bitta SMSNatija tahlili. 1-bo'limda bir xil 600 so'rov (150 mijozlik hovuzdan, mashhurlari ko'proq takrorlanadi; 6% so'rovda model 280 ms "o'ylab qoladi") uch konfiguratsiyada virtual vaqtda o'tkazildi. Oddiy xizmatda p50 9.0 ms, lekin p95, p99 va max — 289.0 ms: sekin so'rovlar dumni butunlay egallaydi. Timeout + zaxira dumni byudjetga kesdi: 33 so'rov (5.5%) zaxira javob oldi va eng uzoq kechikish 101.2 ms. Kesh qo'shilganda 447 so'rov (74.5%) keshdan javob oldi, p50 1.5 ms ga, p95 esa 9.0 ms ga tushdi. Modelga faqat 140 marta murojaat qilindi, zaxira ham kamaydi (13 ta) — sekin so'rovlarning bir qismi keshga tushgan mijozlarga to'g'ri keldi. 2-bo'lim bu tezlikning narxini halol ko'rsatadi: zaxira javoblar asosiy modeldan o'rtacha 0.133 (0.124) ga farq qiladi. Qolgan javoblar (model va kesh) aynan asl qiymatlar, ya'ni kesh aniqlikni yo'qotmaydi. Zaxira ulushi monitoringda kuzatiladigan metrika bo'lishi kerak. 3-bo'limda kesh kaliti kanonik — maydonlar tartibi kalitga ta'sir qilmadi. 4-bo'lim idempotentlikni ko'rsatadi. Kalitsiz uchta urinish (javob "yo'qolgan", mijoz qayta yuborgan) 6 ta SMS yubordi — har urinishda 2 tadan. Idempotency-Key bilan esa faqat 2 ta ketdi: ikkinchi va uchinchi urinishlar saqlangan javobni takror: True bilan oldi. O'sha kalit boshqa tana bilan kelganda — 409. 5-bo'limda cheklov qatlamlari o'z joyida ishladi: 100 mijoz — 200, 101 — pydantic 422 too_long, 300 mijozlik 12014 baytli tana esa 413 oldi. Bu javob middleware da, JSON parslanishi va validatsiyadan oldin berildi, ya'ni katta tana xizmat resurslarini umuman ishlatmadi.
Misol 3 — Strukturali loglar va API kaliti
"""JSON loglar (sorov_id, niqoblash) va muhitdan olinadigan API kalitlari."""
import hmac
import itertools
import json
import logging
import os
import re
import secrets
import sys
import warnings
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from pydantic import BaseModel, Field
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from fastapi.testclient import TestClient
class JsonFormat(logging.Formatter):
def format(self, record):
yozuv = {"daraja": record.levelname, "xabar": record.getMessage()}
yozuv.update(getattr(record, "maydonlar", {}))
return json.dumps(yozuv, ensure_ascii=False, sort_keys=True)
LOG = logging.getLogger("model_api")
LOG.setLevel(logging.INFO)
LOG.propagate = False
_chiqish = logging.StreamHandler(sys.stdout)
_chiqish.setFormatter(JsonFormat())
LOG.addHandler(_chiqish)
XOTIRA = [] # tahlil uchun nusxa
class XotiraHandler(logging.Handler):
def emit(self, record):
XOTIRA.append(json.loads(self.format(record)))
_xotira = XotiraHandler()
_xotira.setFormatter(JsonFormat())
LOG.addHandler(_xotira)
def telefon_niqobla(t):
return re.sub(r"^(\+998)\d{5}(\d{4})$", r"\1*****\2", t) if t else None
def email_niqobla(e):
if not e or "@" not in e:
return None
nom, domen = e.split("@", 1)
return f"{nom[0]}***@{domen}"
class Mijoz(BaseModel):
oylar: int = Field(ge=0, le=600)
tarif: str
telefon: str | None = Field(default=None, pattern=r"^\+998\d{9}$")
email: str | None = None
SANOQ = itertools.count(1)
SOAT = {"ms": 0.0}
@asynccontextmanager
async def lifespan(app):
matn = os.environ.get("MODEL_API_KALITLARI")
if not matn:
raise RuntimeError("MODEL_API_KALITLARI o'rnatilmagan")
# format: "egasi:rol:kalit,egasi:rol:kalit"
app.state.kalitlar = [tuple(q.split(":", 2)) for q in matn.split(",")]
LOG.info("ishga tushdi", extra={"maydonlar": {
"kalitlar_soni": len(app.state.kalitlar)}})
yield
app = FastAPI(lifespan=lifespan)
def kalit_tekshir(request: Request, x_api_key: str | None = Header(default=None)):
if x_api_key is None:
raise HTTPException(status_code=401, detail="API kaliti kerak",
headers={"WWW-Authenticate": "ApiKey"})
for egasi, rol, kalit in request.app.state.kalitlar:
if hmac.compare_digest(x_api_key.encode(), kalit.encode()):
request.state.egasi = egasi
return rol
raise HTTPException(status_code=403, detail="kalit yaroqsiz")
def admin_kerak(rol: str = Depends(kalit_tekshir)):
if rol != "admin":
raise HTTPException(status_code=403, detail="admin roli kerak")
@app.middleware("http")
async def sorov_logi(request: Request, call_next):
sid = request.headers.get("x-request-id") or f"req-{next(SANOQ):04d}"
request.state.sorov_id = sid
boshlanish = SOAT["ms"]
javob = await call_next(request)
javob.headers["X-Request-ID"] = sid
daraja = (logging.ERROR if javob.status_code >= 500 else
logging.WARNING if javob.status_code >= 400 else logging.INFO)
LOG.log(daraja, "sorov", extra={"maydonlar": {
"sorov_id": sid, "metod": request.method, "yol": request.url.path,
"status": javob.status_code,
"kechikish_ms": round(SOAT["ms"] - boshlanish, 1),
"egasi": getattr(request.state, "egasi", None)}})
return javob
@app.post("/predict")
def predict(m: Mijoz, request: Request, rol: str = Depends(kalit_tekshir)):
SOAT["ms"] += 12.0 # virtual model vaqti
p = 0.72 if m.tarif == "oylik" and m.oylar < 12 else 0.18
LOG.info("bashorat", extra={"maydonlar": {
"sorov_id": request.state.sorov_id, "ehtimol": p,
"telefon": telefon_niqobla(m.telefon), "email": email_niqobla(m.email)}})
return {"ehtimol": p}
@app.post("/admin/qayta_yukla", dependencies=[Depends(admin_kerak)])
def qayta_yukla():
return {"holat": "qayta yuklandi"}
def main() -> None:
print("=== 1. Kalitsiz muhit: ilova ishga tushmaydi ===")
os.environ.pop("MODEL_API_KALITLARI", None)
try:
with TestClient(app):
pass
except RuntimeError as xato:
print(f" RuntimeError: {xato}")
print("\n=== 2. Kalitlar muhitdan (kodda emas) ===")
crm, admin = secrets.token_hex(16), secrets.token_hex(16)
os.environ["MODEL_API_KALITLARI"] = f"crm:oqish:{crm},ops:admin:{admin}"
print(f" kalitlar yaratildi: 2 ta, har biri {len(crm)} belgi (qiymatlari "
f"chop etilmaydi)")
with TestClient(app) as k:
print("\n=== 3. So'rovlar (har biri stdout ga bitta JSON log) ===")
mijoz = {"oylar": 3, "tarif": "oylik", "telefon": "+998901234567",
"email": "ali.valiyev@misol.uz"}
holatlar = [
("kalitsiz", {}, "/predict"),
("noto'g'ri kalit", {"X-API-Key": "a" * 32}, "/predict"),
("crm kaliti", {"X-API-Key": crm, "X-Request-ID": "mobil-77"},
"/predict"),
("crm -> admin", {"X-API-Key": crm}, "/admin/qayta_yukla"),
("ops -> admin", {"X-API-Key": admin}, "/admin/qayta_yukla"),
]
natijalar = []
for nom, sarlavha, yol in holatlar:
r = k.post(yol, json=mijoz if yol == "/predict" else None,
headers=sarlavha)
natijalar.append((nom, r.status_code, r.json().get("detail", "ok"),
r.headers.get("x-request-id"),
r.headers.get("www-authenticate")))
print("\n=== 4. Javoblar ===")
for nom, kod, izoh, sid, www in natijalar:
print(f" {nom:<16} {kod} {izoh:<18} sorov_id={sid}"
f"{' WWW-Authenticate=' + www if www else ''}")
print("\n=== 5. Loglarni tahlil qilish ===")
darajalar = {}
for y in XOTIRA:
darajalar[y["daraja"]] = darajalar.get(y["daraja"], 0) + 1
print(f" jami yozuvlar: {len(XOTIRA)}, darajalar: {dict(sorted(darajalar.items()))}")
mobil = [y["xabar"] for y in XOTIRA if y.get("sorov_id") == "mobil-77"]
print(f" sorov_id=mobil-77 yozuvlari: {mobil}")
hammasi = json.dumps(XOTIRA, ensure_ascii=False)
print(f" logda ochiq telefon bormi: {'901234567' in hammasi}")
print(f" logda ochiq email bormi: {'ali.valiyev' in hammasi}")
print(f" logda API kaliti bormi: {crm in hammasi or admin in hammasi}")
print(" ⭐ Log - mashina o'qiydigan JSON; sirlar va shaxsiy ma'lumot yo'q")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Kalitsiz muhit: ilova ishga tushmaydi ===
RuntimeError: MODEL_API_KALITLARI o'rnatilmagan
=== 2. Kalitlar muhitdan (kodda emas) ===
kalitlar yaratildi: 2 ta, har biri 32 belgi (qiymatlari chop etilmaydi)
{"daraja": "INFO", "kalitlar_soni": 2, "xabar": "ishga tushdi"}
=== 3. So'rovlar (har biri stdout ga bitta JSON log) ===
{"daraja": "WARNING", "egasi": null, "kechikish_ms": 0.0, "metod": "POST", "sorov_id": "req-0001", "status": 401, "xabar": "sorov", "yol": "/predict"}
{"daraja": "WARNING", "egasi": null, "kechikish_ms": 0.0, "metod": "POST", "sorov_id": "req-0002", "status": 403, "xabar": "sorov", "yol": "/predict"}
{"daraja": "INFO", "ehtimol": 0.72, "email": "a***@misol.uz", "sorov_id": "mobil-77", "telefon": "+998*****4567", "xabar": "bashorat"}
{"daraja": "INFO", "egasi": "crm", "kechikish_ms": 12.0, "metod": "POST", "sorov_id": "mobil-77", "status": 200, "xabar": "sorov", "yol": "/predict"}
{"daraja": "WARNING", "egasi": "crm", "kechikish_ms": 0.0, "metod": "POST", "sorov_id": "req-0003", "status": 403, "xabar": "sorov", "yol": "/admin/qayta_yukla"}
{"daraja": "INFO", "egasi": "ops", "kechikish_ms": 0.0, "metod": "POST", "sorov_id": "req-0004", "status": 200, "xabar": "sorov", "yol": "/admin/qayta_yukla"}
=== 4. Javoblar ===
kalitsiz 401 API kaliti kerak sorov_id=req-0001 WWW-Authenticate=ApiKey
noto'g'ri kalit 403 kalit yaroqsiz sorov_id=req-0002
crm kaliti 200 ok sorov_id=mobil-77
crm -> admin 403 admin roli kerak sorov_id=req-0003
ops -> admin 200 ok sorov_id=req-0004
=== 5. Loglarni tahlil qilish ===
jami yozuvlar: 7, darajalar: {'INFO': 4, 'WARNING': 3}
sorov_id=mobil-77 yozuvlari: ['bashorat', 'sorov']
logda ochiq telefon bormi: False
logda ochiq email bormi: False
logda API kaliti bormi: False
⭐ Log - mashina o'qiydigan JSON; sirlar va shaxsiy ma'lumot yo'qNatija tahlili. 1-bo'limda muhitda kalitlar yo'q edi — lifespan RuntimeError bilan ilovani ishga tushirmadi. Kalitsiz "ochiq" ishlaydigan xizmatdan ko'ra, umuman ishlamaydigan xizmat xavfsizroq. 2-bo'limda kalitlar ish vaqtida secrets.token_hex bilan yaratildi va muhit o'zgaruvchisiga yozildi; kodda ham, chiqishda ham ularning qiymati yo'q. Haqiqiy tizimda ularni sir boshqaruvchisi beradi 27.10-bob. Ishga tushish logida faqat kalitlar soni bor. 3-bo'limda har so'rov stdout ga bitta JSON qator yozdi: sorov_id, yo'l, status, virtual kechikish va kalit egasining nomi (crm, ops) — kalitning o'zi emas. Status bo'yicha daraja avtomatik tanlandi: 401/403 — WARNING, 200 — INFO. Bashorat logida telefon +998*****4567, email a***@misol.uz ko'rinishida niqoblangan. Mijoz o'z X-Request-ID sini (mobil-77) yuborganda xizmat aynan shu id ni ishlatdi va javob sarlavhasida qaytardi. Shu tufayli mobil ilova logidagi yozuvni server logi bilan bog'lash mumkin. 4-bo'lim 401 va 403 farqini aniq ko'rsatadi. Kalitsiz so'rov 401 va WWW-Authenticate sarlavhasini oldi ("o'zingizni tanishtiring"). Noto'g'ri kalit — 403. To'g'ri kalit, lekin yetarsiz rol (crm -> /admin) — ham 403. 5-bo'limda loglar dastur bilan tahlil qilindi: darajalar bo'yicha sanash, mobil-77 ning barcha yozuvlarini topish (bashorat va sorov) va avtomatik tekshiruv — loglarda ochiq telefon, email yoki API kaliti yo'q. Bunday tekshiruvni CI ga test sifatida qo'yish mumkin.
Misol 4 — Rate limiting va yuklama simulyatsiyasi
"""Token chelak (virtual soat), FastAPI da 429, navbat modeli bilan yuklama sinovi."""
import heapq
import math
import warnings
import numpy as np
from fastapi import Depends, FastAPI, Header, HTTPException
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from fastapi.testclient import TestClient
class TokenChelak:
def __init__(self, sigim, tezlik):
self.sigim, self.tezlik = sigim, tezlik
self.tokenlar, self.oxirgi = float(sigim), 0.0
def ol(self, hozir):
self.tokenlar = min(self.sigim,
self.tokenlar + (hozir - self.oxirgi) * self.tezlik)
self.oxirgi = hozir
if self.tokenlar >= 1:
self.tokenlar -= 1
return True, 0.0
return False, (1 - self.tokenlar) / self.tezlik
SOAT = {"s": 0.0}
CHELAKLAR = {}
app = FastAPI()
def tezlik_chegarasi(x_api_key: str = Header()):
chelak = CHELAKLAR.setdefault(x_api_key, TokenChelak(sigim=10, tezlik=5))
ruxsat, kutish = chelak.ol(SOAT["s"])
if not ruxsat:
raise HTTPException(status_code=429, detail="juda ko'p so'rov",
headers={"Retry-After": str(math.ceil(kutish))})
@app.post("/predict", dependencies=[Depends(tezlik_chegarasi)])
def predict():
return {"ehtimol": 0.42}
def navbat_simulyatsiyasi(lam, c, n=20000, seed=0, chelak=None):
"""M/G/c navbati: Puasson kelishlar, lognormal xizmat (o'rtacha 20 ms)."""
rng = np.random.default_rng(seed)
kelish = np.cumsum(rng.exponential(1 / lam, n))
xizmat = rng.lognormal(np.log(0.020) - 0.5 ** 2 / 2, 0.5, n)
bosh = [0.0] * c
kechikish, rad = [], 0
for t, s in zip(kelish, xizmat):
if chelak is not None and not chelak.ol(t)[0]:
rad += 1
continue
erta = heapq.heappop(bosh)
tugash = max(t, erta) + s
heapq.heappush(bosh, tugash)
kechikish.append(tugash - t)
k = np.array(kechikish) * 1000
davom = kelish[-1] - kelish[0]
return {"throughput": len(k) / davom, "p50": np.percentile(k, 50),
"p95": np.percentile(k, 95), "p99": np.percentile(k, 99),
"rad": rad / n}
def main() -> None:
print("=== 1. Token chelak: sig'im 10, tezlik 5 token/s ===")
ch = TokenChelak(sigim=10, tezlik=5)
reja = [(0.0, 15), (1.0, 7), (3.0, 12)]
for t, soni in reja:
natija = [ch.ol(t) for _ in range(soni)]
ruxsat = sum(r for r, _ in natija)
kutish = max(k for _, k in natija)
print(f" t={t:.1f}s: {soni:>2} so'rov -> ruxsat {ruxsat:>2}, rad "
f"{soni - ruxsat:>2}, eng uzoq Retry-After {kutish:.2f}s")
ch2 = TokenChelak(sigim=10, tezlik=5)
ruxsat = sum(ch2.ol(i / 8)[0] for i in range(80))
print(f" 10 s davomida 8 so'rov/s: ruxsat {ruxsat} / 80 "
f"(= 10 sig'im + 5 x 10 s ga yaqin)")
print("\n=== 2. FastAPI: har kalitga alohida chelak, 429 ===")
k = TestClient(app)
for kalit, soni in [("crm", 12), ("mobil", 3)]:
kodlar = [k.post("/predict", headers={"X-API-Key": kalit}) for _ in range(soni)]
sanoq = {}
for r in kodlar:
sanoq[r.status_code] = sanoq.get(r.status_code, 0) + 1
oxirgi = kodlar[-1]
print(f" {kalit:<6} {soni} so'rov: {dict(sorted(sanoq.items()))}, oxirgisi "
f"{oxirgi.status_code} Retry-After={oxirgi.headers.get('retry-after')}")
SOAT["s"] = 2.0
r = k.post("/predict", headers={"X-API-Key": "crm"})
print(f" crm, 2 s dan keyin: {r.status_code} {r.json()}")
print("\n=== 3. Yuklama: c=4 ishchi, E[S]=20 ms, sig'im 200 so'rov/s ===")
print(f" {'lambda':>6} {'rho':>5} {'throughput':>10} {'p50':>7} {'p95':>7} "
f"{'p99':>8}")
for lam in [60, 120, 160, 180, 190, 210]:
r = navbat_simulyatsiyasi(lam, c=4)
print(f" {lam:>6} {lam * 0.020 / 4:5.2f} {r['throughput']:10.1f} "
f"{r['p50']:7.1f} {r['p95']:7.1f} {r['p99']:8.1f}")
print("\n=== 4. lambda=210 da ikki yechim ===")
variantlar = {
"hech narsa (c=4)": dict(c=4),
"rate limit 170/s (c=4)": dict(c=4, chelak=TokenChelak(40, 170)),
"ko'proq ishchi (c=6)": dict(c=6),
}
for nom, kw in variantlar.items():
r = navbat_simulyatsiyasi(210, **kw)
print(f" {nom:<24} rad {r['rad']:6.1%} p95 {r['p95']:8.1f} ms "
f"p99 {r['p99']:8.1f} ms")
print(" ⭐ rho -> 1 da dum portlaydi; limit yoki sig'im uni tiyadi")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Token chelak: sig'im 10, tezlik 5 token/s ===
t=0.0s: 15 so'rov -> ruxsat 10, rad 5, eng uzoq Retry-After 0.20s
t=1.0s: 7 so'rov -> ruxsat 5, rad 2, eng uzoq Retry-After 0.20s
t=3.0s: 12 so'rov -> ruxsat 10, rad 2, eng uzoq Retry-After 0.20s
10 s davomida 8 so'rov/s: ruxsat 59 / 80 (= 10 sig'im + 5 x 10 s ga yaqin)
=== 2. FastAPI: har kalitga alohida chelak, 429 ===
crm 12 so'rov: {200: 10, 429: 2}, oxirgisi 429 Retry-After=1
mobil 3 so'rov: {200: 3}, oxirgisi 200 Retry-After=None
crm, 2 s dan keyin: 200 {'ehtimol': 0.42}
=== 3. Yuklama: c=4 ishchi, E[S]=20 ms, sig'im 200 so'rov/s ===
lambda rho throughput p50 p95 p99
60 0.30 60.4 17.9 40.6 58.0
120 0.60 120.9 19.8 45.8 64.4
160 0.80 161.2 25.6 66.2 91.6
180 0.90 181.3 37.0 106.2 150.2
190 0.95 191.4 61.7 187.0 225.2
210 1.05 211.5 2997.2 5617.0 5757.1
=== 4. lambda=210 da ikki yechim ===
hech narsa (c=4) rad 0.0% p95 5617.0 ms p99 5757.1 ms
rate limit 170/s (c=4) rad 19.4% p95 55.8 ms p99 83.4 ms
ko'proq ishchi (c=6) rad 0.0% p95 45.8 ms p99 63.9 ms
⭐ rho -> 1 da dum portlaydi; limit yoki sig'im uni tiyadiNatija tahlili. 1-bo'limda token chelak virtual vaqtda ishladi. t=0 da 15 so'rovli portlashdan sig'im bo'yicha 10 tasi o'tdi, 5 tasi rad etildi va Retry-After 0.20 s bo'ldi — tezlik 5 token/s bo'lgani uchun bitta token 0.2 s da to'ladi. 1 s dan keyin chelakda 5 token bor edi: 7 so'rovdan 5 tasi o'tdi. 2 s tanaffusdan keyin chelak yana to'liq (10) bo'ldi. Uzoq muddatda 8 so'rov/s tezlik bilan 10 s da 80 so'rovdan 59 tasi o'tdi — bu nazariy 10 + 5 x 10 = 60 ga deyarli teng: portlash sig'imgacha ruxsat etiladi, o'rtacha tezlik esa R dan oshmaydi. 2-bo'limda FastAPI da har kalitning o'z chelagi bor. crm 12 so'rovdan 10 tasini o'tkazdi va 2 tasiga 429 hamda Retry-After: 1 oldi. mobil esa bundan umuman ta'sirlanmadi (3/3). 2 virtual sekunddan keyin crm yana xizmat oldi. 3-bo'lim navbat nazariyasining asosiy saboqini raqam bilan ko'rsatadi. rho = 0.30 dan 0.60 gacha p99 deyarli o'zgarmadi (58.0 -> 64.4 ms). 0.80 da 91.6, 0.90 da 150.2, 0.95 da 225.2 ms. rho = 1.05 da (sig'imdan 5% ortiq trafik) navbat cheksiz o'sa boshladi va p50 ham 2997.2 ms ga sakradi. Throughput esa trafikni kuzatib boraverdi — ya'ni throughput ga qarab "hammasi yaxshi" deyish mumkin, kechikish esa allaqachon halokatli. 4-bo'limda lambda = 210 uchun ikki yechim solishtirildi. 170 so'rov/s rate limit so'rovlarning 19.4% ini 429 bilan rad etdi, lekin qabul qilinganlar uchun p99 ni 83.4 ms da ushladi. Ikki qo'shimcha ishchi (c = 6, rho = 0.70) esa hech kimni rad etmasdan p99 ni 63.9 ms ga tushirdi. Rate limit — himoya vositasi (bir mijozning ortiqcha trafigi hammaga zarar yetkazmasligi uchun), sig'im esa haqiqiy yechim. Ikkalasi birga kerak.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "Testlar yashil — API tayyor" | Faqat baxtli yo'l testlari NaN, bo'sh batch va cheksiz qiymatni ko'rmaydi |
"Golden test == bilan aniqroq" |
Mo'rt; tolerantlik javob aniqligiga mos bo'lsin |
| "Kontrakt testi — sxema bilan to'liq tenglik" | Orqaga moslik qoidalari: ixtiyoriy maydon qo'shish buzilish emas |
"Pydantic bor — 422 doim to'g'ri ishlaydi" |
Standart ishlovchi NaN da 500 beradi; o'z ishlovchingiz kerak |
| "Timeout — faqat mijoz tomonida" | Server ham byudjet va zaxira javobga ega bo'lishi kerak |
| "Kesh kaliti — kirish JSON i" | Kanonik JSON + model versiyasi; aks holda yangi modelga eski javob |
| "Qayta urinish zararsiz" | Yon ta'sirli endpointda idempotentlik kalitisiz — ikki SMS |
"Log — bu print" |
JSON, sorov_id, niqoblash; shaxsiy ma'lumot va kalit yo'q |
"401 va 403 — bir xil" |
401 — kimligi noma'lum; 403 — ruxsat yo'q / kalit yaroqsiz |
| "O'rtacha kechikish yaxshi — hammasi yaxshi" | p99 rho 1 ga yaqinlashganda portlaydi |
6. Keng tarqalgan xatolar va yechimlari
1. Standart 422 va NaN
app = FastAPI() # ⚠️ NaN -> 500
@app.exception_handler(RequestValidationError)
async def v(request, exc): # ✅ input siz
return JSONResponse(status_code=422, content={"detail": [
{"loc": list(e["loc"]), "type": e["type"], "msg": e["msg"]} for e in exc.errors()]})2. Mo'rt golden test
assert p == 0.8268 # ⚠️
assert abs(p - golden) <= 1e-6 # ✅3. Timeout siz model
p = model_chaqir(x) # ⚠️ 300 ms ham kutadi
try: p = await asyncio.wait_for(model_chaqir(x), timeout=0.1) # ✅
except TimeoutError: p = zaxira(x)4. Versiyasiz kesh kaliti
kalit = json.dumps(x) # ⚠️ tartib va versiya
kalit = sha256(json.dumps(x, sort_keys=True) + versiya) # ✅5. Shaxsiy ma'lumot logda
log.info(f"bashorat {m.telefon} {p}") # ⚠️
LOG.info("bashorat", extra={"maydonlar": {"telefon": telefon_niqobla(m.telefon)}}) # ✅6. Kalit kodda va == bilan
if x_api_key == "sk-12345": ... # ⚠️
if hmac.compare_digest(x_api_key.encode(), os.environ[...].encode()): ... # ✅7. Hamma uchun bitta limit
chelak = TokenChelak(100, 50) # ⚠️ bitta mijoz hammani to'sadi
CHELAKLAR.setdefault(x_api_key, TokenChelak(10, 5)) # ✅ kalit bo'yicha7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 18-qism (o'tilgan): persentillar va taqsimot dumlari — kechikishni o'lchash tili
- 25.6-dars (o'tilgan): LLM API — o'shanda mijoz sifatida retry/backoff/kesh; endi server tomonini qurdik
- 25.10-dars (o'tilgan): LLM regressiya testlari — golden testlarning boshqa ko'rinishi
- 27.6-dars (o'tilgan): nazorat namunalari — golden testlarning manbasi
- 27.7-dars (o'tilgan): FastAPI ilova — bu darsda uni mustahkamladik
- 27.9-dars: Docker bilan konteynerlash — stdout loglar va muhit o'zgaruvchilari konteynerda tabiiy
- 27.10-dars: Bulut va deploy strategiyalari — sirlar boshqaruvchisi, avtomasshtablash (
rhoni pastda ushlash) - 27.11-dars: Monitoring — log va metrikalardan (zaxira ulushi, p99, 4xx/5xx) ogohlantirishlar
8. Eng yaxshi amaliyotlar
Test to'plami besh qatlamli: birlik, kontrakt (orqaga moslik), golden (tolerantlik bilan), chegara holatlar, batch/yakka mosligi — har PR da.
O'z
422ishlovchingiz —loc,type,msg; kiruvchi qiymat qaytarilmaydi.Hamma kirish cheklangan: tana (
413), batch (422), maydon uzunligi, tezlik (429).Model chaqiruvi byudjet bilan, zaxira javob oldindan belgilangan va
manbabilan belgilanadi.Kesh kaliti — kanonik kirish + model versiyasi.
Yon ta'sirli endpointlarda idempotentlik kaliti (
409— boshqa tana).JSON loglar stdout ga:
sorov_idhar joyda, shaxsiy ma'lumot niqoblangan, kalitlar yo'q.Kalitlar muhitdan, yo'q bo'lsa ilova ishga tushmaydi;
compare_digest;401/403aniq.Rate limit har mijozga alohida,
Retry-Afterbilan.Sig'imni p95/p99 bo'yicha,
rho ~ 0.6-0.7ga rejalang, yuklama sinovini muntazam o'tkazing.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
1. # kontrakt testi nimani tekshiradi?
2. # yangi ixtiyoriy maydon - orqaga moslikni buzadimi?
3. # golden testni nega == bilan yozmaymiz?
4. # standart FastAPI da {"tolov": NaN} qaysi kod qaytaradi? tuzatilganda?
5. # tana 1 MB bo'lsa qaysi kod?
6. # timeout bo'lganda javobda nima bo'lishi kerak?
7. # kesh kalitiga nega model versiyasi kiradi?
8. # o'sha idempotentlik kaliti boshqa tana bilan kelsa?
9. # logda telefon qanday ko'rinadi?
10. # X-API-Key yo'q / noto'g'ri - qaysi kodlar?
11. # token chelak (sig'im 10, tezlik 5): t=0 da 15 so'rov - nechtasi o'tadi?
12. # rho = 0.95 da p99 ga nima bo'ladi?Javoblar
- API shartnomasi: endpointlar, javob shakli, eski maydonlar va majburiylar
- Yo'q
- Mayda suzuvchi nuqta farqlari testni mo'rt qiladi; tolerantlik aniqlikka mos
500; tuzatilganda422413- Zaxira javob va
"manba": "zaxira" - Yangi model chiqqanda eski javoblar ishlatilmasligi uchun
409 Conflict+998*****4567401/403- 10 ta
- Keskin o'sadi —
rho1 ga yaqin, navbat uzun
Vazifa 2: Xatolarni tuzating
1. assert r.json()["ehtimol"] == 0.8268
2. kalit = json.dumps(kirish)
3. API_KALIT = "sk-live-9f8e7d"
4. log.info(f"mijoz {m.email} ehtimol {p}")
5. mijozlar: list[Mijoz]Javoblar
1. assert abs(r.json()["ehtimol"] - golden) <= 1e-6
2. kalit = sha256((json.dumps(kirish, sort_keys=True) + versiya).encode()).hexdigest()
3. API_KALIT = os.environ["MODEL_API_KALITI"] # yo'q bo'lsa - ishga tushmaslik
4. LOG.info("bashorat", extra={"maydonlar": {"email": email_niqobla(m.email), "ehtimol": p}})
5. mijozlar: list[Mijoz] = Field(min_length=1, max_length=100)Vazifa 3: Kontrakt testini kuchaytiring
1-misoldagi test_orqaga_moslik ga qo'shing: (1) Field chegaralari toraymagan bo'lsin (masalan, oylar uchun maximum kamaymagan), (2) javob sxemasidan (Bashorat) maydon o'chirilmagan bo'lsin. oylar chegarasini le=100 ga o'zgartirgan D variant bilan sinang.
Vazifa 4: Zaxira siyosati
2-misolda zaxira model o'rniga "oxirgi ma'lum natija" siyosatini qo'shing: keshda bu mijoz uchun eski natija bo'lsa (hatto boshqa model versiyasida) — uni "manba": "eski_kesh" bilan qaytaring, bo'lmasa qoida modeli. Uchala siyosatni zaxira javoblardagi o'rtacha xato bo'yicha solishtiring.
Vazifa 5: Log so'rovlari
3-misol loglariga kechikishni qo'shib, 200 ta so'rov yuboring (virtual kechikish 5-50 ms). Loglardan: (1) status bo'yicha taqsimot, (2) har egasi uchun p95 kechikish, (3) eng sekin 3 ta sorov_id ni chiqaring. Nega bu savollarga matnli loglardan javob berish qiyin?
Vazifa 6: Taqsimlangan limit
4-misoldagi CHELAKLAR lug'ati bitta jarayonda yashaydi. Ikki "server" (ikkita FastAPI ilova, umumiy lug'atsiz) orasida so'rovlar navbat bilan taqsimlansa, mijoz aslida qancha so'rov o'tkaza oladi? Umumiy ombor (bitta umumiy lug'at — Redis o'rnida) bilan solishtiring.
Vazifa 7: O'ylash
Model API ning p99 kechikishi 40 ms, SLA esa "p99 < 150 ms". Marketing katta aksiya e'lon qildi — trafik 3 barobar oshishi kutilmoqda. Rahbar so'radi: "hozir o'rtacha yuklanish 30% — demak 3 barobar ham bemalol ko'taramiz, to'g'rimi?"
Javob
Qisqa javob: yo'q, bunday xulosa xavfli. 30% × 3 = 90% — bu rho = 0.9. 4-misoldagi simulyatsiya ko'rsatdiki, rho 0.3 dan 0.9 ga o'tganda p99 chiziqli emas, ko'p barobar o'sadi. 40 ms dan 150 ms dan ortiqqa chiqish ehtimoli yuqori.
1. Nega "30% -> 90%" yetarli emas
- Navbat nazariyasida kutish vaqti taxminan
1 / (1 - rho)ga proporsional:rho = 0.3da bu 1.4,rho = 0.9da — 10. Dum (p99) undan ham tezroq o'sadi. - "O'rtacha 30%" — kun bo'yicha o'rtacha. Aksiya boshlanishidagi eng yuqori daqiqada trafik 3 barobardan ham ko'p bo'lishi mumkin.
- Kechikish taqsimoti dumli: model ba'zan sekinlashadi (2-misol) — yuqori
rhoda bu sekin so'rovlar navbatni uzoq to'sib qo'yadi.
2. Nima qilish kerak
| Qadam | Nima beradi |
|---|---|
| Yuklama sinovi (k6/locust) 1x, 2x, 3x, 4x trafikda | Haqiqiy p99 egri chizig'i — taxmin emas |
Sig'imni rho ~ 0.6 ga oshirish (ko'proq ishchi/replika) |
3x trafikda ham zaxira |
| Avtomasshtablash 27.10-bob — CPU/kechikish bo'yicha | Portlashlarga moslashish |
| Rate limiting va ustuvorlik | Ortiqcha yuklama boshqalarni to'smaydi |
| Timeout + zaxira javob (2-misol) | Eng yomon holatda ham p99 byudjet ichida |
| Keshni isitish (mashhur mijozlar) | Model chaqiruvlari kamayadi |
3. Qanday hisobot beriladi
"Hozirgi sig'im bilan 3x trafikda rho ≈ 0.9, simulyatsiya/sinov bo'yicha p99 ≈ X ms — SLA buziladi. 6 ta replika bilan rho ≈ 0.6, p99 ≈ Y ms. Qo'shimcha narx — Z (faraziy, bulut narxlarini rasmiy sahifadan tekshiring)." Raqamlar yuklama sinovidan olinadi.
Xulosa: yuklanish foizi kechikish haqida to'g'ridan-to'g'ri gapirmaydi. Sig'im p99 bo'yicha, yuklama sinovi bilan va zaxira bilan rejalashtiriladi; o'rtacha yuklanishni trafik koeffitsientiga ko'paytirish — navbat nazariyasining eng keng tarqalgan xatosi.
Nimani mustahkamlaydi: 2.4, 2.6, 2.9, 2.12-bo'limlar.
Xulosa
Bu darsda model API uchun testlar to'plamini yozdik va uni ishonchlilik, kuzatuvchanlik va xavfsizlik qatlamlari bilan mustahkamladik — hammasi virtual vaqtda, deterministik.
Eng muhim uch fikr:
Chegara holatlar testlari "hech kim o'ylamagan" xatolarni topadi. 1-misolda FastAPI ning standart validatsiya ishlovchisi
NaNva1e400uchun422o'rniga500qaytardi — bu 15 testdan faqat chegara holatlaridagi ikkitasida ko'rindi.inputsiz o'z ishlovchimiz bilan 15/15. Kontrakt testi ixtiyoriy yangi maydonni orqaga mos deb o'tkazdi, golden test esa model almashganini (0.2529va0.7148) darhol ushladi.Byudjet, zaxira, kesh va idempotentlik — dum va takrorlarga qarshi. 2-misolda timeout va zaxira javob max kechikishni
289.0dan101.2ms ga tushirdi (5.5% so'rov zaxira bilan, o'rtacha0.133sifat narxi). Kesh 74.5% so'rovga javob berib, p95 ni9.0ms ga tushirdi. Idempotentlik kaliti uchta urinishda 6 o'rniga 2 ta SMS yubordi, katta tana esa validatsiyagacha413oldi. 3-misolda JSON loglarsorov_idbilan bog'landi, shaxsiy ma'lumot niqoblandi, kalitlar faqat muhitdan olindi va loglarga tushmadi;401va403aniq ajratildi.Kechikish yuklanishga chiziqli emas. 4-misolda p99
rho = 0.6da64.4ms,0.9da150.2ms, sig'imdan 5% ortiqda esa soniyalar bo'ldi. Token chelak har mijozni alohida chekladi, rate limit ortiqcha yuklamada qabul qilinganlar uchun p99 ni ushlab qoldi, qo'shimcha sig'im esa muammoni rad etishsiz hal qildi.
Keyingi darsda Docker bilan konteynerlash: API, paket va muhitni bitta takrorlanadigan obrazga joylaymiz — Dockerfile tuzilishi, qatlamlar keshi, .dockerignore, root bo'lmagan foydalanuvchi va muhit o'zgaruvchilari orqali konfiguratsiya.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!