ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Scheduler Lock(SchedLock) - Multiple Scheduler Instance 방지
    Spring 2023. 2. 22. 08:46
    반응형

    Scheduler를 사용하려고 할때, 서버가 Scailing 되는 환경이라면
    Multiple Scheduler Instances가 발생하기 때문에
    고민을 해봐야 한다.
    그리고 이를 해결하기 위한 방법은 2가지 정도로 살펴볼 수 있다.

     

    첫 번째로는, Quartz와 같은 분산 스케줄링 프레임워크를 사용하여 여러 인스턴스에서 스케줄러 실행을 조정할 수 있다.

    이렇게 하면 새 인스턴스가 환경에서 추가되거나 제거되더라도 한 번에 하나의 스케줄러 인스턴스만 실행되도록 할 수 있다.

     

    두 번째로는, 한 번에 하나의 스케줄러 인스턴스만 실행되도록 할 수 있다.
    스케줄러의 각 인스턴스가 예약된 작업을 실행하기 전에 공유 리소스에 대한 잠금을 획득하도록 함으로써 이를 수행할 수 있다.

    스케줄러의 다른 인스턴스가 잠금을 이미 보유하고 있는 경우 현재 인스턴스는 예약된 작업을 건너뛰고 잠금이 해제될 때까지 기다린다.

     

    이 중에서 두 번째 방법인 Spring Boot에서 Scheduler에 Lock을 거는 ShedLock을 적용하는 방법에 대해 포스팅 해본다.

     

     

    ShedLock은 예약된 작업에 대한 분산 잠금을 제공하는 Spring Boot용 library다.
    ShedLock은 잠금 데이터베이스 테이블과의 조합을 사용하여 분산 환경에서 실행되는 애플리케이션의 여러 인스턴스에서
    예약된 작업이 한 번만 실행되도록 한다. 작동 방식은 다음과 같다.

     

      1. 예약된 작업이 ShedLock에 등록되면 ShedLock은 먼저 Lock 데이터베이스 테이블을 사용할 수 있는지 확인한다.
        테이블을 사용할 수 없는 경우 ShedLock은 작업 등록을 건너뛰고 경고를 기록한다.
      2. 그런 다음 ShedLock은 예약된 작업의 cron 표현식을 기반으로 시간을 계산한다. 이 기간은 작업을 실행할 수 있는
        시기를 결정하는 데 사용된다. 현재 시간이 벗어나면 ShedLock은 작업 실행을 건너뛰고 경고를 기록한다.
      3. ShedLock은 Lock 데이터베이스 테이블에 레코드를 삽입하여 예약된 작업에 대한 잠금을 획득하려고 시도한다.
        삽입이 성공하면 ShedLock이 성공적으로 잠금을 획득한 것이며 작업을 실행할 수 있다. 삽입이 실패하면
        ShedLock은 경고를 기록하고 작업 실행을 건너뛴다.
      4. 작업이 완료되면 ShedLock은 Lock 데이터베이스 테이블에서 잠금 레코드를 삭제하여 잠금을 해제한다.
      5. 잠금이 해제되기 전에 잠금을 보유한 응용 프로그램 인스턴스가 다운되면 ShedLock은 지정된 시간 초과 기간 후에 잠금을
        해제하여 다른 인스턴스가 차단되지 않도록 한다.

    적용해보자.

     

    📌 개발 환경

    - Spring Boot 2.7

    - Mysql


    📌 시작하기

     

    1. Dependency 추가

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-spring</artifactId>
        <version>4.14.0</version>
    </dependency>
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-jdbc-template</artifactId>
        <version>4.14.0</version>
    </dependency>

     

     

     

     

    2. SchedLock용 테이블 생성

    CREATE TABLE shedlock (
      name VARCHAR(64),
      lock_until TIMESTAMP(3) NULL,
      locked_at TIMESTAMP(3) NULL,
      locked_by VARCHAR(255),
      PRIMARY KEY (name)
    )

     

     

    3. Docker-compose.yml 작성

    version: '3.1'
    services:
      mysql:
        image: mysql:8.0.22
        platform: linux/amd64
        command:
          - --character-set-server=utf8mb4
          - --collation-server=utf8mb4_general_ci
        restart: always
        ports:
          - 3308:3306
        environment:
          MYSQL_ROOT_PASSWORD: 1234
          MYSQL_USER: admin
          MYSQL_PASSWORD: 1234
          MYSQL_DATABASE: test
          TZ: Asia/Seoul

     

     

    4. application.yml 설정

    server:
      port: 8080
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3308/test
        username: admin
        password: 1234
      sql:
        init:
          platform: mysql-local
          mode: always

     

     

    5. Configuration 설정

    @Configuration
    public class SchedulerConfig {
    
        @Bean
        public LockProvider lockProvider(DataSource dataSource) {
            return new JdbcTemplateLockProvider(dataSource);
        }
    }

     

     

    @SpringBootApplication
    @EnableScheduling
    @EnableSchedulerLock(defaultLockAtMostFor = "PT1M")
    public class WebApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(WebApplication.class, args);
        }
    
    }

    - defaultLockAtMostFor는 1분으로 설정했다.

     

     

     

    6. Task 작성

    @Slf4j
    @Component
    public class MissionScheduler {
        @Scheduled(cron = "0/10 * * * * *") // 0초부터 10초마다 실행
        @SchedulerLock(
                name = "scheduler_lock", // 스케줄러 락 이름 지정. (이름이 동일한 스케줄러일 경우, 락의 대상이 된다.)
                lockAtLeastFor = "PT9S", // 락을 유지하는 시간을 설정한다. (9초로 설정했는데, 스케줄러 주기보다 약간 짧게 지정하는 것이 좋다.)
                lockAtMostFor = "PT9S" // 보통 스케줄러가 잘 동작하여 잘 종료된 경우 잠금을 바로 해제하게 되는데, 스케줄러 오류가 발생하면 잠금이 해제되지 않는다. 이런 경우 잠금을 유지하는 시간을 설정한다.
        )
        public void scheduler1() {
            log.info("1번 스케줄러");
        }
    }
    • name: 스케줄 작업의 고유 이름. 해당 문자열은 shedlock 테이블의 name 칼럼으로 기본키 역할을 하게 되므로 스케줄 작업의 고유한 이름을 입력해야한다.
    • lockAtLeastFor: 작업이 lock 되어야 할 최소한 시간을 입력한다. 짧은 작업일 경우 노드간의 클럭 차이로 중복 실행되는 것을 막기 위함이다.
    • lockAtMostFor: 작업을 진행 중인 노드가 소멸될 경우에도 lock 이 유지될 시간을 입력한다. 해당 시간은 실제 작업에 소요되는 시간보다 훨씬 길게 해야한다. 그렇지 않을 경우 예상치 못한 스케줄 작업이 일어날 수 있다. 해당 시간을 따로 입력하지 않으면 @EnableSchedulerLock의 디폴트 값으로 세팅 된다.

     

    - 만약 SchedulerLock Annotation에 deprecated warning이 뜬다면 import 경로를 아래로 수정하길 바란다.

    import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
    import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;


    2023년 부터는 import 경로가 변경되었다. 

     

    https://javadoc.io/static/net.javacrumbs.shedlock/shedlock-core/4.0.0/deprecated-list.html

     

    Deprecated List (shedlock-core 4.0.0 API)

     

    javadoc.io

    - 참고로 lockAtMostFor이 lockAtLeastFor보다 작으면 에러가 발생하니 주의하길 바란다.

     

     

     

     

    7. 테스트

     

    아래 명령어로 application.yml server port 8080과 8090으로 각각 maven package를 해준다.

    mvn package -DskipTests

     

    2개의 jar를 생성 했다면 각각 실행해주자.

    java -jar [jar name]

     

     

    로그를 보면 거의 동시에 스케쥴러가 실행이 됐지만, Lock 테이블은 업데이트가 이뤄졌으나 실제 스케쥴러 실행은 한쪽에서만 이뤄진것을

    확인할 수 있다.

     

     

     

     

     

     

    Reference:

    https://velog.io/@recordsbeat/%EB%A9%80%ED%8B%B0-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%8A%A4%EC%BC%80%EC%A4%84-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-ShedLock

    반응형

    댓글

Designed by Tistory.