Как стать автором
Обновить
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Deckhouse Prom++: мы добавили плюсы к Prometheus и сократили потребление памяти в 7,8 раза

Уровень сложностиСложный
Время на прочтение18 мин
Количество просмотров8.4K

Prometheus для хранения 1 миллиона метрик, собираемых раз в 30 секунд на протяжении 2 часов, требуются 500 МБ на диске и 5 ГБ памяти. Нам показалось, что это слишком много. Вместо этого хотелось получить «бесплатный» мониторинг, который не будет требовать значительных затрат на инфраструктуру. 

Больше двух лет мы работали над этой задачей. Её результатом стал Deckhouse Prom++. Это Open Source-система мониторинга, которой в среднем требуется в 7,8 раза меньше памяти и в 2,2 раза меньше ресурсов CPU, чем Prometheus v2.53. И здесь ещё есть пространство для оптимизации. 

В статье мы расскажем, как появилась идея Deckhouse Prom++, что уже получилось оптимизировать, какие результаты показывает наше решение по сравнению с Prometheus и VictoriaMetrics, а также о ближайших планах.

Если для вас актуальна задача экономии ресурсов под мониторинг, попробовать Deckhouse Prom++ можно уже сейчас. Ссылку на GitHub-репозиторий вы найдёте на лендинге

Ниже вас ждёт текстовая версия доклада Давида Мэгтона, Евгения Бастрыкова и Владимира Гурьянова с Highload+ 2024. Если вы предпочитаете смотреть видео, а не читать, вот запись:

Идея о бесплатном мониторинге

Раньше у проектов была монолитная архитектура. Монолит — это десятки объектов для мониторинга, каждый из которых отдаёт тысячу метрик. Потом пришли микросервисы и Kubernetes. А с ними — тысячи подов, с каждого из которых системы мониторинга собирают ту же тысячу метрик. 

Умножение десятков и тысяч объектов на количество собираемых метрик даёт совершенно разные результаты. Это итоговые десятки тысяч метрик в случае монолита и миллионы — в случае микросервисов:

С объёмами метрик, снимаемых с монолитов, прекрасно справлялся Zabbix, их значения было легко хранить в MySQL. А вот хранение миллионов метрик микросервисов — это уже задачка со звёздочкой. Объём данных за последнее десятилетие значительно вырос, и это важно держать в уме, когда вы делаете мониторинг в Kubernetes

Давайте на примере реальных данных посмотрим, за каким количеством метрик следят компании сегодня. Мы разрабатываем Deckhouse Kubernetes Platform, под управлением которой работает больше 1000 кластеров. Из них у нас на телеметрии 309 — это кластеры, которые не находятся в закрытых контурах или air-gapped-окружениях. 

В 309 кластерах — 11 870 узлов, которые в сумме дают 398 миллионов метрик. То есть на один кластер в Kubernetes приходится в среднем 1,28 миллиона метрик. Из них примерно 33,5 тысячи — на один узел. 

Телеметрия с 309 кластеров наших клиентов за 11 месяцев
Телеметрия с 309 кластеров наших клиентов за 11 месяцев

На базе этих цифр можно прикинуть, во сколько обойдётся мониторинг кластеров разного размера. Цены в таблице рассчитаны по упрощённому принципу, когда мы в недорогом публичном облаке платим только за оперативную память и храним две копии данных:

Размер кластера

Количество
метрик, млн

RAM, ГБ

Цена, ₽/год 

Маленький кластер

(5–10 рабочих узлов, 1000 подов)

≈ 1

≈ 10

≈ 75 000 

Средний кластер

(20–50 рабочих узлов, 5000 подов)

≈ 3

≈ 30

≈ 225 000

Большой кластер

(100–200 рабочих узлов, 20 000 подов)

≈ 10

≈ 100

≈ 750 000

Получается недешёво. Вместо этого мы хотели сделать условно бесплатный мониторинг. 

Мониторинг вместе с логами и бэкапами нужен только тогда, когда что-то идёт не так. Тратить на него половину стоимости основной инфраструктуры нерационально. И чем более дешёвое решение мы сможем сделать, не потеряв в качестве, тем лучше. Аналогия здесь простая: если цена пожарной сигнализации в здании составляет половину стоимости здания, её никто не будет делать — это неоправданно дорого. С мониторингом ситуация похожая. 

Определимся с definition of done задачи — тем, что такое «бесплатный мониторинг». Возьмём 1 миллион метрик за репрезентативный кейс. Если собирать их каждые 30 секунд на протяжении 2 часов, получится примерно 240 миллионов точек. Если взять Prometheus, то хранение такого количества точек потребует 500 МБ на диске и 5 ГБ в памяти. 

У нас появились два вопроса: 

  1. А можно ли сделать так, чтобы в памяти было не больше, чем на диске?

  2. А можно ли сделать так, чтобы на диске было меньше, чем сейчас?

Мы решили попробовать с этим разобраться. Ведь если получится выполнить оба пункта, это и будет бесплатный мониторинг. 

Перед тем как двигаться к рассказу о нашем решении задачи, договоримся о терминах. Если вы уже знаете, что такое метрики, лейблы, labelset, временные метки и серии, можно пропустить следующий раздел и сразу перейти к тому, как мы оптимизировали хранение лейблов.  

Договоримся о терминах

Когда мы мониторим Kubernetes в Prometheus, каждая метрика выглядит следующим образом:

{
    __name__="container_cpu_usage_seconds_total",
    node="some-server",
    namespace="foo", 
    pod="backend-5c8f97d88f-xv29h",
    container="web"
}

Она идентифицируется набором пар «ключ-значение». Каждую такую пару называют лейбом (label). В паре __name__="container_cpu_usage_seconds_total":

  • __name__ — это имя лейбла;

  • сontainer_cpu_usage_seconds_total — это значение лейбла.

Набор лейблов так и называют — набор лейблов (labelset). Каждому labelset’у в Prometheus назначается ID. В базе данных временных рядов TSDB хранятся ID labelset’а и его точки. Точки мы собираем по каждой из метрик с определённой периодичностью, например раз в 30 секунд:

t1    2345
t2    2360
t3    2380
...   ...

Здесь t1, t2 и t3 — это временные метки (timestamp), а в правой колонке записаны значения (value). Например, значением может быть потребление памяти или процессора. Весь набор временных меток с их значениями называют данными либо точками

Labelset и его значения составляют одну серию, или временной ряд:

Также в Prometheus есть индекс. Он позволяет находить нужную метрику, например, по имени или пространству имён. Благодаря индексу мы можем посмотреть, сколько памяти занимают все контейнеры одного пространства имён или все контейнеры на одном сервере. 

Итого, Prometheus хранит в памяти лейблы и точки, а также позволяет искать по лейблам. Больше деталей об устройстве его Time Series Database можно прочитать в нашей статье «Потребление ресурсов в Prometheus: кто виноват и что делать».

Оптимизируем хранение лейблов

Вернёмся к нашему кейсу. По каждой метрике кластера мы собираем 240 точек: 2 часа — это 120 минут, и каждую минуту данные снимаются 2 раза. Всего для 1 миллиона метрик получается 240 миллионов точек, которые занимают 500 МБ на диске и 5 ГБ в оперативной памяти. 

Нам было интересно понять, что именно занимает столько места: сколько нужно labelset’ам, сколько — точкам, а сколько — индексу:

Быстрое исследование помогло понять, что индекс с точками в Prometheus уже неплохо оптимизированы, а вот labelset’ы для 1 миллиона метрик занимают примерно 1 ГБ в памяти. Поэтому мы решили начать оптимизацию с них.

И первый вопрос здесь — сколько памяти занимает строка в Go. Разберёмся на примере значения web. Может показаться, что 3 символа занимают в памяти 3 байта. Но это не так: помимо самих символов, есть ещё длина строки (8 байт) и указатель (8 байт). Всего получается 3 + 8 + 8 = 19 байт. 

Лейбл container="web" занимает уже 52 байта:

  • 9 + 8 + 8 = 25 — 9 символов + длина строки + указатель

  • 3 + 8 + 8 = 19 — 3 символа + длина строки + указатель

  • 25 + 19 = 52

А весь labelset из нашего примера — целых 283 байта. Это значения отдельных строк плюс 16 байт на длину и указатель:

Поскольку labelset’ов — миллион, в совокупности они занимают уже 269 МБ. 

