8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 253,
|
||
"slug": "editorial-2020-12-field-incident-review",
|
||
"title": "Разбор инцидента: как не принять обход за исправление",
|
||
"excerpt": "Запрос возвращает blocked, потому что обязательное поле теряется на границе mapper. Разбираем, как отделить факт от гипотезы, проверить обратимый обход и оставить защиту от повторения.",
|
||
"contentHtml": "<p>Представим разбор инцидента: запрос <code>preview-order</code> возвращает <code>blocked</code>, хотя вход выглядит допустимым. После преобразования объекта пропадает обязательное поле <code>currency</code>. Возврат к прежнему mapper даёт <code>allowed</code>, и команда закрывает задачу. Цена ошибки проявится позже: новый mapper снова попадёт в путь, поле снова исчезнет, а временный обход уже будут считать исправлением.</p>\n<p>Восстановление сервиса и устранение причины — разные события. Временное действие должно иметь условие запуска, ожидаемый результат и путь отмены. Отдельно нужна проверка, которая не даст известному нарушению контракта вернуться. Ниже — учебный сценарий с воспроизводимой fixture, а не заявление о конкретном production-инциденте.</p>\n<h2>Граница примера</h2>\n<p>Рассмотрим только переход от mapper к локальной функции <code>previewOrder</code>, которая имитирует границу <code>pricing-adapter</code>. Стенд не обращается к сети, базе, очереди или внешнему API. Вход содержит сумму, идентификатор товара и валюту. Новый mapper намеренно теряет <code>currency</code>, старый сохраняет все поля. Так мы проверяем одну гипотезу и не выдаём результат fixture за результат системы.</p>\n<pre><code>const input = { amount: 1000, itemId: 'demo-1', currency: 'RUB' };\n\nfunction newMapper(order) {\n return { amount: order.amount, itemId: order.itemId };\n}\n\nfunction oldMapper(order) {\n return { ...order };\n}\n\nfunction previewOrder(mapped) {\n if (!mapped.currency) {\n return { status: 'blocked', reason: 'currency_missing' };\n }\n return { status: 'allowed', currency: mapped.currency };\n}\n\nconst broken = previewOrder(newMapper(input));\nconst fallback = previewOrder(oldMapper(input));\n\nif (broken.status !== 'blocked' || fallback.status !== 'allowed') {\n throw new Error('fixture contract failed');\n}\n\nconsole.log({ broken, fallback });</code></pre>\n<p>Ожидаемый вывод — <code>broken.status === 'blocked'</code> с причиной <code>currency_missing</code> и <code>fallback.status === 'allowed'</code> с валютой <code>RUB</code>. Важна не удачная строка в консоли, а контраст двух результатов на одном входе. Если изменить вход или правило в <code>previewOrder</code>, вывод уже нельзя переносить на прежний сценарий.</p>\n<p>Функции в примере определены прямо в fixture, поэтому её можно сохранить как небольшой файл Node.js и запустить без зависимостей. Это не интеграционный тест: он не проверяет сериализацию, сеть, базу или реальный код mapper. Эти границы нужно проверять отдельно.</p>\n<h2>Сначала факт, потом гипотеза</h2>\n<p>Наблюдение описывает то, что можно увидеть: один и тот же вход дошёл до границы, после mapper поле отсутствует, а <code>previewOrder</code> вернул <code>blocked</code>. Гипотеза связывает наблюдения: новый mapper не переносит обязательное поле. Она правдоподобна, но не доказана, пока не сравнены вход и результат на этой границе.</p>\n<p>До изменения сохраните четыре значения: входной объект, результат mapper, ответ проверки и идентификатор попытки. Если в настоящем сервисе есть лог или trace, запишите ссылку на него; если есть только локальная fixture, назовите её fixture. Не заменяйте отсутствующее наблюдение уверенной фразой «причина найдена».</p>\n<p>Проверка должна менять одну переменную. В нашем случае новая версия mapper заменяется старой только на ограниченном пути. Если тот же вход даёт <code>allowed</code> и возвращает <code>currency</code>, гипотеза получает поддержку. Но это ещё не доказывает, что она объясняет все отказы: нужно проверить другие обязательные поля и реальные точки сериализации.</p>\n<div class='table-scroll'><table><caption>Связь наблюдения, проверки и решения</caption><thead><tr><th scope='col'>Наблюдение</th><th scope='col'>Гипотеза</th><th scope='col'>Проверка</th><th scope='col'>Владелец</th><th scope='col'>Решение</th></tr></thead><tbody><tr><td><code>preview-order</code> вернул <code>blocked</code></td><td>Нарушен контракт обязательного поля или сработала другая ветка отказа</td><td>Сохранить ответ и проверить вход на границе mapper</td><td>Инженер, ведущий диагностику</td><td>Не менять retry и timeout до локализации причины</td></tr><tr><td>После нового mapper нет <code>currency</code></td><td>Mapper мог отбросить поле при сборке объекта</td><td>Сравнить вход, результат нового mapper и результат старого</td><td>Владелец адаптера</td><td>Сформулировать узкую гипотезу H1</td></tr><tr><td>Старый mapper даёт <code>allowed</code></td><td>Обход возвращает нужный контракт, но причина не устранена</td><td>Повторить fixture на том же входе и проверить сохранение поля</td><td>Владелец релиза</td><td>Оставить обход ограниченным и записать условие отмены</td></tr><tr><td>Проверка обхода не прошла</td><td>Гипотеза неполна или отказ возник на другой границе</td><td>Собрать новое наблюдение до следующего изменения</td><td>Ведущий диагностики</td><td>Отменить вывод и проверить альтернативную ветку</td></tr><tr><td>Ошибка вернулась после изменения mapper</td><td>Для контракта нет автоматической защиты</td><td>Запустить тест на отсутствие обязательного поля</td><td>Владелец компонента</td><td>Добавить контрактную проверку и связать её с релизом</td></tr></tbody></table></div>\n<h2>Восстановление не равно исправлению</h2>\n<p>Возврат к старому mapper может быть правильным способом уменьшить влияние ошибки. У него должны быть границы: какой маршрут переключается, какой сигнал включает обход, кто его подтверждает и по какому условию он снимается. Без этого временное решение превращается в новый постоянный путь, который никто не проверяет.</p>\n<p>После восстановления отдельно зафиксируйте причинный вывод. В нашем примере он звучит узко: при входе с <code>currency</code> новый mapper возвращает объект без этого поля, а старый mapper поле сохраняет. Формулировка не обещает, что найден единственный дефект во всей системе. Она указывает конкретную границу, которую можно проверить.</p>\n<p>Такое разделение совпадает с практикой управления инцидентами: во время сбоя нужны отдельные роли для операционной работы, коммуникации и координации, а после восстановления — живой документ с действиями и состоянием. Это снижает риск, что несколько инженеров одновременно изменят систему и сотрут причинную связь между изменением и результатом.</p>\n<h2>Почему нельзя лечить все симптомы сразу</h2>\n<p>Увеличение <code>timeout</code> скрывает медленный ответ, но не возвращает пропавшее поле. Дополнительный <code>retry</code> повторяет тот же неверный объект и может умножить побочный эффект. Одновременная правка mapper, retry и timeout стирает причинную связь: если результат изменится, станет непонятно, какая правка помогла.</p>\n<p>Это не запрет на retry или timeout. Они уместны, когда наблюдение указывает на временную сетевую ошибку или ограничение времени ответа. Но у такого решения должны быть собственный trigger, лимит повторов и собственная проверка. Инструмент выбирают по наблюдаемому механизму, а не по привычке.</p>\n<figure><img src='/assets/editorial/2020/incident-learning-loop-2020.svg' alt='Цикл разбора инцидента: факт, гипотеза, ограниченное действие, проверка и профилактика' loading='lazy' /><figcaption>Ограниченное действие снижает влияние сейчас. Контрактная проверка снижает риск повторения позже.</figcaption></figure>\n<h2>Отрицательный путь обязателен</h2>\n<p>Предположим, возврат к старому mapper не дал <code>allowed</code>. Это не повод объявить старый код неисправным. Новый факт говорит только о том, что выбранный обход не подтвердил H1. Поле могло отсутствовать уже во входе. Отказ мог зависеть от другой обязательной величины. Следующая проверка должна отличить эти варианты.</p>\n<p>Проверим первый вариант: уберём <code>currency</code> из самого входа и запустим старый mapper. Поле останется отсутствующим, поэтому результат <code>blocked</code> больше не подтверждает гипотезу о новом mapper. Проверим второй вариант: оставим валюту, но уберём другой обязательный атрибут. Если отказ сохранится, причина находится не в потерянной валюте.</p>\n<p>Хороший review не прячет отрицательный результат. Запись «A1 не подтвердил H1 на входе E1» полезнее, чем «фикс не сработал»: первая формулировка указывает границу нового исследования. Она также не обвиняет автора mapper и оставляет следующий шаг проверяемым.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Назвать один симптом и одну границу. В примере это <code>blocked</code> на переходе к <code>pricing-adapter</code>.</li><li>Собрать факты до изменения: вход, результат mapper, ответ и идентификатор попытки.</li><li>Разделить факт и гипотезу. Возможную причину не записывать как доказанное наблюдение.</li><li>Выбрать одно обратимое действие. Указать trigger, ожидаемый результат и условие отмены.</li><li>Повторить тот же сценарий на том же входе или выполнить безопасный контролируемый запрос.</li><li>Проверить результат только в пределах входа и среды. Не переносить fixture на неизвестные пути.</li><li>Прогнать отрицательные варианты: поле отсутствует во входе, отказ вызван другим полем, обход не меняет ответ.</li><li>Добавить контрактный тест: он должен падать на результате mapper без <code>currency</code> и проходить на корректном объекте.</li><li>Закрыть работу только после проверки действия и профилактики. Если проверка отрицательна, начать новый цикл с наблюдения.</li></ol>\n<h2>Ограничения</h2>\n<p>Fixture не измеряет доступность, нагрузку, время восстановления, права доступа или поведение внешней зависимости. Она не заменяет журнал, мониторинг, резервирование и процедуру отката. Схема поясняет последовательность, но не является доказательством результата. Учебный код ограничен одним объектом, двумя mapper и одним обязательным полем.</p>\n<p>Нельзя объявлять production-инцидент исправленным по одному успешному примеру. В реальной системе нужно подтвердить границу на фактическом запросе, проверить безопасный rollout и посмотреть на отрицательные случаи. Нельзя также превращать разбор в поиск виноватого: смена автора mapper не объясняет, почему контракт оказался без защиты.</p>\n<p>Источники ниже подтверждают практики incident management и postmortem, но не подтверждают вымышленный запрос <code>preview-order</code> или состояние конкретного сервиса. Факты такого инцидента должны подтверждаться собственными логами, изменениями и конфигурацией мониторинга.</p>\n<h2>Критерий готовности</h2>\n<p>Работа готова, когда симптом воспроизводится на известном входе, действие меняет только заявленную границу и проходит свою проверку, а отрицательный путь приводит к новой гипотезе. Дополнительно автоматическая проверка должна падать на результате mapper без <code>currency</code> и проходить на корректном результате.</p>\n<p>Этот критерий не обещает, что система больше никогда не откажет. Он делает более узкое утверждение: конкретный контракт виден, действие проверено, а известный способ регрессии получает автоматическую защиту. Такой результат можно связать с последующим изменением и проверить повторно.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://sre.google/sre-book/managing-incidents/' target='_blank' rel='noopener noreferrer'>Google SRE Book: Managing Incidents</a> — официальная глава о разделении операционной работы, коммуникации, планирования, живом документе инцидента и передаче ответственности.</li><li><a href='https://sre.google/sre-book/postmortem-culture/' target='_blank' rel='noopener noreferrer'>Google SRE Book: Postmortem Culture</a> — официальное описание postmortem как записи влияния, действий, причин и follow-up; отдельно объясняет объективные триггеры и принцип без поиска виноватого.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/61/r3/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-61 Rev. 3</a> — актуальная рекомендация NIST, опубликованная в апреле 2025 года, по incident response в контексте кибербезопасности; это дополнительная рамка, а не источник фактов об учебном прикладном баге.</li></ul>"
|
||
}
|