Shanraq.org Shanraq.org
Бөтен серверге сыпайылық: таймаут, шегінісі бар қайталау және кэш
IT

Python: деректен өз есебіңізге дейін 20-сабақ (барлығы 56)

Бөтен серверге сыпайылық: таймаут, шегінісі бар қайталау және кэш

Python курсының жиырмасыншы сабағы. Бөтен серверге сыпайылық үш нәрседен тұрады: таймаут — мәңгі күтпеу, кідірісі екі еселенетін қайталау — құлағанды ұрмау, кэш — екі рет сұрамау. Өз серверімізде өлшенді: төрт оқу, бір сұраныс.

Не үшін керек

Өткен үш сабақ Ұлттық банкке барды. Жұмыс істеді — әрі дәл сондықтан мұны қалай істемеу керегін айтатын кез келді.

Бөтен сервер сізге ештеңе қарыз емес. Ол үндемей қалуы, «кейінірек көріңіз» деуі, қызмет көрсетуге кетуі мүмкін. Ол әрқашан тез жауап береді деп есептеп жазылған бағдарлама бір күні қатып қалады — ал бас тартуға кідіріссіз қайталаумен жауап беретін бағдарлама әбден шайқалып тұрғанды құлатады.

Мұндағы сыпайылық — әдеп емес, үш нақты нәрсе: таймаут, өсіп отыратын кідірісі бар қайталау және кэш. Өз серверімізде үйренеміз: бөтенде жаттығу — сабақ айтып отырған сол сыпайысыздықтың өзі.

Бірден тұтас

sypaiy.py файлы. Іске қосу: ортадан python sypaiy.py.

Алғашқы жиырма жол — баяулай да, бас тарта да, сұраныс санай да алатын оқу сервері; оны бет-жүзінен танысаңыз жеткілікті, серверлердің құрылысына курс бөлек оралады. Міндеттісі — одан кейінгі үш блок.

"""20-сабақ: бөтен серверге сыпайылық — таймаут, қайталау, кэш.

Алдыңғы үш сабақ желіде нағыз банкке барды. Бүгін мұны мәңгі күтпейтіндей әрі
сұрап отырған адамға кедергі келтірмейтіндей етуді үйренеміз — әрі өз
серверімізде үйренеміз, себебі бөтенде жаттығудың өзі — сол сыпайысыздық.
"""

import json
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

# Әр мекенжай бойынша серверді неше рет мазалады. Сыпайылықтың өлшемі де сол.
HITS = {"data": 0, "flaky": 0}


class Teacher(BaseHTTPRequestHandler):
    """Қасындағы оқу сервері: ол баяулай да, құлай да, сұраныс санай да алады."""

    def do_GET(self):
        if self.path == "/slow":
            time.sleep(2)                      # сервер ойланып қалды
            self.answer(200, b'{"value": 1}')
        elif self.path == "/flaky":
            HITS["flaky"] += 1
            if HITS["flaky"] < 3:
                self.answer(503, b"try later")  # уақытша шамасы жоқ
            else:
                self.answer(200, b'{"value": 42}')
        else:
            HITS["data"] += 1
            self.answer(200, b'{"value": 510.43}')

    def answer(self, code, body):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        return                                  # журналы шығуымызға түспесін


