Перейти к содержимому

Наблюдаемость и диагностика

Как понять, что происходит с сервисом в проде: какие сигналы собирать, как сообщить балансировщику о здоровье экземпляра, как расследовать высокий CPU или утечку на живом процессе и как настроить алерты, которые будят только по делу. По этой теме проверяют опыт эксплуатации: что вы делаете, когда «всё тормозит», а в логах тихо.

Перед этой темой: Конфигурация и логирование.

Junior+

Что такое наблюдаемость и чем логи, метрики и трассировки отличаются друг от друга?

Заголовок раздела «Что такое наблюдаемость и чем логи, метрики и трассировки отличаются друг от друга?»
Ответ

Что это

Наблюдаемость — свойство системы, которое позволяет по её внешним данным понять внутреннее состояние. Основных видов данных, или сигналов, три.

Три сигнала

Сигнал На какой вопрос отвечает
Метрика: число во времени сколько и как быстро: RPS, доля ошибок, задержка
Трассировка: путь одного запроса где тратится время и где упало
Лог: отдельное событие что именно произошло и с какими данными

Как пользоваться

  1. Алерт по метрике сообщает: выросла доля ошибок.
  2. Трейс упавшего запроса показывает, в каком сервисе или запросе к БД проблема.
  3. Логи с тем же TraceId дают текст ошибки и параметры.

Частая ошибка

Считать метрики по логам: «сколько ошибок за минуту» через поиск в Elastic. Это медленно и дорого, для счётчиков есть метрики.

Как устроено

  • Метрика — значение с метками, агрегированное по времени: счётчик, гистограмма, текущее значение. Хранится как временной ряд, и его размер не зависит от числа запросов.
  • Трейс — дерево спанов. Спан — одна операция (входящий запрос, SQL-запрос, HTTP-вызов) с началом, длительностью и атрибутами. Между сервисами контекст передаёт заголовок traceparent.
  • Лог — событие с уровнем, шаблоном и полями. Объём растёт вместе с трафиком.

Где хранят

Сигнал Типичный стек
Логи ELK (Elasticsearch, Logstash, Kibana), OpenSearch, Seq, Loki
Метрики Prometheus и Grafana, VictoriaMetrics
Трейсы Jaeger, Grafana Tempo, Zipkin
Всё вместе APM: Elastic APM, Application Insights, Datadog

Ловушки

  • Нет TraceId в логах. Сигналы не связаны, и расследование превращается в поиск по времени.
  • Pull против push. Prometheus сам забирает метрики с эндпоинта сервиса, а OTLP отправляет их из сервиса. От модели зависит, кто кому открывает сеть.

Правило

Алерты — по метрикам, поиск узкого места — по трейсам, детали — по логам.

Мониторинг и наблюдаемость

  • Мониторинг — заранее заданные дашборды и алерты. Он отвечает на известные вопросы: жив ли сервис, какой p99.
  • Наблюдаемость — возможность ответить на вопрос, о котором не думали заранее. Например, почему медленно только у одного региона на одной версии клиента. Для этого нужны события с богатым контекстом и срезы по любому полю.

Цена

  • Метрика стоит одинаково при 10 и 10 000 RPS, но каждая уникальная комбинация меток — отдельный ряд.
  • Трейсы при полном сборе дают десятки спанов на запрос. Их сэмплируют: head-based в приложении или tail-based в коллекторе, чтобы сохранить ошибки и медленные запросы.
  • Логи платят за индексацию и хранение каждого события, поэтому Debug в проде выключен.

Компромиссы

  • Агент APM-вендора даёт быстрый старт, но привязывает к нему. OpenTelemetry — стандартный API и протокол OTLP: бэкенд меняется конфигурацией, ценой своего конвейера (OpenTelemetry Collector).
  • Exemplars связывают точку на графике метрики с конкретным TraceId: с выброса p99 можно сразу перейти к медленному трейсу.

Как проверить в проде

Разберите последний инцидент: сколько прошло от алерта до найденной причины и пришлось ли добавлять логи и передеплоить, чтобы её найти. Если пришлось, наблюдаемости не хватает.

Итог: мониторинг отвечает на известные вопросы, наблюдаемость — на новые, и платят за неё объёмом телеметрии.

Пример
# Метрика: агрегат за интервал, без деталей отдельного запроса
http_server_request_duration_seconds_count{http_route="/orders",http_response_status_code="500"} 17
# Трейс: дерево спанов одного запроса
POST /orders 480 ms trace 4bf92f3577b34da6
├─ SELECT orders 12 ms
└─ POST payments-api 455 ms ← здесь время
# Лог: событие с полями и тем же TraceId
{"Level":"Error","MessageTemplate":"Payment {PaymentId} failed","PaymentId":"p-42","TraceId":"4bf92f3577b34da6"}

Один сбой виден тремя способами: метрика сообщает о 17 ошибках, трейс указывает на payments-api, лог даёт номер платежа.

Middle+

Как сделать health checks в ASP.NET Core и чем liveness отличается от readiness?

Заголовок раздела «Как сделать health checks в ASP.NET Core и чем liveness отличается от readiness?»
Ответ

Что это

Health check — HTTP-эндпоинт, который сообщает состояние приложения: Healthy, Degraded или Unhealthy. Его опрашивают балансировщик, оркестратор (например, Kubernetes) и мониторинг.

Как пользоваться

  1. builder.Services.AddHealthChecks() — регистрирует сервис проверок.
  2. .AddCheck<T>("name") — добавляет проверку: класс, реализующий IHealthCheck.
  3. app.MapHealthChecks("/health") — публикует эндпоинт.

Коды по умолчанию: Healthy и Degraded — 200, Unhealthy — 503.

Liveness и readiness

Проба Что при провале
Liveness: процесс жив и не завис? процесс перезапускают
Readiness: готов принимать трафик? экземпляр убирают из балансировки

Частая ошибка

Один /health с проверкой БД для обеих проб. Упала БД — оркестратор по кругу перезапускает все экземпляры.

Как устроено

  1. Каждая проверка регистрируется с именем, тегами, failureStatus и таймаутом.
  2. MapHealthChecks с Predicate выбирает проверки по тегам. Так из одного набора получаются /health/live и /health/ready.
  3. Выбранные проверки выполняются на каждый запрос к эндпоинту, итог — худший статус.
  4. ResponseWriter меняет формат ответа. По умолчанию это одно слово: Healthy.

Что проверять

Эндпоинт Проверки
/health/live ничего внешнего: ответил — значит жив (Predicate = _ => false)
/health/ready БД, кеш, брокер — без чего экземпляр бесполезен

Готовые проверки: AddDbContextCheck из пакета Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore и библиотека AspNetCore.Diagnostics.HealthChecks (SQL Server, PostgreSQL, Redis, RabbitMQ). Вторую Microsoft не поддерживает.

Ловушки

  • Нет таймаута. Зависшая БД держит пробу дольше, чем ждёт оркестратор.
  • Частота. Оркестратор, балансировщик и мониторинг вместе дают десятки запросов в БД в минуту на каждый экземпляр.
  • AllowCachingResponses управляет HTTP-заголовками кеширования, а не кешем результата проверки.
  • Подробный ответ. Список зависимостей и тексты ошибок полезны атакующему. Эндпоинт закрывают через RequireHost на служебный порт или RequireAuthorization.

Правило

Liveness — без внешних зависимостей, readiness — только критичные зависимости и всегда с таймаутом.

Под капотом

  • DefaultHealthCheckService запускает проверки параллельно через Task.Run и ждёт все. Каждая получает свой DI-scope и токен, который отменяется по её таймауту.
  • Исключение или таймаут дают failureStatus регистрации, по умолчанию Unhealthy.
  • IHealthCheckPublisher получает HealthReport от фонового сервиса: первый запуск через 5 секунд, дальше каждые 30. С .NET 8 у отдельной регистрации можно задать свои Delay и Period.

Цена

  • Тяжёлая проверка, умноженная на частые пробы, — заметная нагрузка на зависимость. Её снимают кешем результата на несколько секунд внутри проверки.
  • Другой вариант — publisher сохраняет последний отчёт, а свой эндпоинт отдаёт его без запроса в БД. Встроенный MapHealthChecks так не умеет: он выполняет проверки при каждом обращении.

Компромиссы

  • Каскадный отказ. Readiness с проверкой общей зависимости выключает весь пул разом. Если без неё сервис хотя бы частично работает (кеш, очередь), лучше отдать Degraded и пусть отказывают отдельные запросы.
  • Глубокие проверки. Сервис A проверяет B, B проверяет C, и падение C делает «больными» всех. Соседние сервисы в readiness обычно не проверяют: от их сбоев защищают таймауты и circuit breaker.

Как проверить в проде

Publisher пишет статус и длительность каждой проверки в лог или метрику. Так видны флапы и медленные проверки. Алерт ставят не на одну неудачную пробу, а на долю неготовых экземпляров.

Итог: health check управляет трафиком, поэтому в нём проверяют только то, что действительно меняет решение о трафике.

Пример
Microsoft.Data.SqlClient
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck<SqlHealthCheck>("sql", tags: ["ready"], timeout: TimeSpan.FromSeconds(3));
var app = builder.Build();
// Liveness: ни одной проверки — ответил, значит процесс жив
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false });
// Readiness: только проверки с тегом "ready"; Unhealthy → 503
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.Run();
sealed class SqlHealthCheck(IConfiguration config) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct = default)
{
// Исключение или таймаут (ct отменится через 3 с) станут Unhealthy
await using var conn = new SqlConnection(config.GetConnectionString("Default"));
await conn.OpenAsync(ct);
await using var cmd = new SqlCommand("SELECT 1", conn);
await cmd.ExecuteScalarAsync(ct);
return HealthCheckResult.Healthy();
}
}
Publisher: статус проверок в лог раз в 15 секунд · Senior
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy());
builder.Services.Configure<HealthCheckPublisherOptions>(o => o.Period = TimeSpan.FromSeconds(15));
builder.Services.AddSingleton<IHealthCheckPublisher, LogPublisher>();
var app = builder.Build();
app.Run();
sealed class LogPublisher(ILogger<LogPublisher> logger) : IHealthCheckPublisher
{
public Task PublishAsync(HealthReport report, CancellationToken ct)
{
foreach (var (name, entry) in report.Entries)
{
// История статусов и длительностей видна в хранилище логов, а не только в последнем ответе
logger.LogInformation("Health check {Check}: {Status} in {Duration} ms",
name, entry.Status, entry.Duration.TotalMilliseconds);
}
return Task.CompletedTask;
}
}
Middle+

Какие метрики собирать для веб-сервиса и как?

Заголовок раздела «Какие метрики собирать для веб-сервиса и как?»
Ответ

Что это

Метрика — числовой показатель, который сервис записывает постоянно, а система мониторинга хранит как график во времени.

Что собирать: RED

  • Rate — запросов в секунду.
  • Errors — доля запросов с ошибкой, например ответов 5xx.
  • Duration — время ответа.

Как пользоваться

  1. Встроенная метрика ASP.NET Core http.server.request.duration (.NET 8+) уже даёт все три значения с разбивкой по маршруту и коду ответа.
  2. Бизнес-события («создан заказ») считают своим счётчиком Counter<T>.
  3. Локально метрики смотрят через dotnet-counters, в проде — в Prometheus и Grafana или в APM.

Частая ошибка

Смотреть на среднее время ответа. 99 запросов по 10 мс и один на 10 секунд дают среднее около 110 мс, а пользователь ждал 10 секунд.

Как устроено

  • Meter — именованный источник метрик. В ASP.NET Core его создают через IMeterFactory из DI (.NET 8+).
  • Метки (tags) — пары «ключ — значение» у каждого измерения: маршрут, код ответа, способ оплаты.
Инструмент Для чего
Counter<T> растущий счётчик событий: заказы, ошибки
UpDownCounter<T> значение растёт и убывает: активные соединения
Histogram<T> распределение: длительность, размер ответа
ObservableGauge<T> текущее значение, читается при сборе: длина очереди

С .NET 9 есть и синхронный Gauge<T>: значение записывают в момент изменения.

Когда что выбирать

Подход Что измеряет
RED каждый сервис или эндпоинт: rate, errors, duration
USE каждый ресурс (CPU, память, пул соединений): утилизация, насыщение, ошибки
Золотые сигналы latency, traffic, errors, saturation — RED плюс насыщение

Встроенные метрики

  • Microsoft.AspNetCore.Hosting: http.server.request.duration, http.server.active_requests.
  • Microsoft.AspNetCore.Server.Kestrel: соединения, очереди, TLS-рукопожатия.
  • System.Runtime (.NET 9+): GC, пул потоков, CPU, dotnet.exceptions.
  • В .NET 10 добавились метрики аутентификации, авторизации, Identity и Blazor.

Ловушки

  • user.id или путь /orders/42 в метке — миллионы рядов. Метка — шаблон маршрута http.route.
  • Длительность в миллисекундах в одном сервисе и в секундах в другом. Единицы указывают явно (s, By, {order}), стандарт — секунды.

Правило

RED на каждый эндпоинт, USE на каждый ограниченный ресурс, счётчики — на ключевые бизнес-операции.

Под капотом

  • Add или Record без слушателя — проверка флага и выход. Слушатель (OpenTelemetry SDK, MeterListener, dotnet-counters через EventPipe) агрегирует в процессе: сумму для счётчика, бакеты для гистограммы.
  • В хранилище попадает не каждое измерение, а ряд на каждую комбинацию меток. Prometheus забирает агрегаты по HTTP, OTLP отправляет их сам.

Цена

  • Рядов = произведение числа значений меток × бакеты гистограммы × экземпляры. 50 маршрутов × 10 кодов × 4 метода × 15 бакетов × 20 подов — до 600 тысяч рядов у одной метрики.
  • Хранилище метрик платит памятью за каждый активный ряд, поэтому новая метка с тысячей значений — решение, а не мелочь.

Компромиссы

  • Перцентиль по гистограмме приблизителен: точность зависит от границ бакетов. Для http.server.request.duration с OpenTelemetry по умолчанию границы от 5 мс до 10 с. Свои границы задают InstrumentAdvice (.NET 9+) или View в OpenTelemetry.
  • Перцентили нельзя усреднять по экземплярам: p99 считают из суммы бакетов, в Prometheus — histogram_quantile по sum by (le).
  • Среднее и максимум дешевле, но не отвечают на вопрос «сколько пользователей ждали дольше секунды». Доля запросов быстрее порога считается из тех же бакетов.

Как проверить в проде

  • dotnet-counters monitor -n MyApp --counters Shop.Orders — свои метрики живого процесса без экспорта.
  • В Prometheus count(http_server_request_duration_seconds_bucket) показывает, сколько рядов даёт одна метрика.

Итог: перцентили считают из суммированных гистограмм, а цену метрик определяет кардинальность меток.

Пример
using System.Diagnostics;
using System.Diagnostics.Metrics;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<OrderMetrics>();
var app = builder.Build();
app.MapPost("/orders", async (OrderMetrics metrics) =>
{
long start = Stopwatch.GetTimestamp();
await Task.Delay(50); // обработка заказа
metrics.OrderCreated("card", Stopwatch.GetElapsedTime(start));
return Results.Accepted();
});
app.Run();
sealed class OrderMetrics
{
private readonly Counter<long> _created;
private readonly Histogram<double> _duration;
private int _queueLength;
public OrderMetrics(IMeterFactory factory)
{
Meter meter = factory.Create("Shop.Orders");
_created = meter.CreateCounter<long>("shop.orders.created", unit: "{order}");
_duration = meter.CreateHistogram<double>("shop.orders.duration", unit: "s",
advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5] });
// Значение читается только в момент сбора
meter.CreateObservableGauge("shop.orders.queue.length", () => Volatile.Read(ref _queueLength), unit: "{order}");
}
public void OrderCreated(string paymentMethod, TimeSpan elapsed)
{
// Несколько возможных значений — низкая кардинальность; id заказа сюда не кладут
var tag = new KeyValuePair<string, object?>("payment.method", paymentMethod);
_created.Add(1, tag);
_duration.Record(elapsed.TotalSeconds, tag);
}
}
Окно терминала
# Свои и встроенные метрики работающего процесса
dotnet-counters monitor -n MyShop --counters Shop.Orders,Microsoft.AspNetCore.Hosting
Senior+

Как разобрать проблему производительности или памяти на живом сервере?

Заголовок раздела «Как разобрать проблему производительности или памяти на живом сервере?»
Ответ

Что это

Диагностика на живом сервере — поиск причины без отладчика и без остановки сервиса.

Как пользоваться

  1. По метрикам определить симптом: что выросло и когда.
  2. Посмотреть счётчики процесса: dotnet-counters monitor -n MyApp.
  3. Высокий CPU — записать трейс на 30 секунд и открыть в Visual Studio или PerfView.
  4. Растёт память или процесс завис — снять дамп и открыть в dotnet-dump analyze.

Инструменты

Инструмент Что даёт
dotnet-counters живые метрики: CPU, GC, пул потоков, запросы
dotnet-trace где тратится время, события рантайма
dotnet-dump снимок всей памяти и стеков потоков
dotnet-gcdump только граф объектов кучи

Частая ошибка

Сразу перезапустить процесс, ничего не сняв. Симптом пропал, улики тоже, и через день всё повторится.

Как устроено

Инструменты подключаются к процессу через diagnostic port: именованный канал на Windows или Unix-сокет в /tmp на Linux. Данные идут по EventPipe, поэтому не нужны ни SDK на сервере, ни перезапуск. Нужны тот же пользователь, что у процесса, или root.

Когда что выбирать

Симптом Что смотреть
Высокий CPU dotnet-trace: горячие стеки в PerfView или speedscope
Растёт память рост кучи после GC в dotnet-counters, затем два dotnet-gcdump
Медленно, CPU низкий очередь и потоки пула в dotnet-counters, стеки в дампе
Медленные запросы к БД трейсы со SQL-спанами, затем план запроса
Всплеск исключений dotnet.exceptions по error.type

Подробности: утечки — в теме Сборка мусора, голодание пула — в Многопоточности, планы запросов — в Индексах.

Ловушки

  • Профиль dotnet-trace по умолчанию сэмплирует все управляемые потоки, в том числе ждущие. В отчёте отделяют работу от ожидания. Профиль cpu-sampling из старых инструкций в новых версиях инструмента удалён.
  • На Linux инструменту и процессу нужен общий TMPDIR, иначе подключение отвалится по таймауту.
  • dotnet-gcdump вызывает полную сборку мусора, на большой куче это заметная пауза.

Правило

Одна гипотеза — один точечный сбор. Сначала счётчики, потом трейс, дамп — последним.

План по шагам

  1. Смягчить. Откатить релиз, если проблема совпала с деплоем, или вывести экземпляр из балансировки. Один «больной» экземпляр оставить для сбора.
  2. Метрики. Когда началось, все экземпляры или один, совпадает ли с деплоем, ростом трафика, паузами GC.
  3. Лёгкий сбор. dotnet-counters, затем dotnet-trace на 30–60 секунд.
  4. Тяжёлый сбор. Дамп — только когда нужен. Перед ним проверить место на диске (дамп весит как память процесса) и лимит памяти контейнера: сбор может подкачать память и довести до OOM kill.
  5. Анализ вне прода. Дамп копируют на рабочую машину и разбирают там.
  6. Проверка. Исправление подтверждают теми же метриками.

