load-tester Все курсы

✻ Урок 13.1 · Тема 13: Финал: работа и собеседования

Инцидент под нагрузкой: дежурство, разбор, постмортем

⏱ 3 ч

Зачем это нужно

Я твой опытный коллега, который сидит рядом, и начну с байки. Как-то на моём первом дежурстве пришло ночное сообщение «магазин отвечает медленно». Я полчаса искал, почему это случилось, а заказы тем временем не проходили. Старший коллега спросил: «А что ты сделал для покупателей в первые пять минут?» Ничего. С тех пор я знаю: сначала вернуть людям работающий сервис, потом выяснять причину.

Вернёмся к магазину. До сих пор ты ломал его сам и смотрел на графики с готовым ответом в голове. Перед распродажей нужна репетиция настоящего дежурства: причину ты не знаешь, а в чате уже спрашивают «что происходит?». Инцидент (incident) это событие, из-за которого покупатели получают сервис хуже обещанного: он медленный, выдаёт ошибки или недоступен. Дежурный (on-call) это инженер, который в свою смену первым получает тревогу и отвечает за её разбор.

Нагрузочнику это нужно дважды. Твой тест сам может вызвать сбой, поэтому его согласуют заранее, а чужой сервис без разрешения нагружать нельзя. А на проде ты читаешь нагрузку лучше всех: «это насыщение пула», «это ретраи», «это хвост задержки».

Шаг проекта: в ~/perf-lab/13-final/incident/ ты запустишь нагрузку на «Магазин», а скрипт в случайный момент и случайным способом сломает оплату. Ты продежуришь этот инцидент: найдёшь причину по дашборду, метрикам и логам, смягчишь последствия и напишешь постмортем (postmortem) по шаблону ~/perf-lab/13-final/postmortem.md. Это разбор сбоя после того, как всё починено: что случилось и что меняем, чтобы не повторилось. Постмортем пойдёт в портфолио (урок 13.3): работодатели любят, когда кандидат разбирает сбои без поиска виноватых.

Что нужно знать

  • Стенд «Магазин» с профилем мониторинга и дашбордом «Магазин: обзор (эталон)»: урок 7.3. Алерты и Alertmanager: урок 7.6.
  • PromQL: rate, sum by, histogram_quantile: урок 7.2. Логи в Loki: урок 7.5.
  • Метод «от симптома к причине», RED и USE: урок 11.1. Пул соединений: урок 11.3. Таймауты и ретраи оплаты: урок 11.5.
  • Locust в безголовом режиме: урок 9.4. Закон Литтла и очереди: урок 8.2.

Картина целиком

В ресторане в час пик повар (приложение) пробивает каждый заказ на терминале оплаты и стоит у него, пока тот не ответит. Терминал «задумался» на две секунды: повара выстроились в очередь, плита простаивает, гости уходят. Управляющий (дежурный) не чинит терминал сам: он смотрит, сколько гостей страдает, зовёт мастера или переводит гостей на наличные. Разбираться, почему терминал не предупредил, он будет после вечера. Аналогия ломается в одном: в ресторане всё видно глазами, а в сервисе есть только графики, и они показывают следствие, а не причину.

Цепочка нашего учебного инцидента:

sequenceDiagram
    participant Г as Генератор
    participant М as Магазин
    participant П as Пул БД
    participant О as Оплата
    Г->>М: POST /api/orders
    М->>П: взять соединение
    М->>О: POST /pay
    Note over О: delay_ms вырос до 2500
    О-->>М: ответ через 2,5 с
    М->>П: вернуть соединение

Заказ держит соединение с базой, пока ждёт оплату. Когда оплата медленная, пять соединений пула быстро заняты, а остальные запросы (даже каталог, которому оплата не нужна) стоят в очереди и получают 503. Источник беды снаружи, а страдает всё.

А так выглядит порядок дежурного: сначала смягчить, потом разбираться.

flowchart TD
    A["Алерт или жалоба"] --> B["Оценить влияние:<br/>кто и как сильно страдает"]
    B --> C["Найти, что изменилось:<br/>RED, USE, логи"]
    C --> D["Смягчить:<br/>вернуть сервис в строй"]
    D --> E["Убедиться по метрикам,<br/>что стало лучше"]
    E --> F["Постмортем:<br/>причины и действия"]

Поиск причины нужен ровно настолько, насколько он помогает смягчить. Полный разбор идёт после того, как покупателям стало хорошо.

Теория

Что считается инцидентом

На графике каждый день что-то подпрыгивает. Будить людей из-за каждого прыжка нельзя, а пропустить настоящий сбой ещё хуже. Где граница?

Метрика, которая выросла на минуту и вернулась, это просто событие. Инцидент начинается там, где покупатели страдают заметно. Чтобы не спорить об этом ночью, команды заранее договариваются о серьёзности (severity). SEV1 (от severity, «серьёзность»): сервис недоступен или заказы не проходят у всех, будим людей. SEV2: сильная деградация у заметной части покупателей, дежурный занимается сразу. SEV3: неприятно, но есть обходной путь, разберёмся днём.

Граница привязана к SLO, целям по надёжности из урока 8.4: p95 или ошибки вышли за цель и не возвращаются. На стенде алерты ShopDown и ShopHighErrorRate (из project/shop/monitoring/prometheus/rules/) помечены critical, а ShopHighLatencyP95 и ShopDbPoolExhausted помечены warning.

Прикинь сам: p95 вырос до 1,3 с на две минуты и вернулся сам. Инцидент это или нет?

