{ "index": 255, "slug": "editorial-2020-12-practice-incident-review", "title": "Разбор инцидента: как отделить факт от поспешного исправления", "excerpt": "Сервис отвечает ошибкой, команда меняет timeout, а причина остаётся неизвестной. Разбираем учебный случай через наблюдение, гипотезу, ограниченное действие, проверку и профилактику.", "contentHtml": "

Сервис предварительного расчёта заказа начал возвращать blocked вместо цены. Ошибка видна пользователю, но её граница не видна инженеру: поле могло исчезнуть во входе, mapper-е или контракте downstream-сервиса. Цена поспешной правки — не только несколько минут простоя. Новый retry может увеличить нагрузку, timeout может спрятать отказ, а возврат старой версии может оставить причину без защиты. Через месяц тот же дефект вернётся под другим сообщением.

\n

Тезис: разбор начинается с сохранения наблюдаемого факта. Затем команда формулирует одну проверяемую гипотезу, выбирает ограниченное действие и заранее называет проверку. Временный обход не становится причиной, а успешный ответ не доказывает готовность системы. Такая последовательность нужна и маленькому модулю, и аварии, в которой участвуют несколько команд.

\n

Сначала зафиксировать симптом

\n

Симптом отвечает на вопрос «что заметил пользователь или мониторинг?». Запишите маршрут, состояние, окно времени и цену повторения. Например: POST /preview-order возвращает blocked для заказа с валютой RUB; расчёт не показывается; повторная попытка не помогает. Это достаточно точное начало. Фраза «сломался mapper» уже содержит гипотезу, поэтому её нельзя использовать как исходный факт.

\n

Следом за симптомом нужны одно или два наблюдения. У каждого должна быть граница и способ повторного получения. В учебном случае первое наблюдение относится к результату вызова, второе — к входу адаптера. Пока мы не знаем, где исчезло поле. Наблюдение должно пережить смену гипотезы: если виноватым окажется не mapper, запись E2 всё равно останется верной.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
preview-order возвращает blockedОбязательное поле не дошло до адаптераСравнить вход до и после mapper-аОстановить новый mapper на учебной ветке
В логах есть «mapper error»Сообщение приняли за доказанную причинуНайти точку записи и исходное значение поляНе менять timeout до проверки контракта
После возврата версии ответ стал allowedОбход вернул старое поведение, но причина не подтвержденаПовторить контролируемый сценарий и проверить границуОставить результат как mitigation, открыть проверку причины
Ошибка исчезла на одном запросеОдин успех приняли за устойчивое исправлениеПроверить отрицательный путь и повторНе объявлять готовность без критерия
\n

Механизм: пять разных записей

\n

Хорошая временная шкала не пересказывает события задним числом. Она связывает разные типы утверждений. Симптом показывает ущерб. Наблюдение фиксирует значение на границе. Гипотеза объясняет одно наблюдение, но остаётся изменяемой. Действие меняет ограниченный участок. Проверка измеряет эффект именно этого действия. Профилактика меняет будущий путь и имеет собственную проверку.

\n

Если написать «mapper сломал заказ, мы откатили его и добавили тест», читателю приходится восстанавливать всю причинную цепочку. Неясно, видел ли кто-то отсутствие поля. Неясно, что именно откатили. Неясно, какой тест должен пройти. Разделение записей делает вывод скромнее, зато его можно проверить.

\n
const incident = [{ id: 'E1', kind: 'observation', value: 'preview-order -> blocked' }, { id: 'E2', kind: 'observation', value: 'pricingInput.currency === undefined' }, { id: 'H1', kind: 'hypothesis', basedOn: ['E2'], value: 'mapper drops currency' }, { id: 'A1', kind: 'action', basedOn: ['H1'], value: 'use known mapper in demo branch' }, { id: 'V1', kind: 'verification', basedOn: ['A1'], expect: 'state === allowed' }]; const validOrder = incident.every((event, position, all) => position === 0 || all[position - 1].id !== event.id);
\n

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

\n
Учебная временная шкала разбора: симптом, наблюдения, гипотеза, ограниченное действие, проверка и отдельная профилактика
Схема показывает зависимость событий. Наблюдение предшествует гипотезе, действие — проверке, а профилактика не выдаётся за выполненную работу.
\n

Пример: обязательное поле исчезло на границе

\n

Представим два mapper-а. Старый переносит amount и currency. Новый собирает объект из разрешённого списка полей, но в список попал только amount. Downstream-адаптер принимает объект, проверяет валюту и возвращает blocked. Мы видим только конечное состояние, поэтому нельзя сразу утверждать, что ошибка возникла в новом mapper-е.

\n
function mapPreview(input) { return { amount: input.amount }; } function checkPricingInput(mapped) { if (mapped.currency === undefined) return { state: 'blocked', reason: 'currency-missing' }; return { state: 'allowed' }; } const mapped = mapPreview({ amount: 100, currency: 'RUB' }); const result = checkPricingInput(mapped);
\n

Этот фрагмент ограничен учебным сценарием. Он не описывает конкретную библиотеку валидации и не сообщает о реальном инциденте. Проверка причинной версии должна смотреть на вход и выход границы, а не только на финальный ответ. Минимальный тест может сравнить объект до mapper-а с объектом после него и явно потребовать currency.

\n
const input = { amount: 100, currency: 'RUB' }; const mapped = mapPreview(input); if (mapped.currency !== input.currency) throw new Error('currency was lost at mapper boundary');
\n

Если этот тест падает, гипотеза получает сильное подтверждение, но сам тест не показывает, как исправлять код. Ограниченное действие может вернуть известный mapper только на отдельной ветке или выключить новый маршрут. После него нужно повторить тот же вход и проверить ожидаемое состояние. Если тест не падает, H1 надо заменить: поле исчезло раньше, вход сформирован неверно или downstream читает другое имя.

\n

Действие не равно устранению причины

\n

Во время инцидента допустимо сначала остановить ухудшение. Такое действие называют mitigation: оно снижает воздействие, но не закрывает причинный вопрос. Запись должна показать границу действия. «Вернули старый mapper» лучше записать как «для маршрута preview-order отключили новый mapper; другие маршруты не изменяли; ожидаем повторный ответ allowed на контролируемом входе».

\n

Не выбирайте retry или timeout только потому, что они привычны. Повтор помогает при временном отказе, но не возвращает пропущенное поле. Больший timeout меняет длительность ожидания, но не контракт. Если наблюдение указывает на отсутствие значения, проверка должна пройти через значение. Если новое наблюдение укажет на 503 от зависимости, появится другая гипотеза и другая проверка.

\n

Отрицательный путь обязателен. Если контрольный вызов снова дал allowed, проверьте заказ без валюты. Он должен получить предсказуемый отказ, а не пройти дальше. Проверьте повтор после возврата. Проверьте, что изменение не коснулось маршрутов, которые не участвовали в симптоме. Иначе команда докажет только один удачный ответ.

\n

Кто и что держит во время сбоя

\n

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

\n

Это особенно важно, когда несколько инженеров одновременно меняют систему. Если каждый «просто посмотрит» и запустит свою правку, временная шкала перестанет объяснять результат. Запишите действие до запуска, его область и ожидаемый сигнал. Не смешивайте в одной команде откат, изменение конфигурации и проверку нового mapper-а. Иначе после успеха нельзя будет понять, что именно помогло.

\n

Порядок действий

\n
  1. Зафиксируйте маршрут, состояние, окно времени и цену повторения. Не называйте причину.
  2. Сохраните один или два факта на границах: вход, выход, код ответа, поле или лог с точным местом записи.
  3. Проверьте, что факты можно получить снова без изменения системы. Если нельзя, отметьте пробел.
  4. Сформулируйте одну гипотезу и укажите, на какое наблюдение она опирается.
  5. Выберите ограниченное действие с владельцем, областью, условием отмены и ожидаемым результатом.
  6. Проверьте действие контролируемым сценарием. Повторите успешный и отрицательный путь.
  7. Если проверка не прошла, отмените действие в разрешённых границах или смените гипотезу. Не переписывайте исходное наблюдение.
  8. Разделите mitigation и исправление причины. Для каждого укажите собственный статус и следующий check.
  9. Добавьте профилактику: обязательное поле в контракт, тест на границе, сигнал или runbook с владельцем.
  10. Закройте разбор только после проверки профилактики и явного критерия готовности.
\n

Ограничения

\n

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

\n

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

\n

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

\n

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

\n

Разбор готов, когда другой инженер без устного пересказа может назвать исходный симптом, увидеть два наблюдения, проследить ссылку от гипотезы к действию и найти проверку действия. Для отрицательного пути известны ожидаемый отказ и граница его действия. Mitigation отделена от устранения причины. Профилактика имеет владельца, срок или очередь и собственный способ проверки.

\n

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

\n

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

\n" }