상세 컨텐츠

본문 제목

Spring Batch 파티셔닝 적용후 JVM이 종료되지 않는 문제 해결

프로젝트/click-me

by seungpang 2024. 5. 3. 19:28

본문

반응형

현재 쿠버네티스의 CronJob을 활용하여 스프링 배치 job들을 실행하고 있었다.

spring:
  main:
    web-application-type: none
  batch:
    job:
      name: ${job.name:NONE}

위에 설정을 보면

  • spring.main.web-application-type: none: 설정을 사용하면 Spring Boot 애플리케이션이 웹 서버를 시작하지 않고, 커맨드라인 애플리케이션으로 동작한다.

이렇게 설정할 경우 배치 작업이 완료된 이후에 애플리케이션이 자동으로 종료된다.

하지만 스프링 배치에서 파티셔닝을 적용한 이후에 CronJob이 정상적으로 동작하지 않았다.

원래는 CronJob이 돌고 해당 스프링 배치 job이 완료되었으면 Succeeded 상태가 되어야 하는데 계속 Running상태로 남아 있는 문제가 발생했다.

이렇게 Running 상태로 남아있을 경우 자원이 한정적이라 다른 CronJob이 돌지 않는다.

그래서 무엇이 문제인가 생각해봤다.

TaskExecutor

spring batch에서 파티셔닝을 사용할때 TaskExecutor을 사용한다.

구현체로 ThreadPoolTaskExecutor를 사용한다.

Spring의 ThreadPoolTaskExecutor가 JVM 종료 시점에 자동으로 종료되지 않는 주요 이유는, 이것이 사용하는 내부 스레드 풀(java.util.concurrent.ThreadPoolExecutor)이 기본적으로 데몬 스레드가 아닌 사용자 스레드를 사용하기 때문이다.

JVM은 실행 중인 사용자 스레드가 모두 종료될 때까지 자동으로 종료되지 않는다.

데몬 스레드와 달리 사용자 스레드는 JVM이 자동으로 종료되는 것을 방지한다.

따라서, ThreadPoolTaskExecutor 내의 스레드들이 아직 실행 중인 작업이 있을 경우, 이 스레드들이 완전히 종료될 때까지 JVM은 종료되지 않는다.

그렇다면 어떤 방법으로 처리하는게 좋을까?


크게 생각한 것은 3가지 방법이다.

  • ThreadPoolTaskExecutor를 데몬 스레드로 생성
  • JobExecutionListener를 구현해서 job이 끝날 때 애플리케이션을 직접 종료시키는 방법
  • allowCoreThreadTimeOut 을 true로 설정

1번째 방법은 스레드를 데몬 스레드로 만들면 jvm이 정상적으로 종료해줘서 해결이 가능하다. 하지만 진행중인 작업이 비정상적으로 종료될 가능성이 있다.

2번째 방법은 JobExecutionListener를 직접 구현해주는 방법이다. afterJob에서 job이 정상적으로 끝났을 때 정상적으로 종료해주면 된다.

3번째 방법은 allowCoreThreadTimeOut의 설정을 true로 해주는 것이다. ThreadPoolTaskExecutor는 keepAliveSeconds 로 설정된 시간만큼 태스크가 할당되지 않은 스레드를 유지하게 되는데

이 값을 true 로 설정하면 core thread 가 일정시간 task를 받지 않을 경우 pool 에서 정리되게 되고, 모든 자식 스레드가 정리되면 jvm도 종료된다.

그래서 3번째 방법으로 구현했다.

@Configuration
public class AppConfig {

    private static final int POOL_SIZE = 10;

    private ThreadPoolTaskExecutor executor;

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(POOL_SIZE);
        executor.setMaxPoolSize(POOL_SIZE);
        executor.setThreadNamePrefix("executor-thread");
        executor.setWaitForTasksToCompleteOnShutdown(Boolean.TRUE);
        executor.setKeepAliveSeconds(30); 
        executor.setAllowCoreThreadTimeOut(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }

    @PreDestroy
    public void destroy() {
        executor.shutdown();
    }
}
  • setCorePoolSize: 스레드 풀의 코어 스레드 수를 설정한다. 코어 스레드는 스레드 풀에서 항상 유지되는 스레드의 최소 수
  • setMaxPoolSize: 스레드 풀에서 허용되는 최대 스레드 수를 설정한다.
  • setThreadNamePrefix: 스레드 풀에서 생성되는 스레드의 이름 접두사를 설정한다.
  • setWaitForTasksToCompleteOnShutdown: 스레드 풀 종료 시 실행 중인 작업을 완료할지 여부를 설정합니다. true로 설정하면, 스레드 풀이 종료되기 전에 실행 중인 작업이 완료될 때까지 대기한다.
  • setKeepAliveSeconds: 여유 스레드(코어 스레드 수를 초과하는 스레드)가 유휴 상태로 있을 수 있는 최대 시간을 설정한다.
  • setAllowCoreThreadTimeOut: 코어 스레드가 유휴 상태일 때 시간 초과로 종료될 수 있도록 허용한다.
  • setAwaitTerminationSeconds: shutdown이 호출된 후 대기 및 종료를 위한 최대 시간을 설정한다.

그리고 destroy 메소드는 @PreDestroy로 스프링 컨테이너가 종료될 때 호출된다.

이 메소드에서는 executor.shutdown()을 호출하여 스레드 풀을 안전하게 종료한다.

이는 애플리케이션이 종료될 때 남은 작업이 정상적으로 종료되도록 보장하도록 했다.

관련글 더보기