Скорее нет: алерт ShopHighLatencyP95 ждёт пять минут как раз ради таких всплесков. Но запиши его: повторяющиеся всплески это уже проблема (problem), глубинная причина многих инцидентов, например «оплата держит транзакцию». Инцидент это конкретный сбой с началом и концом. Сначала гасят инцидент, потом занимаются проблемой.

Главное: инцидент начинается там, где покупатели страдают заметно, а границу серьёзности договаривают заранее, по SLO.

Как нескольким людям не мешать друг другу?

Роли: кто что делает

Если за инцидент взялись трое без договорённости, все трое смотрят один график и никто не пишет в общий чат. Поэтому роли делят заранее.

Руководитель инцидента (incident commander, IC) принимает решения и держит общую картину, сам по консолям не лазит. Его вопросы: «что знаем, что делаем, кто чем занят». Оператор (operator) смотрит графики, меняет настройки, откатывает и говорит вслух, что делает. Связь (communications) коротко и по расписанию пишет тем, кого касается: поддержке, руководству. Секретарь (scribe) ведёт хронологию, и из его записей потом вырастает постмортем.

flowchart TD
    IC["Руководитель инцидента<br/>решает"] --> OP["Оператор<br/>проверяет и меняет"]
    IC --> CM["Связь<br/>сообщает"]
    IC --> SC["Секретарь<br/>записывает"]
    OP --> SC

Всё, что делает оператор, попадает секретарю: память после ночи плохой свидетель. На стенде ты один, и я бы тут сделал так: надевай «шляпы» по очереди. Пиши «шляпа: оператор», потом «шляпа: IC», а решения вроде «откатываем» проговаривай, а не принимай молча.

Главное: роли делят заранее, хронологию ведут с первой минуты.

По хронологии считают, насколько быстро мы справились.

Хронология и три важных времени

Инцидент измеряют цифрами, как нагрузку. Три сокращения ниже значат «сколько минут до того-то»: сначала узнали, потом ответили, потом починили. Допустим, сбой начался в 14:05, алерт пришёл в 14:11, ты ответил в 14:13, а порядок вернулся в 14:40. Разбор начнётся с трёх вопросов.

Сколько мы не знали о сбое: 14:11 минус 14:05, шесть минут. Это время обнаружения (time to detect, TTD: узнали). Сколько алерт ждал человека: 14:13 минус 14:11, две минуты. Это время реакции (time to acknowledge, TTA: ответили). Сколько покупатели жили с проблемой: от 14:05 до 14:40, 35 минут. Это время восстановления (time to restore, MTTR: починили; так же называют среднее по многим инцидентам). Оно включает обнаружение, реакцию, диагностику и смягчение.

Для алерта с for: 5m обнаружение не бывает быстрее пяти минут: это защита от ложных тревог, но и цена, пока ты ждёшь. Поэтому быстрые алерты (пул, доля ответов 5xx, то есть кодов вроде 503: «сервер сам не справился») и медленные (p95) живут рядом. Подвигай слайдер реакции на модели.

Сбой начинается на минуте 5, красная зона это время, когда покупатели страдают. Чем позже откат, тем больше ошибок. Найди, при какой реакции алерты не успевают сработать: это не победа, а повод спросить, как о такой деградации узнают люди.

Главное: TTD, TTA и MTTR считаются из хронологии вычитанием, а for задаёт нижнюю границу обнаружения.

Времена посчитаны. Теперь сам вопрос ночи: как за минуты найти, что сломалось?

Пошаговая диагностика

На стенде есть ровно то, что ты настраивал в теме 7: метрики, логи и алерты. Идти надо от общего к частному, не гадая.

Первые три шага про масштаб, остальные про причину. RED и USE здесь те же, что в уроке 11.1, но цель другая: не изучить, а быстро решить.

Пример. На дашборде p95 равен 4,2 с, 5xx составляют 18%, CPU shop 12%, shop_db_pool_available равно 0, shop_db_pool_waiting равно 6. p95 и 5xx говорят «плохо всем». CPU низкий: приложение не считает, а ждёт. Пул пуст: ждёт соединения с базой. Сама база вряд ли медленная, иначе вырос бы её CPU. Кто же держит соединения? Ты вспоминаешь урок 11.5: заказ держит соединение, пока платит. Смотришь shop_payment_duration_seconds: p95 равен 2,9 с вместо 0,05. Гипотеза готова, и её проверяют действием: вернуть оплату в норму.

Осторожно: искать причину в том, что «выглядит хуже всего». Самый красный график часто последствие. Ищи тот, который изменился первым.

Главное: от алерта к причине идут так: масштаб по RED, «занят или ждёт» по USE, подтверждение логом, и первым ищут то, что изменилось раньше всех.

Гипотеза есть. Что делать первым: чинить или спасать?

Смягчение, исправление, причина

Смягчение (mitigation) быстро возвращает покупателям нормальную работу, даже грубым действием: откатить настройку, отключить функцию, снизить нагрузку, перезапустить сервис. Оно не обязано быть красивым, оно обязано быть быстрым и безопасным. Исправление (fix) убирает слабое место, например выносит оплату из транзакции. Его делают потом, в рабочее время, с тестом. Причина (cause) отвечает на вопрос «почему такое вообще могло случиться». Причин почти всегда несколько: триггер (оплата замедлилась), слабое место (транзакция держит соединение) и дыра в наблюдении (нет алерта на задержку оплаты).

Что выбрать в нашем случае? Лучше всего вернуть оплату в норму, если можешь откатить настройку: риск низкий. Нельзя вернуть: снижай входящую нагрузку, часть покупателей получит отказ, зато остальные будут работать. Перезапуск shop уместен при «зависших» соединениях, но причину не лечит. А увеличивать пул почти никогда не стоит. В уроке 11.3 это помогало, потому что причиной был размер пула. Здесь ты лишь спрячешь симптом и нагрузишь базу (см. «Сломай и почини»).

