8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 148,
|
||
"slug": "editorial-2023-11-field-postmortem",
|
||
"title": "Postmortem без поиска виноватого: от неполного сигнала к проверяемому действию",
|
||
"excerpt": "Как отделить факт от поздней гипотезы, восстановить контекст решения и выбрать одну защиту с измеримым критерием. Внутри — безопасный пример и границы применимости.",
|
||
"contentHtml": "<p>После сбоя отчёт часто начинается с фамилии и заканчивается формулой «усилить контроль». Такой текст выглядит решительным, но не отвечает на два рабочих вопроса: что команда действительно знала в момент решения и какое изменение обнаружит повтор. Цена неточного postmortem — следующая смена повторяет риск, а спор о виноватом вытесняет проверку механизма.</p>\n<p>Postmortem полезен как запись наблюдений, решений и последующих действий. Его задача — не восстановить красивую историю задним числом, а очертить границу знания: какой симптом увидели, какой сигнал был доступен, что сделали, какая гипотеза ещё не доказана и как её проверить. Подход без поиска виноватого не отменяет ответственности за действие; он переносит ответственность с оценки личности на изменение условий, в которых система и люди принимают решения.</p>\n<h2>Начните с наблюдаемого симптома</h2>\n<p>Первая строка должна описывать эффект, а не причину. «В 10:02 доля ответов 5xx выросла с 0,4% до 8,1% на маршруте <code>/checkout</code>» проверяема. «Инженер не уследил за релизом» — это оценка и к тому же не говорит, какой сигнал отсутствовал. Для симптома зафиксируйте период, затронутый маршрут или ресурс, способ обнаружения и влияние на пользователя.</p>\n<p>Каждый факт связывайте с источником: метрикой, логом, трассировкой, записью изменения или сообщением дежурного. Источник не превращает запись в абсолютную истину: метрика может иметь задержку, лог — потерянные поля, трасса — выборку. Но ссылка даёт читателю возможность повторить проверку и увидеть, где начинаются предположения.</p>\n<p>Разделяйте время события и время знания. Сбой мог начаться в 10:02, alert прийти в 10:06, а полная трасса появиться в 10:20. Решение в 10:07 оценивают по данным, доступным в 10:07. Поздно найденная причина полезна для расследования, но не должна незаметно подменять контекст дежурного.</p>\n<h2>Разведите факт, решение и гипотезу</h2>\n<p>У этих записей разные роли. Факт отвечает на вопрос «что наблюдалось и откуда это известно». Решение фиксирует действие, момент и набор доступных данных. Гипотеза объясняет возможную связь и должна иметь проверку, которая способна её ослабить или опровергнуть. Если всё записать в одном абзаце, читатель перестаёт видеть, где заканчивается evidence и начинается интерпретация.</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>Факт</td><td>В 10:06 alert показал рост 5xx на одном маршруте</td><td>Открыть метрику с тем же окном и фильтром</td><td>Причину роста</td></tr><tr><td>Решение</td><td>В 10:07 остановили дальнейший rollout</td><td>Сверить журнал изменения и доступные сигналы</td><td>Что остановка была единственным верным выбором</td></tr><tr><td>Гипотеза</td><td>Новая конфигурация меняет тайм-аут upstream</td><td>Сравнить конфигурации и повторить запрос в изолированном контуре</td><td>Что гипотеза объясняет весь impact</td></tr><tr><td>Действие</td><td>Добавить проверку перед применением опасного списка</td><td>Прогнать пустой, частичный и повторный вход</td><td>Что защита устраняет все классы отказов</td></tr></tbody></table>\n<p>Слово «unknown» здесь обозначает состояние данных, а не провал автора. Запишите неизвестный вопрос, допустимый источник и безопасный способ проверки. Если лог удалён, так и напишите: «причина не установлена из-за retention 24 часа». Это честнее, чем выбирать наиболее правдоподобную версию и выдавать её за факт.</p>\n<figure><img src='/assets/editorial/2023/postmortem-2023-fact-timeline.svg' alt='Схема postmortem: факты и время события отделены от решения, гипотезы и последующей профилактической проверки' loading='lazy' /><figcaption>Короткая временная линия помогает не смешать данные, доступные во время инцидента, с объяснением, найденным позже.</figcaption></figure>\n<h2>Оцените решение в его моменте</h2>\n<p>Выберите одно решение из хронологии и восстановите его контекст. Запишите время, сигнал, доступные права, ограничения по времени и варианты возврата. Такой разбор может выявить ошибку, но нередко обнаруживает системный пробел: dashboard не показывал разбивку по версии, безопасный rollback требовал другой роли, а runbook не описывал тайм-аут.</p>\n<p>Сравнивайте варианты по ожидаемому риску, а не по тому, насколько убедительно они выглядят после события. <code>rollback</code> может вернуть код, но не отменить уже отправленное письмо, внешний платёж или созданную запись. Переключение флага обратимо только для маршрута, который флаг контролирует. Для необратимого состояния нужны сверка, идемпотентность или отдельная компенсация.</p>\n<p>Отдельно фиксируйте отрицательный путь. Если после остановки метрика не снижается, кто принимает следующее решение? Если повтор запроса пришёл после тайм-аута, будет ли создана вторая запись? Если сигнал пропал, какие данные считаются достаточными для остановки? Без ответов postmortem описывает намерение, но не управляемый механизм.</p>\n<h2>Безопасный пример: не обрабатывать пустой набор</h2>\n<p>Рассмотрим учебный, ограниченный случай. Сервис строит план операции по списку идентификаторов. Ошибка в контракте трактует пустой список как отсутствие фильтра, и повтор команды может затронуть все объекты. Ниже нет сети, базы и реального удаления: функция только строит план и отказывает на пустом входе. Это позволяет воспроизвести защиту локально и не выдаёт результат за production-наблюдение.</p>\n<pre><code>node <<'NODE'\nfunction buildPlan(targetIds) {\n if (!Array.isArray(targetIds) || targetIds.length === 0) {\n throw new Error('refuse empty target set');\n }\n\n return targetIds.map((id) => ({ id, action: 'delete' }));\n}\n\nfor (const [name, ids] of [['normal', ['a', 'b']], ['empty', []]]) {\n try {\n console.log(name, buildPlan(ids));\n } catch (error) {\n console.log(name, error.message);\n }\n}\nNODE</code></pre>\n<p>Запустите этот блок в Node.js без дополнительных пакетов. Ожидаемый вывод — план для <code>normal</code> и <code>empty refuse empty target set</code> для пустого входа. Проверяется только граница функции: пустой набор не превращается в широкую операцию. В настоящем сервисе дополнительно нужны авторизация, лимит размера, проверка существования объектов, журналирование решения и транзакционные правила.</p>\n<p>В postmortem такой пример связывают с фактами аккуратно. Факт: вход после повторного чтения оказался пустым. Гипотеза: библиотека или API различает «фильтр не передан» и «фильтр пуст». Проверка: контрактный тест на оба значения плюс журнал фактического набора перед побочным эффектом. Действие: отклонять пустой набор и показывать причину оператору. Локальный тест не доказывает отсутствие проблемы в базе или безопасность всего endpoint.</p>\n<h2>Превратите вывод в измеримое действие</h2>\n<p>Фраза «улучшить мониторинг» не имеет конца. Действие должно называть изменение, владельца роли, приоритет, критерий готовности и способ возврата. «Добавить метрику» тоже недостаточно: укажите имя измерения, допустимую задержку и ветку, в которой alert должен сработать. Один узкий action item лучше десятка обещаний без проверки.</p>\n<table><caption>Как сделать последующее действие проверяемым</caption><thead><tr><th scope='col'>Слабая запись</th><th scope='col'>Уточнённая запись</th><th scope='col'>Критерий</th></tr></thead><tbody><tr><td>Усилить валидацию</td><td>Отклонять пустой список до вызова операции</td><td>Тест на пустой вход завершается отказом и не вызывает побочный эффект</td></tr><tr><td>Улучшить alert</td><td>Показывать долю 5xx по маршруту и версии за пятиминутное окно</td><td>Тестовый отказ виден в метрике не позднее заданного порога</td></tr><tr><td>Обновить runbook</td><td>Добавить решение для timeout, владельца и команды возврата</td><td>Новый дежурный выбирает действие без устного пояснения</td></tr></tbody></table>\n<p>Назначьте одного владельца результата и оставьте коллабораторов в описании. Владелец не обязан лично выполнять всю работу, но отвечает за достижение критерия. После внедрения проведите проверку на том же отрицательном пути, который был в инциденте. Если критерий не выполнен, действие не закрыто, даже если код уже смёржен.</p>\n<h2>Выберите сигнал, который отвечает на вопрос</h2>\n<p>Наблюдаемость не равна коллекции панелей. Сначала сформулируйте вопрос, затем выберите сигнал. Трасса показывает путь конкретного запроса, метрика — измерение во времени, лог — запись события. Для случая с опасным повтором нужны как минимум факт набора целей и факт отказа на пустом входе; одна общая метрика ошибок не объяснит, какой набор был передан.</p>\n<p>OpenTelemetry перечисляет traces, metrics и logs как разные сигналы. Это полезная классификация, но наличие SDK не подтверждает полноту данных. Sampling может убрать нужную трассу, высокая кардинальность может сделать разрез по идентификатору дорогим, а лог без correlation id не свяжет событие с решением. В postmortem укажите эти ограничения рядом с выводом.</p>\n<h2>Порядок действий после инцидента</h2>\n<ol><li>Опишите симптом, окно времени, область воздействия и способ обнаружения.</li><li>Соберите факты с timestamp и ссылкой на источник; отметьте задержки, sampling и пробелы retention.</li><li>Отделите решение от результата: запишите, что было известно до действия и какие альтернативы существовали.</li><li>Сформулируйте одну или несколько конкурирующих гипотез, не называя их доказанной причиной без проверки.</li><li>Проверьте отрицательный путь: пустой или повторный вход, timeout, частичный ответ, потерянный сигнал и неудачный rollback.</li><li>Выберите одно обратимое действие с владельцем роли, приоритетом, критерием и точкой возврата.</li><li>Повторите проверку на том же классе отказа и запишите, что тест не покрывает.</li><li>Передайте действие в рабочий трекер и закрывайте его только после наблюдаемого критерия.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Эта схема не восстанавливает удалённые логи и не устанавливает причинность по одной корреляции. При коротком retention, sampling, ручной хронологии или неполном доступе к системе часть ответа останется неизвестной. Это не повод дополнять отчёт догадкой: расширьте сбор данных или сузьте утверждение.</p>\n<p>Blameless-подход не означает бездействие при нарушении безопасности или правил доступа. Такие случаи могут требовать отдельного процесса с необходимой конфиденциальностью. В техническом postmortem всё равно следует показать, какая граница позволила нарушению пройти и какая проверка должна его обнаруживать.</p>\n<p>Учебная функция из примера проверяет только защиту от пустого набора. Она не подтверждает безопасность endpoint, корректность транзакции, доступность сервиса, финансовый эффект или отсутствие других причин сбоя. Перед применением в production нужны контракт владельца API, тесты побочных эффектов, права, лимиты и наблюдаемая точка возврата.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://sre.google/sre-book/postmortem-culture/' target='_blank' rel='noopener noreferrer'>Google SRE Book: Postmortem Culture</a> — описывает цели postmortem, blameless-подход и связь отчёта с профилактическими действиями. Это общая практика, а не доказательство результата для конкретной команды.</li><li><a href='https://sre.google/workbook/postmortem-culture/' target='_blank' rel='noopener noreferrer'>Google SRE Workbook: Postmortem Culture</a> — показывает публичный пример с ошибкой обработки пустого набора и критерии хороших action items: владелец, приоритет и измеримый конец.</li><li><a href='https://opentelemetry.io/docs/concepts/signals/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Signals</a> — даёт официальные определения traces, metrics и logs. Страница не подтверждает покрытие или качество телеметрии конкретного проекта.</li></ul>"
|
||
}
|