서비스 트래픽이 증가하고 사용자가 늘어나면 가장 먼저 고민하게 되는 것이 바로 무중단 배포(Zero-Downtime Deployment) 이다.

간단한 기능 수정이나 핫픽스조차도 배포 순간에 서버가 잠깐이라도 멈추면 사용자 경험에 치명적인 영향을 준다.

실제로 우리는 핫픽스나 자잘한 기능 변경을 위해 하루에도 몇 번 씩 프론트/백엔드 배포를 하곤 하는데, 이때 사용자는 중단됐다고 느끼면 안 된다.

우리는 EKS 환경에서 백엔드를 운영하고 있고, Kubernetes 환경에서는 다행히 무중단 배포를 위한 메커니즘이 내장되어 있다.

이 글에서는 실제 우리 서비스에 적용했던 Kubernetes의 Rolling Update 전략과 Health Probe, 그리고 우리가 만든 간단한 Health API가 어떻게 무중단 배포를 이루는지 설명한다.


아래는 매우 간단한 Health Checking API이다.

Kotlin
@RestController
class HealthController {

    @GetMapping("/health")
    fun health(): ResponseEntity<String> {
        return ResponseEntity.ok("OK")
    }

}

응답은 문자열 "OK"뿐이지만, 이 API는 Pod가 정상인지 판단하는 기준점이 된다.

Kubernetes는 이 API를 기반으로 파드의 생존 여부를 판단한다
Deployment의 readinessProbe / livenessProbe가 이 엔드포인트를 사용한다.

  • readinessProbe
  • 이 Pod은 서비스 트래픽을 받을 준비가 되었는가?
  • livenessProbe
  • 이 파드는 내부적으로 문제가 없고 계속 살아 있는가?

⠀Kubernetes는 이 Probe를 기준으로 다음을 결정한다.

  • 준비되지 않은 Pod에는 트래픽을 보내지 않는다.
  • 오류가 반복되면 자동으로 Pod를 재시작한다.
  • Rolling Update 시 새 Pod가 완전히 준비되기 전까지 기존 Pod를 제거하지 않는다.

Health API가 정상적으로 작동하지 않으면, 무중단 배포는 불가능해진다.


만약 readinessProbe, livenessProbe 관련 설정이 없으면, 컨테이너의 상태가 Running이 되는 순간, 기존 Pod가 바로 종료되기 시작한다.

하지만 컨테이너가 Running인 것과 서비스가 Running인 것에는 차이가 있다.
컨테이너가 시작되고 서버가 실제로 실행되기 까지는 실제로 시간이 조금 걸리기 때문이다.

따라서 readinessProbe, livenessProbe 설정을 추가함으로써, 컨테이너가 돌아간다는 기준점을 서비스의 시작으로 설정하는 것이다.


Helm Chart의 Deployment 템플릿에서는 다음과 같이 Probe를 설정한다.

YAML
readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 3
  periodSeconds: 3
  timeoutSeconds: 1
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

이 구성이 하는 역할은 다음과 같다.

  • 새로운 Pod가 뜨면 /health가 성공을 반환할 때까지 서비스에 등록되지 않는다.
  • 기존 Pod는 새 Pod가 Ready 상태가 되기 전에는 제거되지 않는다.
  • 만약 Pod가 죽거나 장애 발생 시 자동으로 재시작된다.

Deployment의 기본 전략은 RollingUpdate이다.

YAML
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

이 설정은 다음을 의미한다.

  • maxUnavailable: 0
  • 서비스 가능한 Pod 수가 줄어들면 안 된다.
  • 즉, 기존 Pod이 죽기 전에 새 Pod가 반드시 준비되어 있어야 한다.
  • maxSurge: 1
  • 배포 시 항상 (기존 개수 + 1)개의 Pod가 생성된다.

즉, 항상 여유로운 Pod을 하나 더 띄운 다음, 준비가 되면 기존 파드를 하나씩 내려 서비스 중단을 막는다.

이것이 쿠버네티스에서 가장 널리 사용되는 무중단 배포 방식이고, 우리도 그렇게 사용하고 있다.


우리는 점진적으로 버전을 옮기는 방식으로 무중단 배포를 구현하지만, Blue–Green Deployment라고 새 버전이 완전히 준비되고나서 기존 버전을 내리는 방식을 채택하기도 한다.

또한 EKS에서는 Kubernetes의 Pod 종료 절차와 AWS Load Balancer의 연결 관리가 결합되어 약간의 프레임워크 단 설정만 추가하면 Load Balancer Graceful Shutdown 을 어렵지 않게 구현할 수 있다.

Load Balancer Graceful Shutdown란, Pod가 종료되는 시점에 그 Pod에서 처리되고 있던 요청을 즉시 중단하지 않고, 더 이상 트래픽을 해당 Pod로 보내지 않으면서 요청이 완료될 때까지 기다린 후 종료되는 것이다.


우리의 경우 Kubernetes의 기본적인 무중단 배포 메커니즘에 매우 얇은 로직만 추가해서 하루 수십 번의 배포에도 큰 API 장애 없이 서비스를 운영할 수 있었다.

무중단 배포의 본질은 사용자는 서비스가 배포되고 있다는 사실조차 몰라야 한다.

댓글 남기기

Dalmeng's Footprints에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기