본문 바로가기

개발일지/TIL

[230913] Scale-out 환경에서 Scheduler 중복으로 실행되는 문제

Scheduler 중복 실행

💬 Scale-out을 적용하고 보니 경매 시작, 경매 종료, 경매 시작 전 알림을 처리하기 위한 Scheduler가 각 인스턴스마다 실행이 된다는 것을 알았습니다. 이것은 불필요한 실행이 일어나게 했습니다.

 

✔ 문제 해결 고민

💬 실행되는 서버 Instance가 늘어나더라도 Scheduler는 한 번만 실행할 수 있는 방법이 없는지 고민했습니다. 고민해 본 바로 3가지 방법을 고려해 볼 수 있다고 판단했습니다.
     ➡ Scheduler를 위한 별도의 서버를 띄우는 것이었습니다.  
     ➡ 일정 주기마다 Scheduler가 들어있는 jar를 실행하는 것이었습니다.     
     ➡ Scheduler에 Lock을 걸어 먼저 실행된 것이 있다면 다른 Scheduler는 실행되지 않게 하는 것이었습니다.

 

✔ 해결 방법 선정

✅ Scheduler에 Lock을 걸어 사용
      ➡ 별도의 Instance를 추가적으로 만들지 않고, 기존에 돌아가고 있는 서버에서 바로 처리할 수 있습니다. Scheduler에 Lock을 걸기 위한 별도의 공유 자원이 필요했지만, 그것은 DB를 사용하면 문제가 없다고 판단을 했습니다.

💬 Scheduler에 대한 별도의 서버
      ➡ 별도의 서버로 관리하기에는 Scheduler에서 수행하는 작업이 크지 않았으며, 새로운 Instance 생성 및 CI/CD 구축을 해야 하는 추가적인 자원이 발생합니다.

💬 일정 주기마다 Scheduler가 들어있는 jar를 실행
       ➡ Scheduler 실행되는 주기가 길다면 이러한 방법을 고려해 볼 수 있겠지만, 이 프로젝트에서 Scheduler는 1분 단위로 실행이 됩니다. 그래서 jar를 실행이 너무 자주 일어나게 됩니다.

 

✔ 해결 방법 기술

✅ Scheduler에 Lock을 걸기 위한 방법을 찾아봤습니다. ShedLock이라는 DB를 사용해 Lock을 거는 라이브러리를 알게 되었습니다. 다양한 DB를 지원했으며, 이번 프로젝트에서 사용하고 있는 PostgreSQL를 통해서도 적용해 볼 수 있었습니다. 그리고 설정 또한 어렵지 않았기에 프로젝트에 바로 적용해 보기로 했습니다. 

 

 

GitHub - lukas-krecan/ShedLock: Distributed lock for your scheduled tasks

Distributed lock for your scheduled tasks. Contribute to lukas-krecan/ShedLock development by creating an account on GitHub.

github.com

 

✔ 해결 방법 적용

1. DB(PostgreSQL)에 ShedLock을 위한 테이블을 생성합니다. 
CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP NOT NULL,
    locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));​

 

2. Dependency를 추가합니다.
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.7.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.7.0'​

 

3. 최상단 Application 클래스에 어노테이션을 추가합니다. 
     ➡ 1분 단위 작업들만 있기 때문에 기본 최대 Lock 시간을 50초로 설정을 했습니다.
@EnableSchedulerLock(defaultLockAtMostFor = "50s")
public class IdeaRushApplication {​


4. ShedLock Config 클래스를 생성해 줍니다.
     ➡ 주의해야 할 점은 Lock을 걸 때 사용하는 시간을 DB 기준으로 사용한다는 것입니다. DB 기준으로 시간을 설정하지 않으면 각 서버 시간을 사용하게 되는데, 이것으로 인해 서버 간의 시간 싱크를 맞춰야 하는 추가적인 작업이 필요하게 될 수도 있습니다.

@Configuration
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
                JdbcTemplateLockProvider.Configuration.builder()
                        .withJdbcTemplate(new JdbcTemplate(dataSource))
                        .usingDbTime()
                        .build()
        );
    }
}


5. Lock이 필요한 Scheduler에 어노테이션을 추가합니다.
     ➡ 1분 단위 작업이기에 기본 최소, 최대 Lock 시간을 50초로 설정했습니다.

@Scheduled(cron = "0 * * * * *")
@SchedulerLock(name = "beforeTimeBidOfIdea", lockAtMostFor = "50s", lockAtLeastFor = "50s")
public void beforeTimeBidOfIdea() {

 

✔ 해결 방법 적용 결과

💬 8080, 8083 포트로 두 개의 서버를 실행시켜 본 결과 정상적으로 Scheduler가 한 번만 실행이 되는 것을 확인할 수 있었습니다. 그리고 아래 결과처럼 항상 동일한 서버에서만 실행이 되는 것이 아닌, 먼저 Lock을 획득한 서버에서 실행되는 것도 확인할 수 있습니다.

 

첫 번째 실행
두 번째 실행