~/
~/

Содержание

Tempo metrics generator OOM (24Gb RAM nom nom nom)

В какой-то прекрасный день, мы решили переехать с Jaeger на Tempo, для сбора трейсов, метрик и логов в одно место и клевое перемещение между ними в графане при расследовании инцедентов и аварий
Этот пост не о том как все это задеплоить, а о том как Metrics generator съедал больше 24Gb оперативной памяти всеми своими тремя репликами

Metrics generator компонент в составе Grafana tempo, не обязательный к использованию, но предоставляющий крутые метрики, RED Metrics или Golden Signals, которые мы можем получать на основании метрик которые генерит для нас Metrics generator

Note

Metrics generator создает метрики из трейсов которые отражают следующие показатели:

  • Rates show the rate of incoming spans per second.
  • Errors show spans that are failing.
  • Duration displays the amount of time those spans take; represented as a heat map that shows response time and latency.

Что мы используем:

  • Grafana Tempo
  • Tempo Distributed
  • Metrics Generator
  • VictoriaMetrics в качестве remote_write

Весь стек отвечающий за сбор и поставку трейсов в графану был задеплоен так, как это предусмотрено хельм чартом, за исключением тюнинга ресурсов, каких-то очевидных мест в конфигах, нод селекторов, тейнтов и тд, без глубого изменения параметров работы, ибо на берегу не всегда понятно, что необходимо подкрутить

На этом этапе график потребления памяти генератором выглядел так:

image

На самый первый взгляд похоже на какую-то утечку памяти, но на графике видно, ночью когда трейсов генерируется меньше, картинка меняется, начинаем расследование

Note

Скрин сделан уже после снижения лимита до 6Гб, но кушал он все, что дают, поверьте :)

Первое место в которое хочется заглянуть, это логи, в логах нашелся следуюший warn:

level=warn ts=2026-07-11T11:52:14.337926809Z caller=servicegraphs.go:147 tenant=single-tenant component=service-graphs msg="skipped processing of spans" maxItems=10000 err="dropped 81 spans" 
level=warn ts=2026-07-11T11:52:14.341528007Z caller=servicegraphs.go:147 tenant=single-tenant component=service-graphs msg="skipped processing of spans" maxItems=10000 err="dropped 14 spans"

Service Graph достиг лимита в maxItems=10000 и начал дропать спаны, первое, что приходит в голову генератор не справляется с нагрузкой, отбрасывает спаны, растет потребление памяти
На этом этапе генератор работал вообще в 1 реплику, что ж меняем колличество реплик, добавляем еще 2
Логи нормализовались, нагрузка распределилась между тремя репликами, но потребление памяти осталось прежним, мы все так же ловим OOM спустя примерно 40-50 минут работы

Включено оба стандартных процессора:

metrics_generator:
  processor:
    service_graphs:
    span_metrics:

Именно такую конфигурацию рекомендует документация.

Tip

Если кто-то сталкивался с Jaeger то в его составе была джоба, которая тоже строила карту сервисов, давным давно когда я его крутил эта джоба тоже была способна съесть всю оперативную память на постоении карты, тогда карта мне была не принципиальна и я ее просто не использовал
К тому же нашлось issue в котором такое потребление памяти преподносилось как плюс/минус нормальное, ну чего же вы хотели, у вас много сервисов вот и кушает

Service graph занимается тем же, строит карту сервисов и что еще важно, он хранит незавершенные связи между клиентом и сервером
По опыту с Jaeger и соответствующему логу в котором мы видели service graph пробуем отключить его полностью и посмотреть, что будет, поправим конфиг:

metrics_generator:
  processor:
    span_metrics:

Процессор отключен, графы не строятся, потребление памяти не изменилось мы продолжаем ловить OOM

Первая интересная метрика:

tempo_metrics_generator_registry_active_series

image

400-700k active series, на каждый под…

Следующая занятная метрика:

tempo_metrics_generator_registry_series_added_total

image

Почти 4 миллиона добавленных серий, метрика постоянно растет, вплоть до OOMa, т.е. генератор неприрывно создает новые серии, это уже выглядит как то не очень…

Решаю снять снимок памяти приложения в тот момент когда оно близко к ООМу

curl http://localhost:3200/debug/pprof/heap > heap.pb.gz

go tool pprof heap.pb.gz

image

Самое интересное было в top

github.com/prometheus/prometheus/model/labels.Builder.Labels

github.com/prometheus/prometheus/model/labels.ScratchBuilder.Labels

Они занимали почти 75% всей памяти

Еще одна интересная функция:

github.com/grafana/tempo/modules/generator/registry.(*histogram).newSeries

То есть память расходовалась вовсе не на трейсы, она расходовалась на создание новых Prometheus series

По документации Tempo процессор span_metrics автоматически использует следующие intrinsic labels, даже если явно их не указать:

  • service
  • span_name
  • span_kind
  • status_code

Именно span_name здесь выглядит подозрительней всего:

Если приложение создает span вроде

GET /users/123

А затем

GET /users/124

то для Tempo это уже совершенно другая серия, если таких запросов миллионы, получаем миллионы series.

В Tempo есть возможность отключить отдельные intrinsic dimensions.
Поменяем конфигурацию:

metrics_generator:
  processor:
    span_metrics:
      intrinsic_dimensions:
        service: true
        span_name: false
        span_kind: true
        status_code: true

После отключения только одного поля:

span_name: false

График стал выглядить так: (здесь лимиты уже тоже поправлены)

Если span_name содержит динамические значения:

GET /users/123
GET /users/124
GET /users/125
...

то Metrics Generator считает каждую комбинацию labels новой Prometheus series.

Внутри создаются:

  • новые label sets;
  • новые histogram series;
  • новые объекты Prometheus TSDB.

Именно поэтому в heap profile почти вся память находилась в

labels.Builder
labels.ScratchBuilder

Проблема здесь в том, как приложения “отдают” трейсы, что и приводит к бесконтрольному росту кардинальности

В моем случае проблема оказалась вовсе не в Tempo и не в утечке памяти, причиной был span_name, который участвовал в формировании Prometheus series. Отключение одной intrinsic dimension полностью стабилизировало Metrics Generator.

Конечно, правильнее исправить инструментирование приложений и сделать span_name низкокардинальным (например, использовать шаблоны маршрутов вместо конкретных URL), но это совсем другая история…