Asosiy qismga o'tish

O'zini tuzatuvchi quvur

20-bob 10 kod bloki 2364 so'z

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

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:

yaml1 qator
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.

python9 qator
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"},
]))
XatoTuriTo'g'ri javob
Tarmoq timeoutO'tkinchiQayta urinish
HTTP 503 (server band)O'tkinchiQayta urinish
HTTP 429 (juda ko'p so'rov)O'tkinchiKutib qayta urinish
HTTP 404 (topilmadi)DoimiyTo'xtash, xabar berish
HTTP 401 (parol xato)DoimiyTo'xtash, xabar berish
Ustun nomi yo'qDoimiyTo'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:

yaml5 qator
for i in range(3):
    try:
        return api_dan_ol()
    except Exception:
        continue

Bunda uch xato bor:

  1. Hamma xatoni ushlaydi — doimiy xato ham qayta urinadi.
  2. Kutmaydi — server band bo'lsa, uni darhol yana urib yanada band qiladi.
  3. 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

python72 qator
"""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

python35 qator
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')
"
Chiqish (qaytish kodi 0, 411 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.

python22 qator
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')
"
Chiqish (qaytish kodi 0, 22 ms)
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.

python27 qator
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')
"
Chiqish (qaytish kodi 0, 708 ms)
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.

python41 qator
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%)')
"
Chiqish (qaytish kodi 0, 330 ms)
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.

python29 qator
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.')
"
Chiqish (qaytish kodi 0, 507 ms)
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

python19 qator
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.

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

  1. qayta_urin ga HTTP 429 uchun alohida yo'l qo'shing: server Retry-After bersa, o'sha vaqtni kuting.
  2. Zanjir uzgichga "yarim ochiq" holatni aniq qo'shing: bitta sinov so'rovi yuborilsin, u muvaffaqiyatli bo'lsagina to'liq ochilsin.
  3. Karantin qatorlarini sabab bo'yicha alohida fayllarga yozing. Qaysi qoida eng ko'p ishga tushdi?
  4. almashtirib_yoz ni 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.

Telegramda yozish Narxlar