Spring Boot 비정상 종료 시 Kubernetes에서 CrashLoopBackOff 방지하는 방법

Spring Boot 애플리케이션을 Kubernetes 환경에서 실행할 때, 비정상 종료(System.exit(1)) 처리를 적절히 하지 않으면 두 가지 주요 문제가 발생할 수 있다.

  1. ApplicationContext가 닫힌 후 SpringApplication.exit()을 호출하면 발생하는 IllegalStateException 오류
  2. Pod가 예상치 못하게 종료되면서 Kubernetes에서 CrashLoopBackOff 상태로 빠지는 문제

이 문제는 Spring의 종료 프로세스를 올바르게 관리하지 않기 때문에 발생한다. 이를 해결하려면 비정상 종료 시에도 Spring의 컨텍스트 종료 이벤트(ContextClosedEventListener)를 정상적으로 실행하도록 설정해야 한다.


🚨 문제 원인 분석

1️⃣ IllegalStateException: ApplicationContext has been closed already 오류 발생

비정상 종료 시 SpringApplication.exit(configurableApplicationContext);을 실행하면, 이미 닫힌 ApplicationContext를 참조할 가능성이 있다.
이 경우, 아래와 같은 예외가 발생할 수 있다.

java.lang.IllegalStateException: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@48ea2003 has been closed already
at org.springframework.context.support.AbstractApplicationContext.assertBeanFactoryActive(AbstractApplicationContext.java:1150)
at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1311)
at org.springframework.boot.SpringApplication.exit(SpringApplication.java:1343)

이는 System.exit(1);이 너무 빨리 실행되거나, Spring이 종료되면서 ContextClosedEventListener가 완전히 실행되지 않았을 때 발생할 수 있다.


2️⃣ Kubernetes에서 CrashLoopBackOff 발생

System.exit(1);을 호출하면 Kubernetes는 해당 Pod가 비정상 종료되었다고 판단하고, 자동으로 새로운 Pod를 생성한다.
하지만 애플리케이션이 적절한 종료 절차 없이 즉시 종료되면, Kubernetes가 이를 반복적으로 감지하여 CrashLoopBackOff 상태가 발생할 수 있다.

이 문제를 해결하려면:

  • SpringApplication.exit()을 먼저 실행하여 Spring의 종료 프로세스를 정상적으로 수행
  • 그 후 System.exit(1);을 실행하여 Kubernetes가 비정상 종료로 감지하고 Pod를 재시작할 수 있도록 해야 한다.

🚀 해결 방법

✅ configurableApplicationContext.isActive() 체크 후 exit() 호출

SpringApplication.exit()을 호출하기 전에 ApplicationContext가 닫히지 않았는지 확인하는 것이 중요하다.
이미 닫힌 컨텍스트에 대해 exit()을 호출하면 IllegalStateException이 발생하므로, 이를 방지하려면 isActive() 또는 isRunning() 체크를 수행해야 한다.


🔹 수정된 코드 (job.namebatch.process.name 변경 포함)

ConfigurableApplicationContext configurableApplicationContext = null; // try 바깥에서 선언

try {
if (log.isDebugEnabled()) {
log.debug("###############batch.process.name = " + System.getProperty("batch.process.name"));
}

SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(Application.class);
SpringApplication springApplication = springApplicationBuilder.build();

// try 바깥에서 선언한 변수에 값 할당
configurableApplicationContext = springApplication.run(args);

JobExecution jobExecution = LoggingJobExecutionListener.getLastJobExecution();
if (log.isDebugEnabled()) {
log.debug("jobExecution.getInstanceId()=" + jobExecution.getJobInstance().getInstanceId());
log.debug("jobExecution.getJobName()=" + jobExecution.getJobInstance().getJobName());
log.debug("jobExecution.getExitCode()=" + jobExecution.getExitStatus().getExitCode());
log.debug("jobExecution.getId()=" + jobExecution.getId());
}

// Batch Process가 실패한 경우 강제로 예외 발생
if (!jobExecution.getExitStatus().getExitCode().equals("COMPLETED")) {
log.error("Batch Process Failed: " + jobExecution.getExitStatus().getExitDescription());
throw new BizException("Batch Process Failed: " + jobExecution.getExitStatus().getExitDescription());
}

// 정상적으로 종료되었을 경우
int exitCode = SpringApplication.exit(configurableApplicationContext);
System.exit(exitCode);

} catch (Exception e) {
log.error("예외 발생! 정리 후 종료 시도", e);

int exitCode = 1; // 기본적으로 비정상 종료 코드 설정
if (configurableApplicationContext != null && configurableApplicationContext.isActive()) {
exitCode = SpringApplication.exit(configurableApplicationContext);
}

log.info("Application will exit with code: {}", exitCode);

// **System.exit()을 호출하여 Kubernetes가 Pod를 재시작하도록 유도**
System.exit(exitCode);
}

✅ Kubernetes에서 정상적으로 종료하도록 설정

Kubernetes에서 비정상 종료가 발생해도 CrashLoopBackOff를 방지하려면, preStop 훅을 추가하여 컨테이너 종료 전에 정리할 시간을 주는 것도 고려할 수 있다.

🔹 preStop 훅 설정 (선택 사항)

lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]

이렇게 설정하면, Pod가 종료될 때 10초 동안 종료 작업을 수행할 시간을 확보할 수 있다.


🎯 결론

  1. 비정상 종료 시에도 Spring의 종료 프로세스를 정상적으로 수행해야 한다.
    • SpringApplication.exit(configurableApplicationContext)을 호출하여 Spring이 ContextClosedEvent를 정상적으로 발생시키도록 한다.
  2. 이미 닫힌 컨텍스트에서 SpringApplication.exit()을 호출하면 IllegalStateException이 발생할 수 있다.
    • 이를 방지하려면 configurableApplicationContext.isActive() 체크 후 실행해야 한다.
  3. Kubernetes에서 Pod가 재시작될 수 있도록 exit code 1을 유지해야 한다.
    • System.exit(1);을 호출하면 Kubernetes가 이를 감지하고 새 Pod를 생성한다.
    • 하지만 System.exit(1);을 호출하기 전에 Spring의 종료 이벤트(ContextClosedEventListener)가 실행될 시간을 확보해야 한다.
  4. 추가적인 안정성을 위해 Kubernetes의 preStop 훅을 활용할 수도 있다.

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다