
Меня зовут Стас Погоржельский, я технологический евангелист VK Cloud и последние месяцы гоняю Prometheus и VictoriaMetrics на одном стенде, чтобы понять, сколько на самом деле стоит одна точка метрики на диске.
Обе системы делают одно и то же: принимают миллионы точек в минуту, хранят их месяцами и быстро отдают в алерты и дашборды. Обе сжимают точку до байтов и долей байта. Именно эти байты, умноженные на миллиарды точек в сутки, превращаются в счёт за диск. Ошибка в расчёте цены сэмпла заканчивается переполненным диском под мониторингом, урезанным retention или незапланированным расходом.
Когда место под метрики кончается, первым на планёрке звучит «сменим движок». Публичные бенчмарки подталкивают к тому же: Флант показывает, что Prom++, форк Prometheus, тратит памяти в 7,8 раза меньше Prometheus v2 (разбор на Habr), VictoriaMetrics показывает трёхкратную экономию диска против Grafana Mimir (бенчмарк VictoriaMetrics, замер вендора, 2022 год).
На моём стенде картина оказалась сложнее. В Prometheus 3.13 и VictoriaMetrics 1.149 ушёл один и тот же набор: 2,88 млн сэмплов, шесть профилей данных, один генератор с фиксированным seed. Внутри одного движка цена сэмпла различалась в 139 раз у VictoriaMetrics и в 12,8 раза у Prometheus. Между движками на одних данных разрыв доходил до 30,7 раза на шести профилях и до 34,3 раза в опыте с точностью. На равномерно случайных float64 движки менялись местами. Один лишний знак после запятой поднимал цену сэмпла у Prometheus в восемь раз, у VictoriaMetrics на 12%.
Главное из статьи: Профиль метрик важнее выбора движка
Одни и те же метрики заняли от 0,058 до 8 байт на точку у VictoriaMetrics и от 0,61 до 7,8 байта у Prometheus.
На стенде получилось следующее:
Разброс внутри одного движка оказался шире разрыва между движками. У VictoriaMetrics — 139 раз против максимального разрыва в 30,7 раза с Prometheus, то есть в 4,5 раза шире
Один лишний знак после запятой стоил Prometheus восьмикратной цены сэмпла, а VictoriaMetrics — 12%. Округление до целых сокращало место на диске в 8,6 и 2,6 раза соответственно
Сто тысяч короткоживущих серий вместо тысячи долгих поднимали размер индекса в 28 раз у Prometheus и в 38 раз у VictoriaMetrics при том же числе точек
Диапазон «1–2 байта на сэмпл» из документации Prometheus не описал ни один из шести профилей. Два профиля оказались ниже нижней границы, четыре ушли выше верхней, худший — в 3,9 раза
Отсюда следует практический порядок действий: сначала проверить точность значений, затем метки, после этого стабильность скрейпа и только потом выбирать движок. На масштабе миллиона активных серий один первый шаг даёт разницу между 20 и 2,4 ГБ на диске в сутки — больше, чем любой переезд.
Преимущество VictoriaMetrics на некоторых профилях держится на одном приёме. Перед кодированием движок приводит float к целой мантиссе с десятичной экспонентой. Это хорошо работает, пока в значении мало значащих цифр.
Все данные ниже сняты на моём стенде и в моих условиях: один узел с двумя ядрами, синтетические данные, дефолтные настройки движков, без remote write и реплик. Абсолютные цифры на живом парке будут другими. Формат чанка и выбор кодирования зависят от формы ряда, поэтому здесь важнее отношения между профилями, а не сами значения.
Две статьи расходов: из чего складывается цена хранения метрик
Сэмпл, или точка, состоит из серии, метки времени и значения. Серия — это набор меток, например:
synthetic_gauge{ instance="node-3:9100", pod="workload-17", zone="ru-msk-1", job="synthetic", env="prod", shard="7" }
Значение хранится как float64, метка времени — как int64 в миллисекундах.
Стоимость хранения складывается из двух частей. За точку приходится платить байтами внутри чанка, за серию — байтами в индексе и постоянным местом в памяти. Если смешать эти расходы, появляются решения вроде «давайте писать реже», хотя на дешёвых профилях почти вся цена сидит в индексе.
Формат Prometheus TSDB опубликован. Его же используют Thanos, Mimir и Prom++, поэтому механику удобно разбирать на нём. VictoriaMetrics хранит те же сэмплы по-другому.
Официальная формула Prometheus для расчёта диска выглядит так:
Needed_disk_space = retention_time_seconds × ingested_samples_per_second × bytes_per_sample
Первые два множителя можно узнать заранее: retention задаётся в конфигурации, скорость приёма считается по числу целей. Для третьего множителя документация даёт диапазон: «Prometheus stores an average of only 1–2 bytes per sample». На моём стенде в него не попал ни один из шести профилей.
Блочная модель Prometheus
Почему метрики не пишут в обычную СУБД построчно, объяснил Фабиан Райнартц, когда проектировал современный формат TSDB. Один сэмпл занимает 16 байт, тогда как страница на диске имеет размер 4 КиБ. Поэтому построчная запись отдельных сэмплов была бы неэффективной: ради нескольких байт пришлось бы обращаться к целой дисковой странице. Документ был опубликован в 2017 году и описывает подход, на котором построена третья версия хранилища.
При этом для временных рядов важен не только способ записи, но и характер чтения. Райнартц отмечает, что свежие данные читают несравнимо чаще старых. Именно поэтому хранилище построено вокруг блочной модели: активные данные обрабатываются отдельно от уже сформированной исторической части.
Сначала сэмплы поступают в head-блок, который находится в памяти. Одновременно каждый скрейп записывается в WAL, чтобы данные можно было восстановить после перезапуска. Для этого WAL разбивается на сегменты по 128 МБ. По мере заполнения чанк сбрасывается на диск и затем вновь отображается в адресное пространство процесса через mmap. В самой памяти при этом остаётся лишь 8-байтная ссылка: четыре байта занимают номер файла, ещё четыре — смещение внутри него. Подробнее этот механизм описан в разборе mmap head chunks.
Далее, примерно каждые два часа, head сбрасывает накопленные данные в неизменяемый блок на диске, а WAL усекается по созданному чекпойнту. После этого в работу вступает компакция: она объединяет небольшие блоки в более крупные, чтобы со временем уменьшить их количество. Максимальный размер такого блока ограничен 10% периода retention или 31 днём — в зависимости от того, какое значение меньше. Наконец, сами сегменты чанков разделяются на файлы размером до 512 МБ.

