MEASUREMENT
bench/probes/model.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/health-checks
- How to run it
python3 bench/probes/model.py > bench/probes/runs/model.txt python3 bench/probes/practice.py > bench/probes/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
Модель для урока «Health-checks»
Это симуляция, а не замер: кластера в среде замеров нет, а поведение проб
задаётся документацией конкретных оркестраторов. Допущения перечислены в
докстринге model.py.
| Файл | Что делает |
|---|---|
model.py |
балансировщик и три реплики в четырёх режимах: без проб, только liveness, liveness с readiness и liveness, завязанный на общую базу |
practice.py |
ответы к задачам урока: медленные ответы в трёх режимах и цена каскада |
Запуск из корня репозитория:
python3 bench/probes/model.py > bench/probes/runs/model.txt
python3 bench/probes/practice.py > bench/probes/runs/practice.txt
Что в модели содержательно
Отношения: readiness сокращает число медленных ответов по сравнению с одним liveness; liveness, проверяющий общую зависимость, добавляет отказов сверх тех, что стоила сама авария. Абсолютные числа заданы допущениями (10 запросов на шаг, горизонт 60 секунд).
Чего в модели нет
Настоящего оркестратора, сети, разогрева процессов и разных порогов у разных проб.
Script
158 lines"""Health-checks: три разные проверки, которые зовут одним словом. Модель.
ЧТО ЭТО ЗА ФАЙЛ. Симуляция балансировщика и трёх реплик — без сети и без
кластера. Выводы верны настолько, насколько верны допущения (ADR-017), и
допущения названы здесь.
ДОПУЩЕНИЯ:
1. Время дискретно: шаг 100 мс, горизонт 60 секунд.
2. Три реплики, балансировщик раздаёт запросы по кругу между теми, кого
считает готовыми. Поток постоянный: 10 запросов на шаг.
3. Одна реплика с 10-й секунды становится «больной»: отвечает в десять раз
медленнее, но отвечает. Это не отказ, а деградация.
4. Проба выполняется раз в секунду; порог — три подряд неудачных ответа.
5. Перезапуск реплики занимает 5 секунд, всё это время она не обслуживает.
6. Общая зависимость (база) недоступна с 20-й по 30-ю секунду.
ЧТО СРАВНИВАЕТСЯ. Четыре режима: без проб; только liveness; liveness плюс
readiness; и liveness, завязанный на общую зависимость. Считаются медленные и
неуспешные ответы.
ЗАПУСК: python3 bench/probes/model.py
Вывод: runs/model.txt
"""
import os
import sys
STEP_MS = 100
HORIZON_S = 60
RPS_PER_STEP = 10
REPLICAS = 3
SICK_FROM_S = 10
SLOW_FACTOR = 10
DB_DOWN_FROM_S = 20
DB_DOWN_TO_S = 30
PROBE_EVERY_S = 1
FAIL_THRESHOLD = 3
RESTART_S = 5
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def row(label: str, value: object) -> None:
print(f" {label:<44} {value}")
def simulate(mode: str) -> dict[str, int]:
"""Один прогон модели.
mode:
none — проб нет, балансировщик считает живыми всех;
liveness — проба перезапускает больную реплику;
liveness+readiness — плюс вывод из ротации без перезапуска;
liveness-on-db — liveness проверяет общую зависимость.
"""
ready = [True] * REPLICAS
restart_until = [0] * REPLICAS
fails = [0] * REPLICAS
slow = 0
failed = 0
steps = HORIZON_S * 1000 // STEP_MS
for step in range(steps):
now_s = step * STEP_MS / 1000
db_down = DB_DOWN_FROM_S <= now_s < DB_DOWN_TO_S
# Пробы: раз в секунду
if step % (PROBE_EVERY_S * 1000 // STEP_MS) == 0:
for i in range(REPLICAS):
if now_s < restart_until[i]:
continue
sick = i == 0 and now_s >= SICK_FROM_S
# Что именно считает проба неудачей — и есть предмет урока.
if mode == "none":
bad = False
elif mode == "liveness":
bad = sick
elif mode == "liveness+readiness":
bad = sick
elif mode == "liveness-on-db":
bad = sick or db_down
else:
raise ValueError(mode)
fails[i] = fails[i] + 1 if bad else 0
if fails[i] >= FAIL_THRESHOLD:
fails[i] = 0
if mode == "liveness+readiness":
ready[i] = False # вывели из ротации, не перезапуская
else:
restart_until[i] = now_s + RESTART_S
ready[i] = False
for i in range(REPLICAS):
if now_s >= restart_until[i] and restart_until[i] != 0 and not ready[i]:
if mode != "liveness+readiness":
ready[i] = True
serving = [i for i in range(REPLICAS) if ready[i] and now_s >= restart_until[i]]
if not serving:
failed += RPS_PER_STEP
continue
for n in range(RPS_PER_STEP):
replica = serving[n % len(serving)]
sick = replica == 0 and now_s >= SICK_FROM_S
if db_down:
failed += 1
elif sick:
slow += 1
return {"slow": slow, "failed": failed, "total": steps * RPS_PER_STEP}
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"{REPLICAS} replicas, {RPS_PER_STEP} requests per {STEP_MS} ms step, {HORIZON_S} s horizon")
print(f"replica 1 degrades from {SICK_FROM_S} s; the shared database is down {DB_DOWN_FROM_S}-{DB_DOWN_TO_S} s")
results = {}
for mode in ("none", "liveness", "liveness+readiness", "liveness-on-db"):
results[mode] = simulate(mode)
show("REQUESTS SERVED SLOWLY OR NOT AT ALL")
print(f" {'mode':>20} {'slow':>10} {'failed':>10} {'total':>10}")
for mode, res in results.items():
print(f" {mode:>20} {res['slow']:>10} {res['failed']:>10} {res['total']:>10}")
show("WHAT THE THREE MODES ACTUALLY CHANGE")
row("no probes: slow answers", results["none"]["slow"])
row("liveness only: slow answers", results["liveness"]["slow"])
row("liveness + readiness: slow answers", results["liveness+readiness"]["slow"])
row(
"liveness tied to the database: failures",
results["liveness-on-db"]["failed"],
)
row(
"the same failures without that tie",
results["liveness"]["failed"],
)
print()
extra = results["liveness-on-db"]["failed"] - results["liveness"]["failed"]
row("failures added by tying liveness to the database", extra)
print()
print(" The outage itself is the same length in both runs. The extra")
print(" failures are the cascade: a liveness probe that checks a shared")
print(" dependency restarts every replica at once, and the service stays")
print(" down after the database is already back.")
if __name__ == "__main__":
main()