raphaelIl / study

TIL
0 stars 0 forks source link

graceful shutdown #22

Open raphaelIl opened 2 years ago

raphaelIl commented 2 years ago

TL;DR

  1. 대부분 프레임워크에선 signal handler를 지원한다. (SpringBoot 2.3 >= ver)
  2. 지원하지 않는 버전(레거시)이라면 app내에 따로 컨트롤러를 만들거나 플러그인 붙이거나 해야하는데 덜 귀찮은 방법은 Container Hook`s PreStop을 이용하자.

목표

이미 들어온 트래픽이 있는 와중에 새로운 배포프로세스가 실행될때 우아한 종료는 어떻게 될런지 정리해본다.

Kubernetes의 Pod Termination 과정은 요약하면

image

  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 삭제.

Config

Application

Enable Graceful shutdown

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s # Default

server:
  shutdown: graceful

Actuator Shutdown API

management:
  endpoints:
    web:
      exposure:
        include: "*" # test로 모든 엔드포인트 노출
  endpoint:
    shutdown:
      enabled: true

Kubernetes

preStop

shutdown api는 get이 아닌 post를 사용해야 한다.

      containers:
        - name: blahblah
          ...
          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - /bin/sleep 10 && /usr/bin/curl -X POST http://localhost:28080/actuator/shutdown

위와 아래의 차이는 생각보다 컸다.

          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "/bin/sleep 15"]

main process는 왜 SIGTERM을 감지하지 못하나

갑자기 pid를 왜 언급하냐면 container 레벨에서 pid는 1번이여야한다. graceful 설정을 했음에도 감지하지 못하는 이유는 컨테이너 pid1(main process)가 was 프로세스가 아니라 컨테이너 프로세스이기 때문이다

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 16:05 ?        00:00:00 /bin/bash /usr/local/bin/entrypoint.sh
root         7     1 21 16:05 ?        00:01:21 java -jar -Dspring.profiles.active=dev,perf -Djava.awt.headless=true -Duser.timezone=Asia/Seoul -javaagent:/usr/local/bin/dd-java-agent.jar -XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=50 /usr/local/bin/cloud-application.jar

해결책은 child process로 trap 해야할 signal을 전달하거나 구조를 변경하거나.. SIGTERM을 catch해서 처리하는게 근본적인 해결방법이라 생각되나 그러기엔 배포 파이프라인에서 변경해야할게 너무 많았다.

dockerfile을 변경하면 된다, 왜 이걸 하기 싫었나면 레거시 버전에서 잘될것인지 확인해야 했는데 생각보다 쉽게 확인할 수 있었다.

참고 사항

raphaelIl commented 2 years ago
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.'.format(signum))
    time.sleep(1)
    self.kill_now = True

if __name__ == '__main__':
  killer = GracefulKiller()
  t = 0;
  while True:
    time.sleep(1)
    t = t+1
    print(str(t) + "sec")
    if killer.kill_now:
      break

  print("graceful shutdown Done")
raphaelIl commented 2 years ago

docker-compose kubernetes에서 어디서 sigterm을 날리는지 확인할것

raphaelIl commented 1 year ago

dumb_init을 이용하면 signal을 trap하여 하위 프로세스에게 전달할 수 있다.