{ "index": 254, "slug": "editorial-2020-12-mechanism-incident-review", "title": "Разбор инцидента: как не перепутать симптом с причиной", "excerpt": "После восстановления сервиса обход легко принять за окончательное решение. Разбираем цепочку от наблюдения до проверки и показываем, как сохранить отрицательный результат и превратить вывод в проверяемое действие.", "contentHtml": "
Синтетический сценарий выглядит знакомо: операция вернула ошибку, после отката проверка снова прошла, и команда записала результат как «исправили». Через месяц похожий симптом появился после другого изменения. Повторный обход сработал на одном входе, но никто не мог показать, какая граница нарушилась и какой сигнал подтвердил решение.
\nПроблема здесь не в самом откате. Временное действие становится опасным, когда его принимают за объяснение причины. Оно может скрыть потерю данных, увеличить задержку или перенести отказ на следующую границу. Отчёт тогда сохраняет уверенный пересказ, но не сохраняет факты, ход проверки и то, что осталось неизвестным.
\nРазбор полезен, когда разделяет пять состояний: наблюдение, гипотезу, действие, проверку и профилактику. Наблюдение фиксирует факт. Гипотеза объясняет факт и допускает опровержение. Действие меняет одну ограниченную часть системы. Проверка измеряет результат именно этого действия. Профилактика меняет будущий путь и считается выполненной только после отдельной проверки.
\nРазбор начинается с границы, а не со слова «причина». Граница — место, где можно сравнить вход и выход: переход между mapper и адаптером, запись в базу, публикация сообщения или вызов внешнего API. Рядом с ней запишите фактический вход, ожидаемое поведение и наблюдаемый результат.
\nВ учебном примере объект заказа проходит через order-mapper и попадает в pricing-adapter. Адаптер принимает заказ только с непустым полем currency. После изменения mapper поле исчезает, а операция preview-order получает статус blocked. Это два наблюдения. Они ещё не доказывают, что mapper — единственная причина отказа.
Сценарий синтетический: он не описывает production-систему, реальный трафик, время восстановления или фактический инцидент. Маленький вход нужен для воспроизводимой проверки формы рассуждения. В рабочем расследовании те же места заполняются данными из запроса, журнала, трассировки и версии контракта.
\n| Состояние | Запись | Проверка | Граница вывода |
|---|---|---|---|
| Наблюдение | preview-order вернул blocked | Сохранить вход и ответ операции | Зафиксирован симптом, но не его причина |
| Гипотеза | После mapper отсутствует currency | Сравнить объект до и после преобразования | Версия объясняет этот вход, но не все пути заказа |
| Действие | Временно выбрать известный mapper с тем же входом | Не менять одновременно retry, timeout и кеш | Решение ограничено выбранной границей |
| Проверка | Старый mapper вернул заказ с currency | Повторить валидацию на том же входе | Подтверждено действие, а не устранение всех причин |
В первой строке есть только симптом. Во второй появляется проверяемая гипотеза. В третьей действие меняет одну переменную. В четвёртой измеряется ожидаемый сигнал. Если добавить к откату повторные попытки и новый таймаут, улучшение результата уже нельзя будет связать с одной причиной.
\nНаблюдение должно пережить неудачную гипотезу. Запись «после преобразования поле отсутствует» останется верной, даже если поле пропало до mapper. Запись «mapper отбросил поле» утверждает больше, чем показывает сигнал. Если эксперимент опровергнет эту версию, меняется гипотеза, а не прошлое наблюдение.
\nКод ниже воспроизводит симптом и отдельно проверяет ограниченный обход. В нём нет сети, базы и скрытого состояния, поэтому результат можно получить на любом запуске JavaScript:
\nconst input = { amount: 1250, currency: 'RUB' };\n\nfunction mapOrderV2(order) {\n return { amount: order.amount };\n}\n\nfunction mapOrderV1(order) {\n return { amount: order.amount, currency: order.currency };\n}\n\nfunction validateForPricing(order) {\n return typeof order.currency === 'string' && order.currency.length > 0\n ? 'allowed'\n : 'blocked';\n}\n\nconst mapped = mapOrderV2(input);\nconst symptom = validateForPricing(mapped);\n\nif (symptom !== 'blocked') throw new Error('expected the exercise symptom');\nif ('currency' in mapped) throw new Error('currency must be absent');\n\nconst restored = mapOrderV1(input);\nconst check = validateForPricing(restored);\n\nif (check !== 'allowed') throw new Error('the bounded check failed');\nПервый вызов подтверждает воспроизводимость симптома: версия V2 теряет поле до адаптера. Второй вызов проверяет только действие A1 — выбор известного преобразования для того же объекта. Статус allowed не доказывает, что исправлены новая версия, все валюты, пустые значения, другие ветки или рабочая система.
Важна и отрицательная проверка. Если mapOrderV1 тоже вернёт blocked, обход не подтвердился. Нельзя переписать результат как «система всё равно восстановилась»: новый факт сужает поиск. Следующий шаг — сравнить вход до mapper, версию схемы, ветку маршрутизации и контракт адаптера.
Во время инцидента решение выбирают быстро, но его границы можно записать сразу. Зафиксируйте trigger, изменение и ожидаемый сигнал. Добавьте условие возврата к диагностике. Тогда результат действия не смешается со случайным совпадением.
\nВ учебном случае trigger таков: на границе адаптера отсутствует currency. Изменение одно: для этого входа выбрать известный mapper. Ожидаемый сигнал — поле присутствует, а валидатор возвращает allowed. Условие возврата — любой другой ответ, отсутствие поля или расхождение входа; при нём действие считают неподтверждённым.
Этот порядок не делает retry или timeout вредными сами по себе. Они проверяют другие гипотезы. Повторная попытка уместна при обоснованной временной ошибке и защите от дублей. Таймаут проверяет задержку, а не потерю поля. Каждой мере нужен собственный trigger, ожидаемый сигнал и отрицательный путь.
\nФраза «добавить тест» слишком коротка для action item. Назовите контракт, место проверки и ожидаемый отказ. Например: mapper обязан сохранить непустой currency; проверка должна стоять перед вызовом pricing-adapter; минимальный вход без валюты должен приводить к явной ошибке до внешнего вызова.
Профилактика не завершена в момент, когда её записали в отчёт. Она готова, когда изменение появилось в нужном коде, проверка запускается в этом пути, а отрицательный сценарий действительно обнаруживает нарушение. До этого состояние следует назвать планом. Иначе отчёт приписывает системе защиту, которой в ней ещё нет.
\nДля рабочего postmortem полезно сохранить не только действие, но и влияние, исходные данные, contributing causes и follow-up. При этом техническая запись может быть blameless: она называет поле, границу и отсутствующую защиту, но не приписывает человеку намерение, которого нет в evidence.
\nСначала сохраните базовый результат. Затем измените только одну переменную и повторите проверку на сопоставимом входе. Сравните объект на границе, ответ валидатора и побочные сигналы. Если результат изменился, это evidence в пользу гипотезы, но не доказательство для всех вариантов.
\nЧтобы не расширить вывод случайно, перечислите непроверенные ветки: другая версия схемы, пустая строка, неизвестная валюта, повторная доставка, параллельный маршрут и ошибка внешнего сервиса. Для каждой ветки укажите отдельный тест или наблюдение. Успех одного минимального примера остаётся успехом одного минимального примера.
\nРазделение состояний не вычисляет корневую причину автоматически и не заменяет мониторинг, резервирование, контроль доступа, откат или техническое расследование. У одного отказа может быть несколько contributing causes: потеря поля, отсутствие проверки и слишком широкий обход. Для каждого условия нужны свои наблюдения.
\nКод выше не измеряет доступность, задержку, нагрузку, восстановление или влияние на пользователей. Он не подтверждает, что конкретный mapper вызвал реальный отказ. В рабочей системе проверьте фактический контракт адаптера, версию схемы, трассировку поля, права, данные и внешние зависимости. Если данных нет, вывод ограничивается учебным сценарием.
\nРазбор готов, когда другой инженер без устного пересказа может назвать симптом и границу, увидеть исходный evidence, повторить ограниченное действие, получить ожидаемый сигнал и найти отрицательный путь. Он также должен видеть, что осталось непроверенным и какая профилактика защищает контракт в будущем.
\nФинальная проверка должна отвечать на пять вопросов: что произошло; где это наблюдалось; какая гипотеза проверялась; что именно изменили; какой результат получен. Если на любой вопрос приходится отвечать «так принято» или «сервис восстановился», в записи не хватает evidence.
\n