Deep Engineering

ЗАМЕР

bench/retries/storm.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/sre/retries-jitter
Как запустить
python3 bench/retries/storm.py    > bench/retries/runs/storm.txt
python3 bench/retries/practice.py > bench/retries/runs/practice.txt

Запись прогона

Модель для урока «Ретраи и джиттер»

Это симуляция, а не замер. Ни одного сетевого пакета здесь нет; все допущения перечислены в докстринге storm.py и продублированы в уроке.

Файл Что делает
storm.py тысяча клиентов, недоступность две секунды, три стратегии повтора: постоянный интервал, экспоненциальная выдержка и она же с полным джиттером
practice.py ответы к задачам урока: пики после восстановления и кратность, на которую джиттер их срезает

Запуск из корня репозитория:

python3 bench/retries/storm.py    > bench/retries/runs/storm.txt
python3 bench/retries/practice.py > bench/retries/runs/practice.txt

Что в модели содержательно

Направления, а не абсолютные числа: экспоненциальная выдержка сокращает число бесполезных запросов, джиттер срезает пик после восстановления и растягивает само восстановление. Пик и время зависят от зерна (20260905) и от ширины диапазона выдержки.

Чего в модели нет

Сети, задержек, потерь, ограниченной ёмкости сервиса и бюджета повторов на уровне системы. Всё это меняет числа; направления — нет.

Скрипт

145 строк
"""Ретраи, джиттер и стадный эффект: модель, а не замер.

ЧТО ЭТО ЗА ФАЙЛ И ЧЕГО ОН НЕ ДЕЛАЕТ. Здесь нет ни одного сетевого пакета: это
СИМУЛЯЦИЯ, и её выводы стоят ровно настолько, насколько верны её допущения. Они
названы ниже поимённо, и число в уроке подаётся как модельное, а не измеренное
(docs/adr/ADR-017-evidence-in-interview-lessons.md).

ДОПУЩЕНИЯ МОДЕЛИ:
  1. Время дискретно: шаг 10 мс, всё происходит на границах шагов.
  2. Тысяча клиентов делает первый запрос одновременно — в шаг 0. Это худший
     случай («все проснулись разом»), а не типичный.
  3. Сервис недоступен первые 2 секунды и мгновенно здоров после; отказ
     возвращается сразу, без задержки.
  4. Клиент повторяет запрос до успеха, но не больше двадцати раз.
  5. Сеть без задержек и потерь, обработка мгновенна.

ЧТО СРАВНИВАЕТСЯ. Три стратегии повтора: постоянный интервал, экспоненциальная
выдержка без джиттера и она же с полным джиттером. Считаются пиковая нагрузка
в шаге, общее число запросов и момент, когда обслужены все.

ПОЧЕМУ ЭТО ВСЁ РАВНО ПОЛЕЗНО. Стадный эффект — свойство расписания повторов, а
не сети: если тысяча клиентов ждёт одинаково, они и придут одинаково. Модель
показывает именно расписание, и в этом её ответ проверяем.

ЗАПУСК: python3 bench/retries/storm.py
Вывод: runs/storm.txt
"""

import os
import random
import sys

STEP_MS = 10
CLIENTS = 1000
OUTAGE_MS = 2000
MAX_ATTEMPTS = 20
BASE_MS = 100
HORIZON_MS = 20000
SEED = 20260905


def show(title: str) -> None:
    print()
    print(title)
    print("-" * len(title))


def row(label: str, value: object) -> None:
    print(f"  {label:<40} {value}")


def delay_constant(attempt: int, rng: random.Random) -> int:
    return BASE_MS


def delay_exponential(attempt: int, rng: random.Random) -> int:
    return BASE_MS * (2**attempt)


def delay_full_jitter(attempt: int, rng: random.Random) -> int:
    return rng.randint(0, BASE_MS * (2**attempt))


STRATEGIES = {
    "constant interval": delay_constant,
    "exponential, no jitter": delay_exponential,
    "exponential + full jitter": delay_full_jitter,
}


def simulate(delay_of) -> dict[str, int]:
    """Один прогон модели. Возвращает пик, сумму запросов и момент готовности."""
    rng = random.Random(SEED)
    # Каждый клиент: [момент следующей попытки, номер попытки, обслужен ли]
    pending = [[0, 0, False] for _ in range(CLIENTS)]
    load: dict[int, int] = {}
    served = 0
    finished_at = -1

    for now in range(0, HORIZON_MS, STEP_MS):
        arrivals = 0
        for client in pending:
            if client[2] or client[0] > now or client[1] >= MAX_ATTEMPTS:
                continue
            arrivals += 1
            client[1] += 1
            if now >= OUTAGE_MS:
                client[2] = True
                served += 1
            else:
                client[0] = now + delay_of(client[1] - 1, rng)
        if arrivals:
            load[now] = arrivals
        if served == CLIENTS and finished_at < 0:
            finished_at = now

    after = {t: n for t, n in load.items() if t >= OUTAGE_MS}
    return {
        "first_wave": load.get(0, 0),
        "peak_after": max(after.values()) if after else 0,
        "peak_after_at": max(after, key=lambda k: after[k]) if after else 0,
        "requests": sum(load.values()),
        "wasted": sum(n for t, n in load.items() if t < OUTAGE_MS),
        "served": served,
        "ready_at": finished_at,
    }


def main() -> None:
    print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
    print("model, not a measurement: no packets are sent; assumptions are in the docstring")
    print(f"clients {CLIENTS}, outage {OUTAGE_MS} ms, base delay {BASE_MS} ms, seed {SEED}")

    results = {}
    for name, delay_of in STRATEGIES.items():
        show(name.upper())
        result = simulate(delay_of)
        results[name] = result
        row("first wave at t=0 (all clients at once)", result["first_wave"])
        row("peak in one 10 ms step after recovery", result["peak_after"])
        row("when that peak happened, ms", result["peak_after_at"])
        row("requests sent into the outage (wasted)", result["wasted"])
        row("total requests sent", result["requests"])
        row("clients served", result["served"])
        row("all clients served by, ms", result["ready_at"])

    show("SIDE BY SIDE")
    base = results["constant interval"]
    for name, result in results.items():
        row(f"{name}: peak after recovery", result["peak_after"])
    row("peak after recovery, jitter against none",
        f"{results['exponential, no jitter']['peak_after']} -> {results['exponential + full jitter']['peak_after']}")
    row("wasted requests, exponential against constant",
        f"{base['wasted']} -> {results['exponential, no jitter']['wasted']}")
    row("all served by, no jitter against jitter",
        f"{results['exponential, no jitter']['ready_at']} ms -> {results['exponential + full jitter']['ready_at']} ms")
    print()
    print("  The service is identical in all three runs. What changes is the")
    print("  schedule of retries, and with it the height of the wave that")
    print("  arrives the moment the service comes back.")


if __name__ == "__main__":
    main()