Prometheus: от основ до mem-saving оптимизации


Мониторинг в контексте современных инфраструктур играет критически важную роль и является неотъемлемой частью успешного управления и обеспечения стабильности систем. С ростом их сложности и масштаба, а также ввиду высоких ожиданий пользователей, возникает необходимость в непрерывном и детальном контроле за работой приложений и расходом аппаратных ресурсов. Мониторинг позволяет оперативно выявлять аномалии, прогнозировать потенциальные проблемы и оптимизировать работу системы, что в свою очередь, способствует повышению производительности, надежности и экономической эффективности инфраструктурных решений. Без качественного мониторинга становится невозможным обеспечить конкурентоспособность и успешное функционирование в реалиях современного рынка IT-продуктов.

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

Введение


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

В типовых сценариях Prometheus использует pull-based модель сбора данных, что означает, что он активно запрашивает данные у целевых систем (соответственно, push-based — модель, где целевая система активно отправляет данные на сервер мониторинга). При выборе между pull и push моделями нужно учитывать сценарии мониторинга: например, для короткоживущих сервисов (джоб) или разовых событий подойдет только push-based модель, а в динамической системе типа kubernetes-кластера удобнее использовать pull-based модель (за счёт нативного — по крайней мере, у Prometheus — service discovery).

Учитывая вышесказанное, в своей инфраструктуре скорее всего хотелось бы видеть гибрид push и pull моделей. Prometheus можно назвать таким гибридом, поскольку pull — это дефолтная схема, границы которой могут быть расширены за счёт Prometheus PushGateway.

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

Но несмотря на достоинства Prometheus, его успешное использование требует правильной конфигурации и оптимизации (особенно, в части ресурсопотребления). В данной статье поговорим об управлении этой системой мониторинга, основах хранения данных в ней, исследуем аспекты потребления RAM, а также методы оптимизации.

Несколько комментариев о Prometheus в Kubernetes-кластере


Как правило, сервис разворачивается в Kubernetes (для других сред запуска концепт будет аналогичным), поэтому предлагаем вкратце затронуть его инсталляцию в кластер и дальнейшее управление.

Наиболее удобным и распространенным способом установки Prometheus в k8s-кластер является helm-чарт. Он включает в себя параметры из зависимых чартов (kube-state-metrics, node-exporter, grafana), поэтому для более гибкой настройки этих компонентов можно обратиться к их документации.

Одним из вариантов организации управления ресурсами Prometheus может быть следующий:
— выносим правила для алертов в отдельную папку, назовём ee rules;
— разбиваем их на логические группы (по сервисам или как-либо еще);
— для управления используем плагин helm-multivalues.

Итоговая команда может выглядеть примерно так:

helm multivalues upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack -f rules -f . -n monitoring --version=55.5.1 --install


Описание каких-то дополнительных таргетов для мониторинга лучше также вынести в отдельную папку, в отдельный конфигурационный файл, скажем, additional-scrape-config.tpl. Мы в основном здесь описываем облачные managed-сервисы. Например, таргет для Redis в Yandex Cloud может описываться следующим образом:

- job_name: 'managed-redis'
  metrics_path: '/monitoring/v2/prometheusMetrics'
  params:
    folderId:
    - ''
    service:
    - 'managed-redis'
  bearer_token: ''
  static_configs:
  - targets: ['monitoring.api.cloud.yandex.net']
    labels:
      folderId: ''
      service: 'managed-redis'


После редактирования кодируем его в base64 и копируем результат в манифест секрета, на который сошлемся в основном конфиге, затем деплоим его в кластер:

cat additional-scrape-config.tpl | base64 -w0
prometheus:
...
  prometheusSpec:
  ...
    additionalScrapeConfigsSecret:
      enabled: true
      name: additional-scrape-configs
      key: prometheus-additional.yaml


Такой подход удобен тем, что, настраивая основательную систему мониторинга, мы имеем не один громоздкий конфигурационный файл, а несколько более мелких.

Также не забывайте, что ресурсы Prometheus описываются с помощью CRD (Custom Resource Definitions), поэтому перед обновлением сервиса необходимо обновить и соответствующие CRD (в документации GitHub подробно описан процесс обновления для каждой версии).

Особенности хранения данных в Prometheus TSDB


Перед тем, как вести разговор непосредственно о тюнинге, давайте освежим в памяти как работает Prometheus TSDB под капотом, вспомним несколько основных понятий.

Базовая единица, которой оперирует Prometheus, это точка данных (sample). Каждая точка данных описывается двумя параметрами: временной меткой (timestamp) и значением (value), при чем timestamp может быть только целым числом, а value — любым.

Последовательность точек данных со строго монотонно возрастающими временными метками — это и есть тот самый временной ряд (timeseries), который однозначно идентифицируется по имени метрики (metric name) и набору лейблов (labels). Имя метрики указывает на характеристику системы, которая измеряется, а лейблы являются её метадатой.

В реальной инфраструктуре мы имеем несколько целевых сервисов (targets), с которых мы хотим собрать множество данных (временных рядов). Порядок количества целей, как и количества временных рядов, может измеряться тысячами. При таких условиях запись на диск отдельных сэмплов будет крайне долгой, поэтому данные записываются кусками (chunks).

Чанки группируются в блоки (blocks) — каждый блок выступает как полностью независимая база данных, содержащая все данные временных рядов для своего временного окна (по дефолту двухчасового). Блок имеет свой индекс (index), который служит для увеличения эффективности поиска элементов данных на основе подмножества их содержимого. В этом контексте «подмножество их содержимого» — это комбинации лейблов метрик, которые могут быть использованы для фильтрации данных при выполнении запросов.

При запросе к нескольким блокам необходимо объединить их отдельные результаты в общий. Эта операция требует какого-то количества ресурсов, поэтому с целью его сокращения блоки данных объединяются в блоки большего размера. Этот процесс называется компакцией (compaction).

Как в итоге выглядит процесс сохранения данных?

  • Prometheus забирает сэмплы с таргетов и сохраняет их в активный чанк в специальном блоке в памяти — head. Для предотвращения потери данных они также записываются в WAL-журнал.
  • После заполнения чанк сбрасывается на диск и отображается в память (см. mmap, вкратце — мы можем обращаться с содержимым так, как если бы оно находилось в памяти, не занимая при этом физической памяти), а последующие сэмплы пишутся в новый активный чанк.
  • После нескольких таких итераций первый блок (т.е. совокупность отображаемых в память чанков) становится персистентным и цикл повторяется.
  • Далее в дело вступает компакция, и блоки объединяются.


Снижаем потребление памяти


Исходя из информации выше можно предположить, что для снижения потребления памяти мы бы хотели уменьшить длительность чанков и их количество в head-блоке, тогда бы запись на диск происходила чаще, но и в памяти бы хранилось меньше данных. Однако мы не можем корректировать эти настройки, по крайней мере в Prometheus v2.

Так что же можно сделать?

Рациональный подход к управлению таргетами

Очевидно, что чем больше таргетов, тем больше объём активного чанка. Найдите баланс между необходимостью мониторинга различных компонентов системы и потребностью оптимизировать потребление памяти. Например, если важно снизить потребление памяти именно в рамках одной машины, и в наличии есть свободные ресурсы на других, то решением будет развертывание дополнительного инстанса Prometheus. Это позволит распределить таргеты между несколькими экземплярами, снизив тем самым нагрузку на каждый из них.

P.S. В любой момент времени должен работать только один инстанс prometheus-operator, поэтому убедитесь, что дополнительные релизы не устанавливают его. Для этого в values необходимо передать следующий параметр:

prometheusOperator:
  enabled: false


P.P. S. Тема масштабируемости Prometheus в данной статье обсуждаться не будет, для самостоятельного изучения вопроса почитайте про федерацию, Thanos. Полезно будет также рассмотреть инструменты VictoriaMetrics и Cortex как альтернативу или как дополнение к Prometheus.

Оптимизация запросов

