ЗАМЕР
bench/rollout/strategies.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/sre/rollout-rollback
- Как запустить
python3 bench/rollout/strategies.py > bench/rollout/runs/strategies.txt python3 bench/rollout/practice.py > bench/rollout/runs/practice.txt
Запись прогона
Модель для урока «Выкатка и откат»
Это симуляция, а не замер: стратегии выкатки описываются документацией
конкретных продуктов, кластера в среде замеров нет. Допущения перечислены в
докстринге strategies.py.
| Файл | Что делает |
|---|---|
strategies.py |
четыре стратегии выкатки при двух способах обнаружения дефекта — по числу накопленных ошибок и по их доле |
practice.py |
ответы к задачам урока: задетые запросы у канарейки при обоих детекторах и у rolling |
Запуск из корня репозитория:
python3 bench/rollout/strategies.py > bench/rollout/runs/strategies.txt
python3 bench/rollout/practice.py > bench/rollout/runs/practice.txt
Что в модели содержательно
Сравнение строк, а не абсолютные значения: канарейка выигрывает только вместе с детектором, способным увидеть дефект на маленькой доле трафика; rolling проигрывает из-за медленного отката; ущерб идёт до конца отката, а не до момента обнаружения.
Чего в модели нет
Неравномерного потока, ретраев, частичных дефектов (проявляющихся только на части данных) и стоимости самой выкатки для системы.
Скрипт
164 строк"""Выкатка и откат: сколько запросов задето при четырёх стратегиях. Модель.
ЧТО ЭТО ЗА ФАЙЛ. Симуляция выкатки с дефектом — без кластера и без сети.
Выводы верны настолько, насколько верны допущения (ADR-017), и допущения
названы здесь.
ДОПУЩЕНИЯ:
1. Время дискретно: шаг 1 секунда, горизонт 30 минут.
2. Поток постоянный: 100 запросов в секунду.
3. Новая версия ломает 5 % запросов, которые на неё попадают. Дефект есть с
первой секунды её работы и не зависит от нагрузки.
4. Обнаружение: автоматика замечает беду, когда накопилось 200 ошибок, и
тратит на решение ещё 60 секунд. Раньше этого никто не смотрит.
5. Откат у каждой стратегии свой: 5 с у blue-green, 300 с у rolling (те же
десять реплик по одной), 30 с у канарейки, мгновенно у флага. Значения
стоят в rollback_seconds() и ниоткуда не выводятся — это допущение.
6. Ни ретраев, ни очередей: считаются только задетые запросы.
СТРАТЕГИИ. rolling — десять реплик по 30 с каждая; blue-green — переключение
всего трафика разом; canary — 5 % на пять минут, потом всё; feature-flag —
1 %, 10 %, 100 % с мгновенным откатом.
ЗАПУСК: python3 bench/rollout/strategies.py
Вывод: runs/strategies.txt
"""
import os
import sys
RPS = 100
HORIZON_S = 30 * 60
DEFECT_SHARE = 0.05
DETECT_ERRORS = 200
DECIDE_S = 60
RATE_WINDOW_S = 30
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def row(label: str, value: object) -> None:
print(f" {label:<40} {value}")
def share_on_new(strategy: str, t: int) -> float:
"""Доля трафика на новой версии в секунду t от начала выкатки."""
if strategy == "blue-green":
return 1.0
if strategy == "rolling":
# Десять реплик, по одной каждые 30 секунд.
return min(1.0, (t // 30 + 1) / 10)
if strategy == "canary":
return 0.05 if t < 300 else 1.0
if strategy == "feature-flag":
if t < 300:
return 0.01
if t < 600:
return 0.10
return 1.0
raise ValueError(strategy)
def rollback_seconds(strategy: str) -> int:
"""Сколько занимает возврат трафика на старую версию."""
return {"blue-green": 5, "rolling": 300, "canary": 30, "feature-flag": 0}[strategy]
def simulate(strategy: str, detector: str = "count") -> dict[str, int]:
"""Прогон одной стратегии с одним из двух способов обнаружения.
detector="count" — тревога, когда накопилось DETECT_ERRORS ошибок;
detector="rate" — тревога, когда доля ошибок на новой версии держится
выше порога RATE_WINDOW_S секунд, независимо от их
абсолютного числа.
"""
errors = 0
affected = 0
detected_at = -1
rolled_back_at = -1
bad_seconds = 0
for t in range(HORIZON_S):
if rolled_back_at >= 0 and t >= rolled_back_at:
share = 0.0
else:
share = share_on_new(strategy, t)
broken = RPS * share * DEFECT_SHARE
errors += broken
affected += broken
if share > 0 and broken > 0:
bad_seconds += 1
alarm = (
errors >= DETECT_ERRORS
if detector == "count"
else bad_seconds >= RATE_WINDOW_S
)
if detected_at < 0 and alarm:
detected_at = t
rolled_back_at = t + DECIDE_S + rollback_seconds(strategy)
return {
"affected": int(affected),
"detected_at": detected_at,
"rolled_back_at": rolled_back_at,
"share_at_detection": share_on_new(strategy, detected_at) if detected_at >= 0 else 0.0,
}
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
print("model, not a measurement: assumptions are in the docstring")
print(f"{RPS} requests per second, the new version breaks {DEFECT_SHARE:.0%} of what reaches it")
print(f"detection needs {DETECT_ERRORS} errors, then {DECIDE_S} s to decide")
show("1. DETECTION BY ERROR COUNT: 200 ERRORS RAISE THE ALARM")
print(f" {'strategy':>14} {'affected':>10} {'detected, s':>13} {'rolled back, s':>16}")
results = {}
for strategy in ("blue-green", "rolling", "canary", "feature-flag"):
res = simulate(strategy, "count")
results[strategy] = res
print(
f" {strategy:>14} {res['affected']:>10} {res['detected_at']:>13}"
f" {res['rolled_back_at']:>16}"
)
print()
print(" A counting detector needs a fixed number of errors, so a small")
print(" canary reaches it late: the alarm waits for the sample to grow.")
show("2. DETECTION BY ERROR RATE: 30 SECONDS OF A BAD SHARE")
print(f" {'strategy':>14} {'affected':>10} {'detected, s':>13} {'rolled back, s':>16}")
by_rate = {}
for strategy in ("blue-green", "rolling", "canary", "feature-flag"):
res = simulate(strategy, "rate")
by_rate[strategy] = res
print(
f" {strategy:>14} {res['affected']:>10} {res['detected_at']:>13}"
f" {res['rolled_back_at']:>16}"
)
print()
print(" A rate detector does not wait for volume, so the strategies")
print(" finally differ the way they are supposed to.")
show("3. THE SAME STRATEGY UNDER THE TWO DETECTORS")
print(f" {'strategy':>14} {'by count':>10} {'by rate':>10} {'times fewer':>13}")
for strategy in ("blue-green", "rolling", "canary", "feature-flag"):
a, b = results[strategy]["affected"], by_rate[strategy]["affected"]
print(f" {strategy:>14} {a:>10} {b:>10} {a / max(1, b):>12.1f}x")
print()
row("canary: traffic share at detection, by count", f"{results['canary']['share_at_detection']:.0%}")
row("canary: traffic share at detection, by rate", f"{by_rate['canary']['share_at_detection']:.0%}")
print()
print(" The defect is identical in all eight runs. What differs is how much")
print(" traffic stood on the new version when the problem became visible -")
print(" and a small canary only helps if the detector can see it there.")
if __name__ == "__main__":
main()