diff --git a/editorial/agent-rewrites/254.json b/editorial/agent-rewrites/254.json index 1208f29..89ebda7 100644 --- a/editorial/agent-rewrites/254.json +++ b/editorial/agent-rewrites/254.json @@ -2,6 +2,6 @@ "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Синтетический сценарий выглядит знакомо: операция вернула ошибку, после отката проверка снова прошла, и команда записала результат как «исправили». Через месяц похожий симптом появился после другого изменения. Повторный обход сработал на одном входе, но никто не мог показать, какая граница нарушилась и какой сигнал подтвердил решение.
\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