최근에 스케줄러가 동시에 발생한 이슈가 생겼다.
우리의 스케줄러는 각각 하루에 2번만 발생하기 때문에, 매초 실행하는 스케줄러들과는 다르다.
현재 메모리를 t3.micro로 두었고, 이를 Scale-out을 하게될 경우 스케줄러가 동시에 실행되는 문제를 확인했다.
현재 질문이 12개인 QuestionSet를 생성하는 스케줄러, 각 사용자의 해당 회차의 QuestionSet에 대한 후보자를 뽑는 QuestionSheet를 생성하는 스케줄러가 있다.
2024.08.14 - [대외활동] - JPA 배치 처리로 벌크성 쿼리 최적화하기 (Mash-Up Maship Project)
JPA 배치 처리로 벌크성 쿼리 최적화하기 (Mash-Up Maship Project)
JPARepository에서 기본적으로 제공하는 메서드들이 있다. findById, findAll, delete(), deleteAll() 등. 이들 메서드는 CRUD 작업을 손쉽게 처리할 수 있도록 돕는다. 그런데 최근에 궁금해진
toychip.tistory.com
기존 스케줄러는 아래와 같다.
/* SchedulerConfig */
@Bean(name = ["questionSetSchedulerExecutor"])
fun questionSetSchedulerExecutor(): Executor {
return ThreadPoolTaskExecutor().apply {
corePoolSize = 5
maxPoolSize = 5
queueCapacity = 20
setThreadNamePrefix("questionSetScheduler-")
setWaitForTasksToCompleteOnShutdown(true)
}
}
@Bean(name = ["questionSheetSchedulerExecutor"])
fun questionSheetSchedulerExecutor(): Executor {
return ThreadPoolTaskExecutor().apply {
corePoolSize = 5
maxPoolSize = 5
queueCapacity = 20
setThreadNamePrefix("questionSheetScheduler-")
setWaitForTasksToCompleteOnShutdown(true)
}
}
/* Scheduler */
@Scheduled(cron = "\${scheduler.cron}")
@Async("questionSetSchedulerExecutor")
fun createQuestionSet() {
log.info { "=== Start Create questionSet at ${LocalDateTime.now()}. ===" }
questionUseCase.createQuestionSet()
log.info { "=== Done Create questionSet at ${LocalDateTime.now()}. ===" }
}
@Scheduled(cron = "\${scheduler.sheet-cron}")
@Async("questionSheetSchedulerExecutor")
fun createQuestionSheet() {
log.info { "=== Start Create questionSheet at ${LocalDateTime.now()}. ===" }
questionUseCase.createQuestionSheet()
log.info { "=== Done Create questionSheet at ${LocalDateTime.now()}. ===" }
}
로그를 보니 EC2 인스턴스 2개가 교차로 사용되는 것을 확인했다.
이 EC2 인스턴스 때문에 2개가 생성되는 것이였다.
결국 클러스터 환경에서 스케줄러를 사용할 때 하나의 인스턴스만 생성되게 하거나, Lock을 사용하던 여러 방법들이 있었다.
나는 ShedLock을 사용하여 이를 해결했다.
ShedLock을 사용하는 방법은 아래와 같다.
/* SchedulerConfig */
@Configuration
@EnableScheduling
@ComponentScan(basePackageClasses = [SchedulerBasePackage::class])
// 스케줄러 락을 사용하여 중복 실행 방지, 최대 락 시간 5분 설정
@EnableSchedulerLock(defaultLockAtMostFor = "PT5M")
class SchedulerConfig {
@Bean
fun lockProvider(dataSource: DataSource): JdbcTemplateLockProvider {
return JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(JdbcTemplate(dataSource))
// 데이터베이스 시간 사용
.usingDbTime()
.build()
)
}
}
/* Scheduler */
@Scheduled(cron = "\${scheduler.cron}")
@SchedulerLock(name = "createQuestionSet", lockAtMostFor = "PT5M", lockAtLeastFor = "PT4M")
fun createQuestionSet() {
log.info { "=== Start Create questionSet at ${LocalDateTime.now()}. ===" }
questionUseCase.createQuestionSet()
log.info { "=== Done Create questionSet at ${LocalDateTime.now()}. ===" }
}
@Scheduled(cron = "\${scheduler.sheet-cron}")
@SchedulerLock(name = "createQuestionSheet", lockAtMostFor = "PT5M", lockAtLeastFor = "PT4M")
fun createQuestionSheet() {
log.info { "=== Start Create questionSheet at ${LocalDateTime.now()}. ===" }
questionUseCase.createQuestionSheet()
log.info { "=== Done Create questionSheet at ${LocalDateTime.now()}. ===" }
}
위와 같이 ShedLock을 사용하여, PTM의 식을 통해 DB에 Lock을 걸어 여러 개의 인스턴스가 스케줄러를 동시에 실행할 수 없도록 한다.
실제 DB에는 아래와 같이 Lock이 걸리게 된다.
이때 key는 name으로 사용되므로, 스케줄러가 동시에 실행되는 것을 방지하고 한 곳에서만 생성되도록 하여 문제가 해결되었다
'프로젝트 > Maship' 카테고리의 다른 글
Maship 4. MVP 발표, 서버 터짐 🥵 (Mash-Up Maship Project) (0) | 2024.08.24 |
---|---|
Maship 2. Select, Insert 쿼리 분리 및 JPA 배치 처리로 벌크성 쿼리 최적화하기 (Mash-Up Maship Project) (0) | 2024.08.14 |