Осторожно: «смягчили» это не «починили». Пока оплата держит транзакцию, следующий её сбой даст тот же инцидент, поэтому в постмортеме обязательны действия по исправлению.

Главное: сначала смягчение (быстро и безопасно), потом исправление, а причин обычно несколько.

Теперь вторая поломка оплаты: она не медленная, а нестабильная. Тут магазин сам делает ситуацию хуже.

Ретраи: как сбой зависимости размножается

Оплата отвечает отказом в 60% случаев (fail_rate 0,6). Магазин при отказе повторяет запрос: PAYMENT_RETRIES=3 значит три повтора без паузы, то есть до четырёх попыток на один заказ (урок 11.5).

Посчитаем. Все четыре попытки подряд упадут с вероятностью 0,6 в четвёртой степени, это около 13%. Значит, большинство заказов всё же проходит. А вот оплате достаётся больше работы: первая попытка есть всегда, вторая нужна в 60% случаев, третья в 36%, четвёртая в 22%. Всего 1 + 0,6 + 0,36 + 0,216, это около 2,2 запроса на заказ (точнее 2,18). Оплата получает примерно в 2,2 раза больше запросов и тормозит ещё сильнее.

Ошибки зависимости превращаются в лишнюю нагрузку на неё же. Это называют штормом повторов (retry storm). В метриках он виден так: shop_payment_requests_total{result="error"} растёт быстрее, чем shop_orders_created_total.

Главное: повторы без пауз умножают нагрузку на уже больную зависимость, при 60% отказов примерно в 2,2 раза.

Остаётся понять, почему при нагрузке всё рушится вдруг, а не плавно.

Почему под нагрузкой сбои каскадные

Под нагрузкой сбои не нарастают плавно, а обрушиваются: больная часть заставляет остальные ждать, ожидание занимает ресурсы, и плохими становятся ещё больше запросов. Покажу на числах.

В пуле пять соединений. Пока оплата отвечает за 0,05 с, заказ держит соединение около 0,06 с: оплата плюс пара быстрых запросов к базе. Пять соединений пропускают 5 / 0,06, то есть около 83 заказов в секунду. Приходит 4 заказа в секунду: запас в двадцать раз, очереди нет.

Прикинь сам: оплата замедлилась до 2,5 с, соединение теперь занято 2,56 с. Сколько заказов в секунду пропустит пул, если приходит по-прежнему 4?

5 / 2,56 даёт около 2 заказов в секунду. Приходит 4, значит два в секунду «не помещаются», очередь растёт без остановки. Ожидание упирается в DB_POOL_TIMEOUT: так называется настройка стенда в .env, сколько секунд запрос ждёт свободное соединение, у нас пять. По истечении запрос получает 503. Сервис перешёл от «запас в двадцать раз» к «двойной перегрузке» из-за одного изменённого числа, времени оплаты. Это закон Литтла из урока 8.2 в действии. Порог здесь резкий. При оплате в 1000 мс соединение занято около 1,06 с, пул тянет 5 / 1,06, то есть 4,7 заказа в секунду, и 4 помещаются. При 1300 мс это 5 / 1,36, около 3,7: четыре уже не помещаются, и ошибки появляются, хотя до этого их не было совсем.

Сбой усиливается дважды. Каталог, карточки и корзина не связаны с оплатой, но ждут тот же пул: любой общий ресурс связывает независимые части системы. А клиенты, получив 503, пробуют снова, и очередь растёт. Хорошие системы ограничивают это паузами между повторами и быстрым отказом вместо долгого ожидания.

Главное: пропускная способность пула равна числу соединений, делённому на время удержания одного, поэтому замедление зависимости обрушивает всё, что делит с ней пул.

Пока оператор ищет причину, кто-то должен отвечать людям. Как это сделать без паники?

Как писать статусы: связь без паники

«Проблемы с магазином, разбираемся» никому не помогает. В хорошем сообщении четыре вещи: что видим (факт с цифрой), кого касается, что делаем и когда следующее обновление. Например:

11:42 Заказы оформляются медленно (p95 около 5 с), у части запросов ошибки (около 20%).
Затронуты все покупатели. Похоже, замедлилась оплата, проверяем и готовим откат настройки.
Следующее обновление в 11:50.

Не называй причину, пока она не подтверждена: «похоже» честнее ошибочного «виновата оплата». Пиши цифры, а не прилагательные. И выполняй обещание об обновлении, даже если сказать нечего: «без изменений, продолжаем».

Главное: в статусе всегда четыре вещи: что видим, кого касается, что делаем, когда следующее обновление.

Как сделать, чтобы следующей ночью порядок не пришлось изобретать заново?

Инструкция для дежурного и бюджет ошибок

Для типовых алертов пишут runbook (дословно «книга запуска»): короткую инструкцию, что значит алерт, куда смотреть, какие смягчения безопасны, кого звать. Для ShopDbPoolExhausted хватит десяти строк.

Серьёзность связана и с бюджетом ошибок (error budget). Если SLO обещает, что 99,5% запросов успешны и быстры, остальные 0,5% можно «потратить» на сбои. Посчитаем на месяце в 43 200 минут: бюджет 0,5%, это 216 минут. Пятая часть запросов провалилась на десять минут: это как две минуты полного простоя, то есть около 1% бюджета. А три часа таких отказов съедят уже 36 минут, то есть шестую часть. Когда бюджет тает, команда сначала занимается надёжностью. Нагрузочнику это удобный язык: «замедление оплаты стоит нам часть бюджета» убедительнее, чем «пул маленький».