Важным аспектом эффективной работы Prometheus является оптимизация запросов, так как это напрямую влияет на потребление памяти. При запросе данных, которые не кэшируются в оперативной памяти, Prometheus открывает файлы соответствующих чанков и считывает в память фрагменты, содержащие нужные ряды. Если их объем превышает доступный объем оперативной памяти, это может привести к падению Prometheus из-за OOM.

Часто в стеке мониторинга на ряду с Prometheus используют и Grafana. Теоретически, возможна ситуация, когда в ней существует множество панелей со сложными запросами, которые могут создать нагрузку на Prometheus, особенно когда данные нужны за большой диапазон (тут можно посмотреть в сторону долгосрочных хранилищ (s3, например) и Thanos).

Дополнительно с версии Prometheus 2.5.0 можно настроить параметр query.max.samples, который ограничит максимальное количество сэмплов, загружаемых в память при выполнении запроса. Ниже приведено описание из документации:


Оптимизация данных

Мы не можем повлиять на длительность чанка, но можем повлиять на его объём. И основной стратегией уменьшения объема данных, пожалуй, можно назвать релейблинг (relabeling).

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

Давайте рассмотрим, как именно можно с помощью релейблинга уменьшить объем данных. В примерах ниже не прямые рекомендации, а лишь наглядная демонстрация.

1. Изменение названия лейбла
Если название лейбла слишком громоздкое (в большинстве случаев, конечно, они краткие и емкие, но для галочки упомянем), можно его переименовать:

- action: labelmap
  regex: name(.*) ### исходное название лейбла - namespace
  replacement: 'n$1' ### новое название лейбла – nspace (можно и просто прописать 'ns')


При этом исходный лейбл сохраниться, и нужно будет удалить его вручную (см. ниже).

2. Изменение значения лейбла
Если мы хотим присвоить новое значение лейблу (покороче), можно сделать это следующим образом:

- targetLabel: namespace ### исходный лейбл namespace со значением monitoring
  replacement: 'mon' ### новое значение лейбла namespace - mon


Если какая-то часть лейбла кажется нам неинформативной и ненужной, можно ее отбросить (на практике такой способ не применяли, т.к. хватало релейблинга, описанного выше):

- sourceLabels: ['namespace'] ### исходный лейбл namespace со значением monitoring
  targetLabel: namespace ### целевой лейбл namespace (если изменить на ns, то создастся новый лейбл ns со значением из replacement). Это обязательный параметр: relabel configuration for replace action needs targetLabel value
  regex: '(.*)itoring' ### вычленяем часть значения лейбла mon
  replacement: $1 ### сохраняем часть значения лейбла mon


3. Объединение нескольких старых лейблов в один новый
Это позволит сократить количество уникальных комбинаций лейблов: объём данных, который необходимо агрегировать и фильтровать, станет меньше, а значит на обработку запросов потребуется меньше ресурсов. Сокращение уникальных комбинаций лейблов также снизит количество timeseries, что уменьшит размер чанка.

- sourceLabels: ['namespace', 'service'] ### лейблы, которые нужно объединить (namespace=monitoring, service=prometheus-postgres-exporter)
  Separator: ":" ### разделитель значений старых лейблов
  targetLabel: ns_svc ### новый лейбл ns_svc со значением monitoring:prometheus-postgres-exporter


Можно также собрать новый лейбл из частей старых лейблов:

- sourceLabels: ['endpoint', 'instance'] ### исходные лейблы (endpoint=http, instance=qwertyuiop)
  targetLabel: url ### новый лейбл url
  regex: '(.*);(.*)uiop' ### регулярки для значений каждого из исходных лейблов, разделенные символом ;
  replacement: '$1://$2' ### значение нового лейбла на основе регулярок из regex, то есть url=http://qwerty


4. Удаление ненужных лейблов
Если не часть, а весь лейбл не несет полезной информации, можно его удалить:

- action: labeldrop
  regex: namespace ### регулярка для названия лейбла, который хотим удалить


Также можно удалить лейблы с определенным значением:

