비동기 ThreadPool의 Thread 실행중 JVM이 종료된다면?(feat. setWaitForTasksToCompleteOnShutdown)

공병주(Chris)·2023년 11월 2일
0
post-thumbnail

지난 번에는, Tomcat 스레드가 특정 요청을 처리중에 kill -15를 통해 서버가 종료될 때의 Graceful Shutdown에 대해 알아보았습니다.

비동기 처리를 하는 스레드 풀은?

그렇다면 Tomcat 스레드가 특정 스레드 풀의 스레드에게 비동기로 처리를 맡기고 사용자에게 빠르게 응답을 내주었다고 가정해보겠습니다. 비동기 처리를 하는 작업은 DB에 매우 중요한 정보를 저장한다고 가정해보겠습니다. 그렇다면 Graceful Shutdown은 이 작업마저도 종료해줄까요?

아래와 같은 세팅을 통해 테스트 해보겠습니다.

server:
  shutdown: graceful
@RestController
public class GracefulShutdownController {

    private final GracefulTestExecutor executor;

    public GracefulShutdownController(GracefulTestExecutor executor) {
        this.executor = executor;
    }

    @PostMapping("/graceful-test")
    public ResponseEntity<Void> checkGracefulShutdown(@RequestBody GracefulShutDownRequest request) {
        executor.doSomething(request);
        return ResponseEntity.noContent().build();
    }
}
@Async("gracefulShutdownAsyncExecutor")
@Component
public class GracefulTestExecutor {

    private final Logger logger = LoggerFactory.getLogger(GracefulTestExecutor.class);

    public void doSomething(GracefulShutDownRequest request) {
        logger.info("요청 수신 seq : ({})", request.getSequence());
        try { // 비즈니스 로직이라고 가정
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.info("요청 처리 완료 seq : ({})", request.getSequence());
    }
}
@Configuration
@EnableAsync
public class GracefulShutdownTestAsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "gracefulShutdownAsyncExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(1);
        return taskExecutor;
    }
}

테스트 결과

결과는 위의 로직에서 요청 수신 로그만 찍히고 JVM이 종료됩니다.

Graceful shutdown은 tomcat의 worker 스레드들이 처리하고 있는 요청이 있는지 확인합니다. 사용자에게 이미 응답은 나간 상태고 비동기 스레드 풀의 스레드가 일을 처리하고 있습니다. 따라서, Graceful Shutdown은 이를 알 수가 없습니다.

비동기로 처리하는 일이 정상적으로 완료되지 못하고 스레드 풀 빈이 shutdown 되면 데이터 손실 등의 결과를 야기할 것 입니다.

해결 방법

그렇다면 비동기 스레드 풀의 스레드가 하던 일이 있으면 기다렸다가 JVM을 종료시키는 방법이 없을까요?

없을리가요. 아래와 같이 ThreadPoolTaskExecutor의 setWaitForTasksToCompleteOnShutdown을 사용하면 됩니다. 해석 그대로, Shutdown 시에 Task가 완료되기를 기다려준다는 것입니다.

@Configuration
public class GracefulShutdownTestAsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "gracefulShutdownAsyncExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(1);
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        taskExecutor.setAwaitTerminationSeconds(30);
        return taskExecutor;
    }
}

위 처럼 setWaitForTasksToCompleteOnShutdown을 true로 지정하고 대기 시간을 지정해주면 해당 시간동안 처리되는 비동기 스레드들의 작업은 대기했다가 Shutdown을 시킵니다.

WARN  org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor - Timed out while waiting for executor 'gracefulShutdownAsyncExecutor' to terminate

만약, setAwaitTerminationSeconds를 스레드가 일을 완료하기 보다 짧은 시간으로 지정한다면 처리를 완료할 수 없으니 적절한 값으로 지정하는 것이 매우 중요해 보입니다.

내부 동작 훑어보기

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
		implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
}

ThreadPoolTaskExecutor은 ExecutorConfigurationSupport을 상속하고 있습니다.

따라서, ThreadPoolTaskExecutor이 소멸될 때, 상위 클래스인 ExecutorConfigurationSupport의 shutdown이라는 메서드가 호출됩니다.

눈여겨 볼 것은 내부적으론 java의 스레드 풀인 ExecutorService 객체를 가지고 있고 실제 Task 처리는 해당 스레드 풀로 처리한다는 것입니다.

package org.springframework.scheduling.concurrent;

public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
		implements BeanNameAware, InitializingBean, DisposableBean {

	private boolean waitForTasksToCompleteOnShutdown = false;

	private long awaitTerminationMillis = 0;

	@Nullable
	private ExecutorService executor;

	// ...

	@Override
	public void destroy() {
		shutdown();
	}

	public void shutdown() {
		if (logger.isDebugEnabled()) {
			logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
		}
		if (this.executor != null) {
			if (this.waitForTasksToCompleteOnShutdown) {
				this.executor.shutdown();
			}
			else {
				for (Runnable remainingTask : this.executor.shutdownNow()) {
					cancelRemainingTask(remainingTask);
				}
			}
			awaitTerminationIfNecessary(this.executor);
		}
	}
}

shutdown 메서드를 살펴보면 waitForTasksToCompleteOnShutdown를 체크하고 true라면 더 이상의 Task 처리 요청은 받지 않기 위해 shutdown을 호출합니다. 이름이 shutdown이라서 다 끝낼 것 같지만, 진행중인 Task들에 대해서는 그대로 실행합니다. waitForTasksToCompleteOnShutdown가 false라면 진행 중인 작업을 즉시 cancel 해버립니다.

그리고 awaitTerminationIfNecessary를 통해서 해당 ExecutorService 의 스레드 중 완료되지 않은 것이 있으면 대기합니다. 아래에서 이어서 확인하겠습니다.

public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
		implements BeanNameAware, InitializingBean, DisposableBean {

	private boolean waitForTasksToCompleteOnShutdown = false;

	private long awaitTerminationMillis = 0;

	@Nullable
	private ExecutorService executor;

	private void awaitTerminationIfNecessary(ExecutorService executor) {
		if (this.awaitTerminationMillis > 0) {
			try {
				if (!executor.awaitTermination(this.awaitTerminationMillis, TimeUnit.MILLISECONDS)) {
					if (logger.isWarnEnabled()) {
						logger.warn("Timed out while waiting for executor" +
								(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
					}
				}
			}
			catch (InterruptedException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Interrupted while waiting for executor" +
							(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
				}
				Thread.currentThread().interrupt();
			}
		}
	}

대기 시간이 0보다 크다면 ExecutorService의 awaitTermination 메서드를 통해 timeout을 주고 스레드풀에게 timeout 동안 처리중인 Task를 실행할 수 있게 해줍니다.

awaitTermination 메서드는 boolean을 반환하고, 처리하던 일을 모두 처리하지 못했을 시엔 false를 응답합니다. 그렇다면 위 분기문에서 Timed out while waiting for executor 로그가 찍힐 것입니다.

참고 사항

위에서 설명드린 것은 Graceful Shutdown 세팅 값과는 다릅니다. Graceful Shutdown를 지정하지 않아도 -15 옵션을 통해 JVM을 종료한다면 이뤄지는 일입니다.

이렇게 비동기 처리 중인 스레드 풀이 하던일을 마무리하고 안전하게 소멸될 수 있도록 하는 방식에 대해 알아보았습니다.

0개의 댓글