Graceful Shutdown Series
해당 시리즈는 애플리케이션을 Graceful 하게 종료하는 방법에 대한 시리즈입니다.
Golang 을 기반으로 Graceful Shutdown 에 대한 주제를 담고 있습니다.
- Go Context를 활용하여 애플리케이션을 안전하게 종료하기
- 파일 업로드 도중 배포를 진행해도 괜찮을까?-애플리케이션 개발편
- 파일 업로드 도중 배포를 진행해도 괜찮을까?-Graceful shutdown 적용편
지난 글 회고
지난 글에서 저희는 대량의 파일을 업로드를 진행하는 도중 애플리케이션에게 중단 신호가 들어오더라도 고객의 파일 업로드를 모두 완료한 후 중단되는 것을 확인할 수 있었습니다.
이는 곧 graceful 한 무중단 배포를 지원한다는 의미이기도 합니다.
업로드를 진행하던 기존 고객은 이전 애플리케이션에서 업로드를 완료하도록 지원하며, 배포를 위해 신규로 생성된 애플리케이션에서는 신규 업로드를 진행하게 됩니다.
특히 저의 경우 업로드 전용 서버를 운영하던 중이었기 때문에 서버를 재시작하는 것만으로도 부담이 되는 작업이었습니다. 타팀에서 서버의 네트워크 트래픽과 세션으로 end-user 의 요청을 모니터링하며 GSLB에서 서비스를 제외 시켜가며 배포를 진행해왔습니다. 그러면 저도 Network Traffic 의 Public 과 Private 의 사용량을 보고 있다가 배포를 진행하였습니다.
글로벌 서비스를 준비하는 과정에서 업로드 서버가 늘어날 경우에 일일이 배포에 대한 모니터링을 진행할 수는 없기 때문에 위와 같은 graceful 한 배포가 필요하였습니다.
이때까지의 과정은 배포로 인해 업로드가 끊기는 사용자의 불편함을 해소할 수도 있으며, 배포에 대한 불안감을 해소시켜줄 수 있는 작업이기도 합니다.
그럼 이제 실서버 적용을 위해 Docker 환경과 Kubernetes 환경에서는 어떻게 적용되는지 알아보도록 하겠습니다.
Docker Graceful Shutdown Test
먼저 애플리케이션의 Dockerfile 을 작성하였습니다. Dockerfile 의 경우 Multi Stage Build 를 적용하였습니다. 이는 이전 글 Docker 의 Multi Stage Build 를 참고하면 좋습니다.
Dockerfile
FROM golang:1.21 AS build
ENV GO111MODULE=on
RUN apt-get update && apt-get install -y build-essential
WORKDIR /usr/src/app
COPY . .
RUN go mod download
RUN make build
FROM alpine:3.17.3
RUN apk update && apk add --no-cache libc6-compat
RUN mkdir -p /home/jake
WORKDIR /home/jake
COPY --from=build /usr/src/app/templates/ /home/jake/templates/
COPY --from=build /usr/src/app/bin/upload-app .
CMD ["./upload-app"]
EXPOSE 8082
위의 Dockerfile 같은 경우는 Multi Stage 로 구성되어 있습니다. FROM 명령어를 기준으로 Docker Stage 단계가 구분됩니다.
위처럼 Multi Stage 로 구성하는 가장 큰 이유는 도커 이미지의 용량을 줄이기 위함입니다. 저희는 서비스를 지원하기 위해 필요한 것은 오직 서비스의 실행파일 뿐이기 때문입니다.
따라서 AS build
단계에서 생성한 실행파일을 alpine Base Image 를 기반한 컨테이너에서 그대로 실행할 수 있습니다.
Makefile
PROJECT_PATH=$(shell pwd)
MODULE_NAME=upload-app
BUILD_NUM_FILE=build.num
BUILD_NUM=$$(cat ./build.num)
APP_VERSION=$$(cat ./version.txt)
TARGET_VERSION=$(APP_VERSION).$(BUILD_NUM)
IMAGE_REPOSITORY={IMAGE_REPOSITORY}
TARGET_DIR=bin
OUTPUT=$(PROJECT_PATH)/$(TARGET_DIR)/$(MODULE_NAME)
MAIN_DIR=/main.go
LDFLAGS=-X main.BUILD_TIME=`date -u '+%Y-%m-%d_%H:%M:%S'`
LDFLAGS+=-X main.GIT_HASH=`git rev-parse HEAD`
LDFLAGS+=-s -w
all: config target-version docker-build
config:
@if [ ! -d $(TARGET_DIR) ]; then mkdir $(TARGET_DIR); fi
build:
CGO_ENABLED=0 GOOS=linux go build -ldflags "$(LDFLAGS)" -o $(OUTPUT) $(PROJECT_PATH)$(MAIN_DIR)
cp $(OUTPUT) ./$(MODULE_NAME)
docker-build:
docker build -f Dockerfile --tag $(IMAGE_REPOSITORY):$(TARGET_VERSION) .
docker-run:
docker run -d --rm --name uploader -p 8082:8082 $(IMAGE_REPOSITORY):$(TARGET_VERSION)
target-version:
@echo "========================================"
@echo "APP_VERSION : $(APP_VERSION)"
@echo "BUILD_NUM : $(BUILD_NUM)"
@echo "TARGET_VERSION : $(TARGET_VERSION)"
@echo "========================================"
build-num:
@echo $$(($$(cat $(BUILD_NUM_FILE)) + 1 )) > $(BUILD_NUM_FILE)
clean:
rm -f $(PROJECT_PATH)/$(TARGET_DIR)/$(MODULE_NAME)*
위의 Dockerfile 에서 make build
라는 키워드를 사용했기 때문에 간단하게 소개합니다. Makefile 은 매크로 파일이라고 할 수 있습니다. 우리가 자주 사용하는 명령어에 대해 매크로를 등록해 해당 키워드만 호출되면 정의해둔 명령어가 실행되는 개념입니다.
go build
의 경우도 build 정보나 옵션들이 필요하기 때문에 Makefile 을 사용하여 간편하게 실행하고 있습니다.
Docker Container Graceful Shutdown Test
docker run -d --rm --name uploader -p 8082:8082 $(IMAGE_REPOSITORY):$(TARGET_VERSION)
먼저 위의 명령어로 docker container 를 실행한 뒤 docker stop
명령어를 실행하였습니다.
docker stop 명령어를 사용하니 종료 시그널이 전달된 후 10초 뒤에 종료가 되지 않으면 강제종료가 되는 것을 알 수 있었습니다.
이에 필요한 docker 옵션이 -t
옵션입니다. -t
옵션을 주면 Docker 가 SIGTERM 시그널을 받은 이후 설정된 시간만큼 기다린 후에 종료되지 않으면 강제 종료를 진행하게 됩니다. 단위는 초 단위입니다.
docker stop -t 600 uploader
-t
옵션을 10분으로 주니 업로드가 완료된 후 docker container 가 종료되는 것을 알 수 있었습니다.
Kubernetes Graceful Shutdown Test
Kubernets 에는 docker stop -t
옵션과 같은 역할을 하는 옵션이 있습니다.
terminationGracePeriodSeconds: 600
위의 옵션은 deployment.yaml manifest 파일에 아래와 같은 예시로 사용됩니다.
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: myimage
terminationGracePeriodSeconds: 600
위의 옵션이 없는 경우 30초 기다리다가 kuberneters 가 pod 에게 sigkill 명령어로 강제 종료합니다.
pre-stop
kubernetes 에서는 pod 가 종료될 때 종료되기 전 실행되는 hook 을 설정할 수 있습니다.
이때 설정되는 hook 또한 위의 옵션에서 설정한 terminationGracePeriodSeconds
시간 내에서 완료되어야 합니다. terminationGracePeriodSeconds
가 설정되지 않으면 기본값으로 30초로 설정됩니다.
pod 가 종료되면 preStop
과 terminationGracePeriodSeconds
이 동시에 SIGTERM 이벤트를 받기 때문입니다.
pre-stop hook 은 동시에 실행되지만 각각 컨테이너는 병렬로 종료됩니다.
kubectl grace period option
kubectl delete po kubia-ssl --grace-period 10
pod 도 docker 와 마찬가지로 delete 명령어를 수행 시 옵션으로 grace period 시간을 조정할 수 있습니다.
만일 grace period
옵션을 0으로 설정하게 된다면 pod 의 pre-stop hook 이 실행되지 않습니다.
결론
애플리케이션에서뿐만 아니라 docker, kubernetes 에서도 graceful shutdown 이 적용되는 것을 확인할 수 있었습니다. docker 의 -t
옵션, kubernetes 의 terminationGracePeriodSeconds
옵션도 절대적인 시간으로 대기하는 것이 아닌, 최대 기다리는 시간으로 적용되는 점을 알 수 있었습니다.
또한 kuberntes 에서는 pre-stop hook 을 제공하여 Pod 의 종료 전에 작업을 수행하는 기능을 제공하고 있습니다.
물론, 예기치 못한 종료 자체는 빈번하지 않은 현상입니다. 만일 애플리케이션이 계속해서 Restart 를 반복한다면 그 애플리케이션 자체가 문제이겠죠. Graceful Shutdown 에 대한 개념은 생각보다 여러 측면들을 고려해야 하다보니 번거롭게 느껴질 수 있습니다.
하지만 이러한 작업 없이 수많은 트래픽을 감당하는 서버를 자신있게 배포할 수 있을까요? 배포를 위해 매번 트래픽을 체크하고, 트래픽이 없을 때 배포하기 위해 배포 시간대를 정하는 번거로움에서 우리는 벗어날 수 없는 건가요?
보다 안정적인 애플리케이션을 운영하기 위해서 우리는 우리가 만든 애플리케이션의 수명주기에 보다 관심을 갖고, 로직 뿐만 아닌 CI-CD 까지 고려하는 노력이 필요하다고 생각합니다.