Большинство инженеров начинают путь с простой задачи - сделать так, чтобы ничего не падало. И в этом нет ничего плохого. Мы ставим мониторинг, настраиваем алерты и радуемся когда всё «зеленое».
Но спустя пару месяцев пользователи начинают жаловаться:
«Поиск выдает результаты через 5 секунд»
«Платежи проходят с задержкой»
«Интерфейс зависает при большом количестве данных»
Идешь смотреть, а метрики в норме, инфраструктура стабильна, но пользователю от этого не легче. И тут проблема в том, что мы не умеем измерять, насколько хорошо она работает с точки зрения опыта конечного пользователя.

И в этой точке важно начинать приходить к пониманию от «горит или нет» к пониманию, как горит. Мониторинг обычно отвечает на вопрос: «жив ли сервис» и даже «на сколько сервису плохо», но не отвечает на вопрос о влиянии этого самого «сервису плохо» на бизнес.
Пример: В одном интернет‑магазине серверы не перегружены, ошибок 500 нет, но продажи упали на 7%. Оказалось, при переходе к оплате страница грузилась по 6–7 секунд и пользователи просто уходили. С точки зрения мониторинга — всё «зелёное». С точки зрения бизнеса — потеря денег.
Тогда возникает вопрос, а что значит «система работает работает хорошо»? Чтобы говорить о качестве, нужно зафиксировать, что вообще считается «хорошим». Для этого инженеры используют три понятия:
SLA (Service Level Agreement) — «обещание» пользователю. Например: «Сервис доступен 99,9% времени».
SLO (Service Level Objective) — цель, к которой стремится команда. Например: «99,95% запросов выполняются за ≤ 200 мс».
SLI (Service Level Indicator) - конкретный измеряемый показатель. Например: “latency P99”, “ошибки 5xx”, “успешные транзакции”.
Пример: API биллинга установил SLO: 99,9% запросов быстрее 150 мс. Однажды latency вырос до 400 мс — не авария, но выход за цель. Виноваты оказались обновления сервиса и после фикса всё вернулось в норму.
Но ведь раз у нас есть цель 99.9, то значит сервис какое то время может сбоить. И тут мы подошли еще к одному термину. Любая система допускает небольшой процент сбоев. Этот процент — бюджет ошибок. Если SLA 99,9%, то за год допустимо примерно 8 часов простоя.
И вот тут важно понять, что бюджет ошибок — это не KPI, а инструмент. Он даёт право на риск. Это возможность для команды использовать бюджет в своих интересах. Бюджет ошибок помогает решать — где стоит рисковать, а где пора стабилизировать.
Примеры: Команда внедряет новый алгоритм рекомендаций. Есть риск, что новая система снизит продажи или перегрузит сервера запросами. Команда видит, что у них есть в бюджете ошибок время на эксперименты и они решаются катить изменения, чтобы проверить новую фичу. Таким образом вместо того чтобы доводить всё до идеала — они могут просто «освоить бюджет» не выходя за рамки бютжета. И если всё прошло гладко, то у них еще есть запас на следующие эксперименты.
Как начать измерять качество:
Определяем критичные сценарии. Например: авторизация, поиск, оплата.
Назначаем целевые значения. Например: “95% запросов успешны, время ответа ≤ 300 мс”.
Собераем метрики. Prometheus + Grafana отлично подойдут.
Визуализирем расход бюджета. Сделайте график: горизонталь - дни, вертикаль - процент стабильности. Если линия ползёт вниз - вы “сжигаете” бюджет.
Ну и настроиваем алерты через генератор SLO. Например, sloth или любой другой.