Главное: runbook экономит ночь, а бюджет ошибок переводит сбой в цену, понятную руководству.

Остаётся документ, ради которого инцидент проживают заново.

Постмортем без обвинений

Постмортем пишут, когда сервис уже работает. Его цель: понять, как система допустила сбой, и договориться, что изменить. Подход называется «blameless», «без поиска виноватого». Мы не выясняем, кто нажал не ту кнопку: в хорошей системе одна кнопка не роняет сервис. Если наказывать за ошибки, люди перестают о них рассказывать.

Прикинь сам: как переписать фразу «Оператор забыл поставить алерт на оплату», чтобы в ней не было виноватого?

Например так: «В системе не было алерта на задержку оплаты, поэтому о сбое мы узнали по ошибкам покупателей». Фраза «Инженер выставил слишком маленький пул» превращается в «Пул рассчитан на нормальную работу, а не на деградацию зависимости». Так называют факт и условие, а не человека, и сразу видно действие.

Структура постмортема, которой ты воспользуешься:

  1. Кратко: два-три предложения, что случилось.
  2. Влияние: сколько длилось, какая доля запросов, какие функции пострадали, цифры из метрик.
  3. Хронология: время, событие, кто или что. Первые три колонки обязательны.
  4. Причины: триггер, слабые места, что способствовало.
  5. Что сработало, что не сработало, где повезло. «Повезло» важно: именно там прячутся следующие инциденты.
  6. Действия: что сделаем, кто владелец, срок. Действие без владельца и срока это пожелание.
  7. Уроки: что узнали.

Для причин есть приём «пять почему» (5 whys): спрашиваешь «почему» от следствия к причине, пока не дойдёшь до того, что можно изменить. Если цепочка закончилась словами «потому что Иван», спроси ещё раз «почему система это допустила».

Главное: постмортем описывает, как система допустила сбой, и заканчивается действиями с владельцем и сроком, а не именем виноватого.

Вернёмся к распродаже: если нужен один навык, пусть это будет «сначала смягчить, потом разбираться, всё записывать». Теперь тренируйся на стенде.

Практика

Всё в этом уроке ты запускаешь на своём локальном стенде. Потом остановишь генератор, и стенд вернётся к норме.

1. Подготовь стенд и дашборд

cd ~/learning/load-tester/project/shop
docker compose --profile monitoring up -d --wait
curl -s localhost:8000/readyz
curl -s localhost:8001/admin/config

Первая команда поднимает стенд вместе с мониторингом (если уже работает, ничего не изменится). curl localhost:8000/readyz проверяет магазин, а localhost:8001/admin/config показывает текущие настройки оплаты.

Ожидаемый вывод:

{"status":"ready"}
{"delay_ms":50.0,"fail_rate":0.0}

Как читать вывод: ready значит, что база и Redis отвечают. delay_ms 50 и fail_rate 0 это здоровое состояние оплаты, с которым мы сравниваем. Если у тебя другие числа после прошлых экспериментов, верни нормальные (см. шаг 6).

