Сравнение Quartz, kagkarlsson и кастомной реализации для запланированного задания в реактивном Spring Boot приложении

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Часто Java-разработчикам требуется реализовать запланированные задания. Современные реалии диктуют нам, что система должна быть масштабируемой, то есть вне зависимости от количества реплик и распределения нагрузки мы ожидаем выполнения задания по условиям, которые были определены на входе. Существуют подключаемые библиотеки для реализации jobService. Используя базу данных, можно написать свой jobService, c одной стороны, вероятность ошибки больше, чем с готовым решением, но с другой стороны – в своем коде разбираться и вносить правки проще, чем в общедоступной библиотеке. Кроме того, собственная реализация будет учитывать особенности системы, в нашем случае, это реактивность. В статье подробнее описаны плюсы и минусы этих подходов.

Я поддерживаю проект, который был написан в микросервисной, реактивной парадигме (spring-boot-starter-data-r2dbc, spring-boot-starter-webflux, r2dbc-postgresql). Основной сервис этого проекта осуществляет взаимодействие с базой данных в реактивном подходе, реализует взаимодействие с другими сервисами через Rest и Kafka. Кроме этого, он содержит 3 запланированных задания с кастомной реализацией: 2 по переотправке сообщений и 1 по очистке таблицы.

Используя кастомную реализацию (описание в п.1), мы столкнулись с проблемой, что после завершения работы пода (приложение развернуто в двух экземплярах на Open Shift), в котором задание было запущено, но не выполнено, оно оставалось в статусе выполняется и так и не перезапускалось, другими словами job завис. Эту проблему решили проверкой последнего времени обновления задания, в случае если видно, что job уже слишком долго в статусе выполняется, он перезапускается.

Это решение помогло, однако возник вопрос, можно ли использовать вместо этого
сервиса готовое решение для запланированных заданий. Решение о том, что
использовать при проектировании принималось не нашей командой, мы занимаемся
поддержкой разработанного решения, но я решила исследовать возможные
альтернативы и сравнить их.

1. Кастомная реализация

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

С помощью аннотаций EnableScheduling, Scheduled, установленных в трех соответствующих сервисах и собственной реализации JobService эти задания запускаются через фиксированное время.

Доступ к таблице с заданиями осуществляется через репозиторий, который унаследован от org.springframework.data.repository.reactive.ReactiveCrudRepository<Job, Long>.

Класс Job имеет соответствующие поля из таблицы jobs. У задания есть только два статуса готово к выполнению и выполняется. Обновление происходит с помощью запроса UPDATE jobs SET status = :status, running_by = :runningBy, updated_at = :updated WHERE name = :name and status = :oldStatus. То есть если один экземпляр приложения начнет обновлять статус job, другой экземпляр эту запись не сможет изменить, этим достигается гарантия того, что job будет выполнен 1 раз при двух репликах приложения.

2. Quartz

Библиотека Quartz существует давно (есть статья на habr от 2013 года, а последний релиз был в 2019, баги правятся и сейчас) и в интернете полно примеров, как ее настраивать, а под этими примерами вопросы так ли нужно использовать эту библиотеку, если она порождает 11 таблиц и так сложна в настройке https://habr.com/ru/company/otus/blog/475996/). Однако Quartz не только может быть встроен в существующее приложение, но и может работать как отдельная программа (внутри собственной виртуальной машины Java), которую можно использовать через RMI.

