Oqimli qayta ishlash (streaming)
Kechki smenadagi kassir
Kechqurun. Do'konda kassir Sardor ishlaydi. Uning oldida oddiy vazifa: har soatda qancha savdo bo'lganini yozib borish.
Soat 19:00 dan 20:00 gacha bo'lgan cheklarni sanaydi, yozadi. Keyin 20:00–21:00.
Soat 21:00 bo'ldi. Sardor 20:00–21:00 oralig'ini yopdi: 47 ta chek.
Shu payt kuryer kirib keladi va qo'lida bitta chek:
— Uzr, bu chek 20:45 da chiqarilgan edi, printerda qolib ketibdi.
Endi Sardorda muammo. U hisobni allaqachon yopgan. Nima qiladi?
- Chekni tashlab yuboradi — hisob noto'g'ri, lekin oson.
- Yopilgan hisobni ochib tuzatadi — to'g'ri, lekin kechagi hisobotlarni ham qayta yuborish kerak.
- Chekni keyingi soatga yozadi — hisob "to'g'ri" ko'rinadi, lekin 20:45 dagi savdo 21:00 dan keyin bo'lgandek chiqadi.
Uchala javob ham noqulay. Va bu — oqimli qayta ishlashdagi eng asosiy muammo. Uning rasmiy nomi bor: kech kelgan ma'lumot (late data).
Bu bobda Sardorning muammosini dastur darajasida hal qilamiz.
Batch va oqim: farqni eslaymiz
9-bobda ikkalasini o'lchagandik. Endi amaliy tomoniga o'tamiz.
Batch — Sardor kun oxirida o'tiradi va butun kunlik cheklarni sanaydi. Hammasi qo'lida, hech narsa kechikmaydi, hisob aniq. Lekin javob kun tugagandan keyin keladi.
Oqim — Sardor har chek chiqqanda hisobni yangilab boradi. Javob doim tayyor. Lekin hisob "tugadi" degan nuqta yo'q — har doim yana bitta chek kelishi mumkin.
diagramma( { "Hodisalar\n(to'xtovsiz)": (0, 1.6, 2.2, 1.1, "manba"), "Oyna\n(19:00-20:00)": (3.2, 2.9, 2.3, 0.9, "qayta"), "Oyna\n(20:00-21:00)": (3.2, 1.6, 2.3, 0.9, "qayta"), "Oyna\n(21:00-22:00)": (3.2, 0.3, 2.3, 0.9, "qayta"), "Watermark\n(chegarani belgilaydi)": (6.8, 1.6, 2.8, 1.1, "kuzatuv"), "Natija": (10.2, 1.6, 2.0, 1.1, "xizmat"), }, [ ("Hodisalar\n(to'xtovsiz)", "Oyna\n(19:00-20:00)"), ("Hodisalar\n(to'xtovsiz)", "Oyna\n(20:00-21:00)"), ("Hodisalar\n(to'xtovsiz)", "Oyna\n(21:00-22:00)"), ("Oyna\n(20:00-21:00)", "Watermark\n(chegarani belgilaydi)"), ("Watermark\n(chegarani belgilaydi)", "Natija"), ], nom="oqim_oyna.png", kenglik=14, izoh="Cheksiz oqimni oynalarga bo'lamiz. Watermark oynani qachon yopishni hal qiladi.", )

