O'zini tuzatuvchi quvur
Soat 03:40 dagi qo'ng'iroq
Kechasi soat 03:40. Telefoningiz jiringlaydi.
— Quvur yiqildi. Ertalabki hisobot bo'lmaydi.
Siz turasiz, noutbukni ochasiz, log ga qaraysiz. Sabab:
ConnectionError: tashqi API javob bermadi (timeout)
Siz quvurni qayta ishga tushirasiz. U ishlaydi. Hammasi joyida. Yotasiz.
Ertasi kechasi — yana o'sha qo'ng'iroq.
Savol: siz o'sha tunda nima qildingiz? Hech narsa. Faqat "qayta urin" tugmasini bosdingiz. Buni dastur o'zi qila olmaydimi?
Aynan shu bobning mavzusi. Maqsad — quvurni xatosiz qilish emas (bu imkonsiz), balki o'zini tiklay oladigan qilish.
Xatolarning ikki turi
Eng muhim ajratish shu. Barcha xatolar ikki guruhga bo'linadi, va ularga teskari munosabat kerak.
O'tkinchi xato (transient) — o'z-o'zidan tuzaladi. Tarmoq uzildi, server band edi, API sekinlashdi. Yana urinsangiz — ishlaydi.
Doimiy xato (permanent) — qayta urinsangiz ham aynan o'sha natija. Fayl yo'q, parol noto'g'ri, ustun nomi xato.
import pandas as pd show(pd.DataFrame([ {"Xato": "Tarmoq timeout", "Turi": "O'tkinchi", "To'g'ri javob": "Qayta urinish"}, {"Xato": "HTTP 503 (server band)", "Turi": "O'tkinchi", "To'g'ri javob": "Qayta urinish"}, {"Xato": "HTTP 429 (juda ko'p so'rov)", "Turi": "O'tkinchi", "To'g'ri javob": "Kutib qayta urinish"}, {"Xato": "HTTP 404 (topilmadi)", "Turi": "Doimiy", "To'g'ri javob": "To'xtash, xabar berish"}, {"Xato": "HTTP 401 (parol xato)", "Turi": "Doimiy", "To'g'ri javob": "To'xtash, xabar berish"}, {"Xato": "Ustun nomi yo'q", "Turi": "Doimiy", "To'g'ri javob": "To'xtash, xabar berish"}, ]))
| Xato | Turi | To'g'ri javob |
|---|---|---|
| Tarmoq timeout | O'tkinchi | Qayta urinish |
| HTTP 503 (server band) | O'tkinchi | Qayta urinish |
| HTTP 429 (juda ko'p so'rov) | O'tkinchi | Kutib qayta urinish |
| HTTP 404 (topilmadi) | Doimiy | To'xtash, xabar berish |
| HTTP 401 (parol xato) | Doimiy | To'xtash, xabar berish |
| Ustun nomi yo'q | Doimiy | To'xtash, xabar berish |
Doimiy xatoni qayta urinish — eng qimmat xato
"Ehtiyot shart hammasini qayta urinaman" degan yondashuv zararli.
Parol noto'g'ri bo'lsa va siz 5 marta urinsangiz — ba'zi tizimlar hisobni bloklaydi. Fayl yo'q bo'lsa, 5 marta urinish 5 barobar ko'proq kutish demak, natija esa bir xil.
Eng yomoni: doimiy xato qayta urinishlar orasida yashirinadi. Log da "urinish 1, 2, 3..." ko'rinadi va haqiqiy sabab pastda qolib ketadi.
Qayta urinish: sodda emas, to'g'ri
Birinchi xayolga keladigan kod:
for i in range(3):
try:
return api_dan_ol()
except Exception:
continue
Bunda uch xato bor:
- Hamma xatoni ushlaydi — doimiy xato ham qayta urinadi.
- Kutmaydi — server band bo'lsa, uni darhol yana urib yanada band qiladi.
- Bir xil kutadi — yuzta quvur bir vaqtda yiqilsa, hammasi bir vaqtda qayta uradi va serverni ikkinchi marta yiqitadi.
Uchinchisi jiddiy muammo va uning nomi bor: momaqaldiroq podasi (thundering herd).
To'g'ri yechim uch qismdan iborat:
- Backoff — har urinishda ko'proq kutish (1s, 2s, 4s, 8s)
- Jitter — kutish vaqtiga tasodifiy qo'shimcha, hamma bir vaqtda urmasligi uchun
- Tanlab urinish — faqat o'tkinchi xatolarda
shu faylni yaratingsrc/quvur/chidamlilik.py
"""Qayta urinish, backoff, jitter va zanjir uzgich.""" import random import time from typing import Any, Callable OTKINCHI = (ConnectionError, TimeoutError) class DoimiyXato(Exception): """Qayta urinish foyda bermaydigan xato.""" def qayta_urin( f: Callable[[], Any], urinish: int = 4, asos: float = 0.1, jitter: bool = True, ) -> Any: """O'tkinchi xatolarda backoff bilan qayta uradi. asos: birinchi kutish (soniya). Har urinishda ikki barobar oshadi. """ oxirgi: Exception | None = None for n in range(urinish): try: return f() except DoimiyXato: raise # doimiy xato - darhol to'xta except OTKINCHI as e: oxirgi = e if n == urinish - 1: break kut = asos * (2 ** n) if jitter: kut *= 0.5 + random.random() # 50%..150% time.sleep(kut) raise RuntimeError(f"{urinish} urinishdan keyin ham muvaffaqiyatsiz: {oxirgi}") class ZanjirUzgich: """Circuit breaker: ketma-ket xatodan keyin urinishni butunlay to'xtatadi. Sabab: yiqilgan servisga urinaverish uni tiklanishga qo'ymaydi. """ def __init__(self, chegara: int = 5, tiklanish: float = 30.0) -> None: self.chegara = chegara self.tiklanish = tiklanish self.xatolar = 0 self.ochilgan: float | None = None @property def ochiqmi(self) -> bool: if self.ochilgan is None: return False if time.monotonic() - self.ochilgan > self.tiklanish: self.ochilgan, self.xatolar = None, 0 # yarim ochiq: yana sinab ko'ramiz return False return True def chaqir(self, f: Callable[[], Any]) -> Any: if self.ochiqmi: raise RuntimeError("zanjir uzilgan - urinish qilinmadi") try: natija = f() except Exception: self.xatolar += 1 if self.xatolar >= self.chegara: self.ochilgan = time.monotonic() raise self.xatolar = 0 return natija
Backoff ni o'lchaymiz
python3 -c " import sys, time; sys.path.insert(0, 'src') from quvur.chidamlilik import qayta_urin, DoimiyXato # 3-urinishda ishlaydigan soxta API holat = {'n': 0} def beqaror_api(): holat['n'] += 1 if holat['n'] < 3: raise ConnectionError('tarmoq uzildi') return 'maʼlumot keldi' t0 = time.perf_counter() natija = qayta_urin(beqaror_api, asos=0.1, jitter=False) dt = time.perf_counter() - t0 print(f'natija : {natija}') print(f'urinishlar : {holat[\"n\"]}') print(f'jami kutish : {dt*1000:.0f} ms (0.1s + 0.2s kutildi)') print() # doimiy xato - qayta urinmasligi kerak holat2 = {'n': 0} def buzuq_api(): holat2['n'] += 1 raise DoimiyXato('401: parol notogri') t0 = time.perf_counter() try: qayta_urin(buzuq_api, asos=0.1) except DoimiyXato as e: dt = time.perf_counter() - t0 print(f'doimiy xato : {e}') print(f'urinishlar : {holat2[\"n\"]} (qayta urinilmadi)') print(f'sarflangan : {dt*1000:.0f} ms') "
natija : maʼlumot keldi urinishlar : 3 jami kutish : 301 ms (0.1s + 0.2s kutildi) doimiy xato : 401: parol notogri urinishlar : 1 (qayta urinilmadi) sarflangan : 0 ms
Farqqa e'tibor bering: o'tkinchi xatoda uch urinish va kutish bo'ldi; doimiy xatoda esa bitta urinish va deyarli nol vaqt. Aynan shu kerak.
Jitter nega kerak
Yuzta quvur bir vaqtda yiqildi deb faraz qilamiz. Jitter bor va yo'q holatlarni solishtiramiz.
python3 -c " import random, collections def urinish_vaqtlari(n_quvur=100, jitter=False, asos=1.0, urinish=4): random.seed(7) vaqtlar = [] for _ in range(n_quvur): t = 0.0 for k in range(urinish): kut = asos * (2 ** k) if jitter: kut *= 0.5 + random.random() t += kut vaqtlar.append(round(t, 1)) return collections.Counter(vaqtlar) for nom, j in (('JITTERSIZ', False), ('JITTER BILAN', True)): c = urinish_vaqtlari(jitter=j) eng_zich = c.most_common(1)[0] print(f'{nom:14s} eng zich soniya: {eng_zich[0]:>5}s da {eng_zich[1]:>3} ta urinish, ' f'jami {len(c)} xil vaqt') "
JITTERSIZ eng zich soniya: 1.0s da 100 ta urinish, jami 4 xil vaqt JITTER BILAN eng zich soniya: 0.9s da 16 ta urinish, jami 147 xil vaqt
Jittersiz hamma quvur aynan bir soniyada uradi — server ikkinchi marta yiqiladi. Jitter bilan urinishlar vaqt bo'ylab tarqaladi.
Zanjir uzgich (circuit breaker)
Qayta urinish bitta so'rovni qutqaradi. Lekin servis butunlay yiqilgan bo'lsa-chi?
Unda qayta urinish zarar keltiradi, va zarar kutganingizdan kattaroq.
O'ylab ko'ring: servis yiqildi, chunki u ortiqcha yuklangan edi. Sizning quvuringiz esa har so'rovni uch marta qayta uradi. Ya'ni siz yuklamani uch barobar oshirdingiz — aynan servisga eng og'ir paytda.
Bu yerda yana bir narsa bor. Sizning quvuringiz yolg'iz emas — o'ntasi bor, va hammasi bir vaqtda qayta uradi.
Natija: servis ko'tarilishga urinadi, lekin har ko'tarilganda darhol yana yiqiladi, chunki uni kutayotgan so'rovlar to'planib qolgan. U hech qachon tiklanmaydi.
Bu hodisaning nomi bor: o'lim spirali (death spiral). Va u ko'pincha kichik nosozlikni soatlab davom etadigan uzilishga aylantiradi.
Yechim qarama-qarshi tuyuladi: urinishni to'xtatish kerak.
Elektr zanjir uzgichini eslang. Qisqa tutashuv bo'lsa, u tokni uzadi — simlar yonib ketmasligi uchun. U "yana urinib ko'ray" demaydi.
Elektr zanjir uzgichini eslang: qisqa tutashuv bo'lsa, u o'zini uzadi. Simlar yonib ketmasligi uchun.
python3 -c " import sys; sys.path.insert(0, 'src') from quvur.chidamlilik import ZanjirUzgich z = ZanjirUzgich(chegara=3, tiklanish=0.5) def yiqilgan_servis(): raise ConnectionError('servis ishlamayapti') urinildi = 0 uzilgan = 0 for i in range(10): try: z.chaqir(yiqilgan_servis) except ConnectionError: urinildi += 1 except RuntimeError: uzilgan += 1 print(f'haqiqiy urinish : {urinildi} (zanjir uzilgunga qadar)') print(f'urinilmadi (uzilgan) : {uzilgan}') print(f'tejaldi : {uzilgan/10*100:.0f}% so\\'rov') print() import time; time.sleep(0.6) print('0.6 s kutildi (tiklanish muddati o\\'tdi)') print('zanjir ochiqmi:', z.ochiqmi, '-> yana sinab ko\\'rish mumkin') "
haqiqiy urinish : 3 (zanjir uzilgunga qadar) urinilmadi (uzilgan) : 7 tejaldi : 70% so'rov 0.6 s kutildi (tiklanish muddati o'tdi) zanjir ochiqmi: False -> yana sinab ko'rish mumkin
Uch xatodan keyin zanjir uzildi va qolgan yetti so'rov umuman yuborilmadi. Servis tinch qoldi.
Karantin: yomon qatorni butun quvurga yiqitmang
6-bobda ma'lumot sifatini ko'rgandik. Endi savol: bitta qator buzuq bo'lsa, butun yuklama to'xtashi kerakmi?
Ko'pincha yo'q. To'g'ri yechim — buzuq qatorlarni karantinga ajratish va qolganini davom ettirish.
python3 -c " import sys, json, pathlib; sys.path.insert(0, 'src') import pandas as pd d = pd.read_csv('../datasets/ijara.csv').head(3000).copy() # ataylab buzamiz import numpy as np rng = np.random.default_rng(42) buzuq = rng.choice(d.index, 90, replace=False) d.loc[buzuq[:30], 'narx_mln'] = -5 # manfiy narx d.loc[buzuq[30:60], 'maydon'] = 0 # nol maydon d.loc[buzuq[60:], 'xonalar'] = 99 # imkonsiz xona soni QOIDA = { 'narx_mln': lambda s: (s > 0) & (s < 200), 'maydon': lambda s: (s >= 10) & (s <= 500), 'xonalar': lambda s: (s >= 1) & (s <= 10), } yaxshi = pd.Series(True, index=d.index) sabab = {} for ustun, tekshir in QOIDA.items(): ok = tekshir(d[ustun]) sabab[ustun] = int((~ok).sum()) yaxshi &= ok toza = d[yaxshi] karantin = d[~yaxshi] print(f'kirgan qatorlar : {len(d):,}') print(f'toza : {len(toza):,}') print(f'karantin : {len(karantin):,}') print() for u, n in sabab.items(): print(f' {u:10s}: {n:>3} qator qoidaga mos emas') print() ulush = len(karantin)/len(d)*100 print(f'karantin ulushi : {ulush:.2f}%') print(f'qaror : {\"DAVOM ETADI\" if ulush < 5 else \"TOXTAYDI\"} (chegara 5%)') "
kirgan qatorlar : 3,000 toza : 2,910 karantin : 90 narx_mln : 30 qator qoidaga mos emas maydon : 30 qator qoidaga mos emas xonalar : 30 qator qoidaga mos emas karantin ulushi : 3.00% qaror : DAVOM ETADI (chegara 5%)
Mana muhim nuqta: quvur to'xtamadi, lekin buzuq qatorlar ham jim o'tmadi. Ular alohida joyda, sababi bilan.
Karantin chegarasiz bo'lsa, u axlat qutisiga aylanadi
Karantin foydali, lekin chegara bo'lishi shart. Aks holda bir kuni manba tizimi buzilib ma'lumotning 80 foizi yaroqsiz bo'ladi, quvur esa 20 foiz bilan "muvaffaqiyatli" tugaydi.
Hisobot chiqadi, raqamlar besh barobar kichik, hech kim sezmaydi.
Qoida: karantin ulushi chegaradan oshsa — to'xtang va odamni chaqiring. Yuqoridagi kodda chegara 5%.
Idempotentlik: qayta ishga tushirish xavfsiz bo'lsin
Oxirgi va eng muhim xususiyat. Quvur yarmida yiqildi. Siz uni qayta ishga tushirasiz.
Savol: birinchi yarmida yozilgan ma'lumot ikki marta yozilmaydimi?
Agar javob "yoziladi" bo'lsa, sizda o'zini tuzatuvchi quvur yo'q — sizda har qayta ishga tushirishda ma'lumotni buzadigan quvur bor.
python3 -c " import pandas as pd, pathlib d = pd.read_csv('../datasets/ijara.csv').head(500).drop_duplicates('elon_id') kun = '2026-09-15' def qoshib_yoz(fayl): '''NOTOGRI: har safar qoshadi''' eski = pd.read_csv(fayl) if pathlib.Path(fayl).exists() else pd.DataFrame() pd.concat([eski, d]).to_csv(fayl, index=False) return len(pd.read_csv(fayl)) def almashtirib_yoz(fayl): '''TOGRI: shu kun uchun ozgarishni almashtiradi''' eski = pd.read_csv(fayl) if pathlib.Path(fayl).exists() else pd.DataFrame(columns=d.columns) if 'kun' in eski.columns: eski = eski[eski['kun'] != kun] yangi = d.assign(kun=kun) pd.concat([eski, yangi]).to_csv(fayl, index=False) return len(pd.read_csv(fayl)) for nom, f, fayl in (('qoshib yozish', qoshib_yoz, 'a.csv'), ('almashtirib yozish', almashtirib_yoz, 'b.csv')): pathlib.Path(fayl).unlink(missing_ok=True) n1 = f(fayl); n2 = f(fayl); n3 = f(fayl) print(f'{nom:20s}: 1-ishga tushirish {n1:>5,} | 2-marta {n2:>5,} | 3-marta {n3:>5,}') pathlib.Path(fayl).unlink(missing_ok=True) print() print('Birinchisi har safar shishadi. Ikkinchisi ozgarmaydi - u idempotent.') "
qoshib yozish : 1-ishga tushirish 500 | 2-marta 1,000 | 3-marta 1,500 almashtirib yozish : 1-ishga tushirish 500 | 2-marta 500 | 3-marta 500 Birinchisi har safar shishadi. Ikkinchisi ozgarmaydi - u idempotent. <string>:19: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.
Uch marta ishga tushirdik. Birinchi usulda qatorlar soni uch barobar oshdi; ikkinchisida o'zgarmadi.
Ikkinchisi idempotent: necha marta bajarsangiz ham natija bir xil. 10-bobdagi
hodisa_id g'oyasi bilan bir xil printsip, faqat jadval darajasida.
Hammasini birlashtiramiz
diagramma( { "Manba": (0, 1.8, 1.8, 0.9, "manba"), "Qayta urinish\n+ backoff": (2.6, 1.8, 2.3, 0.9, "qayta"), "Zanjir\nuzgich": (5.6, 1.8, 1.9, 0.9, "qayta"), "Sifat\ntekshiruvi": (8.2, 1.8, 2.0, 0.9, "qayta"), "Karantin": (8.2, 0.2, 2.0, 0.8, "kuzatuv"), "Idempotent\nyozish": (11.0, 1.8, 2.1, 0.9, "saqlash"), }, [ ("Manba", "Qayta urinish\n+ backoff"), ("Qayta urinish\n+ backoff", "Zanjir\nuzgich"), ("Zanjir\nuzgich", "Sifat\ntekshiruvi"), ("Sifat\ntekshiruvi", "Karantin", "yomon"), ("Sifat\ntekshiruvi", "Idempotent\nyozish", "toza"), ], nom="ozini_tuzatish.png", kenglik=14, izoh="Har bosqich bitta xato turini hal qiladi. Birgalikda tungi qongiroqni yoqotadi.", )