И здесь возникает следующий логичный вопрос: откуда тогда берётся гигабайт? Ответ простой: labelset из примера — синтетический. В типичном labelset’е из реального кластера среднее количество символов в каждом ключе — 13 (то есть 29 байт), а в его значении — 42 (58 байт). Получается 29 + 58 = 87 байт в среднем на пару «ключ-значение». И таких пар не 5, как в примере выше, а 9.

Таким образом, типичный labelset занимает в памяти 87 × 9 + 16 = 799 байт. На миллион метрик получается уже 762 МБ. Это по-прежнему не гигабайт, но к этому значению можно добавить overhead на фрагментацию памяти и особенности работы garbage collector в Go. 

Итак, лобовое решение даёт гигабайт в памяти. Как улучшить результат хотя бы на уровне алгоритмов и экспериментов? 

Экспериментируем с лейблами

Оптимизацию мы начали с классического решения — сделали таблицу символов и выдали каждой строке свой ID:

#0: ”__name__”
#1: ”container_cpu_usage_seconds_total”
#2: ”node”
#3: ”some-server”
#4: ”namespace”
#5: ”foo”
#6: ”pod”
#7: ”backend-5c8f97d88f-xv29h”
#8: ”container”
#9: ”web”

Наш labelset стал выглядеть так:

#0=#1,
#2=#3,
#4=#5,
#6=#7,
#8=#9

В таком случае каждый лейбл занимает в памяти 16 байт, а весь labelset — 160 байт (16 × 9 + 8 + 8). Это 152 МБ на 1 миллион метрик. 

Таблица символов тоже занимает место. Средняя длина одного элемента в ней — 42 байта, то есть средний размер в памяти — 58 байт, а количество уникальных строчек не такое большое — 45 787. Всё это даёт 58 × 45787 = 2,5 МБ. 

Итого, 152 + 2,5 = 154,5 МБ на все labelset’ы и таблицу символов. Кажется, мы выиграли 607,5 МБ (изначальные 762 МБ − 154,5 МБ), и можно порадоваться. И вроде это довольно простое и очевидное решение, но на практике при его внедрении возникают тысяча и один вопрос, например как garbage collector Go будет всё это обрабатывать. И кажется, именно поэтому в Prometheus такой подход пока не используют, хотя попытки были. 

Мы решили эти проблемы, изменив формат работы с WAL. Вместо «отрезания» части WAL’а мы начинаем писать новый, таким образом избегая частичного удаления данных. А снижение нагрузки на GC получилось за счёт переноса структур хранения в память C++.

На этом мы не остановились, а пошли дальше и заметили, что раз количество уникальных значений в таблице символов не такое большое, а именно 45 787 штук, то и восьмибайтные ID не нужны. Достаточно ID поменьше, что сразу даёт 8 байт на лейбл, или 85,5 МБ на все метрики.  

Также мы заметили, что разнообразие наборов ключей в labelset’ах достаточно маленькое. У многих серий отличаются значения лейблов, но не набор ключей. Мы решили хранить набор ключей в отдельной таблице:

#0: ”__name__”, “node”, “namespace”, “pod”, “container”

Из labelset’а ключи лейблов убрали, оставив вместо них идентификатор набора ключей:

{
	#0
	”container_cpu_usage_seconds_total”,
	”some-server”,
	”foo”,
	”backend-5c8f97d88f-xv29h”,
	”web”
}

При этом помним, что раньше мы уже заменили ключи на идентификаторы, а значит, таблица имеет следующий вид:

#0: #0, #2, #4, #6, #8

Соберём всё воедино: вместо ключа и значения остаются идентификаторы. В итоге наш labelset выглядит так:

{
	#0
	#1, #3, #5, #7, #9
}

В таблице с набором имён ключей получилось 1092 значения (на 1 миллион серий), которые занимают всего 55 КБ, которыми можно пренебречь. В таблице значений, как уже говорилось, 45 787 вариантов и 2,5 МБ памяти.  

Эти оптимизации дают нам 41,5 МБ вместо прошлых 85,5 МБ. То есть то, что занимает в Prometheus 1 ГБ, в теории можно уложить в 41,5 МБ.

