Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

k8s Rolling update 무중단 배포 / 내장 톰캣 Graceful shutdown 동작 원리 #37

Closed
ecsimsw opened this issue May 15, 2024 · 0 comments

Comments

@ecsimsw
Copy link
Owner

ecsimsw commented May 15, 2024

k8s Rolling update 무중단 배포

배포 Down time

  • 서버가 운영되는 도중 배포시 Down time 이 발생하고 있다.
  • 배포는 Rolling update로, 새 버전의 파드가 생성되고 정상 운영되면 기존 버전의 파드가 다운되길 반복한다.
  • 서비스 운영 중 파드가 생성되고 제거되며 발생할 수 있는 Down time을 확인하고 해결한다.

문제 여지 1 : Container 의 삭제보다 IpTable update가 더 느린 경우

  • 파드가 삭제되면 Kublet은 Container를 종료하고, 동시에 Endpoint controller 는 IpTable routing rule 에서 해당 파드를 제거한다.
  • 만약 IpTable이 업데이트되기 이전에 Container가 먼저 삭제되면, 요청을 처리할 수 있는 Container가 존재하지 않는 문제가 발생한다.
spec:
  containers:
  - name: "example-container"
    image: "example-image"
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 10"]
  • preStop으로 container 종료 시그널 전 N초를 대기한다.
  • Iptable 업데이트보다 Container 종료가 더 느림을 보장하고, 요청이 해당 파드로 전달되는 것을 막는다.
  • Spring docs, kubernetes container-lifecycle

문제 여지 2 : 요청이 처리되는 중에 Container가 종료되는 경우

  • Kubelet 은 아래와 같은 흐름으로 Container 를 종료한다.
1. Kubelet -> Container 에 종료 시그널 (SIGTERM/15)
2. 유예 기간 동안 대기 (기본 30초)
3. Kubelet -> 유예 기간동안 정상 종료되지 않으면 강제 종료 시그널 (SIGKILL/9) 
  • 만약 Container 종료 시그널에 Spring boot가 바로 종료된다면 처리 중인 요청은 비정상 종료될 것이다.
  • Spring boot 가 처리 중인 요청까지는 정상 응답 후 종료될 수 있도록 Graceful shutdown 옵션을 추가한다.
  • 대신 Kubelet도 종료 시그널 이후 Container의 정상 종료를 무한히 기다리진 않는다.
  • 반드시 유예 대기 시간보다 Graceful shutdown 처리 시간이 작도록 한다.
  • 그 과정에서 IpTable 이 업데이트 되어 해당 Pod로 요청이 더 추가되진 않는다
  • 처리되고 있는 요청의 응답은 정상 처리된다.
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s

문제 여지 3 : 컨테이너에서 요청 처리가 준비되지 않았는데 Routing rule 에 추가되는 경우

  • Container 에서 요청을 처리할 준비가 안되었다면, 해당 Pod 로 요청이 전달되어선 안된다.
  • ReadinessProbe 으로 요청 처리 가능 여부를 확인 후에 IpTable에 추가될 수 있도록 한다.

테스트

  • 5분 동안 300 vUser로 API 요청을 반복한다.
  • 그 동안 Deployment restart 를 반복하며 파드가 생성되고 삭제되는 상황에서 비정상 응답이 존재하는지 확인한다.
  • 5번의 전체 파드가 업데이트, 약 8만건의 요청이 있었지만 응답 실패는 단 한건도 발생하지 않았다. 👍

내장 톰캣 Graceful shutdown 동작 원리

  • 종료 시그널이 오면 Web server manager 에서 웹 서버의 shutDownGracefully 메서드를 호출한다.
  • 이때 callback이 함께 전달되는데, 내부에서 비동기 작업이 수행되고 이를 끝 마치고 결과 반환을 위해 사용될 것임을 예상할 수 있다.
class WebServerManager {
    void shutDownGracefully(GracefulShutdownCallback callback) {
        this.webServer.shutDownGracefully(callback);
    }
}
  • shutDownGracefully 는 사용하는 내장 웹 서버 구현체에 따라 동작 방식이 다르고, 아래는 Tomcat 의 코드이다.
  • 스레드를 하나 생성하고, CountDownLatch와 함께 doShutdown() 를 실행한다.
public void shutDownGracefully(GracefulShutdownCallback callback) {
    logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
    CountDownLatch shutdownUnderway = new CountDownLatch(1);
    new Thread(() -> doShutdown(callback, shutdownUnderway), "tomcat-shutdown").start();
    try {
        shutdownUnderway.await();
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
}
  • doShutdown() 안에서는 Connectors 들을 모두 Close() 하고 넘겨 받은 CountDownLatch를 1 낮추는데, 초기화된 Count 값이 1이었기에 이로써 doShutdown()를 비동기 호출한 shutDownGracefully()는 종료될 수 있다.
  • 이로써 Tomcat 의 shutDownGracefully 는 Connectors 의 종료까지는 대기하되, 그 이후 작업들은 비동기로 남겨둘 수 있게 된다.
private void doShutdown(GracefulShutdownCallback callback, CountDownLatch shutdownUnderway) {
    try {
        List<Connector> connectors = getConnectors();
        connectors.forEach(this::close);
        shutdownUnderway.countDown();
        awaitInactiveOrAborted();
        if (this.aborted) {
            logger.info("Graceful shutdown aborted with one or more requests still active");
            callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
        }
        else {
            logger.info("Graceful shutdown complete");
            callback.shutdownComplete(GracefulShutdownResult.IDLE);
        }
    }
    finally {
        shutdownUnderway.countDown();
    }
}
  • Connector 들을 종료 후 처리 중인 요청이 더 없는지를 50ms 마다 확인하며 요청 처리가 마무리 되길 대기한다.
  • 그리고 모두 정상 처리 또는 Abort 되었음을 확인하면 맨 처음 전달받은 callback 을 통해 Shutdown 결과를 알린다.
private void awaitInactiveOrAborted() {
    try {
        for (Container host : this.tomcat.getEngine().findChildren()) {
            for (Container context : host.findChildren()) {
                while (!this.aborted && isActive(context)) {
                    Thread.sleep(50);
                }
            }
        }
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
}
  • 커넥션들 종료는 블록킹 (CountDownLatch - await) 로 처리 후 반환하고,
  • 남은 요청 처리 대기는 비동기로 작업 후에 callback 으로 결과를 알리는 패턴이 재밌어서 기록해본다.
@ecsimsw ecsimsw closed this as completed May 15, 2024
@ecsimsw ecsimsw changed the title 배포 Down time 확인 / Rolling update, Scale in, Scale out k8s Rolling update 무중단 배포 / 내장 톰캣 Graceful shutdown 동작 원리 May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant