본문 바로가기
Kotlin Spring/Kotlin Spring

Graceful Shutdown (처리중인 로직이 완료되지 않은 상태에서 종료할 것인가?)

by Bill Lab 2026. 1. 15.
반응형

1. K8S 기반 소스 배포 시 기존 POD 내 실행중인 프로세스가 있는 상화에도 불구하고 종료할 것인가?

    : No, 처리중인 내역이 모두 정상처리 후에 기존 Pod 를 정리해야 한다.

 

2. Graceful Shotdown 설정(spring boot 2.3이상 지원)

server:
  port: 8080
  shutdown: graceful  # SIGTERM 수신 시 graceful shutdown 모드 활성화
  
  #생략
  
spring:
  profiles:
    active: "local"
  lifecycle:
    timeout-per-shutdown-phase: 20s #종료대기 시간 설정(최대 허용 시간)

 

3. 처리 순서

     1) K8S SIGTERM 을 보냄

     2) spring boot 서버에서 신규 요청을 차단, 기존 요청이 처리 완료까지 대기

     3) timeout-per-shutdown-phase 초과 시: 아직 처리중인 요청이 있더라도 강제 종료

 

4. 특징

     1) Netty, Tomcat 모두 지원

     2) 기존 in-flignt http 요청만 처리(Kafka 메시지에 대한 처리는 별도 관리 필요 - transactional outbox pattern 및 DLQ 처리 등)

     3) Websocket 요청은 별도 처리 필요

     4) 애플리케이션 단에서 in-flight 요청/작업 추적draining 모드를 함께 구현하는 것이 안전

 

 

종료 시 아래와 같은 로그를 확인할 수 있다.

 

 

5. in-flight, draining 모드 예시

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.event.ContextClosedEvent
import org.springframework.context.event.EventListener
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger

//전역 애플리케이션 상태 관리
object AppState {
    @Volatile
    var draining: Boolean = false              // SIGTERM 수신 후 draining 모드 여부
    val inFlight: AtomicInteger = AtomicInteger(0) // 진행 중 요청/작업 수
}

//HTTP 요청 추적 및 신규 요청 차단 WebFilter
@Component
class InFlightTrackingFilter : WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        if (AppState.draining) {
            exchange.response.statusCode = HttpStatus.SERVICE_UNAVAILABLE
            return exchange.response.setComplete()
        }

        AppState.inFlight.incrementAndGet()
        return chain.filter(exchange)
            .doFinally { AppState.inFlight.decrementAndGet() }
    }
}

//비동기 작업 관리용 예제 Executor
@Component
class AsyncTaskExecutor {
    private val executor = Executors.newFixedThreadPool(4)

    fun submit(task: Runnable) {
        if (AppState.draining) {
            println("Draining 중 신규 작업은 거부")
            return
        }

        AppState.inFlight.incrementAndGet()
        executor.submit {
            try {
                task.run()
            } finally {
                AppState.inFlight.decrementAndGet()
            }
        }
    }

    fun shutdown() {
        executor.shutdown()
    }
}

//SIGTERM 수신 시 draining + in-flight 대기 후 종료
@Component
class ShutdownHandler(private val asyncTaskExecutor: AsyncTaskExecutor) {

    @EventListener
    fun onContextClosed(event: ContextClosedEvent) {
        println("SIGTERM 수신: draining 모드 전환")
        AppState.draining = true

        // 종료 전 in-flight 작업 완료 대기
        while (AppState.inFlight.get() > 0) {
            println("in-flight 작업 남음: ${AppState.inFlight.get()}, 대기중")
            Thread.sleep(100)
        }

        println("모든 in-flight 작업완료, Executor 종료")
        asyncTaskExecutor.shutdown()
    }
}

 

반응형