Откуда взялись 1,37 байта
В 2015 году инженеры Facebook опубликовали работу Gorilla. Из неё в отрасль пришла цифра, которую до сих пор повторяют в статьях и докладах: «сжимать временные ряды в среднем до 1,37 байта на точку — то есть уменьшать объём данных в 12 раз». Двенадцатикратное сокращение считается от наивных 16 байт на точку.
Временные метки сжимаются методом дельта-дельта: вместо самих значений хранится изменение разницы между соседними метками. Например, при скрейпе каждые 30 секунд разница между метками всегда равна 30 000 мс. Поэтому изменение этой разницы равно нулю, а ноль кодируется одним битом. В Gorilla благодаря этому примерно 96% временных меток удавалось представить всего одним битом.
Значения сжимаются через XOR с предыдущим значением. Если число не изменилось, XOR даёт ноль — ещё один бит. Если значение изменилось, кодируется блок значащих бит с указанием числа отброшенных ведущих нулей.
В Gorilla:
около 51% значений сжимаются до одного бита
ещё 30% используют схему с контрольными битами 10 и средним размером 26,6 бита
оставшиеся 19% используют схему 11 со средним размером 36,9 бита
Замеры делались на выборке из 440 тысяч реальных меток времени и 1,6 млн реальных значений. Финальная проверка проводилась на полном продовом наборе — примерно двух миллиардах серий.
Из Gorilla также пришёл двухчасовой блок, который Prometheus использует по умолчанию. В статье прямо сказано: «blocks that extend longer than two hours provide diminishing returns for compressed size». Двух часов достаточно, чтобы выйти на 1,37 байта.
Эту цифру можно попытаться пересчитать по опубликованному распределению:
0,51×1+0,30×26,6+0,19×36,9=15,5 бита
Если добавить метки времени, где 96% значений занимают один бит, а остальные 4% имеют переменную длину, получится ещё примерно полтора бита. Итого — около 17 бит, или 2,1 байта на точку, а не заявленные 1,37.
Противоречия здесь нет. Распределение контрольных бит снято на выборке из 1,6 млн значений, а 1,37 байта — это среднее по продовому набору из двух миллиардов серий. Это разные замеры, и складывать их в одну оценку нельзя. 1,37 байта — результат конкретного набора метрик Facebook в 2015 году. Формат гарантирует механику кодирования, а не этот размер.
Как Prometheus сжимает значения: XOR-чанк
В Prometheus механика Gorilla реализована в XOR-чанке. По спецификации формата чанк начинается с числа сэмплов в uint16, первой метки времени в varint и первого значения в полном float64.
Второй сэмпл содержит простую дельту метки времени. Начиная с третьего идут пары:
дельта-дельта временной метки переменной длины, от 1 до 68 бит;
XOR значения переменной длины, от 1 до 77 бит.
Сверху добавляются накладные расходы чанка: длина в uvarint, байт кодировки и четыре байта CRC-32C.
Возьмём утилизацию CPU, округлённую до двух знаков:
50,17 → 50,23 → 50,29
На глаз числа близкие и меняются плавно. В float64 это три разные мантиссы. У них совпадают знак и экспонента, но младшие биты расходятся. Дробная часть выбивает значение из однобитного кодирования, и XOR выдаёт десятки значащих бит.
Шаг между этими значениями равен 0,06. В двоичном виде это бесконечная периодическая дробь, обрезанная на 52-м бите мантиссы. На гладком gauge с двумя знаками Prometheus разошёлся с VictoriaMetrics в 16 раз. Разницу задаёт десятичная нормализация, которую VictoriaMetrics применяет до кодирования.

