Kubernetes Terminating 이슈
Kubernetes를 이용하다보면 자주 겪는 이슈인데 별로 문제가 되지 않아 그냥 넘어가게 되는 그런 이슈 하나를 소개드립니다.
현상은 kubectl delete po
커맨드 후 Pod을 지켜보면 Pod 상태가 1/1에서 사라질 때 까지 0/1로 변하지 않는 현상입니다. (
$ kubectl get po
esevan-park-e9c9158b-8348-47d3-9b8b-a41df430b1c9 1/1 Terminating 0 2m15s
Pod 상태가 의미하는 바는 "1개의 Container가 여전히 Running 중이다." 입니다.
좀 더 정확히 이야기해보면 Container Entrypoint로 지정한 Process가 본인이 종료된 것을 인지하지 못하여 여전히 Running중인 상황입니다.
현업에서 다음과 같은 문제 상황을 겪어봤습니다.
-
Stateful Application의 Replica 생성 (Node 장애 혹은 서비스 업데이트)시, 기존 Process로 인한 Split brain 현상 야기
-
Rolling Update로 서비스를 업데이트시, SIGKILL에 의해 처리 중인 Request에 대한 Response 실패
-
즉각 적인 Pod Termination을 기대하는 경우, Pod Graceful Termination Period (30초)로 인한 Termination 지연
-
Docker가 삭제할 수 없는 Resource를 점유하고 있을 때, Container 삭제가 정상적으로 이루어지지 않아 Docker Daemon에 영향 (계속해서 삭제 시도하는 Container가 쌓이기 시작...)
Kubernetes-friendly feature (조만간 한 번 다루도록 하겠습니다.)가 자체적으로 구축되지 않은 프로젝트의 경우 "동시에 하나의 인스턴스만 동작해야한다." 는 제약사항이 있는 경우가 있습니다.
이 때, Split Brain을 피하기 위해 "Recreate" Deployment Strategy 를 사용한다면 Terminating 상황에서 Endpoint가 없어지기 때문에 새로운 Pod가 동작할 때까지 Connection / Request Timeout 으로 인한 30초 + @의 Downtime이 발생하게 됩니다.(kubectl get ep 참고)
제가 분석한 4가지 상황이 흔한 상황은 아니라서 큰 이슈가 아닐 수 있습니다만..
저는 모두 겪었던 이슈들이라서 본 현상에 대해 공유 드리려 합니다.
Kubernetes Pod Termination 과정으로 보는 문제 원인
Kubernetes의 Pod Termination 과정은 요약하면 다음과 같습니다.
-
Pod 삭제 요청 (Grace Period = 30s)
-
Pod 상태 변경 (Running -> Terminating)
-
2번과 동시에 Pod의 preStop hook 발생 및 Container 내 1번 프로세스에 SIGTERM 시그널 발생
-
2번과 동시에 Service Endpoint에서 Pod 삭제 -> Service 주소로 Request할 경우 해당 Pod으로 Request가 전달되지 않음.
-
Grace Period (기본 30초) 후 SIGKILL로 Container 강제 삭제 후 Kubernetes Object 삭제.
제가 주목한 것은 3번입니다.
SIGTERM 시그널 발생
만약 Main Process의 SIGTERM Handler를 구현하지 않으셨다면 언어별 기본 SIGTERM Handler가 불리게 되고 대부분 SIGKILL과 동일하게 Process의 즉시 종료입니다.
위 이슈는 발생하지 않으나 Graceful Shutdown이 발생하지 않아 현재 Queuing된 Request들을 처리할 수 없고 사용자는 5xx에러 Response를 받아 API Downtime이 발생할 수 있습니다.
Service Manager Framework를 사용하신다면 대부분 Handler를 Framework내부에서 지원하며, 사용하지 않을 경우 signal() 시스템콜을 통해 직접 Graceful Shutdown Logic을 등록하실 수 있습니다.
# Python예시
import os
import subprocess
import signal
import time
class GracefulKiller:
kill_now = False
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self,signum, frame):
print('{} signal has been trapped. Cleaning my room before you kick me out.'.format(signum))
time.sleep(1)
self.kill_now = True
if __name__ == '__main__':
killer = GracefulKiller()
while True:
time.sleep(1)
print("doing something in a loop ...")
if killer.kill_now:
break
print "End of the program. I was killed gracefully :)"
1번 프로세스에 SIGTERM 시그널 발생
-
만약 Container의 Dockerfile에 CMD로 프로세스를 실행하도록 정의하셨다면,
Container는 /bin/sh를 1번 프로세스로 실행하고 CMD에 정의된 프로그램을 Fork하여 Main Process를 실행합니다. -
만약 Container 실행 시 쉘 스크립트를 사용하신다면,
Container는 쉘을 1번 프로세스로 실행하고 내부에서 Fork하여 Main Process를 실행합니다.
따라서 SIGTERM은 /bin/sh 혹은 정의한 쉘에만 전달되고 Main Process에 전달되지 않아 Main Process가 SIGTERM을 처리할 수 없고 Graceful Shutdown Logic이 Trigger되지 않아 정상적으로 점유중인 Resource를 삭제하지 못한채 Grace Period 이 후 찝찝한 상태로 SIGKILL을 맞게 됩니다. (이 때도 역시 Main Process에 SIGKILL이 전달되지 않아 문제가 될 수 있습니다.)
아래는 Bad practice입니다. Main Process의 PID가 57입니다...
root@esevan-park-a5d58818-292b-48af-852f-7e133c9511d2:/home/esevan.park# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 16:33 ? 00:00:00 /bin/sh -c /usr/local/bin/bootstrap-kernel.sh
root 8 1 0 16:33 ? 00:00:00 /bin/bash /usr/local/bin/bootstrap-kernel.sh
root 17 1 0 16:33 ? 00:00:00 /usr/sbin/sshd
esevan.+ 57 8 12 16:33 ? 00:00:00 python /usr/local/bin/kernel-launchers/python/scripts/launch_ipykerne
esevan.+ 65 57 0 16:33 ? 00:00:00 python /usr/local/bin/kernel-launchers/python/scripts/launch_ipykerne
root 76 0 0 16:33 pts/0 00:00:00 bash
root 87 76 0 16:33 pts/0 00:00:00 ps -ef
해결방법 5가지
해결 방법은 Dockerfile을 수정하거나 Script를 수정하는 것입니다.
-
Dockerfile에서 실행되는 프로세스는
CMD
대신ENTRYPOINT
를 사용 -
스크립트 내에서는 직접 Executable 실행하지 않고
exec
을 통해 "Fork없이" 현재 프로세스에서 실행되도록 변경. -
스크립트 내에서 User를 변경해야 할 경우, sudo 커맨드 혹은 su 커맨드 대신 gosu 사용
-
Script 내에서 Signal Trap 후 전달.
# before #!/bin/bash Executable # after #!/bin/bash Executable & sid=($!) trap "echo \"SIGTERM to ${sid}\"; kill -SIGTERM ${sid}" SIGTERM trap "echo \"SIGHUP to ${sid}\"; kill -SIGHUP ${sid}" SIGHUP trap "echo \"SIGINT to ${sid}\"; kill -SIGINT ${sid}" SIGINT wait $sid
-
Pod의 preStop hook 발생 및 Container 내 1번 프로세스에 SIGTERM 시그널 발생
마지막으로 소개드릴 방법은 preStop hook 입니다.
SIGTERM 시그널을 발생시키기 전에 Kubernetes에서는 preStop hook을 발생시킵니다.
Container 명세에 아래와 같이 명시하면 Pod 종료 시 preStop hook 스크립트 실행이 가능합니다.
containers:
- name: user-container
lifecycle:
preStop:
exec:
command: ['/bin/terminate.sh']
Kubernetes 공식 문서에 따르면 HTTP도 지원한다고 하니 참고하시면 좋겠습니다.
Graceful Shutdown은 Application이 인지가능한 Shutdown이라는 점에서 여러 장점을 가질 것 같습니다.
제가 만난 이슈 외에도 Graceful Shutdown 관련한 다른 이슈나 해결방법이 있다면 공유 부탁드립니다!
'Cloud' 카테고리의 다른 글
CKA 후기 - 2020년 12월 (2) | 2020.12.19 |
---|---|
Secure Volume Hot-plugging for Containers (0) | 2020.12.19 |