ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Quartz] Spring Boot에서 Quartz 클러스터 적용
    스프링프레임워크/quartz 2021. 1. 26. 12:50

    이번 시간에는 Spring Boot 프로젝트에서 Quartz 클러스터를 적용해보겠습니다.

     

    우선 Quartz는 아래와 같은 특징을 제공하는 Job 스케줄링 라이브러리입니다.

    • 모든 Java 어플리케이션에 통합 가능
    • 수십에서 수천 개의 작업도 실행 가능하며 간단한 Interval 형식이나 Cron 표현식으로 복잡합 스케줄링도 지원
    • Job에서 수행되는 작업들은 직접 프로그래밍 할 수 있음
    • JTA 트랜잭션 처리나 클러스터링 기능도 지원

    Cron Expression

    • 특수문자
      • * : 모두 포함
      • ? : 해당 필드 고려 X
      • - : 일련의 범위
        • 2-4는 2, 3, 4를 의미
      • , : 일련의 값을 나열
        • 2-4는 2,3,4로 표현 가능
      • / : 초기치를 기준으로 일정하게 증가하는 값을 의미
        • 초를 나타내는 필드에 0/15는 0초를 시작으로 15초씩 증가를 의미 (0, 15, 30, 45)
    • 사용 예제
      • 매 초마다 실행 : * * * ? * *
      • 매 분마다 실행 : 0 * * ? * *
      • 매 시간마다 실행 : 0 0 * ? * *
      • 매일 0시에 실행 : 0 0 0 * * ?
      • 매일 1시에 실행 : 0 0 1 * * ?
      • 매일 1시 15분에 실행 : 0 15 1 * * ?
      • 4시간마다 실행 : 0 0 */4 ? * *

     

     

    01. build.gradle에 필요한 dependency 추가

    • Quartz 클러스터링 시 데이터베이스를 이용해서 작업을 처리하므로 데이터베이스 관련 dependency도 추가합니다.
    • 예제에서는 MySQL 데이터베이스를 사용하므로 관련 connector도 추가해줍니다.
    plugins {
        id 'java'
        id 'eclipse'
        id 'idea'
        id 'org.springframework.boot'
        id 'io.spring.dependency-management'
    }
    
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-quartz'
    
        // mysql
        implementation 'mysql:mysql-connector-java'
    
    
        implementation 'org.projectlombok:lombok:1.18.12'
        annotationProcessor 'org.projectlombok:lombok:1.18.12'
    
        testCompile 'org.springframework.boot:spring-boot-starter-test'
    }

     

    02. 데이터베이스에 Quartz 클러스터링시 필요한 테이블 생성

    • 해당 테이블 스키마는 spring-boot-starter-quartz가 로드 된 상태에서 tables_mysql_innodb.sql을 찾아서 생성할 수 있습니다.
    #
    # In your Quartz properties file, you'll need to set
    # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    #
    #
    # By: Ron Cordell - roncordell
    #  I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM.
    
    DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
    DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
    DROP TABLE IF EXISTS QRTZ_LOCKS;
    DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
    DROP TABLE IF EXISTS QRTZ_CALENDARS;
    
    CREATE TABLE QRTZ_JOB_DETAILS(
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME VARCHAR(190) NOT NULL,
    JOB_GROUP VARCHAR(190) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME VARCHAR(250) NOT NULL,
    IS_DURABLE VARCHAR(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    JOB_NAME VARCHAR(190) NOT NULL,
    JOB_GROUP VARCHAR(190) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT(13) NULL,
    PREV_FIRE_TIME BIGINT(13) NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT(13) NOT NULL,
    END_TIME BIGINT(13) NULL,
    CALENDAR_NAME VARCHAR(190) NULL,
    MISFIRE_INSTR SMALLINT(2) NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
    REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    REPEAT_COUNT BIGINT(7) NOT NULL,
    REPEAT_INTERVAL BIGINT(12) NOT NULL,
    TIMES_TRIGGERED BIGINT(10) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_CRON_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    CRON_EXPRESSION VARCHAR(120) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SIMPROP_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(190) NOT NULL,
        TRIGGER_GROUP VARCHAR(190) NOT NULL,
        STR_PROP_1 VARCHAR(512) NULL,
        STR_PROP_2 VARCHAR(512) NULL,
        STR_PROP_3 VARCHAR(512) NULL,
        INT_PROP_1 INT NULL,
        INT_PROP_2 INT NULL,
        LONG_PROP_1 BIGINT NULL,
        LONG_PROP_2 BIGINT NULL,
        DEC_PROP_1 NUMERIC(13,4) NULL,
        DEC_PROP_2 NUMERIC(13,4) NULL,
        BOOL_PROP_1 VARCHAR(1) NULL,
        BOOL_PROP_2 VARCHAR(1) NULL,
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_BLOB_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    BLOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_CALENDARS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME VARCHAR(190) NOT NULL,
    CALENDAR BLOB NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_FIRED_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    INSTANCE_NAME VARCHAR(190) NOT NULL,
    FIRED_TIME BIGINT(13) NOT NULL,
    SCHED_TIME BIGINT(13) NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(190) NULL,
    JOB_GROUP VARCHAR(190) NULL,
    IS_NONCONCURRENT VARCHAR(1) NULL,
    REQUESTS_RECOVERY VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SCHEDULER_STATE (
    SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(190) NOT NULL,
    LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
    CHECKIN_INTERVAL BIGINT(13) NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_LOCKS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME VARCHAR(40) NOT NULL,
    PRIMARY KEY (SCHED_NAME,LOCK_NAME))
    ENGINE=InnoDB;
    
    CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
    CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
    CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
    CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
    CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
    CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
    CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
    CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
    CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
    CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
    CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
    CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
    CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
    CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
    CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
    CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
    CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
    CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
    
    commit;
    

    03. application.yml에 Quartz 관련 설정 추가

    • misfireThreshold (단위: milliseconds)
      • misfire라고 판단되는 기준 시간
    • clusterCheckinInterval (단위: milliseconds)
      • 스케줄러가 데이터베이스를 체크하는 주기
      • 실패 된 인스턴스를 파악하는 속도에 영향을 끼침
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC
        username: admin
        password: password
        hikari:
          driver-class-name: com.mysql.jdbc.Driver
    
      jpa:
        hibernate:
          ddl-auto: create
        properties:
          hibernate:
            #show_sql: true
            format_sql: true
            use_sql_comments: true
            default_batch_fetch_size: 3
    
      quartz:
        job-store-type: jdbc
        jdbc:
          initialize-schema: never
        properties:
          org:
            quartz:
              scheduler:
                instanceId: AUTO
              jobStore:
                class: org.quartz.impl.jdbcjobstore.JobStoreTX
                driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
                useProperties: false
                tablePrefix: QRTZ_
                misfireThreshold: 60000
                clusterCheckinInterval: 1000
                isClustered: true
              threadPool:
                class: org.quartz.simpl.SimpleThreadPool
                threadCount: 10
                threadPriority: 5
                threadsInheritContextClassLoaderOfInitializingThread: true

     

    04. Quartz관련 서비스 생성

    • addSimpleJob()
      • 특정 Interval마다 Job을 실행시키도록하는 함수
    • addCronJob()
      • Cron 표현식을 통해 실행시키도록하는 함수
    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class QuartzService {
        private final Scheduler scheduler;
    
        public void addSimpleJob(Class job, String name, String desc, Map params, Integer seconds) throws SchedulerException {
            JobDetail jobDetail = buildJobDetail(job, name, desc, params);
    
            if (scheduler.checkExists(jobDetail.getKey())) {
                scheduler.deleteJob(jobDetail.getKey());
            }
    
            scheduler.scheduleJob(
                    jobDetail,
                    buildSimpleJobTrigger(seconds)
            );
        }
    
        public void addCronJob(Class job, String name, String desc, Map params, String expression) throws SchedulerException {
            JobDetail jobDetail = buildJobDetail(job, name, desc, params);
    
            if (scheduler.checkExists(jobDetail.getKey())) {
                scheduler.deleteJob(jobDetail.getKey());
            }
    
            scheduler.scheduleJob(
                    jobDetail,
                    buildCronJobTrigger(expression)
            );
        }
    
        private JobDetail buildJobDetail(Class job, String name, String desc, Map params) {
            JobDataMap jobDataMap = new JobDataMap();
            if(params != null) jobDataMap.putAll(params);
            return JobBuilder
                    .newJob(job)
                    .withIdentity(name)
                    .withDescription(desc)
                    .usingJobData(jobDataMap)
                    .build();
        }
    
        /**
         * Cron Job Trigger
         */
        // *  *   *   *   *   *     *
        // 초  분  시   일   월  요일  년도(생략가능)
        private Trigger buildCronJobTrigger(String scheduleExp) {
            return TriggerBuilder.newTrigger()
                    .withSchedule(CronScheduleBuilder.cronSchedule(scheduleExp))
                    .build();
        }
    
        /**
         * Simple Job Trigger
         */
        private Trigger buildSimpleJobTrigger(Integer seconds) {
            return TriggerBuilder.newTrigger()
                    .withSchedule(SimpleScheduleBuilder
                            .simpleSchedule()
                            .repeatForever()
                            .withIntervalInSeconds(seconds))
                    .build();
        }
    
        public static String buildCronExpression(LocalDateTime time) {
            LocalDateTime fireTime = time.plusSeconds(10);
            // 0 0 0 15 FEB ? 2021
            return String.format("%d %d %d %d %s ? %d", fireTime.getSecond(), fireTime.getMinute(), fireTime.getHour(), fireTime.getDayOfMonth(), fireTime.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH).toUpperCase(), fireTime.getYear());
        }
    }

     

    05. Quartz Job 생성

    Job 생성 시 아래 2개의 어노테이션을 같이 사용할 수 있습니다.

    • @DisallowConcurrentExecution
      • 하나의 Job이 동시에 실행되는 것을 막아줍니다.
    • @PersistJobDataAfterExecution
      • Job의 JobDataMap의 값을 유지시켜줍니다.
    @Slf4j
    @Component
    public class QuartzJob extends QuartzJobBean {
        @Override
        protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
            log.info("Quartz Job Executed : {}", LocalDateTime.now().toString());
    
        }
    }

     

    06. Job을 생성하는 Component 생성

    • 테스트를 위해서 어플리케이션 시작 시 Job을 Quartz 스케줄러에 추가하는 Component 생성
    @RequiredArgsConstructor
    @Component
    public class BatchService {
        private final QuartzService quartzService;
    
        @PostConstruct
        public void init() {
            try {
                quartzService.addSimpleJob(QuartzJob.class, "QuartzJob", "Quartz 잡",null , 10);
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }

     

    07. 클러스터링 테스트 진행

    • 두 개의 Instance을 띄우서 테스트를 진행해야 하기 때문에 저는 server.port 번호 8080과 8081로 바꿔서 두 개의 bootJar를 생성하였습니다.
    • A Instance와 B Instance 둘 다 실행을 시키면 클러스터링 기능을 통해서 같은 Job에 대해 A Instance만 Job을 주기적으로 실행시키고 B Instance는 대기하게 됩니다.
    • 13:38:21에 A Instance를 shutting down 시킵니다.
    • 클러스터링 기능에 의해서 13:38:28에 B Instance가 해당 Job을 주기적으로 실행시킵니다.

    A Instance
    B Instance

    이상으로 Spring Boot 프로젝트에 Quartz 라이브러리를 적용하고 Quartz의 클러스터링 기능을 사용하는 방법에 대해서 알아보았습니다.

    TAG

    댓글 1

Designed by Tistory.