control 10 и control 11 по замерам Gorilla стоят в среднем 27 и 37 бит, а потолок по спецификации составляет 77 бит. Десятичная нормализация в VictoriaMetrics
VictoriaMetrics не хранит float64 как float64. Перед кодированием значение превращается в пару «целая мантисса плюс десятичная экспонента». Функция FromFloat в lib/decimal/decimal.go преобразует −1,234 в v = −1234 и e = −3. AppendFloatToDecimal делает то же для всего массива, используя одну общую экспоненту на блок.
Точность ограничена константой conversionPrecision = 1e12, то есть примерно 12 значащими цифрами. Значение 50,17 становится целым 5017, а ряд округлённых процентов превращается в целочисленный ряд.
Кодирование выбирается по форме ряда. В lib/encoding/encoding.go определено шесть типов. Комментарии в коде говорят прямо:
MarshalTypeConst— ряд из одного постоянного значенияMarshalTypeDeltaConst— ряд с постоянной дельтойMarshalTypeZSTDNearestDelta— для gauge-временных рядовMarshalTypeZSTDNearestDelta2— для counter-временных рядовещё два варианта без ZSTD — на случай, когда сжатие не помогает
ZSTD включается, если блок занимает не меньше 128 байт и сжатый вариант даёт размер менее 90% от исходного.
Получаются два разных способа сжать точку. Prometheus упаковывает float64 битовыми операциями. VictoriaMetrics приводит поток float к целым числам, а затем сжимает целочисленные дельты универсальным компрессором.
На округлённых значениях второй подход выигрывает в 8–34 раза, на монотонных счётчиках — до 30,7 раза. На равномерно случайных float64 он, наоборот, проигрывает 3%.
Шесть профилей метрик для одного стенда
Один генератор с фиксированным seed создаёт 1000 серий по 2880 точек с интервалом 30 секунд. Это сутки данных и 2 880 000 сэмплов на профиль. Каждая серия содержит шесть меток.
Один и тот же набор записывается в двух форматах: OpenMetrics для Prometheus и Prometheus exposition с миллисекундными временными метками для VictoriaMetrics.
def values(profile, n, rnd): if profile == "constant": # idle-gauge, значение не меняется for in range(n): yield 1.0 elif profile == "counter": # монотонный счётчик, постоянный шаг с редким скачком v, step = float(rnd.randint(0, 10000)), rnd.randint(1, 50) for i in range(n): v += step if i % 97 else step 3 yield v elif profile == "gauge_smooth": # утилизация в процентах, 2 знака phase = rnd.random() math.pi for i in range(n): yield round(50 + 40 math.sin(phase + i / 120.0), 2) elif profile == "gauge_random": # худший случай для XOR for _ in range(n): yield rnd.random() 1e6
Здесь оставлена только суть генератора. Шумный gauge, профиль, который флапает между 0 и 1, а также профили с разной точностью устроены похожим образом. Метки серий и код записи на диск опущены.
Методика эксперимента: загрузка и измерения
В Prometheus данные заливаются штатным бэкфиллом, без запуска сервера:
promtool tsdb create-blocks-from openmetrics \ --max-block-duration=24h data.om ./prom-data promtool tsdb analyze ./prom-data В VictoriaMetrics — через импорт, а затем принудительный сброс и слияние: victoria-metrics-prod -storageDataPath=./vm-data \ -httpListenAddr=127.0.0.1:8428 -retentionPeriod=100y & curl -s -X POST --data-binary @data.prom \ 'http://127.0.0.1:8428/api/v1/import/prometheus' curl -s 'http://127.0.0.1:8428/internal/force_flush' curl -s 'http://127.0.0.1:8428/internal/force_merge'
После этого я беру du -sb по каталогу данных и делю размер на число сэмплов. Метрика грубая, но она измеряет ровно то, за что приходит счёт.
После бэкфилла я запускал сервер Prometheus и дополнительно измерял задержку запросов и память. У обоих движков задержка получилась одинаковой: 7–8 мс на sum, на avg_over_time за час и на topk(10, max_over_time[24h]). На 2,88 млн сэмплов и одном узле это скорее проверка, что оба движка отвечают, чем сравнение скорости.
RSS в дефолтной конфигурации составил 88 МБ у Prometheus и 182 МБ у VictoriaMetrics.
Заодно я проверил документированную величину чанка. Флаг --max-block-duration=24h при заливке суток данных создал не один блок, а два: 1 787 000 сэмплов в первом и 1 093 000 во втором. В первом блоке promtool показал 15 000 чанков — 119 сэмплов на чанк. По всему набору из 2 880 000 сэмплов получилось 25 000 чанков, то есть в среднем по 115 сэмплов.
Лимит в 120 сэмплов на чанк, который описан в разборе head-блока Prometheus и документации Mimir, работает как потолок. Фактическое заполнение на стенде составило 115–119 сэмплов.
Ловушка с кэшами VictoriaMetrics
Я сам сначала попался на том, что du -sb по всему -storageDataPath завышает размер. При остановке VictoriaMetrics создаёт внутри него каталог cache и сбрасывает туда прогретые кэши:
metricName_tsid
metricID_tsid
metricID_metricName
rollupResult
На моём наборе кэши занимали 1,46 МБ при тысяче серий и 4,51 МБ при ста тысячах. Их размер почти не зависел от объёма самих данных.
Поэтому степень завышения менялась от профиля к профилю:
На константе, где сами данные занимали 0,17 МБ, кэш завышал размер в 9,6 раза.
На гладком gauge с размером 1,28 МБ — вдвое.
На случайных
float64— только на 6%.
Кэши восстанавливаются автоматически и к хранилищу не относятся. Измерять нужно только подкаталог data, где лежат indexdb, small и big, причём на работающем процессе и после force_merge:
du -sb ./vm-data/data # хранилище: индекс плюс значения du -sb ./vm-data/data/indexdb # только индекс du -sb ./vm-data/cache # кэши, в размер хранилища не входят
Первая команда показывает размер хранилища целиком, вторая — только индекс. Разница между ними и есть цена значений. У Prometheus индекс лежит одним файлом внутри блока, поэтому его размер можно получить тем же du, а цену значений посчитать вычитанием размера файлов чанков.
Результаты: размер одного сэмпла по профилям
Профиль данных |
Prometheus 3.13 |
VictoriaMetrics 1.149 |
Prom / VM |
VM: индекс |
VM: значения |
|---|---|---|---|---|---|
константа |
0,614 |
0,058 |
10,6 |
0,056 |
0,002 |
0/1 с редким переключением |
0,636 |
0,106 |
6,0 |
0,059 |
0,047 |
счётчик, монотонный |
2,146 |
0,070 |
30,7 |
0,055 |
0,014 |
gauge шумный, 2 знака |
5,957 |
0,486 |
12,3 |
0,057 |
0,429 |
гладкий gauge, 2 знака |
7,100 |
0,444 |
16,0 |
0,057 |
0,387 |
случайные float64 |
7,836 |
8,064 |
0,97 |
0,057 |
8,007 |
Все значения в таблице — это байты на сэмпл. Две последние колонки показывают, из чего складывается размер VictoriaMetrics.

