도커기반으로 작동하고 SpringBoot+KAFKA채택시 고려해야할 우아한 종료(안전한 자동종료)를 작동시키기 위해서 정리한내용이며
Linux Signals/Spring Boot/Docker 과 관련된 종료사이클 3가지를 먼저 이해해야합니다.
Signals
UNIX 시스템의 핵심 중의 하나인 Signal은 프로세스에게 어떤 Event의 발생을 알리기 위해 전달 되는 소프트웨어 인터럽트다. 유닉스/리눅스 운영체제는 매우 다양한 종류의 Signal이 있으며 이러한 Signal은 각각의 의미를 가지고 사용되어집니다.
[root@peterdev dev]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
Application 종료와 관련된 주요 시그널
프로세스 정상종료 신호 : SIGTERM
프로세스 강제종료 : SIGKILL
정상종료신호는 프로세스가 정상종료될때까지 대기하게되며, 강제종료의 경우 강제중단을 모든스레드에게 전송함으로 진행중인 작업이 완료되지 않고 모두 중단되게 됩니다.
Spring Boot옵션
server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20s
Spring Boot 2.3이상부터 지원하는기능이며 진행중인 스레드의 종료와 함께 네트워크 외부 요청을 막게됩니다.
Spring Boot 이 JVM환경위에 동작하기때문에 JVM과 관련한 정상종료 규칙도 추가적으로 살펴보면 도움이됩니다.
JVM은 다음과 같은 경우 정상적인 종료 절차를 밟게 됩니다.
데몬 스레드가 아닌 일반 스레드가 모두 종료되는 시점
System.exit 메서드가 호출 될 경우
프로세스가 종료 시그널을 받게 된 경우
IDE 종료편
InteliJ
관련 테스트를 하게되어 알게된 사실인데 InteliJ의 네모(중단) 버튼은 최초 클릭시 정상종료 시그널을 보냅니다.
네모버튼이여서 강제중단으로 인지하였으나~ 안전한 종료 시그널을 보내게 되네요
VisualStudio
app.Lifetime .ApplicationStopping .Register(() => Console.WriteLine("Application Stopping"));
AKKA는 .NET으로도 이용할수 있으며 VisualStudio IDE가 사용됩니다.
VS의 중지기능의 경우 InteliJ와 다르게 강제종료로 수행됩니다.정상종료 시그널을 보내려면 콘솔창에서 ctrl+c 인터럽트를 보내면 정상종료코드 수행됩니다.
Docker 옵션
docker를 중지하는 방법은 두가지가 있으며
docker stop
docker kill
차이점은
- sotp은 정상종료 신호(SIGTERM)을 보내고 어플리케이션의 정상 종료를 대기
- kill의 경우 강제종료(SIGKILL)명령을 보내 즉각 종료를 시도
- 공통으로 특정시간이내 종료가안되면 도커의 데몬에의해 해당 프로세스 강제종료가 마지막에 수행될수 있습니다.
여기서 고려해야할점은 정상종료까지 기다리는 도커 기본 default 대기시간은 10s 이며
어플리케이션 우아한 종료 설계와 상관없이 도커가 강제 중단을 시킬수 있기때문에
spring boot의 timeout-per-shutdown-phase 설정값은 도커대기시간보다 작아야합니다.
10s는 우아한 종료를 하기위해 부족한 시간일수 있으며 다음과같은 중지옵션으로 종료 대기시간을 늘릴수 있습니다.
docker stop -t 60 somAPP
우리는 쿠버나 도커를 다루는 기타 오케스트레이션툴을 이용하기 때문에 업데이트시 도커명령자체를 직접 수행하지 않습니다.
docker의 경우 10s이지만 설정 기본값이 다를수 있으며 DockerCompose나 쿠버의 경우 다음값을 통해 컨테이너 종료 대기를 할수 있습니다.
stop_grace_period : DockerCompose
terminationGracePeriodSeconds
: 쿠버네티스
도커컨테이너를 빌드할때 실행명령을 쉘(.sh)로 수행하면 종료시그널이 어플리케이션까지 전달되지 않습니다.
시그널을 발생하는 스크립트 작업을 별도로 해야하며 일반적으로 .jar를 직접 실행하는것이 좋습니다.
# 종료신호 전달됨 CMD ["java", "-jar", "app.jar"]
# run.sh 에 자바실행코드가 포함된경우 java -jar app.jar # 종료신호가 어플리케이션에 전달안됩니다. 별도의 이벤트 전달 코드를 sh내에 구현해야함 RUN chmod +x run.sh
GraceFulShotDown전략
정상종료를 수행하면 어플리케이션이 지원하는 DI의 라이프사이클에 따라 진행되는 프로세스가 일반적으로 완료처리가 됩니다.
하지만 외부 네트워크 요청을 기본적으로 막기때문에 모든 요청에대해 누락없이 수행한다라는 의미이며 현재 진중중인 작업을 안전하게 종료하는 최소한의 프로그래밍이 필요없는 장치이며
서비스 업데이트시 무중단을 고려한다고하면 배포전략도 함께 세워야합니다. 상태없는 서비스인경우 블루/그린 배포방식을 채택할수도 있습니다.
Blue Green배포
상태없이 설계된 웹서비스에서 무중단으로 배포가되기 Blue/Green 배포방식을 채택을해서 로드밸런스를 통한 Green으로의 트래픽이 완전하게 전환되면 Blue셧다운 시킬수도 있습니다.
이것 자체가 잘작동된다고 하면 처리해야할 작업이 남아있지 않기때문에 GraceFulShotDown의 종료대기가 없을수 있습니다. 하지만 종료 로그를 마지막에 남길수 있고 스케줄러작업이 작동할수도 있기때문에 함께 이용될수 있습니다.
카프카와같이 메시징 큐를 도입하거나 , 별도의 메시징큐를 가지고 있는 상태있는 서비스의 경우 다음과 같이 고려해야할 사항이 더 있습니다.
Kafka GracefulDown 전략
카프카 컨슈머의 경우 shutdown hook과 KafkaConsumer.wakeup()을 활용하여 컨슈머 종료전에 offset 커밋처리를 할수 있습니다.
어느정도 의도된 종료전략을 사용해야합니다.
public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new ShutdonwThread()); } static class ShutdownThread extends Thread { public void run() { consumer.wakeup(); } }
메시지 전송보장을위해 어떠한 전략을 채택할것인가? 더 고급주제로 이어갈수 있습니다.
일반적으로 이러한것이 고려되지 않을때 장애가 아님에도불구 업데이트 셧다운 과정도 발생할수 있기 때문입니다.
- Kafka Message Delivery Semantics - Exactly Once 는 정말 가능할까?
- Message order and delivery guarantees in Elixir/Erlang
- Kafka를 포함 메시징을 처리하는 플랫폼에서 메시지 전송보장을 위해 다양한 옵션을이용할수 있으며 이러한 개념은 언랭에서 유례되었습니다.
Coordinated shutdown
코드구현이 필요없는 GracefulDown 작동은 일반적으로 상태가 없는 서비스에서 활용할수 있으며
상태가 있는 메시징 서비스 또는 자체적으로 배치스케줄러가 지속 작동한다고 하면 종료코드를 의도해서 작성해야합니다.
샘플코드 : gracefulDown 종료시점 의도된 종료시그널을 대기하는 샘플
@BeforeClass public static void bootUp() { actorSystem = serverStart("ClusterSystem", "router-test", "seed"); logger.info("========= sever loaded ========="); appActor = actorSystem.actorOf(WorkStatusActor.Props(), "APPActor"); } @AfterClass public static void bootDown() { logger.info("========= try graceful down ========="); int retryCount = 5; for (int i = 0; i < retryCount; i++) { CoordinatedShutdown.get(actorSystem).addTask( CoordinatedShutdown.PhaseBeforeServiceUnbind(), "WorkCheckTask", () -> { return akka.pattern.Patterns.ask(appActor, "stop", Duration.ofSeconds(1)) .thenApply(reply -> Done.getInstance()); }); } }
Kafka의 경우도 GraceFulDown을 공식지원하는 형태가 아니고 의도된 작성을 해야하기때문에 의도된 종료에 더 가깝습니다.
AKKA Stack에서도 HTTP의 경우 GraceFul종료라고 정의를하고 큐를 주로 사용하는쪽에서는 Coordinated종료 라고 표현하고 있습니다.
- https://doc.akka.io/docs/akka/current/coordinated-shutdown.html
- https://doc.akka.io/docs/akka-http/current/server-side/graceful-termination.html
스마트한 종료,우아한종료,안전한 종료등 다양한 한글적 의미가 사용될수 있으며 특정 하나만을 채택하는것이아닌
전략적으로 복수개 채택될수 있습니다.
- GraceFulShoutDown : 안전한 자동종료에 가깝고, 프레임워크가 제공해주지만 어플리케이션및 스레드 라이프사이클을 이해해야합니다.
- CoordinatedShoutDown : 안전하게 종료하기위해 의도된 설계종료 코드가 필요할수 있으며, 스레드종료보다는 상태있는 메시지의 흐름을 고려해야합니다.