Files
progcode/editorial/agent-rewrites/004.json
T

8 lines
24 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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}{&#34;\\n&#34;}'\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>"
}