- sourceLabels: ['mode'] ### исходный лейбл mode
  regex: 'accessexclusive(.*)' ### регулярка, под которую попадают значения равные accessexclusivelock
  targetLabel: mode ### целевой лейбл mode
  replacement: "" ### удаление лейбла со значением accessexclusivelock


А можно сохранить нужные лейблы, и удалить все остальные:

- action: labelkeep
  regex: '__name__|instance' ### не забывайте добавлять __name__ , т.к. имя метрики это тоже лейбл, без него нельзя


5. Удаление ненужных метрик
Часто бывает так, что какие-то экспортеры собирают метрики, которые нам не нужны, и тогда можно либо их удалить (если их немного):

- sourceLabels: [__name__]
  regex: "pg_locks_count|pg_replication_lag" ### метрики, которые необходимо дропнуть
  action: drop


или перечислить только нужные, и удалить всё, кроме них:

- sourceLabels: [__name__]
  regex: "pg_locks_count|pg_replication_lag" ### метрики, которые необходимо оставить (после применения подождите, изменения могут запоздать)
  action: keep


Дополнительно можно настроить интервал сбора метрик (scrape_interval) как экспортеров, так и самого Prometheus в соответствии с тем, насколько критично получать данные за каждый промежуток времени. Установив бОльший интервал для метрик, ежесекундное или ежеминутное обновление которых нам не нужно, можно снизить объём чанка.

На практике самым эффективным способом существенно снизить потребление памяти является дроп лейблов и метрик.

Анализ потребления памяти


Окей, давайте дропать лейблы, давайте дропать метрики. А какие?

Конечно, можно проанализировать все свои дашборды, запросы, выписать нужное, удалить ненужное… Но что, если есть способ получше?

Для начала хорошо бы понять, а что вообще потребляет память? Точно ли в лейблах дело? Для этой цели подойдёт go-профилировщик. Устанавливаем Go, запускаем команду:

go tool pprof prometheus.test/debug/pprof/heap

Не забывайте указывать порт (9090), если это требуется.

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

web — отрисовка отчета о потреблении памяти в браузере. С инструкцией по чтению можно ознакомиться тут. Если не хочется вдаваться в подробности, стоит поискать большие блоки с надписью labels (картинка кликабельна):

gobrl7lhuz_fd4pjii9fltf5eqs.png

Если таковые будут (а они будут, с высокой вероятностью) присутствовать, скорее всего повышенное потребление памяти связано не с какими-то подтеканиями, а с наличием большого количества лейблов и/или большого количества уникальных значений лейблов.

Если же нет, потребуется провести более глубокий анализ, и погрузиться в более низкоуровневые вещи…

Небольшое замечание для тех, кто использует wsl: для того, чтобы команда отрабатывала корректно, установите пакет с утилитами для WSL и добавьте в переменные окружения BROWSER=wslview. Теперь после запуска команды web, откроется окно выбора хостового браузера.

top — консольный вывод n функций, которые требуют больше всего памяти:

riu9umdrgmo5f5ocyybo9c2lihe.png

Здесь:

  • flat / flat%: общий объем и процент памяти, выделенной непосредственно самой функцией (без учета вызванных дочерних функций).
  • sum%: сумма значений flat% предыдущих строк вывода, т.е. согласно примеру выше топ 10 функций потребляют 78,71% от общей памяти.
  • cum / cum%: то же, что flat, но с учетом дочерних функций.


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

Команда tsdb analyze утилиты promtool. Отдельно её устанавливать не нужно, входит в дефолтную инсталляцию. Запускаем, изучаем help. Типовой запуск будет выглядеть примерно вот так:

promtool tsdb analyze /prometheus 01HBFNB1BRZAPA676Q28H4C13P --limit 10


где /prometheus это путь до datadir, 01HBREEEW73CG383CDMVTKT7WT — идентификатор блока (см. скрин ниже), 10 — это, собственно, топ-10 строк вывода по каждому параметру.

k_goezobwqwkwpkaqzjiikurwka.png

1. Первым в выводе идет краткое summary, в пояснениях не нуждается:

