diff --git a/editorial/agent-rewrites/149.json b/editorial/agent-rewrites/149.json index 0f9414b..bbef2b8 100644 --- a/editorial/agent-rewrites/149.json +++ b/editorial/agent-rewrites/149.json @@ -1,7 +1,7 @@ { "index": 149, "slug": "editorial-2023-11-mechanism-postmortem", - "title": "Postmortem без заднего знания: как связать факт, решение и действие", - "excerpt": "После сбоя команда легко принимает позднюю гипотезу за причину. Разбираем временную границу знания, контракт записей и проверяемый профилактический шаг.", - "contentHtml": "
После сбоя в чате появляется короткое объяснение: «релиз сломал обработку, поэтому инженер откатил его». В одной фразе смешаны событие, причина, решение и оценка. Но в момент отката команда могла не знать, был ли виноват релиз. Она могла видеть только рост ошибок и доступный способ остановить поток.
\nЦена ошибки — не неточная формулировка. Команда ставит защиту вокруг самого заметного элемента истории. Она добавляет проверку к релизу, хотя сбой мог возникнуть из-за данных, лимита или внешней зависимости. Следующий разбор повторяет ту же подмену. Postmortem становится рассказом задним числом, а не инструментом изменения системы.
\nРабочая модель разделяет три записи: наблюдаемый факт, решение с доступной в тот момент информацией и будущую проверку гипотезы. Время ограничивает вывод. Поздний лог может объяснить событие, но не доказывает, что этот лог был доступен оператору при выборе. Так документ сохраняет неизвестное и показывает, какое действие нужно проверить.
\nФакт описывает то, что можно привязать к источнику: время, сигнал, значение поля, изменение состояния. Он не обязан содержать причину. Запись «в 10:03 доля ответов 5xx превысила порог» сильнее записи «сервис упал из-за релиза», если связь с релизом ещё не проверена.
\nРешение описывает действие и снимок доступных фактов. Его нельзя оценивать полным набором данных, который появился позже. Иначе документ наказывает человека за информацию, которой у него не было, и скрывает вопрос к системе: почему нужный сигнал, инструкция или безопасный способ остановки не были доступны раньше.
\nЭксперимент переводит гипотезу в проверяемую работу. Он должен назвать один риск, способ проверки, критерий успеха и обратный путь. Фраза «добавить больше мониторинга» не даёт критерия. Фраза «для этого маршрута появляется alert при трёх последовательных ошибках, а дежурный подтверждает его в тестовом окружении» уже задаёт проверку формы. Она всё ещё не доказывает эффект в production.
\n| Слой | Что записать | Что не утверждать |
|---|---|---|
| Факт | Время, наблюдение и ссылка на лог, метрику или change. | «Это точно причина» без проверки связи. |
| Решение | Действие и факты, доступные до него. | Оценку через поздние данные. |
| Гипотеза | Какой механизм нужно проверить. | Причину, если она пока только предполагается. |
| Эксперимент | Критерий, владелец роли и rollback. | Обещание предотвратить любой повтор. |
У каждой записи есть occurredAt. У решения есть availableFactIds. В список попадают только факты, которые уже существовали до решения. Это простое правило удерживает границу знания. Новая запись может изменить гипотезу о причине, но не меняет набор данных, на котором приняли исходное решение.
Рассмотрим учебный пример. Он не читает реальные логи и не описывает настоящий инцидент. В нём зафиксированы три факта, решение остановить проверку изменения и эксперимент с обратным путём.
\n{\n \"facts\": [\n {\"id\": \"f-01\", \"at\": \"10:00\", \"text\": \"доля ответов 5xx выросла\", \"source\": \"metric-card-01\"},\n {\"id\": \"f-02\", \"at\": \"10:03\", \"text\": \"изменена версия конфигурации\", \"source\": \"change-02\"}\n ],\n \"decision\": {\n \"at\": \"10:05\",\n \"action\": \"остановить продвижение\",\n \"availableFactIds\": [\"f-01\", \"f-02\"]\n },\n \"experiment\": {\n \"hypothesis\": \"явная проверка версии сократит время обнаружения\",\n \"successCriterion\": \"проверка видна в тестовом сценарии\",\n \"rollback\": \"удалить проверку и вернуть прежнюю конфигурацию\"\n }\n}\nКод показывает форму, а не результат. В реальном документе source должен указывать разрешённый артефакт, который команда действительно может открыть. Время должно использовать одну часовую зону. Если источник недоступен, это нужно записать как ограничение, а не заменить догадкой.
Модель допускает, что причиной окажется не изменение версии. Например, поздняя проверка покажет исчерпанный лимит внешнего сервиса. Тогда факты и исходное решение остаются полезными. Меняется гипотеза и, возможно, эксперимент. Нельзя переписать факт так, чтобы он заранее подтверждал новую версию.
\n| Симптом | Вероятная причина записи | Проверка | Действие |
|---|---|---|---|
| В первом абзаце назван виновник. | Имя человека используется как объяснение состояния. | Убрать имя и спросить, какое условие системы нужно изменить. | Записать владельца будущего действия отдельно от причины. |
| Решение выглядит очевидным после чтения всей timeline. | К решению добавили факты, появившиеся позже. | Сравнить время решения с каждым availableFactId. | Оставить только предшествующие факты и сохранить unknown. |
| Action item звучит как «добавить мониторинг». | Гипотеза не имеет измеримого критерия. | Спросить, какой артефакт должен измениться и как увидеть проход. | Указать сигнал, порог, владельца роли и rollback. |
| После исправления обещают отсутствие повторов. | Учебная проверка выдана за production-результат. | Найти источник эффекта и период наблюдения. | Сузить вывод до «проверяет форму» или собрать реальные данные. |
Отсутствие поиска виноватого не отменяет ответственности. В документе должны быть владельцы действий, сроки и правила эскалации. Но роль владельца отвечает на вопрос «кто доведёт изменение», а не на вопрос «почему система оказалась в таком состоянии». Для второго вопроса нужны условия: доступный сигнал, версия инструкции, права, лимит, автоматическая защита или отсутствие безопасной остановки.
\nПолезно отделить две оценки. Первая: было ли действие разумным при доступной информации? Вторая: какие условия сделали такой выбор вероятным? Первая требует воспроизвести границу знания. Вторая ведёт к изменению интерфейса, runbook, алерта или архитектуры. Поздняя причина может помочь второй оценке, но не должна подменять первую.
\nОтрицательный путь важен не меньше положительного. Если источник не открывается, поле остаётся неизвестным. Если rollback нельзя выполнить безопасно, эксперимент не готов. Если критерий нельзя проверить без production-доступа, нужно сначала спроектировать безопасную проверку или признать границу. Документ не должен заполнять пробелы уверенным тоном.
\nЭта модель не заменяет incident command, расследование безопасности, юридическую оценку или правила хранения персональных данных. В security-контуре источники и доступы требуют отдельной политики. В распределённой системе часы могут расходиться, а источник может измениться после события. Тогда нужно хранить версию артефакта, часовой пояс и допустимый уровень точности.
\nТри слоя не доказывают причинность. Они только не дают написать вывод шире доступных данных. Причинную связь проверяют отдельными методами: воспроизведением, сравнением изменений, экспериментом или анализом данных. Если эти методы недоступны, корректная формулировка — «причина не подтверждена».
\nУчебный JSON выше фиксирует структуру и отрицательный путь. Он не запускается на настоящей инфраструктуре, не читает метрики и не измеряет влияние. Не переносите его идентификаторы, время и критерий в production без адаптации к своим источникам, ролям и процедурам отката.
\nPostmortem готов к техническому review, если независимый читатель может открыть источник каждого факта, увидеть, что решение ссылается только на предшествующую информацию, и проверить один профилактический эксперимент по его критерию. Если хотя бы один пункт не выполняется, статус должен быть «не готов», а следующий шаг — устранение конкретного пробела: источник, временная граница, критерий или rollback.
\nПроблема postmortem часто начинается с одной убедительной фразы: «релиз сломал обработку, поэтому инженер его откатил». В ней сразу смешаны событие, причина, решение и оценка человека. После инцидента команда уже знает больше, чем в момент отката, и легко принимает позднее объяснение за причину, которую можно было увидеть заранее.
\nТакой разбор плохо меняет систему. Команда ставит новый алерт на самый заметный объект, хотя сбой мог вызвать лимит партнёра, неполный runbook или неизвестный формат данных. Следующий инцидент получает тот же набор объяснений. Рабочая модель должна разделить факт, решение в моменте и следующую проверяемую гипотезу.
\nPostmortem — это запись события, влияния, действий по смягчению и последующих изменений. Это не стенограмма поиска виноватого и не доказательство того, что одна правка устранит все будущие отказы. В официальном описании Google SRE цель такого документа — сохранить данные об инциденте, разобраться в способствующих причинах и поставить профилактические действия. Конкретный шаблон и пороги команда выбирает сама.
\nТермин blameless здесь означает отсутствие обвинения людей за решения, принятые с доступной им информацией. Ответственность не исчезает: у каждого действия есть владелец, срок и критерий. Меняется предмет разговора. Вместо «кто допустил ошибку?» появляются вопросы «какой сигнал был доступен?», «какая инструкция действовала?» и «какое системное ограничение подтолкнуло к этому выбору?».
\n| Слой | Минимальное содержимое | Что можно утверждать | Чего пока нельзя утверждать |
|---|---|---|---|
| Факт | Время, наблюдение, источник и идентификатор. | Событие зафиксировано указанным источником. | Факт сам по себе доказывает причинность. |
| Решение | Действие и факты, доступные до него. | Команда выбрала действие при данном контексте. | Выбор был очевидным после появления поздних данных. |
| Гипотеза | Предполагаемый механизм и способ проверки. | Назван вопрос для следующего эксперимента. | Гипотеза уже является подтверждённой причиной. |
| Action item | Изменение, владелец роли, критерий и откат. | Понятно, какой артефакт должен измениться. | Изменение гарантирует отсутствие повторения. |
Для каждого факта введём occurredAt и source. Для решения сохраним decidedAt и список availableFactIds. Правило простое: факт можно связать с решением только тогда, когда его время не позже времени решения. Если запись появилась после отката, она может изменить гипотезу о механизме, но не должна притворяться частью исходного контекста.
Время нужно хранить в одной зоне или в явном формате UTC. Для распределённых систем одной отметки мало: полезно указать точность часов и версию источника. Если dashboard пересчитывает прошлый период, сохраните запрос или снимок, иначе читатель не восстановит, что именно было видно дежурному.
\nНиже — самостоятельный учебный пример. Он не читает логи, не обращается к сети и не доказывает, что версия конфигурации вызвала рост ошибок. Скрипт проверяет только внутреннее правило временной границы: ссылка решения не может указывать на факт из будущего.
\nnode <<'NODE'\nconst facts = [\n { id: 'f-01', occurredAt: '2023-11-15T10:00:00Z', text: '5xx выше порога', source: 'metric-snapshot-01' },\n { id: 'f-02', occurredAt: '2023-11-15T10:03:00Z', text: 'обновлена конфигурация', source: 'change-record-02' },\n { id: 'f-03', occurredAt: '2023-11-15T10:06:00Z', text: 'лимит партнёра исчерпан', source: 'partner-status-03' },\n];\nconst decision = {\n decidedAt: '2023-11-15T10:05:00Z',\n action: 'остановить продвижение изменения',\n availableFactIds: ['f-01', 'f-02'],\n};\nconst known = new Map(facts.map((fact) => [fact.id, fact]));\nconst future = decision.availableFactIds.filter((id) => {\n const fact = known.get(id);\n return !fact || fact.occurredAt > decision.decidedAt;\n});\nif (future.length) throw new Error('future facts: ' + future.join(', '));\nconsole.log('PASS: decision uses facts available at decision time');\nNODE\n\n# Ожидаемый вывод:\n# PASS: decision uses facts available at decision time\nВ примере f-03 появляется в 10:06 и намеренно не входит в решение в 10:05. Позднее он может поддержать другую гипотезу — например, что главным ограничением был внешний лимит. Но это не превращает исходный откат в ошибку и не позволяет переписать его контекст задним числом.
Проверка не валидирует правдивость источников. Строка source лишь связывает запись с ожидаемым артефактом. На рабочем проекте нужно отдельно проверить доступ, сохранность и авторство метрики, change record или лога. Идентификаторы из примера нельзя переносить в production.
Временная последовательность сужает поиск, но не устанавливает причинность. Если конфигурация изменилась перед ростом 5xx, остаются альтернативы: тот же период мог совпасть с пиком трафика, исчерпанием квоты или изменением у партнёра. В postmortem полезно хранить эти альтернативы, пока проверка не отсеет их.
\n| Наблюдение | Гипотеза | Проверка | Действие после результата |
|---|---|---|---|
| 5xx выросли сразу после изменения. | Изменение несовместимо с частью входных данных. | Сравнить фиксированный набор запросов до и после, не используя персональные данные. | Добавить тест на найденный формат или отклонить гипотезу. |
| Ошибки совпали с ростом трафика. | Ресурсный лимит ниже фактической нагрузки. | Сверить rate, quota и saturation в одном временном окне. | Ограничить нагрузку или изменить capacity после review. |
| Дежурный узнал о сбое вручную. | Сигнал не покрывает пользовательский симптом. | Проверить alert на синтетическом сценарии и его маршрут доставки. | Изменить сигнал, порог или инструкцию, сохранив rollback. |
| Источник появился после решения. | Причина реконструирована позднее. | Убрать ссылку из контекста решения и обозначить unknown. | Поставить отдельный эксперимент для проверки механизма. |
Формулировка «причина не подтверждена» не является провалом документа. Это точная граница знания. Неподтверждённая причина лучше красивого, но ложного вывода: она показывает, какую телеметрию, тест или безопасный эксперимент нужно добавить.
\n«Добавить мониторинг» — намерение, а не действие. Запись становится проверяемой, если в ней названы изменяемый артефакт, владелец роли, критерий и обратный путь. Например: «до следующего тестового релиза добавить alert на долю 5xx для маршрута X; критерий — synthetic-запрос вызывает сигнал за пять минут; откат — удалить правило и вернуть прежний порог». Такой пункт проверяет наличие защиты, но ещё не доказывает снижение production-инцидентов.
\nПолезно разделить исправление и измерение. Исправление меняет код, конфигурацию, runbook или процесс. Измерение показывает, сработала ли защита в выбранном окне. Для измерения нужно назвать baseline, вариант сравнения и длительность наблюдения. Без них фраза «ошибок стало меньше» неотличима от впечатления.
\nУ каждого action item есть стоимость и риск. Алерт дешевле изменения протокола, но создаёт шум, если команда не определила владельца реакции. Регрессионный тест ловит известный класс входа раньше production, но не покрывает неизвестный внешний отказ. Перечислите альтернативы и выберите одну по риску, обратимости и доступному доказательству.
\nЭта модель не заменяет incident command, расследование безопасности, юридическую оценку или правила работы с персональными данными. В security-контуре доступ к логам, копирование доказательств и сроки хранения задаются отдельной политикой. Руководство NIST SP 800-61 Rev. 2 относится к компьютерным инцидентам безопасности и не является универсальным шаблоном для любой деградации продукта.
\nВ распределённой системе часы могут расходиться, логи могут быть потеряны, а dashboard — пересчитать историю. Тогда честная запись должна указать неопределённость и версию источника. Если нельзя безопасно воспроизвести сценарий, не называйте учебную проверку экспериментом в production. Если откат сам создаёт риск, сначала получите одобрение и подготовьте промежуточный защитный шаг.
\nBlameless-подход также не отменяет расследование умышленного нарушения, контроля доступа или требований закона. Он отвечает на другой вопрос: как извлечь технический урок из условий, в которых система и люди действовали. Для дисциплинарных и юридических решений нужна отдельная процедура с соответствующими полномочиями.
\nРазбор можно отдавать на технический review, если независимый читатель открывает источник каждого факта, видит границу между временем решения и поздними находками, понимает, какая гипотеза ещё не доказана, и может проверить один action item по критерию и rollback. Если не хватает источника, временной связи или способа измерения, статус должен оставаться «не готов», а следующий шаг — устранение конкретного пробела.
\nПосле сбоя команда обычно помнит две вещи: какой сигнал сработал и кто последним менял систему. На встрече эти детали быстро превращаются в объяснение: «ошибка произошла из-за этого изменения». Такой вывод может быть неверным. Он смешивает факт, решение и гипотезу о причине.
\nЦена ошибки высока. Команда тратит время на защиту вокруг случайного признака, а настоящий механизм остаётся без проверки. Следующий дежурный получает документ с обвинением и общим советом «быть внимательнее». Похожий сбой повторяется, но его уже труднее разобрать: нужные логи истекли, контекст решения забыт, а исправление объявили готовым без измерения.
\nРабочая схема проста: сначала записать симптом и его цену, затем собрать факты с источниками, отдельно описать решение в моменте и только после этого сформулировать небольшую профилактическую проверку. Postmortem не должен угадывать причину по одной строке лога. Он должен показывать, что известно, чего не известно и какое действие уменьшит неопределённость.
\nФакт отвечает на вопрос «что было зафиксировано и где это видно?». Это узкое утверждение с временем и ссылкой на разрешённый артефакт: лог, trace, метрику, версию конфигурации или запись мониторинга. Фраза «в 10:03 запросы к маршруту получили 502» может быть фактом, если рядом есть запрос к источнику и задано окно времени.
\nРешение отвечает на вопрос «что команда сделала, имея такую информацию?». В него входят время, действие и список фактов, доступных до действия. Поздний trace или результат расследования нельзя добавлять в этот список задним числом. Он помогает понять выбор, но не доказывает, что выбор был правильным или ошибочным.
\nПрофилактическая проверка отвечает на вопрос «что мы проверим, чтобы уменьшить риск повторения?». В ней нужны гипотеза, минимальный метод, бинарный критерий и обратимый путь. До прогона это намерение проверить, а не доказательство того, что новый alert, лимит или тест уже защитил пользователей.
\n| Тип | Вопрос | Минимальные поля | Нельзя выводить |
|---|---|---|---|
| Факт | Что зафиксировано? | время, наблюдение, ссылка на источник | виновника и root cause |
| Решение | Что выбрали тогда? | время, действие, доступные fact ID | оценку задним числом |
| Проверка | Что проверим дальше? | гипотеза, метод, критерий, rollback | реальный эффект до измерения |
| Действие | Кто доведёт работу? | владелец роли, срок, ссылка на проверку | обещание устранить все риски |
Представим учебный инцидент в HTTP-сервисе. После релиза доля ответов 502 выросла. Дежурный откатил конфигурацию таймаута. Через несколько минут доля ошибок снизилась. Этого недостаточно, чтобы написать «новый таймаут был причиной». За это время могли исчезнуть входной всплеск, зависший upstream или другая ошибка маршрутизации.
\nСначала запишите наблюдения:
\nconst facts = [\n { id: 'f-1', at: '10:03', text: 'gateway reported 502 for /checkout', source: 'metric:gateway_5xx' },\n { id: 'f-2', at: '10:04', text: 'timeout config was version 17', source: 'config:checkout@17' },\n { id: 'f-3', at: '10:05', text: 'on-call restored version 16', source: 'change:rollback-482' },\n];\n\nconst decision = {\n at: '10:05',\n action: 'restore checkout config to version 16',\n availableFactIds: ['f-1', 'f-2'],\n};\n\nconst check = {\n hypothesis: 'the timeout change contributes to the 502 path',\n method: 'replay the same request class with versions 16 and 17',\n criterion: 'both outcomes and upstream status are captured',\n rollback: 'keep version 16 and stop the replay if error rate rises',\n};\nПример учебный. Он не читает настоящие метрики, не запускает rollback и не доказывает связь между таймаутом и 502. Его польза в форме: у решения видны только два доступных факта, а проверка имеет отдельный критерий и остановку. Если команда позже найдёт новый trace, его добавят в факты и пересмотрят гипотезу, но не перепишут историю доступной информации.
\nНужна и отрицательная ветка. Если в карточке факта появилось поле rootCause, система или ревью должны остановить запись. Если решение ссылается на факт, который возник позже, его нельзя считать контекстом решения. Если проверка говорит «сбой больше не повторится», но не называет вход, окно и измерение, это обещание, а не критерий.
| Симптом | Вероятная причина смешения | Проверка | Действие |
|---|---|---|---|
| В черновике есть имя инженера, но нет источников | оценка человека заменяет анализ условий | найти timestamp и evidence для каждой фразы | убрать имя из объяснения, добавить владельца следующего действия |
| «Релиз вызвал ошибку» написано как факт | гипотеза попала в timeline | сравнить время релиза, симптома и альтернативные изменения | пометить связь как непроверенную и сформулировать эксперимент |
| Action item звучит как «добавить мониторинг» | нет сценария и порога срабатывания | назвать вход, сигнал, окно и ожидаемое значение | сделать критерий бинарным и указать обратное действие |
| После отката написано «проблема решена» | снижение симптома приняли за доказательство причины | сопоставить ошибку с upstream, версиями и временем | описать откат как mitigation, а причину оставить открытой |
| Документ нельзя проверить через неделю | в нём остались воспоминания без артефактов | проверить каждое утверждение по ссылке и сроку хранения | сохранить минимальный разрешённый evidence или отметить пробел |
Временная граница нужна не для бюрократии. Она защищает от hindsight bias: после сбоя команда видит больше, чем видела в момент действия. Поэтому в записи решения храните не весь итоговый материал, а именно набор сведений, который мог повлиять на выбор. Если действие необратимо, отдельно запишите владельца точки возврата и сигнал остановки.
\nТакая схема не заменяет расследование распределённой системы. Один trace может не показать потерю сообщения. Откат может убрать симптом, но оставить повреждённые данные. Метрика может считать только успешные запросы и скрывать ошибки до входа в сервис. Поэтому границы источников и неполные данные нужно писать прямо.
\nBlameless не означает «никто ни за что не отвечает». Документ должен содержать владельца действия, срок и критерий завершения. Он также должен фиксировать небезопасное изменение, нарушенный контроль или отсутствие доступа к сигналу, если это подтверждено. Не следует приписывать человеку мотив или использовать его имя как техническую причину.
\nУчебный код выше не подключается к сети, CI, логам, alert-системе или production runtime. Его нельзя выдавать за результат прогона. В реальной системе доступ к incident data, приватность, retention и право публикации требуют отдельной проверки. Если источник недоступен, честная запись — «не проверено», а не правдоподобная реконструкция.
\nPostmortem готов к разбору, когда другой инженер может пройти его без устного пересказа:
\nПроверка готовности не утверждает, что система стала надёжнее. Она утверждает более узкую вещь: документ сохраняет границу знания и задаёт следующий эксперимент, который можно проверить. После прогона обновите запись фактическим результатом, источником и новой оценкой риска.
\nПосле сбоя команда быстро находит удобную историю: «ошибка началась после релиза, значит, релиз всё сломал». История может оказаться неверной. Она смешивает наблюдение, решение дежурного и позднюю гипотезу о причине. В результате следующий инженер получает уверенный текст, но не получает способ проверить его.
\nЦена такого postmortem — не только повторный простой. В документе теряется контекст: какие сигналы были доступны в момент решения, почему выбрали откат, какие данные ещё не проверены и что именно должно изменить систему. Обсуждение смещается к имени последнего автора изменения, а не к слабому контролю, неясному лимиту или отсутствующему сигналу.
\nНиже — практический маршрут для небольшого HTTP-инцидента. Он не выдаёт учебный сценарий за production-расследование. Его задача — помочь записать факты, связать действие с доступной информацией и назначить следующую проверку так, чтобы другой инженер мог воспроизвести её без устного пересказа.
\nПервая запись должна описывать наблюдаемое событие, а не объяснение. Назовите путь или компонент, окно времени, сигнал и последствия для пользователя. «С 10:03 до 10:08 шлюз вернул 502 на 18% запросов к /checkout» уже можно проверять по метрике. «Новый таймаут сломал оплату» — пока гипотеза.
Цена ошибки нужна, чтобы выбрать глубину разбора. Ею может быть число недоступных операций, объём ручного восстановления, потеря данных или задержка обработки. Не подменяйте измерение оценкой: если количество затронутых запросов неизвестно, так и напишите и назначьте способ его посчитать.
\n| Запись | На какой вопрос отвечает | Минимальные поля | Граница вывода |
|---|---|---|---|
| Симптом | Что увидел пользователь или мониторинг? | путь, окно, сигнал, воздействие | не объясняет причину |
| Факт | Что подтверждено источником? | timestamp, наблюдение, ссылка на лог, метрику или конфигурацию | не назначает виновника |
| Решение | Что сделали с доступной информацией? | время, действие, список доступных fact ID, обратимость | не доказывает, что решение устранило причину |
| Проверка | Как отделить гипотезу от альтернативы? | вход, метод, критерий PASS/FAIL, остановка, владелец | не обещает результат до прогона |
В postmortem полезно различать две временные шкалы. Первая — события системы: ошибка, изменение конфигурации, запрос к upstream, откат. Вторая — знания команды: что было видно до решения и что выяснилось уже после. Поздний trace может объяснить механизм, но не был основанием для решения, принятого пять минут раньше.
\nДля каждого события храните источник и время, а для решения — список фактов, которыми дежурный действительно располагал. Это защищает от hindsight bias, то есть от подмены прежней неопределённости знанием, появившимся позже. Так команда оценивает не личную «внимательность», а качество сигналов и доступных процедур.
\nВозьмём изолированный пример. В 10:03 шлюз заметил рост 502 на /checkout. В 10:04 команда увидела, что сервис использует версию конфигурации 17 с новым таймаутом. В 10:05 дежурный вернул версию 16. В 10:08 доля 502 снизилась. Последнее наблюдение подтверждает эффект отката во времени, но не доказывает, что именно таймаут был единственной причиной: мог закончиться всплеск нагрузки, восстановиться upstream или исчезнуть другая ошибка маршрутизации.
Следующий фрагмент можно запустить локально: ему нужен только Node.js. Он проверяет узкое правило — решение не может ссылаться на факт, появившийся позже. Команда не обращается к логам и не меняет конфигурацию, поэтому результат запуска не является доказательством состояния настоящего сервиса.
\nnode <<'NODE'\nconst facts = [\n { id: 'f-1', at: '2023-11-07T10:03:00Z', text: 'gateway returned 502 for /checkout', source: 'metric://gateway-5xx' },\n { id: 'f-2', at: '2023-11-07T10:04:00Z', text: 'checkout config is version 17', source: 'config://checkout/17' },\n { id: 'f-3', at: '2023-11-07T10:08:00Z', text: '502 share fell after rollback', source: 'metric://gateway-5xx' },\n];\nconst decision = {\n at: '2023-11-07T10:05:00Z',\n action: 'restore checkout config to version 16',\n availableFactIds: ['f-1', 'f-2'],\n};\n\nconst byId = new Map(facts.map((fact) => [fact.id, fact]));\nfor (const factId of decision.availableFactIds) {\n const fact = byId.get(factId);\n if (!fact || new Date(fact.at) >= new Date(decision.at)) {\n throw new Error('Fact ' + factId + ' was not available at decision time');\n }\n}\nconsole.log('PASS: decision uses only earlier facts');\nconsole.log(JSON.stringify({ facts, decision }, null, 2));\nNODE\nВ shell этот блок завершится строкой PASS и напечатает запись. Если заменить список availableFactIds на ['f-1', 'f-3'], проверка завершится ошибкой: факт о снижении ошибок возник после решения. Это и есть полезный отрицательный тест. Он не доказывает root cause, зато не позволяет задним числом приписать дежурному знание, которого у него не было.
Для реального расследования к этой структуре добавьте идентификатор инцидента, часовой пояс, единицы измерения и ссылки с понятным сроком хранения. Источник должен вести к разрешённому артефакту. Ссылка на поиск в логах без фиксированного окна и запроса через неделю может вернуть уже другой результат.
\nОткат — это mitigation: действие, которое уменьшает воздействие прямо сейчас. Причина — объяснение механизма, подтверждённое несколькими наблюдениями или безопасным экспериментом. Между ними может быть связь, но её нельзя объявлять установленной только потому, что симптом ослаб после отката.
\n| Наблюдение | Что пока нельзя утверждать | Проверка | Действие и критерий |
|---|---|---|---|
| 502 вырос после релиза | релиз — единственная причина | сопоставить версии, трафик, upstream-коды и соседние изменения в одном окне | сохранить откат как mitigation; PASS — все сравниваемые источники согласованы |
| ошибки снизились после возврата конфигурации | новый таймаут доказан как root cause | повторить на тестовом трафике тот же класс запросов с версиями 16 и 17 | остановить эксперимент при росте 5xx; PASS — записаны ответ шлюза и статус upstream для каждой попытки |
| в логе есть имя инженера | человек объясняет техническую причину | проверить, какой контроль разрешил изменение и какая информация была доступна | заменить имя в причинной цепочке на изменение, контроль и владельца follow-up |
| в отчёте написано «добавить мониторинг» | новый сигнал предотвратит повтор | назвать маршрут, окно, порог, канал и тест тревоги | PASS — тестовый сигнал срабатывает на искусственном нарушении; FAIL — открыть действие с rollback |
Гипотеза должна быть узкой: «при одинаковом классе запроса версия 17 увеличивает долю 502 из-за таймаута ожидания upstream». Она лучше общего «конфигурация ненадёжна», потому что задаёт входы и наблюдения. Альтернатива тоже нужна: например, «502 вызван исчерпанием соединений независимо от таймаута». Иначе эксперимент будет подтверждать любимое объяснение, а не различать варианты.
\nСтрока «разобраться с таймаутами» не является планом. Хорошая corrective action меняет контроль или код и имеет наблюдаемый критерий. В ней должны быть один владелец, срок, ссылка на место изменения, способ проверки и обратное действие. Если результат нельзя выразить через PASS/FAIL, сначала уточните, какой артефакт должен появиться.
\nAction: add a bounded upstream-timeout regression test\nOwner: checkout-service team\nInput: the recorded /checkout request class from incident INC-482\nPASS: versions 16 and 17 produce captured gateway and upstream statuses;\n the test fails when the timeout exceeds the contract.\nRollback: keep version 16 until the test and staged replay are green.\nEvidence: test run URL + configuration revision + timestamp.\nПример описывает будущую проверку, а не уже достигнутый результат. До запуска нельзя писать «тест защитил пользователей». После запуска сохраните фактический статус, версию теста и условия прогона. Если staged replay не повторяет production-нагрузку или upstream недоступен, результат ограничен именно этой средой.
\nPostmortem стоит запускать по заранее известным признакам, а не только по субъективному ощущению масштаба. Google SRE перечисляет среди типовых триггеров заметную деградацию для пользователей, потерю данных, вмешательство дежурного вроде отката или перенаправления трафика, длинное восстановление и отказ мониторинга. Это ориентир, а не обязательный порог для каждой команды: его нужно сопоставить с риском сервиса, договорённостями об уровне доступности и правилами приватности.
\nДо инцидента зафиксируйте, кто открывает документ, где лежит timeline, кто делает технический review и что считается закрытым follow-up. После инцидента сначала сохраните артефакты с нужным сроком хранения, затем попросите review у людей, которые могут проверить полноту воздействия, глубину причины и реалистичность действий. Не закрывайте запись лишь потому, что пользовательский симптом исчез.
\nBlameless не означает отсутствие ответственности. Такой подход убирает обвинение человека из причинной цепочки, но оставляет владельца действия, срок, контроль и критерий завершения. Если изменение нарушило правило доступа или процесс, документируйте сам факт нарушения и слабость контроля; не приписывайте мотив, не публикуйте лишние персональные данные и не превращайте имя в техническое объяснение.
\nВ распределённой системе один trace не равен полной истории. Сообщение могло потеряться до входа в сервис, часы на узлах могли расходиться, а метрика шлюза могла не учитывать ошибки до него. Откат может убрать симптом и оставить повреждённые данные. Поэтому для операций записи отдельно проверяйте целостность, повторяемость и идемпотентность; для приватных инцидентов — доступ, retention и допустимый объём публикации.
\nУчебный Node.js-код проверяет только порядок двух timestamp. Он не подключается к HTTP, не читает production-метрики, не выполняет откат и не устанавливает причинность. Приведённые имена маршрута, версии и проценты вымышлены для воспроизводимости. В своём проекте замените их реальными источниками и укажите версию среды: синтетический прогон на Node.js не подтверждает поведение конкретного шлюза, базы данных или провайдера.
\nГотовность postmortem — это не обещание, что инцидент больше не повторится. Более узкий и честный критерий таков: инженер, не участвовавший в сбое, видит, что произошло, что было известно в момент решения, какое объяснение ещё проверяется и какое действие даст следующий измеримый сигнал.
\nВ день релиза на панели краснеет error budget. Один инженер говорит: «бюджет почти закончился». Другой просит не задерживать исправление. On-call не может показать, какой пользовательский путь пострадал, за какой период считался показатель и какие события попали в знаменатель. Команда спорит о цвете, а не о данных.
\nЦена ошибки двойная. Шумный сигнал может остановить безопасное изменение. Слишком узкий SLI может пропустить отказ после точки измерения, и команда выпустит рискованный релиз. В обоих случаях SLO превращается в отчётность: число есть, но оно не подсказывает следующий безопасный шаг.
\nТезис статьи простой: error budget не принимает решение вместо команды. Он запускает проверяемую петлю. Сначала нужно подтвердить договор SLI: что измеряем, для кого, в каком окне и по какой формуле. Затем нужно проверить контекст и выбрать действие по policy. Только после этого можно обсуждать rollout, паузу или исправление.
\nSLI — количественная мера свойства сервиса. Например, доля запросов, которые завершились полезным результатом, или доля операций с задержкой ниже порога. SLO — целевое значение этой меры в заданных условиях. Error budget — допустимая часть неуспеха в том же договоре. Если target равен 99%, бюджет равен 1% eligible-событий за указанное окно.
\nФормула сама по себе ничего не решает. Для success ratio нужны как минимум scope, eligible count, good count, target и window. Scope задаёт путь пользователя и границу ответственности. Eligible определяет знаменатель. Good определяет успешный исход. Window задаёт период сравнения. Policy связывает состояние бюджета с действием и владельцем.
\neligible = 1000\ngood = 994\ntarget = 0.99\nactual = good / eligible // 0.994\nallowed_bad = eligible * (1 - target) // 10\nactual_bad = eligible - good // 6\nremaining = allowed_bad - actual_bad // 4\n\n// Все числа учебные. Источник событий отсутствует.\n// Результат не описывает production-доступность.\nВ этом примере остаются четыре условные единицы бюджета. Это арифметика модели, а не факт о сервисе. Если исключить отменённые операции, изменить окно или считать только ответы одного backend, результат станет другим. Поэтому процент без версии договора нельзя сравнивать с прошлым процентом и нельзя использовать как самостоятельную причину для блокировки.
\nПользователь оценивает путь, а не внутренний HTTP-ответ. Запрос может получить код 202, но очередь позже отклонит операцию. Backend может ответить быстро, пока клиент ждёт подтверждение в другом компоненте. Если SLI измеряет только первый ответ, он может быть технически точным и продуктово бесполезным.
\nСначала назовите действие пользователя: например, «отправить заказ и получить подтверждение». Затем определите границу: где путь считается завершённым, какие отказы входят в оценку, кто владеет источником событий. Если путь нельзя связать с наблюдаемым результатом, не объявляйте готовый SLO. Сначала сократите вопрос или добавьте нужный сигнал.
\nЗнаменатель также требует явного правила. Eligible-события нельзя выбирать по удобству. Если фильтр исключает таймауты, повторные попытки или отмены, запишите причину и отрицательный пример. Иначе команда улучшит процент удалением сложных случаев. Это не повышение надёжности, а изменение измеряемой популяции.
\nФиксированное окно проще объяснить: события с 1 по 28 число сравниваются с предыдущим таким же периодом. Скользящее окно быстрее показывает недавнее ухудшение, но каждый момент измерения содержит немного иной набор событий. В обоих вариантах нужно назвать часовой пояс, границы, задержку поступления событий и правило пересчёта.
\nНизкий трафик усиливает цену одного отказа. При десяти eligible-событиях один failure меняет ratio сильнее, чем при миллионе. Это не означает, что малотрафиковый путь нельзя измерять. Это означает, что порог, окно и способ реакции надо выбирать вместе. Иногда полезнее ticket и ручной разбор, чем срочное оповещение на каждое колебание.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Процент стал красным | Неизвестны окно и версия формулы | Сверить SLI-contract, границы и период | Приостановить интерпретацию, запросить источник |
| Команды считают доступность по-разному | Разные eligible и good | Сравнить запросы, исключения и отрицательные случаи | Зафиксировать одну формулу и владельца |
| Процент хороший, путь сломан | SLI измеряет ранний backend-ответ | Пройти пользовательский сценарий до результата | Расширить scope или добавить отдельный SLI |
| Один отказ резко изменил ratio | Малое окно или низкий трафик | Посчитать eligible и проверить распределение событий | Выбрать устойчивое окно и ручной response |
| Красный график блокирует любой релиз | У policy нет исключений и владельца | Проверить обратимость, срочность и evidence | Сузить rollout, исправить, отложить или продолжить по policy |
Policy должна отвечать на пять вопросов. Какое состояние бюджета запускает разбор? Какие данные обязан принести владелец? Какое действие обратимо? Кто принимает решение? Когда команда пересматривает договор? Запись «при красном графике остановить всё» не отвечает ни на один вопрос до конца.
\nПрактичная ветка может выглядеть так: если формула или scope неизвестны, решение откладывают до проверки данных. Если договор подтверждён, но причина неясна, открывают разбор и уменьшают exposure рискованного изменения. Если budget exhausted, non-urgent rollout приостанавливают, а обязательное исправление оценивают отдельно с владельцем и планом отката. Если сигнал восстановился, повторяют ту же проверку; новый процент не должен появиться из другой формулы.
\nТакая policy не запрещает каждый релиз. Она не разрешает и каждый релиз. Она задаёт минимальное evidence и оставляет полномочие у владельца. Security fix, изменение инфраструктуры и продуктовый rollout могут иметь разные уровни срочности, поэтому один универсальный gate создаёт ложную уверенность.
\nОдин SLI не объясняет корневую причину. Trace, log и dashboard помогают только тогда, когда они относятся к той же операции, версии и периоду. Корреляция не доказывает причинность. Красный budget не доказывает инцидент. Зелёный budget не доказывает, что весь пользовательский путь работает.
\nУчебная арифметика выше не читает файлы, часы, monitoring, CI, сеть, HTTP или production-конфигурацию. Она не создаёт alert, не меняет rollout и не выдаёт разрешение на выпуск. В реальной системе эти полномочия должны находиться в явно назначенных инструментах и runbook. Если команда не может проверить источник или обратить действие, это ограничение нужно записать до решения.
\nРазбор готов, когда другой инженер без устного контекста может воспроизвести число и понять решение. В записи есть пользовательский путь, версия формулы, eligible и good, target, окно, источник, владелец, выбранное действие, план отката и повторная проверка. Есть хотя бы один отрицательный пример: событие, которое нельзя молча исключить, или путь, который текущий SLI не покрывает.
\nРелизный разговор также готов, если команда может ответить на три вопроса: что измеряем, почему этому сигналу можно доверять в данном решении и что произойдёт при ухудшении. Если на любой вопрос отвечает только цвет графика, договор ещё не готов.
\nНа релизе график error budget становится красным. Один инженер предлагает остановить выкладку, другой просит не задерживать исправление. Но никто не может сразу ответить, какой пользовательский путь измеряет график, какие события входят в знаменатель и за какое окно посчитан расход. В итоге команда обсуждает цвет панели вместо проверяемого факта.
\nОшибка в такой ситуации стоит дорого в обе стороны. Слабый SLI может оставить сломанным путь, который сервер формально считает успешным. Нечёткая policy может остановить безопасное изменение из-за одного нерепрезентативного всплеска. Поэтому SLO полезен не как печать «можно» или «нельзя», а как часть петли: измерили, сравнили с целью, проверили контекст, выбрали действие, повторили измерение.
\nНиже — практический способ провести этот разговор. Сначала зафиксируем контракт показателя, затем разберём знаменатель и окно, после чего превратим расход бюджета в ограниченное и обратимое решение. Все числа в примерах учебные: они показывают арифметику и порядок проверки, но не описывают конкретную production-систему.
\nSLI (service level indicator) — количественная мера свойства сервиса: например, доля успешных запросов или доля запросов, завершившихся быстрее порога. SLO (service level objective) — целевое значение SLI при явно названных условиях. Error budget — допустимая часть неуспеха за то же окно. Для цели 99% это 1% событий, которые могут не соответствовать критерию, если договор считает их одинаково.
\nМинимальный контракт должен назвать пользовательский scope, способ измерения, eligible-события, good-события, target и window. Scope говорит, какой результат защищаем. Eligible задаёт знаменатель. Good задаёт числитель. Window задаёт период, в котором результат сравнивают с целью. Policy добавляет владельца и действие, но не меняет саму формулу.
\neligible = 1000\ngood = 994\ntarget = 0.99\nactual = good / eligible // 0.994 = 99.4%\nallowed_bad = eligible * (1 - target) // 10\nactual_bad = eligible - good // 6\nremaining_bad_capacity = allowed_bad - actual_bad // 4\n\n# Учебные числа: здесь нет источника событий и реального окна.\nВ учебной модели осталось место ещё для четырёх неуспешных событий до цели 99%. Это не означает, что сервис «на 99,4% надёжен» во всех смыслах: код 202 может лишь поставить операцию в очередь, а клиентский JavaScript может сломаться после ответа API. Если изменить exclusions, источник или границу завершения, изменятся eligible и good, а вместе с ними — весь вывод.
\nСерверная метрика удобна, но удобство не делает её пользовательским SLI. Запрос «создать заказ» может получить 202, а очередь позднее отклонит заказ. Или backend ответит за 80 мс, пока браузер ждёт загрузки скрипта и не показывает подтверждение. Такой backend-SLI честно описывает свою точку измерения, но не весь путь.
\nФормулировка должна начинаться с действия: «пользователь отправляет заказ и видит подтверждение». Затем задайте границу завершения: получение 202, появление записи в заказах или видимый экран подтверждения. Выберите источник, который действительно видит эту границу: серверный лог, black-box проверка или клиентская телеметрия. У каждого способа своя цена покрытия и сопровождения.
\nНе смешивайте спецификацию и реализацию. Спецификация может звучать как «доля заказов, подтверждённых не позднее пяти минут». Реализация через лог API не увидит отказы до backend; реализация через браузерный synthetic-проверяющий охватит доступность пути, но может не отражать всех клиентов. В договоре нужно записать, какой пробел принят и зачем.
\nEligible — не «все записи, которые удобно посчитать», а заранее определённая популяция. Таймауты, повторные попытки, отмены и некорректные входы нельзя молча выкинуть только потому, что они ухудшают процент. Если событие исключается, запишите техническую причину, владельца правила и отрицательный пример. Иначе команда улучшает отчёт, меняя объект измерения.
\nОкно тоже часть контракта. Скользящее окно сохраняет недавний сбой в расчёте и не обнуляет его в начале календарного месяца. Календарное окно удобнее для отчётности и планирования, но посреди периода сложнее оценить, сколько трафика ещё поступит. Для обоих вариантов укажите часовой пояс, границы, задержку событий и правило пересчёта.
\nНа малом трафике один отказ заметно меняет ratio. Это не запрет на SLO, а причина не строить срочный автоматический вывод на малой выборке. Можно увеличить окно, поднять минимальное число eligible-событий для page или отправлять такой сигнал в ticket на ручной разбор. Порог реакции выбирается вместе с формулой, а не после неё.
\n| Наблюдение | Возможная причина | Проверяем | Следующее действие |
|---|---|---|---|
| Красный budget, но неизвестна формула | Смешаны окно, exclusions или версия запроса | Сверяем SLI-контракт и исходные выборки | Не принимать релизное решение до восстановления evidence |
| Команды получили разные проценты | Разные eligible, good или часовой пояс | Сравниваем запрос, период, фильтры и повторные попытки | Назначаем владельца одной версии расчёта |
| Зелёный SLI, но путь не работает | Точка измерения раньше пользовательского результата | Проходим сценарий до подтверждения | Расширяем scope или добавляем отдельный клиентский SLI |
| Один отказ сильно изменил процент | Малое окно или низкий трафик | Считаем объём и распределение событий | Меняем окно или переводим сигнал в ручной разбор |
| Красный график блокирует любой релиз | Policy не различает срочность и обратимость | Проверяем blast radius, rollback и тип изменения | Сужаем rollout, откатываемся или продолжаем с контролем |
Policy — это не фраза «при красном остановить всё». В ней должны быть условие, evidence, владелец и действие. Например, при неизвестной формуле команда сначала восстанавливает источник данных. При подтверждённом расходе и неясной причине уменьшает долю трафика для нового релиза и назначает разбор. При исчерпанном бюджете откладывает несрочный rollout, но отдельно рассматривает security fix или исправление причины с явным планом отката.
\nПрогрессивный rollout ограничивает blast radius, но не исправляет плохой SLI. На каждом этапе задайте длительность наблюдения, минимальный объём событий, критерий остановки и способ вернуть предыдущую версию. Ручное решение остаётся важным: одинаковый расход может означать известную деградацию, ошибку сбора или новую проблему после выкладки.
\nПосле действия повторите расчёт на той же популяции и в сопоставимом окне. Если процент улучшился только после удаления таймаутов из eligible, это не восстановление сервиса. Если причина устранена, а signal не изменился, проверяйте задержку доставки или гипотезу, а не подгоняйте denominator.
\nSLI показывает соответствие выбранному критерию, но не объясняет корневую причину. Он также не покрывает то, что не попало в scope: не дошедшие до backend запросы, неверный результат, отдельный регион или позднее событие. Для этих рисков нужны дополнительные сигналы и проверки. Несколько зелёных SLI не складываются автоматически в доказательство здоровья всей системы.
\nЧисловой пример не подключён к файлам, CI, мониторингу, сети, HTTP или реальной конфигурации. Он не создаёт alert, не меняет rollout и не разрешает выпуск. Google SRE описывает error budget как механизм совместного решения, но конкретные target, окно, page и исключения требуют согласования продуктового владельца, разработки и эксплуатации.
\nЕсли данных мало, задержка не известна или источник нельзя воспроизвести, правильный результат проверки — «решение отложено» либо «сигнал недостаточен», а не выдуманный инцидент. Если действие необратимо, сначала нужна дополнительная защита: staged rollout, snapshot, rollback или ручное подтверждение.
\nРазговор можно закрыть, когда другой инженер без устного контекста воспроизводит число и понимает действие. В записи есть пользовательский результат, версия формулы, eligible и good, target, окно, источник, владелец, критерий остановки, план отката и повторная проверка. Есть отрицательный пример, который текущий SLI не скрывает.
\nПеред релизом достаточно задать три вопроса: что именно измеряется, почему это покрывает нужный путь и что произойдёт при ухудшении. Если на первый вопрос отвечает только цвет графика, а на третий — «разберёмся по ситуации», SLO пока остаётся отчётной метрикой, а не рабочим договором.
\nНа панели появляется красный процент. В релизном чате говорят: «бюджет почти закончился». Но никто не может быстро ответить, какой пользовательский путь измеряет график, какие события попали в знаменатель и когда началось окно. Один отчёт считает отменённый запрос, другой исключает его. Один смотрит последние 28 дней, другой — календарный месяц.
\nЦена ошибки — не спор о терминах. Команда может остановить полезное исправление из-за неверной формулы. Или продолжить рискованный rollout, потому что неуспешные события не попали в расчёт. Красный цвет не становится решением, пока за ним нет проверяемого договора.
\nSLI — это количественная мера конкретного свойства сервиса. SLO задаёт для этой меры цель или диапазон. Error budget — допустимая часть неуспешных событий внутри той же границы и того же окна. Если команда меняет scope, eligible-события, good-события, окно или target, она меняет модель. Старый процент больше нельзя сравнивать с новым без оговорки.
\nНачинайте не с девяток. Сначала назовите действие пользователя. Для условного checkout это может быть «пользователь отправил заказ и получил подтверждение». Затем определите множество eligible-событий, правило успеха, исключения, окно, источник данных и владельца. Только после этого формула получает смысл.
\n| Поле | Пример | Проверка |
|---|---|---|
| Scope | checkout-submit | Событие относится к нужному пользовательскому пути |
| Eligible | завершённая попытка отправки | Знаменатель включает все случаи этой границы |
| Good | подтверждение получено в срок | Правило не зависит от цвета dashboard |
| Окно | rolling 28 days | Период одинаков в расчёте и обсуждении |
| Target | 99,5% | Число связано с policy и владельцем |
Учебный пример ниже не читает monitoring и не описывает production. В окне есть 1 000 eligible-событий. Из них 994 соответствуют правилу good. SLI равен 994 / 1 000 = 99,4%. При target 99,5% условный budget исчерпан: допустимая доля bad равна 0,5%, то есть пять событий, а фактическая — шесть.
\nconst eligible = 1000; const good = 994; const target = 0.995; const sli = good / eligible; const allowedBad = eligible * (1 - target); const actualBad = eligible - good; console.log({ sli, allowedBad, actualBad, budgetExhausted: actualBad > allowedBad }); // учебный результат: { sli: 0.994, allowedBad: 5, actualBad: 6, budgetExhausted: true }\nЧисла в коде намеренно synthetic. Они не доказывают доступность, burn rate, incident или стоимость простоя. В рабочей системе нужно подтвердить, откуда пришло каждое событие и почему оно относится к scope. Формула без этого лишь аккуратно делит неизвестные данные.
\nЕсть и отрицательный путь. Если знаменатель равен нулю, процент нельзя объявлять равным 100%. Если good больше eligible, источник или преобразование сломаны. Если сервис измеряет только HTTP-ответ, а пользовательская ценность появляется после фоновой обработки, SLI может быть полезным proxy, но не прямым измерением результата. Proxy gap надо назвать явно.
\nRolling window показывает недавнее состояние и постепенно вытесняет старые события. Fixed window проще связать с отчётным периодом. Ни один режим не исправляет ошибку в eligible set. При малом трафике одна ошибка резко меняет процент. При большом трафике среднее может скрыть хвост задержки. Для latency среднее также может быть слишком грубым: несколько очень медленных запросов исчезнут в общей цифре.
\nОкно нужно записать рядом с формулой, а не оставить подписью графика. При смене 28 дней на 30 дней, при смене fixed на rolling или при изменении часового пояса меняется сравнение. Пересчитайте исторические значения либо пометьте границу новой версией договора.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Два отчёта показывают разные SLI | Разные scope или exclusions | Сравнить определения eligible и good на трёх одинаковых событиях | Версионировать один контракт и убрать скрытый фильтр |
| Процент равен 100% при отсутствии трафика | Нулевой знаменатель превращён в успех | Проверить обработку пустого окна | Вернуть состояние no-data и отдельное правило для него |
| Красный budget не связан с жалобами | SLI измеряет proxy или не тот путь | Сопоставить событие метрики с user journey | Сузить scope либо добавить пользовательский сигнал |
| После смены окна исчезла деградация | Сравнили несовместимые периоды | Проверить версию окна и границы timestamp | Пересчитать историю или явно разделить серии |
| Budget требует немедленной блокировки | Нет policy и проверки контекста | Назвать owner, обратимость и тип изменения | Выбрать review, rollback, сужение rollout или продолжение с контролем |
Расход бюджета — сигнал для принятия решения, а не универсальная команда остановить deploy. Policy должна назвать владельца, обязательные evidence, допустимые действия и исключения. Security fix может потребовать другого пути согласования. Обратимый rollout может потребовать сужения exposure. Неверный расчёт требует остановить интерпретацию метрики, а не обязательно остановить весь релиз.
\nОтдельно разделяйте SLI и диагностику. SLI отвечает на вопрос, нарушается ли выбранная мера. Логи, трассы, версии, очереди и зависимости помогают искать причину. Один сигнал не обязан объяснять другой. Если после красного процента команда сразу объявляет incident, она пропускает проверку scope, времени и источника данных.
\nSLI не измеряет всё качество продукта. Доступность backend не гарантирует успешный пользовательский сценарий. Error budget не показывает корневую причину и не определяет важность изменения. Target 99,5% и окно 28 дней в примере не являются рекомендацией. Для редкого трафика, пакетной обработки, долгих операций и юридического SLA нужны отдельные решения.
\nУчебный код также не создаёт alert, не читает реальные события, не меняет CI и не принимает решение о выпуске. Его можно использовать для проверки арифметики и отрицательных веток. Production-вывод появляется только после проверки источника данных, владельца, разрешений и поведения системы на реальном трафике.
\nДоговор готов, если инженер за несколько минут может показать scope, eligible, good, exclusions, окно, target, источник, owner и policy branch. Для трёх выбранных событий два инженера получают одинаковый ответ. Пустое окно не становится успешным автоматически. После действия та же версия формулы даёт ожидаемое изменение, а новое значение можно связать с timestamp и источником.
\nЕсли хотя бы одно поле неизвестно, статус должен быть «договор не готов». Не добавляйте ещё один график поверх неопределённости. Сначала восстановите границу измерения, затем решайте, какое действие безопасно.
\nНа панели появляется красный процент. В релизном чате говорят: «бюджет почти закончился». Но никто не может быстро ответить, какой пользовательский путь измеряет график, какие события попали в знаменатель и когда началось окно. Один отчёт считает отменённый запрос, другой исключает его. Один смотрит последние 28 дней, другой — календарный месяц.
\nЦена ошибки — не спор о терминах. Команда может остановить полезное исправление из-за неверной формулы. Или продолжить рискованный rollout, потому что неуспешные события не попали в расчёт. Красный цвет не становится решением, пока за ним нет проверяемого договора.
\nSLI — это количественная мера конкретного свойства сервиса. SLO задаёт для этой меры цель или диапазон. Error budget — допустимая часть неуспешных событий внутри той же границы и того же окна. Если команда меняет scope, eligible-события, good-события, окно или target, она меняет модель. Старый процент больше нельзя сравнивать с новым без оговорки.
\nНачинайте не с девяток. Сначала назовите действие пользователя. Для условного checkout это может быть «пользователь отправил заказ и получил подтверждение». Затем определите множество eligible-событий, правило успеха, исключения, окно, источник данных и владельца. Только после этого формула получает смысл.
\n| Поле | Пример | Проверка |
|---|---|---|
| Scope | checkout-submit | Событие относится к нужному пользовательскому пути |
| Eligible | завершённая попытка отправки | Знаменатель включает все случаи этой границы |
| Good | подтверждение получено в срок | Правило не зависит от цвета dashboard |
| Окно | rolling 28 days | Период одинаков в расчёте и обсуждении |
| Target | 99,5% | Число связано с policy и владельцем |
Учебный пример ниже не читает monitoring и не описывает production. В окне есть 1 000 eligible-событий. Из них 994 соответствуют правилу good. SLI равен 994 / 1 000 = 99,4%. При target 99,5% условный budget исчерпан: допустимая доля bad равна 0,5%, то есть пять событий, а фактическая — шесть.
\nconst eligible = 1000; const good = 994; const target = 0.995; const sli = good / eligible; const allowedBad = eligible * (1 - target); const actualBad = eligible - good; console.log({ sli, allowedBad, actualBad, budgetExhausted: actualBad > allowedBad }); // учебный результат: { sli: 0.994, allowedBad: 5, actualBad: 6, budgetExhausted: true }\nЧисла в коде намеренно synthetic. Они не доказывают доступность, burn rate, incident или стоимость простоя. В рабочей системе нужно подтвердить, откуда пришло каждое событие и почему оно относится к scope. Формула без этого лишь аккуратно делит неизвестные данные.
\nУчебный расчёт полезен только вместе с проверками входа. Нулевой знаменатель нельзя превращать в 100%: это состояние no-data, а не доказательство успеха. Значение good, превышающее eligible, указывает на ошибку агрегации или источника. Target вне диапазона от нуля до единицы нельзя молча принимать как SLO. Такие условия лучше отвергать до построения графика.
\nnode --input-type=module <<'EOF'\nfunction check({ eligible, good, target, windowDays }) {\n if (!Number.isInteger(eligible) || eligible <= 0) throw new Error('eligible > 0');\n if (!Number.isInteger(good) || good < 0 || good > eligible) throw new Error('0 <= good <= eligible');\n if (!(target > 0 && target < 1)) throw new Error('0 < target < 1');\n if (windowDays !== 28) throw new Error('window is 28 days in this fixture');\n\n const bad = eligible - good;\n const sli = good / eligible;\n const exactAllowedBad = eligible * (1 - target);\n return {\n sli,\n bad,\n allowedBad: Number(exactAllowedBad.toFixed(6)),\n budgetExhausted: bad > exactAllowedBad,\n releaseAuthority: 'not-granted'\n };\n}\n\nconsole.log(check({ eligible: 1000, good: 994, target: 0.995, windowDays: 28 }));\nEOF\n\n# ожидается: sli 0.994, bad 6, allowedBad 5, budgetExhausted true\n\nЗапуск не обращается к monitoring, CI, сети, HTTP, SDK или часам. Поле releaseAuthority намеренно не даёт fixture полномочий: PASS подтверждает арифметику и границы входа, но не доступность сервиса и не разрешение на rollout.
Есть и отрицательный путь. Если знаменатель равен нулю, процент нельзя объявлять равным 100%. Если good больше eligible, источник или преобразование сломаны. Если сервис измеряет только HTTP-ответ, а пользовательская ценность появляется после фоновой обработки, SLI может быть полезным proxy, но не прямым измерением результата. Proxy gap надо назвать явно.
\nRolling window показывает недавнее состояние и постепенно вытесняет старые события. Fixed window проще связать с отчётным периодом. Ни один режим не исправляет ошибку в eligible set. При малом трафике одна ошибка резко меняет процент. При большом трафике среднее может скрыть хвост задержки. Для latency среднее также может быть слишком грубым: несколько очень медленных запросов исчезнут в общей цифре.
\nОкно нужно записать рядом с формулой, а не оставить подписью графика. При смене 28 дней на 30 дней, при смене fixed на rolling или при изменении часового пояса меняется сравнение. Пересчитайте исторические значения либо пометьте границу новой версией договора.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Два отчёта показывают разные SLI | Разные scope или exclusions | Сравнить определения eligible и good на трёх одинаковых событиях | Версионировать один контракт и убрать скрытый фильтр |
| Процент равен 100% при отсутствии трафика | Нулевой знаменатель превращён в успех | Проверить обработку пустого окна | Вернуть состояние no-data и отдельное правило для него |
| Красный budget не связан с жалобами | SLI измеряет proxy или не тот путь | Сопоставить событие метрики с user journey | Сузить scope либо добавить пользовательский сигнал |
| После смены окна исчезла деградация | Сравнили несовместимые периоды | Проверить версию окна и границы timestamp | Пересчитать историю или явно разделить серии |
| Budget требует немедленной блокировки | Нет policy и проверки контекста | Назвать owner, обратимость и тип изменения | Выбрать review, rollback, сужение rollout или продолжение с контролем |
Перед тем как обсуждать процент, возьмите один good, один bad и один спорный случай. Для каждого ответьте на четыре вопроса: относится ли событие к scope, почему оно eligible, какое поле доказывает good и когда событие попало в окно. Эта маленькая выборка обнаруживает скрытый фильтр быстрее, чем ещё один dashboard.
\n| Наблюдение | Проверяем | Решение до новых данных |
|---|---|---|
| Два запроса дают разный знаменатель | Scope, exclusions и версию фильтра | Приостановить сравнение и оставить одну формулу |
| Процент равен 100% без событий | Ветку no-data | Отделить отсутствие наблюдений от успеха |
| Backend good, пользователь видит сбой | Участок пути после серверного ответа | Назвать SLI proxy и добавить клиентский сигнал |
Расход бюджета — сигнал для принятия решения, а не универсальная команда остановить deploy. Policy должна назвать владельца, обязательные evidence, допустимые действия и исключения. Security fix может потребовать другого пути согласования. Обратимый rollout может потребовать сужения exposure. Неверный расчёт требует остановить интерпретацию метрики, а не обязательно остановить весь релиз.
\nОтдельно разделяйте SLI и диагностику. SLI отвечает на вопрос, нарушается ли выбранная мера. Логи, трассы, версии, очереди и зависимости помогают искать причину. Один сигнал не обязан объяснять другой. Если после красного процента команда сразу объявляет incident, она пропускает проверку scope, времени и источника данных.
\nSLI не измеряет всё качество продукта. Доступность backend не гарантирует успешный пользовательский сценарий. Error budget не показывает корневую причину и не определяет важность изменения. Target 99,5% и окно 28 дней в примере не являются рекомендацией. Для редкого трафика, пакетной обработки, долгих операций и юридического SLA нужны отдельные решения.
\nУчебный код также не создаёт alert, не читает реальные события, не меняет CI и не принимает решение о выпуске. Его можно использовать для проверки арифметики и отрицательных веток. Production-вывод появляется только после проверки источника данных, владельца, разрешений и поведения системы на реальном трафике.
\nДоговор готов, если инженер за несколько минут может показать scope, eligible, good, exclusions, окно, target, источник, owner и policy branch. Для трёх выбранных событий два инженера получают одинаковый ответ. Пустое окно не становится успешным автоматически. После действия та же версия формулы даёт ожидаемое изменение, а новое значение можно связать с timestamp и источником.
\nЕсли хотя бы одно поле неизвестно, статус должен быть «договор не готов». Не добавляйте ещё один график поверх неопределённости. Сначала восстановите границу измерения, затем решайте, какое действие безопасно.
\nНа дашборде сервис зелёный: 99,9% запросов завершились без HTTP 5xx. Пользователь всё равно нажимает «Оплатить» второй раз. Первый запрос принял API, но очередь не создала платёж, а клиент получил тайм-аут после точки измерения. Команда видит хороший процент и плохой результат. Цена ошибки — неверный приоритет: релиз считают безопасным, расследуют не тот компонент и позже спорят, был ли сбой частью SLO.
\nSLI и SLO исправляют эту ошибку только при точном договоре. SLI отвечает на вопрос «что измеряем?». SLO задаёт цель для этого измерения. Договор должен назвать путь пользователя, события в знаменателе, хороший исход, исключения, окно, источник данных и владельца решения. Если хотя бы одно поле скрыто, процент остаётся удобной, но декоративной метрикой.
\nGoogle SRE определяет SLI как количественную меру свойства сервиса, а SLO — как целевое значение или диапазон для этой меры. Из этого следует практический порядок: сначала назвать важное для пользователя действие, затем выбрать измеримый признак. Не начинайте с готового счётчика HTTP-кодов только потому, что он уже есть в системе.
\nДля checkout полезный вопрос звучит так: «Как часто завершённая попытка оформления получает подтверждение, которое клиент может показать пользователю?» Это ещё не SLI. Он задаёт границу. Теперь нужно решить, какое событие означает завершённую попытку и какое событие означает успех. Ответы должны быть наблюдаемыми и одинаково понятными владельцу продукта, разработчику и on-call.
\nУptime процесса может остаться диагностическим сигналом. Он показывает состояние компонента, но не доказывает успех пользовательского маршрута. И наоборот, один backend-ответ может быть плохим, а продукт — успешно показать fallback. Поэтому название proxy должно оставаться явным. Proxy нельзя выдавать за прямое измерение результата.
\nДля success ratio удобно разделить события на eligible и good. Eligible — все попытки, которые имеют право попасть в знаменатель. Good — подмножество eligible с заранее названным допустимым исходом. Тогда показатель считают так: SLI = good / eligible. SLO задаёт нижнюю границу, например 99% в выбранном окне. Error budget равен допустимой доле bad-событий в том же знаменателе и окне.
Правило исключения нужно записывать рядом с формулой. Отмена клиентом до отправки запроса может не входить в eligible. Отмена после принятия запроса может быть уже частью результата, если сервис обязан её обработать. Нельзя молча вычёркивать спорное событие после того, как оно ухудшило график. Иначе два отчёта получат разные знаменатели, хотя ссылаются на один маршрут.
\n| Часть | Значение | Проверка | Ограничение |
|---|---|---|---|
| Путь | Отправка оформленного checkout | Есть идентификатор попытки и граница завершения | Не описывает весь сайт |
| Eligible | Запрос дошёл до точки завершения API | Событие содержит request_id и итоговый статус | Не включает отмену до запроса |
| Good | Клиент получил подтверждение приёма платежа | Статус связан с пользовательским ответом, а не только с 2xx | Не доказывает фактическое списание |
| Окно | 28 дней в учебном примере | Все сравнения используют одну границу времени | Не является универсальным окном |
| Цель и владелец | 99%; владелец checkout | Есть правило пересмотра и способ связи | Не даёт автоматического права блокировать релиз |
Число 99% без окна и знаменателя неполно. Сто успешных попыток из ста дают 100%, но один сбой при десяти попытках меняет показатель сильнее, чем один сбой при миллионе. Для малотрафикового маршрута процент может быть шумным. Для пакетной обработки важнее throughput или время завершения. Окно выбирают вместе с типом нагрузки и решением, которое SLO должно поддержать.
\nНиже — ограниченный пример. Он проверяет только арифметику договора на заранее заданных числах. Он не читает логи, не обращается к мониторингу и не сообщает состояние какого-либо сервиса. В учебном окне есть 1 000 eligible-событий, из них 994 good. Показатель равен 99,4%. При SLO 99% допустимы 10 bad-событий, а в примере их 6. Остаток условного бюджета — 4 события.
\nconst eligible = 1000;\nconst good = 994;\nconst target = 0.99;\n\nconst bad = eligible - good;\nconst sli = good / eligible;\nconst allowedBad = eligible * (1 - target);\nconst budgetLeft = Math.max(0, allowedBad - bad);\n\nconsole.log({\n sliPercent: sli * 100,\n bad,\n allowedBad,\n budgetLeft,\n});\n// Учебный вывод: 99.4%, 6, 10, 4\n// Числа не являются измерением production-сервиса.\nФормула полезна только при сохранении условий. Если из знаменателя убрать неудачные попытки, показатель вырастет без улучшения пути. Если заменить good на «ответ не 5xx», можно начать считать принятый запрос успехом, хотя пользователь ещё не получил подтверждение. Если смешать 28-дневное окно с дневным числом ошибок, error budget потеряет смысл.
\nЦель 100% в таком примере не нужна: она скрывает допустимый риск и превращает каждое событие в повод для ручного спора. Это не запрет на строгие требования. Для финансовой операции продукт может выбрать особое правило, но оно должно быть обосновано сценарием, риском и способом проверки. Учебный target не переносится в рабочую систему автоматически.
\nПоследний шаг важен. SLO не объясняет корневую причину. Для неё нужны диагностические сигналы: логи, метрики, трассы или данные продукта. OpenTelemetry описывает эти сигналы как разные виды наблюдений: trace показывает путь запроса, metric — измерение во времени, log — запись события. Их можно связать контекстом, но ни один сигнал не доказывает причину без проверки источника и границы времени.
\nЕсли событие не содержит request_id, нельзя надёжно связать его с пользовательской попыткой. Если good event появляется раньше фактического завершения, SLI измеряет промежуточный шаг. Если трафика мало, процент может не поддерживать срочное решение. Если внешний провайдер недоступен, нужно заранее определить, входит ли его сбой в договор и кто владеет реакцией. Если событие потеряно, отсутствие строки нельзя считать успехом.
\nОтрицательный путь должен быть виден в примерах: нет знаменателя; good больше eligible; в событии отсутствует поле, необходимое для границы; отмена произошла после отправки и ошибочно исключена; два источника считают разные окна. В каждом случае честное действие — остановить интерпретацию и исправить договор или источник. Нельзя дорисовать процент, чтобы сохранить зелёный статус.
\nУчебная арифметика также не доказывает SLO compliance, availability, burn rate, incident или безопасность релиза. Она не показывает реальную стоимость простоя. Официальные документы Google помогают выбрать термины и форму описания, но не назначают target конкретному продукту. Производственный критерий должен опираться на реальные события, согласованный владелец и проверяемое правило реакции.
\nДоговор готов, если независимый инженер может без устных пояснений ответить на семь вопросов: какой путь измеряем; что входит в eligible; что считается good; какие исключения действуют; за какое окно считается показатель; где лежат исходные события; кто принимает решение при отклонении. Для трёх заранее выбранных событий формула должна дать одинаковый результат в отчёте и в проверочном запросе. После изменения должен существовать обратимый шаг и способ повторить ту же проверку.
\nЕсли на любой вопрос нет ответа, готовность не достигнута. Сначала исправьте границу и названия событий. Потом меняйте дашборд, алерт или policy. Такой порядок защищает от главной ошибки SLI/SLO: принять число, которое легко измерить, за результат, который действительно важен пользователю.
\nНа дашборде сервис зелёный: 99,9% запросов завершились без HTTP 5xx. Пользователь всё равно нажимает «Оплатить» второй раз. В рассмотренном учебном сценарии API принял запрос, но платёж не создался, а клиент получил тайм-аут после точки измерения. Команда видит хороший технический процент и плохой пользовательский результат. Цена ошибки — неверный приоритет релиза и расследование не того компонента.
\nSLI и SLO помогают только после точного договора. SLI отвечает на вопрос «что измеряем?», SLO задаёт цель для этого измерения, а error budget показывает допустимую долю плохих исходов в выбранном окне. Договор должен назвать путь пользователя, eligible-события в знаменателе, good-исход, исключения, источник данных и владельца решения. Иначе процент остаётся удобной, но декоративной метрикой.
\nGoogle SRE описывает SLI как количественную меру свойства сервиса, а SLO — как целевое значение или диапазон для уровня сервиса, измеренного этим SLI. Практический вывод простой: начинайте с действия, которое пользователь считает завершённым, и только потом выбирайте доступный сигнал. Готовый счётчик HTTP-кодов не становится хорошим SLI лишь потому, что уже есть в мониторинге.
\nДля checkout полезный вопрос звучит так: «Как часто завершённая попытка оформления получает подтверждение, которое клиент может показать пользователю?» Это ещё не формула. Нужно определить границу попытки, идентификатор, событие подтверждения и допустимое время ожидания. Если сервис лишь принимает запрос в очередь, SLI приёма не доказывает создание платежа. Такой показатель можно оставить техническим proxy, но назвать его proxy в документации и на графике.
\nПуть должен быть достаточно узким, чтобы его можно было восстановить по одному событию. «Весь сайт работает» — плохой scope для первой договорённости. «Отправка оформленного checkout до подтверждения приёма платежа» уже допускает проверку: видны начало, конец, идентификатор и ожидаемый исход.
\nSLI — способ измерения: например, доля eligible-попыток, для которых пришёл good-результат. SLO — цель, например не менее 99% в rolling window, то есть в скользящем окне. SLA — договор с пользователем, где за невыполнение SLO предусмотрено явное последствие. Внутренний SLO не становится SLA автоматически: наличие графика не создаёт финансовых или организационных обязательств.
\nЦель 99% ниже 100% намеренно оставляет допустимый риск. Это не рекомендация ставить именно 99%. Число должно учитывать цену ошибки, нагрузку, ожидания пользователя и возможность команды реагировать. Нельзя выбрать target только по текущему лучшему результату: тогда команда зафиксирует случайное достижение и получит дорогое обязательство.
\nДля success ratio разделите события на eligible и good. Eligible — попытки, которые имеют право попасть в знаменатель. Good — подмножество eligible с заранее названным допустимым исходом. Базовая формула: SLI = good / eligible. Каждое исключение меняет знаменатель, поэтому его записывают до расчёта, а не добавляют после неудачного дня.
Например, отмена до отправки запроса может не входить в eligible. Отмена после принятия запроса уже может быть частью результата, если сервис обязан её обработать. Потерянное событие нельзя молча считать успехом. При неоднозначном статусе нужно остановить интерпретацию, найти запись операции и решить, какое правило действует для всех таких случаев.
\n| Поле | Учебное значение | Что проверить | Граница |
|---|---|---|---|
| Путь | От отправки checkout до подтверждения приёма | Есть начало, конец и request_id | Не описывает создание платежа |
| Eligible | Запрос принят точкой завершения API | Событие содержит итоговый статус | Отмена до запроса не входит |
| Good | Клиент получил подтверждение приёма | Статус связан с ответом клиенту | Не доказывает фактическое списание |
| Окно | 28 дней в учебном примере | Отчёт и запрос используют одну границу | Не универсальная норма |
| Target | 99% good среди eligible | Есть обоснование и правило пересмотра | Не назначается по одному графику |
| Владелец | Команда checkout | Названо лицо, принимающее решение | Метрика сама не блокирует релиз |
Один и тот же процент имеет разный смысл при разном трафике. 99 из 100 попыток дают 99%, но одна ошибка в десяти попытках меняет показатель сильнее, чем одна ошибка в миллионе. Поэтому рядом с SLI показывают число eligible, good и bad. Для пакетной обработки может быть важнее throughput или время завершения, а для интерактивного пути — latency и доля успешных исходов. Не нужно насильно сводить разные пользовательские задачи к одной цифре.
\nError budget — это допустимая доля bad-событий в том же знаменателе и окне. В учебном наборе 1 000 eligible-событий, 994 good и 6 bad. При target 99% допустимы 10 bad-событий, поэтому условный остаток равен 4. Этот пример проверяет только арифметику; он не читает логи, не обращается к мониторингу и не сообщает состояние сервиса.
\nconst eligible = 1000;\nconst good = 994;\nconst target = 0.99;\n\nif (!Number.isInteger(eligible) || eligible <= 0) {\n throw new Error('eligible must be a positive integer');\n}\nif (!Number.isInteger(good) || good < 0 || good > eligible) {\n throw new Error('good must be between 0 and eligible');\n}\n\nconst bad = eligible - good;\nconst sli = good / eligible;\nconst allowedBad = Math.round(eligible * (1 - target));\nconst budgetDelta = allowedBad - bad;\n\nconsole.log({\n sliPercent: sli * 100,\n bad,\n allowedBad,\n budgetDelta,\n budgetState: budgetDelta >= 0 ? 'remaining' : 'exhausted',\n});\n// { sliPercent: 99.4, bad: 6, allowedBad: 10,\n// budgetDelta: 4, budgetState: 'remaining' }\n// Это synthetic-арифметика, а не измерение production-сервиса.\nСохраните содержимое блока в файл sli-example.mjs и выполните node sli-example.mjs. В рабочем отчёте нужно заранее договориться об округлении, если допустимое число bad-событий получается дробным. В примере явно выбран Math.round для целого числа событий; другая policy требует отдельного решения. Если good больше eligible, знаменатель нулевой или данные неполны, расчёт должен остановиться, а не выдавать красивый процент.
Наивная замена good на «ответ не 5xx» опасна: запрос может быть принят, но пользователь ещё не получил результат. Обратная ошибка тоже возможна: клиентский fallback завершил путь, а серверная метрика записала ошибку. В обоих случаях сравните технический сигнал с событием, которое действительно закрывает пользовательскую попытку.
\nSLO полезен как часть петли управления: измерить SLI, сравнить его с SLO, оценить риск, выбрать действие и снова измерить. Error budget поставляет evidence для приоритизации, но не является универсальным приказом остановить изменения. В официальном примере Google policy расход бюджета определяет, когда надёжности нужно уделить больше внимания; конкретные исключения и полномочия остаются договорённостью команды.
\nАлерт должен сообщать о действующей угрозе бюджету, а не о каждом колебании процента. Google отдельно разбирает precision, recall, detection time и reset time для SLO-алертов. Для low-traffic сервиса один отказ может дать огромную мгновенную долю и ложную срочность. Возможные решения — более длинное окно, искусственный трафик с оговоркой о его покрытии, объединение связанных потоков или изменение продукта так, чтобы единичный сбой меньше вредил пользователю. Ни один вариант нельзя выбрать без оценки конкретного пути.
\nПосле срабатывания SLO-алерта не ищите причину в одной панели. OpenTelemetry разделяет traces, metrics и logs: trace показывает путь запроса, metric — измерение во времени, log — запись события. Эти сигналы помогают связать симптом с компонентом, но ни один из них сам по себе не доказывает причинность. Нужны исходное событие, граница времени и проверка гипотезы.
\nЭта схема подходит для пути, где можно определить событие начала, события завершения и принадлежность к знаменателю. Она не заменяет модель latency, throughput, durability или стоимости. Для долгих пакетных процессов success ratio может скрыть время ожидания; для хранения данных одного успешного ответа мало, если важно долговременное сохранение. Для финансовой операции подтверждение приёма также не равно фактическому списанию — это отдельная граница и отдельный показатель.
\n28-дневное окно и target 99% здесь учебные значения. Официальный пример Google показывает форму SLO-документа и четырёхнедельное rolling window, но не назначает такую длительность вашему сервису. Низкий трафик, внешняя зависимость, ретраи и частично потерянная телеметрия меняют интерпретацию. Если команда не может доказать полноту знаменателя, честный результат — «данных недостаточно», а не приблизительный SLI.
\nДоговор можно передавать в работу, если независимый инженер без устных пояснений отвечает на семь вопросов: какой путь измеряется; что входит в eligible; что считается good; какие исключения действуют; за какое окно считается показатель; где лежат исходные события; кто и по какому правилу принимает решение. Для трёх заранее выбранных записей отчёт и проверочный запрос должны дать одинаковый результат. После изменения должны остаться обратимый шаг и команда, которой можно повторить проверку.
\nЕсли ответа нет хотя бы на один вопрос, сначала исправьте границу и названия событий. Затем меняйте дашборд, алерт или policy. Такой порядок не даёт принять число, которое легко измерить, за результат, который действительно важен пользователю.
\nГрафик ошибок растёт, но инженер не может назвать запрос и этап, на котором возник отказ. В журналах много похожих сообщений, а в trace-поиске нет понятного ключа. Самая дорогая ошибка в этот момент — принять громкий сигнал за причину: увеличить timeout, добавить retry или обвинить downstream. Сбой может остаться, а новые записи и задержки вырастут.
\nПроблема возникает, когда metric, trace и log описывают один путь разными словами. Метрика считает класс исходов. Trace показывает путь запроса через операции. Log или event фиксирует событие и его контекст. Если между ними нет общего договора, команда видит три витрины, а не одну проверяемую цепочку.
\nНачинайте с вопроса, а не с поиска текста ошибки. Metric отвечает: «какой класс исходов изменился?». Trace отвечает: «через какие операции прошёл один путь?». Log/event отвечает: «какое событие произошло на конкретном шаге?». Общий trace ID или другой разрешённый correlation key связывает записи. Он не превращает metric в журнал запросов.
\nИдентификатор одного запроса нельзя бездумно добавлять в labels метрики. Каждый новый идентификатор может создавать отдельный time series. График станет дороже, агрегация — менее полезной, а проблема поиска не исчезнет. Для метрики оставляют небольшой словарь: service, route и outcome. Подробный контекст отправляют в trace или log после проверки политики доступа и хранения.
\nПредставим учебный checkout-сценарий. Metric сообщает: для маршрута authorization вырос класс rejected. Эта запись не знает пользователя, заказа и конкретного trace. Она только выбирает поле поиска. Далее trace с тем же synthetic correlation key показывает gateway span и дочерний payment span. Затем log/event указывает, что отказ произошёл на payment span, и повторяет trace ID и span ID.
Каждая стрелка требует отдельной проверки. Наличие метрики не доказывает существование trace. Наличие trace не доказывает, что log экспортирован и доступен. Совпавший ID не доказывает причину отказа, если событие записалось после ошибки или относится к другому шагу. Доказательство должно состоять из наблюдаемых объектов и честного статуса каждой связи.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Счётчик ошибок изменился, trace не находится | Нет перехода от route/outcome к trace или trace не экспортируется | Взять один разрешённый outcome и проверить correlation в реальной среде | Починить передачу контекста или назвать путь неподтверждённым |
| В metric появились request ID | Идентичность запроса использовали как label | Посчитать набор labels и рост series на выбранном окне | Остановить изменение, вернуть малый словарь labels, ID оставить в trace/log |
| Trace есть, событие не объясняет отказ | Log не содержит span ID, событие относится к другому шагу или потеряно при sampling | Сверить trace ID, span ID, имя события и время | Исправить корреляцию; не объявлять downstream причиной |
| Свободный текст не ищется стабильно | Сообщение меняется между версиями и не имеет event name | Проверить структурированные поля и стабильное имя события | Добавить минимальную схему и сохранить текст как дополнительный context |
| Учебный тест зелёный, production неизвестен | Проверили форму записей в памяти, а не экспорт и поиск | Отделить fixture от реальной выборки и явно отметить границу | Назначить проверку в разрешённой среде; не публиковать результат как incident evidence |
Ниже — классификатор учебных записей. Он проверяет только договор между объектами в памяти. Значения synthetic-* выдуманы для примера. Код не обращается к приложению, не создаёт telemetry и не подтверждает, что downstream действительно вернул ошибку.
function checkRoute({ metric, trace, event }) {\n const labels = Object.keys(metric.labels);\n const allowed = ['service', 'route', 'outcome'];\n const metricShape = labels.length === 3\n && labels.every((name) => allowed.includes(name))\n && !labels.includes('trace_id');\n const traceShape = trace.root.traceId === trace.payment.traceId;\n const eventShape = event.traceId === trace.payment.traceId\n && event.spanId === trace.payment.spanId;\n return {\n metricShape,\n traceShape,\n eventShape,\n readyForRealCheck: metricShape && traceShape && eventShape,\n };\n}\n\nconst result = checkRoute({\n metric: { labels: { service: 'checkout', route: 'authorization', outcome: 'rejected' } },\n trace: {\n root: { traceId: 'synthetic-trace-1' },\n payment: { traceId: 'synthetic-trace-1', spanId: 'synthetic-span-payment' },\n },\n event: { traceId: 'synthetic-trace-1', spanId: 'synthetic-span-payment' },\n});\n\nconsole.log(result);\n// readyForRealCheck: true — только договор synthetic-записей.\nОтрицательный путь важнее зелёного результата. Если event получит другой trace ID, eventShape станет false. Если в metric появится trace_id, metricShape станет false. Код не угадывает причину и не исправляет систему. Он останавливает вывод: сначала нужно восстановить связь или признать, что её нет.
Metric полезна на первом шаге, потому что сжимает поток в устойчивые классы. Используйте route template и outcome, а не полный URL, user ID, order ID или текст ошибки. Набор dimensions должен быть заранее ограничен. Точное число допустимых series зависит от платформы, окна и числа значений, поэтому его нельзя объявлять безопасным без расчёта и проверки владельца backend.
\nTrace нужен, когда вопрос перешёл от класса к пути. Найдите один разрешённый trace и проверьте дерево spans: gateway должен вести к payment operation, а не просто соседствовать с ней по времени. Сверьте parent-child связь, статус, длительность и границы sampling. Даже полный trace показывает путь инструментирования, а не автоматически истинную причину бизнес-ошибки.
\nLog/event нужен для контекста шага. Структурированное событие должно иметь стабильное имя, время, trace ID и, если событие связано с конкретной операцией, span ID. Дополнительные attributes должны пройти review на чувствительные данные, redaction, retention и права доступа. «Добавим весь request на всякий случай» — плохая стратегия: она увеличивает риск и не делает гипотезу точнее.
\nЕсли новый label резко расширяет cardinality или event содержит запрещённое поле, остановите распространение изменения. Сначала определите, какие записи ещё могут появляться и какие потребители уже зависят от схемы. Затем выберите обратимое действие для конкретной конфигурации: отключить добавленный label, ограничить event attributes или вернуть предыдущую версию instrumentation. Нельзя обещать удаление уже сохранённых данных, пока не известны storage, retention и политика доступа.
\nЕсли metric уже есть, а trace не связывается, не добавляйте ещё один ID в счётчик. Проверьте propagation на границе сервиса, sampling, exporter и возможность поиска. Если log не содержит span ID, назовите это дефектом корреляции. Если настоящая система не позволяет безопасно проверить путь, остановите расследование на статусе «не подтверждено» и не заменяйте evidence догадкой.
\nПример не содержит реальных logs, metrics, traces, latency, traffic, backend records или incident data. Synthetic value и IDs не являются измерениями. Статья не утверждает, что конкретная SDK, collector, exporter или backend поддерживает одинаковые поля и поиск. Sampling может скрыть часть trace. Асинхронная очередь может разорвать контекст. Событие может прийти позже операции. Эти условия нужно проверять в выбранном контуре.
\nКритерий готовности проверяемый: для одного разрешённого route есть metric с заранее названными dimensions; для выбранного outcome найден trace с тем же correlation key; trace содержит ожидаемый span; log/event имеет тот же trace ID и корректный span ID; отрицательные ветки дают отказ; после изменения не выросли запрещённые labels и не появились чувствительные поля. Если хотя бы одна связь не доказана, итог — неполный.
\nСимптом для диагностики звучит знакомо: график ошибок показывает изменение, но инженер не может назвать запрос и этап, на котором оно возникло. В ответ часто начинают искать текст исключения во всех logs или добавляют request ID в metric labels. Первый путь тонет в несвязанных записях, второй смешивает счётчик с идентичностью одного запроса. Причина не становится ближе: у трёх источников нет договора, который превращает один сигнал в вопрос к следующему.
\nЦена такого разрыва — решение на основании наиболее громкой витрины. Можно увеличить timeout, включить retry или объявить downstream виновником, хотя связь между error count, span и event не подтверждена. Эта статья не расследует реальный инцидент и не собирает telemetry. Она строит безопасный diagnostic route для одного fixed synthetic сценария, чтобы показать: evidence одного отказа складывается из разных объектов, а не из максимального количества labels.
\nУ диагностики есть три уровня. Metric помогает сформулировать, какой класс исходов стоит рассматривать: например, synthetic `outcome=synthetic-rejected` для synthetic checkout route. Trace должен показать предполагаемый причинный путь из gateway к payment шагу. Log/event record должен назвать событие на payment step и сохранить тот же correlation key. Только после этого появляется evidence-card: она говорит, какую гипотезу можно проверить и чего пока нет. Ни один из объектов по отдельности не заменяет остальные.
\n| Очередь | Вопрос | Нужное представление | Допустимый результат | Что не делать |
|---|---|---|---|---|
| 1 | какой класс результата разбираем? | metric labels | synthetic route + outcome | не добавлять request ID ради фильтра |
| 2 | какой путь должен ему соответствовать? | trace + span tree | один synthetic trace ID, два шага | не считать график доказательством причины |
| 3 | какое событие произошло на шаге? | log/event record | event name + trace ID + span ID | не искать по свободному тексту без correlation |
| 4 | какой вывод честен? | evidence card | not-a-production-observation | не объявлять hypothesis подтверждённой fixture-ом |
В учебном наборе metric record содержит имя `synthetic.checkout.authorization.rejected.total`, значение `1` и три labels. Значение `1` — не измеренный в системе counter, а фиксированная часть fixture. Оно нужно только чтобы показать форму: одна маленькая точка может обозначать класс outcome. По ней нельзя определить user, order, request или span. Такую границу полезно сохранять даже если UI backend позволяет кликнуть на dimensions: возможность фильтра не превращает metric в достоверный журнал событий.
\nЕсли на первом шаге неизвестно, какой вопрос нужно решить, не пополняйте labels «на всякий случай». Сначала назовите route template и outcome class, которые должны быть малым словарём. Затем спросите владельца инструмента, какая реальная единица агрегации поддерживается, какие resource attributes добавляются и где будет измеряться cardinality. Без ответа status должен быть «не проверено», а не «у нас низкая cardinality». Fixture помогает удержать именно эту дисциплину: лишний `trace_id`, `request_id` или `user_id` он отвергает до того, как поле станет привычным.
\nДальше мы идём по `synthetic-trace-2023-09-A`. В trace object есть root span gateway и дочерний payment span; оба названия и состояния synthetic. Связь потомка с родителем — модель причинного маршрута, а не свидетельство выполнения вызова. Log/event record ссылается на payment span, имеет тот же trace ID и event name отказа. Если trace ID или span ID в log отличаются, fixture возвращает отказ. Это простое правило полезнее длинного списка полей: событие должно либо объяснять конкретный шаг пути, либо честно оставаться несвязанным.
\nEvent attributes нужны для узкой диагностики события. В примере есть `failure.class=synthetic-declined` и `retry.advice=synthetic-do-not-retry`. Они не говорят, как надо обрабатывать настоящие платежи, и не являются production error message. Их роль — показать разницу между типом отказа и точной идентичностью запроса. В реальном проекте перед добавлением любых attributes нужно отдельно решить privacy, возможность redaction, retention, доступ к поиску и стабильность названий. Нельзя прятать эти решения под словом «контекст».
\nКод ниже создаёт fixed synthetic scenario в памяти. Он не может обратиться к приложению или telemetry backend, не читает clock и не создаёт telemetry. `runTelemetryFixture()` проверяет девятнадцать assertions: общий trace ID для trace и log, правильный span, три разрешённых labels, отсутствие trace ID в labels, закрытый список входных полей, отрицательные ветки mismatch и предел rollback. Это упражнение для review контракта. Его PASS не подтверждает, что downstream отказал, что metric выросла, что span записался или что log можно найти.
\nimport {\n assembleSyntheticTelemetryScenario,\n runTelemetryFixture,\n} from './upgrade-2023-09.mjs';\n\nconst scenario = assembleSyntheticTelemetryScenario({\n synthetic: true,\n traceId: 'synthetic-trace-2023-09-A',\n rootSpanId: 'synthetic-span-gateway-A',\n downstreamSpanId: 'synthetic-span-payment-A',\n logTraceId: 'synthetic-trace-2023-09-A',\n logSpanId: 'synthetic-span-payment-A',\n eventName: 'synthetic.payment.authorization-rejected',\n metricLabels: {\n service: 'synthetic-checkout-api',\n route: 'synthetic-checkout',\n outcome: 'synthetic-rejected',\n },\n});\n\nconst report = runTelemetryFixture();\nif (!Object.values(report.assertions).every(Boolean)) throw new Error('fixture failed');\nconsole.log(scenario.evidence.conclusion); // not-a-production-observation\n\n// Не создаёт trace/log/metric, не запускает SDK и не отправляет данные.\n\nnode web/scripts/upgrade-2023-09.mjs --verify-fixture\n\n# PASS проверяет только fixed synthetic records in memory.\n# Не доказывает incident, production latency, trace export или cardinality.\nПосле локального прогона полезно записать evidence без переобобщения. Корректная формулировка: «модель ожидает, что один correlation key соединяет заданный payment event и заданный trace; metric использует только три fixed dimensions». Некорректная: «причина ошибки найдена» или «metric безопасна для production». Разница кажется формальной только пока первое решение не затронуло retry, alert policy или пользовательские данные. В инженерном разборе неизвестное — это тоже результат, который должен пережить передачу задачи.
\nЕсли на review обнаружился label с высокой кардинальностью или несвязанный event, первое действие — остановить распространение нового контракта. Это не равно удалить все данные: нельзя обещать удаление, не зная платформы, retention, доступа и состоявшегося rollout. Затем нужно отделить два вопроса: какие новые записи могут продолжать возникать и какие потребители уже зависят от этого поля. Только после этого владелец выбирает обратимое действие для конкретной конфигурации.
\nFixture умеет только вернуть snapshot synthetic полей и пометить `telemetry=not-created-or-deleted`. Он не выключает instrumentation, не меняет sampling, alert, dashboard или access policy. Такой rollback не декоративен: он ставит границу между проверкой модели и операционным изменением. В реальном runbook точка возврата должна быть названа точнее: версия конфигурации, набор approved fields, способ проверить отсутствие дальнейшего потока и владелец подтверждения.
\nЗдесь нет реальных logs, metrics, traces, latency, cardinality, traffic, backend records или incident data. Нет отправки данных, запроса, collector, exporter, storage, sampling, alerting, query, dashboard или эффекта в production. Synthetic `value: 1` не является измерением; synthetic IDs не являются request IDs. Пакет также не утверждает, что реальные error messages, user fields или маршруты допустимы для хранения. Он только различает роли полей и показывает, какую связь надо проверить позднее.
\nСледующий шаг — провести ограниченное design review одного изменения инструментирования. Договоритесь о: одном вопросе к метрике, одном route template, малом outcome vocabulary, одном correlation key и минимальном event schema. Затем выберите реальную разрешённую среду и способ проверить путь без публикации чувствительных значений. Если итогом окажется, что trace context не проходит конкретную границу, это не поражение модели: это точная задача для следующего изменения, а не основание расширять cardinality метрики.
\nМатериал использует только OpenTelemetry Specification v1.20.0, опубликованную 7 апреля 2023 и доступную к сентябрю того года. Она уже описывала tracing API, metrics data model и logs data model, на которые опирается различение сигналов. Статья не утверждает, что конкретная SDK, transport, collector или backend имеют одинаковую зрелость, и не переносит в 2023 год более поздние договорённости команды или инструмента.
\n