본문 바로가기
공부/JAVA

여러대의 서버에서 스케줄 처리하기 (ShedLock)

by yeaseul912 2023. 5. 15.
728x90

요즘에는 트래픽을 분산시키거나 서버가 다운 되었을 때에도 안정적으로 서비스가 돌아가게 하기 위해 서버를 여러 대 두고 어플리케이션을 운영하는 것이 추세이다.

 

하지만 스케줄링을 하는데 있어 여러대의 서버에서 중복으로 여러개의 스케줄링이 일어나는 것은 곤란하다.!

 

이러한 중복 실행을 막기 위해 ShedLock 이라는 라이브러리를 사용해보았다.

 

자세한 것은 깃헙을 참고하면 된다.

ShedLock GitHub Page

- 주요 내용 -

if one task is already being executed on one node, execution on other nodes does not wait, it is simply skipped.

 

버전은 JDK 버전을 고려하여 사용하면 된다.

If you are using JDK >17 and up-to-date libraries like Spring 6, use version 5.1.0 (Release Notes).

If you are on older JDK or library, use version 4.44.0 (documentation).

 

1. dependency 추가

shedlock 과 내가 사용하는 DB 에 맞는 provider 를 추가한다.

나는 java 8 과 mysql 을 사용하였기 때문에 아래와 같은 버전으로 했다.

그 외의 다양한 DB 도 지원하고 있으니 github 참고하면 된다.

implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-spring', version: '4.44.0'
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-provider-jdbc-template', version: '4.44.0'

2. 어노테이션 추가

Component 혹은 Configuration 파일에 @EnableSchedulerLock 을 추가한다.

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
    ...
}

defaultLockAtMostFor : 작업을 진행 중인 노드가 예기치 않게 종료된 경우 Lock을 유지하는 기본 시간, lockAtMostFor 를 설정 하지 않으면 여기서 설정 한 값으로 설정이 된다.

 

내가 사용하고 있는 DB를 활용하여 Lock정보를 Insert/Update 할 수 있도록 @LockProvider 를 추가한다.(심플한버전)

import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.core.LockProvider;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
...
@Bean
public LockProvider lockProvider(DataSource dataSource) {
  return new JdbcTemplateLockProvider(dataSource);
}

 

 

그리고 Lock을 걸 작업에 @SchedulerLock 을 추가한다.

name: DB 상에 primary key 로 들어가는 고유의 작업 이름

lockAtLeastFor : 작업이 Lock 되어야 할 최소 시간, 짧은 작업 일 경우 노드간의 클럭 차이로 중복 실행되는 것을 막기 위함이다.

lockAtMostFor : 작업을 진행 중인 노드가 소멸될 경우에도 lock이 유지되는 시간 해당 시간은 실제 작업에 소요되는 시간보다 훨씬 길게 해야다. 그렇지 않을 경우 예상치 못한 스케줄 작업이 일어날 수 있다. 해당 시간을 따로 입력하지 않으면 위 defaultLockAtMostFor 값으로 설정 된다.

import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
...
@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(name = "scheduledTaskName", lockAtMostFor = "14m", lockAtLeastFor = "14m")
public void scheduledTask() {
    // do something
}

*위 예시 처럼 lockAtMostFor 와 lockAtLeatFor 는 스케줄링 실행 주기에 -1 을 한 값으로 설정하는 것이 안전하다고 한.

3. DB 추가

ShedLock 은 자동으로 스케줄러의 Lock 시간을 Insert, Update 하여 중복을 방지하는 방식이다.

위에서 DB가 연결되어 있다면 알아서 Insert, Update를 해준다.

이때 사용할 Table은 최초로 사용자가 생성해 주어야 한다. 

이것도 db별로 github에 잘 나와 있으니 참고하도록 하자. 

Mysql 과 MariaDB는 아래와 같다.

# MySQL, MariaDB
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));

이때 주의해야 할 점은 row 를 절대로 사용자가 수동으로 삭제하면 안된다. 인메모리 캐시에 행이 존재해서 어플리케이션을 재시작 할 때까지 행이 자동으로 재생성 되지 않아 update 정보가 누락되어 lock 이 작동하지 않을 수 있다고 한다.

 

4. Test

나는 1분에 한번씩 api 를 실행하는 스케줄러를 만들었다. 

그래서 59초씩 lock 시간을 설정해 주었다. (PT ~ 도 시간을 나타내는 포맷이라고는 하는데 그냥 m, s 써도 된다.)

@Async
@Scheduled(cron = "0 0/1 * * * *")
@SchedulerLock(name = "api_scheduler", lockAtMostFor = "PT59S", lockAtLeastFor = "PT59S")
public void apiScheduler() throws UnknownHostException {
...
}

DB 가 이렇게 생기고, 값이 하나 들어갔다.

test01 서버에서 먼저 스케줄을 실행하고 locked_at 에 실행시간과 lock_until 에 언제까지 lock 을걸 건지 Update 가 되었다.

만약 위 시간 사이에( ex) 2023-05-15 14:55:00.042 ) test02 서버에서 스케줄을 실행하려고 했다가 DB 에서 이미 api_scheduler가 실행된 것을 보고 실행하지 않게 된다. (lock)

 

만약 lockAtLeastFor 를 1분으로 했다면 14:56:00.021 까지 lock 이 걸리게 되는데, 만약 test01 과 test02 가 그 시간보다 이전에 실행되어 버린다면 자칫 잘못하다가 스케줄러가 작동하지 않게 될수 있으므로 -1 을 해주는 것이 안전하다고 하는 것같다.

 

Document

멀티 서버에서 스케줄 처리하기 - ShedLock

반응형

댓글