Asosiy qismga o'tish

Real vaqtda ML: oqimga model qo'shish

36-bob 9 kod bloki 1796 so'z

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

Javob kelganda kech bo'lsa

27-bobdagi servis so'rov kutadi: kimdir /bashorat ga murojaat qiladi, javob oladi.

Bu ko'p holatda to'g'ri. Lekin hamma holatda emas.

NOVA VIVO ni eslang. Ballon noto'g'ri omborga keldi. Kim so'raydi? Hech kim. Skaner hodisa yuboradi va o'z ishini davom ettiradi. Agar siz javobni so'rov kutib o'tirsangiz, uni hech qachon olmaysiz.

Bunday tizimda model so'ramaydi — tinglaydi.

10-bobdagi daftarni eslang: hodisalar oqimga yoziladi, iste'molchilar o'qiydi. Model ham shunday iste'molchi bo'lishi mumkin.

Ikki xil "real vaqt"

Bu ibora ikki boshqa narsani anglatadi va ularni chalkashtirish keng tarqalgan.

So'rov-javob (online). Foydalanuvchi kutib turibdi. 27-bobdagi servis. Muhimi — kechikish: p99 100 ms dan oshmasin.

Oqimli (streaming). Hech kim kutmaydi. Hodisa keladi, model qaror qiladi, natija boshqa oqimga yoziladi. Muhimi — to'xtamaslik: hodisa yo'qolmasin, navbat o'smasin.

python8 qator
import pandas as pd
show(pd.DataFrame([
    {"": "Kim boshlaydi", "So'rov-javob": "Foydalanuvchi", "Oqimli": "Hodisa"},
    {"": "Kim kutadi", "So'rov-javob": "Foydalanuvchi", "Oqimli": "Hech kim"},
    {"": "Asosiy o'lchov", "So'rov-javob": "p99 kechikish", "Oqimli": "Orqada qolish (lag)"},
    {"": "Yuk oshsa", "So'rov-javob": "429 qaytaring (30-bob)", "Oqimli": "Navbat o'sadi, kutadi"},
    {"": "Misol", "So'rov-javob": "RostUy filtri", "Oqimli": "NOVA VIVO ballon hodisasi"},
]))
So'rov-javobOqimli
Kim boshlaydiFoydalanuvchiHodisa
Kim kutadiFoydalanuvchiHech kim
Asosiy o'lchovp99 kechikishOrqada qolish (lag)
Yuk oshsa429 qaytaring (30-bob)Navbat o'sadi, kutadi
MisolRostUy filtriNOVA VIVO ballon hodisasi

