Привет, Олимпийский Хабр! Меня зовут Аня, я работаю в Ozon: строю и развиваю инфраструктуру мониторинга в Observability-платформе. Моя команда помогает разработчикам следить за своими сервисами и своевременно получать алерты, если что-то идёт не так. На Хабре довольно много статей о том, что такое кроны, как и где можно запускать их по расписанию, о плюсах и минусах каждого варианта и о том, как запускает кроны Kubernetes. Поэтому в моей статье об этом мы говорить не будем. Я расскажу, как мы решали конкретную проблему мониторинга CronJob, запущенных в Kubernetes.   Чтобы сделать наш кейс более понятным, стоит немного рассказать про то, как устроен мониторинг в целом. В этой статье под мониторингом понимаются сбор метрик и алертинг. Сейчас мы в Ozon используем связку Prometheus-Thanos-Grafana. В общем случае мониторинг у нас представляет собой pull-модель, состоящую из большого количества Prometheus c поднятыми рядом Thanos Sidecar. Prometheus сгруппированы по своему назначению: В платформе большинство метрик стандартизированы, они выдаются по определённому порту и адресу. С некоторым интервалом Prometheus приходит за метриками, затем осуществляется подсчёт агрегаций и алертов. Prometheus не рассчитан на длительное хранение метрик. Он складывает метрики на диск, а Thanos Sidecar в свою очередь доставляет их в long-term-хранилище. Дальше Thanos и его модули отвечают за долгосрочное хранение метрик, а также предоставляют единую точку входа для их чтения и просмотра. В Grafana мы настроили некоторое количество базовых дашбордов, в которых разработчику достаточно выбрать свой сервис из выпадающего списка — и он получит исчерпывающую информацию о нём. Все микросервисы создаются в рамках нашей конвенции разработки и имеют набор стандартных метрик. Эти стандартные метрики позволяют сервисам из коробки иметь базовые инструменты мониторинга: базовые алерты, базовые дашборды в Grafana. Если командам не хватает базовых алертов или дашбордов, они всегда могут создать свои. 
Упрощённая схема мониторинга В редких случаях, когда требовалось отправлять метрики и сбор непосредственно с источника по pull-модели был невозможен, команды поднимали под свои нужны преднастроенный Prometheus Pushgateway, c которого мы потом забирали метрики.  Как можно понять из вышесказанного, вся наша инфраструктура сбора метрик и алертинга предназначена для существующих длительное время объектов. Однако по своей сути CronJob очень отличается от сервиса. СronJob — это повторяющаяся и работающая короткое время задача. Именно это и делает мониторинг кронов проблематичным: нельзя собрать с них метрики тем же способом, что с сервиса.  Сейчас в Ozon очень много CronJob (в основном написанных на Go и С#), которые используют множество технологий и отвечают за разные задачи. Большинство разработчиков хотели знать состояние кронов в случаях: Продуктовые команды в дополнение к этому хотели писать свои метрики и получать по ним алерты.  В итоге задача звучала так: организовать мониторинг CronJob с минимальным влиянием на разработчиков и джобы. Для себя мы разделили задачу на несколько этапов:  
Распределение языков программирования, используемых в СronJob в Ozon Для организации базовых алертов мы решили дистанцироваться от содержания CronJob. Для этого мы стали использовать те данные, которые предоставляет о них сам Kubernetes. Для этого у нас используется kube-state-metrics (KSM). KSM — это сервис, который слушает Kubernetes API и генерирует метрики о состоянии объектов. В нашем случае нас интересовали метрики для CronJobs, Jobs и Pods. В результате мы написали набор базовых алертов.  Агрегации, на основе которых написаны алерты, представлены ниже:  Кронджоба зафейлилась. Алерт базируется на комбинации метрик kube_job_status_start_time (джоба начала свою работу) и kube_job_failed{conditon=”true”} (джоба зафейлилась). Агрегация, которая лежит в основе алерта: Крон не запустился. Алерт базируется на метрике kube_cronjob_status_last_schedule_time (когда крон был запущен последний раз):  Долгое время работы крона. Алерт базируется на метриках kube_job_status_start_time и kube_job_status_active (крон работает в данный момент): Все агрегации затем сравниваются с порогами, которые разработчики выставляют сами для своих кронов. Особенностью написания алертов по метрикам является то, что Kubernetes в общем виде работает с джобами, а кронджоба — это настройка расписания запусков, политика рестартов, политика времени работы. И многие метрики существуют до тех пор, пока существует объект в Kubernetes: если задача когда-то давно зафейлилась, но при этом до сих пор существует pod, то метрика kube_job_failed{conditon="true"} будет существовать всё это время. Данный аспект делает необходимым отсечение заведомо старых джоб от последних запущенных в выражениях. В результате многие алерты разрастаются и становятся трудночитаемыми. На первый взгляд задача по сбору кастомных метрик с кронджоб является довольно простой, но стоит помнить о масштабах уже существующей инфраструктуры. Новый функционал должен вписываться в концепцию развёрнутой системы мониторинга. Для начала надо определиться, по какой модели мы будем собирать метрики: push или pull. Кроны будут отдавать метрики, как обычные сервисы, по пути /metrics и ждать, пока Prometheus придёт и заберёт метрику. Плюсы: Минусы: 
Pull-модель Кронджоба отправляет метрики в Thanos Receive / Prometheus Pushgateway. Мы предоставляем либу, которая в фоновом режиме отправляет метрики. Плюсы: Минусы: Стоит отметить, что к моменту перехода мониторинга в стадию тестирования в Prometheus версии 2.25.0 появился новый флаг, который позволяет включать экспериментальную функциональность, в частности возможность обрабатывать запросы remote_write (пушить в Prometheus). Но поскольку в дальнейшем эта опция может быть удалена, мы не стали рассматривать данный вариант. 
Push-модель Мы выбрали push-модель, так как нам было важно сохранить независимость кронджоб от систем мониторинга. Нестабильность инфраструктуры мониторинга не должна влиять на работу других систем. Если принимающий метрики сервис недоступен, то крон продолжит свою работу по расписанию. С моделью мы разобрались. Но реализовывать её можно двумя разными способами. По своей сути Pushgateway — это некий мост между push- и pull-моделями. Кронджоба будет пушить в гейтвей, а Prometheus затем заберёт из него метрики.  Но возникает вопрос удаления метрик. Метрика, отправленная в Pushgateway, существует в нём, пока не будет в явном виде удалена или перезаписана.  Удаление по таймеру не подходит: у нас огромное количество кронов, все они разные, и мы не можем предположить, когда тот или иной крон остановится.  Внедрение управляющего сигнала с крона не подходит. Мы не можем обновить или удалить метрику, если её не успел забрать Prometheus, — в этом случае мы просто все полученные метрики выбрасываем. Да и патчить Pushgateway или ставить проксю не хотелось, чтобы не плодить новые сервисы, которые надо будет поддерживать. Thanos Receive является полноценным модулем Thanos. Он реализует Prometheus Remote Write API и построен поверх базы Prometheus TSDB, добавляя функциональные возможности за счёт длительного хранения и горизонтальной масштабируемости. Фактически внутри Thanos Receive реализован инстанс Prometheus, который настроен на непрерывную запись. Он пишет метрики в TSDB-блоки на диск, а затем ресивер (shipper) загружает TSDB-блоки в Object Storage каждые два часа по умолчанию. Thanos Receive предоставляет StoreAPI, благодаря чему Thanos Query может запрашивать метрики в реальном времени.  
Упрощённая схема Thanos Receive В отличие от варианта с использованием Pushgateway, большим плюсом ресивера является то, что мы отправляем метрики и нам не надо следить за ними: удалять или специальным образом обновлять. Мы выбрали полноценную push-модель на основе Thanos Receive. В итоге мы написали небольшую библиотеку для Go, которая отправляет метрики в фоновом режиме раз в 15 секунд и перед завершением работы крона.  Под капотом библиотеки запускается тикер. На каждый тик запрашиваются метрики у prometheus.Gatherer, который в свою очередь запрашивает все метрики у коллекторов и отдаёт их в виде лексикографически отсортированного слайса. Если возникает ошибка, то Gatherer пытается собрать столько метрик, сколько может. Однако Prometheus работает с метриками типа dto.MetricFamily, а remote write API принимает prompb.WriteRequest. Поэтому нам пришлось написать некий мост: из полученных от Gatherer метрик формируем реквест с нужным типом данных, который и отправляем на ресивер.

Bridge Библиотека обогащает метрики служебными лейблами, чтобы определять, кому они принадлежат, и избегать конфликта наименований: Также она предоставляет определённый набор готовых для использования метрик: Разработчикам, со своей стороны, достаточно инициализировать пакет в main.go: Способ регистрации метрик и их использование никак не отличаются от сервисов: Endpoint ресивера проставляем в env-переменную пода через Admission Controller. Admission Controller проверяет поды при создании или изменении и, если под относится к кронджобе, проставляет receiver endpoint.  Данный подход позволяет нам в любой момент изменить endpoint без участия разработчиков, не требуется редеплой кронджобы. При новом запуске крона у пода будет уже новый адрес.  Кронджоба пушит метрики в ресивер, далее ресивер доставляет метрики в Object Storage.  Но теперь у нас все метрики минуют Prometheus, где мы до этого считали алерты. Поэтому мы подняли Thanos Rule. Но стоит помнить о том, что Thanos Rule зависит от доступности и работоспособности Thanos Query, который, в свою очередь, запрашивает данные из Store APIs. 
Упрощённая итоговая схема мониторинга CronJob В итоге мы реализовали инфраструктуру мониторинга CronJob следующим образом: C момента ввода в эксплуатацию прошло четыре месяца, в течение которых нашу систему мониторинга стали использовать на проде (игрушечные кластеры не учитываю) более чем для 200 кронджоб. 150 кронджоб используют базовые алерты, 125 кронов отправляют метрики. Команды написали более 50 кастомных алертов. Суммарно на проде было получено свыше 100 базовых алертов. Стоит отметить, что Thanos Rule используется далеко за пределами мониторинга CronJob. Этот модуль позволил реализовать алерты, которые считаются за большой промежуток времени (несколько дней или недель). Observability-platform | Monitoring