{ "index": 4, "slug": "editorial-2027-11-field-mistakes-revisions", "title": "Incident runbook: как пройти от симптома к проверенному rollback", "excerpt": "Практический маршрут инцидента: зафиксировать симптом и границу влияния, проверить одну гипотезу, выполнить обратимое действие и доказать recovery по техническому и бизнес-сигналу.", "contentHtml": "
После выката payments-api начал отвечать 503 на POST /payments. Ошибки видны только в EU, а health-check продолжает быть зелёным. Первое решение — откатить последнюю версию, но одного статуса Deployment недостаточно: он может сообщить о завершённом rollout, пока платежи остаются в очереди.
Цена неточного диагноза — второй инцидент поверх первого. Если одновременно перезапустить pod, изменить лимит и откатить код, команда теряет причинную связь. Если старая версия не совместима с уже изменённой схемой или событиями, rollback вернёт бинарник, но добавит ошибки чтения, дубли или потерянные операции. Вопрос runbook такой: как перейти от наблюдаемого симптома к rollback, а затем подтвердить восстановление системы?
\nКод 503 сообщает о недоступности сервиса, а не о том, какой компонент виноват. Код 500 означает, что сервер столкнулся с неожиданным условием и не смог выполнить запрос; он также не раскрывает первопричину. Поэтому первой записью инцидента должен быть не вывод «сломался backend», а воспроизводимый факт: время, метод, endpoint, регион, текущая версия, доля ответов и baseline.
Сигналы нужно разделять по функции. Метрика — числовое измерение во времени, trace — путь отдельного запроса через систему, log — запись события. Они отвечают на разные вопросы и дополняют друг друга. Например, error rate показывает масштаб отказа, trace — где выросло ожидание, а log — какое условие привело к отказу. Не стоит заменять три источника одним общим графиком.
\nСимптом: 503 на POST /payments\nScope: EU, revision 18, 12:10–12:18 UTC\nBaseline: предыдущее сопоставимое окно\nГипотеза: revision 18 не проходит запрос к payment-provider\nПроверка: сравнить trace/span provider.call и ошибки revision 18/17\nОжидаемый признак: проблема сосредоточена в revision 18\nЧисла в этом фрагменте — учебные значения, а не отчёт о реальном сервисе. В настоящем инциденте baseline, окно и источник данных берутся из вашей системы наблюдаемости. Если baseline неизвестен, так и запишите: неизвестное нельзя превращать в порог только ради красивого отчёта.
\nRunbook ограничивает неопределённость не количеством команд, а границами. Перед rollback нужно ответить на пять вопросов.
\nrollout status, но и технический, пользовательский и, при необходимости, data-сигнал.Эта последовательность связывает состояние с владельцем. On-call фиксирует факт и координирует действие, владелец сервиса подтверждает совместимость версии, а владелец данных отвечает за незавершённые операции и recovery. В маленькой команде роли может выполнять один человек, но сами проверки не исчезают.
\n| Действие | Что оно возвращает | Цена и риск | Когда выбирать |
|---|---|---|---|
| Выключить feature flag | Поведение за границей flag | Не поможет, если ошибка в общем коде; старый путь должен оставаться рабочим | Проблема привязана к новому сценарию и flag уже предусмотрен |
| Уменьшить traffic split | Долю пользователей на новой версии | Оставляет часть риска и усложняет сравнение сигналов | Есть независимый старый и новый маршрут, а полное отключение не нужно |
| Откатить Deployment | Pod template и версию контейнера | Не отменяет миграцию базы, внешние записи и уже созданные события | Старая версия подтверждённо совместима с текущим состоянием |
| Перезапустить процесс | Текущее runtime-состояние процесса | Может временно скрыть симптом и уничтожить полезный контекст | Есть подтверждение утечки или зависшего процесса, но это не замена rollback |
Самый дешёвый по blast radius вариант не всегда самый быстрый по времени восстановления. Flag хорош, если проблема действительно находится за ним. Rollback Deployment понятнее, но его граница уже: он не возвращает схему базы и не стирает побочные эффекты. Перезапуск выбирают только под подтверждённую гипотезу, а не как универсальную кнопку.
\nПредставим, что payments-api работает в namespace payments, текущая ревизия — 18, а ревизия 17 прошла проверку совместимости. Это пример для Kubernetes; имена, namespace, таймаут и номер ревизии нужно заменить на значения своего кластера.
# 1. Сохраняем факт и смотрим, что именно было развернуто\nkubectl -n payments rollout history deployment/payments-api\nkubectl -n payments get deployment payments-api \\\n -o jsonpath='{.spec.template.spec.containers[*].image}{"\\n"}'\n\n# 2. Выполняем один обратимый шаг\nkubectl -n payments rollout undo deployment/payments-api --to-revision=17\n\n# 3. Ждём завершения именно этого rollout\nkubectl -n payments rollout status deployment/payments-api --timeout=5m\n\n# 4. Проверяем фактическое состояние и пользовательский путь\nkubectl -n payments get deployment payments-api\ncurl -sS -o /dev/null -w '%{http_code}\\n' \\\n https://payments.example.test/health/ready\nКоманда rollout undo возвращает Deployment к выбранной ревизии, а rollout status показывает состояние rollout. Это техническая проверка контроллера. Она не доказывает, что авторизованный платёж завершился, очередь уменьшается или данные согласованы. Для этого нужен отдельный безопасный контрольный запрос и сверка бизнес-сигнала. Если у endpoint нет публичного readiness URL, используйте внутренний проверочный путь с нужной авторизацией, но не подменяйте его произвольным 200 OK.
Проверку совместимости нельзя заменить номером ревизии. До команды ответьте: читает ли версия 17 текущую схему; понимает ли события, созданные версией 18; можно ли безопасно повторить неуспешную операцию; кто разберёт операции, принятые во время отката. Если хотя бы один ответ неизвестен, остановите rollback и поднимите владельца данных. Это не бюрократическая пауза: команда выбирает между ограниченным отказом и риском повредить состояние.
\nДаже маленькая проверка в коде помогает не превратить runbook в список команд. Функция ниже не выполняет откат и не объявляет recovery. Она только проверяет минимальные условия для учебного плана: есть наблюдаемый scope, известна целевая ревизия, записан путь возврата и подтверждена совместимость.
\nfunction rollbackGate({\n scope,\n currentRevision,\n targetRevision,\n schemaCompatible,\n eventsCompatible,\n rollbackPathReady,\n}) {\n const blockers = [];\n\n if (!scope) blockers.push('scope-missing');\n if (!Number.isInteger(currentRevision)) blockers.push('current-revision-missing');\n if (!Number.isInteger(targetRevision)) blockers.push('target-revision-missing');\n if (currentRevision === targetRevision) blockers.push('same-revision');\n if (schemaCompatible !== true) blockers.push('schema-compatibility-unconfirmed');\n if (eventsCompatible !== true) blockers.push('event-compatibility-unconfirmed');\n if (rollbackPathReady !== true) blockers.push('rollback-path-unprepared');\n\n return { allowed: blockers.length === 0, blockers };\n}\n\nconsole.log(rollbackGate({\n scope: 'EU / POST /payments / revision-18',\n currentRevision: 18,\n targetRevision: 17,\n schemaCompatible: true,\n eventsCompatible: true,\n rollbackPathReady: true,\n}));\n// { allowed: true, blockers: [] }\nГейт намеренно консервативен: undefined не считается подтверждением. В production вместо булевых значений должны быть ссылки на migration contract, описание события или результат отдельной проверки. Сам код не знает, разрешено ли действие политикой доступа, есть ли активная атака и выдержит ли система выбранное окно наблюдения.
| Симптом после действия | Что это может означать | Следующая проверка | Решение |
|---|---|---|---|
| 5xx снизился, но операции не завершаются | Доступность вернулась, а состояние платежа осталось незавершённым | Контрольная операция, очередь, дубли и записи в журнале | Инцидент не закрывать; запускать recovery по процедуре владельца данных |
| p95 снизился, очередь продолжает расти | HTTP-слой быстрее, consumer не успевает обрабатывать вход | Lag, rate обработки и ошибки consumer | Проверять consumer отдельно; не делать вывод об общем восстановлении |
| Ошибки остались только в EU | Локальная зависимость, конфигурация или маршрут региона | Сравнить trace, конфигурацию и версию по регионам | Ограничить scope действия, не откатывать глобально без доказательства |
| После rollback метрики колеблются | Окно наблюдения короче, чем цикл нагрузки или очередь ещё дренируется | Сопоставить окно с baseline и записать критерий остановки | Не считать quiet period доказательством; продолжить наблюдение по плану |
| Команда undo завершилась ошибкой | Нет нужной ревизии, конфликтует rollout или недоступен control plane | История Deployment, фактическая версия, audit log и права | Остановить новые изменения и перейти к заранее описанному ручному пути |
В последней строке используется закрытая ветка: если control plane не отвечает, повтор команды не становится диагностикой. Сначала фиксируем ошибку и фактическое состояние. Действия в обход штатного пути разрешены только политикой вашей платформы и с отдельной записью владельца.
\nКритерий «график стал зелёным» слишком слаб. Минимальный критерий recovery должен быть записан в терминах вашего сервиса: какой error rate допустим, в каком окне; какой latency считается приемлемой; должна ли очередь перестать расти или полностью опустеть; какая безопасная операция подтверждает пользовательский результат. Порог и длительность нельзя честно вывести из этого текста без данных о нагрузке и SLO.
\nЭтот runbook описывает управляемый релизный инцидент, а не все виды аварий. При подозрении на компрометацию, утечку данных, повреждение базы или нарушение регуляторных требований действуют security, legal и data-recovery процедуры. NIST SP 800-61r3 задаёт общий язык и модель Detect, Respond, Recover для киберинцидентов, но не выдаёт пороги, команды Kubernetes или разрешение на конкретный rollback.
\nKubernetes-пример зависит от наличия истории ревизий и прав доступа; политика хранения истории может сделать старую ревизию недоступной. Команда откатывает шаблон Deployment, но не является миграцией базы и не отменяет внешние побочные эффекты. В системах без versioned rollout нужен другой адаптер: например, атомарный переключатель трафика или заранее подготовленный конфигурационный rollback.
\nНаконец, этот текст не доказывает, что ваш сервис восстановится. Перед production добавьте тестовый прогон с контролируемым отказом, проверьте negative path и назначьте владельца каждого сигнала. Если прогон показывает, что команду можно выполнить, но recovery-сигнал получить нельзя, runbook ещё не готов.
\nrollout undo и проверки rollout. Команды нужно сверить с версией и политикой своего кластера.