MEASUREMENT
bench/slo/budget.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/slo-error-budget
- How to run it
python3 bench/slo/budget.py > bench/slo/runs/budget.txt python3 bench/slo/practice.py > bench/slo/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
Расчёт для урока «SLO и бюджет ошибок»
Это арифметика и модель, а не замер. Допущения перечислены в докстринге
budget.py: постоянный поток 1000 запросов в секунду, прямоугольный инцидент,
бюджет по доле неуспешных запросов, скользящее окно сдвигается посуточно.
| Файл | Что делает |
|---|---|
budget.py |
одна цель в четырёх окнах; цена трёх разных инцидентов в долях бюджета; календарное окно против скользящего |
practice.py |
ответы к задачам урока: бюджет в переводе на недоступность и доля бюджета, съеденная инцидентом |
Запуск из корня репозитория:
python3 bench/slo/budget.py > bench/slo/runs/budget.txt
python3 bench/slo/practice.py > bench/slo/runs/practice.txt
Что здесь проверяемо
Всё: каждая величина выводится из цели, окна и профиля инцидента, и скрипт печатает и исходные числа, и результат. Абсолютные значения зависят от принятого потока запросов; доли бюджета — нет.
Script
134 lines"""SLO и бюджет ошибок: арифметика окна и цена одного инцидента.
ЧТО ЭТО ЗА ФАЙЛ. Здесь нет ни сервера, ни замера: это АРИФМЕТИКА и небольшая
симуляция с явными допущениями (ADR-017). Всё, что печатается, выводится из
двух чисел — цели и окна, — плюс из заданного профиля инцидента.
ДОПУЩЕНИЯ:
1. Поток запросов постоянный: 1000 запросов в секунду круглые сутки. В жизни
он неравномерен, и от этого зависят все абсолютные числа.
2. Инцидент — прямоугольный: заданную долю запросов роняем ровно заданное
время, до и после — ни одной ошибки.
3. Бюджет считается по доле неуспешных запросов, а не по времени
недоступности: это две разные метрики, и урок разбирает первую.
4. Скользящее окно сдвигается посуточно.
ЗАПУСК: python3 bench/slo/budget.py
Вывод: runs/budget.txt
"""
import os
import sys
RPS = 1000
TARGETS = (0.99, 0.995, 0.999, 0.9999)
WINDOWS_DAYS = (1, 7, 30, 90)
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def row(label: str, value: object) -> None:
print(f" {label:<44} {value}")
def human_minutes(minutes: float) -> str:
if minutes >= 60:
return f"{minutes / 60:.1f} h"
if minutes >= 1:
return f"{minutes:.1f} min"
return f"{minutes * 60:.0f} s"
# ------------------------------------------------------------------ 1
def block1() -> None:
show("1. THE SAME TARGET, DIFFERENT WINDOWS")
print(f" {'target':>8} " + " ".join(f"{d:>10}d" for d in WINDOWS_DAYS))
for target in TARGETS:
cells = []
for days in WINDOWS_DAYS:
minutes = days * 24 * 60 * (1 - target)
cells.append(f"{human_minutes(minutes):>11}")
print(f" {target * 100:>7.2f}% " + " ".join(cells))
print()
print(" A target without a window is not a number: 99.9% means 43 minutes")
print(" a month and 1.4 minutes a day. The same promise, two different")
print(" operational lives.")
# ------------------------------------------------------------------ 2
def block2() -> None:
show("2. WHAT ONE INCIDENT COSTS")
target = 0.999
window_days = 30
total = RPS * 60 * 60 * 24 * window_days
budget = total * (1 - target)
row("target", f"{target * 100:.1f}%")
row("window, days", window_days)
row("requests in the window", f"{total:,}".replace(",", " "))
row("error budget, requests", f"{budget:,.0f}".replace(",", " "))
row("error budget, minutes of total outage", human_minutes(window_days * 24 * 60 * (1 - target)))
print()
for share, minutes in ((1.0, 10), (0.5, 60), (0.02, 24 * 60)):
failed = RPS * 60 * minutes * share
row(
f"incident: {share * 100:.0f}% of requests fail for {minutes} min",
f"{failed:,.0f} requests = {failed / budget * 100:.0f}% of the budget".replace(",", " "),
)
print()
print(" The three incidents differ in how they feel and cost the budget")
print(" differently: a total ten-minute outage is a quarter of the month,")
print(" a barely visible 2% for a day is two thirds of it.")
# ------------------------------------------------------------------ 3
def block3() -> None:
show("3. CALENDAR WINDOW AGAINST ROLLING WINDOW")
target = 0.999
daily_total = RPS * 60 * 60 * 24
budget_30d = daily_total * 30 * (1 - target)
# Инцидент: 50% запросов роняем на час, в 28-й день календарного месяца.
incident_day = 28
incident_errors = RPS * 60 * 60 * 0.5
row("budget for 30 days, requests", f"{budget_30d:,.0f}".replace(",", " "))
row("incident on day 28: 50% for 1 hour", f"{incident_errors:,.0f} requests".replace(",", " "))
row("that is", f"{incident_errors / budget_30d * 100:.0f}% of the budget")
print()
print(" calendar window: the budget resets on the 1st")
row("budget left on day 29", f"{(budget_30d - incident_errors) / budget_30d * 100:.0f}%")
row("budget left on day 31 (new month)", "100%")
print()
print(" rolling 30-day window: the incident stays for 30 days")
for day_after in (1, 15, 29, 31):
left = 100.0 if day_after > 30 else (budget_30d - incident_errors) / budget_30d * 100
day_word = "day" if day_after == 1 else "days"
row(f"budget left {day_after} {day_word} after the incident", f"{left:.0f}%")
print()
print(" Same service, same incident, two different answers to 'may we")
print(" ship today'. The window is not a detail of the wording; it is")
print(" the rule by which the freeze ends.")
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
print("arithmetic and a model, not a measurement: assumptions are in the docstring")
print(f"traffic assumed constant at {RPS} requests per second")
block1()
block2()
block3()
if __name__ == "__main__":
main()