Monitoring: jim buzilishni eshitiladigan qilish
Bir bobdan keyin, yana o'sha savol
1-bobda taksichi bor edi — o'n yil oldingi xaritani yoddan biladigan. U xato qilmaydi, signal chalmaydi, shunchaki sizni noto'g'ri olib boradi.
O'shanda shunday deb yozgandik: buzilgan dastur baqiradi, buzilgan model esa jim.
O'shandan beri o'ttiz bob o'tdi. Biz takrorlanish qurdik, sifat tekshirdik, quvur yasadik, registr qildik, servis yozdik, xavfsiz chiqarishni o'rgandik.
Lekin taksichi hali ham jim.
33-bobda shadow bilan ikki versiyani solishtirdik — chiqarishdan oldin. Endi savol boshqa: model allaqachon ishlab turibdi, va u sekin-asta eskirmoqda. Buni kim aytadi?
Javob: hech kim. Agar siz o'lchamasangiz.
Monitoring nima emas
Ko'p jamoada "monitoring bor" deganda server grafiklari tushuniladi: protsessor, xotira, disk.
Bular kerak, lekin ular modelga aloqador emas.
Protsessor 20% da, xotira normal, disk bo'sh, servis 200 qaytaryapti — va model brokerlarning yarmini o'tkazib yuboryapti. Hamma grafik yashil.
1-bobdagi jadvalni eslang: F1 0.860 dan 0.675 ga tushgandi. O'sha paytda birorta ham server ko'rsatkichi o'zgarmagan bo'lardi.
import pandas as pd show(pd.DataFrame([ {"Qatlam": "Infratuzilma", "Misol": "CPU, xotira, disk", "Model eskirsa ko'rinadimi": "Yo'q"}, {"Qatlam": "Servis", "Misol": "So'rov soni, xato ulushi, p99", "Model eskirsa ko'rinadimi": "Yo'q"}, {"Qatlam": "**Model**", "Misol": "Bashorat taqsimoti, ishonch", "Model eskirsa ko'rinadimi": "**Ha**"}, {"Qatlam": "**Biznes**", "Misol": "Shikoyatlar, qo'lda tuzatishlar", "Model eskirsa ko'rinadimi": "**Ha, lekin kech**"}, ]))
| Qatlam | Misol | Model eskirsa ko'rinadimi |
|---|---|---|
| Infratuzilma | CPU, xotira, disk | Yo'q |
| Servis | So'rov soni, xato ulushi, p99 | Yo'q |
| **Model** | Bashorat taqsimoti, ishonch | **Ha** |
| **Biznes** | Shikoyatlar, qo'lda tuzatishlar | **Ha, lekin kech** |
Uchinchi qatorni deyarli hech kim o'lchamaydi. Shuning uchun model eskirganini odatda to'rtinchi qator aytadi — mijoz shikoyati orqali, uch oy kechikib.
Nimani o'lchash kerak
Servis uchun to'rtta klassik ko'rsatkich bor:
- Trafik — sekundiga nechta so'rov
- Xatolar — nechtasi muvaffaqiyatsiz
- Kechikish — p50, p99 (29-bob)
- To'yinganlik — resurs chegaraga qanchalik yaqin
ML uchun beshinchisi qo'shiladi va u eng muhimi:
- Bashorat taqsimoti — model qanday javob berayapti
Nega bu ishlaydi? Chunki sizda to'g'ri javob yo'q (1-bob). Lekin modelning javoblarini ko'rish mumkin. Agar kecha e'lonlarning 35% ini broker deb belgilagan bo'lsa, bugun esa 60% — nimadir o'zgargan.
Bu drift ekanini isbotlamaydi, lekin qarashga majbur qiladi.
Servisga o'lchov qo'shamiz
Prometheus — eng keng tarqalgan monitoring tizimi. U oddiy ishlaydi: servis /metrics
manzilida oddiy matn qaytaradi, Prometheus uni har necha soniyada o'qib turadi.
Format shunchalik oddiyki, uni qo'lda ham yozish mumkin.
shu faylni yaratingsrc/xizmat/olchov.py
"""Prometheus formatidagi o'lchovlar. Tashqi kutubxonasiz. Format: har qator `nom{yorliq="qiymat"} son`. Prometheus shu matnni o'qiydi va vaqt qatoriga aylantiradi. """ import threading import time from collections import Counter _qulf = threading.Lock() # hisoblagichlar: faqat oshadi _sorovlar = Counter() # holat kodi bo'yicha _bashoratlar = Counter() # broker / ega # kechikish: oddiy gistogramma _chegaralar = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0) _paqir = Counter() _jami_vaqt = 0.0 _jami_soni = 0 # ehtimol taqsimoti: o'nta oraliq _ehtimol = Counter() _boshlandi = time.time() def sorov_yoz(kod: int, sekund: float) -> None: global _jami_vaqt, _jami_soni with _qulf: _sorovlar[kod] += 1 _jami_vaqt += sekund _jami_soni += 1 for c in _chegaralar: if sekund <= c: _paqir[c] += 1 def bashorat_yoz(ehtimol: float, broker: bool) -> None: with _qulf: _bashoratlar["broker" if broker else "ega"] += 1 _ehtimol[min(int(ehtimol * 10), 9)] += 1 def matn() -> str: """Prometheus o'qiydigan matn.""" q = [] q.append("# HELP servis_sorovlar_jami Jami so'rovlar") q.append("# TYPE servis_sorovlar_jami counter") for kod, n in sorted(_sorovlar.items()): q.append(f'servis_sorovlar_jami{{kod="{kod}"}} {n}') q.append("# HELP servis_kechikish_sekund So'rov kechikishi") q.append("# TYPE servis_kechikish_sekund histogram") yigindi = 0 for c in _chegaralar: yigindi = _paqir[c] q.append(f'servis_kechikish_sekund_bucket{{le="{c}"}} {yigindi}') q.append(f'servis_kechikish_sekund_bucket{{le="+Inf"}} {_jami_soni}') q.append(f"servis_kechikish_sekund_sum {_jami_vaqt:.6f}") q.append(f"servis_kechikish_sekund_count {_jami_soni}") # MODEL qatlami - eng muhimi q.append("# HELP model_bashoratlar_jami Qaror bo'yicha bashoratlar") q.append("# TYPE model_bashoratlar_jami counter") for nom, n in sorted(_bashoratlar.items()): q.append(f'model_bashoratlar_jami{{qaror="{nom}"}} {n}') q.append("# HELP model_ehtimol_taqsimoti Ehtimol oraliqlari") q.append("# TYPE model_ehtimol_taqsimoti counter") for i in range(10): q.append(f'model_ehtimol_taqsimoti{{oraliq="0.{i}"}} {_ehtimol[i]}') q.append("# HELP servis_ishlash_sekund Ishga tushgandan beri") q.append("# TYPE servis_ishlash_sekund gauge") q.append(f"servis_ishlash_sekund {time.time() - _boshlandi:.1f}") return "\n".join(q) + "\n"
Hisoblagich (counter) va o'lchagich (gauge)
Prometheus da ikki asosiy tur bor va ularni chalkashtirish keng tarqalgan xato.
Counter faqat oshadi: jami so'rovlar, jami xatolar. Servis qayta ishga tushsa nolga tushadi, va Prometheus buni tushunadi.
Gauge ikki tomonga o'zgaradi: hozirgi xotira, navbatdagi so'rovlar.
Nega farqi muhim? Chunki counter dan tezlik olinadi: rate(sorovlar[5m]) — sekundiga
nechta. Gauge dan bunday olinmaydi.
Eng ko'p uchraydigan xato: "hozirgi so'rov/s" ni gauge qilib yozish. Uni servis o'zi hisoblashi shart emas — Prometheus counter dan o'zi hisoblaydi.
Servisga ulaymiz
shu faylni yaratingsrc/xizmat/servis_olchovli.py
"""27-bobdagi servis + o'lchovlar.""" import os import time from pathlib import Path from typing import Annotated, Any from fastapi import FastAPI, HTTPException, Request, Response from pydantic import BaseModel, Field from quvur.format import onnx_ehtimol, onnx_sessiya from quvur.xususiyat import bitta_qator from xizmat.olchov import bashorat_yoz, matn, sorov_yoz MODEL_YOLI = os.environ.get("MODEL_YOLI", "model.onnx") CHEGARA = float(os.environ.get("BROKER_CHEGARASI", "0.5")) app = FastAPI(title="Broker aniqlash (o'lchovli)") HOLAT: dict[str, Any] = {"sessiya": None} def yukla() -> None: p = Path(MODEL_YOLI) if p.exists(): HOLAT["sessiya"] = onnx_sessiya(p) yukla() @app.middleware("http") async def olchov_oraliq(request: Request, call_next): """Har so'rovni o'lchaydi — bitta joyda, har yo'l uchun alohida emas.""" t0 = time.perf_counter() javob = await call_next(request) if request.url.path != "/metrics": sorov_yoz(javob.status_code, time.perf_counter() - t0) return javob class Elon(BaseModel): 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)] @app.get("/healthz") def tirikmi() -> dict[str, str]: return {"holat": "tirik"} @app.get("/ready") def tayyormi() -> dict[str, Any]: if HOLAT["sessiya"] is None: raise HTTPException(status_code=503, detail="model yuklanmagan") return {"holat": "tayyor"} @app.get("/metrics") def olchovlar() -> Response: return Response(content=matn(), media_type="text/plain; version=0.0.4") @app.post("/bashorat") def bashorat(elon: Elon) -> dict[str, Any]: if HOLAT["sessiya"] is None: raise HTTPException(status_code=503, detail="model yuklanmagan") x = bitta_qator(elon.model_dump()) e = float(onnx_ehtimol(HOLAT["sessiya"], x.values)[0]) qaror = e >= CHEGARA bashorat_yoz(e, qaror) return {"broker_ehtimoli": round(e, 4), "broker": qaror}
Eng muhim qism — middleware. U har so'rovni avtomatik o'lchaydi. Har yo'lga qo'lda kod yozish shart emas, va unutish ham mumkin emas.
Ishga tushiramiz va trafik yuboramiz
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 tayyor') " export PYTHONPATH=src python3 -m uvicorn xizmat.servis_olchovli:app --host 127.0.0.1 --port 18150 \ --log-level warning > olchov.log 2>&1 < /dev/null & echo $! > olchov.pid for i in $(seq 1 40); do [ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:18150/ready 2>/dev/null)" = "200" ] && { echo "servis tayyor"; break; } sleep 0.5 done
model tayyor servis tayyor
python3 -c " import json, urllib.request, urllib.error import pandas as pd d = pd.read_csv('../datasets/ijara.csv').head(400) XUS = ['xonalar','maydon','qavat','jami_qavat','narx_mln','tavsif_uzunligi', 'rasmlar_soni','telefon_takrorlanishi','kunlik_elonlar','egasi_tasdiqlangan'] ok = xato = 0 for e in d[XUS].to_dict('records'): r = urllib.request.Request('http://127.0.0.1:18150/bashorat', data=json.dumps(e).encode(), headers={'Content-Type': 'application/json'}) try: with urllib.request.urlopen(r, timeout=10) as j: j.read(); ok += 1 except Exception: xato += 1 # ataylab yomon sorovlar - xato hisoblagichini tekshirish uchun for _ in range(25): r = urllib.request.Request('http://127.0.0.1:18150/bashorat', data=json.dumps({'xonalar': 2, 'maydon': -5}).encode(), headers={'Content-Type': 'application/json'}) try: with urllib.request.urlopen(r, timeout=5) as j: j.read() except urllib.error.HTTPError: xato += 1 print(f'yuborildi: {ok} yaxshi, {xato} yomon') "
yuborildi: 400 yaxshi, 25 yomon
O'lchovlarni o'qiymiz
curl -s http://127.0.0.1:18150/metrics | grep -vE "^#" | grep -E "sorovlar_jami|bashoratlar_jami|kechikish_sekund_(sum|count)"
servis_sorovlar_jami{kod="200"} 401
servis_sorovlar_jami{kod="422"} 25
servis_kechikish_sekund_sum 0.707171
servis_kechikish_sekund_count 426
model_bashoratlar_jami{qaror="broker"} 140
model_bashoratlar_jami{qaror="ega"} 260
Mana monitoring tizimi ko'radigan narsa. Uch qatlam bir joyda:
servis_sorovlar_jami — holat kodi bo'yicha. 200 va 422 alohida sanalgan.
servis_kechikish_sekund — 29-bobdagi kechikish, endi doimiy yig'ilmoqda.
model_bashoratlar_jami — va mana model qatlami. Nechta e'lon broker deb
belgilandi, nechtasi ega.
Bashorat taqsimoti: eng qimmatli grafik
curl -s http://127.0.0.1:18150/metrics | grep "model_ehtimol_taqsimoti" | grep -v "^#"
model_ehtimol_taqsimoti{oraliq="0.0"} 192
model_ehtimol_taqsimoti{oraliq="0.1"} 41
model_ehtimol_taqsimoti{oraliq="0.2"} 3
model_ehtimol_taqsimoti{oraliq="0.3"} 11
model_ehtimol_taqsimoti{oraliq="0.4"} 13
model_ehtimol_taqsimoti{oraliq="0.5"} 5
model_ehtimol_taqsimoti{oraliq="0.6"} 9
model_ehtimol_taqsimoti{oraliq="0.7"} 9
model_ehtimol_taqsimoti{oraliq="0.8"} 24
model_ehtimol_taqsimoti{oraliq="0.9"} 93
Bu o'nta raqam — modelning qanday o'ylayotgani.
Ko'p e'lon 0.0–0.1 oralig'ida bo'lsa, model ishonch bilan "ega" deyapti. Ko'pi 0.4–0.6 da to'plansa — model ikkilanayapti, va bu yomon belgi.
Eng muhimi: bu taqsimot kundan-kunga barqaror bo'lishi kerak. U siljisa — dunyoda nimadir o'zgargan.
35-bobda aynan shu taqsimotdan drift aniqlaymiz.
Nimaga ogohlantirish qo'yish kerak
Har ko'rsatkichga ogohlantirish qo'yish — eng keng tarqalgan xato. Natija: kuniga o'ttiz xabar, va odamlar ularni o'qimay qo'yadi.
Ogohlantirish harakat talab qilishi kerak. Agar xabar kelganda qiladigan ishingiz yo'q bo'lsa, u ogohlantirish emas, shovqin.
Model servisi uchun yetarli to'plam:
| Ogohlantirish | Nega |
|---|---|
/ready 5 daqiqa 503 |
Model yuklanmagan — xizmat yo'q |
| Xato ulushi > 5% (5 daq) | Nimadir buzildi |
| p99 > 500 ms (10 daq) | Foydalanuvchi sezyapti |
| Broker ulushi kunlik o'rtachadan 2x farq | Model xulqi o'zgardi |
Oxirgisi ML ga xos va eng qimmatli. Qolgan uchtasi har qanday servis uchun bir xil.
Tozalash
kill "$(cat olchov.pid)" 2>/dev/null sleep 1 echo "o'lchovli servis to'xtatildi" ss -ltn 2>/dev/null | grep -c 18150 || echo "18150 port bo'sh"
o'lchovli servis to'xtatildi 0 18150 port bo'sh
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Asosiy muammo | Model jim buziladi (1-bob) — monitoring uni eshitiladigan qiladi |
| Server grafiklari | Kerak, lekin model eskirganini ko'rsatmaydi |
| To'rt klassik signal | Trafik, xato, kechikish, to'yinganlik |
| Beshinchi (ML) | Bashorat taqsimoti — to'g'ri javobsiz ham o'lchanadi |
| Prometheus formati | Oddiy matn; /metrics qaytaradi |
| Counter va gauge | Counter oshadi (rate olinadi), gauge o'zgaradi |
| Middleware | Bitta joyda o'lchash — unutib bo'lmaydi |
| Ogohlantirish qoidasi | Harakat talab qilmasa — u shovqin |
| Eng muhim ML signali | Qaror ulushi keskin o'zgarsa |
Talaba ishi
/metricsga xususiyat qiymatlarining o'rtachasini qo'shing (masalan o'rtacha maydon). Nega bu drift uchun foydali?- Ataylab 100 ta yomon so'rov yuboring.
422hisoblagichi qanday o'zgardi? - Gistogramma chegaralarini o'zgartiring (
_chegaralar). p99 ni aniqroq ko'rish uchun qaysi oraliqlar kerak? - Ikkinchi servis (33-bobdagi green) ni ham o'lchovli qiling va ikkala taqsimotni solishtiring.
- Qaysi ogohlantirish sizning loyihangizda tungi soat 3 da telefon qilishga arziydi? Ro'yxatni uchtadan oshirmang.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.