Пользование Quartz Scheduler

Пользование Quartz Scheduler

1. Введение: почему необходима распределенная схемы

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

Для решения этой проблемы был разработан микро-сервис планирования, отвечающий за выполнение задач на основе времени. Когда другие сервисы отправляют запросы, например, «пожалуйста, опубликуйте событие в определенное время» или «пожалуйста, вызовите API в определенное время», он выполняет их точно в заданный момент. Можно публиковать события в Kafka topic в заданное время или вызывать внешние HTTP API. Также поддерживаются повторяющиеся расписания (по секундам/минутам/часам/дням/неделям/месяцам/годам) и одноразовые расписания. Например, «опубликовать уведомление о счете каждый день в 9 утра» или «один раз вызвать API расчета в полночь 31 декабря 2024 года». Кроме того, целью разработки было добавить поддержку динамической регистрации/изменения/удаления расписаний (на основе REST API), гарантировать единственное выполнение без дублирования в распределенной среде и автоматическое восстановление при сбоях.

2. Выбор технологий: почему Quartz JDBC Clustering?

Весна@ScheduledЯ рассмотрел простое планирование с помощью аннотаций, но столкнулся с ограничениями. Я изучил другие варианты по трем причинам: невозможность дублирования выполнения, недоступность восстановления после сбоя и отсутствие динамического управления расписанием. Технологический стек, который я нашел для реализации распределенного планирования, включает ShedLock, db-scheduler, Spring Batch и Quartz Scheduler. ShedLock основан на простой блокировке, что усложняет сложное выражение расписания. db-scheduler является легковесным, но имеет ограничения на выражение триггера, а Spring Batch, ориентированный на пакетную обработку, не подходит для реального расписания.

В конечном итоге мы выбрали режим JDBC Clustering для Quartz Scheduler по следующим причинам:

  1. Кластерное управление на основе JDBC: Без использования отдельного ZooKeeper или Redis возможно управление узлами кластера только с помощью уже используемой RDBMS.

  2. Динамическое управление расписанием: Можно регистрировать/изменять/удалять задания во время выполнения, что позволяет управлять расписанием через REST API.

  3. Богатые типы триггеров: Поддерживает различные повторяющиеся шаблоны, такие как CronTrigger, SimpleTrigger, CalendarIntervalTrigger и др.

  4. Обработка сбоя: Вы можете детально контролировать политику для пропущенных задач из-за задержек сети или сбоя узлов (мгновенное выполнение, игнорирование, переназначение и т. д.).

  5. Интеграция Spring Boot: Поскольку автоматическая настройка предоставляется через spring-boot-starter-quartz, начальная установка относительно проста.

Мы использовали Quartz JDBC Clustering, который позволяет безопасно планировать в распределенной среде без дополнительной инфраструктуры.

3. Дизайн: разделение ролей приложения и Quartz

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

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

4. Реализация основных задач

4.1 Настройка кластера

Основной принцип кластеризации Quartz (распределенного планирования)application.ymlЭто настройка. Сервисы планирования, находящиеся в нескольких подах, обращаются к одной и той же базе данных и распределяют выполнение задач через блокировку базы данных.

spring:
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: never    # DB Schema를 별도로 관리하는 경우 (예시: Flyway)
    properties:
      org.quartz:
        scheduler:
          instanceName: SchedulerName
          instanceId: AUTO          # 노드마다 고유 ID 자동 생성
        jobStore:
          class: LocalDataSourceJobStore
          isClustered: true          # 클러스터 모드 활성화
	    tablePrefix: qrtz_        # 테이블 prefix 설정(vendor별 대소문자 특징 주의)
          clusterCheckinInterval: 10000  # 10초마다 하트비트
          misfireThreshold: 60000    # 60초 이내 지연은 허용
        threadPool:
          class: SimpleThreadPool
          threadCount: 10            # 노드당 동시 실행 10개
          threadPriority: 5

Если рассмотреть значение каждого элемента настройки:

  • isClustered: Включает/выключает кластерный режим. Если эта настройка включена, Quartz будет использовать базу данных QRTZ_LOCKS Получение распределенной блокировки через таблицу, QRTZ_SCHEDULER_STATEРегулярно записывает сердцебиение в таблицу.

  • instanceId: AUTO: Каждый Pod автоматически генерирует уникальный идентификатор инстанса при его запуске. В среде K8s каждому Pod присваивается другой идентификатор, что позволяет идентифицировать каждую ноду в кластере.

  • clusterCheckinInterval: 10000: С каждой 10 секунд сердце записывается в базу данных. Другие узлы проверяют этот пульс, чтобы определить, жив ли конкретный узел. Если узел не отвечает в несколько раз дольше этого интервала, этот узел считается мертвым, и работа, занимаемая данным узлом, передается другим узлам.

  • misfireThreshold: 60000: Задержка в пределах 60 секунд от запланированного времени выполнения считается нормальной.

  • Потоков: 10: На одном узле можно одновременно выполнять максимум 10 заданий. Общая максимальная параллельная пропускная способность кластера определяется как (число узлов × 10).

Для работы кластеризации в базе данных должна существовать таблица, предназначенная для Quartz.

(Ссылка – ddl) https://github.com/quartznet/quartznet/tree/main/database/tables

В целом требуется около 11 таблиц Quartz, которые я настроил для автоматического создания при развертывании сервиса с помощью скрипта миграции Flyway.

4.2 Стратегия триггера