Мы же любим все автоматизировать и поэтому не хотим описывать все выражения руками. И тут нам на помощь приходят генераторы SLO. Возьмем для примера один из множества sloth.
https://sloth.dev - это инструмент, который позволяет описывать SLO декларативно и автоматически генерировать правила для Prometheus и Alertmanager. Вместо того чтобы писать PromQL-выражения руками, вы описываете цели в YAML (пример с официального сайта):
version: "prometheus/v1" service: "myservice" labels: owner: "myteam" repo: "myorg/myservice" tier: "2" slos: # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). - name: "requests-availability" objective: 99.9 description: "Common SLO based on availability for HTTP request responses." sli: events: error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) alerting: name: MyServiceHighErrorRate labels: category: "availability" annotations: # Overwrite default Sloth SLO alert summmary on ticket and page alerts. summary: "High error rate on 'myservice' requests responses" page_alert: labels: severity: pageteam routing_key: myteam ticket_alert: labels: severity: "slack" slack_channel: "#alerts-myteam"
Из этого Sloth сам создаёт нужные PrometheusRule и алерты с множителями согласно Google SRE Book.
Результат работы генератора
--- # Code generated by Sloth (dev): https://github.com/slok/sloth. # DO NOT EDIT. groups: - name: sloth-slo-sli-recordings-myservice-requests-availability rules: - record: slo:sli_error:ratio_rate5m expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 5m tier: "2" - record: slo:sli_error:ratio_rate30m expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 30m tier: "2" - record: slo:sli_error:ratio_rate1h expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 1h tier: "2" - record: slo:sli_error:ratio_rate2h expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 2h tier: "2" - record: slo:sli_error:ratio_rate6h expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 6h tier: "2" - record: slo:sli_error:ratio_rate1d expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 1d tier: "2" - record: slo:sli_error:ratio_rate3d expr: | (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) / (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 3d tier: "2" - record: slo:sli_error:ratio_rate30d expr: | sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) / ignoring (sloth_window) count_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability sloth_window: 30d tier: "2" - name: sloth-slo-meta-recordings-myservice-requests-availability rules: - record: slo:objective:ratio expr: vector(0.9990000000000001) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: slo:error_budget:ratio expr: vector(1-0.9990000000000001) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: slo:time_period:days expr: vector(30) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: slo:current_burn_rate:ratio expr: | slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: slo:period_burn_rate:ratio expr: | slo:sli_error:ratio_rate30d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: slo:period_error_budget_remaining:ratio expr: 1 - slo:period_burn_rate:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_service: myservice sloth_slo: requests-availability tier: "2" - record: sloth_slo_info expr: vector(1) labels: cmd: examplesgen.sh owner: myteam repo: myorg/myservice sloth_id: myservice-requests-availability sloth_mode: cli-gen-prom sloth_objective: "99.9" sloth_service: myservice sloth_slo: requests-availability sloth_spec: prometheus/v1 sloth_version: dev tier: "2" - name: sloth-slo-alerts-myservice-requests-availability rules: - alert: MyServiceHighErrorRate expr: | ( max(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate1h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) ) or ( max(slo:sli_error:ratio_rate30m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) ) labels: category: availability routing_key: myteam severity: pageteam sloth_severity: page annotations: summary: High error rate on 'myservice' requests responses title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast. - alert: MyServiceHighErrorRate expr: | ( max(slo:sli_error:ratio_rate2h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate1d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) ) or ( max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate3d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) ) labels: category: availability severity: slack slack_channel: '#alerts-myteam' sloth_severity: ticket annotations: summary: High error rate on 'myservice' requests responses title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast
Как видно из результата работы генератора — у нас появился довольно большой yaml, в котором вся грязная работа сделана уже за вас. И самый частый вопрос бывает «а зачем там мультиокна»? Разберем прямо на этом же примере.
Page‑алерт (срочный): сработает, если оба окна превышают порог 14.4× бюджета на 5m + 1h, или 6× бюджета на 30m + 6h. Это ловит резкие и устойчивые всплески (деплой поломал API, лавина 5xx). Короткий «пшик» на 5 минут не пробьёт, потому что нужно подтвердить на длинном окне пары. Ticket‑алерт (фоново): сработает при более мягкой, но длительной деградации — 3× на 2h + 1d или 1× (то есть ровно бюджет) на 6h + 3d. Это про «тихо растущие» проблемы и регрессии качества.
Что в итоге? Тимлид в такой экосистеме уже не «смотрит на алерты», а управляет балансом между скоростью и надёжностью. Он помогает команде использовать бюджет ошибок осознанно и видеть, как изменения влияют на SLO.
Мониторинг говорит, жив ли сервис. SLO — как живёт ваш продукт.
SLO, SLI и SLA делают качество измеримым.
Бюджет ошибок даёт свободу для экспериментов.
Sloth и подобные генераторы делают SLO воспроизводимыми и упрощают внедрение.
Таким образом мы пришли к тому, что стабильность — это не когда «ничего не ломается», а когда даже если ломается — понятно почему. Когда у команды есть цифры, на которые можно опереться, и смелость что‑то менять, не боясь что сработают алерты. Вот тогда можно сказать: «да, всё работает — и работает хорошо».
Хватит читать DevOps-статьи от людей без продакшена. Я рассказываю про свой реальный опыт в своем Telegram-канале DevOps Brain 🧠 ↩
