Мониторинг на Python: как сохранить метрики в мультипроцессном режиме

7tujauupc_fps540eixpdskg4hm.png


Привет, Хабр! Меня зовут Никита, я backend-разработчик команды клиентских сервисов. В Selectel мы строим и поддерживаем IT-инфраструктуру для компаний, которые развивают свои цифровые продукты. В нашем департаменте около 20 приложений, большая часть из которых работает на Flask и Gunicorn. Чтобы отслеживать их производительность, мы мониторим параметры системы с помощью Prometheus.

С развитием бизнеса нагрузка на приложения возрастает, один из способов масштабировать его под большее количество запросов — запустить Gunicorn-сервер с несколькими worker-процессами в мультипроцессном режиме. Однако при таком подходе клиент Prometheus не выводит нужные нам метрики CPU и RAM. В статье расскажу, как мы решили эту проблему, сохранив метрики и организовав мониторинг в мультипроцессном режиме.
Используйте навигацию, чтобы выбрать интересующий блок:

→ Для чего нужен мониторинг
→ Как работает мониторинг при помощи Prometheus
→ Как мы организовали мониторинг в мультипроцессном режиме
→ Заключение

Для чего нужен мониторинг


Представьте: вы backend-разработчик. Вам нужно регулярно контролировать состояние приложения: например, были ли в системе проблемы после нового релиза. Чтобы отвечать на подобные вопросы за пару секунд, не изучая сотни строк логов, необходимо настроить мониторинг.

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

lw_dc6xf6ukmyvmnfbdc_vxqcno.png


Пример графика Grafana.

Метрики CPU и RAM


При мониторинге веб-приложений чаще всего отслеживают количество поступающих HTTP-запросов, длительность их обработки, а также расход ресурсов процессора (CPU) и оперативной памяти (RAM). Последние две — это системные метрики, с их помощью мы можем:

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


Для мониторинга мы используем библиотеку prometheus-flask-exporter. Если прочитать ее документацию, в самом конце можно найти врезку, что в мультипроцессном режиме Prometheus не будет собирать метрики CPU и RAM.

Please also note, that the Prometheus client library does not collect process level metrics, like memory, CPU and Python GC stats when multiprocessing is enabled. See the prometheus_flask_exporter#18 issue for some more context and details.


Текст из документации.

Может возникнуть ситуация, когда с помощью мониторинга вы понимаете, что текущих ресурсов в приложении не хватает. Вы заказываете более мощную виртуальную машину и запускаете веб-сервер в несколько процессов. В результате ваши CPU- и RAM-метрики, с помощью которых вы узнали о проблеме, пропадают.

У этой проблемы есть четыре пути решения.

  1. Смириться с отсутствием метрик CPU и RAM. Этот вариант для нас нежелательный, поскольку мы активно используем их показания.
  2. Разработать с нуля библиотеку или найти другой вариант. Разработка займет много времени и ресурсов компании, а подходящих альтернатив для связки мониторинга Flask при помощи Prometheus еще не нашли.
  3. Использовать внешний мониторинг с хост-машины или Kubernetes.Иногда это приемлемо, но есть свои тонкости — о них будет ниже.
  4. Доработать текущее решение. На этом варианте мы и остановились.


В третьем варианте есть несколько минусов. Во-первых, управление хост-машинами и их контроль — это чужая зона ответственности в продуктах. Во-вторых, не все метрики можем собрать с хост-машины. Так, статистика Python Garbage Collector находится только внутри приложения. Этот вариант нам не подошел: мы стремимся к более общему и гибкому решению за счет внутренних ресурсов нашей команды.


Как работает мониторинг при помощи Prometheus


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

В Prometheus есть внешний агент, который раз в несколько секунд приходит и запрашивает метрики. Они хранятся у нас на веб-сервисе в виде счетчиков. Их значанения доступны либо в эндпоинте (адресе, на который отправляется запрос) основного приложения, либо из отдельного веб-сервера на соседнем TCP-порту — кому как удобнее.

xgkzqk9tzug4ksut87wh8noeyte.png