Har bosqich bitta xato turini hal qiladi. Birgalikda tungi qongiroqni yoqotadi.
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Xato turi | O'tkinchi — qayta urin; doimiy — to'xta |
| Hamma xatoni urinish | Zararli: hisob bloklanadi, sabab yashirinadi |
| Backoff | Har urinishda ko'proq kutish |
| Jitter | Hamma bir vaqtda urmasligi uchun tasodif |
| Momaqaldiroq podasi | Jittersiz qayta urinish serverni ikkinchi marta yiqitadi |
| Zanjir uzgich | Yiqilgan servisga urinishni to'xtatadi |
| Karantin | Yomon qator butun yuklamani to'xtatmasin |
| Karantin chegarasi | Oshsa — to'xtang, aks holda jim buzilish |
| Idempotentlik | Qayta ishga tushirish natijani o'zgartirmasin |
Talaba ishi
qayta_uringaHTTP 429uchun alohida yo'l qo'shing: serverRetry-Afterbersa, o'sha vaqtni kuting.- Zanjir uzgichga "yarim ochiq" holatni aniq qo'shing: bitta sinov so'rovi yuborilsin, u muvaffaqiyatli bo'lsagina to'liq ochilsin.
- Karantin qatorlarini sabab bo'yicha alohida fayllarga yozing. Qaysi qoida eng ko'p ishga tushdi?
almashtirib_yozni ikki jarayon bir vaqtda chaqirsa nima bo'ladi? Buni qanday xavfsiz qilish mumkin?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.