Привет, Хабр! Для моих задач Prometheus ел слишком много памяти, и я стал искать альтернативу. Нашёл Deckhouse Prom++ и решил проверить его сам: у Флант в статье про Prom++ заявлено 7,8 раза экономии памяти, но цифры сняты на их данных.

На 500 000 активных серий Prom++ 0.8.15 занял 255 МиБ резидентной памяти, Prometheus 3.15.0 занял 1,11 ГиБ, а 2.55.1 занял 1,09 ГиБ. Процессора при записи уходит в 3,8 раза меньше, запросы при 16 клиентах быстрее на 24%, диск почти такой же. Это медиана 12 прогонов, которые я гонял два дня подряд. По памяти вышло 4,4 раза, а не 7,8, ниже расскажу, почему.

Над стендом мне немного помогал Claude. Все цифры взяты из артефактов в репозитории, их можно перепроверить.

Сравнение с Prometheus 3.15.0
Сравнение с Prometheus 3.15.0

Как я мерил

Один узел k3s (AMD EPYC 7713, 8 ядер, 15,6 ГиБ). Движки идут по одному, в одной реплике, с лимитами 2 ядра и 6 ГиБ, данные в emptyDir, между движками всё удаляется. Сравниваю три версии: Prom++ 0.8.15 (форк Prometheus, где по описанию авторов на C++ переписаны построение блока в памяти и журнал записи WAL), Prometheus 3.15.0 и 2.55.1, от которой Prom++ отпочковался.

Данные идут через remote write. Генератор с фиксированным seed поднимает число серий ступенями до 50, 200 и 500 тысяч с интервалом 15 секунд, на движок уходит 27,4 млн точек, время первой точки закреплено, так что все получают одни и те же байты. Потом 38 запросов PromQL при 1, 4 и 16 клиентах, результат каждого сворачивается в sha256, чтобы сверить ответы.

Разбор VK Cloud показал, что цена хранения точки сильно зависит от профиля метрик. У меня синтетика с короткими лейблами, поэтому цифры могут не подойти вашим данным.

Где я чуть не ошибся

Первый прогон выглядел замечательно, и меня это насторожило. Я открыл сырые данные и увидел, что мой querybench гонял каждый запрос ровно один раз, так что все p50 и p99 были одним замером. Теперь запрос повторяется, пока не наберётся минимум замеров, и число замеров стоит рядом с перцентилями.

Потом Go. В документации я уверенно написал, что Prom++ сам ставит GOMEMLIMIT, а в исходниках этого нет: у него и у 2.55.1 автоподстройка лимита памяти и числа процессоров включается отдельными флагами, а 3.x делает это по умолчанию. Выходило, что 3.x ограничивал себе кучу, а Prom++ жил без лимита. Теперь всем трём я задаю GOMEMLIMIT=5529MiB и GOMAXPROCS=2 руками, а в отчёт попадает то, что движок сам отвечает в /api/v1/status/runtimeinfo.

Были и мелочи: нагрузочный под убивал OOM на запросе с 500 тысячами серий, а один прогон пропал, потому что скрипт удалял данные раньше, чем я их скопировал. Всё исправлено, подробности в репозитории.

Результаты

на 500 тысячах серий

Prom++ 0.8.15

Prometheus 3.15.0

Prometheus 2.55.1

резидентная память

255 МиБ

1,11 ГиБ

1,09 ГиБ

рабочий набор cgroup

176 МиБ

1,06 ГиБ

1,03 ГиБ

процессор при записи, ядер

0,05

0,19

0,23

каталог данных

281 МиБ

303 МиБ

303 МиБ

Между самым быстрым и самым медленным прогоном разброс меньше 8%, кроме рабочего набора Prom++ (12%). На серию выходит около 0,5 КБ у Prom++ и 2,8 КБ у Prometheus. На меньших ступенях разрыв скромнее: на 50 тысячах серий 104 МиБ против 185 у 3.15 и 168 у 2.55.1, на 200 тысячах 159 против 482 и 439. По процессору разрыв даже больше, чем по памяти, а диск почти не отличается: сжатие точек у всех одинаковое.

Резидентная память во время записи
Резидентная память во время записи

У Prometheus память растёт ступенями и заметно скачет вверх и вниз, у Prom++ идёт ровной пилой с небольшой амплитудой. Видимо, из-за того, что основная часть данных у него лежит вне кучи Go, но я это не проверял.

Запросы, геометрическое среднее медиан по всем 38:

параллельных клиентов

Prom++ 0.8.15

Prometheus 3.15.0

Prometheus 2.55.1

1

298 мс

348 мс

345 мс

4

682 мс

828 мс

839 мс

16

2,53 с

3,35 с

3,46 с

Prom++ быстрее на 14% при одном клиенте и на 24% при шестнадцати. Самый тяжёлый запрос, range_quantile, идёт 4,5 секунды против 5,0. Есть пара запросов, где он чуть медленнее: quantile_over_time (639 мс против 592 у 3.15) и regex_name (826 против 781).

Время запросов при разной параллельности
Время запросов при разной параллельности

Пять запросов, где ответы разошлись

33 из 38 запросов дали у всех трёх движков побайтно одинаковый результат. Пять остальных (alerting_shape, double_subquery, range_quantile, range_sum_rate и recording_rule_shape) разошлись так: Prom++ совпал с Prometheus 3.15, а 2.55.1 ответил иначе. Это странно, ведь Prom++ форк как раз 2.55.1.

Во всех пяти есть rate или _over_time по диапазону, а в Prometheus 3.0 диапазон [5m] стал открытым слева: точка ровно на левой границе окна больше не считается. В promql/engine.go у 2.55.1 стоит floats[drop].T < mint, у 3.x и у Prom++ 0.8.15 уже <=. Мои синтетические точки лежат ровно на границах окон, поэтому 2.55.1 видит на одну точку больше. При переезде с 2.x на Prom++ алерты с rate могут чуть иначе вести себя на границах окон. На реальных данных точки редко попадают точно на границу, но знать об этом стоит.

Почему 4,4, а не 7,8

Точно назвать главную причину я не могу. Скорее всего, дело в данных: во Флант мерили на телеметрии настоящих кластеров с длинными лейблами, а выигрыш Prom++ растёт вместе с их длиной, у моего генератора лейблы короткие. Кроме того, у них Prometheus 2.53, а я выровнял GOMEMLIMIT и GOMAXPROCS, и при разных настройках Go разрыв выглядит иначе.

Что остаётся за кадром

Это один узел и одна реплика, ступени по несколько минут, синтетические данные и общий узел с компонентами управляющего слоя (поэтому прогоны разнесены по времени суток). WAL у Prom++ в своём формате, а у Prometheus по умолчанию сжатие snappy, так что я сравнивал то, что получаешь из коробки. GOMEMLIMIT не покрывает память C++, которую Prom++ берёт через jemalloc. Репликацию, долгое хранение и Thanos я не мерил.

Повторить у себя

Код стенда: xdearboy/prompp-vs-prometheus, нагрузочный код отдельно: xdearboy/prom-loadgen. Один прогон идёт около двух с половиной часов:

export BENCH_NODE=<узел>
scripts/node-overlay.sh
scripts/harness.sh
scripts/run.sh

Серию из 12 прогонов можно запустить из пода внутри кластера (scripts/runner.sh). Если собираетесь переезжать с Prometheus, прогоните стенд на своих метриках и проверьте алерты с rate по диапазону.