Под счетчиками я имею в виду некие обертки над каждым показателем. Если вы хотите мониторить, например, количество HTTP-запросов, вы создаете под это новый счетчик. Если хотите мониторить время запросов, создаете другой. Важно понимать, что счетчики — это переменные, значение в них не обновляется само по себе. Необходимо, чтобы некий участок кода изменил значение этого счетчика. Для этого код должно что-то вызвать, некое триггерное событие.

Триггерные события в системе


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

Обновление счетчика после подсчитываемого события. Триггером может быть сам HTTP-запрос к эндпоинту: во время его обработки мы вызываем участок кода, который инкрементирует значение счетчика после каждого HTTP-запроса. К тому моменту, когда приходит внешний агент, значение будет актуальным.

ptecwrro8p1cimard0fthz3rvxs.png


Обновление счетчика побуждается неким триггером в системе.

Запрос метрик от агента. Если рассматривать триггерное события для сбора CPU- и RAM-метрик в однопроцессном режиме, необходима другая схема.

Схема: внешний агент запрашивает у приложения показатели CPU и RAM; приложение считывает метрики их и включает в ответ агенту.

xpi0jshmlkwwurdghs1knjo__og.png


Показания расхода CPU и RAM считываются тогда, когда приходит запрос от внешнего агента.

Мультипроцессный режим


1epbxdnedoreaohupebpss5ysme.png


Схема работы Gunicorn-сервера (master) с несколькими воркерами (worker).

Рассмотрим, какие проблемы возникают при сборе метрик расхода CPU и RAM в мультипроцессном режиме.

Сбор данных с дочерних процессов. Нас интересуют показатели ресурсов по worker-процессам — именно там находятся счетчики CPU и RAM. Дочерние процессы имеют изолированную память, поэтому нужно просуммировать показатели и сделать их доступными для чтения с Gunicorn master-процесса.

Триггер сбора данных в дочерние процессы. Запрос агента (триггера) приходит к мастер-процессу, тогда как счетчики находятся на воркерах. Нужно довести триггерные события до последних, чтобы инициировать считывание показателей CPU и RAM аналогично однопроцессному режиму.

Ниже расскажем, как мы решили эти проблемы, доработав текущее решение на базе библиотеки flask-prometheus-exporter.

k5h7xuqzyygjvfyumceelhmk65q.png

Как мы организовали мониторинг в мультипроцессном режиме


Подготовка к мониторингу


Наши приложения работают на Flask под Gunicorn. Чтобы настроить в них мониторинг, устанавливаем open source-библиотеку prometheus-flask-exporter:

pip install prometheus-flask-exporter


Интегрируем ее в Flask-приложение с помощью несколько строк кода из документации. Коробочное решение предоставляет количественные показатели HTTP-запросов (сколько их всего), временные (продолжительность обработки), расход CPU и RAM в приложении для однопроцессного режима.

В комплекте с библиотекой есть дашборд от Grafana. Он позволяет создавать наглядные графики в режиме реального времени. Пригодится, если нужно быстро оценить состояние приложения.

s8t5x07brdu9omon1d-2wgidep8.png


Стандартный дашборд Grafana для библиотеки flask-prometheus-exporter.

Рассмотрим, как мы решили обозначенные выше проблемы мультипроцессного режима.

Сбор данных из дочерних процессов


Библиотека flask-prometheus-exporter основана на prometheus_client, официальном Python-клиенте Prometheus. Находим класс Gauge, который умеет работать в мультипроцессном режиме (он записывает показания счетчиков в отдельные файлы на диске). Главный master-процесс, в котором запущен сервер метрик, читает показания из отдельных файлов и агрегирует их согласно одному из методов. Доступны функции суммирования, поиска минимумов и максимумов.

dk6ke4s4uk0jnjpwgvf_o58htyk.png


Доступные функции агрегирования показаний от отдельных процессов.

Для наших целей нужно суммировать показатели с воркеров. В качестве режима для расхода памяти выбираем livesum. Приставка live- означает, что будем учитывать еще существующие процессы, исключая те, которые уже завершились (даже если они записывали метрики). В режиме sum, напротив, учитываются все процессы, которые записывают метрики на диск. Его используем для отслеживания расхода CPU, поскольку система считывает функцию как показатель неубывающей величины — период времени, когда работал CPU с момента старта процесса.

Триггерное событие для считывания данных


