Наблюдаемость и диагностика
Как понять, что происходит с сервисом в проде: какие сигналы собирать, как сообщить балансировщику о здоровье экземпляра, как расследовать высокий CPU или утечку на живом процессе и как настроить алерты, которые будят только по делу. По этой теме проверяют опыт эксплуатации: что вы делаете, когда «всё тормозит», а в логах тихо.
Перед этой темой: Конфигурация и логирование.
Что такое наблюдаемость и чем логи, метрики и трассировки отличаются друг от друга?
Заголовок раздела «Что такое наблюдаемость и чем логи, метрики и трассировки отличаются друг от друга?»Что это
Наблюдаемость — свойство системы, которое позволяет по её внешним данным понять внутреннее состояние. Основных видов данных, или сигналов, три.
Три сигнала
| Сигнал | На какой вопрос отвечает |
|---|---|
| Метрика: число во времени | сколько и как быстро: RPS, доля ошибок, задержка |
| Трассировка: путь одного запроса | где тратится время и где упало |
| Лог: отдельное событие | что именно произошло и с какими данными |
Как пользоваться
- Алерт по метрике сообщает: выросла доля ошибок.
- Трейс упавшего запроса показывает, в каком сервисе или запросе к БД проблема.
- Логи с тем же
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, лог даёт номер платежа.
- Чем мониторинг отличается от наблюдаемости? — Мониторинг отвечает на заранее заданные вопросы, наблюдаемость позволяет задать новые без деплоя.
- Почему нельзя считать ошибки по логам? — Поиск по логам медленный и дорогой, а счётчик в метрике стоит копейки и годится для алерта.
- Что такое спан? — Одна операция внутри трейса: имя, начало, длительность, атрибуты и ссылка на родителя.
- Как связать логи с трейсами? — Писать в каждую запись
TraceIdиSpanIdтекущейActivity, ASP.NET Core делает это сам. - Зачем сэмплировать трейсы? — Полный сбор дорог; tail-based сэмплинг сохраняет все ошибки и медленные запросы.
Как сделать health checks в ASP.NET Core и чем liveness отличается от readiness?
Заголовок раздела «Как сделать health checks в ASP.NET Core и чем liveness отличается от readiness?»Что это
Health check — HTTP-эндпоинт, который сообщает состояние приложения: Healthy, Degraded или Unhealthy. Его опрашивают балансировщик, оркестратор (например, Kubernetes) и мониторинг.
Как пользоваться
builder.Services.AddHealthChecks()— регистрирует сервис проверок..AddCheck<T>("name")— добавляет проверку: класс, реализующийIHealthCheck.app.MapHealthChecks("/health")— публикует эндпоинт.
Коды по умолчанию: Healthy и Degraded — 200, Unhealthy — 503.
Liveness и readiness
| Проба | Что при провале |
|---|---|
| Liveness: процесс жив и не завис? | процесс перезапускают |
| Readiness: готов принимать трафик? | экземпляр убирают из балансировки |
Частая ошибка
Один /health с проверкой БД для обеих проб. Упала БД — оркестратор по кругу перезапускает все экземпляры.
Как устроено
- Каждая проверка регистрируется с именем, тегами,
failureStatusи таймаутом. MapHealthChecksсPredicateвыбирает проверки по тегам. Так из одного набора получаются/health/liveи/health/ready.- Выбранные проверки выполняются на каждый запрос к эндпоинту, итог — худший статус.
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 управляет трафиком, поэтому в нём проверяют только то, что действительно меняет решение о трафике.
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 → 503app.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(); }}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; }}- Почему liveness не должен проверять БД? — Рестарт не чинит БД, а оркестратор перезапустит все экземпляры по кругу.
- Какой код вернёт
Degraded? — 200 по умолчанию; меняется черезResultStatusCodes. - Что будет, если проверка бросит исключение? — Результат получит
failureStatusрегистрации, по умолчаниюUnhealthy. - Как не нагружать БД частыми пробами? — Таймаут, кеш результата на несколько секунд или отдача последнего отчёта от publisher.
- Как защитить эндпоинт здоровья? —
RequireHostна служебный порт,RequireAuthorization, краткий ответ наружу.
Какие метрики собирать для веб-сервиса и как?
Заголовок раздела «Какие метрики собирать для веб-сервиса и как?»Что это
Метрика — числовой показатель, который сервис записывает постоянно, а система мониторинга хранит как график во времени.
Что собирать: RED
- Rate — запросов в секунду.
- Errors — доля запросов с ошибкой, например ответов 5xx.
- Duration — время ответа.
Как пользоваться
- Встроенная метрика ASP.NET Core
http.server.request.duration(.NET 8+) уже даёт все три значения с разбивкой по маршруту и коду ответа. - Бизнес-события («создан заказ») считают своим счётчиком
Counter<T>. - Локально метрики смотрят через
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- Чем RED отличается от USE? — RED — для запросов к сервису, USE — для ресурсов: CPU, памяти, пулов.
- Почему p99, а не среднее? — Среднее прячет медленный хвост, который видят реальные пользователи.
- Можно ли усреднить p99 по подам? — Нет, суммируют бакеты гистограмм и считают перцентиль из суммы.
- Почему нельзя класть
user.idв метку? — Каждое значение — новый временной ряд, хранилище раздувается. - Чем
ObservableGauge<T>отличается отGauge<T>? — Первый читает значение колбэком при сборе, второй записывают в момент изменения (.NET 9+).
Как разобрать проблему производительности или памяти на живом сервере?
Заголовок раздела «Как разобрать проблему производительности или памяти на живом сервере?»Что это
Диагностика на живом сервере — поиск причины без отладчика и без остановки сервиса.
Как пользоваться
- По метрикам определить симптом: что выросло и когда.
- Посмотреть счётчики процесса:
dotnet-counters monitor -n MyApp. - Высокий CPU — записать трейс на 30 секунд и открыть в Visual Studio или PerfView.
- Растёт память или процесс завис — снять дамп и открыть в
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вызывает полную сборку мусора, на большой куче это заметная пауза.
Правило
Одна гипотеза — один точечный сбор. Сначала счётчики, потом трейс, дамп — последним.
План по шагам
- Смягчить. Откатить релиз, если проблема совпала с деплоем, или вывести экземпляр из балансировки. Один «больной» экземпляр оставить для сбора.
- Метрики. Когда началось, все экземпляры или один, совпадает ли с деплоем, ростом трафика, паузами GC.
- Лёгкий сбор.
dotnet-counters, затемdotnet-traceна 30–60 секунд. - Тяжёлый сбор. Дамп — только когда нужен. Перед ним проверить место на диске (дамп весит как память процесса) и лимит памяти контейнера: сбор может подкачать память и довести до OOM kill.
- Анализ вне прода. Дамп копируют на рабочую машину и разбирают там.
- Проверка. Исправление подтверждают теми же метриками.
Контейнеры
- 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 или PerfViewdotnet-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.dmpdotnet-dump analyze /diag/app.dmp -c "threadpool" -c "parallelstacks" -c "exit"{ "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-секундный трейс, не чаще раза в час.
- Чем
dotnet-dumpотличается отdotnet-gcdump? — Дамп — вся память процесса со значениями полей, gcdump — только граф объектов, лёгкий, но с полной сборкой мусора. - Почему
dotnet-traceне видит процесс в соседнем контейнере? — Нет общего/tmpс diagnostic port или общего пространства процессов. - Как понять, что медленно из-за пула потоков, а не из-за CPU? — CPU низкий, очередь пула длинная, потоков всё больше; стеки показывают блокирующие вызовы.
- Как поймать проблему, которая длится минуту ночью? — Правила
dotnet-monitorс триггером или--stopping-eventуdotnet-trace. - Чем опасен дамп на проде? — Пауза процесса, рост памяти до OOM kill, персональные данные в файле.
Что такое 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. Дежурный привыкает игнорировать уведомления и пропускает настоящий отказ.
Как устроено
- SLI = хорошие события / все события. Его считают из метрик, например из
http.server.request.durationс кодом ответа. - SLO задают на окно, обычно скользящие 28 или 30 дней.
- Бюджет в минутах: 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 минут.
- Чем SLO отличается от SLA? — SLO — внутренняя цель, SLA — договор с последствиями, обычно мягче SLO.
- Сколько простоя даёт 99,9% в месяц? — Около 43 минут.
- Что такое burn rate 1? — Бюджет закончится ровно к концу окна SLO.
- Зачем короткое окно в алерте по burn rate? — Чтобы алерт быстро погас после восстановления, а не висел час.
- Что должно быть в postmortem? — Хронология, влияние, причины, действия с владельцами; без поиска виноватых.