Tempo metrics generator OOM (24Gb RAM nom nom nom)
В какой-то прекрасный день, мы решили переехать с Jaeger на Tempo, для сбора трейсов, метрик и логов в одно место и клевое перемещение между ними в графане при расследовании инцедентов и аварий
Этот пост не о том как все это задеплоить, а о том как Metrics generator съедал больше 24Gb оперативной памяти всеми своими тремя репликами
Для чего вообще нужен Metrics generator
Metrics generator компонент в составе Grafana tempo, не обязательный к использованию, но предоставляющий крутые метрики, RED Metrics или Golden Signals, которые мы можем получать на основании метрик которые генерит для нас Metrics generator
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
Весь стек отвечающий за сбор и поставку трейсов в графану был задеплоен так, как это предусмотрено хельм чартом, за исключением тюнинга ресурсов, каких-то очевидных мест в конфигах, нод селекторов, тейнтов и тд, без глубого изменения параметров работы, ибо на берегу не всегда понятно, что необходимо подкрутить
Потребление памяти генератором
На этом этапе график потребления памяти генератором выглядел так:

На самый первый взгляд похоже на какую-то утечку памяти, но на графике видно, ночью когда трейсов генерируется меньше, картинка меняется, начинаем расследование
Скрин сделан уже после снижения лимита до 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 минут работы
Первая гипотеза - Service Graphs
Включено оба стандартных процессора:
metrics_generator:
processor:
service_graphs:
span_metrics:
Именно такую конфигурацию рекомендует документация.
Если кто-то сталкивался с Jaeger то в его составе была джоба, которая тоже строила карту сервисов, давным давно когда я его крутил эта джоба тоже была способна съесть всю оперативную память на постоении карты, тогда карта мне была не принципиальна и я ее просто не использовал
К тому же нашлось issue в котором такое потребление памяти преподносилось как плюс/минус нормальное, ну чего же вы хотели, у вас много сервисов вот и кушает
Service graph занимается тем же, строит карту сервисов и что еще важно, он хранит незавершенные связи между клиентом и сервером
По опыту с Jaeger и соответствующему логу в котором мы видели service graph пробуем отключить его полностью и посмотреть, что будет, поправим конфиг:
metrics_generator:
processor:
span_metrics:
Процессор отключен, графы не строятся, потребление памяти не изменилось мы продолжаем ловить OOM
Метрики
Первая интересная метрика:
tempo_metrics_generator_registry_active_series

400-700k active series, на каждый под…
Следующая занятная метрика:
tempo_metrics_generator_registry_series_added_total

Почти 4 миллиона добавленных серий, метрика постоянно растет, вплоть до OOMa, т.е. генератор неприрывно создает новые серии, это уже выглядит как то не очень…
Heap profile
Решаю снять снимок памяти приложения в тот момент когда оно близко к ООМу
curl http://localhost:3200/debug/pprof/heap > heap.pb.gz
go tool pprof heap.pb.gz

Самое интересное было в 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
Где создаются эти series?
По документации Tempo процессор span_metrics автоматически использует следующие intrinsic labels, даже если явно их не указать:
- service
- span_name
- span_kind
- status_code
Именно span_name здесь выглядит подозрительней всего:
Если приложение создает span вроде
GET /users/123
А затем
GET /users/124
то для Tempo это уже совершенно другая серия, если таких запросов миллионы, получаем миллионы series.
Проверяем гипотезу (span_name)
В 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), но это совсем другая история…