Devops

EKS Kubernetes의 롤링 업데이트 시 일시적인 500 에러의 원인과 해결

Seungwoo Lee 2023. 11. 30. 23:08

 

 

EKS(Elastic Kubernetes Service)는 AWS에서 제공하는 Managed Kubernetes 서비스입니다.

 

EKS는 "aws-loadbalancer-controller"를 제공해 AWS ALB(Application Load Balancer)를 Ingress로 사용할 수 있게끔 하고 있는데요.

 

ALB 기반의 Ingress로 Deployment를 배포할시 AWS와 통합된 환경을 활용해 서비스를 노출시킬 수 있다는 장점이 존재합니다.

 

하지만 위와 같은 환경에서 롤링 업데이트 시 일시적으로 500 에러가 발생하는 현상을 종종 발견할 수 있는데요.

 

이번 포스팅에서는 이 500에러의 원인이 무엇인지, 해결 방법에는 무엇이 있는지 알아보도록 하겠습니다.

 

 

1. 증상

이슈가 되는 에러를 재구현해 어떠한 환경에서 해당 이슈가 발생하는지 알아보도록 하겠습니다.

 

재구현에 사용된 환경의 아키텍쳐는 다음과 같습니다.

 

 

 

위 다이어그램에서 볼 수 있듯이 실험 환경은 Rolling update를 수행할 Deployment와 이를 내부로 노출시킬 ClusterIP, 외부로 노출시킬 ALB 기반의 Ingress로 이루어져 있습니다. 

 

이 상태에서 Rolling update 중 500 에러가 발생하는 이슈를 재구현하기 위해 Deployment에서 Rolling update를 수행해보겠습니다.

 

이제 Rolling update 수행 시 관찰되는 현상을 환경별로 확인해보겠습니다.

  

1-1. Kubernetes

가장 먼저 Kubernetes 환경에서 Rolling update시 일어나는 현상을 살펴보겠습니다.

 

에러 재구현을 위해 Pod가 1개인 Nginx Deployment를 생성했습니다. 따라서 nginx Pod 1개가 Running 상태에 있는 것을 확인할 수 있습니다.

 

 

이 상태에서 Nginx Deployment에 Rolling update를 수행하겠습니다.

 

Rolling update 수행 후에는 아래와 같은 순서로 Pod의 생애주기가 순환하는 것을 확인할 수 있습니다.

 

New Pod Pending -> New Pod ContainerCreating -> New Pod Running -> Old Pod Terminating

 

 

여기서 Kubernetes에서는 Deployment가 새 Pod의 Running 상태를 확인한 후 기존 Pod를 Terminating한다는 것을 알 수 있습니다.

 

이는 Deployment가 Zero Downtime으로 새 버전의 Pod를 배포하기 위해 사용하는 방법으로, 현재까지는 새 버전을 배포하는 동안 아무 문제도 없어보입니다.

 

1-2. Client

다음으로 애플리케이션으로 접근하는 Client 입장에서 Rolling update시 볼 수 있는 현상을 확인해보겠습니다.

 

아래는 Nginx Deployment의 Rolling update 중 Clinet 단에서 기록한 GET 상태 코드 그래프입니다.

 

노란 선 : 200 OK

주황 선 : 500 Bad Gateway

 

 

그래프를 확인해보면 Rolling update 실행 후 대략 3초부터 15초까지 12초간 500 상태 코드를 반환하는 것을 볼 수 있습니다.

 

즉 Client 입장에서는 대략 12초 동안 서비스에 대한 Downtime이 발생했다는 것을 알 수 있습니다.

 

하지만 이전 Kuberntes 단에서 저희는 새 Pod를 배포할때 트래픽을 받을 준비가 되었다는 것을 확인한 후 기존 Pod를 제거하는 것으로 확인했는데요. 

 

이론상 Zero Downtime으로 새 버전이 배포되어야 하지만 분명히 Client는 Downtime을 겪고 있습니다.

 

이러한 현상이 왜 발생했는지 더 알아보도록 하겠습니다.

 

1-3. AWS ALB

이제는 Deployment를 외부로 노출하기 위한 Ingress를 구현하는 AWS ALB에서 Rolling update이 발생하는 현상을 확인해보겠습니다.

 

아래는 배포 상태에 따른 ALB의 Target Group 상태입니다.

 

Rolling update 실행 전

배포를 실행하지 않은 기본 상태에서는 ALB가 1개의 Target으로만 트래픽을 전달하며, Target은 정상적인 Healthy 상태를 보여주고 있습니다.

 

 

 

Rolling update 실행 직후

다음으로 Kubernetes에서 Rolling update를 실행한 직후에는 기존 Target이 Drainging되며, 배포되는 새 버전으로 Target이 Initial되는 것을 확인할 수 있습니다.

 

 

 

Rolling update 진행 중

Rolling update가 진행되고 있는 중에는 새 Pod가 Healthy 상태로 트래픽을 전달받고, 기존 Pod는 계속해서 Draining 상태를 유지해 남아있는 Connection을 처리합니다.

 

 

 

Rolling update 완료

Rolling update가 완료되면 Draining 중이던 기존 Pod로의 모든 연결이 종료되고 새 Pod만 Healthy 상태로 남아 트래픽을 전달받습니다.

 

 

 

