8 lines
24 KiB
JSON
8 lines
24 KiB
JSON
{
|
||
"index": 4,
|
||
"slug": "editorial-2027-11-field-mistakes-revisions",
|
||
"title": "Incident runbook: как пройти от симптома к проверенному rollback",
|
||
"excerpt": "Практический маршрут инцидента: зафиксировать симптом и границу влияния, проверить одну гипотезу, выполнить обратимое действие и доказать recovery по техническому и бизнес-сигналу.",
|
||
"contentHtml": "<p>После выката <code>payments-api</code> начал отвечать <code>503</code> на <code>POST /payments</code>. Ошибки видны только в EU, а health-check продолжает быть зелёным. Первое решение — откатить последнюю версию, но одного статуса Deployment недостаточно: он может сообщить о завершённом rollout, пока платежи остаются в очереди.</p>\n<p>Цена неточного диагноза — второй инцидент поверх первого. Если одновременно перезапустить pod, изменить лимит и откатить код, команда теряет причинную связь. Если старая версия не совместима с уже изменённой схемой или событиями, rollback вернёт бинарник, но добавит ошибки чтения, дубли или потерянные операции. Вопрос runbook такой: как перейти от наблюдаемого симптома к rollback, а затем подтвердить восстановление системы?</p>\n<h2>Сначала факт, потом причина</h2>\n<p>Код <code>503</code> сообщает о недоступности сервиса, а не о том, какой компонент виноват. Код <code>500</code> означает, что сервер столкнулся с неожиданным условием и не смог выполнить запрос; он также не раскрывает первопричину. Поэтому первой записью инцидента должен быть не вывод «сломался backend», а воспроизводимый факт: время, метод, endpoint, регион, текущая версия, доля ответов и baseline.</p>\n<p>Сигналы нужно разделять по функции. Метрика — числовое измерение во времени, trace — путь отдельного запроса через систему, log — запись события. Они отвечают на разные вопросы и дополняют друг друга. Например, error rate показывает масштаб отказа, trace — где выросло ожидание, а log — какое условие привело к отказу. Не стоит заменять три источника одним общим графиком.</p>\n<pre><code>Симптом: 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</code></pre>\n<p>Числа в этом фрагменте — учебные значения, а не отчёт о реальном сервисе. В настоящем инциденте baseline, окно и источник данных берутся из вашей системы наблюдаемости. Если baseline неизвестен, так и запишите: неизвестное нельзя превращать в порог только ради красивого отчёта.</p>\n<h2>Механизм: пять границ одного действия</h2>\n<p>Runbook ограничивает неопределённость не количеством команд, а границами. Перед rollback нужно ответить на пять вопросов.</p>\n<ol><li><strong>Что наблюдаем?</strong> Один симптом с единицей измерения и временным окном.</li><li><strong>Где действует проблема?</strong> Endpoint, регион, версия, доля трафика и зависимость.</li><li><strong>Какую гипотезу проверяем?</strong> Одно изменение должно иметь один ожидаемый сигнал.</li><li><strong>Что именно возвращаем?</strong> Flag, traffic split, конфигурацию или версию; это не взаимозаменяемые операции.</li><li><strong>Что считаем recovery?</strong> Не только <code>rollout status</code>, но и технический, пользовательский и, при необходимости, data-сигнал.</li></ol>\n<p>Эта последовательность связывает состояние с владельцем. On-call фиксирует факт и координирует действие, владелец сервиса подтверждает совместимость версии, а владелец данных отвечает за незавершённые операции и recovery. В маленькой команде роли может выполнять один человек, но сами проверки не исчезают.</p>\n<figure><img src='/assets/editorial/2027/mistakes-revisions-2027-evidence-handoff-loop.svg' alt='Схема incident runbook: симптом и область влияния переходят в одну гипотезу, обратимое действие и проверку метрикой и бизнес-сигналом' loading='lazy' /><figcaption>Цикл должен замыкаться проверкой: после действия смотрим не на факт выполнения команды, а на восстановление нужного результата.</figcaption></figure>\n<h2>Как выбрать обратимое действие</h2>\n<div class='table-scroll'><table><caption>Варианты первого действия при инциденте</caption><thead><tr><th scope='col'>Действие</th><th scope='col'>Что оно возвращает</th><th scope='col'>Цена и риск</th><th scope='col'>Когда выбирать</th></tr></thead><tbody><tr><td>Выключить feature flag</td><td>Поведение за границей flag</td><td>Не поможет, если ошибка в общем коде; старый путь должен оставаться рабочим</td><td>Проблема привязана к новому сценарию и flag уже предусмотрен</td></tr><tr><td>Уменьшить traffic split</td><td>Долю пользователей на новой версии</td><td>Оставляет часть риска и усложняет сравнение сигналов</td><td>Есть независимый старый и новый маршрут, а полное отключение не нужно</td></tr><tr><td>Откатить Deployment</td><td>Pod template и версию контейнера</td><td>Не отменяет миграцию базы, внешние записи и уже созданные события</td><td>Старая версия подтверждённо совместима с текущим состоянием</td></tr><tr><td>Перезапустить процесс</td><td>Текущее runtime-состояние процесса</td><td>Может временно скрыть симптом и уничтожить полезный контекст</td><td>Есть подтверждение утечки или зависшего процесса, но это не замена rollback</td></tr></tbody></table></div>\n<p>Самый дешёвый по blast radius вариант не всегда самый быстрый по времени восстановления. Flag хорош, если проблема действительно находится за ним. Rollback Deployment понятнее, но его граница уже: он не возвращает схему базы и не стирает побочные эффекты. Перезапуск выбирают только под подтверждённую гипотезу, а не как универсальную кнопку.</p>\n<h2>Учебный пример: rollback Deployment</h2>\n<p>Представим, что <code>payments-api</code> работает в namespace <code>payments</code>, текущая ревизия — <code>18</code>, а ревизия <code>17</code> прошла проверку совместимости. Это пример для Kubernetes; имена, namespace, таймаут и номер ревизии нужно заменить на значения своего кластера.</p>\n<pre><code># 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</code></pre>\n<p>Команда <code>rollout undo</code> возвращает Deployment к выбранной ревизии, а <code>rollout status</code> показывает состояние rollout. Это техническая проверка контроллера. Она не доказывает, что авторизованный платёж завершился, очередь уменьшается или данные согласованы. Для этого нужен отдельный безопасный контрольный запрос и сверка бизнес-сигнала. Если у endpoint нет публичного readiness URL, используйте внутренний проверочный путь с нужной авторизацией, но не подменяйте его произвольным <code>200 OK</code>.</p>\n<p>Проверку совместимости нельзя заменить номером ревизии. До команды ответьте: читает ли версия 17 текущую схему; понимает ли события, созданные версией 18; можно ли безопасно повторить неуспешную операцию; кто разберёт операции, принятые во время отката. Если хотя бы один ответ неизвестен, остановите rollback и поднимите владельца данных. Это не бюрократическая пауза: команда выбирает между ограниченным отказом и риском повредить состояние.</p>\n<h2>Гейт перед rollback</h2>\n<p>Даже маленькая проверка в коде помогает не превратить runbook в список команд. Функция ниже не выполняет откат и не объявляет recovery. Она только проверяет минимальные условия для учебного плана: есть наблюдаемый scope, известна целевая ревизия, записан путь возврата и подтверждена совместимость.</p>\n<pre><code>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: [] }</code></pre>\n<p>Гейт намеренно консервативен: <code>undefined</code> не считается подтверждением. В production вместо булевых значений должны быть ссылки на migration contract, описание события или результат отдельной проверки. Сам код не знает, разрешено ли действие политикой доступа, есть ли активная атака и выдержит ли система выбранное окно наблюдения.</p>\n<h2>Матрица симптомов и следующих проверок</h2>\n<div class='table-scroll'><table><caption>Как не перепутать улучшение графика с recovery</caption><thead><tr><th scope='col'>Симптом после действия</th><th scope='col'>Что это может означать</th><th scope='col'>Следующая проверка</th><th scope='col'>Решение</th></tr></thead><tbody><tr><td>5xx снизился, но операции не завершаются</td><td>Доступность вернулась, а состояние платежа осталось незавершённым</td><td>Контрольная операция, очередь, дубли и записи в журнале</td><td>Инцидент не закрывать; запускать recovery по процедуре владельца данных</td></tr><tr><td>p95 снизился, очередь продолжает расти</td><td>HTTP-слой быстрее, consumer не успевает обрабатывать вход</td><td>Lag, rate обработки и ошибки consumer</td><td>Проверять consumer отдельно; не делать вывод об общем восстановлении</td></tr><tr><td>Ошибки остались только в EU</td><td>Локальная зависимость, конфигурация или маршрут региона</td><td>Сравнить trace, конфигурацию и версию по регионам</td><td>Ограничить scope действия, не откатывать глобально без доказательства</td></tr><tr><td>После rollback метрики колеблются</td><td>Окно наблюдения короче, чем цикл нагрузки или очередь ещё дренируется</td><td>Сопоставить окно с baseline и записать критерий остановки</td><td>Не считать quiet period доказательством; продолжить наблюдение по плану</td></tr><tr><td>Команда undo завершилась ошибкой</td><td>Нет нужной ревизии, конфликтует rollout или недоступен control plane</td><td>История Deployment, фактическая версия, audit log и права</td><td>Остановить новые изменения и перейти к заранее описанному ручному пути</td></tr></tbody></table></div>\n<p>В последней строке используется закрытая ветка: если control plane не отвечает, повтор команды не становится диагностикой. Сначала фиксируем ошибку и фактическое состояние. Действия в обход штатного пути разрешены только политикой вашей платформы и с отдельной записью владельца.</p>\n<h2>Runbook от симптома до закрытия</h2>\n<ol><li><strong>Зафиксировать.</strong> Записать timestamp, endpoint, регион, версию, метрику, baseline, trace ID или ссылку на конкретный запрос. Не менять систему на этом шаге.</li><li><strong>Ограничить scope.</strong> Сравнить затронутые регионы, методы, версии, долю трафика и зависимости. Отделить пользовательскую ошибку от отказа сервера.</li><li><strong>Сформулировать гипотезу.</strong> Назвать один предполагаемый механизм и один сигнал, который его подтвердит или опровергнет.</li><li><strong>Выбрать рычаг.</strong> Сравнить flag, traffic split и rollback по blast radius, обратимости и совместимости. Зафиксировать владельца команды.</li><li><strong>Проверить гейт.</strong> Убедиться, что target version, schema, events, права и путь возврата известны. Если неизвестно — остановить опасную ветку.</li><li><strong>Выполнить одно действие.</strong> Сохранить команду, время начала, ожидаемый эффект и фактический результат. Не добавлять параллельный restart без новой гипотезы.</li><li><strong>Проверить rollout.</strong> Посмотреть фактическую версию и состояние контроллера. Успешное выполнение команды — промежуточный результат.</li><li><strong>Проверить recovery.</strong> Сверить error rate, latency, saturation, очередь и контрольную бизнес-операцию в заранее выбранном окне. Для денежных или иных важных операций добавить проверку согласованности данных.</li><li><strong>Закрыть или эскалировать.</strong> Закрывать инцидент только при выполнении всех критериев. Иначе сохранить отрицательный результат, остановить новые изменения и передать владельцу следующую ветку.</li></ol>\n<p>Критерий «график стал зелёным» слишком слаб. Минимальный критерий recovery должен быть записан в терминах вашего сервиса: какой error rate допустим, в каком окне; какой latency считается приемлемой; должна ли очередь перестать расти или полностью опустеть; какая безопасная операция подтверждает пользовательский результат. Порог и длительность нельзя честно вывести из этого текста без данных о нагрузке и SLO.</p>\n<h2>Ограничения</h2>\n<p>Этот runbook описывает управляемый релизный инцидент, а не все виды аварий. При подозрении на компрометацию, утечку данных, повреждение базы или нарушение регуляторных требований действуют security, legal и data-recovery процедуры. NIST SP 800-61r3 задаёт общий язык и модель Detect, Respond, Recover для киберинцидентов, но не выдаёт пороги, команды Kubernetes или разрешение на конкретный rollback.</p>\n<p>Kubernetes-пример зависит от наличия истории ревизий и прав доступа; политика хранения истории может сделать старую ревизию недоступной. Команда откатывает шаблон Deployment, но не является миграцией базы и не отменяет внешние побочные эффекты. В системах без versioned rollout нужен другой адаптер: например, атомарный переключатель трафика или заранее подготовленный конфигурационный rollback.</p>\n<p>Наконец, этот текст не доказывает, что ваш сервис восстановится. Перед production добавьте тестовый прогон с контролируемым отказом, проверьте negative path и назначьте владельца каждого сигнала. Если прогон показывает, что команду можно выполнить, но recovery-сигнал получить нельзя, runbook ещё не готов.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://doi.org/10.6028/NIST.SP.800-61r3' target='_blank' rel='noopener noreferrer'>NIST SP 800-61r3, Incident Response Recommendations and Considerations for Cybersecurity Risk Management</a> — финальная редакция апреля 2025 года, заменяет Rev. 2 и описывает роли, Detect, Respond, Recover и continuous improvement. Это руководство не является инструкцией для конкретного кластера.</li><li><a href='https://www.rfc-editor.org/rfc/rfc9110.html' target='_blank' rel='noopener noreferrer'>RFC 9110, HTTP Semantics</a> — нормативное описание классов HTTP-статусов и смысла 500/5xx. Статус не доказывает компонент-причину.</li><li><a href='https://opentelemetry.io/docs/concepts/signals/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Signals</a> — определения metrics, traces и logs как разных сигналов наблюдаемости. Названия атрибутов и способ сбора зависят от конкретного backend.</li><li><a href='https://kubernetes.io/docs/concepts/workloads/controllers/deployment/' target='_blank' rel='noopener noreferrer'>Kubernetes: Deployments</a> — поведение revision history, <code>rollout undo</code> и проверки rollout. Команды нужно сверить с версией и политикой своего кластера.</li></ul>"
|
||
}
|