안녕하세요. J4J입니다.
이번 포스팅은 shedlock을 이용하여 분산 환경 배치 처리하는 방법에 대해 적어보는 시간을 가져보려고 합니다.
관련 글
[SpringBoot] SpringBatch 사용하기 (1) - Scheduler를 이용하여 Tasklet, Chunk 배치 만들기
[SpringBoot] SpringBatch 사용하기 (2) - Job Parameter 활용 및 동일 Job 반복 실행
[SpringBoot] SpringBatch 사용하기 (3) - Quartz로 배치 만들기
[SpringBoot] SpringBatch 사용하기 (4) - Quartz로 클러스터링 처리하기
Shedlock 이란?
shedlock은 분산 환경에서 스케줄링 처리가 된 여러 task 들을 동시에 최대 1번만 실행할 수 있도록 도와줍니다.
shedlock과 비교해 볼 수 있는 관련 기술들은 위의 링크에서 확인할 수 있는 것처럼 spring batch와 quartz가 존재합니다.
spring batch와 quartz에 대해 간단하게 설명하면 모두 spring에서 스케줄링 관리를 위해 사용되지만 quartz에서만 클러스터링 기능을 제공해주고 있습니다.
즉, scale-out을 통한 분산 환경이 많이 구성되는 최근 서비스들에서는 spring batch 보다는 quartz를 더 사용할 확률이 높을 것입니다.
그리고 quartz의 클러스터링 처리를 대신해서 사용할 수 있는 것이 바로 shedlock입니다.
하지만 quartz의 클러스터링 기능과 shedlock은 분산 환경에서 동시에 동일한 task를 최대 1번만 실행할 수 있도록 보장해 주지만 동작 방식이 다릅니다.
먼저 quartz 동작에 대해 간단하게 얘기해 보면 다음과 같습니다.
- 분산된 각 서버에서는 DB에 자신의 상태를 주기적으로 전달
- task에 대한 trigger가 발생할 경우 trigger를 처리할 수 있는 서버가 task를 실행
- task를 실행하고 있지 않은 서버들은 처리되고 있는 task를 주기적으로 확인
- task를 작업하고 있던 서버에서 올바르게 작업을 처리하지 못한 경우 task를 다른 서버에 재 배치한 뒤 task 실행
반대로 shedlock 같은 경우는 quartz와 같이 클러스터링 처리를 수행하기보다는 task에 대한 lock 기능을 제공합니다.
그래서 사실 shedlock은 분산 스케줄러라고 말하지는 않는다고 합니다.
shedlock 동작에 대해 간단하게 얘기해 보면 다음과 같습니다.
- 분산된 각 서버에서 task 처리를 수행하는 경우 task에 대한 lock을 획득
- 동일한 task를 처리해야 되는 다른 서버에서는 lock을 획득하지 못한 경우 기다리지 않고 task 처리를 스킵
이와 같이 분산 처리를 수행하는 방식이 다르기에 어떤 목적을 가지고 스케줄링 기능을 사용할 것인지에 따라 선택지가 달라질 수 있습니다.
어떤 것을 선택해야 되는지 더 도움을 얻기 위해 특징을 살펴보도록 하겠습니다.
Shedlock 특징
shedlock의 특징은 다음과 같습니다.
[ 장점 ]
- 분산 환경에서 동일 task에 대해 중복 실행이 되지 않는 것을 보장
- 간단한 설정 및 관리를 통해 기능 제공
- RDB, NoSQL, Redis 등 다양한 환경에 맞는 저장소 지원
- 유연한 잠금 설정을 통한 lock 관리 기능 제공
[ 단점 ]
- lock 기능 제공 외에 trigger, listener 등의 고급 기능을 제공하지 않음
- lock을 습득한 서버에서 작업 중 문제가 발생할 경우 다음 lock을 관리하는 상황에 문제가 발생할 수 있음
- task 실행 보장을 위해서 부가적인 로직 추가 및 설정이 필요함
- 데드락 및 동시성 문제에 대한 이슈가 발생할 수 있음
shedlock의 특징들을 간단하게 정리해 보면 기본적인 사용법은 간단하지만 복잡한 상황에 부가적은 기능을 활용해야 되는 상황에는 적합하지 않은 것을 볼 수 있습니다.
지금까지 개인적인 경험으로는 스케줄링 처리를 위한 개발을 진행할 때 단순 비즈니스 로직 처리 위주로 많이 활용되었기에 shedlock을 사용하는 것이 문제가 되지 않습니다.
그리고 여러 많은 서비스들이 스케줄링 처리를 수행할 때 복잡한 비즈니스 로직을 가져가지 않는 것으로 알고 있어 해당 서비스들에서 shedlock을 통한 스케줄링 관리는 일반적으로 적합한 선택지가 될 수 있습니다.
Shedlock 사용 환경 설정
지금까지 shedlock이 무엇인지에 대해 알아봤고 이번에는 shedlock을 실제로 어떻게 사용할 수 있는지에 대해 적어보겠습니다.
참고 사항으로 환경 설정 테스트를 위해 사용되는 DB는 mysql입니다.
[ 1. shedlock lock 테이블 생성 query ]
CREATE TABLE shedlock(
name VARCHAR(64) NOT NULL, -- lock 이름
lock_until TIMESTAMP(3) NOT NULL, -- lock을 가지고 있는 최대 시간
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), -- lock을 소유하기 시작한 시간
locked_by VARCHAR(255) NOT NULL, -- lock 소유자
PRIMARY KEY (name)
);
lock 테이블 생성 query는 shedlock github을 확인하면 DB 저장소 별로 어떤 query를 사용해야 되는지 가이드를 해주고 있습니다.
만약 mysql을 사용하지 않으시는 분들은 github을 확인하여 각 사용 환경에 맞게 활용하시면 됩니다.
[ 2. dependency 추가 ]
dependencies {
// shedlock
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.13.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.13.0'
}
[ 3. spring scheduling config 클래스 추가 ]
package com.jforj.shedlolck.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
[ 4. shedlock config 클래스 추가 ]
package com.jforj.shedlolck.config;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10s") // default lock 점유 시간 최대 10초
public class ShedlockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration
.builder()
.withJdbcTemplate(new JdbcTemplate((dataSource))) // jdbc datasource 연결
.usingDbTime() // DB에 설정되어 있는 서버 시간에 맞춰서 작업
.build()
);
}
}
[ 5. scheduling job 클래스 추가 ]
package com.jforj.shedlolck.job;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@Slf4j
public class LoggingJob {
@Scheduled(cron = "0/10 * * * * ?") // 10초마다 task 실행, cron 표현식 활용 (초 분 시 일 월 요일)
@SchedulerLock(name = "time-logging-lock") // lock 테이블에 저장되는 lock name 설정
public void timeLogging() {
log.info("time logging start");
log.info("current time is " + new Date().getTime());
log.info("time logging end");
}
}
위와 같이 설정을 했다면 다음과 같은 결과를 확인할 수 있습니다.
- 10초마다 현재 시간 로그 출력
- 로그 출력 로직을 실행하기 전 lock을 점유하여 lock 관리 테이블에 정보 적재
Shedlock Lock 점유 시간 설정
특징 파트에서 설명드린 것처럼 shedlock은 lock을 점유하여 task를 실행하다가 이슈가 발생될 경우 다양한 문제가 발생될 수 있습니다.
그중 이를 방지 및 대응하기 위한 유연한 lock 관리 기능을 제공해주고 있습니다.
lock 관리를 위해 속성 값을 설정할 수 있으며 다음과 같은 목적으로 사용할 수 있습니다.
- lockAtLeastFor → lock을 점유하고 있을 최소 시간 설정으로 속성에 설정된 시간은 최소한으로 lock 점유를 보장
- lockAtMostFor → lock을 점유하고 있을 최대 시간 설정으로 속성에 설정된 시간이 지나면 다른 곳에서 lock 점유 가능
leastFor은 위와 같은 특징을 가지고 있기 때문에 lock을 가지고 있는 서버에서 task 처리가 빠르게 진행되었다고 하더라도 설정된 시간 동안 lock을 계속 점유하고 있습니다.
mostFor도 위와 같은 특징을 가지고 있기 때문에 lock의 최대 소유 시간을 설정하여 데드락 등의 이슈를 방지하는데 활용될 수 있습니다.
참고 사항으로 특정 서버가 task를 처리하는 상황에 mostFor의 설정 시간이 넘어가 lock을 잃어버려도 동작되고 있는 task는 즉시 중단되지는 않고 비즈니스 로직을 계속 처리하기는 합니다.
다만, 해당 task를 계속 처리하고 있기에 lock을 소유해야 되는 상황에 lock을 소유하지 못하는 상황이 발생할 수 있습니다.
그래서 실행되는 task가 무엇인지에 따라 위와 같은 속성을 적절히 적용해 주는 것은 shedlock을 이용하여 스케줄링 기능을 제공하는데 필요한 부분입니다.
또한 각 속성 별 특징에 맞는 동작을 위해 mostFor은 leastFor보다 작게 설정할 수 없습니다.
leastFor과 mostFor 설정은 다음과 같이 2가지 방식으로 활용할 수 있습니다.
첫 번째는 @EnableSchedulerLock에 설정하여 모든 shedlock 사용에 기본 설정 값을 부여하거나 job 내부 클래스 전체에 기본 설정 값을 부여하는 방식입니다.
// shedlock config
@Configuration
@EnableSchedulerLock(defaultLockAtLeastFor = "3s", defaultLockAtMostFor = "10s") // default lock 점유 시간 최소 3초, 최대 10초
public class ShedlockConfig {
...
}
// job class
@Component
@EnableSchedulerLock(defaultLockAtLeastFor = "3s", defaultLockAtMostFor = "10s") // 해당 클래스 내부에 정의된 default lock 점유 시간 최소 3초, 최대 10초
@Slf4j
public class LoggingJob {
...
}
두 번째는 @SchedulerLock에 설정하여 task 별로 설정 값을 부여하는 방식입니다.
public class LoggingJob {
@Scheduled(cron = "0/10 * * * * ?") // 10초마다 task 실행, cron 표현식 활용 (초 분 시 일 월 요일)
@SchedulerLock(name = "time-logging-lock", lockAtLeastFor = "1s", lockAtMostFor = "5s") // lock 테이블에 저장되는 lock name 설정, lock 점유 시간 최소 1초, 최대 5초
public void timeLogging() throws InterruptedException {
...
}
}
Shedlock Transactional 처리
shedlock에서 transactional 처리를 통한 일관된 데이터 처리를 유지할 수 있습니다.
다만, 예를 들어 스케줄링 처리를 위해 다음과 같이 transactional 설정이 담겨 있는 service 내부 기능을 사용하는 경우 아무 설정도 하지 않으면 transactional 처리가 보장되지 않습니다.
// service
@Service
@Transactional
public class service {
...
}
// job
@RequiredArgsConstructor
public class job {
private final Service service;
@Scheduled()
@SchedulerLock()
public void schedule() {
service.method();
}
}
transactional 처리가 필요하다면 service에 설정하는 것과 동일하게 다음과 같이 job 클래스 자체에 추가해 주던가 또는 transactional 처리가 필요한 task method에 추가를 꼭 해주셔야 합니다.
@Transactional
public class job {
...
}
@RequiredArgsConstructor
public class job {
private final Service service;
@Scheduled()
@SchedulerLock()
@Transactional
public void schedule() {
...
}
}
이상으로 shedlock을 이용하여 분산 환경 배치 처리하는 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > SpringBoot' 카테고리의 다른 글
[SpringBoot] Redis NOAUTH HELLO must be called with the client already authenticated 이슈 (0) | 2024.06.30 |
---|---|
[SpringBoot] Querydsl에서 페이징 처리하기 (0) | 2024.06.05 |
[SpringBoot] Redis 사용하기 (4) - Redis Cache Manager 사용하기 (0) | 2024.05.15 |
[SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기 (0) | 2024.05.13 |
[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기 (0) | 2024.05.12 |
댓글