{ "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, пока платежи остаются в очереди.

\n

Цена неточного диагноза — второй инцидент поверх первого. Если одновременно перезапустить pod, изменить лимит и откатить код, команда теряет причинную связь. Если старая версия не совместима с уже изменённой схемой или событиями, rollback вернёт бинарник, но добавит ошибки чтения, дубли или потерянные операции. Вопрос runbook такой: как перейти от наблюдаемого симптома к rollback, а затем подтвердить восстановление системы?

\n

Сначала факт, потом причина

\n

Код 503 сообщает о недоступности сервиса, а не о том, какой компонент виноват. Код 500 означает, что сервер столкнулся с неожиданным условием и не смог выполнить запрос; он также не раскрывает первопричину. Поэтому первой записью инцидента должен быть не вывод «сломался backend», а воспроизводимый факт: время, метод, endpoint, регион, текущая версия, доля ответов и baseline.

\n

Сигналы нужно разделять по функции. Метрика — числовое измерение во времени, 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 неизвестен, так и запишите: неизвестное нельзя превращать в порог только ради красивого отчёта.

\n

Механизм: пять границ одного действия

\n

Runbook ограничивает неопределённость не количеством команд, а границами. Перед rollback нужно ответить на пять вопросов.

\n
  1. Что наблюдаем? Один симптом с единицей измерения и временным окном.
  2. Где действует проблема? Endpoint, регион, версия, доля трафика и зависимость.
  3. Какую гипотезу проверяем? Одно изменение должно иметь один ожидаемый сигнал.
  4. Что именно возвращаем? Flag, traffic split, конфигурацию или версию; это не взаимозаменяемые операции.
  5. Что считаем recovery? Не только rollout status, но и технический, пользовательский и, при необходимости, data-сигнал.
\n

Эта последовательность связывает состояние с владельцем. On-call фиксирует факт и координирует действие, владелец сервиса подтверждает совместимость версии, а владелец данных отвечает за незавершённые операции и recovery. В маленькой команде роли может выполнять один человек, но сами проверки не исчезают.

\n
Схема incident runbook: симптом и область влияния переходят в одну гипотезу, обратимое действие и проверку метрикой и бизнес-сигналом
Цикл должен замыкаться проверкой: после действия смотрим не на факт выполнения команды, а на восстановление нужного результата.
\n

Как выбрать обратимое действие

\n
Варианты первого действия при инциденте
ДействиеЧто оно возвращаетЦена и рискКогда выбирать
Выключить feature flagПоведение за границей flagНе поможет, если ошибка в общем коде; старый путь должен оставаться рабочимПроблема привязана к новому сценарию и flag уже предусмотрен
Уменьшить traffic splitДолю пользователей на новой версииОставляет часть риска и усложняет сравнение сигналовЕсть независимый старый и новый маршрут, а полное отключение не нужно
Откатить DeploymentPod template и версию контейнераНе отменяет миграцию базы, внешние записи и уже созданные событияСтарая версия подтверждённо совместима с текущим состоянием
Перезапустить процессТекущее runtime-состояние процессаМожет временно скрыть симптом и уничтожить полезный контекстЕсть подтверждение утечки или зависшего процесса, но это не замена rollback
\n

Самый дешёвый по blast radius вариант не всегда самый быстрый по времени восстановления. Flag хорош, если проблема действительно находится за ним. Rollback Deployment понятнее, но его граница уже: он не возвращает схему базы и не стирает побочные эффекты. Перезапуск выбирают только под подтверждённую гипотезу, а не как универсальную кнопку.

\n

Учебный пример: rollback Deployment

\n

Представим, что payments-api работает в namespace payments, текущая ревизия — 18, а ревизия 17 прошла проверку совместимости. Это пример для Kubernetes; имена, namespace, таймаут и номер ревизии нужно заменить на значения своего кластера.

\n
# 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.

\n

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

\n

Гейт перед rollback

\n

Даже маленькая проверка в коде помогает не превратить runbook в список команд. Функция ниже не выполняет откат и не объявляет recovery. Она только проверяет минимальные условия для учебного плана: есть наблюдаемый scope, известна целевая ревизия, записан путь возврата и подтверждена совместимость.

\n
function 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, описание события или результат отдельной проверки. Сам код не знает, разрешено ли действие политикой доступа, есть ли активная атака и выдержит ли система выбранное окно наблюдения.

\n

Матрица симптомов и следующих проверок

\n
Как не перепутать улучшение графика с recovery
Симптом после действияЧто это может означатьСледующая проверкаРешение
5xx снизился, но операции не завершаютсяДоступность вернулась, а состояние платежа осталось незавершённымКонтрольная операция, очередь, дубли и записи в журналеИнцидент не закрывать; запускать recovery по процедуре владельца данных
p95 снизился, очередь продолжает растиHTTP-слой быстрее, consumer не успевает обрабатывать входLag, rate обработки и ошибки consumerПроверять consumer отдельно; не делать вывод об общем восстановлении
Ошибки остались только в EUЛокальная зависимость, конфигурация или маршрут регионаСравнить trace, конфигурацию и версию по регионамОграничить scope действия, не откатывать глобально без доказательства
После rollback метрики колеблютсяОкно наблюдения короче, чем цикл нагрузки или очередь ещё дренируетсяСопоставить окно с baseline и записать критерий остановкиНе считать quiet period доказательством; продолжить наблюдение по плану
Команда undo завершилась ошибкойНет нужной ревизии, конфликтует rollout или недоступен control planeИстория Deployment, фактическая версия, audit log и праваОстановить новые изменения и перейти к заранее описанному ручному пути
\n

В последней строке используется закрытая ветка: если control plane не отвечает, повтор команды не становится диагностикой. Сначала фиксируем ошибку и фактическое состояние. Действия в обход штатного пути разрешены только политикой вашей платформы и с отдельной записью владельца.

\n

Runbook от симптома до закрытия

\n
  1. Зафиксировать. Записать timestamp, endpoint, регион, версию, метрику, baseline, trace ID или ссылку на конкретный запрос. Не менять систему на этом шаге.
  2. Ограничить scope. Сравнить затронутые регионы, методы, версии, долю трафика и зависимости. Отделить пользовательскую ошибку от отказа сервера.
  3. Сформулировать гипотезу. Назвать один предполагаемый механизм и один сигнал, который его подтвердит или опровергнет.
  4. Выбрать рычаг. Сравнить flag, traffic split и rollback по blast radius, обратимости и совместимости. Зафиксировать владельца команды.
  5. Проверить гейт. Убедиться, что target version, schema, events, права и путь возврата известны. Если неизвестно — остановить опасную ветку.
  6. Выполнить одно действие. Сохранить команду, время начала, ожидаемый эффект и фактический результат. Не добавлять параллельный restart без новой гипотезы.
  7. Проверить rollout. Посмотреть фактическую версию и состояние контроллера. Успешное выполнение команды — промежуточный результат.
  8. Проверить recovery. Сверить error rate, latency, saturation, очередь и контрольную бизнес-операцию в заранее выбранном окне. Для денежных или иных важных операций добавить проверку согласованности данных.
  9. Закрыть или эскалировать. Закрывать инцидент только при выполнении всех критериев. Иначе сохранить отрицательный результат, остановить новые изменения и передать владельцу следующую ветку.
\n

Критерий «график стал зелёным» слишком слаб. Минимальный критерий recovery должен быть записан в терминах вашего сервиса: какой error rate допустим, в каком окне; какой latency считается приемлемой; должна ли очередь перестать расти или полностью опустеть; какая безопасная операция подтверждает пользовательский результат. Порог и длительность нельзя честно вывести из этого текста без данных о нагрузке и SLO.

\n

Ограничения

\n

Этот runbook описывает управляемый релизный инцидент, а не все виды аварий. При подозрении на компрометацию, утечку данных, повреждение базы или нарушение регуляторных требований действуют security, legal и data-recovery процедуры. NIST SP 800-61r3 задаёт общий язык и модель Detect, Respond, Recover для киберинцидентов, но не выдаёт пороги, команды Kubernetes или разрешение на конкретный rollback.

\n

Kubernetes-пример зависит от наличия истории ревизий и прав доступа; политика хранения истории может сделать старую ревизию недоступной. Команда откатывает шаблон Deployment, но не является миграцией базы и не отменяет внешние побочные эффекты. В системах без versioned rollout нужен другой адаптер: например, атомарный переключатель трафика или заранее подготовленный конфигурационный rollback.

\n

Наконец, этот текст не доказывает, что ваш сервис восстановится. Перед production добавьте тестовый прогон с контролируемым отказом, проверьте negative path и назначьте владельца каждого сигнала. Если прогон показывает, что команду можно выполнить, но recovery-сигнал получить нельзя, runbook ещё не готов.

\n

Проверяемые источники

\n" }