Graceful Shutdown

장준영·2024년 5월 10일

개요

blue/green배포를 하면서 위와 같은 문제가 발생하였다.

현재의 Blue/Green배포의 프로세스를 알아보자

Github 브랜치에 새로운 버전이 병합된다.
Github는 Merge가 되었을때 Jenkins에 Webhook요청을 보낸다.
Jenkins는 새로운 버전 코드를 pull해오고 빌드한다.
빌드한 파일을 배포 서버로 전송한다
(8081 포트를 사용하고 있다는 전제하에) 8082포트로 신버전 애플리케이션을 구동한다.
지속적으로 8082 포트로 헬스체크를 한다.
8082포트의 애플리케이션이 떴다면, 리버스 프록시로 사용중인 Nginx의 프록시 설정을 변경하여 8081으로 프록시하던 설정을 8081로 변경하고 reload한다.
그후 8081포트로 구동중인 애플리케이션의 프로세스를 제거한다.

현재의 프로세스는 kill9를 하고 죽인다.

하지만 위와같이 처리를 하면 처리하지 못한 프로세스 또한 종료되기 때문에 위와같은 문제를 해결하고자 GracfulShutdown을 도입하게 되었다.

Graceful shutdown 이란?

  • Graceful shutdown이 진행되면 더이상 요청은 거부한다.
  • 처리중인 요청이 있다면 마무리하고 server를 종료시킨다.

서버 종료방식

서버를 종료하는 방식을 바꿔줘야 한다.
Linux에서 process를 종료할때, kill명령어를 사용한다.

위의 개요처럼 현재는 -9옵션으로 프로세스를 종료하고 있다.
하지만 이번 gracefulShutdown을 하기 위해선 -15로 종료를 해야 한다.

-9(SIGKILL): 프로세스를 즉기 종료. 따라서 처리중이던 작업들의 유무에 관계없이 즉시 종료됨
-15(SIGTERM): 프로세스를 정상적으로 종료시킨다. 소프트웨어 프로세스에게 종료하라는 시그널을 준다고 생각하면 된다.

따라서, 프로세스를 정상적을 종료시켜줘야 Graceful-Shutdown을 사용할 수 있다.
Spring의 경우 SpringApplicationShutdownHook 이라는 객체를 통해 Spring을 종료시키기 시작한다.

GracefulShutdown 절차

  1. Process 죽이는 방법을 변경
echo "> 현재 구동중인 Port 확인"

CURRENT_PORT=8081;
NEW_PORT=8082;

# CURRENT & NEW 포트 확인
if lsof -Pi :8082 -sTCP:LISTEN -t >/dev/null; then
    echo "현재 8082 포트가 사용 중입니다."
    CURRENT_PORT=8082
    NEW_PORT=8081
elif lsof -Pi :8081 -sTCP:LISTEN -t >/dev/null; then
    echo "현재 8081 포트가 사용 중입니다."
    CURRENT_PORT=8081
    NEW_PORT=8082
else
    echo "8082과 8081 포트 모두 사용 중이지 않습니다."
fi
# NEW 포트 사용중일 경우 종료
if lsof -Pi :$NEW_PORT -sTCP:LISTEN -t >/dev/null; then
  echo "NEW_PORT가 사용중입니다."
  PID=$(lsof -Pi :$NEW_PORT -sTCP:LISTEN -t)
  kill -15 $PID
  echo "사용 중인 NEW_PORT 종료했습니다.."
  sleep 3
else
  echo "$NEW_PORT가 사용 중이지 않습니다."
fi

#Spring ON
nohup java -jar "KOIN_API_V2.jar" --server.port=$NEW_PORT --spring.profiles.active=dev > log.txt 2>&1 &

echo 백그라운드 모드로 애플리케이션 실행 성공 !!

# 새로 가동하는 서버 상태 확인
sleep 20

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$NEW_PORT/actuator/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
 then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      echo "> Health check 성공"
      break
  else
      echo "> 새롭게 가동하는 서버의 상태가 UP이 아닙니다."
      echo "> Health check: ${response}"
  fi

  if [ $retry_count -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> Nginx에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done

# 새로 가동하는 서버로 전환

echo "> 전환할 Port: $NEW_PORT"

echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${NEW_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

echo "> NGINX Reload"
sudo service nginx reload

echo "> CURRENT_PORT를 종료합니다"
CURRENT_PID=$(lsof -Pi :$CURRENT_PORT -sTCP:LISTEN -t)
kill -9 $CURRENT_PID

kill을 하는 명령어를 -9에서 -15로 바꾸어 주었다.

  1. Spring YAML 파일 수정
spring:
  lifecycle:
    timeout-per-shutdown-phase: 5s
    
server:
  shutdown: graceful

위의 두가지 또한 설정해 준다.

현재 KOIN에서 처리하는 요청은 대부분 3초 이내에 해결하기에 5초로 time-out을 정해주었다.
(사실 5초가 넘어가면 데드락, 무한루프등 문제가 있는 요청이라고 봐도 무방하다.)

출처:
https://velog.io/@byeongju/SpringBoot%EC%9D%98-Graceful-Shutdown
https://velog.io/@dongvelop/Springboot-Graceful-Shutdown
https://hudi.blog/how-to-gracefully-zero-downtime-deploy/

1개의 댓글

comment-user-thumbnail
2024년 11월 24일

(초반에, 8082로 변경하고 reload인데 오타인듯.)
오 이런것도 고려해서 처리하고 있었구나. 진짜 생각치도 못한게 많네.

답글 달기