To'rtinchi qatorga e'tibor bering. Oqimli tizimda rad etish yo'q — hodisa Kafka da turadi va kutadi. Bu yaxshi (yo'qolmaydi) va yomon (orqada qolish o'sadi).

Brokerni ko'taramiz

bash34 qator
docker rm -f kitob-kafka > /dev/null 2>&1
docker run -d --name kitob-kafka -p 19092:9092 \
  -e KAFKA_NODE_ID=1 -e KAFKA_PROCESS_ROLES=broker,controller \
  -e KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
  -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:19092 \
  -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
  -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
  -e KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 \
  -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
  -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \
  -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \
  -e KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 \
  apache/kafka:3.9.0 > /dev/null
for i in $(seq 1 60); do
  python3 -c "from kafka import KafkaAdminClient; KafkaAdminClient(bootstrap_servers='localhost:19092')" > /dev/null 2>&1 \
    && { echo "kafka ${i} soniyada tayyor"; break; }
  sleep 1
done
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')
"
Chiqish (qaytish kodi 0, 6774 ms)
kafka 1 soniyada tayyor
model tayyor

Oqimli bashoratchi

Kod sodda: Kafka dan o'qi, bashorat qil, Kafka ga yoz.

shu faylni yaratingsrc/xizmat/oqimli.py

python30 qator
"""Oqimli bashorat: Kafka -> model -> Kafka.

3-bobdagi qoida bu yerda ham amal qiladi: xususiyat kodi O'SHA `tayyorla()`.
Oqimli tizimda alohida kod yozish - skewning eng keng tarqalgan sababi.
"""
import json
import time
from typing import Any

import pandas as pd

from quvur.format import onnx_ehtimol, onnx_sessiya
from quvur.xususiyat import XOM_USTUNLAR, tayyorla


def toplamni_bahola(sessiya, xabarlar: list[dict[str, Any]],
                    chegara: float) -> list[dict[str, Any]]:
    """To'plamni birgalikda baholaydi (29-bob: to'plam arzonroq)."""
    d = pd.DataFrame([x["elon"] for x in xabarlar], columns=XOM_USTUNLAR)
    e = onnx_ehtimol(sessiya, tayyorla(d).values)
    natija = []
    for xabar, ehtimol in zip(xabarlar, e):
        natija.append({
            "elon_id": xabar["elon_id"],
            "ehtimol": round(float(ehtimol), 4),
            "broker": bool(ehtimol >= chegara),
            "model_versiya": "v1",
            "vaqt": time.time(),
        })
    return natija

Oqimli tizim — skew uchun eng xavfli joy

3-bobdagi tikuvchi va fabrikani eslang: ikki xil santimetr, kostyum tor.

Oqimli tizimda bu xato ayniqsa oson sodir bo'ladi. Sabab: oqimli kod ko'pincha boshqa jamoada, boshqa tilda (Java, Scala) yoziladi, va u xususiyatlarni qaytadan hisoblaydi.

O'qitish pandas bilan, oqim Java bilan. Ikkalasi ham "to'g'ri" yozilgan. Natija — model bema'ni javob beradi va sabab topilmaydi.

Shuning uchun yuqoridagi kod tayyorla() ni import qiladi, qayta yozmaydi. Agar oqim boshqa tilda bo'lsa, yagona yechim — xususiyat hisobini alohida servisga chiqarish va ikkala tomon o'shani chaqirsin.

Hodisalarni yuboramiz

python26 qator
python3 -c "
import json
import pandas as pd
from kafka import KafkaProducer
from kafka.admin import KafkaAdminClient, NewTopic

a = KafkaAdminClient(bootstrap_servers='localhost:19092')
mavjud = a.list_topics()
for t in ('elon-kirish', 'elon-natija'):
    if t not in mavjud:
        a.create_topics([NewTopic(name=t, num_partitions=3, replication_factor=1)])

XUS = ['xonalar','maydon','qavat','jami_qavat','narx_mln','tavsif_uzunligi',
       'rasmlar_soni','telefon_takrorlanishi','kunlik_elonlar','egasi_tasdiqlangan']
d = pd.read_csv('../datasets/ijara.csv').drop_duplicates('elon_id').head(2000)

p = KafkaProducer(bootstrap_servers='localhost:19092',
                  value_serializer=lambda v: json.dumps(v).encode(),
                  key_serializer=lambda k: k.encode(),
                  linger_ms=20, batch_size=65536)
for r in d.itertuples():
    p.send('elon-kirish', key=r.elon_id,
           value={'elon_id': r.elon_id, 'elon': {u: getattr(r, u) for u in XUS}})
p.flush()
print(f'{len(d):,} elon oqimga yuborildi')
" 2>&1 | grep -v Warning
Chiqish (qaytish kodi 0, 670 ms)
2,000 elon oqimga yuborildi

Bashoratchini ishga tushiramiz

python42 qator
python3 -c "
import sys, json, time; sys.path.insert(0, 'src')
from kafka import KafkaConsumer, KafkaProducer
from quvur.format import onnx_sessiya
from xizmat.oqimli import toplamni_bahola

s = onnx_sessiya('model.onnx')
c = KafkaConsumer('elon-kirish', bootstrap_servers='localhost:19092',
                  auto_offset_reset='earliest', group_id='bashoratchi',
                  enable_auto_commit=False, consumer_timeout_ms=8000,
                  max_poll_records=200)
p = KafkaProducer(bootstrap_servers='localhost:19092',
                  value_serializer=lambda v: json.dumps(v).encode(),
                  key_serializer=lambda k: k.encode(),
                  linger_ms=20, batch_size=65536)

jami = broker = 0
toplam = []
t0 = time.perf_counter()

def jonat(toplam):
    global jami, broker
    if not toplam: return
    for n in toplamni_bahola(s, toplam, 0.5):
        p.send('elon-natija', key=n['elon_id'], value=n)
        broker += n['broker']
    jami += len(toplam)

for m in c:
    toplam.append(json.loads(m.value))
    if len(toplam) >= 200:
        jonat(toplam); toplam = []
jonat(toplam)
p.flush()
c.commit()
c.close()

dt = time.perf_counter() - t0
print(f'qayta ishlandi : {jami:,} elon')
print(f'broker dedi    : {broker:,} ({broker/max(jami,1)*100:.1f}%)')
print(f'vaqt           : {dt:.2f} s -> {jami/dt:,.0f} elon/s')
" 2>&1 | grep -v Warning
Chiqish (qaytish kodi 0, 10019 ms)
2026-09-29 21:25:15.745283767 [W:onnxruntime:Default, device_discovery.cc:164 DiscoverDevicesForPlatform] GPU device discovery failed: device_discovery.cc:89 ReadFileContents Failed to open file: "/sys/class/drm/card0/device/vendor"
qayta ishlandi : 2,000 elon
broker dedi    : 671 (33.6%)
vaqt           : 9.25 s -> 216 elon/s

Diqqat: to'plam bilan ishladik — 200 tadan. 29-bobda o'lchagandik: to'plam bitta-bitta ishlashdan o'nlab barobar arzon.

Oqimli tizimda bu bepul keladi: foydalanuvchi kutmayapti, demak to'plam yig'ishdan kechikish zarari yo'q.

Natijani o'qiymiz

python21 qator
python3 -c "
import json, collections
from kafka import KafkaConsumer

c = KafkaConsumer('elon-natija', bootstrap_servers='localhost:19092',
                  auto_offset_reset='earliest', group_id='hisobot',
                  consumer_timeout_ms=8000)
n = collections.Counter()
namuna = None
for m in c:
    v = json.loads(m.value)
    n['broker' if v['broker'] else 'ega'] += 1
    if namuna is None and v['broker']: namuna = v
c.close()

print('natija oqimida:')
for k, v in sorted(n.items()):
    print(f'  {k:8s}: {v:>6,}')
print()
print('namuna xabar:', json.dumps(namuna, ensure_ascii=False))
" 2>&1 | grep -v Warning
Chiqish (qaytish kodi 0, 8437 ms)
natija oqimida:
  broker  :    671
  ega     :  1,329

namuna xabar: {"elon_id": "E100997", "ehtimol": 0.8716, "broker": true, "model_versiya": "v1", "vaqt": 1790699117.0010428}

Har natija elon_id kaliti bilan yozildi — 10-bobdagi qoida: bir e'lonning barcha natijalari bitta partitionda, tartibda.

model_versiya maydoniga ham e'tibor bering. Keyinroq "bu qaror qaysi model chiqargan?" degan savol albatta tug'iladi — va u javobsiz qolmasin.

Orqada qolish: oqimning asosiy o'lchovi

So'rov-javobda p99 ni o'lchaganmiz (29-bob). Oqimda boshqa raqam muhim: lag — iste'molchi oqimdan qancha orqada.

python16 qator
python3 -c "
from kafka import KafkaConsumer, TopicPartition
c = KafkaConsumer(bootstrap_servers='localhost:19092', group_id='bashoratchi')
tps = [TopicPartition('elon-kirish', p) for p in c.partitions_for_topic('elon-kirish')]
c.assign(tps)
oxiri = c.end_offsets(tps)
jami_lag = 0
print(f'{\"partition\":>10s} {\"oxirgi\":>8s} {\"oqilgan\":>9s} {\"lag\":>6s}')
for tp in sorted(tps, key=lambda t: t.partition):
    komit = c.committed(tp) or 0
    lag = oxiri[tp] - komit
    jami_lag += lag
    print(f'{tp.partition:>10d} {oxiri[tp]:>8d} {komit:>9d} {lag:>6d}')
c.close()
print(f'{\"JAMI LAG\":>10s} {jami_lag:>25d}')
" 2>&1 | grep -v Warning
Chiqish (qaytish kodi 0, 416 ms)
 partition   oxirgi   oqilgan    lag
         0      712       712      0
         1      642       642      0
         2      646       646      0
  JAMI LAG                         0

Lag nol — bashoratchi oqimni quvib yetgan.

Bu raqam ishlab chiqarishdagi eng muhim signal. U o'sib borsa — model hodisalarga ulgurmayapti.

Lag o'sishi — kechikish emas, **qulash** belgisi

So'rov-javobda yuk oshsa, p99 sekin o'sadi va siz sezasiz.

Oqimda esa boshqacha: model soniyasiga 1000 hodisani uddalaydi, oqimga 1200 kelyapti. Farq atigi 200 — kichik ko'rinadi.

Lekin bu farq to'planadi. Bir soatda 720 000 hodisa orqada qolasiz. Ertalabga millionlab.

Va eng yomoni: grafik chiziqli o'sadi, shuning uchun uni erta payqash oson — agar qarayotgan bo'lsangiz.

Ogohlantirish qoidasi (34-bob): lag doimiy o'sayotgan bo'lsa xabar bering. Absolut son emas, yo'nalish muhim.

Onlayn xususiyatlar: eng qiyin qism

Bizning xususiyatlar oson edi — hammasi hodisaning ichida keldi.

Haqiqiy tizimda ko'pincha bunday bo'lmaydi. Masalan: "bu telefon oxirgi 7 kunda nechta e'londa uchradi?"

Bu savol tarixni talab qiladi. Hodisada u yo'q.

Uch yechim bor:

python9 qator
import pandas as pd
show(pd.DataFrame([
    {"Yechim": "Hodisaga qo'shib yuborish", "Kim hisoblaydi": "Manba tizimi",
     "Afzalligi": "Model sodda", "Kamchiligi": "Manba o'zgarishi kerak"},
    {"Yechim": "Oqimda hisoblash (14-bob)", "Kim hisoblaydi": "Oyna + holat",
     "Afzalligi": "Tez, yangi", "Kamchiligi": "Murakkab, holat saqlanadi"},
    {"Yechim": "Xususiyat ombori", "Kim hisoblaydi": "Alohida tizim",
     "Afzalligi": "O'qitish va xizmat bitta manba", "Kamchiligi": "Yana bitta tizim"},
]))
YechimKim hisoblaydiAfzalligiKamchiligi
Hodisaga qo'shib yuborishManba tizimiModel soddaManba o'zgarishi kerak
Oqimda hisoblash (14-bob)Oyna + holatTez, yangiMurakkab, holat saqlanadi
Xususiyat omboriAlohida tizimO'qitish va xizmat bitta manbaYana bitta tizim

Xususiyat ombori (feature store) — 3-bobdagi muammoning sanoat yechimi: xususiyat bir marta hisoblanadi, o'qitish ham, xizmat ham o'sha qiymatni oladi.

Kichik loyihada u ortiqcha. Lekin o'nlab model va yuzlab xususiyat bo'lganda, u skewni butunlay yo'q qiladi.

Tozalash

bash2 qator
docker rm -f kitob-kafka > /dev/null 2>&1 && echo "kafka o'chirildi"
docker ps --format '{{.Names}}' | grep -v k3d
Chiqish (qaytish kodi 0, 674 ms)
kafka o'chirildi
novavivo-postgres
novavivo-redis
stratix-med-frontend-1

Xulosa

Tushuncha Asosiy fikr
So'rov-javob Foydalanuvchi kutadi; p99 muhim
Oqimli Hech kim kutmaydi; lag muhim
Oqimda rad etish yo'q Hodisa navbatda kutadi — yo'qolmaydi
To'plam Oqimda bepul keladi (hech kim kutmaydi)
Skew xavfi Oqim kodi ko'pincha alohida yoziladi — eng xavfli joy
Yechim tayyorla() ni import qiling, qayta yozmang
model_versiya Har natijaga yozing — keyin so'raladi
Lag o'sishi Kechikish emas, to'planuvchi qarz
Ogohlantirish Absolut son emas, yo'nalish
Onlayn xususiyat Tarix kerak bo'lsa — oqimda hisoblang yoki ombor quring

Talaba ishi

  1. Bashoratchini to'plamsiz (bittalab) ishlating. Elon/s qanday o'zgardi?
  2. elon-kirish ga 10 000 hodisa yuboring, lekin bashoratchini ishga tushirmang. Lag nima ko'rsatadi?
  3. enable_auto_commit=True qiling va bashoratchini yarmida to'xtating. Qayta ishga tushirganda nima bo'ldi va nega bu xavfli?
  4. Natija oqimiga 34-bobdagi o'lchovlarni qo'shing: qaror ulushi vaqt bo'yicha.
  5. "Oxirgi 7 kundagi telefon takrorlanishi" xususiyatini 14-bobdagi oyna bilan hisoblang.

Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar