MEASUREMENT
bench/retries/storm.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/sre/retries-jitter
- How to run it
python3 bench/retries/storm.py > bench/retries/runs/storm.txt python3 bench/retries/practice.py > bench/retries/runs/practice.txt
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Модель для урока «Ретраи и джиттер»
Это симуляция, а не замер. Ни одного сетевого пакета здесь нет; все
допущения перечислены в докстринге 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) и от ширины диапазона выдержки.
Чего в модели нет
Сети, задержек, потерь, ограниченной ёмкости сервиса и бюджета повторов на уровне системы. Всё это меняет числа; направления — нет.
Script
145 lines"""Ретраи, джиттер и стадный эффект: модель, а не замер.
ЧТО ЭТО ЗА ФАЙЛ И ЧЕГО ОН НЕ ДЕЛАЕТ. Здесь нет ни одного сетевого пакета: это
СИМУЛЯЦИЯ, и её выводы стоят ровно настолько, насколько верны её допущения. Они
названы ниже поимённо, и число в уроке подаётся как модельное, а не измеренное
(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()