Константа. Константа стоит 0,614 байта в Prometheus и 0,058 байта в VictoriaMetrics, то есть VictoriaMetrics занимает в 10,6 раза меньше места.
У Prometheus работает XOR: неизменившееся значение кодируется одним битом, остаются накладные расходы чанка и блока. У VictoriaMetrics срабатывает MarshalTypeConst. Значения занимают 0,002 байта на сэмпл, практически ничего. Почти всё пространство уходит на индекс.
Монотонный счётчик. На монотонном счётчике разрыв максимальный: 30,7 раза. Prometheus тратит 2,1 байта на сэмпл, потому что каждое новое значение отличается от предыдущего и XOR выдаёт значащие биты.
VictoriaMetrics расходует 0,070 байта: ряд с почти постоянным шагом попадает в ZSTDNearestDelta2, где кодируются вторые разности. Идеального DeltaConst генератор не даёт, потому что раз в 97 точек утраивает шаг.
Шумный gauge. Шумный gauge оказался для Prometheus дешевле гладкого: 5,96 байта против 7,10. У шума чаще совпадает число ведущих нулей в мантиссе, поэтому XOR чаще попадает в ветку с контрольными битами 10 и средним размером 26,6 бита, а не в 11 со средним размером 36,9 бита.
Профиль 0/1. Профиль, который редко переключается между 0 и 1, стоит 0,64 байта, почти как константа. Переключения происходят редко, поэтому на большинстве шагов XOR возвращает ноль.
Гладкий gauge с двумя знаками. Это профиль из примера с утилизацией CPU. Здесь движки расходятся в 16 раз. Prometheus тратит 7,1 байта на сэмпл, что в 3,6 раза выше верхней границы официального диапазона «1–2 байта».
VictoriaMetrics укладывается в 0,444 байта, потому что представляет 50,17 как целое 5017 с экспонентой.
Случайные float64. Это единственная строка, в которой VictoriaMetrics занимает больше места: 8,064 байта против 7,836 в Prometheus. Сжимать здесь нечего. У обоих движков остаётся почти полный float64 плюс накладные расходы, а десятичная нормализация VictoriaMetrics добавляет ещё 3%.
Prometheus сохраняет float64 бит в бит, а VictoriaMetrics сокращает значение до 12 значащих цифр из почти 16 возможных.
Разброс внутри Prometheus составил 12,8 раза: от 0,614 до 7,836 байта. У VictoriaMetrics — 139 раз: от 0,058 до 8,064 байта. Разрыв между движками на одинаковых данных лежит в диапазоне от 0,97 до 30,7 раза.
У VictoriaMetrics разброс внутри движка оказался в 4,5 раза шире максимального разрыва с Prometheus. У Prometheus эти величины одного порядка.
Тест проводился на тысяче длительно существующих временных рядов. Если кардинальность растёт, вместе с индексом увеличивается и минимально необходимый объём памяти. При этом различия между профилями данных оказались заметнее, чем различия между самими движками. Оценивать экономичность движка поэтому нужно в двух плоскостях: сравнивать типичные расходы разных движков между собой и отдельно учитывать, насколько их потребление меняется в зависимости от кардинальности и структуры данных.

