II qism loyihasi: uchtasini bitta quvurga yig'ish
Topshiriq
Bu bobda yangi tushuncha yo'q. Bu — imtihon.
RostUy sizga aniq vazifa berdi:
Uy egasi saytda narxni o'zgartirsa yoki e'lonni o'chirsa, bu bir daqiqa ichida hisobot panelida ko'rinsin. Buzuq ma'lumot panelga tushmasin. Quvur kechasi yiqilsa, ertalab o'zi tiklansin.
Uch talab, va ularning har biri II qismdagi alohida bobga tegishli:
| Talab | Qaysi bob | Nima ishlatiladi |
|---|---|---|
| O'zgarish darhol ko'rinsin | 11-bob | CDC — WAL dan o'qish |
| Ishonchli yetkazish | 10-bob | Kafka — jurnal |
| Buzuq ma'lumot tushmasin | 6, 20-bob | Sifat tekshiruvi + karantin |
| Tarix va sxema | 15, 16-bob | Iceberg — bronza/kumush/oltin |
| Panel tez bo'lsin | 19-bob | Oldindan hisoblangan oltin qatlam |
| O'zi tiklansin | 20-bob | Qayta urinish + idempotentlik |
diagramma( { "Postgres\n(RostUy)": (0, 1.8, 2.0, 1.1, "manba"), "CDC\n(WAL slot)": (2.8, 1.8, 1.9, 1.1, "qayta"), "Kafka\n(jurnal)": (5.4, 1.8, 1.8, 1.1, "qayta"), "Sifat\n+ karantin": (8.0, 1.8, 2.0, 1.1, "qayta"), "Karantin\nfayli": (8.0, 0.1, 2.0, 0.8, "kuzatuv"), "Iceberg\nbronza": (10.8, 3.2, 1.9, 0.9, "saqlash"), "Iceberg\nkumush": (10.8, 1.9, 1.9, 0.9, "saqlash"), "Oltin\n(panel)": (10.8, 0.6, 1.9, 0.9, "xizmat"), }, [ ("Postgres\n(RostUy)", "CDC\n(WAL slot)"), ("CDC\n(WAL slot)", "Kafka\n(jurnal)"), ("Kafka\n(jurnal)", "Sifat\n+ karantin"), ("Sifat\n+ karantin", "Karantin\nfayli", "yomon"), ("Sifat\n+ karantin", "Iceberg\nbronza", "toza"), ("Iceberg\nbronza", "Iceberg\nkumush"), ("Iceberg\nkumush", "Oltin\n(panel)"), ], nom="de_loyiha.png", kenglik=14, izoh="II qismning barcha boblari bitta quvurda. Har quti - bitta bob.", )

