В этом докладе я расскажу о мониторинге — как собрать множество метрик из разных мест в одном, как разруливать права для разных частей этих метрик и как хранить большие объемы данных. А в конце рассмотрим пример выбора системы мониторинга на примере небольшого сериала о вымышленной компании, которая столкнулась с необходимостью эволюции системы мониторинга вместе с ростом их инфраструктуры.
Мониторинг и ИТ-инфраструктура — как выглядела раньше и что с ней стало сейчас
Одна из самых крутых и «старых» систем мониторинга — это Zabbix.
Я с ней познакомился около 15 лет назад, когда устроился на первую работу. Уже тогда это было зрелое решение, первый релиз которого состоялся в далеком 2001 году.
Какие основные задачи у системы мониторинга? Их две — мониторить приложение и мониторить инфраструктуру.
Как выглядела ИТ-инфраструктура 15–20 лет назад?
Тогда это были физические серверы, монолитные приложения, редкие релизы и сравнительно небольшое количество метрик на один проект — 10–20 тысяч. Обычно это были метрики ОС и железа, метрики с баз данных, статус приложения (запущено или нет) и, если повезет, health check'и.
Современная инфраструктура сильно изменилась.
Теперь все приложения размещены в облаках, монолиты разбиты на микросервисы, появился Kubernetes, частота релизов сильно возросла (в некоторых компаниях бывает до 100 релизов в неделю). Особое значение приобрели бизнес-метрики. В результате с одного хоста сегодня собирается до 10–50 тыс. метрик! Вы не ошиблись: именно с одного хоста, а не со всего проекта.
Облака, Kubernetes и частые релизы сделали инфраструктуру динамичной, подстегнув количество метрик. Это в свою очередь привело к тому, что старые подходы к сбору метрик оказались нерабочими. Потребовалась новая система.
Современная система мониторинга должна:
Сама находить источники новых метрик. «Ручками» заводить в нее каждый новый под в кластере — не вариант; такая система просто не будет работать.
Собирать бизнес метрики легко и непринужденно.
Агрегировать данные. Их слишком много, и «сырыми» данными оперировать уже практически невозможно.
Обрабатывать миллионы и миллионы метрик, т.к. их просто стало больше.
С учетом всех этих требований Zabbix теперь выглядит как Nokia 3310. Есть задачи, с которыми он отлично справляется, но увидеть человека с таким телефоном — явная экзотика сегодня.
Нужна система мониторинга, которая будет удовлетворять всем нашим потребностям. И я считаю, что такая система мониторинга — Prometheus.
Что такое Prometheus и насколько он нам подходит
Система мониторинга Prometheus была разработана компанией Sound Cloud. Однажды на одной из конференций они услышали про внутреннюю систему мониторинга Borgmon компании Google, и им захотелось сделать нечто похожее, но с открытым исходным кодом. В результате в 2014 году появился Prometheus.
Это приложение на Gо представляет собой один бинарный файл, который вы запускаете. Внутри его есть несколько условно независимых процессов.
Рассмотрим их подробнее.
Service Discovering. Отвечает за взаимодействие с внешними API-системами, такими как EC2, Consul, K8s и т.д. Опрашивает эти системы и составляет список хостов, с которых можно получать метрики.
Scraping. Scraping забирает список хостов у Service Discovering и собирает метрики. В Prometheus сбор метрик осуществляется по Pull-модели по протоколу HTTP. Другими словами, Prometheus ходит на разные эндпоинты и говорит: «Дай мне метрики!». Приложение ему отвечает: «Держи!» — и возвращает страничку, на которой plain text'ом описаны метрики и их текущее значение.
Откуда Prometheus может собирать метрики?
Есть целая группа приложений-exporter'ов. Это приложение, которое умеет доставать метрики из разных источников, таких как базы данных или операционная система, и отдавать их в понятном для Prometheus формате.
Есть куча библиотек для всех возможных фреймворков, которые позволяют очень быстро доставать бизнес-метрики из приложений.
Метрики собрали, теперь их нужно сохранить. Для этого в Prometheus есть своя собственная Time Series Database (база данных временных рядов).
После того как мы сохранили метрики, нам захочется изучить их. Для этого в Prometheus есть свой собственный UI. Но он очень ограничен по функциональности: в нем нельзя сохранять графики, у него есть только один тип отображения (график) и т.д. Поэтому им никто особо не пользуется. Вместо это все работают с Grafana.
Для извлечения данных из Prometheus используется свой собственный язык запросов — PromQL, который, в том числе, позволяет агрегировать данные. Это могут быть как простые операции (сложение или вычитание нескольких метрик), так и достаточно сложные — перцентили, квантили, синусоиды, косинусоиды и вообще все, на что хватает фантазии и еще немного сверху.
Помимо этого в Prometheus есть Prometheus Rules. Этот процесс позволяет делать две простые вещи: описать запрос, а результат этого запроса сохранить в виде новой метрики.
По сути там внутри «сидит» простенький cron, который каждые N секунд или минут делает запрос, а результат сохраняет как новую метрику. Это очень удобно, когда есть большие нагруженные дашборды со сложными вычислениями: можно доставать сразу подсчитанные метрики, выполняя запросы. Получаем более быстрое отображение и экономию на ресурсах.
Также PromQL позволяет описывать алерты (триггеры). В нем можно задать PromQL-выражение и установить threshold (порог срабатывания), пересечение которого запустит триггер и отправит уведомление. При этом Prometheus не шлет уведомления в каналы мессенджера или на электронную посту — он просто выполняет push-запрос к внешней системе.
В базовой системе такой системой является Alertmanager.
Он предварительно обрабатывает алерты, позволяя строить достаточно сложный пайплайны, группировать алерты, глушить ненужное, маршрутизировать их и так далее. И уже после этого алерты можно доставлять в соответствующие каналы.
Как устроена TSDB
На мой взгляд, одним из самых важных элементов любой системы мониторинга является база данных. Если она медленная, неэффективная и ненадежная, то и вся система будет такой же. Давайте разберемся, как работает база данных в Prometheus, и насколько она подходит для наших задач.
Давайте начнем с простого абстрактного примера: у нас есть градусник, с которого каждые 60 секунд собираются данные. В данные входят текущее значение температуры и timestamp (временная метка, когда данные были получены).
Данные имеют определённые форматы. Для обоих параметров это float64. Суммарный объем данных будет 16 байт — по 8 на каждый параметр.
Теперь полученные 16 байт записываются в файл.
Проходит минута, и приходят следующие данные, которые мы также записываем в файл.
Через какое-то время собираются данные за целый час. Теперь их нужно вывести на график для дальнейшего изучения. Мы читаем файл (либо его часть), содержащий нужные нам данные, и отображаем их на графике: по оси Х — временные метки, по оси Y — значения температуры.
Все выглядит достаточно просто. Но что делать, если у нас не один источник данных, а, скажем, миллион. Как поступить в таком случае?
Попробуем решить проблему «в лоб» — мы убедились, что запись данных в файл работает хорошо, поэтому будем писать данные от каждого источника в свой отдельный файл. Получим миллион файлов.
Посмотреть данные от одного из источников тоже просто: берем нужный файл, читаем данные и отображаем их на графике.
Кажется, что этот вариант нам отлично подходит, и в Prometheus первой версии использовалась примерно такая же схема.
Но есть нюансы — куда же без них?
У нас есть миллион источников, и нам нужно раз в минуту записать в миллион файлов миллион значений. Это требует миллиона input-операций в течение одной минуты, то есть около 16500 IOPS.
Это количество не выглядит огромным, с ним справится любой современный SSD-накопитель. Но давайте копнем еще глубже!
Как мы помним, данные от одного источника у нас занимают 16 байт. Если посчитать, то при 16500 IOPS получится примерно 22 Гб в день. Это число тоже не кажется большим. Копнем еще глубже…
Что нужно, чтобы записать 16 байт на SSD? Кажется, просто взять и записать 16 байт. Однако современные SSD пишут данные блоками. Как правило, это блоки по 16 Кб (могут быть и по 32 Кб, но сути это не меняет). Для того чтобы записать 16 байт в SSD нам нужно прочитать 16 Кб, изменить в них 16 байт и затем записать обратно 16Кб.
С учетом этого пересчитаем объем данных, и получим уже не 22 Гб, а 22 Тб!
Если каждый день перезаписывать на SSD по 22 Тб, то диск быстро «умрет», т.к. все SSD имеют определенное допустимое количество циклов перезаписи.
И это только при миллионе источников. А что, если их 10 миллионов? 220 Тб в день... SSD умрет еще быстрее.
Если кажется, что миллион метрик — это много, давайте рассмотрим следующий пример. Пустой кластер Kubernetes выдает где-то 200–300 тысяч метрик. Если добавить в него приложения и начать скейлить кластер… В большом кластере количество метрик может достигать и 10–15 миллионов. Плюс они собираются намного чаще, чем раз в минуту.
Все это делает такой вариант сбора нерабочим.
Второй вариант — писать все данные в один файл.
При этом данные пишутся блоками в один файл, поэтому проблемы с производительностью и умиранием SSD у нас нет. Через какое-то время мы дописываем еще данные, затем еще… и в итоге получаем набор данных в одном файле.
Получается, что при миллионе записей в минуту мы получим около 15 Мб данных суммарно, и всего 23 Гб данных в день.
Теперь встает вопрос — как отобразить данные из этого файла на графике.
Для этого нужно просканировать весь файл, найти в нем нужные точки и отобразить их на графике.
Такой метод более или менее работает на файлах в 23 Гб, но если источников будет не миллион, а 10 миллионов, то файл уже будет 230 Гб. Объем вырастет еще сильнее, если мы хотим посмотреть данные не за сутки, а за месяц.
Такой подход имеет очевидные проблемы с масштабированием и производительностью. И это Проблема.
Заключается она в том, что данные мы всегда читаем горизонтально: нам нужны последовательные данные от какого-либо источника; а пишем — вертикально: нам приходят срезы сразу от большого количества источников данных.
Как ее решить? Мы уже знаем, что можно записывать данные от всех источников в один файл. Еще их можно записывать в оперативную память (RAM), сгруппировав по временным рядам.
Оперативная память рассчитана на существенное большее количество циклов перезаписи, а поскольку она Random Access Memory, запись в произвольные места там происходит существенно быстрее. При этом хранение данных в ней не предусмотрено — RAM штука ненадежная, после перезагрузки может быть очищена, и так далее. То есть данные все равно нужно хранить в постоянной памяти и подгружать их в RAM при необходимости.
Получается следующая схема: собранные данные мы пишем в оперативную память, структурируем там, формируем блок (например, за час) и сохраняем его на SSD.
После этого данные из оперативной памяти можно удалять. Повторяем этот цикл раз за разом.
Спустя какое-то время у нас получается множество блоков с информацией, сохраненных на SSD.
Допустим, за месяц получилось 720 файлов. И мы хотим посмотреть данные с метриками для CPU на узле. По CPU у нас не одна метрика, а целых пять: System, User, IOWait и т.д.. И при запросе такого количества метрик из разных блоков быстродействия системы может не хватить.
Для решения этой проблемы можно, например, померджить часовые блоки в блоки по 4 часа.
Затем в блоки по 16 часов…
… и так далее.
Глубина такого слияния очень сильно зависит от реализации TSDB. Они бывают разные: как на приведенной выше схеме, формирующие одноуровневые блоки в памяти с последующей записью на SSD, или даже с еще более глубоким уровнем слияния.
Подход полностью решает проблему горизонтального чтения и вертикальной записи.
Но есть еще один важный нюанс, который я целенаправленно опустил в начале. Как понять, к какому из миллиона датчиков относятся те или иные данные?
Как правило, в TSDB для этого используются лейблсеты (LableSet). Это набор пар ключ-значение.
Для каждого датчика есть свои лейблсеты, позволяющие уникально идентифицировать этот датчик.
При этом хранить их рядом с каждым временным рядом значений очень неэффективно и приводит к существенному перерасходу ресурсов. Поэтому с каждым лейблсетом ассоциируется соответствующий ID, который затем присваивается временному ряду.
Как это будет выглядеть в нашей схеме?
У нас есть миллион датчиков, у каждого датчика есть свой лейблсет, а в памяти — его специальный массив. После поступления данных мы присваиваем каждому лейблсету ID, после чего записываем его на диск и сохраняем данные от этого источника, добавляя к нему соответствующий ID. И затем то же самое записываем в память.
Затем мы проделываем то же самое для остальных источников.
Список источников, как правило, меняется слабо, поэтому постоянно выполнять маппинг данных не нужно. Можно просто продолжить дописывать новые данные.
После окончания формирования блока данных мы записываем его на диск уже вместе с маппингом.
При этом в зависимости от реализации TSDB есть разные варианты: где-то используется сквозная нумерация лейблсетов независимо от блока, в других же случаях для каждого блока ID лейблсетов уникальны.
Как построить график по данным?
Нам приходит запрос на отображение данных по лейблсету. Первое, что нужно сделать — определить, к какому ID относится лейблсет каждого блока.
Затем мы находим необходимые ID в блоках.
… и получаем по ним данные. Далее по этим данным можно построить график.
Подведем промежуточный итог:
мы решили проблему горизонтального чтения и вертикальной записи, «повернув» логику работы на 90 градусов;
для идентификации источников используются лейблсеты;
у нас есть журнал, в который данные пишутся последовательно;
в памяти хранится активный блок с данными, сгруппированный по временным рядам;
завершенные блоки записываются на диск.
Такой подход называется Log Structured Merge Tree (LSMT).
Именно он используется в большинстве современных систем мониторинга:
У InfluxDB и VictoriaMetrics это прямо прописано в документации: «Мы используем TSDB на основе LSMT». У Prometheus это не указано, но большая часть того, что я вам рассказывал, была именно про него.
Подходит ли Prometheus под требования, озвученные в самом начале?
В нем есть Service Discovery, который позволяет подстраиваться под динамичность нашей инфраструктуры.
В нем есть инструменты для сбора метрик с приложений.
В нем собственный язык запросов, который позволяет быстро и удобно агрегировать метрики.
В нем есть база данных, заточенная под хранение метрик.
Также у него сейчас огромное комьюнити, что позволяет довольно быстро находить ответы на любые вопросы.
Для него существует огромное количество экспортеров, которые позволяют быстро начать мониторить все что угодно — даже воду в бутылке, если это требуется.
Получается, что Prometheus соответствует всем требованиям, предъявляемым к современной системе мониторинга, и отлично подходит на главную роль.
А теперь давайте посмотрим наш сериальчик.
Сериал о компании «ПРОМЕТЕЙ»
Серия 1
«ПРОМЕТЕЙ» — небольшой стартап, у которого еще нет внешних клиентов. У них есть классная идея, и сейчас они ищут инвесторов, заказчиков и так далее.
Сейчас в ней три человека: два разработчика и СТО.
Основная задача Василия — искать инвесторов. Он встречается со многими людьми, представляет свою идею и рассказывает о проекте. И вот как-то раз презентация прошла не очень успешно: приложение тормозило и даже один раз упало. После выступления Вася пришел к разработчикам и спросил: «Ребята, что это было? Давайте разберемся!».
Разработчик Петя ответил, что системы мониторинга у них нет, поэтому он не знает, что произошло. И тут появляется первая задача для компании: сбор метрик.
В случае с Prometheus схема будет выглядеть очень просто.
Есть Prometheus, который собирает данные; к нему подключена Grafana. У Васи возникает логичный вопрос — что будет, если Prometheus упадет?
Очевидно, что в такой схеме не будет ничего: нет Prometheus — нет и данных. Используется Pull-модель, так что если Prometheus не работает, то и метрики собирать некому.
И это один из первых минусов такой схемы — нет отказоустойчивости.
Второй — Prometheus сильно ограничен ресурсами, которые есть на машине, ибо он не приспособлен к масштабированию.
Серия 2
Компания подросла: ей уже больше года. Появились внешние клиенты, инвесторы, сообщество. Штат компании увеличился, инфраструктура подросла.
Однажды в пятницу в половину второго ночи (как это часто бывает) Вася получает от клиента жалобу на проблему.
Он звонит Пете и просит посмотреть, что случилось. Это важно — страдают клиенты. Петя отвечает, что у них всего один Prometheus. Виртуалка с ним упала, и он не знает, что происходит.
Появляется следующая задача для компании — отказоустойчивость.
Как ее можно решить? Поставить второй Prometheus!
Поскольку он работает по Pull-модели, мы просто копируем конфигурационные файлы из одного в другой, и оба начинают собирать данные из одних и тех же источников.
Давайте подробнее рассмотрим, как это работает.
У нас есть два Prometheus'а; перед ними поднят Load Balancer, который балансирует трафик:
Если запрос приходит на первый Prometheus — данные отдаются с него. Если на второй — то со второго.
Но что будет, если один Prometheus упадет?
На первый взгляд, все должно быть хорошо. LB выкинет «упавший» инстанс из балансировки, запросы на него перестанут приходить. Второй продолжит работать, собирать метрики и отображать графики. До поры до времени все хорошо. Затем запускается первый Prometheus, и тут возникает проблема в той самой Pull-модели, поскольку в данных появляется пропуск.
Если данные будут запрошены с Prometheus'а, который временно не работал, мы увидим «дырку» на графике.
Что с этим можно сделать?
Первый и самый простой вариант — сделать в Grafana два дополнительных источника данных (data source), которые в случае «дырок» на графиках позволяют посмотреть данные в том экземпляре Prometheus'а, в котором этих «дырок» нет.
Мы приходим в рабочий Prometheus, получаем от него данные и живем счастливо. Но на этом проблемы не заканчиваются.
В этом случае они будут последовательно перезапускаться. Картина будет следующей: в одном Prometheus есть одна часть данных, в другом — другая их часть. Куда бы мы ни пришли, в любом случае увидим дырки на графиках.
На самом деле такая схема с двумя Data Source в большинстве случаев достаточна. Но если ее не хватает, то есть решение!
Суть его в том, что перед Prometheus ставится прокси:
Это не какой-то обычный прокси вроде nginx, это специализированный прокси для Prometheus.
Прокси, получив запрос, извлекает данные со всех Prometheus, объединяет их и уже на них выполняет запрос PromQL, после чего возвращает результат.
Какие минусы у такого решения? От разработчиков Prometheus нет никаких решений по реализации таких прокси, есть только сторонние разработки, и мы становимся зависимыми от них, т.к. версия PromQL должна соответствовать тому, какие у нас Prometheus. Это не всегда возможно.
Тестировались разные версии — в целом есть рабочие решения, но в production мы их не используем, поэтому что-то конкретное не могу порекомендовать.
Также у Prometheus есть еще один нюанс. Изначально он не задумывался как система для длительного хранения данных.
Об этом написано в его документации.
То есть 14 дней, 30 дней — все отлично хранится. Но если нам необходимо хранить данные 5 лет, то это уже проблема.
Серия 3
Компания превратилась в большую корпорацию, количество сотрудников тоже увеличилось: появилась служба технической поддержки. Инфраструктура увеличилась, поэтому появилась команда эксплуатации, плюс появились несколько команд разработки.
Изначально у них был один кластер, и все было хорошо. Теперь кластеров стало больше, они размещены в разных датацентрах, но приведенная выше схема мониторинга все еще работает.
И вот как-то раз Вася получает эскалацию о том, что часть сервисов не работает. Он, как хороший СТО, начинает разбираться в проблеме.
Команда эксплуатации объясняет, что у них 10 датацентров, они бегают между разными Grafana'ми и пытаются разобраться, что происходит.
Выясняется, что такая схема с локальными Prometheus'ами уже не работает, если кластеров много, и расположены они в разных местах.
Немного можно исправить ситуацию, поставив одну общую Grafana, которая будет ходить сразу во все датацентры, но это все равно поможет увидеть картину целиком.
Становится актуальной централизация метрик — метрики от разных источников нужно собрать в одном месте.
Такими решениям являются Thanos, Cortex или Mimir. Это целая группа приложений, но суть у них примерно одинакова: есть некая «коробка», в которую можем писать из наших Prometheus'ов, и к ней мы можем подключить Grafana.
Логичный вопрос от СТО, которые часто хотят сэкономить деньги — а можно ли теперь отказаться от локальных Prometheus, установленных в кластере?
Да, но нет.
Можно заменить Prometheus на несколько маленьких приложений, таких как Prometheus Agent, VM Agent, OpenTelemetry Collector и Grafana Agent. Они умеют собирать метрики и отправлять их во внешнее хранилище.
Но здесь есть некоторые нюансы.
Поскольку пропадает штука, которая отвечает на запросы, HPA/VPA не будет работать в кластере. Также могут быть проблемы с предпосчитанными метриками, поскольку их просто нечем считать. Еще могут быть проблемы с алертами — если порвалась связь между нашим Prometheus и централизованным хранилищем, то максимум, что мы получим, это уведомление об обрыве (без конкретики).
Как вариант, можно использовать Prometheus с retention 1d. Это позволит сократить размеры: Prometheus не будет «съедать» столько ресурсов, но все три описанных проблемы не будут нас беспокоить.
В итоге получается следующая схема.
Есть некоторое количество кластеров, которые пишут метрики в централизованное хранилище метрик, и есть Grafana, в которой можно посмотреть все метрики.
Что представляет из себя централизованное хранилище метрик?
Рассмотрим это на примере Mimir'а.
Основные проблемы Prometheus заключаются в его монолитности. Чтобы их решить, разработчики Mimir'а сделали одну простую штуку — они взяли Prometheus и разделили его на отдельные микросервисы. Буквально «порезали» его на кусочки, и получился Mimir, который является микросервисным вариантом Prometheus.
Также они добавили разделение данных между различными тенантами, что позволяет хранить данные от различных кластеров или датацентров отдельно друг от друга. Но при этом можно делать мультитенантные запросы, извлекая все данные.
Как это работает?
Схема централизованного хранилища выглядит так:
На первый взгляд кажется сложным, но давайте рассмотрим по частям.
Запись метрик
Все метрики изначально поступают в Distributor. Основная его задача — проверить, что к нам пришли правильные метрики, и выбрать Ingester, в который отправить их дальше.
Затем данные поступают в Ingester, который формирует у себя в памяти блоки данных (те самые блоки, о которые мы говорили ранее, и фактически это Prometheus блоки). Когда блок сформирован, он финализируется, сохраняется на диск и отправляется в S3-хранилище для долгосрочного размещения.
После того как метрики записаны в S3, в работу вступает Compactor, который оптимизирует хранение блоков (проводит то самое объединение блоков из часовых в двухчасовые и так далее).
Чтение метрик
Здесь все чуть сложнее.
Все запросы изначально поступают в Query Frontend, который:
проверяет кеш на наличие готового ответа на полученный запрос и возвращает ответ, если данные найдены;
если данные не найдены, запрос передается дальше в Querier.
Querier забирает запрос от Query Frontend и начинает подготавливать данные, которые необходимы для выполнения этого запроса. Для этого он запрашивает их сначала у Ingester, а затем у Store Gateway, который выступает интерфейсом доступа к данным в S3.
Для чего нужен Store Gateway и почему бы сразу не пойти за метриками в S3? Разберем чуть подробнее.
Store Gateway
Блоки данных хранятся в S3. При запуске Store Gateway выкачивает части блоков, содержащие маппинги лейблсетов в ID.
Как правило, это 5–10% от общего объема данных.
Когда приходит запрос на получение данных, Store Gateway, имея локальные данные о соответствии лейблов и ID, выкачивает из S3 только конкретные данные из конкретных блоков.
Далее он собирает нужные метрики воедино и отдает их в Querier.
Данные приходят к Query Frontend, который производит вычисления по PromQL и отдает их пользователю.
Почему схема такая сложная?
Есть несколько причин:
Надежность. Все компоненты Mimir масштабируются независимо друг от друга. Это позволяет обеспечить отказоустойчивость системы и подстраиваться под необходимы профиль нагрузок.
Все компоненты, в которые пишутся данные или из которых они читаются, поддерживают репликацию. Например, Distributor отправляет данные не в один, а сразу в несколько Ingester'ов — это позволяет спокойно обновлять систему или переживать падения отдельных ВМ, на которых развернута система.
Решается вопрос с масштабированием. Все компоненты поддерживают шардирование данных, и каждый из них содержит не весь комплект данных, а только одну их часть.
Оптимизация хранения благодаря Compactor'у.
Общие плюсы и минусы использования Mimir приведены на схеме ниже:
Итоги
В качестве общего вывода предлагаю посмотреть на небольшую табличку. В ней сведены все плюсы и минусы описываемых решений применительно к конкретным задачам.
Видео и слайды
Видеозапись выступления (~50 минут):
Презентация:
P.S.
Читайте также в нашем блоге: