8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 255,
|
||
"slug": "editorial-2020-12-practice-incident-review",
|
||
"title": "Разбор инцидента: как отделить факт от поспешного исправления",
|
||
"excerpt": "Сервис отвечает ошибкой, команда меняет timeout, а причина остаётся неизвестной. Разбираем учебный случай через наблюдение, гипотезу, ограниченное действие, проверку и профилактику.",
|
||
"contentHtml": "<p>Сервис предварительного расчёта заказа начал возвращать <code>blocked</code> вместо цены. Ошибка видна пользователю, но её граница не видна инженеру: поле могло исчезнуть во входе, mapper-е или контракте downstream-сервиса. Цена поспешной правки — не только несколько минут простоя. Новый retry может увеличить нагрузку, timeout может спрятать отказ, а возврат старой версии может оставить причину без защиты. Через месяц тот же дефект вернётся под другим сообщением.</p>\n<p><strong>Тезис:</strong> разбор начинается с сохранения наблюдаемого факта. Затем команда формулирует одну проверяемую гипотезу, выбирает ограниченное действие и заранее называет проверку. Временный обход не становится причиной, а успешный ответ не доказывает готовность системы. Такая последовательность нужна и маленькому модулю, и аварии, в которой участвуют несколько команд.</p>\n<h2>Сначала зафиксировать симптом</h2>\n<p>Симптом отвечает на вопрос «что заметил пользователь или мониторинг?». Запишите маршрут, состояние, окно времени и цену повторения. Например: <code>POST /preview-order</code> возвращает <code>blocked</code> для заказа с валютой <code>RUB</code>; расчёт не показывается; повторная попытка не помогает. Это достаточно точное начало. Фраза «сломался mapper» уже содержит гипотезу, поэтому её нельзя использовать как исходный факт.</p>\n<p>Следом за симптомом нужны одно или два наблюдения. У каждого должна быть граница и способ повторного получения. В учебном случае первое наблюдение относится к результату вызова, второе — к входу адаптера. Пока мы не знаем, где исчезло поле. Наблюдение должно пережить смену гипотезы: если виноватым окажется не mapper, запись E2 всё равно останется верной.</p>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><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>Остановить новый mapper на учебной ветке</td></tr><tr><td>В логах есть «mapper error»</td><td>Сообщение приняли за доказанную причину</td><td>Найти точку записи и исходное значение поля</td><td>Не менять timeout до проверки контракта</td></tr><tr><td>После возврата версии ответ стал <code>allowed</code></td><td>Обход вернул старое поведение, но причина не подтверждена</td><td>Повторить контролируемый сценарий и проверить границу</td><td>Оставить результат как mitigation, открыть проверку причины</td></tr><tr><td>Ошибка исчезла на одном запросе</td><td>Один успех приняли за устойчивое исправление</td><td>Проверить отрицательный путь и повтор</td><td>Не объявлять готовность без критерия</td></tr></tbody></table>\n<h2>Механизм: пять разных записей</h2>\n<p>Хорошая временная шкала не пересказывает события задним числом. Она связывает разные типы утверждений. Симптом показывает ущерб. Наблюдение фиксирует значение на границе. Гипотеза объясняет одно наблюдение, но остаётся изменяемой. Действие меняет ограниченный участок. Проверка измеряет эффект именно этого действия. Профилактика меняет будущий путь и имеет собственную проверку.</p>\n<p>Если написать «mapper сломал заказ, мы откатили его и добавили тест», читателю приходится восстанавливать всю причинную цепочку. Неясно, видел ли кто-то отсутствие поля. Неясно, что именно откатили. Неясно, какой тест должен пройти. Разделение записей делает вывод скромнее, зато его можно проверить.</p>\n<pre><code>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);</code></pre>\n<p>Код выше — учебный пример в памяти процесса. Он не подключается к HTTP, очереди, базе, логам или production. Его задача — показать форму записи: позднее событие ссылается на более раннее, а проверка относится к действию. Такая модель не доказывает причину автоматически. Она не даёт перепутать гипотезу с наблюдением и не позволяет назвать «готово» без ожидаемого результата.</p>\n<figure><img src='/assets/editorial/2020/incident-timeline-2020.svg' alt='Учебная временная шкала разбора: симптом, наблюдения, гипотеза, ограниченное действие, проверка и отдельная профилактика' loading='lazy' /><figcaption>Схема показывает зависимость событий. Наблюдение предшествует гипотезе, действие — проверке, а профилактика не выдаётся за выполненную работу.</figcaption></figure>\n<h2>Пример: обязательное поле исчезло на границе</h2>\n<p>Представим два mapper-а. Старый переносит <code>amount</code> и <code>currency</code>. Новый собирает объект из разрешённого списка полей, но в список попал только <code>amount</code>. Downstream-адаптер принимает объект, проверяет валюту и возвращает <code>blocked</code>. Мы видим только конечное состояние, поэтому нельзя сразу утверждать, что ошибка возникла в новом mapper-е.</p>\n<pre><code>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);</code></pre>\n<p>Этот фрагмент ограничен учебным сценарием. Он не описывает конкретную библиотеку валидации и не сообщает о реальном инциденте. Проверка причинной версии должна смотреть на вход и выход границы, а не только на финальный ответ. Минимальный тест может сравнить объект до mapper-а с объектом после него и явно потребовать <code>currency</code>.</p>\n<pre><code>const input = { amount: 100, currency: 'RUB' }; const mapped = mapPreview(input); if (mapped.currency !== input.currency) throw new Error('currency was lost at mapper boundary');</code></pre>\n<p>Если этот тест падает, гипотеза получает сильное подтверждение, но сам тест не показывает, как исправлять код. Ограниченное действие может вернуть известный mapper только на отдельной ветке или выключить новый маршрут. После него нужно повторить тот же вход и проверить ожидаемое состояние. Если тест не падает, H1 надо заменить: поле исчезло раньше, вход сформирован неверно или downstream читает другое имя.</p>\n<h2>Действие не равно устранению причины</h2>\n<p>Во время инцидента допустимо сначала остановить ухудшение. Такое действие называют mitigation: оно снижает воздействие, но не закрывает причинный вопрос. Запись должна показать границу действия. «Вернули старый mapper» лучше записать как «для маршрута <code>preview-order</code> отключили новый mapper; другие маршруты не изменяли; ожидаем повторный ответ <code>allowed</code> на контролируемом входе».</p>\n<p>Не выбирайте retry или timeout только потому, что они привычны. Повтор помогает при временном отказе, но не возвращает пропущенное поле. Больший timeout меняет длительность ожидания, но не контракт. Если наблюдение указывает на отсутствие значения, проверка должна пройти через значение. Если новое наблюдение укажет на 503 от зависимости, появится другая гипотеза и другая проверка.</p>\n<p>Отрицательный путь обязателен. Если контрольный вызов снова дал <code>allowed</code>, проверьте заказ без валюты. Он должен получить предсказуемый отказ, а не пройти дальше. Проверьте повтор после возврата. Проверьте, что изменение не коснулось маршрутов, которые не участвовали в симптоме. Иначе команда докажет только один удачный ответ.</p>\n<h2>Кто и что держит во время сбоя</h2>\n<p>Фактологичный разбор не ищет виноватого человека, но требует технической ответственности. Должны быть понятны владелец решения, исполнитель изменения и канал обновлений. Один человек держит общую картину и решает, что делать дальше. Другой выполняет изменение. Третий, если есть такая возможность, фиксирует состояние и проверяет результаты. Для маленькой команды роли могут совмещаться, но функции нельзя потерять.</p>\n<p>Это особенно важно, когда несколько инженеров одновременно меняют систему. Если каждый «просто посмотрит» и запустит свою правку, временная шкала перестанет объяснять результат. Запишите действие до запуска, его область и ожидаемый сигнал. Не смешивайте в одной команде откат, изменение конфигурации и проверку нового mapper-а. Иначе после успеха нельзя будет понять, что именно помогло.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте маршрут, состояние, окно времени и цену повторения. Не называйте причину.</li><li>Сохраните один или два факта на границах: вход, выход, код ответа, поле или лог с точным местом записи.</li><li>Проверьте, что факты можно получить снова без изменения системы. Если нельзя, отметьте пробел.</li><li>Сформулируйте одну гипотезу и укажите, на какое наблюдение она опирается.</li><li>Выберите ограниченное действие с владельцем, областью, условием отмены и ожидаемым результатом.</li><li>Проверьте действие контролируемым сценарием. Повторите успешный и отрицательный путь.</li><li>Если проверка не прошла, отмените действие в разрешённых границах или смените гипотезу. Не переписывайте исходное наблюдение.</li><li>Разделите mitigation и исправление причины. Для каждого укажите собственный статус и следующий check.</li><li>Добавьте профилактику: обязательное поле в контракт, тест на границе, сигнал или runbook с владельцем.</li><li>Закройте разбор только после проверки профилактики и явного критерия готовности.</li></ol>\n<h2>Ограничения</h2>\n<p>Пять типов записи не заменяют мониторинг, резервирование, коммуникацию или анализ безопасности. Они не определяют допустимое время восстановления и не говорят, какой rollback безопасен для конкретной базы. При внешнем побочном эффекте нужно отдельно проверить идемпотентность, повторную доставку и состояние данных. Выключение маршрута не отменяет уже отправленный платёж, созданный заказ или опубликованное сообщение.</p>\n<p>Учебные значения и имена в коде вымышлены. Здесь нет production-метрик, реального трафика, подтверждённого времени восстановления или доказательства, что конкретная команда применяла этот сценарий. Не переносите <code>allowed</code>, <code>blocked</code> и выбранный mapper в свою систему без проверки её контракта. Если граница неизвестна, безопасное действие — остановить расширение изменения и собрать недостающие данные.</p>\n<p>Не всякий сбой требует большого postmortem. Но если затронут пользовательский путь, вовлечена вторая команда, повторяется ошибка или исправление меняет состояние данных, короткая запись должна превратиться в отдельный разбор с владельцем и follow-up. Нельзя объявлять профилактику выполненной только потому, что её записали.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Разбор готов, когда другой инженер без устного пересказа может назвать исходный симптом, увидеть два наблюдения, проследить ссылку от гипотезы к действию и найти проверку действия. Для отрицательного пути известны ожидаемый отказ и граница его действия. Mitigation отделена от устранения причины. Профилактика имеет владельца, срок или очередь и собственный способ проверки.</p>\n<p>Практический критерий простой: запись позволяет повторить контрольный сценарий, получить ожидаемый результат, увидеть предсказуемый отказ на плохом входе и объяснить, что изменится при провале проверки. Если причина ещё неизвестна, это честно указано. Если действие временное, указана граница возврата. Если хотя бы одного элемента нет, разбор не готов к закрытию.</p>\n<h2>Проверяемые источники</h2>\n<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> — официальная глава о разборе влияния, причин и follow-up без персонального обвинения.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/61/r2/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-61 Rev. 2: Computer Security Incident Handling Guide</a> — официальный документ для security-инцидентов; его цикл нельзя механически переносить на любой прикладной баг.</li></ul>"
|
||
}
|