Tez serving qatlami
Sahifa ochilmayapti
RostUy saytida foydalanuvchi filtr bosadi: "Yunusobod, 2 xona, 5 milliongacha".
Sahifa 4 soniya aylanadi.
Siz so'rovga qaraysiz — u to'g'ri yozilgan, 18-bobdagi barcha qoidalarga rioya qilingan. Reja toza. Indeks bor. Lekin baribir sekin.
Sabab boshqa joyda: bu so'rov ombor uchun yozilgan ombordan kelmoqda. Ombor esa boshqa ish uchun qurilgan.
Ikki xil mijoz
Farqni tushunish uchun ikki xil mijozni solishtiring.
Tahlilchi. Kuniga 20 ta so'rov yuboradi. Har biri butun jadvalni o'qiydi. 10 soniya kutishga tayyor — u choy ichib keladi.
Sayt foydalanuvchisi. Sekundiga 500 ta so'rov keladi. Har biri bir nechta qator so'raydi. 300 millisekunddan ko'p kutsa — sahifani yopadi.
import pandas as pd show(pd.DataFrame([ {"": "So'rovlar soni", "Tahlil (OLAP)": "Kuniga o'nlab", "Serving": "Sekundiga minglab"}, {"": "Har so'rov o'qiydi", "Tahlil (OLAP)": "Millionlab qator", "Serving": "O'nlab qator"}, {"": "Kutish chegarasi", "Tahlil (OLAP)": "Soniyalar", "Serving": "Millisekundlar"}, {"": "So'rov shakli", "Tahlil (OLAP)": "Oldindan noma'lum", "Serving": "Oldindan ma'lum"}, {"": "Nima muhim", "Tahlil (OLAP)": "Moslashuvchanlik", "Serving": "Kechikish (p99)"}, ]))
| Tahlil (OLAP) | Serving | |
|---|---|---|
| So'rovlar soni | Kuniga o'nlab | Sekundiga minglab |
| Har so'rov o'qiydi | Millionlab qator | O'nlab qator |
| Kutish chegarasi | Soniyalar | Millisekundlar |
| So'rov shakli | Oldindan noma'lum | Oldindan ma'lum |
| Nima muhim | Moslashuvchanlik | Kechikish (p99) |
Oxirgi qatorga e'tibor bering — so'rov shakli oldindan ma'lum. Sayt foydalanuvchisi istalgan so'rov yozmaydi; u faqat siz bergan filtrlarni bosadi.
Bu juda muhim, chunki ma'lum savolga oldindan tayyorlanish mumkin.
Oshxonadagi ikki usul
Restoranga qaytamiz.
Birinchi usul — buyurtma kelganda oshpaz noldan tayyorlaydi. Har taom yangi, istalgan o'zgartirish mumkin. Lekin 40 daqiqa kutasiz.
Ikkinchi usul — eng ko'p buyurtma qilinadigan beshta taom oldindan tayyorlab qo'yiladi. Buyurtma kelsa — 2 daqiqada chiqadi. Lekin menyudan tashqari narsa so'rasangiz, u yo'q.
Serving qatlami — ikkinchi usul. Siz ma'lum savollarga oldindan javob tayyorlaysiz.
Ma'lumotni tayyorlaymiz
python3 -c " import duckdb, pandas as pd d = pd.read_csv('../datasets/ijara.csv').drop_duplicates('elon_id') con = duckdb.connect('serving.db') con.execute('DROP TABLE IF EXISTS elon') con.execute('CREATE TABLE elon AS SELECT * FROM d') n = con.execute('SELECT count(*) FROM elon').fetchone()[0] print(f'elonlar: {n:,}') print(f'tumanlar: {con.execute(\"SELECT count(DISTINCT tuman) FROM elon\").fetchone()[0]}') con.close() "
elonlar: 11,906 tumanlar: 10
Sayt so'rovini o'lchaymiz
Foydalanuvchining filtri — bu sayt uchun eng ko'p takrorlanadigan so'rov.
python3 -c " import duckdb, time import numpy as np con = duckdb.connect('serving.db') SORQV = ''' SELECT elon_id, tuman, xonalar, narx_mln, maydon FROM elon WHERE tuman = ? AND xonalar = ? AND narx_mln <= ? ORDER BY narx_mln LIMIT 20 ''' def olch(n=300): con.execute(SORQV, ['Yunusobod', 2, 5.0]).fetchall() v = [] for _ in range(n): t0 = time.perf_counter() con.execute(SORQV, ['Yunusobod', 2, 5.0]).fetchall() v.append((time.perf_counter()-t0)*1000) return np.array(v) v = olch() print(f'p50 : {np.percentile(v,50):6.2f} ms') print(f'p95 : {np.percentile(v,95):6.2f} ms') print(f'p99 : {np.percentile(v,99):6.2f} ms') print() print(f'natija: {len(con.execute(SORQV, [\"Yunusobod\", 2, 5.0]).fetchall())} elon') con.close() "
p50 : 1.08 ms p95 : 1.25 ms p99 : 1.47 ms natija: 20 elon
Bu raqamlar yaxshi ko'rinadi. Lekin diqqat: bizda atigi 12 ming qator bor. RostUy o'ssa va 1 million e'lon bo'lsa nima bo'ladi?
python3 -c " import duckdb, time import numpy as np con = duckdb.connect('serving.db') con.execute('DROP TABLE IF EXISTS katta') con.execute('''CREATE TABLE katta AS SELECT e.* FROM elon e, generate_series(1, 84) g''') n = con.execute('SELECT count(*) FROM katta').fetchone()[0] print(f'katta jadval: {n:,} qator') SORQV = '''SELECT elon_id, tuman, xonalar, narx_mln FROM {t} WHERE tuman = ? AND xonalar = ? AND narx_mln <= ? ORDER BY narx_mln LIMIT 20''' def olch(t, n=60): q = SORQV.format(t=t) con.execute(q, ['Yunusobod', 2, 5.0]).fetchall() v = [] for _ in range(n): t0 = time.perf_counter() con.execute(q, ['Yunusobod', 2, 5.0]).fetchall() v.append((time.perf_counter()-t0)*1000) return np.array(v) a, b = olch('elon'), olch('katta') print() print(f'{\"jadval\":10s} {\"qatorlar\":>12s} {\"p50\":>9s} {\"p99\":>9s}') print('-' * 44) print(f'{\"elon\":10s} {12000:>12,} {np.percentile(a,50):>8.2f}ms {np.percentile(a,99):>8.2f}ms') print(f'{\"katta\":10s} {n:>12,} {np.percentile(b,50):>8.2f}ms {np.percentile(b,99):>8.2f}ms') print() print(f'{n/12000:.0f}x koproq malumot -> {np.percentile(b,50)/np.percentile(a,50):.1f}x sekin') con.close() "
katta jadval: 1,000,104 qator jadval qatorlar p50 p99 -------------------------------------------- elon 12,000 1.09ms 1.65ms katta 1,000,104 2.08ms 11.49ms 83x koproq malumot -> 1.9x sekin
Bu yerda to'xtang va raqamlarni diqqat bilan o'qing — men bu bobni yozayotganda adashgandim.
Men "ma'lumot 83 barobar o'ssa, kechikish ham shunga yaqin o'sadi" deb kutgandim. O'lchov buni rad etdi: 83 barobar ko'p ma'lumot atigi 2 barobar sekinlashtirdi.
Nega? Ikki sabab:
- DuckDB ustunli. U faqat kerakli to'rt ustunni o'qiydi, qolganini emas (9-bob).
- Filtr juda tanlovchi. "Yunusobod + 2 xona + 5 mln gacha" million qatordan bir necha yuztasini qoldiradi. Qolgan qatorlar statistika yordamida butunlay o'tkazib yuboriladi.
Demak muammo hajmda emas. Unda nimada?
Muammo so'rovlar sonida. 2 ms bitta so'rov uchun ajoyib. Lekin sekundiga 500 ta so'rov kelsa, bu sekundiga 1000 ms protsessor vaqti degani — bitta yadro to'liq band. Ming foydalanuvchi bo'lsa, server yiqiladi.
Serving qatlamining maqsadi shuning uchun "katta ma'lumotni tez o'qish" emas, balki bir xil ishni qayta-qayta qilmaslik.
Birinchi yechim: oldindan hisoblash
Foydalanuvchi filtrlari cheklangan: 10 tuman × 5 xona varianti × bir nechta narx oralig'i. Bularning hammasini oldindan hisoblab qo'yish mumkin.
python3 -c " import duckdb, time import numpy as np con = duckdb.connect('serving.db') t0 = time.perf_counter() con.execute('DROP TABLE IF EXISTS tayyor') con.execute('''CREATE TABLE tayyor AS SELECT tuman, xonalar, CAST(ceil(narx_mln / 2.5) * 2.5 AS DOUBLE) AS narx_chegara, count(*) AS elonlar, round(avg(narx_mln), 2) AS ort_narx, round(avg(maydon), 1) AS ort_maydon, round(avg(broker), 3) AS broker_ulushi FROM katta GROUP BY tuman, xonalar, narx_chegara''') qurish = (time.perf_counter()-t0)*1000 n = con.execute('SELECT count(*) FROM tayyor').fetchone()[0] print(f'oldindan hisoblash: {qurish:.0f} ms (bir marta)') print(f'natija jadvali : {n:,} qator') print() def olch(q, args, n=200): con.execute(q, args).fetchall() v = [] for _ in range(n): t0 = time.perf_counter() con.execute(q, args).fetchall() v.append((time.perf_counter()-t0)*1000) return np.array(v) xom_q = '''SELECT count(*), avg(narx_mln) FROM katta WHERE tuman = ? AND xonalar = ? AND narx_mln <= ?''' tayyor_q = '''SELECT sum(elonlar), avg(ort_narx) FROM tayyor WHERE tuman = ? AND xonalar = ? AND narx_chegara <= ?''' a = olch(xom_q, ['Yunusobod', 2, 5.0]) b = olch(tayyor_q, ['Yunusobod', 2, 5.0]) print(f'xom jadvaldan : p50 {np.percentile(a,50):7.3f} ms | p99 {np.percentile(a,99):7.3f} ms') print(f'tayyor jadvaldan: p50 {np.percentile(b,50):7.3f} ms | p99 {np.percentile(b,99):7.3f} ms') print() print(f'{np.percentile(a,50)/np.percentile(b,50):.0f}x tez') con.close() "
oldindan hisoblash: 51 ms (bir marta) natija jadvali : 165 qator xom jadvaldan : p50 1.580 ms | p99 5.537 ms tayyor jadvaldan: p50 0.398 ms | p99 0.696 ms 4x tez
Farq katta, va sababi oddiy: tayyor jadvalda bir necha yuz qator bor, xom jadvalda million.
Oldindan hisoblash — bepul tezlik emas
Uch narxi bor va ularni bilib turib to'lash kerak:
1. Eskirish. Tayyor jadval — bu suratga olingan holat. Yangi e'lon qo'shilsa, u yangilanmaguncha ko'rinmaydi. Har necha daqiqada qayta hisoblash kerak.
2. Moslashuvchanlik yo'qoladi. Biz narxni 2.5 million qadamga yaxlitladik. Foydalanuvchi "4.3 milliongacha" deb so'rasa — aniq javob yo'q.
3. Kombinatsiya portlashi. 10 tuman × 5 xona = 50 qator. Lekin yana maydon, qavat, rasmlar soni qo'shsangiz — minglab kombinatsiya. Har biri saqlanadi va yangilanadi.
Qoida: faqat haqiqatan tez-tez so'raladigan filtrlarni oldindan hisoblang. Qolganiga xom jadval javob bersin.
Ikkinchi yechim: kesh
Oldindan hisoblashning kamchiligini ko'rdik: kombinatsiya portlashi. Har yangi filtr qo'shsangiz, tayyorlash kerak bo'lgan javoblar soni ko'payib boradi.
Endi boshqa tomondan yondashamiz.
Oldindan hisoblash — bu hamma savolga javob tayyorlash, hatto hech kim so'ramaydiganiga ham. Restoran misolida: menyudagi 200 ta taomning hammasini oldindan pishirib qo'yish. Ko'pi sovib qoladi.
Kesh esa teskarisini qiladi: so'ralgan savolning javobini eslab qoladi.
Ya'ni siz oldindan hech narsa tayyorlamaysiz. Birinchi mijoz osh so'raydi — pishirasiz (sekin). Ikkinchi mijoz ham osh so'raydi — tayyorini berasiz (tez).
Nega bu ishlaydi? Chunki mijozlar tasodifiy tanlamaydi. Ular bir xil narsani so'rashadi.
RostUy da ham shunday: "Yunusobod, 2 xona" filtri kuniga minglab marta bosiladi, "Bektemir, 7 xona" esa oyiga bir marta.
Buni o'lchab ko'ramiz — haqiqiy trafikka o'xshash taqsimot bilan.
python3 -c " import duckdb, time, functools import numpy as np con = duckdb.connect('serving.db') Q = '''SELECT count(*), avg(narx_mln) FROM katta WHERE tuman = ? AND xonalar = ? AND narx_mln <= ?''' def xom(tuman, xona, narx): return con.execute(Q, [tuman, xona, narx]).fetchall() @functools.lru_cache(maxsize=256) def keshli(tuman, xona, narx): return con.execute(Q, [tuman, xona, narx]).fetchall() # haqiqiy trafik: bir nechta filtr juda ko'p takrorlanadi rng = np.random.default_rng(42) tumanlar = [r[0] for r in con.execute('SELECT DISTINCT tuman FROM elon').fetchall()] # 80% sorov eng mashhur 3 ta filtrga tushadi mashhur = [(tumanlar[0], 2, 5.0), (tumanlar[1], 3, 8.0), (tumanlar[2], 1, 4.0)] sorovlar = [] for _ in range(400): if rng.random() < 0.8: sorovlar.append(mashhur[rng.integers(0, 3)]) else: sorovlar.append((tumanlar[rng.integers(0, len(tumanlar))], int(rng.integers(1, 5)), float(rng.integers(3, 12)))) for nom, f in (('keshsiz', xom), ('keshli', keshli)): if nom == 'keshli': keshli.cache_clear() t0 = time.perf_counter() for s in sorovlar: f(*s) dt = (time.perf_counter()-t0)*1000 print(f'{nom:8s}: {len(sorovlar)} sorov {dt:8.1f} ms -> bitta sorov {dt/len(sorovlar):.3f} ms') i = keshli.cache_info() print() print(f'kesh topildi : {i.hits}') print(f'kesh topilmadi: {i.misses}') print(f'topilish ulushi: {i.hits/(i.hits+i.misses)*100:.1f}%') con.close() "
keshsiz : 400 sorov 708.4 ms -> bitta sorov 1.771 ms keshli : 400 sorov 84.1 ms -> bitta sorov 0.210 ms kesh topildi : 332 kesh topilmadi: 68 topilish ulushi: 83.0%
Kesh ishladi, chunki haqiqiy trafik teng taqsimlanmagan: bir nechta filtr juda ko'p takrorlanadi, qolganlari kamdan-kam.
Bu qonuniyat deyarli hamma joyda uchraydi va uning nomi bor — uzun dum (long tail).
Keshni bekor qilish — eng qiyin qism
Dasturlashda mashhur gap bor: "kompyuter fanidagi ikki qiyin masala — keshni bekor qilish va nom tanlash".
Muammo: e'lon narxi o'zgardi. Kesh esa eski javobni saqlab turibdi. Foydalanuvchi eski narxni ko'radi — 11-bobdagi telefon qo'ng'irog'i qaytadi.
Uch yechim bor: - TTL — javob N soniyadan keyin o'z-o'zidan o'chadi. Oddiy, lekin N soniya davomida ma'lumot eski. - O'zgarishda o'chirish — e'lon yangilanganda tegishli kesh o'chiriladi. Aniq, lekin "tegishli" qaysi ekanini bilish qiyin. - Versiya kaliti — kesh kalitiga ma'lumot versiyasi qo'shiladi. Yangi versiya — yangi kalit, eskisi o'zi eskiradi.
Sayt uchun odatda TTL yetarli (30-60 soniya). Narx va qoldiq uchun — o'zgarishda o'chirish.
Qaysi vositani tanlash
Bu bobda DuckDB ishlatdik, chunki u mahalliy va sodda. Ishlab chiqarishda tanlov boshqacha:
import pandas as pd show(pd.DataFrame([ {"Vosita": "PostgreSQL + indeks", "Kuchli tomoni": "Oddiy, allaqachon bor", "Qachon": "Sekundiga yuzlab so'rov"}, {"Vosita": "Redis", "Kuchli tomoni": "Xotirada, juda tez", "Qachon": "Kalit bo'yicha o'qish"}, {"Vosita": "ClickHouse", "Kuchli tomoni": "Ustunli, yig'ish uchun tez", "Qachon": "Real vaqt tahlil paneli"}, {"Vosita": "Apache Pinot / Druid", "Kuchli tomoni": "Oqimdan darhol o'qiydi", "Qachon": "Hodisa kelishi bilan panelda"}, ]))
| Vosita | Kuchli tomoni | Qachon |
|---|---|---|
| PostgreSQL + indeks | Oddiy, allaqachon bor | Sekundiga yuzlab so'rov |
| Redis | Xotirada, juda tez | Kalit bo'yicha o'qish |
| ClickHouse | Ustunli, yig'ish uchun tez | Real vaqt tahlil paneli |
| Apache Pinot / Druid | Oqimdan darhol o'qiydi | Hodisa kelishi bilan panelda |
Oddiydan boshlang
Ko'pchilik loyihada PostgreSQL + to'g'ri indeks + kesh yetarli. ClickHouse yoki Pinot qo'shishdan oldin uch savolga javob bering:
- Hozirgi p99 qancha? (o'lchadingizmi?)
- Maqsad qancha?
- 18-bobdagi oltita qadamni bajardingizmi?
Yangi baza qo'shish — yangi tizim, yangi nusxa, yangi nosozlik nuqtasi. U oxirgi chora bo'lsin.
Tozalash
python3 -c " import duckdb, pathlib con = duckdb.connect('serving.db') con.execute('DROP TABLE IF EXISTS katta') con.close() print('katta jadval ochirildi, serving.db qoldi') "
katta jadval ochirildi, serving.db qoldi
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Ikki xil mijoz | Tahlilchi soniya kutadi, foydalanuvchi kutmaydi |
| So'rov shakli | Serving da u oldindan ma'lum |
| Chiziqli o'sish | Ma'lumot o'ssa, xom so'rov sekinlashadi |
| Oldindan hisoblash | Ma'lum savolga tayyor javob |
| Uning narxi | Eskirish, moslashuvchanlik, kombinatsiya portlashi |
| Kesh | So'ralgan javobni eslab qolish |
| Uzun dum | Bir nechta so'rov trafikning ko'pini tashkil qiladi |
| Keshni bekor qilish | TTL, o'zgarishda o'chirish yoki versiya kaliti |
| Vosita tanlash | Avval Postgres + indeks + kesh; keyin o'ylang |
Talaba ishi
- Kesh o'lchamini 256 dan 8 ga tushiring. Topilish ulushi qanday o'zgardi?
- Trafikni teng taqsimlang (mashhur filtrlarsiz). Kesh hali ham foyda beradimi?
tayyorjadvalga maydon oralig'ini ham qo'shing. Qatorlar soni necha barobar oshdi?- Yangi e'lon qo'shing va
tayyorjadvalni yangilamang. Foydalanuvchi qanday noto'g'ri javob oladi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.