-
[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을 주기적으로 실행시킵니다.
이상으로 Spring Boot 프로젝트에 Quartz 라이브러리를 적용하고 Quartz의 클러스터링 기능을 사용하는 방법에 대해서 알아보았습니다.
'스프링프레임워크 > quartz' 카테고리의 다른 글
[Quartz] Could't get host name! (0) 2021.02.04