1. Kubernetes에서의 Graceful Termination
쿠버네티스를 사용을 한다면, 파드를 graceful 하게 종료시키기 위해 deployment 매니페스트에 preStop Hook, terminationGracePeriodSeconds 를 설정하곤 한다.
✔️ preStop Hook
- 파드가 종료를 요청받고, 컨테이너가 종료되기 직전 실행되는 훅
- 애플리케이션이 종료 전에 필요한 작업을 완료할 수 있도록 시간을 줄 수 있으며,
- 이는 서비스의 중단을 최소화하고, 데이터 무결성을 보장하는데 도움이 된다.
- 시간을 길게 설정할수록 안정성은 좋아지나 애플리케이션 다운시간이 길어지는 트레이드 오프가 생긴다.
✔️ terminationGracePeriodSeconds
- 파드 내의 모든 컨테이너가 종료 작업을 완료할 수 있도록 주어지는 유예기간 (초단위)
- preStop Hook 과 마찬가지로 데이터 무결성 보장, 서비스 가용성 유지, 안정성 확보 등의 이점이 있다.
- 기본값은 30초이며, 설정된 시간이 넘게되면 파드는 SIGKILL 신호로 즉시 종료된다.
예시
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
spec:
replicas: 3
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
terminationGracePeriodSeconds: 60
containers:
- name: example-container
image: example-image:latest
lifecycle:
preStop:
exec:
command: ["sh", "-c", "echo 'PreStop Hook Triggered'; sleep 10"]
ports:
- containerPort: 80
✔️ Kubernetes에서 파드가 종료되는 순서
그럼, 쿠버네티스에서 파드가 종료되는 순서를 알아보자.
1. 파드 삭제 요청
- deployment rollout, scale down 등의 사용자 또는 시스템으로부터 파드 삭제 요청을 받는다.
2. 파드를 종료중 상태로 업데이트
- K8S API 서버는 파드의 상태를 ‘Terminating’으로 업데이트를 하고, 해당 파드는 모든 서비스의 endpodint에서 제거된다.
- 이 시점에 파드는 새 트래픽 수신을 중지하게되고, 실행중인 요청은 정상적으로 처리한다.
3. preStop Hook 실행
- 파드에 ‘preStop’ 훅이 정의되어 있으면, 종료 신호를 받았을때 정의된 명령어가 실행된다.
4. 컨테이너에 SIGTERM 신호 전송
- preStop Hook 이 완료되면, K8S는 파드 내 컨테이너들에게 SIGTERM 신호를 보낸다.
- 이때 DB연결, WebSocket 스트림 등 오래 지속되는 연결에 대한 중지가 일어난다.
5. Graceful Termination Period
- K8S는 파드에 정의된 ‘terminationGracePeriodSeconds’ 동안 컨테이너가 종료되기를 기다린다.
6. 파드 제거
- 마지막으로 파드가 제거된다.
- 이때, Graceful Termination Period 후에도 컨테이너가 계속 실행 중이면, SIGKILL 신호가 전송되어 강제로 컨테이너가 종료된다.
유의할점.
- Graceful Termination Period 와 preStop hook, SIGTERM signal이 병렬로 발생한다.
- 때문에 ‘애플리케이션이 종료 전에 필요한 작업을 완료할 수 있는 시간(preStop Hook 수행시간) + 애플리케이션 종료시간’ 보다 ‘terminationGracePeriodSeconds’ 시간이 커야 Graceful Termination이 보장된다고 봐야한다.
- 정상적인 Graceful Termination
- terminationGracePeriodSeconds 시간이 충분치 않은 경우
2. 컨테이너에서의 Graceful Termination
적당한 preStop Hook, terminationGracePeriodSeconds 설정으로 Graceful Termination 이 되었을까?
나도 그런줄 알았지만.. 컨테이너에 함정이 숨어 있었다.
리얼 환경 배포시에 위 설정값을 충분히 크게 줘도 DB 커넥션이 애플리케이션에서 강제로 종료되는 현상이 발생하였고,
팀 내에서 아래 글을 발견하게 되었다.
https://vinodhinic.medium.com/are-you-sure-sigterm-is-received-by-your-application-in-kubernetes-885a3ce7ae4e
정리하면 도커는 2가지 방식, Shell과 Exec 방식을 지원하는데, 도커파일에서 애플리케이션 실행 구문에 어떤 방식을 사용하냐에 따라 kubelet이 보낸 SIGTERM이 실제로 애플리케이션에 수신이 되지 않는다는 점이다.
✔️ Shell 방식
- 컨테이너 내 shell에서 해당 명령을 실행하며, PID 1 인 프로세스는 shell 이 된다.
- 계층 순서는 ‘파드 > 컨테이너 > shell > 애플리케이션’ 이 된다.
- kubelet이 SIGTERM 신호를 보내면 shell 프로세스에서 소비된다.
ENTRYPOINT java -Dspring.profiles.active=${APP_PHASE:-sandbox} -jar /app/app.jar
✔️ Exec 방식
- 컨테이너에서 바로 애플리케이션이 실행되며, PID 1인 프로세스는 애플리케이션이다.
- 계층 순서는 ‘파드 > 컨테이너 > 애플리케이션’ 이 된다.
- kubelet이 SIGTERM 신호를 보내면 애플리케이션 프로세스에서 소비된다.
- 최대 단점은 환경변수를 사용할 수 없다는 것이다.
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
✔️Shell 방식의 컨테이너 내 애플리케이션 종료
내가 서비스하고 있는 애플리케이션의 dockerfile 설정은 shell 방식으로 되어 있었고,
kubelet에서 SIGTERM 신호를 보내도 shell 이 받아버려 Graceful Termination 을 위해 세팅되었던 K8S 설정들이 무의미 했다는걸 알게 되었다.
✔️그래서 어떻게 적용해야하나?
보통은 환경변수를 통해 애플리케이션의 페이즈 등을 애플리케이션으로 전달하기 때문에, Exec 방식은 사용하기 힘들다. 그럼 어떻게 PID가 1 인 애플리케이션의 컨테이너 환경을 만들 수 있을까?
아래 해결책이 있다!!
K8S 매니페스트에 command 와 args 필드를 이용하여 ENTRYPOINT 에 정의된 내용을 재정의 할수있다.
https://stackoverflow.com/questions/44316361/difference-between-docker-entrypoint-and-kubernetes-container-spec-command
아래는 실제 수정된 deployment.yaml 파일이다.
ENTRYPOINT에 정의한 내용을 다시 정의하고 있다. 여기서는 환경변수 사용도 가능하다.
수정 전/후 PID 상태
3. 테스트 해보자
간단하게 DB Pool 가진 Springboot 애플리케이션을 만들어서 테스트 해본다.
✔️Exec 환경 테스트
- Exec 방식으로 ENTRYPOINT 를 수행하여 도커 이미지를 빌드한다.
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=docker", "/app/check-pid-0.0.1-SNAPSHOT.jar"]
- 컨테이너 실행 후, PID 확인을 하면 java 가 PID 1 이다.
docker kill --signal=SIGTERM check-pid-exec
- start 로그 이후에 DB Pool이 정리되는 로그가 보인다.
✔️Shell 환경 테스트
- Shell 방식으로 ENTRYPOINT 를 수행하여 도커 이미지를 빌드한다.
ENTRYPOINT java \
-Dspring.profiles.active=${APP_PHASE:-docker} \
-jar /app/check-pid-0.0.1-SNAPSHOT.jar
- 컨테이너 실행 후, PID 확인을 하면 java PID는 7 이고, 이를 수행하는 /bin/sh 이 PID 1 이다.
docker kill --signal=SIGTERM check-pid-exec
- 아무런 반응이 없다. 애플리케이션 종료가 안된다.
docker stop check-pid-exec
- SIGTERM 신호를 전달하고 10초간 대기 후 SIGKILL 신호를 전달하여 강제 종료한다. (start 로그 이후에 아무런 로그가 없다.)
4. 애플리케이션이 SIGKILL 신호를 못받으면 무엇이 문제가 될까?
✔️ 맺고 있었던 DB 커넥션 등이 클라이언트에서 강제 종료가 된다.
- 애플리케이션(클라이언트)에서 커넥션이 강제 종료 되면, 클라이언트가 보낸 데이터가 DB에 적용되지 않을 수 있고,
- 클라이언트가 트랜잭션을 진행 중이었다면, 해당 트랜잭션은 롤백될 수 있다.
- 또한, 강제로 종료된 커넥션은 DB 입장에서 종료되지 않은 상태로 남아있을 수 있어 자원 누수로 이어질 수 있다.
실제 Shell 환경 테스트를 진행해보면, DB에 클라이언트가 비정상적으로 종료된 흔적이 남는다.
SHOW STATUS LIKE 'Aborted%';
Aborted_clients: 클라이언트가 비정상적으로 종료되어 서버가 연결을 중단한 횟수
Aborted_connects: MySQL 서버에 접속을 시도했으나 실패한 연결 횟수
✔️ 애플리케이션 종료 리스너가 수행하지 않는다.
- 애플리케이션이 종료될때 이벤트 리스너를 받아 필요한 로직을 수행하는 경우가 있을 것이다.
- 애플리케이션이 강제 종료되면, 리스너 수행이 되지 않아 문제가 된다.
✔️ 기타
- 애플리케이션이 강제 종료되면, 메모리 누수, 파일 디스크립터 누수 등의 문제를 초래할 수 있고,
- 서비스가 갑자기 중단되므로 해당 서비스의 가용성이 저하된다.