Kubernetes 환경 서비스 무중단 배포
개요
사내에서 팀원들이 사용하는 웹사이트를 운영 중에 있다. 혼자서 개발하고 배포하고 하는데 이상하게 배포할 때마다 1~2초 정도 503 Error가 발생한다. 분명 Deployment로 롤링업데이트를 진행한다고 이해하고 있었는데 이상하다. 그렇게 무중단 배포가 아닌 채로 운영을 하다가 아래 카카오 기술 블로그의 글을 읽었다.
무중단 배포의 Key는 Pod의 readinessProbe이다. 배포를 막 마치고 pod가 올라올 때 애플리케이션은 일시적으로 트래픽을 처리할 수 없을 수 있다. 애플리케이션이 대용량 데이터 또는 구성 파일을 로드해야 하거나 시작 후 외부 서비스에 의존해야 할 수 있는 상황도 있을것이다. 이러한 상황에는 애플리케이션이 서비스에 등록돼도 정상적으로 운영되지 않는다.
이러한 경우 애플리케이션이 정상적으로 활성화가 될 때까지 기다려야 하는데 Kubernetes는 이러한 상황을 감지하고 완화하기 위해 redinessProbe를 제공한다. 준비되지 않았다고 보고하는 컨테이너가 있는 Pod는 service를 통해 트래픽을 수신하지 않도록 구성한다.
Rediness Probe
주로, Healthcheck 응답용 페이지를 생성해두고 HTTP 로그 페이지에 접속해 상태코드가 200으로 돌아오면 요청을 받는 형식으로 설정한다.
...생략
readinessProbe:
httpGet:
port: 80
path: /health
initialDelaySeconds: 15
periodSeconds: 30
...생략
Deployment yaml 파일의 spec.container.readinessProbe 위치에 넣어주면 된다. initialDelaySeconds와 periodSeconds 값을 보자. 파드 동작 후 15 초 부터 (initialDelaySeconds) 최초 헬스체크가 시작되며 실패 시 30초 간격 (periodSeconds)으로 헬스체크를 반복한다. HealthCheck에 실패 시 해당 Pod는 서비스에서 제외된다.
kube-probe를 수신받을 때 부터 Pod는 서비스에 등록된다. 이제 무중단 배포가 완료된 걸까? 확인해 보자.
Rediness Probe 결과
vegta 라는 Tool을 사용하여, 배포 도중 반환하는 Status Code를 확인할 수 있다. 카카오 글을 읽고 처음 사용해 봤는데 시각적으로도 보기 편하고 그래프로도 출력 가능한 것 같다.
vegeta report results.*
Requests [total, rate, throughput] 693, 10.01, 8.35
Duration [total, attack, wait] 1m9s, 1m9s, 7.243ms
Latencies [min, mean, 50, 90, 95, 99, max] 5.439ms, 1.468s, 7.017ms, 10.011s, 10.013s, 10.02s, 10.074s
Bytes In [total, mean] 566930, 818.08
Bytes Out [total, mean] 0, 0.00
Success [ratio] 83.41%
Status Codes [code:count] 200:578 502:24 504:91
Error Set:
502 Bad Gateway
504 Gateway Time-out
Readiness Probe를 적용하기 전 테스트 결과이다. 배포 중 502 Bad Gateway와 504 Gateway Time-out 에러가 발생한다. 504는 ALB의 유휴 제한 시간으로 발생한 듯하다.
vegeta report results.*
Requests [total, rate, throughput] 1524, 10.01, 9.09
Duration [total, attack, wait] 2m32s, 2m32s, 7.199ms
Latencies [min, mean, 50, 90, 95, 99, max] 5.157ms, 809.236ms, 7.257ms, 172.267ms, 10.006s, 10.008s, 10.013s
Bytes In [total, mean] 1340723, 879.74
Bytes Out [total, mean] 0, 0.00
Success [ratio] 90.88%
Status Codes [code:count] 200:1385 502:30 504:109
Error Set:
502 Bad Gateway
504 Gateway Time-out
Readiness Probe를 적용한 뒤 테스트 결과이다. Success의 비율은 올라간 것 같지만, 여전히 502, 504의 반환 값 개수는 비슷하다. Readiness Probe를 설정하면 배포되는 데 시간이 오래 걸린다. 서비스가 준비되면 서비스에 반영하기에 200 값은 증가할 수밖에 없다. 반복된 테스트 끝에 나는 신규 Pod가 올라가고 대체되는 Pod가 Terminate 될 때 502 값을 반환하는 것을 알 수 있었다.
preStop과 terminationGracePeriodSeconds
파드는 종료될 때 SIGTERM, SIGKILL 처리와 서비스 제외 처리가 비동기로 이루어진다. 즉, 서비스가 분리되기 전에 파드가 클라이언트 요청에 정상적으로 응답할 수 없는 상태가 될 수 있다는 것이다.
그렇다면, SIGTERM 처리 이전에 서비스와 분리할 수 있는 방법은 없을까? preStop 처리를 사용하여 SIGTERM처리 전에 설정한 명령을 실행할 수 있다. 컨테이너 설정 내에서 .lifecycle.preStop.exec.command로 sleep 명령어를 실행하여 서비스 분리가 먼저 발생하게 만든다.
... 생략
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 40
... 생략
이제 다시 테스트를 해보자. 40초 동안 sleep명령어를 실행시키고 그 안에 서비스를 분리하게 만든다. 서비스마다 종료를 처리하는 시간이 다르므로 계속적으로 시간을 바꿔보면서 테스트해보는 것이 좋다.
parksanghyeon@bagsanghyeon-ui-iMac ~ % vegeta report results.*
Requests [total, rate, throughput] 1634, 10.01, 9.46
Duration [total, attack, wait] 2m43s, 2m43s, 7.493ms
Latencies [min, mean, 50, 90, 95, 99, max] 5.639ms, 481.059ms, 7.229ms, 13.601ms, 2.923s, 10.007s, 10.358s
Bytes In [total, mean] 1487013, 910.04
Bytes Out [total, mean] 0, 0.00
Success [ratio] 94.55%
Status Codes [code:count] 200:1545 502:21 504:68
Error Set:
504 Gateway Time-out
502 Bad Gateway
확실히 Success 비율이 높아졌다. 하지만 여전히 Error는 발생한다. 컨테이너 설정 부분에 terminationGracePeriodSeconds가 30으로 기본 설정되어 있는 것을 볼 수 있다. 이는 prestop과 SIGTERM을 기다려 주지 않고 병렬로 처리되어 30초가 지나도록 파드가 실행 중일 경우에는 SIGKILL을 전송하여 파드를 강제 종료시킨다. 이를 preStop 커맨드인 sleep 시간보다 긴 60초로 잡아주었다.
parksanghyeon@bagsanghyeon-ui-iMac ~ % vegeta report results.*
Requests [total, rate, throughput] 2082, 10.00, 10.00
Duration [total, attack, wait] 3m28s, 3m28s, 7.557ms
Latencies [min, mean, 50, 90, 95, 99, max] 5.285ms, 12.55ms, 7.051ms, 8.985ms, 31.752ms, 142.239ms, 412.963ms
Bytes In [total, mean] 1988310, 955.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:2082
Error Set:
완벽하다 원하는 결과가 드디어 나왔다. readinessProbe부터 preStop, terminationGracePeriodSeconds 모두 파드 라이프사이클과 연관이 있었으며, 우리는 서비스마다 이를 테스트하여 무중단 배포를 설정하도록 사전에 구성해야만 한다.
spec:
containers:
- image: ***
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 40
name: sre-cont
ports:
- containerPort: 80
protocol: TCP
readinessProbe:
httpGet:
path: /health
port: 80
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 30
terminationGracePeriodSeconds: 60
내 서비스 POD의 설정 값이다. 참고하여 적용하는 것도 괜찮은 방법이다. terminationGracePeriodSeconds, preStop, readinessProbe 값을 참고하면 된다.