Открой в браузере Grafana (http://localhost:3000, дашборд «Магазин: обзор (эталон)»), Prometheus на вкладке Alerts (http://localhost:9090/alerts) и Alertmanager (http://localhost:9093). Включи на дашборде автообновление раз в 5 секунд, а период поставь «последние 15 минут».

Типичные ошибки: connection refused на 3000 или 9090: стенд поднят без профиля monitoring, повтори команду выше с --profile monitoring.

2. Создай рабочий каталог и сценарий нагрузки

mkdir -p ~/perf-lab/13-final/incident
cd ~/perf-lab/13-final/incident
cat > locustfile.py <<'EOF'
import random

from locust import HttpUser, between, task


class Buyer(HttpUser):
    wait_time = between(1, 3)

    def on_start(self):
        number = random.randint(1, 1000)
        response = self.client.post("/api/login", name="/api/login",
                                    json={"email": f"user{number:04d}@shop.lab", "password": "password"})
        self.client.headers.update({"Authorization": "Bearer " + response.json()["token"]})

    @task
    def buy(self):
        product = random.randint(1, 10000)
        self.client.get("/api/products", params={"page": random.randint(1, 10)}, name="/api/products")
        self.client.get(f"/api/products/{product}", name="/api/products/[id]")
        self.client.post("/api/cart/items", json={"product_id": product, "qty": 1}, name="/api/cart/items")
        self.client.post("/api/orders", name="/api/orders")
EOF

Сценарий короткий: каждый «покупатель» один раз входит, потом в цикле смотрит каталог, открывает карточку, кладёт товар в корзину и оформляет заказ, с паузой 1-3 секунды. name= собирает разные URL карточек в одну строку статистики (приём из урока 9.2). Заказ здесь идёт каждый цикл, потому что именно он ходит в оплату. Десять таких пользователей дают порядка 4 заказов и 16-20 запросов в секунду, что для пула из пяти соединений нормально, пока оплата быстрая.

3. Скрипт, который «ломает оплату» без предупреждения

cat > inject.sh <<'EOF'
#!/usr/bin/env bash
# Через 4-7 минут тихо ломает оплату одним из двух способов и записывает, каким.
set -euo pipefail
cd "$(dirname "$0")"
sleep $(( 240 + RANDOM % 180 ))
if (( RANDOM % 2 )); then
  kind=slow;  body='{"delay_ms":2500}'
else
  kind=flaky; body='{"fail_rate":0.6}'
fi
curl -fsS localhost:8001/admin/config -H 'Content-Type: application/json' -d "$body" > /dev/null
echo "$kind $(date +%T)" > .fault
EOF
cat > reset.sh <<'EOF'
#!/usr/bin/env bash
# Возвращает оплату в нормальное состояние.
curl -fsS localhost:8001/admin/config -H 'Content-Type: application/json' -d '{"delay_ms":50,"fail_rate":0}'
EOF
chmod +x inject.sh reset.sh

Разбор. set -euo pipefail останавливает скрипт при любой ошибке (урок 1.5). $(( 240 + RANDOM % 180 )) это случайное число секунд от 240 до 419: RANDOM встроенная переменная bash, % остаток от деления. if (( RANDOM % 2 )) выбирает одну из двух поломок. curl ... -d "$body" отправляет на оплату новые настройки (поле, которое не указано, остаётся прежним). В файл .fault записывается тип и время, но ты его не открываешь до конца разбора: так же, как на реальном дежурстве ты не знаешь ответ заранее. Ты автор скрипта и знаешь варианты, но не знаешь, какой выпадет и когда, а этого достаточно.

4. Запусти нагрузку и дежурство

В первом терминале запусти генератор на 14 минут:

source ~/perf-lab/.venv/bin/activate
cd ~/perf-lab/13-final/incident
rm -f .fault && ./reset.sh > /dev/null
locust -f locustfile.py --host http://localhost:8000 --headless -u 10 -r 2 -t 14m --csv run

Флаги: --headless без веб-интерфейса, -u 10 десять пользователей, -r 2 по два новых в секунду, -t 14m длительность, --csv run результаты в файлы run_*.csv. Locust печатает таблицу каждые 5 секунд, там колонки Requests, Fails, Median, 95%ile.

Во втором терминале запусти скрипт сбоя в фоне:

cd ~/perf-lab/13-final/incident
nohup ./inject.sh > /dev/null 2>&1 &

nohup и & отправляют скрипт в фон, вывод выбрасывается: дежурный не получает уведомления о том, что ему сломали.

Теперь ты дежурный. Заведи файл хронологии и записывай всё, что видишь и делаешь, с временем:

cat > incident-log.md <<'EOF'
# Хронология инцидента (время локальное)
EOF
note() { echo "- $(date +%T) $*" >> ~/perf-lab/13-final/incident/incident-log.md; }
note "нагрузка 10 пользователей запущена, всё в норме"

note функция bash: добавляет в файл строку с текущим временем. Дальше пиши note "алерт ShopDbPoolExhausted в Alertmanager", note "гипотеза: оплата, проверяю" и так далее. Первые 4-7 минут просто наблюдай норму: запомни значения p95, RPS и доли 5xx. Именно с ними ты потом сравниваешь.

5. Диагностика

Когда на дашборде что-то изменится или сработает алерт, действуй по схеме выше. Сначала посмотри, какие алерты горят:

curl -s localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | [.labels.alertname, .state] | @tsv'

Здесь jq -r '.data.alerts[] | [...] | @tsv' берёт массив алертов и печатает по строке «имя, состояние». Пример для медленной оплаты:

ShopDbPoolExhausted	firing
ShopHighErrorRate	pending

Как читать вывод: pending значит, что условие выполняется, но срок for ещё не вышел; firing значит, что алерт уже сработал. Сначала сработал алерт пула (его for одна минута), а алерт по ошибкам ещё ждёт (две минуты). Ровно то, что ты видел на модели.

Оцени масштаб (RED):

q() { curl -s localhost:9090/api/v1/query --data-urlencode "query=$1" | jq -r '.data.result[] | [(.metric | tostring), .value[1]] | @tsv'; }
q 'sum(rate(http_requests_total[1m]))'
q 'sum(rate(http_requests_total{status=~"5.."}[1m])) / sum(rate(http_requests_total[1m]))'
q 'histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[1m])))'

Функция q оборачивает запрос к HTTP API Prometheus: --data-urlencode сам кодирует символы PromQL, jq достаёт значение. Пример вывода во время инцидента:

{}	6.8
{}	0.21
{}	4.9

Как читать вывод: RPS упал с примерно 18 до 6,8, пятая часть запросов (21%) заканчивается 5xx, p95 вырос до 4,9 с (в норме около 0,1). Пользователи страдают заметно: это SEV2.

Теперь отделим «занят» от «ждёт» и найдём, кто держит соединения:

q 'shop_db_pool_waiting'
q 'shop_db_pool_available'
q 'histogram_quantile(0.95, sum by (le) (rate(shop_payment_duration_seconds_bucket[1m])))'
q 'sum by (result) (rate(shop_payment_requests_total[1m]))'
{"__name__":"shop_db_pool_waiting"}	5
{"__name__":"shop_db_pool_available"}	0
{}	2.9
{"result":"ok"}	1.9

Как читать вывод: в очереди за соединением пять запросов, свободных соединений ноль. Оплата отвечает с p95 2,9 с (в норме 0,06), результаты все ok: оплата не падает, она медленная. Если бы result="error" рос, а длительность оставалась маленькой, это был бы вариант «нестабильная оплата». Так по двум числам различаются два сценария.

Подтверди логами. В Grafana открой Explore, источник Loki, запрос:

{service="shop",level="ERROR"}

Строки выглядят так (сокращено):

{"ts":"2026-10-03T11:42:07+00:00","level":"ERROR","msg":"Запрос завершён","method":"GET","route":"/api/products","status":503,"duration_ms":5003.1,"request_id":"c1f0...","trace_id":"8e2b...","error":"couldn't get a connection after 5.00 sec"}

Как читать вывод: status 503 и error «couldn’t get a connection after 5.00 sec» подтверждают, что запросы не получают соединение из пула за DB_POOL_TIMEOUT (5 секунд, поэтому duration_ms около 5000). Заметь route: пострадал /api/products, который к оплате отношения не имеет. Это и есть доказательство, что проблема общая (пул), а не локальная. В полях логов нет «виновника», он только в метриках оплаты: поэтому нужен и тот, и другой источник.

Запиши в хронологию: note "гипотеза: оплата медленная, заказы держат пул. p95 оплаты 2,9 с".

6. Смягчение и проверка

Ты не владелец «внешней» оплаты, но на стенде у тебя есть ручка, которой в жизни соответствует «вернуть предыдущую конфигурацию» или «переключиться на резервного провайдера». Применяем смягчение:

cd ~/perf-lab/13-final/incident
note "смягчение: возвращаю настройки оплаты"
./reset.sh
{"delay_ms":50.0,"fail_rate":0.0}

Подожди 1-2 минуты (Prometheus собирает метрики раз в 5 секунд, а запросы используют окно в минуту) и повтори команды q из шага 5. Ожидаемо: очередь пула равна нулю, 5xx падает до нуля, p95 возвращается к 0,1 с. Алерты перейдут в resolved. Запиши момент, когда по метрикам стало хорошо: note "восстановлено: p95 0,1 с, 5xx 0%". Это время окончания инцидента для постмортема.

Типичные ошибки: {"detail":...} или Connection refused на 8001: контейнер оплаты остановлен, проверь docker compose ps. Графики не восстановились: окно [1m] ещё содержит старые данные, подожди две минуты и только потом делай вывод.

Дождись конца Locust (14 минут от старта) или останови его Ctrl+C. Теперь можно открыть .fault:

cat ~/perf-lab/13-final/incident/.fault
slow 11:39:48

Сравни с хронологией: во сколько ты заметил сбой, во сколько нашёл причину, во сколько смягчил. Разница между временем в .fault и твоей первой записью о сбое и есть твоё время обнаружения.

7. Напиши постмортем

cd ~/perf-lab/13-final
cat > postmortem.md <<'EOF'
# Постмортем: <название одной строкой>

Дата: <ГГГГ-ММ-ДД>. Автор: <ты>. Статус: черновик / готов.

## Кратко
<2-3 предложения: что случилось, сколько длилось, чем закончилось>

## Влияние
- Длительность: <от и до, минут>
- Что видели пользователи: <медленно / ошибки 503 / не оформить заказ>
- Цифры: <пик p95, пик доли 5xx, на сколько упал RPS>

## Хронология
| Время | Событие | Кто / что |
| --- | --- | --- |
|  | начало сбоя (по .fault) | система |
|  | первый алерт | Alertmanager |
|  | заметил | дежурный |
|  | гипотеза | дежурный |
|  | смягчение | дежурный |
|  | по метрикам стало хорошо | дежурный |

## Причины
- Триггер: <что изменилось>
- Слабые места: <что позволило сбою разрастись>
- Дыры в наблюдении: <чего не хватило, чтобы заметить быстрее>

## Что сработало
## Что не сработало
## Где повезло

## Действия
| Действие | Владелец | Срок | Как проверим |
| --- | --- | --- | --- |
|  |  |  |  |

## Уроки
EOF

Скопируй время событий из incident-log.md, цифры из шага 5 и заполни все разделы. Для раздела «Причины» дай минимум три пункта: триггер (оплата замедлилась), слабое место (оплата внутри транзакции держит соединение пула, ретраи без паузы увеличивают нагрузку) и дыру в наблюдении (нет алерта на задержку оплаты shop_payment_duration_seconds). В «Действиях» предложи минимум три конкретных: вынести вызов оплаты за пределы транзакции, добавить алерт на p95 оплаты, добавить паузу и ограничение повторов. Проверь текст на «виноватых»: ни одного человека в причинах быть не должно.

Заверши коммитом:

cd ~/perf-lab
git add 13-final
git commit -m "13.1: инцидент под нагрузкой, хронология и постмортем"
git push

Сломай и почини

Теперь проверим соблазнительное «лечение»: увеличить пул. Повтори весь инцидент (шаги 4-6), но на этот раз не откатывай оплату. Вместо этого, когда найдёшь причину, измени ~/learning/load-tester/project/shop/.env: поставь DB_POOL_MAX=20 и пересоздай магазин:

cd ~/learning/load-tester/project/shop
sed -i.bak 's/^DB_POOL_MAX=.*/DB_POOL_MAX=20/' .env && rm -f .env.bak
docker compose up -d shop

sed -i.bak 's/.../.../' заменяет строку в файле на месте (суффикс .bak делает команду одинаковой для GNU sed и BSD sed на macOS, rm убирает копию). docker compose up -d shop пересоздаёт только контейнер магазина с новой настройкой. Подожди две минуты и посмотри на дашборд.

Что ты увидишь: очередь пула и 503 исчезли, 5xx стало 0%. Но p95 остался около 2,6 с, потому что каждый заказ по-прежнему ждёт медленную оплату. Ты вылечил симптом (отказы), но не пользовательский опыт (задержку), и вдобавок теперь до двадцати соединений с PostgreSQL могут часами висеть в открытых транзакциях с блокировкой строк товаров (FOR UPDATE). Проверь:

docker compose exec -T postgres psql -U shop -d shop -c "SELECT state, count(*) FROM pg_stat_activity WHERE datname='shop' GROUP BY state;"

В строке idle in transaction ты увидишь несколько соединений: это заказы, которые держат транзакцию, пока ждут оплату. Вывод для постмортема: увеличение пула это не смягчение, а перенос проблемы глубже, к базе. Настоящее смягчение: вернуть оплату или снизить нагрузку, настоящее исправление: не держать соединение во время сетевого вызова.

Верни всё как было:

sed -i.bak 's/^DB_POOL_MAX=.*/DB_POOL_MAX=5/' .env && rm -f .env.bak
docker compose up -d shop
~/perf-lab/13-final/incident/reset.sh

Второе задание: повтори инцидент ещё раз, пока .fault не покажет flaky. Составь для него короткую таблицу признаков: чем он отличается от slow в метриках оплаты (результат error, а не рост длительности), какие коды ответов (502 вместо 503) и почему здесь работает шторм повторов.

Сначала сам собери хронологию по дашборду и логам, записав время и источник каждого факта. Потом можно отдать нейросети черновик и попросить придраться. Каждую строку хронологии проверь по графику Grafana.

ИИ в помощь

Нейросеть помогает привести заметки инцидента в порядок и найти дыры в постмортеме, но причины и числа должны быть твоими. Общие правила: ИИ-помощник.

Задача: оформить хронологию из своих заметок.

Вот мои заметки об инциденте на учебном стенде «Магазин» (время и что видел): <вставь заметки со временем>.
Оформи хронологию таблицей: время, событие, источник (метрика, лог, действие). Ничего не добавляй:
если источник неясен, оставь пустую ячейку и перечисли вопросы, которые нужно проверить.

Проверь ответ: сверь каждое время и каждое число с графиками и логами. Типичная ошибка: нейросеть заполняет пробелы правдоподобными событиями, которых не было, и сглаживает порядок событий, из-за чего причина и следствие меняются местами.

Задача: проверить постмортем на «поиск виноватого» и слабые выводы.

Прочитай черновик постмортема: <вставь текст>. Найди: формулировки, где виноват человек, а не система;
причины без доказательств; уроки, которые нельзя проверить; действия без владельца и срока.
Предложи переформулировки, но факты не меняй.

Проверь ответ: каждое предложение должно опираться на твои данные. Типичная ошибка: «глубокая первопричина» в общих словах («недостаточный мониторинг») вместо конкретной цепочки из урока (оплата, ретраи, пул).

Логи и трафик настоящих систем с персональными данными в чат не отправляй.

Словарик урока

Термин Простыми словами
Инцидент (incident) Событие, из-за которого пользователи получают сервис хуже обещанного: медленно, с ошибками или никак.
Серьёзность (severity, SEV) Заранее согласованный уровень «насколько всё плохо», от которого зависит, кого будят и что делают.
Дежурный (on-call) Человек, который в свою смену отвечает на алерты и разбирается с инцидентами.
Руководитель инцидента (IC) Тот, кто принимает решения и держит общую картину; не обязан сам нажимать команды.
Секретарь (scribe) Тот, кто записывает хронологию: что и когда произошло.
Время обнаружения (TTD) Сколько прошло от начала сбоя до того, как о нём узнали.
Время восстановления (MTTR) Сколько в среднем проходит от начала сбоя до возврата к норме.
Смягчение (mitigation) Быстрое действие, возвращающее пользователям нормальную работу, даже без устранения причины.
Исправление (fix) Устранение слабого места, из-за которого сбой возможен; делается после смягчения.
Шторм повторов (retry storm) Лавина повторных запросов к уже больной зависимости, которая усиливает сбой.
Постмортем (postmortem) Документ-разбор после инцидента: влияние, хронология, причины, действия.
Без обвинений (blameless) Подход: описываем, как система допустила сбой, а не кто виноват.
Действие по итогам (action item) Конкретная задача из постмортема с владельцем и сроком.

Вопросы с собеседований

Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы тренируй на скорость: ответ за 30 секунд.

1. [junior] [часто] Что ты делаешь первым, когда пришёл алерт о медленном сервисе?

Ответ

Читаю алерт целиком: что превысило порог и как давно. Оцениваю масштаб по RED: сколько запросов, какая доля ошибок и p95, кто страдает, один маршрут или все. Сравниваю с тем, что было до: что изменилось первым. Если пользователи страдают заметно, сообщаю в канал инцидента, что я занимаюсь. Только потом углубляюсь в причину.

Что хотят услышать: сначала масштаб и влияние, потом причина; сообщение команде; сравнение с нормой.

Красный флаг: «сразу иду в код» или «перезапускаю сервис» без оценки влияния.

2. [junior] [часто] Чем смягчение отличается от исправления?

Ответ

Смягчение быстро возвращает пользователям нормальную работу: откат, отключение фичи, снижение нагрузки. Оно может быть грубым. Исправление убирает слабое место, из-за которого сбой возможен, и делается позже с тестами. Пример: вернуть оплату в нормальный режим это смягчение, а вынести вызов оплаты из транзакции это исправление.

Что хотят услышать: сначала смягчаем, потом исправляем; пример для каждого.

Красный флаг: «пока не найдём причину, ничего не трогаем».

3. [junior] [часто] Что такое постмортем и зачем он «без обвинений»?

Ответ

Постмортем это письменный разбор инцидента: влияние, хронология, причины, действия с владельцами и сроками. Без обвинений потому, что целью является исправить систему, а не наказать человека. Если за ошибки наказывают, люди прячут информацию, и разбор теряет главное. Хорошая формулировка описывает, какое условие в системе допустило сбой.

Что хотят услышать: действия по итогам, а не вывод «виноват X»; польза честности.

Красный флаг: «в постмортеме надо назвать виновного, чтобы он больше не ошибался».

4. [junior] Что значит «ошибки 503 на всех маршрутах, CPU сервиса низкий»?

Ответ

Сервис не занят вычислениями, он чего-то ждёт. Раз затронуты все маршруты, ждёт он общий ресурс: например, соединения пула БД, которые заняты медленной зависимостью. Проверю shop_db_pool_waiting и shop_db_pool_available, а затем длительность зависимостей.

Что хотят услышать: различение «занят» и «ждёт», идея общего ресурса, конкретные метрики.

Красный флаг: «нужно добавить CPU».

5. [junior] Что такое MTTR и из чего он складывается?

Ответ

Среднее время восстановления: от начала сбоя до возврата к норме. Складывается из времени обнаружения, реакции дежурного, диагностики и самого смягчения. Сократить его можно быстрыми алертами, понятными дашбордами, runbook (инструкциями для типовых сбоев) и заранее отработанным откатом.

Что хотят услышать: все четыре составляющие, примеры улучшений.

Красный флаг: «MTTR это время, за которое починили баг в коде».

6. [middle] Оплата стала отвечать за 2,5 с, и магазин упал, хотя оплата нужна только заказам. Почему и как исправить?

Ответ

Заказ вызывает оплату внутри транзакции и держит соединение пула, пока ждёт. Пять соединений быстро заняты, остальные запросы ждут очередь и по таймауту получают 503. Зависимость замедлилась, а пострадало всё, потому что общий ресурс не разделён. Исправление: вызывать оплату вне транзакции (статус заказа «ожидает оплаты», потом «оплачен»), поставить на оплату ограничение таймаута и числа повторов с паузой, отдельный пул или ограничение на число параллельных заказов, алерт на задержку оплаты.

Что хотят услышать: механизм (удержание соединения), общий ресурс, несколько мер, а не «увеличить пул».

Красный флаг: «увеличить пул до 100».

7. [middle] Как отличить медленную зависимость от нестабильной по метрикам?

Ответ

Медленная: растёт p95 длительности вызова (shop_payment_duration_seconds), результат ok, на нашей стороне растёт очередь пула и p95 всех запросов. Нестабильная: растёт доля error в shop_payment_requests_total, длительность попытки нормальная, наружу идут 502, а число попыток оплаты растёт быстрее числа заказов из-за ретраев.

Что хотят услышать: конкретные метрики и разные последствия.

Красный флаг: «в обоих случаях смотрю логи и угадываю».

8. [middle] Почему алерт с for: 5m не ловит быстрый сбой, и что с этим делать?

Ответ

Условие должно держаться пять минут подряд, значит обнаружение не быстрее пяти минут. Это защита от ложных срабатываний, но цена измеряется в страдающих пользователях. Решение: набор алертов разной скорости. Быстрые: на причину-насыщение (очередь пула) и на симптом (доля 5xx за две минуты), медленный на задержку. Плюс алерты на зависимости: p95 оплаты.

Что хотят услышать: компромисс «быстрота против ложных тревог», связка быстрых и медленных алертов.

Красный флаг: «поставить for: 0, чтобы будил сразу».

9. [middle] Как составить хронологию инцидента, если ты не вёл записей?

Ответ

Восстанавливаю по источникам: графики Grafana и сырые метрики Prometheus (когда начался рост), сохранённые уведомления и серия ALERTS в Prometheus (когда сработали), логи Loki (первые 5xx и их текст), история чата и команд оболочки. Время приводить к одному часовому поясу. Но честнее вести записи в ходе инцидента, поэтому у каждого инцидента есть секретарь.

Что хотят услышать: несколько независимых источников, один часовой пояс, признание, что восстановление хуже ведения.

Красный флаг: «по памяти».

10. [middle] Что бы ты сделал после инцидента, чтобы он не повторился?

Ответ

Разделил бы действия на три группы. Исправление слабого места (оплата вне транзакции, таймауты и пауза в ретраях). Наблюдение (алерт на задержку зависимости, панель в дашборде). Проверка (нагрузочный тест с замедленной оплатой как регрессионный сценарий). У каждого действия владелец и срок, и через месяц проверяю, что они сделаны.

Что хотят услышать: конкретные меры по трём направлениям, владельцы и сроки, проверка выполнения.

Красный флаг: «быть внимательнее» и «провести инструктаж».

11. [на скорость] Назови четыре роли в инциденте.

Ответ

Руководитель инцидента, оператор, связь, секретарь.

Что хотят услышать: все четыре и по одной фразе, чем занята каждая.

Красный флаг: «все делают всё».

12. [на скорость] Алерт pending и firing: в чём разница?

Ответ

pending условие выполняется, но срок for ещё не истёк. firing срок истёк, алерт отправлен в Alertmanager.

Что хотят услышать: упоминание for.

Красный флаг: «pending значит, что алерт отключён».

Проверено на версиях

Ubuntu 24.04, Locust 2.46, стенд «Магазин» из project/shop (Python 3.14, PostgreSQL 18.6, Redis 8.10), Prometheus 3.15, Grafana 13.2, Loki 3.7. Октябрь 2026. Числа в примерах вывода иллюстративные: у тебя они будут другими, важна картина.

Итог урока: ты умеешь

  • Назвать роли в инциденте и надеть их по очереди, когда ты один.
  • Вести хронологию с временем и посчитать время обнаружения и восстановления.
  • Пройти от алерта к причине: масштаб по RED, «занят или ждёт» по USE, подтверждение логами.
  • Отличить медленную зависимость от нестабильной по метрикам.
  • Сказать, чем смягчение отличается от исправления, и объяснить, почему увеличение пула не лечит.
  • Написать постмортем без поиска виноватых, с действиями, владельцами и сроками.

Дальше: урок 13.2. Тестовое задание за один день, где ты соберёшь всё пройденное в одну работу.

Проверь себя

Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.

Тест работает с включённым JavaScript.

тема 13 урок 13.1 3 ч курс 0/0 ← → уроки