При регистрации задачи (расписания) в Quartz, чтобы установить различные интервалы повторения от секунд до лет, простого триггера недостаточно. Выражение cron не может точно выразить такие шаблоны, как "каждые 3 дня" (даты фиксируются на 1, 4, 7 и т. д. числа каждого месяца) и такие шаблоны, как "каждые 2 недели" или "каждые 3 месяца" также невозможно выразить с помощью cron. Кроме того, при переходе на летнее время CronTrigger интерпретирует время буквально, что может привести к неожиданному поведению. Поэтому мы использовали CalendarIntervalTrigger, чтобы точно рассчитать по календарю "N дней спустя", "N недель спустя", "N месяцев спустя". Кроме того, в триггере Quartz можно установить политику Misfire на случай, если выполнение расписания не удалось. В большинстве случаев использовалась политика, при которой, если пропущено выполнение, сразу выполняется одно разовое выполнение, после чего следует следующий регулярный график.

4.3 Умное переназначение

Синхронизация расписания между приложением и Quartz Scheduler имеет важное значение. Когда данные расписания, управляeмые приложением, изменяются, это должно быть отражено и в расписании Quartz. Важно точно различать случаи, когда триггер необходимо повторно зарегистрировать, и когда этого не требуется. Когда сущность расписания изменяется, необходимо выпустить событие домена, и четко определить, нужно ли заново регистрировать или обновлять расписание Quartz на основе этого события.

Если выполнять переназначение для каждого изменения расписания, это приведет к бесконечному циклу: завершение выполнения → изменение состояния → переназначение → выполнение → изменение состояния → ... Важно правильно разграничить типы изменений и составить условия, чтобы понять, изменено ли поле, связанное с выполнением расписания, или изменено поле, не относящееся к делу.

5. Проблемы и решения

5.1 Предотвращение дублирования: двойная защита

Во время тестирования в кластерной среде возникла проблема, при которой одно и то же событие публиковалось дважды, и из-за повторной отправки события брокера происходила дубликация одного и того же расписания. Чтобы предотвратить одновременное выполнение одной и той же задачи на нескольких узлах, мы применили двухуровневые меры защиты. Во-первых, мы применили аннотацию @DisallowConcurrentExecution к классу задачи.

Кластерная блокировка БД предотвращает захват задачи одновременно несколькими узлами в момент срабатывания одного и того же триггера, обеспечивая «выполнение только одного узла», а @DisallowConcurrentExecution предотвращает запуск следующего события, если предыдущая задача еще не завершена в повторяющемся расписании, чтобы одна и та же задача не выполнялась одновременно более чем в одной копии. Во-вторых, в логике при регистрации расписания в Quartz была добавлена проверка на дублирование.

Определение "последнего выполнения" повторного расписания

Возникла проблема с тем, что повторное расписание продолжало выполняться, хотя достигло условия завершения, и в расписании с ограничением по количеству раз произошли превышения. Существовали различные условия завершения, такие как завершение после 5 выполнений, запрещение выполнения после указанного времени и другие, поэтому необходимо было учитывать много случаев. Ключевым моментом является правильное сопоставление информации о расписании, управляемой приложением, и информации о расписании, автоматически управляемой Quartz. Определение завершения - это двойная проверка на уровне Quartz и приложения. Например, при проверке следующего выполнения повторного расписания можно определить, существует ли следующее повторное расписание, используя автоматически вычисляемое значение (Время следующего срабатывания и т.д.) от Quartz. После этого необходимо проверить, закончено ли повторение, проверяя установленные время завершения повторения/количество повторений и тому подобное.

5.3 Уточнение часового пояса

При работе с временем всегда необходимо учитывать настройки часового пояса. Если обработка часовых поясов управляется непоследовательно в различных ситуациях, это может вызвать путаницу как у пользователей, так и у разработчиков. Поэтому вся информация о времени обрабатывалась на основе UTC во всех областях, таких как логическая обработка, хранение в БД, события payload, запросы/ответы API, а на фронте время отображалось в браузерном часовом поясе с помощью dayjs. Это позволяет обеспечить единообразное управление временем, чтобы разработчики и пользователи не путались.

6. Заключение

Наиболее заметным при применении JDBC-классификации Quartz Scheduler является, прежде всего, практическая полезность координации на основе базы данных..Без отдельного координатора распределения мы смогли реализовать достаточно надежное кластерное планирование, полагаясь только на блокировки на уровне строк в RDBMS. Наибольшим преимуществом было то, что сложность инфраструктуры не увеличивалась, так как использовалась уже работающая база данных. Следующим аспектом стало согласование с архитектурой, основанной на событиях. Через доменные события, интегрировавшие жизненный цикл расписаний (регистрация/изменение/удаление) с Quartz, стало естественным разделение интересов. Однако было важно заранее предсказать и защититься от побочных эффектов, вызванных цепочками событий (например, бесконечные циклы). Наконец, это касается реагирования на различные случаи. Необходимо разделить типы триггеров в зависимости от шаблонов использования и корректировать расписание в соответствии с различными условиями завершения событий, предсказывая и защищая от побочных эффектов, вызванных цепочками событий. В настоящее время услуга, о которой говорится в этом документе, еще не активно используется. В будущем, с добавлением новых функций и использованием в различных местах, ожидается, что она станет все более продвинутой в процессе решения большего количества проблем.

Quartz — это старый проект, разработанный с 2001 года, однако его ключевая механика наложения JDBC по-прежнему остается надежной и практичной. В частности, я считаю, что это вполне практичный выбор, когда в среде микросервисов, уже использующих RDBMS, необходимо реализовать распределенное расписание без дополнительной инфраструктуры.

Справочные материалы

Официальный сайт Quartz Scheduler

Официальный сайт Spring Quartz

Блог Tistory, Velog-

Коричневый

Site footer