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