ЗАМЕР
bench/queueing/mm1.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/sre/queueing-tail
- Как запустить
python3 bench/queueing/mm1.py > bench/queueing/runs/mm1.txt python3 bench/queueing/practice.py > bench/queueing/runs/practice.txt
Запись прогона
Модель для урока «Очередь и хвост»
Это симуляция, а не замер. Допущения перечислены в докстринге mm1.py:
один обработчик, очередь без предела, показательные промежутки прихода и время
обслуживания со средним 10 мс, сто тысяч запросов на точку, фиксированное
зерно.
| Файл | Что делает |
|---|---|
mm1.py |
время в системе при загрузках 0,5–0,95; отдельно — что растёт быстрее чего; и отдельно — что делает с хвостом разброс времени обслуживания при том же среднем |
practice.py |
ответы к задачам урока: кратности роста среднего и p99 |
Запуск из корня репозитория:
python3 bench/queueing/mm1.py > bench/queueing/runs/mm1.txt
python3 bench/queueing/practice.py > bench/queueing/runs/practice.txt
Что модель показала вопреки ожиданию
Хвост НЕ растёт быстрее среднего при росте загрузки: и то и другое умножается на одно и то же число, а отношение p99 к среднему остаётся постоянным. Хвост отрывается от середины от разброса времени обслуживания — это третий блок.
Чего в модели нет
Нескольких обработчиков, ограниченной очереди, таймаутов и ретраев. Всё это меняет числа; направление — «ожидание растёт быстрее загрузки» — нет.
Скрипт
136 строк"""Очередь и хвост: почему p99 растёт быстрее нагрузки. Модель, а не замер.
ЧТО ЭТО ЗА ФАЙЛ И ЧЕГО ОН НЕ ДЕЛАЕТ. Здесь нет ни сервера, ни сети: это
дискретно-событийная СИМУЛЯЦИЯ одноканальной очереди. Её выводы верны ровно
настолько, насколько верны допущения (ADR-017: модель — законное происхождение
утверждения, но она обязана называть свои допущения).
ДОПУЩЕНИЯ МОДЕЛИ:
1. Один обработчик, очередь без ограничения длины, дисциплина «первым пришёл
— первым обслужен».
2. Промежутки между приходами — показательное распределение (пуассоновский
поток). Время обслуживания — тоже показательное со средним 10 мс.
3. Ни разогрева, ни отказов, ни повторов: клиент ждёт столько, сколько
нужно.
4. Загрузка ρ задаётся отношением темпа прихода к темпу обслуживания.
5. Сто тысяч запросов на каждую загрузку, зерно фиксировано.
ЧТО МОДЕЛЬ ПОКАЗАЛА ВОПРЕКИ ОЖИДАНИЮ. В этой очереди хвост НЕ растёт быстрее
среднего: при росте загрузки среднее и p99 умножаются на одно и то же число,
потому что всё распределение времени в системе растягивается целиком. Растёт
быстрее нагрузки — да, вчетверо против 1,19; быстрее среднего — нет. Хвост
отрывается от среднего по другой причине, и она проверяется третьим блоком:
разброс времени обслуживания при том же среднем.
ЧЕГО МОДЕЛЬ НЕ ПОКАЗЫВАЕТ. Настоящий сервис имеет несколько обработчиков,
ограниченную очередь, таймауты и ретраи. Всё это меняет числа — но не меняет
направления: время ожидания растёт быстрее, чем загрузка.
ЗАПУСК: python3 bench/queueing/mm1.py
Вывод: runs/mm1.txt
"""
import os
import random
import statistics
import sys
SERVICE_MS = 10.0
REQUESTS = 100_000
SEED = 20260905
LOADS = (0.50, 0.70, 0.80, 0.90, 0.95)
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def percentile(values: list[float], share: float) -> float:
ordered = sorted(values)
index = min(len(ordered) - 1, int(share * len(ordered)))
return ordered[index]
def service_exponential(rng: random.Random) -> float:
return rng.expovariate(1 / SERVICE_MS)
def service_mixed(rng: random.Random) -> float:
"""То же среднее, но разброс больше: 95 % по 5 мс и 5 % по 105 мс."""
return 5.0 if rng.random() < 0.95 else 105.0
def simulate(load: float, service_of=service_exponential) -> dict[str, float]:
"""Одноканальная очередь: возвращает статистику полного времени в системе."""
rng = random.Random(SEED)
arrival_gap = SERVICE_MS / load # средний промежуток между приходами
now = 0.0
free_at = 0.0
total: list[float] = []
for _ in range(REQUESTS):
now += rng.expovariate(1 / arrival_gap)
service = service_of(rng)
started = max(now, free_at)
free_at = started + service
total.append(free_at - now)
return {
"mean": statistics.fmean(total),
"p50": percentile(total, 0.50),
"p95": percentile(total, 0.95),
"p99": percentile(total, 0.99),
}
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
print("model, not a measurement: single server, exponential arrivals and service")
print(f"service time {SERVICE_MS:.0f} ms on average, {REQUESTS} requests, seed {SEED}")
show("TIME IN THE SYSTEM AGAINST UTILISATION")
print(f" {'load':>6} {'mean':>10} {'p50':>10} {'p95':>10} {'p99':>10}")
results = {}
for load in LOADS:
stats = simulate(load)
results[load] = stats
print(
f" {load:>6.2f} {stats['mean']:>9.1f}м {stats['p50']:>9.1f}м"
f" {stats['p95']:>9.1f}м {stats['p99']:>9.1f}м".replace("м", "s").replace("s", " ")
)
show("WHAT GROWS FASTER THAN WHAT")
low, high = results[0.80], results[0.95]
print(f" load 0.80 -> 0.95 is a rise of {0.95 / 0.80:.2f}x")
print(f" mean time rises by {high['mean'] / low['mean']:.2f}x")
print(f" p99 rises by {high['p99'] / low['p99']:.2f}x")
print(f" p99 over mean at load 0.80 {low['p99'] / low['mean']:.2f}x")
print(f" p99 over mean at load 0.95 {high['p99'] / high['mean']:.2f}x")
print()
print(" The service is unchanged: every request still takes 10 ms of work")
print(" on average. What changes is the waiting, and it is the waiting")
print(" that the tail is made of.")
show("SAME MEAN SERVICE TIME, DIFFERENT SPREAD")
steady = simulate(0.80, service_exponential)
spread = simulate(0.80, service_mixed)
print(" both runs: load 0.80, average service time 10 ms")
print(" A: service time exponential, average 10 ms")
print(" B: service time is 5 ms in 95% of cases and 105 ms in 5%")
print()
print(f" {'':>18} {'mean':>10} {'p50':>10} {'p99':>10}")
print(f" {'A, exponential':>18} {steady['mean']:>9.1f} {steady['p50']:>9.1f} {steady['p99']:>9.1f}")
print(f" {'B, mixed':>18} {spread['mean']:>9.1f} {spread['p50']:>9.1f} {spread['p99']:>9.1f}")
print(f" p99 of B over p99 of A {spread['p99'] / steady['p99']:.2f}x")
print(f" p50 of B over p50 of A {spread['p50'] / steady['p50']:.2f}x")
print()
print(" Same load, same average work per request - and both the middle and")
print(" the tail move, the tail further: 3.1x against 2.6x. Spread, not")
print(" utilisation, is what pulls the tail away from the middle.")
if __name__ == "__main__":
main()