server = ThreadingHTTPServer(("127.0.0.1", 0), Teacher)
threading.Thread(target=server.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{server.server_address[1]}"


def get(url, timeout=1.0):
    """Бір сұраныс: не жауаптың денесі, не ерекше жағдай."""
    request = urllib.request.Request(url, headers={"User-Agent": "shanraq-course/1.0"})
    with urllib.request.urlopen(request, timeout=timeout) as answer:
        return json.load(answer)


print("== таймаут: бөтеннің үнсіздігін қанша күту керек")
try:
    get(BASE + "/slow", timeout=0.3)
except TimeoutError:
    # Қосылғаннан кейінгі үнсіздік дәл осылай келеді: TimeoutError.
    print("0.3 с күттік те, күтуді қойдық")
except urllib.error.URLError as error:
    # Ал қосыла алмасақ — ішінде себебі бар URLError.
    print("қосыла алмадық:", type(error.reason).__name__)

print()
print("== өсіп отыратын кідірісі бар қайталау: біреудің орнына үш талпыныс")
delay = 0.1
for attempt in range(1, 5):
    try:
        data = get(BASE + "/flaky")
    except urllib.error.HTTPError as error:
        # Тек өзі жақсаруы мүмкін нәрсені ғана қайталайды.
        if error.code not in (429, 500, 502, 503, 504):
            print("қайталамаймыз:", error.code)
            break
        print(f"{attempt}-талпыныс: {error.code}, {delay:.1f} с күтеміз")
        time.sleep(delay)
        delay *= 2
        continue
    print(f"{attempt}-талпыныс: {data['value']} алынды")
    break
print("серверге сұраныс:", HITS["flaky"])

print()
print("== кэш: бір жауапты екі рет сұрамайды")
CACHE = {}
TTL = 60


def cached(url):
    """Ескірмегенше — жадтағы жауап."""
    now = time.monotonic()
    if url in CACHE and now - CACHE[url][0] < TTL:
        return CACHE[url][1], "кэштен"
    data = get(url)
    CACHE[url] = (now, data)
    return data, "желіден"


for _ in range(3):
    data, where = cached(BASE + "/data")
    print(f"{data['value']}{where}")
print("серверге сұраныс:", HITS["data"])

server.shutdown()

Шығатыны:

== таймаут: бөтеннің үнсіздігін қанша күту керек
0.3 с күттік те, күтуді қойдық

== өсіп отыратын кідірісі бар қайталау: біреудің орнына үш талпыныс
1-талпыныс: 503, 0.1 с күтеміз
2-талпыныс: 503, 0.2 с күтеміз
3-талпыныс: 42 алынды
серверге сұраныс: 3

== кэш: бір жауапты екі рет сұрамайды
510.43 — желіден
510.43 — кэштен
510.43 — кэштен
серверге сұраныс: 1

Талдау

Таймаут: бөтеннің үнсіздігін қанша күту керек

0.3 с күттік те, күтуді қойдық

Таймаутсыз urlopen сервер үндемеген уақыттың бәрін күтеді — минуттап. Сырттан бұл қатып қалған бағдарламадан айнымайды, әрі ең жаманы кестеде көрінеді: бір секундта бітуге тиіс тапсырма таңға дейін ілініп тұрады.

Таймаут «қанша күтуге дайынбыз» деген сұраққа жауап береді, әрі бұл жауап әрқашан бар, жазылмаса да: сонда ол «қанша болса, сонша» дегенге тең.

Мұның немен келетініне назар аударыңыз:

except TimeoutError:      # қосылдық, ал жауапты күте алмадық
except urllib.error.URLError:   # мүлде қосыла алмадық

Қосылғаннан кейінгі үнсіздік — TimeoutError. Қосыла алмау — URLError, ал оның ішінде себебі жатыр. Екеуін де ұстайды: бағдарлама үшін бұл екі бөлек бәле, бірақ екеуі де «дерек жоқ» дегенді білдіреді.

Елестетіп көріңіз. Анықтамаға қоңырау. Үшінші гудоктан кейін тұтқаны қоясыз — өштігіңізден емес, басқа да ісіңіз болғандықтан.

Өсіп отыратын кідірісі бар қайталау: біреудің орнына үш талпыныс

1-талпыныс: 503, 0.1 с күтеміз
2-талпыныс: 503, 0.2 с күтеміз
3-талпыныс: 42 алынды
серверге сұраныс: 3

503 — «қазір шамам жоқ, кейінірек көріңіз». Бұл уақытша, әрі мұнда қайталау орынды. Бірақ кідіріссіз қайталау — әбден шамасы келмей тұрғанды ұру: сервер жүктемеден құлады, ал сіз секундына тағы жүз сұраныс қостыңыз.

Сондықтан кідіріс екі еселенеді: 0,1 → 0,2 → 0,4. Мұндай өсіп отыратын кідіріс (ағылшынша exponential backoff) дереккөзге тұрып кетуге уақыт береді, әрі сіздің есебіңізді де мәңгілікке созбайды.

Екінші ереже біріншісінен маңызды: тек өзі жақсаруы мүмкін нәрсені ғана қайталайды.

if error.code not in (429, 500, 502, 503, 504):
    ...   # қайталамаймыз

404 сіз тағы бір рет сұрағаннан 200 болмайды: мекенжай жоқ. 403 те қайталаудан рұқсат етілгенге айналмайды. Ал 429 — «тым жиі» — тікелей күтуді сұрайды, әрі жиі қаншаны күту керегін Retry-After тақырыбында айтады; ол болса, өз формулаңызды емес, соны тыңдайды.

Әрі қайталаудың соңы болуға тиіс. Үш-төрт талпыныс — содан кейін бағдарлама таңға дейін ілініп тұрудың орнына, дереккөз жауап бермеді деп адал айтады.

Кэш: бір жауапты екі рет сұрамайды

510.43 — желіден
510.43 — кэштен
510.43 — кэштен
серверге сұраныс: 1

Үш оқу — бір сұраныс. Кэштің бүкіл арифметикасы да осы, әрі ол не үшін керек деген сұрақтың жауабы да сол: жылдамдық үшін емес, өзіңіз білетін нәрсені бөтен серверден сұрамау үшін.

Кэштің мерзімі міндетті түрде болады. Бүгінгі валюта бағамы күні бойы өзгермейді, жылдық инфляция — жыл бойы, ауа райы — жарты сағат. Мерзім — дерек қаншалықты жаңа болуға тиіс деген шешім; онсыз кэш «алдыңғы аптадағы дерекке» айналады, ал бұл баяулықтан жаман.

Біздің кэш жадта тұрады да, бағдарламамен бірге жоғалады. Күніне бір рет іске қосылатын шолуға бұл аз — онда кэшті он бірінші сабақтағы файл сияқты дискіге қояды; кілті — сұраныстың мекенжайы, ішіндегісі — жауап пен ол алынған уақыт.

Сізді қалаулы қонақ ететін тағы үш нәрсе

Қол қойыңыз. Жобаға сілтемесі бар User-Agent — бұл сізге IP бойынша тыйым салудың орнына хат жазу мүмкіндігі.

Бір рет сұрауға болатынды қатар сұрамаңыз. Кідіріссіз циклмен аралаған екі жүз қаланың тізімі — секундына екі жүз сұраныс; араларына time.sleep(0.2) қойсаңыз — бесеу, әрі мұны ешкім байқамайды.

Шарттарын оқыңыз. Көп дереккөзде қаншалықты жиі сұрауға болатыны туралы бет бар. Ұлттық банк те, Дүниежүзілік банк те деректі кілтсіз береді — бұл сенім, шақыру емес.

Сабақ картасы

Сабақ картасы: таймаут, қайталау және кэш

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Сервер үндемей қалғанда таймаутсыз бағдарлама не істейді?
  2. Қайталаулар арасындағы кідіріс неге бірдей қалмай, екі еселенеді?
  3. Қандай жауап кодтарын қайталаудың мәні жоқ, әрі неге?

Жаттығу

Тапсырмаға дейінгі үш қысқа қадам: болжау, толықтыру, жөндеу. Жауаптары — сабақтың соңында, бірақ алдымен өзіңіз жауап беріңіз.

1. Болжаңыз. Бұл бағдарлама не басып шығарады?

delay = 0.5
for attempt in range(1, 5):
    print(attempt, round(delay, 2))
    delay *= 2

2. Бос орынды толтырыңыз. ... орнына шартты қойыңыз: тек осы кодтарды қайталаған жөн.

RETRY = (429, 500, 502, 503, 504)

for code in (404, 500, 429, 403):
    print(code, "қайталаймыз" if ... else "қайталамаймыз")

3. Жөндеңіз. Дереккөз бүгін жауап бермейді, әрі бұл бағдарлама одан бірде-бір кідіріссіз, мәңгі сұрай береді. Қайталауға да, кідіріске де соңын беріңіз.

attempts = 0


def fetch():
    """Бүгін жауап бермейтін дереккөз."""
    global attempts
    attempts += 1
    raise TimeoutError("timed out")


while True:
    try:
        print(fetch())
        break
    except TimeoutError:
        continue

Тапсырма

Міндетті. Бір сыпайы polite(url) функциясын жазып, оны бірінші жүгінгенде бас тартып, екіншісінде жауап беретін оқу серверінде тексеріңіз (серверді сабақтан алыңыз — ол сіздің файлыңызда).

Функция міндетті түрде: timeout қоюға тиіс; тек 429, 500, 502, 503, 504 кодтарын, үш талпынысқа дейін, 0,1 с-тан бастап кідірісті екі еселеп қайталауға тиіс; әр сәтсіз талпынысты басып шығаруға тиіс; сәтті жауапты мерзімі бар кэшке салуға тиіс; талпыныстар біткенде өз қатесін көтеруге тиіс. Бағамды қатарынан төрт рет оқып, серверді сол кезде неше рет мазалағаныңызды басып шығарыңыз.

Күтілетін шығу:

  1-талпыныс: 503, 0.1 с күтеміз
1: 510.43 — желіден
2: 510.43 — кэштен
3: 510.43 — кэштен
4: 510.43 — кэштен
серверге сұраныс: 2

Дайын болғаны: шығу жол-жолмен сәйкес келеді; сұраныста timeout бар; 404 пен 403 мүлде қайталанбайды; қайталау созылмай, бітеді; төрт оқу серверге дәл екі жүгінуді берді.

Өз деректеріңізде. Он сегізінші сабақтағы өз сұранысыңызды — бекітілген күнгі бағамды — алып, оны осы функциямен ораңыз. Бағдарламаны қатарынан үш рет іске қосып, жүгіну санауышына қараңыз: сыпайы бағдарлама мен сыпайысыздың айырмасы да сол.

Қалауыңызша.

  • Кэшті файлдық қылыңыз: кілті — мекенжай, ішіндегісі — жауап пен алынған уақыт.
  • Сервер 429 деп жауап бергенде Retry-After тақырыбын оқуды қосыңыз.
  • Он мекенжайдан тұратын тізімді time.sleep(0.2)-мен және онсыз циклмен аралап, секундына неше сұраныс шыққанын салыстырыңыз.

Жобада бұл қайда тұрады

Шолу дереккөздер үшін мәселе болуын қояды. Оның әр сұранысы енді уақыт бойынша шектеулі, бас тартулар өсіп отыратын кідірісі бар қайталаумен өткеріледі, ал сол күні қайта іске қосу банкке қайта бармай, кешегі жауапты дискіден оқиды.

Қарыздар. Кэшіміз жадта, ал шолуға ол дискіде керек — әрі сол жерде ескі жазбалармен не істеу керегі туралы сұрақ шығады. Оған келесі сабақта ораламыз: ол деректі өшірілуден аман шығатындай етіп қайда салу керегі туралы.

Жауаптар

Жауаптарды көрсету

Сұрақтарға

  1. Сервер үндемеген уақыттың бәрін күтеді — әрі сырттан бұл қатып қалудан айырғысыз. Таймаут жазылмаса да әрқашан бар: сонда ол «қанша болса, сонша» дегенге тең.
  2. Өйткені дереккөз әдетте шамадан тыс жүктемеден бас тартады, әрі бірдей жиі қайталау оған жүкті дәл шамасы келмей тұрған кезде қосады. Екі еселеу оған тұрып кетуге уақыт береді.
  3. 404 пен 403: мекенжай жоқ және қатынау жабық — қайталаудан екеуі де өзгермейді. Уақытшаны қайталайды: 429, 500, 502, 503, 504.

Жаттығуға

  1. 1 0.5, 2 1.0, 3 2.0, 4 4.0. Кідіріс екі еселенеді, әрі төртінші талпынысқа қарай бағдарлама төрт секунд күтеді — бөтен сервер одан дәл осындай мінез күтеді.
1 0.5
2 1.0
3 2.0
4 4.0
  1. code in RETRY. Уақытша кодтардың тізімін бір рет әрі бөлек жасайды, сонда шарт сандар жиынтығы емес, сөзбен оқылады.
RETRY = (429, 500, 502, 503, 504)

for code in (404, 500, 429, 403):
    print(code, "қайталаймыз" if code in RETRY else "қайталамаймыз")
404 қайталамаймыз
500 қайталаймыз
429 қайталаймыз
403 қайталамаймыз
  1. Қайталаудың соңы да, кідірісі де болмады: continue бар while True — тоқтаусыз сұраныс, әрі ол сізге де, дереккөзге де көмектеспейді. Талпыныс саны саналы болуға тиіс, араларында өсетін кідіріс, ал одан кейін — дерек жоқ деген адал жауап:
import time

attempts = 0


def fetch():
    """Бүгін жауап бермейтін дереккөз."""
    global attempts
    attempts += 1
    raise TimeoutError("timed out")


delay = 0.1
for attempt in range(1, 4):
    try:
        print(fetch())
        break
    except TimeoutError:
        time.sleep(delay)
        delay *= 2
else:
    print("күте алмадық; талпыныс:", attempts)
күте алмадық; талпыныс: 3

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.