여기까지 AWS ALB단에서 확인한 Rolling update시 나타나는 현상이었습니다.

 

 

2. 원인

지금까지 Kubernetes, Client, AWS ALB 각 3가지의 측면에서 Kubernetes Rolling update시 나타나는 현상을 확인해봤습니다.

 

지금까지의 현상을 분석해봤을때, 시간 순서에 따른 각 요소별 동작 타임라인을 정리하면 아래와 같습니다.

 

 

위 타임라인을 봤을때, Downtime은 기존 Pod가 AWS ALB에 의하여 Draining되고 새 Pod가 Initializing되는 순간에 발생하는 것을 확인할 수 있습니다. 

 

즉 Rolling update 중 Downtime이 발생하는 이유는 AWS ALB에 의하여 기존 Pod는 Draining 상태로 변경되어 새 트래픽을 받지 못하고, 새 Pod는 Initializing 상태로 변경되어 마찬가지로 새 트래픽을 받지 못하기 때문입니다. 

 

이 때문에 Kubernetes에서는 Deployment가 Zero Downtime을 보장하도록 Pod 생애주기를 조절했지만, AWS ALB의 Target 등록에 지연 시간이 발생함으로서 Downtime이 발생했던 것입니다.

 

AWS ALB에서 Connection Draining 동작 중에는 기존 Connection은 처리되지만 새 Connection은 들어올 수 없고, Initializing 중에는 어떤 Connection도 맺지 않습니다.

 

reference : https://aws.amazon.com/blogs/aws/elb-connection-draining-remove-instances-from-service-with-care/

 

 

3. 해결 방법

이 문제를 해결할 수 있는 방법은 Kubernetes의 preStop Hook 기능을 사용해 Terminating 시간을 지연하는 것입니다.

 

preStop Hook은 Pod의 Terminating 시작 시 Container에 보내는 SIGTERM 신호를 발생시키기 전에 실행하는 Hook을 말합니다.

 

이 preStop Hook을 이용해 AWS ALB가 충분한 Initializing 시간을 벌 수 있도록 SIGTERM의 발생 시기를 늦춤으로써 Terminating 시간을 늘리는 방법으로 문제를 해결 가능합니다.

 

 

preStop Hook은 아래와 같은 방식으로 적용할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
### deployment.yaml ###
...
containers:
  - name: nginx
    image: nginx
    lifecycle:
      preStop:
          exec:
            command: ["/bin/bash""-c""sleep 40"]
...
cs

 

위와 같이 preStop Hook을 적용할시 주의해야 할 점은, Sleep time + Pod Terminating timeterminationGracePeriodSeconds 값을 넘지 않도록 해야 한다는 것입니다.

 

terminationGracePeriodSeconds 값은 Pod의 Terminating이 수행되는 시간을 지정하는 속성이며, 지정된 값의 시간이 지나면 Container에 SIGKILL 신호를 발생시키기 떄문에 의도한 시간보다 빨리 Terminating될 수 있습니다.

 

 

terminationGracePeriodSeconds 속성의 기본 값은 30초로, 기본 값을 사용할시 Sleep time + Pod Terminating time이 30초를 넘는다면 이 값을 늘리도록 수정해야 합니다.

 

아래와 같이 해당 값을 늘릴 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
### deployment.yaml ###
containers:
- name: nginx
  image: nginx
  lifecycle:
    preStop:
      exec:
        command: ["/bin/bash""-c""sleep 40"]
        
# SIGKILL 신호를 받기까지 60초의 유예시간이 주어집니다.
# 기본값은 30초입니다.
terminationGracePeriodSeconds: 60
cs

 

최종적으로 이 방안을 적용할 시 아래와 같은 타임라인으로 동작하게 됩니다.

 

 

위 타임라인에서 볼 수 있듯이, preStop Hook을 수행하면 지정한 시간만큼 AWS ALB에서 새 Target을 Initializing하는 시간을 벌어주기 때문에 Downtime 없이 새 버전을 배포할 수 있습니다.

 

아래는 preStop Hook 적용 후 Rolling update시 Client단에서 측정한 GET 상태 코드 그래프입니다.

preStop Hook을 적용하기 전과 달리 500 에러가 거의 발생하지 않은 것을 확인할 수 있습니다.

 

이렇게 AWS ALB가 새 Target을 Initializing하는 시간을 벌어줌으로써 Zero Downtime에 근접한 배포를 수행할 수 있습니다.

 

 

4. 마치며

지금까지 Kubernetes 환경에서 AWS ALB Ingress를 기반으로 노출한 서비스를 Rolling update했을 시 나타나는 500 에러에 대한 증상과 원인, 그리고 해결방법을 알아봤습니다.

 

결론적으로 해당 이슈의 원인은 AWS ALB의 새 Target을 Initial하는데 발생하는 지연시간인 것으로 나타났습니다.

 

그리고 이 문제를 해결하기 위해 preStop Hook을 사용해 Initial시간을 보상하는 것으로 Downtime을 제거할 수 있다는 것을 알아봤습니다.

 

동일한 환경에서 Rolling update시 이와 같은 이슈를 겪는 분들께 이 포스팅이 도움이 되었으면 합니다.