Deep Engineering

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()