Разрабатывайте и масштабируйте сервисы в VK Cloud
Ускоряйте запуск продуктов и снижайте расходы на инфраструктуру
Как точность значений влияет на размер метрик
В следующем опыте менялось только число знаков после запятой. Сам ряд оставался прежним: синусоида с той же амплитудой и периодом.
Точность значения |
Prometheus, байт/сэмпл |
VictoriaMetrics, байт/сэмпл |
Prom / VM |
|---|---|---|---|
целые |
0,821 |
0,172 |
4,8 |
1 знак |
6,580 |
0,192 |
34,3 |
2 знака |
7,100 |
0,444 |
16,0 |
3 знака |
7,199 |
0,841 |
8,6 |
6 знаков |
7,211 |
3,043 |
2,4 |
случайный float64 |
7,836 |
8,064 |
0,97 |

У Prometheus резкий рост происходит уже на первом знаке после запятой: 0,821 → 6,580, в восемь раз. Дальше начинается почти плато: разница между одним и шестью знаками составляет лишь 10%. Дробная часть один раз переводит кодирование в режим переменной длины, после чего дорожать почти нечему.
У VictoriaMetrics кривая другая. Первый знак после запятой стоит 12 % (0,172 → 0,192), потому что после умножения на 10−e10^{-e}10−e ряд остаётся целочисленным, и к нему применяются целочисленные дельты. Затем цена растёт плавно. К шести знакам она увеличивается в 17,7 раза относительно целых.
На равномерно случайных float64 оба движка сходятся примерно на восьми байтах на сэмпл.
Даже небольшая правка экспортёра Prometheus, которая устраняет лишнюю точность значений, даёт немедленный эффект. В VictoriaMetrics выгода от такого изменения тоже заметна, но нарастает постепенно: переход от двух знаков после запятой к целым значениям уменьшает объём данных в 2,6 раза, а от шести знаков — уже в 17,7 раза.
На значениях с точностью от одного до трёх знаков после запятой переход с Prometheus на VictoriaMetrics даёт сокращение объёма в 8–34 раза.
Цена серии: индекс, labels и кардинальность
Стоимость хранения серии и отдельной точки различается. В VictoriaMetrics доля, приходящаяся на сами значения, может составлять от 3% для постоянного ряда до 99% для ряда со случайными float64. Остальной объём занимает индекс. На данных, которые хорошо сжимаются, именно индекс становится главным источником расхода места.
В блоке Prometheus используется инвертированный индекс. Согласно спецификации формата, индексный файл состоит из семи секций, если учитывать оглавление и устаревшие разделы. Существенный вклад в размер обычно вносят четыре из них.
Две основные секции — таблица символов и постинги. Таблица символов содержит все уникальные строки: имена метрик, ключи меток и значения меток. Каждая строка хранится только один раз. Строки упорядочены лексикографически и записываются как длина в формате uvarint, за которой идут байты строки. В описании серии вместо самих строк указываются номера соответствующих записей в этой таблице.
Постинги образуют собственно инвертированный индекс. Для каждой пары вида метка = значение хранится отсортированный по возрастанию список ссылок на серии, которым соответствует эта пара. При росте кардинальности увеличиваются прежде всего таблица символов и списки постингов.
Оставшиеся две заметные секции дают сравнительно небольшой вклад — обычно единицы процентов. В секции серий записано, какими символами описывается серия и в каких чанках находятся её данные. Записи выровнены по 16 байтам, поэтому идентификатор серии вычисляется как смещение записи, делённое на 16. В таблице смещений постингов есть запись для каждой пары метка = значение; при этом имя и значение метки записываются там целиком, а не представлены ссылками на таблицу символов.
Например, для запроса synthetic_gauge{zone="ru-msk-1"} движок получает список серий для zone="ru-msk-1" и список серий для имени метрики synthetic_gauge. Затем он пересекает эти списки и получает идентификаторы подходящих рядов. Поскольку постинги отсортированы, пересечение выполняется линейным проходом, без предварительной сортировки и без построения дополнительных структур.
У VictoriaMetrics используется собственный индекс, который хранится в indexdb. Его устройство приходится восстанавливать по исходному коду и наблюдаемому поведению системы. Зато размер indexdb можно измерить отдельно, например через du, поэтому в сравнительных таблицах индекс VictoriaMetrics имеет смысл показывать в отдельной колонке.

Тысяча долгих серий против ста тысяч коротких
Почти одинаковое число сэмплов, 2,88 против 2,9 млн, я разложил двумя способами.
Разбивка |
Сэмплов |
Prom, байт/сэмпл |
Индекс Prom |
VM, байт/сэмпл |
Индекс VM |
|---|---|---|---|---|---|
1000 серий × 2880 точек |
2 880 000 |
7,100 |
0,454 МБ |
0,444 |
0,164 МБ |
100 000 серий × 29 точек |
2 900 000 |
11,672 |
12,68 МБ |
3,375 |
6,15 МБ |

При том же объёме данных индекс Prometheus вырос в 28 раз, а VictoriaMetrics — почти в 38 раз. Стоимость одного сэмпла увеличилась в 1,6 и 7,6 раза соответственно. Число точек не изменилось: изменилось только то, как они распределены между сериями.
VictoriaMetrics хорошо сжимает значения, поэтому на таких данных индекс становится основной статьёй расходов.
Причин две:
Для каждой серии нужны запись в таблице символов, запись в секции серий и вхождения в постинги для каждой метки.
Короткие серии не позволяют полностью заполнить чанк. Он закрывается после 120 сэмплов либо на границе блока — в зависимости от того, что наступит раньше. Если в серии не 120, а 29 точек, заголовок и CRC чанка приходятся на вчетверо меньшее число сэмплов.
У VictoriaMetrics внутреннее устройство другое, но зависимость от числа серий сохраняется. На каждую серию создаются записи в indexdb, поэтому на этом наборе данных индекс вырос ещё сильнее.
Для тысячи серий с шестью метками promtool tsdb analyze показывает 1 026 уникальных пар метка = значение и 6 000 вхождений в списки постингов — по одному для каждой из шести меток. Индекс Prometheus занимает 0,454 МБ в двух блоках, или около 227 байт на серию в каждом блоке. У VictoriaMetrics получается 164 байта на серию.
При 100 тысячах серий по 29 точек все данные помещаются в один блок. Удельный размер индекса снижается до 127 байт на серию в Prometheus и 62 байт в VictoriaMetrics. Одно и то же значение метки используется многими сериями, поэтому стоимость его хранения распределяется между ними. При этом общий размер индекса всё равно продолжает расти.
Память на серию и арифметика кардинальности
Независимые оценки расходятся от 1 до 4,5 КБ памяти на серию. В одном разборе приводят 1–3 КБ. В другом — около 3 КБ на свежие сэмплы и ещё 1,5 КБ на head-чанки (источник).
Там же описан инцидент на кластере из 50 узлов: при двух миллионах активных серий «Prometheus memory crossed 60GB and still OOMed». Это около 30 КБ на серию, то есть на порядок выше обеих оценок.
Арифметика взрыва кардинальности считается в уме. Пять HTTP-методов, 50 кодов ответа и 100 эндпоинтов дают 25 000 серий, и это нормально. Но стоит добавить user_id при миллионе пользователей, и получится 25 млрд потенциальных серий. Метка одна, а множитель — миллионный.
Гистограммы множат серии
Классическая histogram разворачивается в отдельную серию для каждой корзины, а также создаёт серии _sum и _count. Поэтому метрика с десятью корзинами превращается в двенадцать серий.
Затем это число умножается на комбинации меток инстанса, эндпоинта и кода ответа. Например, гистограмма латентности для трёх инстансов и десяти эндпоинтов создаёт 360 серий ещё до добавления разбиения по кодам ответа.
Значения в корзинах монотонно растут и сжимаются так же, как монотонный счётчик: до 0,070 байта на сэмпл в VictoriaMetrics и до 2,1 байта в Prometheus. Поэтому в VictoriaMetrics основная стоимость хранения таких гистограмм приходится на индекс.
Churn: серии, которые живут пятнадцать минут
На размер хранилища влияет не только число активных серий, но и скорость, с которой меняется их состав. В официальном гайде VictoriaMetrics churn предлагают оценивать на таком примере: сервис публикует 1 000 серий и работает в 100 репликах, значит, одновременно существует 100 000 активных серий. После передеплоя имена pod'ов во всех репликах меняются, и сервис создаёт ещё 100 000 новых временных рядов.
Старые серии при этом не исчезают из индекса сразу, а остаются в нём до окончания retention. Если сервис деплоится пять раз в день, за сутки для одного сервиса появляется до 500 000 новых серий.
Тот же гайд отдельно предупреждает, что высокий churn ухудшает эффективность сжатия. Его влияние видно и в таблице выше. Набор из 100 тысяч коротких серий одновременно моделирует высокую кардинальность и высокий churn, поэтому отделить эффект одного фактора от другого на этих данных нельзя.
Это также нижняя оценка расходов. В замер уже попали незаполненные чанки, но к ним добавляется индекс от неактивных серий, который сохраняется до конца retention. Серия, существующая 15 минут при скрейпе раз в 30 секунд, успевает получить только 30 из 120 сэмплов. Чанк закрывается незаполненным. Если вместо одного деплоя в день выполнять пять, таких частично заполненных чанков станет в пять раз больше.
Число активных серий можно оценивать следующими запросами.
В Prometheus:
sum(max_over_time(prometheus_tsdb_head_series[24h]))
В VictoriaMetrics:
sum(max_over_time(vm_cache_entries{type="storage/hour_metric_ids"}[24h]))
Короткие блоки, нестабильный scrape и отложенное удаление
Помимо кардинальности и churn, на размер хранилища влияют короткие блоки, нестабильный интервал скрейпа и отложенное удаление данных.
Индекс хранится внутри каждого блока. Поэтому при большем числе блоков таблица символов повторяется чаще и занимает больше места. Чтобы оценить этот эффект, один и тот же набор данных был записан дважды: сначала в блоки по два часа, затем — в суточные блоки.
Длина блока |
Индекс |
Итого, байт/сэмпл |
|---|---|---|
суточные блоки |
0,454 МБ |
7,100 |
блоки по 2 часа |
1,602 МБ |
7,500 |
Индекс вырос в 3,5 раза, а общий размер хранилища — на 5,6%. Пока основную часть объёма занимают чанки, повторение индекса в нескольких блоках добавляет лишь несколько процентов. Поэтому короткие блоки почти не влияют на степень сжатия.
На запросах их стоимость заметнее. За сутки, разбитые на двенадцать двухчасовых блоков, движок выполняет двенадцать пересечений списков постингов. Для тех же данных в суточных блоках достаточно двух пересечений. На наборе из 2,88 млн сэмплов разница укладывается в те же 7–8 мс, но на большом количестве серий и блоков она будет расти.
Однобитное кодирование временной метки возможно только при постоянном интервале скрейпа: тогда дельта-дельта остаётся нулевой. На практике интервал меняется из-за задержек сети, пауз GC на целевой системе и нагрузки на сервер. Любое отклонение переводит временную метку из однобитного представления в запись переменной длины — вплоть до 68 бит. В худшем случае стоимость одной метки времени увеличивается в 68 раз. В этом генераторе интервал был фиксированным и составлял 30 секунд.
Удаление реализовано через tombstone. Персистентный блок неизменяем, и изменить его можно только полной перезаписью (разбор persistent block и индекса). Поэтому удаление серии сначала создаёт tombstone — маркер с идентификатором серии и временным диапазоном, — а сам блок остаётся без изменений. В WAL tombstone представлен третьим типом записи, наряду с сериями и сэмплами (разбор WAL и checkpoint).
Физически данные удаляются только во время следующей компакции блока: в пределах 10% заданного retention или 31 дня. Tombstone не ухудшает сжатие, но откладывает освобождение дискового пространства. Поэтому после удаления высококардинальных метрик место на диске вернётся только после очередной компакции.
Даунсэмплинг старых данных
Старые данные запрашивают редко, поэтому для них можно снизить разрешение и хранить меньше точек. В самом Prometheus встроенного даунсэмплинга нет. Эту задачу можно частично решить recording rules: они записывают заранее рассчитанные агрегаты в новые серии, но исходные серии и их точки при этом никуда не исчезают
groups: - name: downsample-5m interval: 5m rules: - record: synthetic:gauge_smooth:avg5m expr: avg_over_time(synthetic_gauge_smooth[5m]) - record: synthetic:gauge_smooth:max5m expr: max_over_time(synthetic_gauge_smooth[5m])
Правило записывает агрегат в новую серию, а исходную оставляет без изменений. Экономия появляется только тогда, когда сырые данные удаляются по retention, а агрегированные серии хранятся дольше.
Полноценный даунсэмплинг выполняется уровнем выше — в компакторе. Thanos Compactor строит из исходных блоков копии с разрешением 5 минут и 1 час, а при запросе выбирается подходящее разрешение в зависимости от длины интервала. Grafana Mimir устроен сходным образом: его формат хранения основан на Prometheus TSDB. У VictoriaMetrics даунсэмплинг встроен в движок, но доступен только в Enterprise-версии.
Выбор зависит от retention. Если сырые данные хранятся месяц, даунсэмплинг обычно не нужен: больше эффекта даст пересмотр точности значений и набора меток, причём с меньшими затратами. Начиная с квартала расчёт меняется. При скрейпе раз в 30 секунд пятиминутный агрегат оставляет в десять раз меньше точек, а часовой — в 120 раз меньше.
На таких сроках рядом с Prometheus имеет смысл использовать Thanos или Mimir. Они работают с тем же форматом блоков, поэтому исторические данные не придётся мигрировать.
Что делать сначала: точность, labels, scrape, движок
Начинать стоит с того, что поступает в движок, а сам движок менять в последнюю очередь. Такой порядок определяется прежде всего стоимостью изменений, а уже затем возможным выигрышем в размере.
Ревизия точности значений и набора меток занимает один вечер и не требует менять инфраструктуру. Переход на другой движок — это новый компонент в production, миграция исторических данных и другой язык запросов для части функций.
Если система уже работает на VictoriaMetrics, снижение точности значений обычно даёт меньший эффект, чем в Prometheus. В этом случае важнее пересмотреть метки, а сам движок можно вовсе не менять.
Шаг 1. Точность значений
Точность публикуемых значений видна прямо в экспортёре. Обычно лишние знаки встречаются в утилизации, выраженной в процентах с двумя знаками после запятой, в задержках в секундах с шестью знаками, а также в размерах в байтах, которые рассчитываются через float.
Часто достаточно изменить одну строку в момент публикации метрики: заменить v на math.Round(v). В таблице по точности видно, что для Prometheus такая правка может дать многократный выигрыш, а для VictoriaMetrics при небольшом числе знаков после запятой эффект обычно измеряется процентами.
Шаг 2. Кардинальность и churn
Метки с наибольшим числом уникальных значений можно найти в /api/v1/status/tsdb. Из набора меток стоит исключать всё, что идентифицирует отдельный запрос или пользователя: user_id, session_id, request_id, trace_id, ip_address, сырой url_path, хеш pod'а и текст ошибки.
Именно метки такого типа превращают тысячу серий в сто тысяч. В моём замере при таком переходе индекс вырос в 28 раз. Динамику числа активных серий можно отслеживать двумя запросами из раздела «Серии, которые живут пятнадцать минут».
Шаг 3. Стабильность scrape-интервала
Стабильный scrape-интервал позволяет хранить временные метки в одном бите. Для этого таймаут скрейпа должен быть меньше самого интервала. Также не стоит выполнять тяжёлые вычисления в обработчике /metrics или нагружать один Prometheus скрейпом тысяч целей на пределе его возможностей.
Популярное решение «писать реже» уменьшает только количество точек. Размер индекса от этого почти не меняется. В замере со 100 тысячами серий индекс занимал 12,68 МБ из 33,8 МБ, поэтому двукратное увеличение интервала скрейпа сократит общий размер лишь примерно на треть.
Шаг 4. Выбор или смена движка
Для округлённых gauge со значениями от одного до трёх знаков после запятой переход на VictoriaMetrics даёт выигрыш в 8–34 раза. Для монотонных счётчиков выигрыш достигает 30,7 раза. На равномерно случайных float преимущества нет: VictoriaMetrics в этом профиле занимает на 3% больше места.
Стоимость перехода нужно считать вместе с retention. Даунсэмплинг в VictoriaMetrics доступен только в Enterprise-версии. Поэтому при месячном retention часть экономии на сырых данных потребует либо подписки, либо отдельного компактора Thanos или Mimir рядом с Prometheus.
Отдельный случай — когда заканчивается память, а диска достаточно. Переезд на другой движок сам по себе здесь не решает проблему. Разница в RSS отражает прежде всего настройки кэшей, а не свойства движка. Основную память занимают активные серии, поэтому в таком сценарии сначала нужно сократить кардинальность и churn, а затем распределить цели между несколькими экземплярами Prometheus.
Prom++ — форк Prometheus от Deckhouse под Apache 2.0, в котором сборка блоков и работа с WAL переписаны на C++ при сохранении формата хранилища и API. Его имеет смысл рассматривать именно при дефиците памяти, связанном с высокой кардинальностью: экономия достигается за счёт таблицы символов. По замеру Фланта на миллионе меток её размер сократился с 762 до 41,5 МБ при сравнении с Prometheus v2. На профиле из тысячи долгоживущих серий эффект будет небольшим, потому что сама таблица символов там мала.
Байты в рублях
В облаке эти байты превращаются в рубли. В счёте это обычно две измеримые статьи: диск под TSDB и память под head-блок. Третья появляется, если между зонами передаются данные через remote_write.
Порядок величин можно взять из публикации Фланта. Кластер на 5–10 узлов с примерно миллионом метрик там оценён в 10 ГБ памяти на весь стек мониторинга и примерно в 75 тысяч рублей в год. Для кластера на 100–200 узлов с десятью миллионами метрик оценка составляет 100 ГБ памяти и около 750 тысяч рублей в год. Это оценка вендора за 2025 год.
Миллион активных серий при скрейпе раз в 30 секунд дают 2 млн сэмплов в минуту, или 2,88 млрд в сутки. При 7,1 байта на сэмпл это около 20 ГБ на диске в день. После округления значений до целых те же данные занимают примерно 2,4 ГБ. Такая оценка относится к долгоживущим сериям.
При высокой скорости смены серий стоимость сэмпла вырастает до 11,7 байта, а суточный объём превышает 33 ГБ. На месячном retention разница накапливается до более чем 0,5 ТБ. В Observability VK Cloud эти статьи отображаются в биллинге отдельно, поэтому эффект от ревизии метрик можно отслеживать непосредственно по счёту.
Вендорский бенчмарк меряет свой профиль данных
Счёт в итоге приходит за профиль данных, а не только за выбранный движок. VictoriaMetrics действительно экономнее на округлённых значениях и монотонных счётчиках, но при этом сильнее реагирует на то, какие данные в неё записывают. Разброс внутри одного движка оказался шире, чем максимальная разница между движками.
Поэтому менять движок стоит в последнюю очередь. Сначала имеет смысл пройтись по экспортёрам и убрать избыточную точность, затем пересмотреть метки и исключить request_id и другие идентификаторы запросов. Переезд на другой движок лучше оставить как следующий шаг, если первых изменений окажется недостаточно.
На моих данных эти два действия снижают суточный объём примерно с 20 до 2,4 ГБ. Смена движка сама по себе такого эффекта не даёт.
Стенд можно собрать на ноутбуке за вечер. Движки ставятся бинарями из релизов Prometheus и VictoriaMetrics, формат описан в спецификации TSDB, а для сравнения движков по одной методологии есть TSBS с готовыми сценариями для DevOps и IoT.
Вендорский бенчмарк измеряет свой профиль данных, а счёт приходит за ваш. VictoriaMetrics действительно экономнее на округлённых значениях и монотонных счётчиках, но на моём стенде разброс внутри движка оказался шире максимального разрыва с конкурентом.
Поэтому движок я бы трогал последним. Сначала прошёлся бы по экспортёрам и округлению, затем убрал бы request_id из меток, а переезд оставил в запасе. На моих данных первые два шага дают разницу между 20 и 2,4 ГБ в сутки. Смена движка столько не набирает.
Если на вашем профиле смена движка выиграла больше, чем math.Round, покажите цифры в комментариях — обсудим это.