II qismning barcha boblari bitta quvurda. Har quti - bitta bob.
Infratuzilmani ko'taramiz
docker rm -f kitob-pg kitob-kafka > /dev/null 2>&1 docker run -d --name kitob-pg -p 15455:5432 \ -e POSTGRES_PASSWORD=kitob -e POSTGRES_DB=rostuy \ postgres:16 -c wal_level=logical -c max_replication_slots=4 -c max_wal_senders=4 > /dev/null 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 docker exec kitob-pg pg_isready -U postgres > /dev/null 2>&1 && { echo "postgres ${i}s da tayyor"; break; } sleep 1 done 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}s da tayyor"; break; } sleep 1 done
postgres 1s da tayyor kafka 1s da tayyor
Quvur kodi
Bitta modul — har bosqich alohida funksiya. Har biri o'z bobidan kelgan.
shu faylni yaratingsrc/quvur/loyiha.py
"""II qism loyihasi: CDC -> Kafka -> sifat -> Iceberg -> oltin.""" import json from typing import Any, Iterator import pandas as pd USTUNLAR = ["elon_id", "tuman", "narx_mln", "xonalar", "holat"] # 6-bob: chegaralar. 20-bob: buzilgani karantinga. QOIDA = { "narx_mln": lambda s: s.between(0.5, 200), "xonalar": lambda s: s.between(1, 10), "tuman": lambda s: s.notna() & (s.str.len() > 2), } def hodisadan_qator(hodisa: dict[str, Any]) -> dict[str, Any] | None: """CDC hodisasini jadval qatoriga aylantiradi. DELETE -> holat='ochirildi'.""" q = hodisa["qiymat"] if hodisa["amal"] == "DELETE": return {"elon_id": q.get("elon_id"), "tuman": None, "narx_mln": None, "xonalar": None, "holat": "ochirildi"} return { "elon_id": q.get("elon_id"), "tuman": q.get("tuman"), "narx_mln": float(q["narx_mln"]) if q.get("narx_mln") else None, "xonalar": int(q["xonalar"]) if q.get("xonalar") else None, "holat": q.get("holat") or "faol", } def sifat_ajrat(d: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: """Toza va karantin qatorlarga ajratadi. O'chirilganlar tekshirilmaydi.""" ochirilgan = d["holat"] == "ochirildi" yaxshi = pd.Series(True, index=d.index) for ustun, tekshir in QOIDA.items(): yaxshi &= tekshir(d[ustun]) | ochirilgan return d[yaxshi], d[~yaxshi] def oltin_qatlam(kumush: pd.DataFrame) -> pd.DataFrame: """Panel uchun oldindan hisoblangan javob (19-bob).""" faol = kumush[kumush["holat"] != "ochirildi"] return (faol.groupby("tuman") .agg(elonlar=("elon_id", "count"), ort_narx=("narx_mln", "mean")) .round(3) .reset_index() .sort_values("ort_narx", ascending=False))
1-bosqich: manba va CDC sloti
Tartib muhim (11-bob): avval slot, keyin snapshot.
python3 -c " import psycopg2, pandas as pd c = psycopg2.connect(host='localhost', port=15455, user='postgres', password='kitob', dbname='rostuy') c.autocommit = True k = c.cursor() k.execute('''DROP TABLE IF EXISTS elon; CREATE TABLE elon (elon_id text PRIMARY KEY, tuman text, narx_mln numeric, xonalar int, holat text DEFAULT 'faol');''') k.execute(\"SELECT count(*) FROM pg_replication_slots WHERE slot_name='loyiha_slot'\") if k.fetchone()[0]: k.execute(\"SELECT pg_drop_replication_slot('loyiha_slot')\") k.execute(\"SELECT pg_create_logical_replication_slot('loyiha_slot','test_decoding')\") print('1. slot ochildi (snapshotdan OLDIN)') d = pd.read_csv('../datasets/ijara.csv').drop_duplicates('elon_id').head(4000) k.executemany('INSERT INTO elon (elon_id,tuman,narx_mln,xonalar) VALUES (%s,%s,%s,%s)', [(r.elon_id, r.tuman, float(r.narx_mln), int(r.xonalar)) for r in d.itertuples()]) print(f'2. snapshot yuklandi: {k.execute(\"SELECT count(*) FROM elon\") or c.cursor()}', end='') k.execute('SELECT count(*) FROM elon') print(f' {k.fetchone()[0]:,} qator') c.close() "
1. slot ochildi (snapshotdan OLDIN) 2. snapshot yuklandi: <cursor object at 0x7f0e9a913d30; closed: 0> 4,000 qator
2-bosqich: kun davomidagi o'zgarishlar
Haqiqiy kunni taqlid qilamiz — shu jumladan buzuq ma'lumot ham.
python3 -c " import psycopg2 c = psycopg2.connect(host='localhost', port=15455, user='postgres', password='kitob', dbname='rostuy') c.autocommit = True k = c.cursor() k.execute(\"UPDATE elon SET narx_mln = narx_mln * 1.15 WHERE tuman = 'Yunusobod'\") n1 = k.rowcount k.execute(\"DELETE FROM elon WHERE elon_id IN (SELECT elon_id FROM elon ORDER BY elon_id LIMIT 120)\") n2 = k.rowcount k.execute(\"\"\"INSERT INTO elon (elon_id,tuman,narx_mln,xonalar) SELECT 'YANGI'||g, 'Chilonzor', 6.5, 2 FROM generate_series(1,80) g\"\"\") n3 = k.rowcount # ataylab buzuq: manfiy narx va imkonsiz xona soni k.execute(\"UPDATE elon SET narx_mln = -3 WHERE elon_id LIKE 'YANGI1%'\") n4 = k.rowcount k.execute(\"UPDATE elon SET xonalar = 99 WHERE elon_id LIKE 'YANGI2%'\") n5 = k.rowcount print(f'narx oshirildi : {n1:,}') print(f'ochirildi : {n2:,}') print(f'yangi qoshildi : {n3:,}') print(f'BUZUQ (manfiy narx): {n4}') print(f'BUZUQ (99 xona) : {n5}') print(f'jami ozgarish : {n1+n2+n3+n4+n5:,}') c.close() "
narx oshirildi : 435 ochirildi : 120 yangi qoshildi : 80 BUZUQ (manfiy narx): 11 BUZUQ (99 xona) : 11 jami ozgarish : 657
3-bosqich: CDC dan Kafka ga (qayta urinish bilan)
python3 -c " import sys, collections; sys.path.insert(0, 'src') import psycopg2 from kafka import KafkaProducer from kafka.admin import KafkaAdminClient, NewTopic from quvur.cdc import oqim, kafka_xabari from quvur.chidamlilik import qayta_urin a = KafkaAdminClient(bootstrap_servers='localhost:19092') if 'elon-cdc' not in a.list_topics(): a.create_topics([NewTopic(name='elon-cdc', num_partitions=3, replication_factor=1)]) c = psycopg2.connect(host='localhost', port=15455, user='postgres', password='kitob', dbname='rostuy') c.autocommit = True k = c.cursor() p = qayta_urin(lambda: KafkaProducer(bootstrap_servers='localhost:19092'), urinish=3) tur = collections.Counter() for h in oqim(k, 'loyiha_slot'): kalit, qiymat = kafka_xabari(h) p.send('elon-cdc', key=kalit.encode(), value=qiymat) tur[h['amal']] += 1 p.flush() print('Kafka ga yuborildi:', dict(tur)) print('jami:', sum(tur.values())) c.close() " 2>&1 | grep -v Warning
Kafka ga yuborildi: {'INSERT': 4080, 'UPDATE': 457, 'DELETE': 120}
jami: 4657
Diqqat: INSERT soni snapshot (4000) + yangi (80) ni ham o'z ichiga oladi, chunki slot
snapshotdan oldin ochilgan edi. Aynan shu kerak — hech narsa yo'qolmadi.
4-bosqich: sifat va karantin
python3 -c " import sys, json; sys.path.insert(0, 'src') import pandas as pd from kafka import KafkaConsumer from quvur.loyiha import hodisadan_qator, sifat_ajrat c = KafkaConsumer('elon-cdc', bootstrap_servers='localhost:19092', auto_offset_reset='earliest', group_id='loyiha', consumer_timeout_ms=8000) qatorlar = [] for m in c: q = hodisadan_qator(json.loads(m.value)) if q and q['elon_id']: qatorlar.append(q) c.close() d = pd.DataFrame(qatorlar) toza, karantin = sifat_ajrat(d) toza.to_parquet('bronza.parquet', index=False) karantin.to_parquet('karantin.parquet', index=False) ulush = len(karantin) / len(d) * 100 print(f'Kafka dan oqildi : {len(d):,} hodisa') print(f'toza : {len(toza):,}') print(f'karantin : {len(karantin):,} ({ulush:.2f}%)') print() print(f'chegara 5% -> {\"DAVOM ETADI\" if ulush < 5 else \"TOXTAYDI\"}') if len(karantin): print() print('karantin namunasi:') print(karantin.head(3).to_string(index=False)) " 2>&1 | grep -v Warning
Kafka dan oqildi : 4,657 hodisa toza : 4,635 karantin : 22 (0.47%) chegara 5% -> DAVOM ETADI karantin namunasi: elon_id tuman narx_mln xonalar holat YANGI12 Chilonzor -3.0 2.0 faol YANGI18 Chilonzor -3.0 2.0 faol YANGI21 Chilonzor 6.5 99.0 faol
Buzuq qatorlar ajratildi, quvur esa to'xtamadi. 20-bobdagi qoida bajarildi.
5-bosqich: bronza → kumush → oltin
python3 -c " import sys; sys.path.insert(0, 'src') import pandas as pd from quvur.loyiha import oltin_qatlam bronza = pd.read_parquet('bronza.parquet') # KUMUSH: bir elon_id bo'yicha OXIRGI holat (idempotent - 20-bob) kumush = bronza.drop_duplicates('elon_id', keep='last') kumush.to_parquet('kumush.parquet', index=False) oltin = oltin_qatlam(kumush) oltin.to_parquet('oltin.parquet', index=False) print(f'bronza (hamma hodisa) : {len(bronza):,}') print(f'kumush (oxirgi holat) : {len(kumush):,}') print(f'oltin (panel javobi) : {len(oltin):,}') print() ochirilgan = (kumush['holat'] == 'ochirildi').sum() print(f'ochirilgan elonlar : {ochirilgan:,} (panelda KORINMAYDI)') print(f'faol elonlar : {len(kumush) - ochirilgan:,}') print() print(oltin.head(5).to_string(index=False)) "
bronza (hamma hodisa) : 4,635
kumush (oxirgi holat) : 4,080
oltin (panel javobi) : 10
ochirilgan elonlar : 120 (panelda KORINMAYDI)
faol elonlar : 3,960
tuman elonlar ort_narx
Yakkasaroy 356 7.849
Yunusobod 420 7.826
Mirobod 358 7.575
Mirzo Ulug'bek 412 6.621
Chilonzor 448 5.693
Mana birinchi talab bajarildi: o'chirilgan e'lonlar panelga tushmadi. 11-bobdagi telefon qo'ng'irog'i endi bo'lmaydi.
6-bosqich: idempotentlik sinovi
Eng muhim sinov, va u bitta savolga javob beradi:
Tunda quvur yiqilsa, ertalab uni bemalol qayta ishga tushira olamanmi?
Bu savol texnik ko'rinadi, lekin aslida u ishonch haqida.
Agar javob "bilmayman" bo'lsa, siz har tungi nosozlikda qo'rqib turasiz. Quvurni qayta ishga tushirishdan oldin tekshirasiz, hisoblaysiz, hamkasbingizdan so'raysiz. Ba'zan tuzatishdan ko'ra qo'rquv ko'proq vaqt oladi.
Agar javob "ha" bo'lsa, siz shunchaki tugmani bosasiz.
20-bobda ikki xil yozishni ko'rgandik: qo'shib yozish har safar qatorlarni ko'paytirdi,
almashtirib yozish esa o'zgartirmadi. Bu quvurda biz ikkinchisini qo'lladik —
drop_duplicates('elon_id', keep='last').
Endi buni isbotlaymiz. Quvurni boshidan qayta ishga tushiramiz va natijani oldingisi bilan aynan solishtiramiz.
Diqqat: bu yerda "taxminan bir xil" yetarli emas. equals() ishlatamiz — u har qiymatni
solishtiradi.
python3 -c " import sys, json; sys.path.insert(0, 'src') import pandas as pd from kafka import KafkaConsumer from quvur.loyiha import hodisadan_qator, sifat_ajrat, oltin_qatlam oldingi = pd.read_parquet('oltin.parquet') # butun quvurni BOSHIDAN qayta ishga tushiramiz (yangi group_id = hammasini qayta oqiydi) c = KafkaConsumer('elon-cdc', bootstrap_servers='localhost:19092', auto_offset_reset='earliest', group_id='loyiha-qayta', consumer_timeout_ms=8000) qatorlar = [hodisadan_qator(json.loads(m.value)) for m in c] c.close() d = pd.DataFrame([q for q in qatorlar if q and q['elon_id']]) toza, _ = sifat_ajrat(d) kumush = toza.drop_duplicates('elon_id', keep='last') yangi = oltin_qatlam(kumush) print(f'1-ishga tushirish: {len(oldingi)} qator') print(f'2-ishga tushirish: {len(yangi)} qator') print() bir_xil = oldingi.reset_index(drop=True).equals(yangi.reset_index(drop=True)) print(f'natijalar AYNAN bir xilmi: {bir_xil}') print() if bir_xil: print('Quvur IDEMPOTENT: qayta ishga tushirish xavfsiz.') else: print('FARQ BOR:') print(oldingi.head(3).to_string(index=False)) print(yangi.head(3).to_string(index=False)) " 2>&1 | grep -v Warning
1-ishga tushirish: 10 qator 2-ishga tushirish: 10 qator natijalar AYNAN bir xilmi: True Quvur IDEMPOTENT: qayta ishga tushirish xavfsiz.
True chiqdi — demak kechasi yiqilgan quvurni ertalab bemalol qayta ishga tushirish mumkin.
Ma'lumot ikki marta hisoblanmaydi.
Uch talab bajarildimi
import pandas as pd show(pd.DataFrame([ {"Talab": "O'zgarish darhol ko'rinadi", "Qanday": "CDC WAL dan o'qiydi, polling emas", "Bob": "11"}, {"Talab": "Hech narsa yo'qolmaydi", "Qanday": "Slot snapshotdan oldin ochildi", "Bob": "11"}, {"Talab": "O'chirilgan e'lon panelda yo'q", "Qanday": "DELETE hodisasi holat='ochirildi'", "Bob": "11, 17"}, {"Talab": "Buzuq ma'lumot tushmaydi", "Qanday": "Sifat tekshiruvi + karantin", "Bob": "6, 20"}, {"Talab": "Quvur to'xtamaydi", "Qanday": "Karantin chegarasi 5%", "Bob": "20"}, {"Talab": "Panel tez", "Qanday": "Oltin qatlam oldindan hisoblangan", "Bob": "19"}, {"Talab": "Qayta ishga tushirish xavfsiz", "Qanday": "drop_duplicates(keep='last')", "Bob": "20"}, ]))
| Talab | Qanday | Bob |
|---|---|---|
| O'zgarish darhol ko'rinadi | CDC WAL dan o'qiydi, polling emas | 11 |
| Hech narsa yo'qolmaydi | Slot snapshotdan oldin ochildi | 11 |
| O'chirilgan e'lon panelda yo'q | DELETE hodisasi holat='ochirildi' | 11, 17 |
| Buzuq ma'lumot tushmaydi | Sifat tekshiruvi + karantin | 6, 20 |
| Quvur to'xtamaydi | Karantin chegarasi 5% | 20 |
| Panel tez | Oltin qatlam oldindan hisoblangan | 19 |
| Qayta ishga tushirish xavfsiz | drop_duplicates(keep='last') | 20 |
Tozalash
docker rm -f kitob-pg kitob-kafka > /dev/null 2>&1 && echo "konteynerlar ochirildi" rm -f bronza.parquet kumush.parquet oltin.parquet karantin.parquet echo "vaqtinchalik fayllar tozalandi" docker ps --format '{{.Names}}'
konteynerlar ochirildi vaqtinchalik fayllar tozalandi novavivo-postgres novavivo-redis stratix-med-frontend-1
Nimani qurdingiz
Bu quvur kichik, lekin u haqiqiy. Unda quyidagilar bor:
- Ma'lumotni manbadan yo'qotmasdan olish
- Yetkazishni ishlab chiqaruvchidan ajratish
- Buzuq ma'lumotni to'xtatish, lekin quvurni to'xtatmaslik
- Xom ma'lumotni saqlab qolish (qayta hisoblash uchun)
- Panelga tayyor javob berish
- Xavfsiz qayta ishga tushirish
Etishmayotgani — jadval bo'yicha ishga tushirish (8-bob buni ko'rsatgan), monitoring (34-bob) va Kubernetes (13-bob).
Talaba ishi
- Karantin chegarasini 0.5% ga tushiring. Quvur to'xtadimi? Nega?
hodisadan_qatordaUPDATEhodisasini ataylab tashlab yuboring. Oltin qatlamdagi narxlar qanday o'zgardi?- Kumush qatlamda
keep='last'nikeep='first'ga almashtiring. Nega natija noto'g'ri bo'ladi? - Quvurni ikki marta ishga tushiring, lekin ikkinchisida Kafka ni o'chirib qo'ying.
qayta_urinnima qildi va oxirida qanday xato chiqdi? - Yangi talab qo'shing: "tuman bo'yicha broker ulushi ham panelda bo'lsin". Qaysi qatlamlarni o'zgartirish kerak?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.