Files
progcode/editorial/agent-rewrites/255.json
T

8 lines
22 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>После симптома нужны одно или два наблюдения на границах системы. У каждого должны быть источник и способ повторного получения. Например, первое наблюдение — ответ вызова, второе — значение поля во входе адаптера. Пока не проверены промежуточные значения, мы не знаем, где исчезла валюта. Учебная запись 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>Не менять timeout; остановить новый mapper только в учебной ветке</td></tr><tr><td>В логах есть «mapper error»</td><td>Сообщение приняли за доказанную причину</td><td>Найти точку записи и исходное значение поля</td><td>Сохранить исходный лог и проверить контракт</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 events = [\n { id: 'E1', kind: 'observation', value: 'preview-order -&gt; blocked' },\n { id: 'E2', kind: 'observation', value: 'pricingInput.currency === undefined' },\n { id: 'H1', kind: 'hypothesis', basedOn: ['E2'], value: 'mapper drops currency' },\n { id: 'A1', kind: 'action', basedOn: ['H1'], value: 'use known mapper in demo branch' },\n { id: 'V1', kind: 'verification', basedOn: ['A1'], expect: 'state === allowed' },\n];\nconst byId = new Map(events.map((event) =&gt; [event.id, event]));\nconst position = new Map(events.map((event, index) =&gt; [event.id, index]));\nconst referencesExist = events.every((event) =&gt;\n !event.basedOn || event.basedOn.every((id) =&gt; byId.has(id)),\n);\nconst referencesPrecede = events.every((event) =&gt;\n !event.basedOn || event.basedOn.every((id) =&gt; position.get(id) &lt; position.get(event.id)),\n);\nconst validOrder = referencesExist &amp;&amp; referencesPrecede;\nconsole.log(validOrder); // true</code></pre>\n<p>Этот фрагмент проверяет две вещи: каждая ссылка указывает на существующее событие, а основание стоит раньше события, которое на него ссылается. Он не подключается к HTTP, очереди, базе, логам или production и не доказывает корневую причину. Его задача — не дать записи H1 сослаться на несуществующий факт, а V1 — появиться раньше действия A1.</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>. Это модель для воспроизведения, а не утверждение о реальном проекте.</p>\n<pre><code>function mapPreview(input) {\n return { amount: input.amount };\n}\n\nfunction checkPricingInput(mapped) {\n if (mapped.currency === undefined) {\n return { state: 'blocked', reason: 'currency-missing' };\n }\n return { state: 'allowed' };\n}\n\nconst input = { amount: 100, currency: 'RUB' };\nconst mapped = mapPreview(input);\nconst result = checkPricingInput(mapped);\nconsole.log(mapped, result); // { amount: 100 } { state: 'blocked', ... }</code></pre>\n<p>В этом примере ошибка видна на выходе mapper-а: <code>currency</code> исчезла до проверки downstream. Но один такой запуск ещё не доказывает, что именно новый mapper вызвал реальный отказ. В настоящем сервисе нужно снять значения до и после границы из разрешённого журнала, тестового стенда или другого безопасного источника.</p>\n<p>Сначала полезно зафиксировать RED-проверку — она должна упасть на дефектном mapper-е:</p>\n<pre><code>if (mapped.currency !== input.currency) {\n throw new Error('currency was lost at mapper boundary');\n}</code></pre>\n<p>После исправления mapper переносит оба поля. Проверка должна подтвердить и рабочий, и отрицательный путь:</p>\n<pre><code>function mapPreviewFixed(input) {\n return { amount: input.amount, currency: input.currency };\n}\n\nconst allowed = checkPricingInput(mapPreviewFixed({ amount: 100, currency: 'RUB' }));\nconst missing = checkPricingInput(mapPreviewFixed({ amount: 100 }));\nif (allowed.state !== 'allowed' || missing.state !== 'blocked') {\n throw new Error('preview contract check failed');\n}\nconsole.log('PASS: positive and negative paths are explicit');</code></pre>\n<p>Этот GREEN-пример проверяет только локальный контракт двух функций. Он не заменяет интеграционный тест, проверку реального формата запроса или наблюдение после релиза. Если RED-проверка не падает на дефектной версии, H1 нужно заменить: поле исчезло раньше, вход сформирован иначе или downstream читает другое имя.</p>\n<h2>Действие не равно устранению причины</h2>\n<p>Во время сбоя допустимо сначала остановить ухудшение. Такое действие называют mitigation: оно снижает воздействие, но не закрывает причинный вопрос. Граница должна быть записана явно: «для маршрута <code>preview-order</code> отключили новый mapper; другие маршруты не меняли; на контролируемом входе ожидаем <code>allowed</code>». Это описание плана проверки, а не доказанный результат.</p>\n<p>Не выбирайте retry или timeout только потому, что они привычны. Повтор помогает при временном отказе, но не возвращает пропущенное поле. Больший timeout меняет длительность ожидания, но не контракт. Если наблюдение указывает на отсутствие значения, проверка должна пройти через это значение. Если новое наблюдение покажет <code>503</code> от зависимости, появится другая гипотеза и другая проверка.</p>\n<p>Отрицательный путь обязателен. После контрольного вызова проверьте заказ без валюты: он должен получить предсказуемый отказ, а не пройти дальше. Повторите сценарий после возврата версии и убедитесь, что изменение не коснулось маршрутов, не участвовавших в симптоме. Иначе команда докажет только один удачный ответ.</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> — официальный документ именно для обработки инцидентов информационной безопасности; его цикл нельзя механически переносить на любой прикладной баг.</li></ul>"
}