Cheksiz oqimni oynalarga bo'lamiz. Watermark oynani qachon yopishni hal qiladi.
Ikki xil vaqt
Mana eng ko'p chalkashlik keltiradigan joy. Har hodisada ikkita vaqt bor:
Hodisa vaqti (event time) — voqea haqiqatan sodir bo'lgan payt. Chekda yozilgani: 20:45.
Qayta ishlash vaqti (processing time) — hodisa sizga yetib kelgan payt. Kuryer chekni olib kirgani: 21:03.
Sardorning muammosi aynan shu ikkisi farq qilgani uchun chiqdi.
import pandas as pd show(pd.DataFrame([ {"Vaqt turi": "Hodisa vaqti", "Manbai": "Hodisaning o'zida yozilgan", "Afzalligi": "Haqiqatni aks ettiradi", "Kamchiligi": "Kechikib kelishi mumkin"}, {"Vaqt turi": "Qayta ishlash vaqti", "Manbai": "Serverning soati", "Afzalligi": "Hech qachon kechikmaydi", "Kamchiligi": "Haqiqatni buzadi"}, ]))
| Vaqt turi | Manbai | Afzalligi | Kamchiligi |
|---|---|---|---|
| Hodisa vaqti | Hodisaning o'zida yozilgan | Haqiqatni aks ettiradi | Kechikib kelishi mumkin |
| Qayta ishlash vaqti | Serverning soati | Hech qachon kechikmaydi | Haqiqatni buzadi |
Qoida: hisobot uchun doim hodisa vaqti ishlatiladi. Aks holda internet sekinlashgan kuni sizning butun statistikangiz siljib ketadi.
Brokerni ko'taramiz
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 "broker ${i} soniyada tayyor"; break; } sleep 1 done
broker 1 soniyada tayyor
Oyna (window) nima
Sardorning vazifasini eslang: "har soatda qancha savdo bo'lganini yozib bor".
Nega har soatda? Nega shunchaki "qancha savdo bo'ldi?" emas?
Chunki do'kon yopilmaydi. Savdo davom etaveradi. "Jami qancha?" degan savolning javobi har soniyada o'zgaradi va hech qachon tayyor bo'lmaydi.
Oqim ham xuddi shunday — u cheksiz. Hodisalar kelaveradi. "Hammasining o'rtachasi" degan savol ma'nosiz, chunki "hammasi" degan nuqta yo'q.
Shuning uchun oqimni bo'laklarga bo'lamiz. Har bo'lak — oyna (window).
Oyna — bu vaqt bo'yicha kesma: "19:00 dan 20:00 gacha". Endi savol javob berilishi mumkin bo'ladi, chunki chegara bor.
Diqqat qiling, bu sun'iy chegara. Tabiatda soat 20:00 da hech narsa o'zgarmaydi — biz uni o'zimiz qo'yamiz, javob olish uchun.
Va bo'lish usuli bitta emas. Uch xil oyna bor:
import pandas as pd show(pd.DataFrame([ {"Oyna turi": "Tumbling (ketma-ket)", "Ta'rifi": "Ustma-ust tushmaydi", "Misol": "Har soat: 19-20, 20-21", "Qachon": "Soatlik hisobot"}, {"Oyna turi": "Sliding (siljuvchi)", "Ta'rifi": "Ustma-ust tushadi", "Misol": "Oxirgi 1 soat, har 10 daqiqada", "Qachon": "Trend kuzatish"}, {"Oyna turi": "Session (seans)", "Ta'rifi": "Jimlik bilan ajraladi", "Misol": "Foydalanuvchi 30 daq jim tursa", "Qachon": "Sayt tashrifi"}, ]))
| Oyna turi | Ta'rifi | Misol | Qachon |
|---|---|---|---|
| Tumbling (ketma-ket) | Ustma-ust tushmaydi | Har soat: 19-20, 20-21 | Soatlik hisobot |
| Sliding (siljuvchi) | Ustma-ust tushadi | Oxirgi 1 soat, har 10 daqiqada | Trend kuzatish |
| Session (seans) | Jimlik bilan ajraladi | Foydalanuvchi 30 daq jim tursa | Sayt tashrifi |
Oynali hisobni yozamiz. Kafka ga NOVA VIVO hodisalarini yuboramiz va soatlik oynalar bo'yicha sanaymiz.
shu faylni yaratingsrc/quvur/oqim.py
"""Oqimli qayta ishlash: oyna, watermark, kech kelgan ma'lumot.""" from collections import defaultdict from datetime import datetime, timedelta from typing import Any def oyna_boshi(vaqt: datetime, daqiqa: int) -> datetime: """Hodisa vaqtini oyna boshiga yaxlitlaydi.""" jami = vaqt.hour * 60 + vaqt.minute boshi = (jami // daqiqa) * daqiqa return vaqt.replace(hour=boshi // 60, minute=boshi % 60, second=0, microsecond=0) class OynaliHisob: """Oyna bo'yicha sanaydi va watermark bilan yopadi. kechikish: watermark hodisa vaqtidan qancha orqada yuradi. Katta qiymat — kam yo'qotish, ko'p kutish. Kichik — teskarisi. """ def __init__(self, oyna_daqiqa: int = 60, kechikish_daqiqa: int = 10) -> None: self.oyna_daqiqa = oyna_daqiqa self.kechikish = timedelta(minutes=kechikish_daqiqa) self.oynalar: dict[datetime, int] = defaultdict(int) self.yopilgan: dict[datetime, int] = {} self.watermark: datetime | None = None self.tashlangan = 0 def qabul(self, vaqt: datetime) -> None: """Bitta hodisani qabul qiladi.""" if self.watermark is None or vaqt - self.kechikish > self.watermark: self.watermark = vaqt - self.kechikish self._yop() oyna = oyna_boshi(vaqt, self.oyna_daqiqa) if oyna in self.yopilgan: self.tashlangan += 1 # kech keldi, oyna allaqachon yopilgan return self.oynalar[oyna] += 1 def _yop(self) -> None: """Watermark dan oldingi oynalarni yopadi.""" chegara = self.watermark - timedelta(minutes=self.oyna_daqiqa) for oyna in [o for o in self.oynalar if o < chegara]: self.yopilgan[oyna] = self.oynalar.pop(oyna) def hammasi(self) -> dict[str, Any]: return { "yopilgan_oynalar": len(self.yopilgan), "ochiq_oynalar": len(self.oynalar), "tashlangan_hodisa": self.tashlangan, }
python3 -c " import json import pandas as pd from kafka import KafkaProducer from kafka.admin import KafkaAdminClient, NewTopic a = KafkaAdminClient(bootstrap_servers='localhost:19092') if 'hodisa' not in a.list_topics(): a.create_topics([NewTopic(name='hodisa', num_partitions=1, replication_factor=1)]) d = pd.read_csv('../datasets/ballon.csv').head(5000) p = KafkaProducer(bootstrap_servers='localhost:19092', value_serializer=lambda v: json.dumps(v).encode()) for r in d.itertuples(): p.send('hodisa', value={'vaqt': r.vaqt, 'ombor': r.ombor}) p.flush() print(f'{len(d):,} hodisa yuborildi') " 2>&1 | grep -v Warning
5,000 hodisa yuborildi
Endi ularni oynalar bo'yicha hisoblaymiz:
python3 -c " import sys, json; sys.path.insert(0, 'src') from datetime import datetime from kafka import KafkaConsumer from quvur.oqim import OynaliHisob h = OynaliHisob(oyna_daqiqa=60, kechikish_daqiqa=10) c = KafkaConsumer('hodisa', bootstrap_servers='localhost:19092', auto_offset_reset='earliest', group_id='oyna1', consumer_timeout_ms=6000) n = 0 for m in c: v = json.loads(m.value) h.qabul(datetime.fromisoformat(v['vaqt'])) n += 1 c.close() print(f'qayta ishlangan hodisa: {n:,}') for k, val in h.hammasi().items(): print(f' {k:22s}: {val:,}') " 2>&1 | grep -v Warning
qayta ishlangan hodisa: 5,000 yopilgan_oynalar : 90 ochiq_oynalar : 1 tashlangan_hodisa : 0
Watermark: oynani qachon yopish kerak
Mana Sardorning savoliga javob.
Watermark — bu "men shu vaqtgacha bo'lgan hamma narsani ko'rdim deb hisoblayman" degan chiziq. U hodisa vaqtidan biroz orqada yuradi.
Sardor uchun bu shunday bo'lardi: "Soat 21:00 bo'ldi, lekin men hisobni 21:10 da yopaman. O'n daqiqa kechikkan cheklarni hali qabul qilaman."
Diqqat qiling, watermark soat emas. U devordagi soatga qaramaydi.
U kelgan hodisalarga qaraydi. Agar eng yangi chek 21:05 da chiqarilgan bo'lsa, watermark 21:05 minus 10 daqiqa = 20:55 bo'ladi. Ya'ni "men 20:55 gacha bo'lgan hamma chekni ko'rdim deb hisoblayman".
Nega devordagi soat emas? Chunki hodisalar butunlay to'xtashi mumkin — kechasi do'kon yopiladi. Devor soati yuraveradi va barcha oynalarni yopib tashlaydi, holbuki ertalab kechikkan chek kelishi mumkin.
Watermark ma'lumot bilan birga yuradi, vaqt bilan emas.
Endi asosiy savol: kechikishni qancha qo'yish kerak? Bu savdo (trade-off):
- Kichik kechikish (1 daqiqa) — natija tez tayyor, lekin ko'p hodisa yo'qoladi.
- Katta kechikish (60 daqiqa) — deyarli hech narsa yo'qolmaydi, lekin natija bir soat kutadi.
O'lchab ko'ramiz. Bir xil ma'lumot, turli kechikish:
python3 -c " import sys, json; sys.path.insert(0, 'src') from datetime import datetime from kafka import KafkaConsumer from quvur.oqim import OynaliHisob hodisalar = [] c = KafkaConsumer('hodisa', bootstrap_servers='localhost:19092', auto_offset_reset='earliest', group_id='oyna2', consumer_timeout_ms=6000) for m in c: hodisalar.append(datetime.fromisoformat(json.loads(m.value)['vaqt'])) c.close() # hodisalar tartibsiz kelsin (haqiqiy oqimdagidek) import random random.seed(42) aralash = hodisalar[:] for i in range(0, len(aralash) - 50, 50): j = i + random.randint(1, 49) aralash[i], aralash[j] = aralash[j], aralash[i] sarlavha = ['kechikish', 'yopilgan', 'tashlangan', 'yoqotish'] print(f'{sarlavha[0]:>12s} {sarlavha[1]:>10s} {sarlavha[2]:>12s} {sarlavha[3]:>10s}') print('-' * 48) for k in (0, 1, 5, 10, 30, 60): h = OynaliHisob(oyna_daqiqa=60, kechikish_daqiqa=k) for v in aralash: h.qabul(v) r = h.hammasi() ulush = r['tashlangan_hodisa'] / len(aralash) * 100 print(f'{k:>10d}daq {r[\"yopilgan_oynalar\"]:>10,d} {r[\"tashlangan_hodisa\"]:>12,d} {ulush:>9.2f}%') " 2>&1 | grep -v Warning
kechikish yopilgan tashlangan yoqotish
------------------------------------------------
0daq 90 758 15.16%
1daq 90 696 13.92%
5daq 90 586 11.72%
10daq 90 285 5.70%
30daq 90 81 1.62%
60daq 89 0 0.00%
Jadval savdoni aniq ko'rsatadi: kechikish oshgani sari tashlangan hodisalar kamayadi.
Watermark ni "xavfsizlik uchun" katta qo'yish
"Ehtiyot shart 6 soat qo'yaman" degan qaror mantiqiy tuyuladi, lekin u oqimning butun ma'nosini yo'q qiladi.
Oyna 6 soat yopilmasa, natija ham 6 soat kechikadi. Siz batch quvurini qurdingiz — faqat ancha murakkab kod bilan.
To'g'ri yo'l: haqiqiy kechikishni o'lchang (hodisa vaqti va kelish vaqti farqini yozib boring), keyin 99-persentilga qarab qiymat tanlang.
Kech kelgan ma'lumot jimgina yo'qoladi
Yuqoridagi kodda tashlangan hisoblagichi bor. Ko'p tizimda bunday hisoblagich yo'q.
Natijada kechki hisobot 47 o'rniga 46 chiqadi va hech kim sezmaydi. Bir yildan keyin kimdir "bizning raqamlar har doim biroz kam" deb aytadi, lekin sababni topa olmaydi.
Qoida: tashlangan hodisalarni doim sanang va monitoringga chiqaring (34-bob).
Tartibsizlik qayerdan keladi
Nega hodisalar tartibsiz keladi? Uch sabab:
- Tarmoq. Bitta paket boshqa marshrut bilan ketdi va kechikdi.
- Partition. 10-bobni eslang: tartib faqat partition ichida kafolatlanadi. Uch partitiondan o'qiyotgan iste'molchi ularni aralash ko'radi.
- Qurilma oflayn edi. Kuryerning telefoni tunnel ichida internetsiz qoldi; u chiqqach yig'ilgan hodisalarni birdan yubordi.
Uchinchisi eng yomoni: kechikish daqiqalar emas, soatlar bo'lishi mumkin. Shuning uchun mobil ilovali tizimlarda kech kelgan ma'lumotni butunlay tashlamaydilar — alohida "tuzatish" quvuriga yuboradilar.
Tozalash
docker rm -f kitob-kafka > /dev/null 2>&1 && echo "kitob-kafka o'chirildi" docker ps --format '{{.Names}}'
kitob-kafka o'chirildi novavivo-postgres novavivo-redis stratix-med-frontend-1
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Oqim | Cheksiz, shuning uchun oynalarga bo'linadi |
| Hodisa vaqti | Voqea sodir bo'lgan payt — hisobot shunga tayanadi |
| Qayta ishlash vaqti | Hodisa yetib kelgan payt — haqiqatni buzadi |
| Tumbling oyna | Ustma-ust tushmaydi, soatlik hisobot uchun |
| Sliding oyna | Ustma-ust tushadi, trend uchun |
| Session oyna | Jimlik bilan ajraladi, tashrif uchun |
| Watermark | "Shu vaqtgacha hammasini ko'rdim" chizig'i |
| Kechikish qiymati | Kichik — tez, lekin yo'qotish; katta — aniq, lekin sekin |
| Kech kelgan ma'lumot | Doim sanang, aks holda jim yo'qoladi |
| Tartibsizlik sabablari | Tarmoq, partition, oflayn qurilma |
Talaba ishi
- Oyna 60 daqiqa emas, 10 daqiqa bo'lsa tashlangan hodisalar qanday o'zgaradi? Nega?
- Aralashtirish kuchini oshiring (
random.randint(1, 200)). Qaysi kechikish qiymati endi yetarli bo'ladi? OynaliHisobga sliding oyna qo'shing: har 10 daqiqada oxirgi 60 daqiqani hisoblasin.- Tashlangan hodisalarni tashlab yubormasdan alohida ro'yxatga yozing. Ularning hodisa vaqtlari qanday taqsimlangan?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.