Контейнеры

  • Sidecar видит процесс, если у контейнеров общее пространство процессов и общий /tmp (например, emptyDir). Для dotnet-dump нужна возможность ptrace (SYS_PTRACE).
  • dotnet-monitor — готовый sidecar с HTTP API: /dump, /gcdump, /trace, /metrics. Его collection rules с триггерами вроде AspNetRequestDuration сами снимают артефакт в момент проблемы и выгружают в хранилище.
  • Для падений рантайм пишет дамп сам при DOTNET_DbgEnableMiniDump=1.

Цена

  • Пока пишется дамп, процесс стоит: на большой куче это секунды и больше.
  • В дампе вся память: строки подключения, токены, персональные данные. Его хранят и передают как секрет и удаляют после разбора.

Итог: на проде сначала спасают пользователей, потом снимают минимально достаточные улики, а тяжёлые снимки делают на выведенном из балансировки экземпляре.

Пример
Окно терминала
# 1. Какие .NET-процессы видны (тот же пользователь или root)
dotnet-counters ps
# 2. Живые метрики: CPU, GC, пул потоков, запросы
dotnet-counters monitor -p 1234 --counters System.Runtime,Microsoft.AspNetCore.Hosting
# 3. Высокий CPU: 30 секунд сэмплов стеков, файл для speedscope или PerfView
dotnet-trace collect -p 1234 --duration 00:00:00:30 --format Speedscope
# 4. Рост памяти: граф объектов (вызывает полную сборку мусора)
dotnet-gcdump collect -p 1234 -o before.gcdump
# 5. Зависание или голодание пула: полный дамп и стеки всех потоков
dotnet-dump collect -p 1234 -o /diag/app.dmp
dotnet-dump analyze /diag/app.dmp -c "threadpool" -c "parallelstacks" -c "exit"
dotnet-monitor: трейс при всплеске медленных запросов · Senior
{
"CollectionRules": {
"SlowRequests": {
"Trigger": {
"Type": "AspNetRequestDuration",
"Settings": { "RequestCount": 10, "RequestDuration": "00:00:03", "SlidingWindowDuration": "00:01:00" }
},
"Actions": [
{ "Type": "CollectTrace", "Settings": { "Profile": "Cpu", "Duration": "00:00:30", "Egress": "TmpDir" } }
],
"Limits": { "ActionCount": 1, "ActionCountSlidingWindowDuration": "01:00:00" }
}
}
}

Десять запросов дольше трёх секунд за минуту — и dotnet-monitor сам пишет 30-секундный трейс, не чаще раза в час.

Senior+

Что такое SLI, SLO и error budget и как настроить алерты, которые не будят зря?

Заголовок раздела «Что такое SLI, SLO и error budget и как настроить алерты, которые не будят зря?»
Ответ

Что это

  • SLI (service level indicator) — что измеряем: доля успешных запросов, доля запросов быстрее 300 мс.
  • SLO (service level objective) — цель: 99,9% за 30 дней.
  • SLA — договор с клиентом с последствиями, обычно мягче внутреннего SLO.
  • Error budget — 100% − SLO: сколько неудач можно себе позволить.

Зачем

SLO отвечает на вопрос «достаточно ли мы надёжны?». 100% недостижимо и не нужно: пользователь не отличит 99,99% от 100%.

Как пользоваться

  • Бюджет есть — выкатываем фичи. Бюджет кончился — занимаемся надёжностью.
  • Алерт срабатывает, когда страдают пользователи, а не когда «что-то выглядит странно».

Частая ошибка

Алерт на каждый всплеск CPU или на одну ошибку 500. Дежурный привыкает игнорировать уведомления и пропускает настоящий отказ.