Block ID: 01HBFNB1BRZAPA676Q28H4C13P
Duration: 1h59m59.764s
Series: 202766
Label names: 306
Postings (unique label pairs): 42798
Postings entries (total label pairs): 2469427


2. Далее:

Label pairs most involved in churning:
2059 namespace=monitoring
1470 instance=monitoring.api.cloud.yandex.net:80
1470 resource_type=cluster
…


Здесь отражены комбинации названий и значений лейблов, которые есть у наиболее часто изменяющихся временных рядов. Тут churning обозначает ситуацию, когда определенный набор рядов перестает получать новые точки данных и заменяется новым активным набором рядов. Такая ситуация возникает, например, когда под с вашим приложением переподнимается после выкатки новой версии и получает новый хэш в имени. Соответственно, все ряды с меткой podname и старым именем становятся неактивными, а с новым — активными, но по итогу общее количество выросло, объём чанка вырос, потребление памяти выросло. Данная статистика поможет выявить проблемные метрики.

3.

Label names most involved in churning:
3815 __name__
3813 instance
3813 job
…


То же, что и выше, но не в виде комбинаций с конкретными значениями, а только имена лейблов.

4.

Most common label pairs:
81573 instance=monitoring.api.cloud.yandex.net:80
81563 resource_type=cluster
80006 service=managed-postgresql
…


Наиболее распространенные пары лейбл-значение (число указывает на количество рядов с такими комбинациями).

5.

Label names with highest cumulative label value length:
1525056 __name__
90486 url
30221 id
…


Самые «дорогие» (число — это количество байт) в плане памяти лейблы (потребление высчитывается на основе суммы длин всех значений для данного имени лейбла). На этот вывод обращаем особое внимание. Возможно, тут есть лейблы, которые вам не нужны, и это существенно упростит задачу.

6.

Highest cardinality labels:
34475 __name__
1545 dbname
538 datid
…


Топ лейблов по количеству их уникальных значений.

7.

Highest cardinality metric names:
7051 rest_client_rate_limiter_duration_seconds_bucket
4833 pg_locks_count
4783 _pg_database_size
…


Топ метрик по количеству их временных рядов.

То же самое в виде саммари с топ-10 можно посмотреть на статус-странице веб-интерфейса Prometheus (https://prometheus.test/tsdb-status), но только для активного блока (Head). Данный способ удобен для оценки ситуации в моменте.

По Head Stats все понятно, количество временных рядов, чанков, пар лейблов, минимальное значение времени сэмплов (т.е. в этом блоке будут содержаться данные за отрезок времени, началом которого и является данное значение), максимальное значение времени сэмплов (конец отрезка, по факту — текущее время).

Касательно Head Cardinality Stats, заголовки статистики здесь немного отличаются от тех, которые используются в tsdb analyze:

  • Top 10 label names with value count — это Highest cardinality labels;
  • Top 10 series count by metric names — это Highest cardinality metric names;
  • Top 10 label names with high memory usage — это Label names with highest cumulative label value length;
  • Top 10 series count by label value pairs — это Most common label pairs.


Теорию разобрали. Какого результата удалось добиться на практике?

Без лишних предисловий: на не самой большой инфраструктуре снизили потребление с ~10 Гб памяти до ~3 Гб без какого-то глубокого анализа, опираясь на статистику по head-блоку. В частности, мы увидели, что больше всего нагружают сервис метрики и лейблы от postgres-экспортера. По факту нужна была лишь часть метрик для дашборда в Графане и какого-то периодического мануального анализа, а также алертов. Баз было немало, поэтому лучше стало в разы.

set06pgpzt-nip6m6cin4jwvxc0.jpeg

Заключение


Мы рассмотрели ключевые аспекты применения Prometheus в качестве основы системы мониторинга, начиная с развертывания в Kubernetes и заканчивая анализом потребления памяти.

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

Надеемся, что данная статья окажется полезной для вас в повседневной практике. Делитесь своими мыслями и решениями в комментариях. Спасибо за прочтение!

© Habrahabr.ru