본문 바로가기

Cloud

Kubernetes Terminating 이슈

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중인 상황입니다.

현업에서 다음과 같은 문제 상황을 겪어봤습니다.

  1. Stateful Application의 Replica 생성 (Node 장애 혹은 서비스 업데이트)시, 기존 Process로 인한 Split brain 현상 야기

  2. Rolling Update로 서비스를 업데이트시, SIGKILL에 의해 처리 중인 Request에 대한 Response 실패

  3. 즉각 적인 Pod Termination을 기대하는 경우, Pod Graceful Termination Period (30초)로 인한 Termination 지연

  4. 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 과정은 요약하면 다음과 같습니다.

  1. Pod 삭제 요청 (Grace Period = 30s)

  2. Pod 상태 변경 (Running -> Terminating)

  3. 2번과 동시에 Pod의 preStop hook 발생 및 Container 내 1번 프로세스에 SIGTERM 시그널 발생

  4. 2번과 동시에 Service Endpoint에서 Pod 삭제 -> Service 주소로 Request할 경우 해당 Pod으로 Request가 전달되지 않음.

  5. 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를 수정하는 것입니다.

  1. Dockerfile에서 실행되는 프로세스는 CMD 대신 ENTRYPOINT를 사용

  2. 스크립트 내에서는 직접 Executable 실행하지 않고 exec을 통해 "Fork없이" 현재 프로세스에서 실행되도록 변경.

  3. 스크립트 내에서 User를 변경해야 할 경우, sudo 커맨드 혹은 su 커맨드 대신 gosu 사용

  4. 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
  5. 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