Здесь есть два варианта. Первый — каноничный. В нем мы используем методы межпроцессного взаимодействия — например, linux-сигнал или pipe — и доводим триггерное событие с master-процесса до воркера. Однако такой метод индуцирует зависимость от используемого сервера для наших доработок, то есть от Gunicorn.

q_eyrei-yhu-puc98_gbp-oturm.png


Передача триггерного события от master к worker.

Второй — генерировать события сразу в worker-процесс. Для этого мы создаем событие с помощью запущенного в отдельном потоке таймера. Раз в несколько секунд он побуждает обновление значений счетчика. Такой метод вносит «лаг» в несколько секунд, но на временных промежутках не разрушает общую картину. Мы выбрали этот вариант, потому что он не привязывает нас к Gunicorn-серверу и позволяет доработать решение, не изменяя его исходный код.

l1m1ythghos-okzq0bxidkufvdg.png


Обновление данных по срабатыванию таймера в каждом worker-процессе.

Агрегация метрик с дочерних процессов


Рассмотрим master-процесс Gunicorn. Внутри него в отдельном потоке запущен сервер метрик. Он использует 9001 порт (TCP/UDP), откуда внешний агент запрашивает показания счетчиков. Чтобы его запустить, воспользуемся фрагментом кода из документации.

twwzwalvkovcre9hlqa-vehghi4.png
def when ready(server):

    port = 9100

    GunicornPrometheusMetrics.start_http_server_when_ready(port)

def child_exit(server, worker):

    GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid)


Далее в worker-процессах определяем счетчики для отслеживания расхода оперативной памяти и ресурсов процессора.

vmem = Gauge(..., registry=registry, multiprocess_mode="livesum")

rss = Gauge(..., registry=registry,multiprocess_mode="livesum")

cpu = Gauge(..., registry=registry,multiprocess_mode="sum")


Там же запускаем таймер с помощью стандартной библиотеки Python. При каждом срабатываниии таймера обновляем значение в счетчиках — показания расхода CPU и RAM.

class IntervalTimer(Timer):

    def run(self):

        while not self.finished.wait(self.interval):

            self.function(*self.args, **self.kwargs)

def collect_proc_metrics():

    vmem_val, rss_val, utime, stime = read_proc_metrics()

    vmem.set(vmem_val)

    rss.set(rss_val)

    cpu.set(utime + stime)

interval = IntervalTimer(collect_interval_sec, collect_proc_metrics)

interval.daemon = True

interval.start()


Разберемся, какие шаги происходят в системе.

  1. Мы запускаем таймер, который срабатывает через определенные промежутки времени и обновляет значения счетчиков.
  2. Класс Gauge записывает показания счетчиков на диск в виде файлов для каждого worker-процесса.
  3. Параллельно этому на master-процесс приходит запрос от внешнего агента, который хочет узнать о состоянии нашего сервера. Как мы выяснили ранее, сервер метрик читает все файлы из определенной директории и агрегирует показания — в данном случае, суммирует их по одному из выбранных режимов. Полученные показатели можем вывести в виде графиков на дашборд Grafana.
b-yg83textbrotgave-g-bneyxi.png


Заключение


Мы доработали библиотеку flask-prometheus-exporter — выделим преимущества нашего решения.

Программа позволяет отслеживать метрики CPU и RAM в мультипроцессном режиме. Решение строится поверх используемых библиотек и не требует модификации их исходного кода. Его легко внедрить в другие приложения на аналогичном стеке.

Расширяется для мониторинга других внутренних метрик. Мы можем мониторить другие внутренние метрики, например, количество retries, состояние circuit breaker и другие. Для этого достаточно создать экземпляр класс Gauge и добавить код, обновляющий показания нового счетчика.

Независимость от Gunicorn. Поскольку наше решение не зависит от исходного кода Gunicorn, мы избежали двух загвоздок. Во-первых, у нас нет необходимости поддерживать и синхронизировать с актуальными версиями измененный код. Во-вторых, отвязались от Gunicorn, поэтому запросто можем перенести мониторинг на другие pre-forked веб-серверы.

Пишите в комментариях о своем опыте мониторинга метрик на Python, будет интересно почитать!

Интересные материалы по теме


© Habrahabr.ru