ShedLock으로 다중 서버에서 스프링 스케쥴링 안전하게 사용하기

Kyle·2023년 10월 4일
0

개요


지난 포스트에서 강결합 서비스를 분리하는 내용을 다뤘습니다.
이번 포스트에서는 ShedLock을 활용하여 다중 서버에서 Spring 스케쥴링 기능을 안전하게 사용하는 방법을 적어보겠습니다.

문제상황

저의 경우엔 해당 서비스의 분리를 Spring 스케쥴링과 활용해서, 매일 정해진 시간에 데이터의 이상이 있는 펀드를 찾고 메신저로 정보를 받아보는데 사용하고자 했습니다.

그런데 @Scheduled를 사용해서 스케쥴링을 단순히 하려고 하니 한가지 문제점이 있었습니다.

현재 시스템의 구성에서 WAS 서버는 두대를 사용하고 있었습니다. 그런데 Spring 애플리케이션을 각각의 서버에서 띄우게 되면 스케쥴링이 개별적으로 동작하는 문제가 예상되었습니다.

@Component  
class ScheduledFundCheck(private val fundUpdateServiceWithEvent: FundUpdateServiceWithEvent) {  
  
    private val log = LoggerFactory.getLogger(this::class.java)  
  
    @Value("\${server.port}")  
    lateinit var serverPort: String  
  
    @Scheduled(fixedRate = 5000)  
    fun scheduledTask() {  
        fundUpdateServiceWithEvent.asyncUpdateFund()  
        log.info("실행된 포트는 :$serverPort 입니다.")  
    }  
  
}

다음과 같이 스케쥴링된 상황을 예를들어서, 로컬에서 8080, 8081 포트로 두개의 톰캣 서버를 띄워보겠습니다.

당연하게도 각각의 서버에서 스케쥴링이 동작하고, 개별적으로 메서드가 실행되게 됩니다.
이렇게 되면 저의 상황에선 두 개의 서버에서 각각 펀드를 확인하고 메신저 API를 호출하게 됩니다.

ShedLock 사용하기


저희 시스템 현재 구조는 WAS가 두 대에서 더이상 스케일 아웃될 상황이 없고, 해당 스케쥴 작업은 매일 한번씩 짧은 시간안에 끝나기 때문에 간단히 ShedLock을 사용하기로 결정했습니다.

ShedLock 해당 포스트에서 ShedLock의 사용법을 확인 할 수 있습니다.

현재 운영중인 시스템에선 RDBMS를 사용하기 때문에 JdbcTemplate 설정을 적용하겠습니다.

의존성 추가

먼저 의존성을 추가합니다.

implementation("net.javacrumbs.shedlock:shedlock-spring:5.8.0")  
implementation("net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.8.0")

테이블 생성

ShedLock에서 사용할 테이블을 만들어줍니다.

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

설정 클래스 구성

그리고 설정 클래스를 만들고 LockProvider를 Bean으로 설정합니다.

@Configuration  
@EnableScheduling  
@EnableSchedulerLock(defaultLockAtMostFor = "10m")  
class ScheduleConfig {  
  
    @Bean  
    fun lockProvider(dataSource: DataSource): LockProvider {  
        return JdbcTemplateLockProvider(  
            JdbcTemplateLockProvider.Configuration.builder()  
                .withJdbcTemplate(JdbcTemplate(dataSource))  
                .usingDbTime()  // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2  
                .build()  
        )  
    }  
}

애너테이션 적용

@Scheduled가 적용된 메서드에 @SchedulerLock 애너테이션을 추가합니다.

package com.tistory.tech.gyutech.service  
  
import net.javacrumbs.shedlock.core.LockAssert  
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock  
import org.slf4j.LoggerFactory  
import org.springframework.beans.factory.annotation.Value  
import org.springframework.scheduling.annotation.Scheduled  
import org.springframework.stereotype.Component  
  
@Component  
class ScheduledFundCheck(private val fundUpdateServiceWithEvent: FundUpdateServiceWithEvent) {  
  
    private val log = LoggerFactory.getLogger(this::class.java)  
  
    @Value("\${server.port}")  
    lateinit var serverPort: String  
  
    @Scheduled(fixedRate = 5000)  
    @SchedulerLock(name = "scheduledTaskName", lockAtLeastFor = "4s")  
    fun scheduledTask() {  
        // To assert that the lock is held (prevents misconfiguration errors)  
        LockAssert.assertLocked()  
        fundUpdateServiceWithEvent.asyncUpdateFund()  
        log.info("실행된 포트는 :$serverPort 입니다.")  
    }  
  
}

확인


다시 두 개의 포트에 서버를 띄워서 확인해보겠습니다.

8080 포트를 사용하는 서버에서 Lock을 소유하고 해제하고를 반복하는 모습을 볼 수 있습니다.
8081 서버에서는 "It's locked."라는 메시지를 출력하며 스케쥴링된 작업이 수행되지 않는 모습을 볼 수 있습니다.

여기서 8080 포트를 재시작해보면 ..

Lock을 얻지 못하던 8081 포트에서 Lock을 획득하여 로직을 수행하는 모습을 확인 할 수 있습니다.

현재 운영 환경에서도 이와 비슷한 설정으로, 두 대의 서버를 가용하면서 한 쪽 서버에서만 Spring의 스케쥴링 기능이 동작하게 만들 수 있었습니다.

0개의 댓글