Как устроено

  1. SLI = хорошие события / все события. Его считают из метрик, например из http.server.request.duration с кодом ответа.
  2. SLO задают на окно, обычно скользящие 28 или 30 дней.
  3. Бюджет в минутах: 99,9% за 30 дней — 43,2 минуты полного простоя, 99,99% — 4,3 минуты.

Когда что выбирать

Алерт Пример Куда
Симптомный доля 5xx, p99 выше цели SLO будит дежурного
Причинный CPU 90%, диск заполнен на 80% дашборд или тикет
Причинный, но неизбежный диск заполнится через 4 часа будит: отказ вот-вот случится

Ловушки

  • Мало трафика. Порог «больше 1% ошибок за 5 минут» ночью срабатывает от одной ошибки на 50 запросов.
  • Алерт без действия. Если на уведомление нечего сделать, это строка на дашборде, а не алерт.
  • Только доступность. SLO по ошибкам не заметит, что всё стало медленным; нужен ещё SLI по задержке.

Правило

Будит только то, что требует действия человека прямо сейчас, и у каждого такого алерта есть runbook.

Под капотом: burn rate

Burn rate = доля ошибок / (1 − SLO). При SLO 99,9% и 1,44% ошибок burn rate равен 14,4: месячный бюджет сгорит за 50 часов. Рекомендация SRE Workbook для 99,9%:

Расход бюджета Окна: длинное / короткое Burn rate и реакция
2% 1 ч / 5 мин 14,4 — будить
5% 6 ч / 30 мин 6 — будить
10% 3 дня / 6 ч 1 — тикет

Алерт срабатывает, когда превышены оба окна: длинное даёт точность, короткое — быстрый сброс после восстановления.

Цена

  • Ночное пробуждение — цена в людях. Google SRE рассчитывает максимум на два инцидента за 12-часовую смену: больше — и разбор превращается в тушение пожаров.
  • Слишком жёсткий SLO тормозит выкладки, слишком мягкий не защищает пользователей. Цель берут от того, что замечает пользователь, а не от текущих графиков.

Компромиссы

  • Error budget policy заранее договаривается о последствиях: бюджет исчерпан — фичи ждут, команда чинит надёжность. Без политики SLO остаётся цифрой на дашборде.
  • Runbook — шаги диагностики и безопасные действия для конкретного алерта. Повторяющийся runbook — кандидат на автоматизацию.

Разбор инцидента

Blameless postmortem фиксирует хронологию, влияние на пользователей, причины и действия с владельцами и сроками. Вопрос — «почему система позволила этому случиться», а не «кто виноват»: иначе люди начинают скрывать ошибки.

Итог: алерт по скорости расхода error budget будит только тогда, когда пользователи действительно теряют обещанное качество.

Пример
groups:
- name: orders-api-slo
rules:
# SLI: доля ответов 5xx за окно
- record: slo:error_ratio:rate5m
expr: |
sum(rate(http_server_request_duration_seconds_count{job="orders-api",http_response_status_code=~"5.."}[5m]))
/
sum(rate(http_server_request_duration_seconds_count{job="orders-api"}[5m]))
- record: slo:error_ratio:rate1h
expr: |
sum(rate(http_server_request_duration_seconds_count{job="orders-api",http_response_status_code=~"5.."}[1h]))
/
sum(rate(http_server_request_duration_seconds_count{job="orders-api"}[1h]))
# SLO 99,9% → бюджет 0,001; burn rate 14,4 = 2% месячного бюджета за час
- alert: OrdersApiErrorBudgetBurn
expr: |
slo:error_ratio:rate1h > (14.4 * 0.001)
and
slo:error_ratio:rate5m > (14.4 * 0.001)
labels:
severity: page
annotations:
summary: "orders-api is burning its error budget 14x faster than allowed"
runbook_url: "https://wiki.example.com/runbooks/orders-api-errors"

Правило Prometheus будит дежурного, только если ошибки высоки и за последний час, и за последние 5 минут.