{"index":150,"slug":"editorial-2023-11-practice-postmortem","title":"Postmortem, который помогает исправить систему","excerpt":"Практическая схема разбора сбоя: отделяем наблюдение от гипотезы, сохраняем контекст решения и превращаем профилактику в проверяемое действие.","contentHtml":"

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

\n

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

\n

Ниже — практический маршрут для небольшого HTTP-инцидента. Он не выдаёт учебный сценарий за production-расследование. Его задача — помочь записать факты, связать действие с доступной информацией и назначить следующую проверку так, чтобы другой инженер мог воспроизвести её без устного пересказа.

\n

Начните с симптома и цены ошибки

\n

Первая запись должна описывать наблюдаемое событие, а не объяснение. Назовите путь или компонент, окно времени, сигнал и последствия для пользователя. «С 10:03 до 10:08 шлюз вернул 502 на 18% запросов к /checkout» уже можно проверять по метрике. «Новый таймаут сломал оплату» — пока гипотеза.

\n

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

\n
Четыре записи, которые нельзя смешивать
ЗаписьНа какой вопрос отвечаетМинимальные поляГраница вывода
СимптомЧто увидел пользователь или мониторинг?путь, окно, сигнал, воздействиене объясняет причину
ФактЧто подтверждено источником?timestamp, наблюдение, ссылка на лог, метрику или конфигурациюне назначает виновника
РешениеЧто сделали с доступной информацией?время, действие, список доступных fact ID, обратимостьне доказывает, что решение устранило причину
ПроверкаКак отделить гипотезу от альтернативы?вход, метод, критерий PASS/FAIL, остановка, владелецне обещает результат до прогона
\n

Сохраните временную границу

\n

В postmortem полезно различать две временные шкалы. Первая — события системы: ошибка, изменение конфигурации, запрос к upstream, откат. Вторая — знания команды: что было видно до решения и что выяснилось уже после. Поздний trace может объяснить механизм, но не был основанием для решения, принятого пять минут раньше.

\n
\"Учебная
Учебная схема временной границы: решение связано только с фактами, доступными до него; эксперимент проверяет гипотезу после временного восстановления.
\n

Для каждого события храните источник и время, а для решения — список фактов, которыми дежурный действительно располагал. Это защищает от hindsight bias, то есть от подмены прежней неопределённости знанием, появившимся позже. Так команда оценивает не личную «внимательность», а качество сигналов и доступных процедур.

\n

Разберите учебный HTTP-инцидент

\n

Возьмём изолированный пример. В 10:03 шлюз заметил рост 502 на /checkout. В 10:04 команда увидела, что сервис использует версию конфигурации 17 с новым таймаутом. В 10:05 дежурный вернул версию 16. В 10:08 доля 502 снизилась. Последнее наблюдение подтверждает эффект отката во времени, но не доказывает, что именно таймаут был единственной причиной: мог закончиться всплеск нагрузки, восстановиться upstream или исчезнуть другая ошибка маршрутизации.

\n

Следующий фрагмент можно запустить локально: ему нужен только Node.js. Он проверяет узкое правило — решение не может ссылаться на факт, появившийся позже. Команда не обращается к логам и не меняет конфигурацию, поэтому результат запуска не является доказательством состояния настоящего сервиса.

\n
node <<'NODE'\nconst facts = [\n  { id: 'f-1', at: '2023-11-07T10:03:00Z', text: 'gateway returned 502 for /checkout', source: 'metric://gateway-5xx' },\n  { id: 'f-2', at: '2023-11-07T10:04:00Z', text: 'checkout config is version 17', source: 'config://checkout/17' },\n  { id: 'f-3', at: '2023-11-07T10:08:00Z', text: '502 share fell after rollback', source: 'metric://gateway-5xx' },\n];\nconst decision = {\n  at: '2023-11-07T10:05:00Z',\n  action: 'restore checkout config to version 16',\n  availableFactIds: ['f-1', 'f-2'],\n};\n\nconst byId = new Map(facts.map((fact) => [fact.id, fact]));\nfor (const factId of decision.availableFactIds) {\n  const fact = byId.get(factId);\n  if (!fact || new Date(fact.at) >= new Date(decision.at)) {\n    throw new Error('Fact ' + factId + ' was not available at decision time');\n  }\n}\nconsole.log('PASS: decision uses only earlier facts');\nconsole.log(JSON.stringify({ facts, decision }, null, 2));\nNODE
\n

В shell этот блок завершится строкой PASS и напечатает запись. Если заменить список availableFactIds на ['f-1', 'f-3'], проверка завершится ошибкой: факт о снижении ошибок возник после решения. Это и есть полезный отрицательный тест. Он не доказывает root cause, зато не позволяет задним числом приписать дежурному знание, которого у него не было.

\n

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

\n

Не принимайте откат за доказанную причину

\n

Откат — это mitigation: действие, которое уменьшает воздействие прямо сейчас. Причина — объяснение механизма, подтверждённое несколькими наблюдениями или безопасным экспериментом. Между ними может быть связь, но её нельзя объявлять установленной только потому, что симптом ослаб после отката.

\n
Маршрут от наблюдения к следующему действию
НаблюдениеЧто пока нельзя утверждатьПроверкаДействие и критерий
502 вырос после релизарелиз — единственная причинасопоставить версии, трафик, upstream-коды и соседние изменения в одном окнесохранить откат как mitigation; PASS — все сравниваемые источники согласованы
ошибки снизились после возврата конфигурацииновый таймаут доказан как root causeповторить на тестовом трафике тот же класс запросов с версиями 16 и 17остановить эксперимент при росте 5xx; PASS — записаны ответ шлюза и статус upstream для каждой попытки
в логе есть имя инженерачеловек объясняет техническую причинупроверить, какой контроль разрешил изменение и какая информация была доступназаменить имя в причинной цепочке на изменение, контроль и владельца follow-up
в отчёте написано «добавить мониторинг»новый сигнал предотвратит повторназвать маршрут, окно, порог, канал и тест тревогиPASS — тестовый сигнал срабатывает на искусственном нарушении; FAIL — открыть действие с rollback
\n

Гипотеза должна быть узкой: «при одинаковом классе запроса версия 17 увеличивает долю 502 из-за таймаута ожидания upstream». Она лучше общего «конфигурация ненадёжна», потому что задаёт входы и наблюдения. Альтернатива тоже нужна: например, «502 вызван исчерпанием соединений независимо от таймаута». Иначе эксперимент будет подтверждать любимое объяснение, а не различать варианты.

\n

Сделайте action item проверяемым

\n

Строка «разобраться с таймаутами» не является планом. Хорошая corrective action меняет контроль или код и имеет наблюдаемый критерий. В ней должны быть один владелец, срок, ссылка на место изменения, способ проверки и обратное действие. Если результат нельзя выразить через PASS/FAIL, сначала уточните, какой артефакт должен появиться.

\n
Action: add a bounded upstream-timeout regression test\nOwner: checkout-service team\nInput: the recorded /checkout request class from incident INC-482\nPASS: versions 16 and 17 produce captured gateway and upstream statuses;\n      the test fails when the timeout exceeds the contract.\nRollback: keep version 16 until the test and staged replay are green.\nEvidence: test run URL + configuration revision + timestamp.
\n

Пример описывает будущую проверку, а не уже достигнутый результат. До запуска нельзя писать «тест защитил пользователей». После запуска сохраните фактический статус, версию теста и условия прогона. Если staged replay не повторяет production-нагрузку или upstream недоступен, результат ограничен именно этой средой.

\n

Выберите момент для postmortem

\n

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

\n

До инцидента зафиксируйте, кто открывает документ, где лежит timeline, кто делает технический review и что считается закрытым follow-up. После инцидента сначала сохраните артефакты с нужным сроком хранения, затем попросите review у людей, которые могут проверить полноту воздействия, глубину причины и реалистичность действий. Не закрывайте запись лишь потому, что пользовательский симптом исчез.

\n

Ограничения и границы применимости

\n

Blameless не означает отсутствие ответственности. Такой подход убирает обвинение человека из причинной цепочки, но оставляет владельца действия, срок, контроль и критерий завершения. Если изменение нарушило правило доступа или процесс, документируйте сам факт нарушения и слабость контроля; не приписывайте мотив, не публикуйте лишние персональные данные и не превращайте имя в техническое объяснение.

\n

В распределённой системе один trace не равен полной истории. Сообщение могло потеряться до входа в сервис, часы на узлах могли расходиться, а метрика шлюза могла не учитывать ошибки до него. Откат может убрать симптом и оставить повреждённые данные. Поэтому для операций записи отдельно проверяйте целостность, повторяемость и идемпотентность; для приватных инцидентов — доступ, retention и допустимый объём публикации.

\n

Учебный Node.js-код проверяет только порядок двух timestamp. Он не подключается к HTTP, не читает production-метрики, не выполняет откат и не устанавливает причинность. Приведённые имена маршрута, версии и проценты вымышлены для воспроизводимости. В своём проекте замените их реальными источниками и укажите версию среды: синтетический прогон на Node.js не подтверждает поведение конкретного шлюза, базы данных или провайдера.

\n

Проверьте документ перед разбором

\n
  1. Назовите симптом. Запишите путь, окно, единицы измерения и воздействие. Отделите неизвестное от нулевого значения.
  2. Соберите факты. Привяжите каждое наблюдение к источнику, timestamp и сроку хранения. Исправьте часовые пояса.
  3. Восстановите решение. Укажите только те fact ID, которые существовали до действия, и опишите его обратимость.
  4. Разделите временное и постоянное. Отметьте откат или переключение как mitigation, а root cause оставьте гипотезой, пока её не подтверждает проверка.
  5. Оформите follow-up. Добавьте узкую гипотезу, вход, метод, PASS/FAIL, условие остановки, владельца и evidence.
  6. Проведите review. Попросите другого инженера пройти timeline и выполнить локальный пример с отрицательной веткой.
  7. Закройте действие по факту. Сохраните результат прогона, ссылку на изменение и остаточный риск. Если критерий не выполнен, это FAIL, а не «почти готово».
\n

Готовность postmortem — это не обещание, что инцидент больше не повторится. Более узкий и честный критерий таков: инженер, не участвовавший в сбое, видит, что произошло, что было известно в момент решения, какое объяснение ещё проверяется и какое действие даст следующий измеримый сигнал.

\n

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

\n"}