Asosiy qismga o'tish

Monitoring: jim buzilishni eshitiladigan qilish

34-bob 8 kod bloki 2268 so'z

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

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.

python11 qator
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**"},
]))
QatlamMisolModel eskirsa ko'rinadimi
InfratuzilmaCPU, xotira, diskYo'q
ServisSo'rov soni, xato ulushi, p99Yo'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

python77 qator
"""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

python78 qator
"""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

bash24 qator
python3 -c "
import sys, pathlib; sys.path.insert(0, 'src')
if not pathlib.Path('model.onnx').exists():
    import pandas as pd
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import train_test_split
    from quvur.xususiyat import XUSUSIYATLAR, tayyorla
    from quvur.format import onnx_ga
    d = pd.read_csv('../datasets/ijara.csv')
    x, y = tayyorla(d), d['broker']
    Xtr, _, ytr, _ = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42)
    m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3,
                               random_state=42, n_jobs=1).fit(Xtr, ytr)
    onnx_ga(m, len(XUSUSIYATLAR), 'model.onnx')
print('model 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
Chiqish (qaytish kodi 0, 4441 ms)
model tayyor
servis tayyor
python28 qator
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')
"
Chiqish (qaytish kodi 0, 1132 ms)
yuborildi: 400 yaxshi, 25 yomon

O'lchovlarni o'qiymiz

bash1 qator
curl -s http://127.0.0.1:18150/metrics | grep -vE "^#" | grep -E "sorovlar_jami|bashoratlar_jami|kechikish_sekund_(sum|count)"
Chiqish (qaytish kodi 0, 8 ms)
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

bash1 qator
curl -s http://127.0.0.1:18150/metrics | grep "model_ehtimol_taqsimoti" | grep -v "^#"
Chiqish (qaytish kodi 0, 5 ms)
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

matn4 qator
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"
Chiqish (qaytish kodi 0, 1010 ms)
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

  1. /metrics ga xususiyat qiymatlarining o'rtachasini qo'shing (masalan o'rtacha maydon). Nega bu drift uchun foydali?
  2. Ataylab 100 ta yomon so'rov yuboring. 422 hisoblagichi qanday o'zgardi?
  3. Gistogramma chegaralarini o'zgartiring (_chegaralar). p99 ni aniqroq ko'rish uchun qaysi oraliqlar kerak?
  4. Ikkinchi servis (33-bobdagi green) ni ham o'lchovli qiling va ikkala taqsimotni solishtiring.
  5. 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.

Telegramda yozish Narxlar