Перейти к содержанию

Дашборд для диагностики задержки

Теперь у Grafana есть три настоящих источника данных. Соберём дашборд «Пульса», который помогает выбрать направление диагностики. На нём будут скорость запросов, доля серверных отказов, p95 HTTP и возраст старейшей ожидающей задачи. Эти панели соответствуют конкретным вопросам, а не просто демонстрируют возможности интерфейса. По ним оператор должен понять, изменилась ли нагрузка, какой путь замедлился и куда перейти за подробностями.

В девятом состоянии дашборд описан JSON и подключён file provisioning. Мы не снимали скриншот работающей Grafana: контейнеры и приложения не запускались. Поэтому ожидаемый вид объясняется по структуре конфигурации. Читатель получает полный файл панели и может проверить его после самостоятельной подготовки среды; в тексте не подменяются реальные данные заранее нарисованным успешным графиком.

Четыре вопроса на одном экране

Первая панель показывает rate counter за пять минут. Это скорость, а не накопленное число запросов с момента старта. Если построить total без производной, график будет почти всегда расти и затруднит сравнение текущей нагрузки. rate также учитывает типичные сбросы counter при перезапуске процесса, поэтому сумма rate по сериям обычно полезнее rate от предварительно суммированных несовместимых counters.

Вторая панель делит rate 5xx на rate всех HTTP. В знаменателе применён небольшой clamp, чтобы не делить на ноль. При отсутствии серий результат может всё равно быть пустым: clamp не создаёт несуществующий numerator автоматически. Пустое значение нельзя интерпретировать как доказанный ноль ошибок. Позднее отдельное правило отсутствия heartbeat поможет отличить отсутствие данных от нормального периода без трафика.

Третья панель оценивает p95 по histogram buckets. Сначала вычисляется rate для bucket, затем значения агрегируются по le, затем применяется histogram_quantile. Такая последовательность сохраняет распределение. Усреднение отдельных p95 нескольких процессов не даёт общий p95: квантиль не является линейной величиной. Даже в лаборатории с одним API полезно использовать запрос, который выражает правильную модель измерения.

Четвёртая панель показывает gauge возраста старейшей queued у pulse-worker. Это время ожидания начала работы. Если API быстро отвечает, а этот возраст растёт, пользовательская задержка находится за HTTP-границей. Если возраст мал, а готовые отчёты всё равно задерживаются, следует смотреть active processing, lease и прикладное завершение. Один график очереди не охватывает все состояния задачи.

Полный файл dashboard.json состояния 09:

{
  "uid": "pulse-overview",
  "title": "Пульс: запрос → очередь → worker",
  "schemaVersion": 40,
  "version": 1,
  "time": {
    "from": "now-30m",
    "to": "now"
  },
  "refresh": "5s",
  "panels": [
    {
      "id": 1,
      "title": "Запросы API / секунду",
      "type": "timeseries",
      "datasource": {
        "type": "prometheus",
        "uid": "pulse-prom"
      },
      "gridPos": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(rate(pulse_requests_total{service=\"pulse-api\"}[5m]))"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "reqps"
        },
        "overrides": []
      }
    },
    {
      "id": 2,
      "title": "Доля 5xx",
      "type": "timeseries",
      "datasource": {
        "type": "prometheus",
        "uid": "pulse-prom"
      },
      "gridPos": {
        "x": 12,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(rate(pulse_requests_total{service=\"pulse-api\",status_class=\"5xx\"}[5m])) / clamp_min(sum(rate(pulse_requests_total{service=\"pulse-api\"}[5m])),0.001)"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "percentunit"
        },
        "overrides": []
      }
    },
    {
      "id": 3,
      "title": "p95 HTTP",
      "type": "timeseries",
      "datasource": {
        "type": "prometheus",
        "uid": "pulse-prom"
      },
      "gridPos": {
        "x": 0,
        "y": 8,
        "w": 12,
        "h": 8
      },
      "targets": [
        {
          "refId": "A",
          "expr": "histogram_quantile(0.95, sum by (le) (rate(pulse_request_duration_seconds_bucket{service=\"pulse-api\"}[5m])))"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "s"
        },
        "overrides": []
      }
    },
    {
      "id": 4,
      "title": "Возраст старейшей queued",
      "type": "timeseries",
      "datasource": {
        "type": "prometheus",
        "uid": "pulse-prom"
      },
      "gridPos": {
        "x": 12,
        "y": 8,
        "w": 12,
        "h": 8
      },
      "targets": [
        {
          "refId": "A",
          "expr": "max(pulse_queue_oldest_seconds{service=\"pulse-worker\"})"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "s"
        },
        "overrides": []
      }
    }
  ]
}

Интервал и единицы

Дашборд по умолчанию показывает последние тридцать минут и обновляется каждые пять секунд. Окно rate составляет пять минут, поэтому короткий всплеск сглаживается. Это не ошибка панели, а свойство запроса. Для подробного расследования меняют интервал и сопоставляют более короткие окна с достаточным числом scrape points. Слишком короткое окно при редком scrape делает скорость нестабильной или пустой.

Duration и queue age показываются в секундах. Ошибка с миллисекундами изменила бы смысл порогов в тысячу раз, даже если линия визуально выглядела правдоподобно. Unit в Grafana форматирует значения, но не изменяет физическую единицу измерения, которую записал SDK. Поэтому контракт начинается в createHistogram(..., {unit:'s'}) и расчёте интервала, а заканчивается согласованной подписью оси.

Для доли ошибок используется percentunit: значение 0.05 отображается как пять процентов. Если выбрать формат процентов для уже умноженного на сто значения, получится неправильное представление. Такие ошибки легко пропустить при низкой нагрузке, поэтому полезно вручную разобрать мысленный случай: пять отказов среди ста запросов должны соответствовать 0.05 в выражении и пяти процентам на панели.

От общей панели к trace

Метрика определяет подозрительный интервал, а trace объясняет отдельный случай. Выберите время увеличения p95 и найдите в Tempo SERVER-span маршрута /v1/reports. Сначала сравните длительность внешнего span и postgres.INSERT. Если SQL занимает почти всё время HTTP, смотрите pool, соединение и блокировки базы. Если SQL короткий, исследуйте другие участки callback и приём запроса. Не начинайте с случайного trace другого периода.

Для очереди найдите producer и consumer одного trace. Разрыв до job.process показывает ожидание, а вложенный report.compute — сам участок работы. Если consumer отсутствует, это может означать ещё не начатую задачу, потерянный контекст, sampling либо отказ экспорта. Сопоставьте строку PostgreSQL и метрики worker, прежде чем заключать, что код никогда не выполнялся. Отсутствие telemetry не равнозначно отсутствию события.

В Loki можно выполнить запрос по service_name и trace_id, чтобы прочитать безопасные события выбранного случая. Trace ID не ставится индексной меткой потока, но structured metadata позволяет отфильтровать записи. Полный синтаксис зависит от нормализованных имен OTLP. Если фильтр пуст, проверьте сначала базовую выборку сервиса и временной интервал, затем поле корреляции, а не расширяйте сбор до всего body запроса.

Provisioning и изменения панели

Provider читает JSON из смонтированного каталога. У datasource фиксированный UID pulse-prom, поэтому файл не зависит от случайного выбора имени в интерфейсе. При изменении дашборда исходный JSON остаётся местом согласования и передачи. Ручные правки в UI могут быть перезаписаны provisioning или не попасть в ZIP; их нужно явно перенести в источник, если они должны стать частью выпуска.

Полный файл dashboard-provider.yaml состояния 09:

apiVersion: 1
providers:
  - name: Pulse
    type: file
    options:
      path: /var/lib/grafana/dashboards

Дашборд не управляет сбором данных. Добавленная панель с неизвестным именем метрики не заставит приложение измерять новую величину. Например, количества active tasks пока нет: его нужно сначала определить и instrumentировать, затем провести через Collector и только потом запрашивать. Это порядок изменений от факта к отображению. Обратный подход часто создаёт красивый экран из пустых обещаний.

Для нескольких replicas требуется пересмотреть идентичность metric series и группировку. Наш конфиг рассчитан на один API и один worker в лаборатории. Также нет автоматически настроенных exemplars и клика из каждой bucket-точки в trace. Переход выполняется через время и диагностический trace ID. Такая явная граница лучше, чем объявлять связь, которой в конфигурации пока нет.

Ожидаемый результат девятого состояния — четыре provisioned panels, три источника и понятная последовательность поиска причины. Фактические скорости, p95 и число заданий ещё предстоит измерить. Следующий урок превратит часть этих вопросов в правила оповещения и покажет, почему отказ пользовательской операции нужно отличать от потери самого пути наблюдения.

Предыдущий урок: Tempo, Loki и Prometheus · Следующий урок: Ошибки и правила оповещения

Оглавление серии · Исходники всех состояний.

Документация: Prometheus functions, Grafana provisioning.