Проанализировав несколько примеров (статью из предыдущего абзаца и этот пример), я добавила эту библиотеку себе в реактивное приложение. Для наших трех запланированных заданий понадобится 3 сервиса, которые имплементят org.quartz.Job. Далее необходим класс AutowiringSpringBeanJobFactory, используемый в бине SpringBeanJobFactory, который позволяет задать всю информацию о планируемых заданиях, настройки SchedulerFactoryBean во время создания инстанса. Бинами в конфигурации были созданы такие классы как DataSource, JobDetail[], Trigger[], SchedulerFactoryBean.

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

  • qrtz_triggers —общая информация о триггере, имя описание, статус, временные метки;

  • qrtz_simple_triggers, qrtz_simprop_triggers, qrtz_crons_triggers, qrtz_blob_triggers имеют отношение внешнего ключа к qrtz_triggers, здесь хранятся подробности для каждого вида триггеров. Вот только не очень понятно, если у меня в приложении 3 одинаковых вида заданий, зачем мне хранить пустые таблицы для видов, которые я не использую? Конечно, возможность создавать кастомизированные триггеры очень интересна, но в следующей библиотеке для этого не понадобилось так засорять базу данных;

  • qrtz_job_details — здесь прописан сервис, который относится к конкретному job;

  • qrtz_fired_triggers — это журнал всех сработавших триггеров;

  • qrtz_paused — предназначен для сохранения информации о неактивных триггерах;

  • qrtz_calendars — полезны для исключения блоков времени из расписания срабатывания триггера. Например, вы можете создать триггер, запускающий задание каждый будний день в 10:30, а затем добавить календарь, исключающий все праздники компании;

  • qrtz_lock — значение имени экземпляра, выполняющего задание, чтобы избежать сценария, когда несколько узлов выполняют одно и то же задание;

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

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

Касательно гарантии, что планировщик выполняет задание один раз, Quartz резервирует с помощью SELECT ... FOR UPDATE (само выражение можно менять через настройку selectWithLockSQL) определенное количество триггеров для исполнения, выполняет их, а потом снимает блокировку, подобное решение используется и в следующей библиотеке.

3. kagkarlsson.db-scheduler

В поисках более легковесной библиотеки я нашла этот вопрос, в котором один из авторов kagkarlsson/db-scheduler предлагает свое решение, которое понравилось мне намного больше, за исключением того, что название труднопроизносимо.

Для добавления в проект этой библиотеки понадобился только бины DataSource, 3 бина RecurringTask<Void> для каждого задания. В базе появилась только одна таблица scheduled_tasks, ее пришлось добавить через миграцию, в нашем случае liquibase. Процесс создания разных видов заданий описан в GitHub библиотеки, там же скрипты для создания таблицы и примеры приложений.

Планировщик использует две возможные стратегии, которые можно задать настройкой polling strategy: оптимистическую блокировку(fetch-and-lock-on-execute) или select-for-update(lock-and-fetch) (когда забираем данные для обновления, они заблокированы для изменения другими транзакциями, на данный момент стратегия доступна для postgres, нам это подходит).

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

Выводы

Учитывая, что в нашем конкретном случае настройка календарей, высокая пропускная способность, кастомная настройка заданий не нужны, мы остаемся с собственной реализацией JobService. Чтобы следить за тем, что задания выполняются корректно, мною были настроены метрики, по которым я могу отслеживать зависания и в случае проблем я могу легко вносить правки в существующую таблицу для заданий и в код взаимодействия с ней.

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

Источник: https://habr.com/ru/company/nlmk/blog/679162/


Интересные статьи

Интересные статьи

Думаю, вы знакомы с графиками сравнения точности архитектур. Их применяют в задачах по классификации изображений на ImageNet. В каждом сравнении которые я мог встретить ранее в Интернете, как пра...
Существует проблема, над которой я размышляю уже несколько лет: удобство пользования видеоигровыми консолями гораздо ниже, чем работа со стандартными потребительскими приложениями, но об этом редко ...
В 1992-м году проходил очередной конкурс по обфусцированному программированию на языке С. Один из представленных проектов был небольшой форт системой. Меня поразило, что виртуальная машин...
Привет, Хабр! Думаю, никто не будет отрицать, что виртуальные машины — незаменимая в хозяйстве вещь. Ведь это значительно удобнее, чем ставить множество ОС на свой компьютер. Или я вот, н...
Вступление В проектах я видел три примера, так или иначе связаные с теорией конечных автоматов Пример 1. Занимательный говнокод код. Занимает уйму времени, на то чтоб понять что происходит...