Но и на этом мы не остановились. Вернувшись к первоначальному отображению labelset, мы обратили внимание на то, что количество узлов (node="some-server") в кластере обычно небольшое — от 10 до 100. То же самое касается и количества пространств имён: их обычно от 10 до 30. 

Если подобным образом проанализировать все лейблы на реальных данных, получится, что:

  • 32,7 % из них имеют всего одно возможное значение, например true или 0;

  • 60 % — от 2 до 255 значений, то есть укладываются в 1 байт;

  • 7,3 % — от 256 до 5275 значений;

  • 0 % — больше 65 535 значений. 

Это значит, что все возможные значения укладываются в два байта. 

Мы решили хранить все ID (#0, #1, #2 и т. д.) не в четырёхбайтных значениях, а как два бита, указывающие количество байт, в которые укладывается число, плюс значение. Например 00 + 0 байт для нуля, 01 + 1 байт для значений от 1 до 255, 10 + 2 байта для значений, укладывающихся в 2 байта, и 11 + 4 байта для остальных. В итоге получилась такое кодирование:

32,7 % — всего 1 значение

2 бита (00) + 0 байт

60,0 % — от 2 до 255 значений

2 бита (01) + 1 байт

7,3 % — от 256 до 5275 значений

2 бита (10) + 2 байта

0 % — больше 65 535

2 бита (11) + 4 байта

Это дало нам очередной выигрыш по памяти — теперь весь labelset для 1 миллиона метрик стал занимать 30 МБ. Это в 30 раз лучше стартовых 762 МБ.

На уровне экспериментов и алгоритмов мы поняли, что при желании можно оптимизировать эту часть в Prometheus. На выходе получился работающий прототип на C++, который мог преобразовать labelset в идентификатор, а идентификатор — в labelset. И позволял хранить лейблы эффективнее, чем Prometheus, в 25 раз.

Почему C++
  1. Мы активно используем низкоуровневые оптимизации, в том числе по управлению памятью, SIMD-инструкции и даже собственные shared_ptr. И C++ минимально мешает это делать.

  2. Изначально у нас был большой опыт именно в C++, что позволило быстро собрать прототип, а после довести его до production ready-состояния.

  3. Наше решение является по сути ядром, которое встраивается в Go-сервис. Таким образом, нам нужно было сделать это решение максимально лёгким, безрантаймовым. И снова C++ решает эту задачу с минимальными затратами.

Погружаемся глубже в работу Prometheus

Итак, эксперименты показали, что лейблы можно оптимизировать. Вернёмся к одному из основных вопросов, которые перед нами стояли: а можно ли сделать так, чтобы в памяти было не больше, чем на диске?

Стало понятно, что вроде да. Но для этого нужно, во-первых, как-то соединить С++ и Prometheus, а во-вторых, разобраться с оставшимися 4 ГБ из 5 ГБ. О том, как у нас получилось решить первую задачу, мы напишем в отдельной статье. А для того, чтобы рассказать про оставшиеся 4 ГБ, нужно подробнее рассмотреть то,  как работает Prometheus. 

Итак, у нас есть Kubernetes-кластер и сколько-то подов в нём. С подов мы собираем метрики. И есть Prometheus, внутри которого набор таргетов (targets) — список подов, с которых нужно снимать метрики и их IP. Раз в определённый период, например в 30 секунд, штука под названием Scraper берёт список IP, идёт в поды и получает из них данные: 

Данные, которые получает Scraper, выглядят так:

my_metric{label=”a”} 1 
my_metric{label=”b”} 6
my_metric{label=”c”} 42

Здесь:

  • my_metric — название метрики;

  • {label=”a”} — лейбл;

  • 1 — значение. 

Полученные данные Scraper складывает в хранилище — TSDB. В нём лежат лейблы, точки и индекс для того, чтобы искать серии. При переносе значения за time stamp Scraper обычно берёт время запроса данных из таргета, грубо говоря, time now. Часть лейблов он переносит как есть, а часть дополняет из метаданных наблюдаемого объекта. Вся эта процедура называется relabeling. Дальше она повторяется раз в 30 секунд. Так устроен основной рабочий цикл Prometheus. 

Также в Prometheus есть Service Discovery. Он подключается к API Kubernetes, получает список того, за чем нужно следить, и на основании него пополняет список таргетов. 

Есть и движок PromQL, который умеет находить и агрегировать данные в TSDB. К нему в свою очередь подключается Grafana или же внутренний крон — Ruller. Последний может выполнять определённые запросы и записывать их обратно в базу данных. По сути, он помогает в агрегации: вряд ли нам будет интересно смотреть метрики процессора по одному поду, гораздо интереснее видеть их сразу по всем. И чтобы это можно было увидеть в Grafana, мы всё предварительно агрегируем. Также Ruller выполняет запросы, по которым могут загораться алерты. 

И ещё один важный компонент Prometheus — это RemoteWrite. По сути, это репликация. Этот компонент умеет по своему протоколу отправлять данные во внешние TSDB, например в соседний Prometheus или другое хранилище, поддерживающее этот протокол:

Это то, из чего состоит Prometheus. Во всей этой сложной системе мы оптимизировали кусочек на 1 ГБ. Оставалось понять, сколько занимает всё остальное: 

Считаем, сколько ресурсов «ест» Prometheus

Чтобы найти ответ на вопрос «Сколько ресурсов потребляет каждый кусочек Prometheus?», мы взяли инсталляцию с 1 миллионом метрик. В памяти она занимала 5,5 ГБ, из которых 3,8 ГБ — в heap. Далее мы рассмотрели конкретный профиль pprof, который занимал 3,47 ГБ (разницу от системных 5,5 ГБ занимают объекты, которые ещё не вернулись в систему из аллокатора, а также фрагментация памяти). 

Значение 3,47 ГБ дальше будет приниматься за 100 %. 

Service Discovery занимает лишь 1 % от общего объёма, или 36,24 МБ в абсолютных числах. Скорее всего, в будущем мы захотим это оптимизировать, но это непростая задача. В наших инсталляциях порядка 80 % Service Discovery занимает клиент Kubernetes, а это означает объёмную работу с protobuf, который не так просто оптимизировать.  

Таргеты занимают тот же 1 %, или 36,24 МБ. Мы точно оптимизируем их в будущем просто потому, что уже научились хорошо хранить строки. Пока не добрались до этой оптимизации, поскольку её влияние на общий результат будет незначительным. 

Дальше уже интереснее. Мы не стали разделять в оценке потребления ресурсов Query Engine и Ruller. Запросы извне требуют их совсем немного, несмотря на то что мы открываем огромные дашборды в Grafana. Основную нагрузку создаёт Ruller, потому что он выполняет приличное количество запросов из-за активного использования агрегации в Deckhouse. Но в целом у него больше нагрузки на CPU, чем на память. Вместе Query Engine и Ruller занимают 1,5 %, или 55,76 МБ.

RemoteWrite — это способ передачи данных в другие TSDB. Часто он используется для long-term-хранения, когда мы складываем исторические данные за пределами кластера. RemoteWrite занимает 13,4 %, или 475,8 МБ, но здесь есть важный нюанс. 

Если используется несколько RemoteWrite, то каждый будет требовать свои 475,8 МБ. Даже если отправляется всего одна серия. Например, если RemoteWrite три, то памяти потребуется уже 1427,4 МБ (475,8 МБ × 3). Оптимизация этой части у нас уже готова — успехи покажем в следующем разделе статьи. 

Relabel потребляет 22,8 % памяти, или 812,8 МБ в абсолютных числах. Это тяжёлый процесс, внутри которого происходит работа с регулярными выражениями и строками. Всё это хочется кэшировать и оптимизировать по процессору, поэтому там прикручены «злые» кэши, которые построены на строках и хэш-таблицах со строками. Отсюда и размер — как уже упоминалось, Go не очень эффективно хранит строки.

TSDB требуется львиная доля от всех ресурсов — 58,3 %, или 1075 МБ. Разберёмся подробнее, куда именно они уходят:

Серии

32,3 %

1156 МБ

(которые мы можем
оптимизировать до 30 МБ)

Индекс

3,7 %

131,3 МБ

Точки

22,1 %

787,7 МБ

Наши расчёты можно повторить. Если у вас есть Prometheus, нужно снять с него профиль и сопоставить его кусочки с кусочками кода:

go tool pprof -http=":8081" http://<prometheus_ip>:9090/debug/pprof/heap

Сведём всё потребление ресурсов в одну наглядную таблицу. Labelset объединён с индексом, потому что при разработке своего решения нам удобнее хранить в индексе ID строк, а следовательно, индекс оказывается привязан к структуре хранения labelset’ов:

На диске всё занимает в разы меньше места. LabelSet с индексом — в 8,2 раза меньше, а точки — почти в 2. При хранении на диске в Prometheus используются многие идеи, описанные в прошлом разделе, например таблица символов и маленькие ID. 

Посмотрим, сколько те же кусочки занимают в нашем решении.

Считаем, сколько ресурсов «ест» Deckhouse Prom++ в теории

Своё решение для бесплатного мониторинга мы назвали Deckhouse Prom++. Здесь всё очевидно — мы добавили «плюсы» к Prometheus и сократили его название для простоты.

Labelset c индексами в Prom++ уложились в 71,6 МБ. Это почти в 18 раз лучше хранения в памяти Prometheus. 

Relabel у нас потребляет 68,9 МБ — это выигрыш в 12 раз по сравнению с Prometheus. 

Оптимальное хранение строк дало приличный выигрыш и в индексе. Если раньше в нём были строки и хэш-таблицы, которые не особенно оптимально хранятся в памяти, то в нашей реализации есть ID, которые выдаются по порядку. Это позволяет хранить в кэше четырехбайтные числа uint32 и заменить хэш-таблицы на sparse-векторы и Bitset. 

В итоге основным потребителем памяти у нас стали точки. Prometheus хранит их достаточно компактно: он использует Gorilla Encoding и битовые операции. Это не значит, что оптимизировать нечего, но для этого надо внимательно посмотреть на данные. 

Вспомним, что серия — это labelset и точки. В Gorilla вперемешку хранятся пары timestamp — value, timestamp — value и так далее.

1 миллион метрик собирается с 200–300 таргетов. При этом в подавляющем количестве случаев для всех метрик, собранных с одного таргета, выдаётся один timestamp (время запроса метрик у таргета). Получается, что мы собираем 200–300 уникальных временных меток, а все остальные — это дубли. У двух серий, которые собираются с одного таргета, всегда будет одинаковая последовательность временных меток. А значит, можно избавиться от дублей. 

Представим три серии, которые собираются с одного таргета: 

Кстати, необязательно, чтобы цели собирались с одного таргета. За счёт некоторых механик внутри Prometheus последовательность временных меток иногда повторяется и между разными таргетами
Кстати, необязательно, чтобы цели собирались с одного таргета. За счёт некоторых механик внутри Prometheus последовательность временных меток иногда повторяется и между разными таргетами

Как и ожидалось, последовательность временных меток у наших серий одинаковая. То есть можно хранить её один раз, а не для каждой серии. А рядом с сериями можно хранить четырёхбайтный идентификатор в векторе: 

Оказалось, что уникальных последовательностей временных меток всего 10 % от миллиона. Эта оптимизация позволила выиграть 55,5 % по памяти просто за счёт дедупликации.

Для значений (values) мы тоже выявили некоторые закономерности. 

Во-первых, при внимательном взгляде выяснилось, что некоторые значения постоянны. Например, конфигурационные параметры или редко используемые бакеты. Хранить N раз одно и то же значение, одно и то же число нет смысла. Мы можем делать это один раз:

Иногда серии меняются очень редко, например один раз за два часа. Тогда мы можем хранить первое значение и то, сколько раз оно встречается. А при появлении нового значения сохранять и его. Ведь количество точек у нас уже есть в последовательности временных меток.

Но подавляющее большинство данных, которые хранятся в Prometheus, — это счётчики. Они целочисленны и монотонно возрастают:

Очень монотонно возрастают
Очень монотонно возрастают

Поскольку период сбора значений постоянный, то и дельты между значениями счётчиков будут близки друг к другу. А это значит, что и дельты дельт будут близки к нулю:

Чем ближе число к нулю, тем меньше в нём энтропии. То есть согласно теории информации его можно компактнее записать. Мы можем использовать дельта-дельта-кодирование: хранить первое значение, его дельту со вторым и вторую производную. И дальше с их помощью восстанавливать все исходные значения. По сути, здесь мы переиспользовали знание о том, как в Gorilla Encoding кодируются временные метки. Ведь они — это такие же счётчики. 

После всех преобразований мы получили выигрыш по хранению точек почти в три раза: изначальные 787,72 МБ превратились в 283,9 МБ. При этом чем больше данных и серий, тем лучше срабатывает наша оптимизация. Если на одном миллионе метрик мы выиграли по хранению точек в 2,7 раза, то на трёх миллионах это будет уже три с небольшим раза.

RemoteWrite у нас получилось оптимизировать в 6 раз: потребление памяти составило 77,1 МБ против исходных 475,82 МБ.

Вот итоговая таблица с теоретическими результатами Deckhouse Prom++ в сравнении с Prometheus:

Сравниваем Deckhouse Prom++ с Prometheus и VictoriaMetrics на практике

Теоретические показатели не всегда подтверждаются практикой. Поэтому мы протестировали результаты Prom++ на реальных кластерах и пользователях. Мы уже говорили, что под управлением Deckhouse Kubernetes Platform больше 1000 кластеров, поэтому такая возможность у нас была. 

Deckhouse Prom++ сравнивали с Prometheus v2, Prometheus v3 и VictoriaMetrics. В качестве параметров было потребление ресурсов — CPU, IO, RAM. Мы предполагаем, что их потребление системами мониторинга зависит от размера кластера. Поэтому для тестов взяли два типа кластеров: небольшие (до 1 миллиона активных серий) и побольше (до 10 миллионов активных серий). 

Мы постарались сделать сравнение максимально объективным:

  • Запустили все четыре системы мониторинга на одном железе.

  • Все системы собирали одни и те же метрики и были запущены с одинаковыми конфигурационными файлами.

  • Все продукты выполняли одинаковый набор Prometheus rules. 

  • Во все продукты раз в пять минут приходил Federation API-запрос. 

Результаты в кластере до 1 миллиона активных метрик  

Начнём с результатов для небольшого кластера с 782 345 активными сериями. 

По потреблению CPU нет однозначного лидера или аутсайдера. Результаты похожие, и потребление маленькое — во всех случаях меньше половины ядра. В чтении с диска ситуация аналогичная — у всех продуктов потребление около одной IOPS. При современных дисках на 15 000 IOPS это ничтожно мало. В записи на диск можно заметить только то, что VictoriaMetrics всё время что-то пишет, но это незначительно влияет на результаты.  

Посмотреть графики
Потребление CPU для кластера с 782 345 активными сериями
Потребление CPU для кластера с 782 345 активными сериями
Чтение с диска для кластера с 782 345 активными сериями
Чтение с диска для кластера с 782 345 активными сериями
Запись на диск для кластера с 782 345 активными сериями
Запись на диск для кластера с 782 345 активными сериями

Зато в потреблении памяти Deckhouse Prom++ — явный лидер. Если потребление Prometheus обоих версий и VictoriaMetrics измеряется в гигабайтах, то у Prom++ это мегабайты. Заметный аутсайдер здесь — Prometheus v2. В третьей версии он сделал большой шаг вперёд, но VictoriaMetrics пока не догнал.

Ниже — сравнительная таблица производительности четырёх систем мониторинга для кластеров небольшого размера. Зелёным обозначены победители в каждой «номинации», красным — проигравшие. 

По итогам тестов на реальных пользователях Deckhouse Prom++ потребляет в ~8,6 раза меньше памяти, чем Prometheus v2. VictoriaMetrics эффективнее Prometheus v2 всего в 2,6 раза.

Но что, если потребление памяти системой зависит от общего количества метрик в кластере и эффективность упадёт на большем масштабе? 

Результаты в кластере больше 3 миллионов активных метрик  

Посмотрим на результаты тестов в кластере с 3 105 205 активными сериями. 

По CPU картина остаётся ровной: все системы потребляют плюс-минус два ядра. Чтение с диска тоже практически одинаковое, максимум — это 7,1 IOPS. То же касается и записи на диск — цифры очень маленькие, в рамках статистической погрешности. 

Посмотреть графики
Потребление CPU для кластера с 3 105 205 активными сериями
Потребление CPU для кластера с 3 105 205 активными сериями
Чтение с диска для кластера с 3 105 205 активными сериями
Чтение с диска для кластера с 3 105 205 активными сериями
Запись на диск для кластера с 3 105 205 активными сериями
Запись на диск для кластера с 3 105 205 активными сериями

Самое интересное — это по-прежнему потребление памяти. Во-первых, по графикам видно, что на масштабе от 3 миллионов активных метрик Prometheus v3 уже не так хорош. Его результаты ближе к Prometheus v2, чем к VictoriaMetrics. Последняя всё так же показывает хорошие результаты, но на первом месте с заметным отрывом по эффективности — Deckhouse Prom++.  

Сведём результаты в итоговую таблицу. CPU и IOPS по-прежнему не очень интересны, а вот по потреблению памяти в кластере с 3 миллионами активных метрик Prom++ эффективнее Prometheus v2 в 6,1 раза.

Разница меньше, чем на небольшом кластере, но если посмотреть на абсолютные значения, это 3,8 ГБ для Deckhouse Prom++ против 23,3 ГБ Prometheus v2. То есть фактически с одного инстанса Prometheus мы экономим почти 20 ГБ. Если инстансов два, это порядка 40 ГБ экономии памяти. 

Усреднённые итоговые значения для кластеров разного масштаба

Конечно, кластеров, где раскатан Deckhouse Prom++, у нас больше двух. Как и кластеров с VictoriaMetrics и Prometheus второй или третьей версии. Мы суммировали информацию по ним и получили усреднённые значения производительности всех систем мониторинга.

По потреблению памяти и CPU выигрывает Deckhouse Prom++. Мы не уделяли оптимизации использования процессора много внимания, это скорее приятный побочный эффект. По памяти наше решение в 7,8 раза эффективнее Prometheus v2, по CPU — в 2,2 раза.

По чтению с диска лучшая — VictoriaMetrics, а по записи первое место занимает Prometheus. Другие системы пишут чуть больше:

Другие успехи Deckhouse Prom++ и ближайшие планы 

В процессе работы Prometheus пишет WAL. Мы оптимизировали журнал предзаписи примерно в 19 раз. Изначальные 6,2 ГБ без сжатия в Prometheus v2 для 1 миллиона метрик превратились в 153 МБ в Deckhouse Prom++. 

Мы делали это не для оптимизации использования места на диске, а для упрощения RemoteWrite в другой кластер. Когда мы перекладываем данные из одного Prometheus в long-term-хранилище другого, за 1 миллион метрик и 240 миллионов точек тот умудряется передать по сети 183 ГБ. Если кластер должен отправлять это не по локальной сети, то этот трафик может повлиять на стоимость инфраструктуры или даже на доступность всей системы. 

При этом там много текстовых данных, и обычный XZip сожмёт всё до 20 ГБ, но это по-прежнему приличный объём для двух часов. У нас в Deckhouse Prom++ получилось уложиться в 153 МБ как раз потому, что WAL, который мы пишем на диск, можно отправлять по сети, а это и есть репликация. 

Таким образом, получилась оптимизация RemoteWrite в 133 раза. 

В первой версии мы сделали: 

  • индекс метрик;

  • оптимизацию Gorilla Encoding;

  • парсер /metrics;

  • поддержку архитектур ARM и x86. 

Мы знаем, как сократить потребление и по памяти, и по CPU ещё в 2 раза. Это позволит уложиться в 256 МБ мониторинга для среднего кластера. По сути, это и будет тот самый бесплатный мониторинг, при котором не нужно держать выделенную инфраструктуру. Также в планах на будущее:

  • наш формат «блоков»;

  • агрегация в C++;

  • распределённый режим в Kubernetes.

Мы уже начали переход с Prometheus на Prom++ в Deckhouse Kubernetes Platform. В ближайших версиях платформы Prom++ будет основной системой мониторинга. 

Если хотите попробовать Deckhouse Prom++ уже сейчас, скачайте бинарь по ссылке на лендинге. А если у вас остались вопросы или вы хотите поделиться обратной связью, пишите нам в Telegram-чат.

P. S.

Одна из самых больших проблем программирования — это нейминг. Мы определились, что продукт называется Deckhouse Prom++, но вы можете встретить в статье и на просторах сети и альтернативное название — Prom++.

Теги:
Хабы:
+71
Комментарии23

Публикации

Информация

Сайт
flant.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Александр Лукьянов