개요

쿠버네티스 환경을 운영하다 보면 클러스터의 노드를 변경해야 하는 순간이 온다. 이럴 때 교체 대상이 되는 노드를 drain 한다. 

drain은 노드 위에서 운영되고 있는 Pod를 다른 노드로 재 배치하고 Schedule을 중지시킨다는 내용이다. 

하지만 drain을 무작정하게 되면 모든 파드가 동시에 재배치 되거나 헬스 체크를 통과하기 전에 다음 파드의 재배치가 시작되어 정상적인 파드가 하나도 없는 상태가 될 가능성이 있다. 이러한 현상을 예방해보자.

문제 상황

서비스되고 있는 Deployment가 드레인 상태의 노드에 스케줄링되어있을 경우



Node A 에서 drain을 하게 되면 service 1에 묶여있는 deployment는 동시에 재 배치되므로 이러한 경우에는 Node B와 Node C에 배치가 되고 헬스체크가 통과되기 전까지는 서비스가 불가한 상황이 된다. 

반면에 service 2에 묶여있는 deployment는 파드 중 하나는 계속해서 Node B에서 서비스가 운영 중이므로 서비스의 단절은 발생하지 않는다. 

즉, drain이 될 때 서비스가 가능한 최소 하나의 파드는 가지고 있어야 한다는 말이다.

PodDisruptionBudget

PodDisruptionBudget은 서비스(애플리케이션)의 고가용성을 유지하는 데 도움을 준다.

 

중단(disruption)

이 가이드는 고가용성 애플리케이션을 구성하려는 소유자와 파드에서 발생하는 장애 유형을 이해하기 원하는 애플리케이션 소유자를 위한 것이다. 또한 클러스터의 업그레이드와 오토스케일

kubernetes.io

아래와 같이 yaml 파일을 생성하여 PDB를 생성할 수 있다. maxUnavailable 는 서비스가 불가능한 pod의  최대 값을 지정한다. 

5개의 pod가 운영될 때 maxUnavailable이 1일 경우 drain Node를 하게 된다면 pod는 1개 씩 재배치되고 maxUnavailable 값 이상의 재배치는 제한된다.

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: pdb-srebgk
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: srebgk


$ kubectl get pdb
NAME         MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
pdb-srebgk   N/A             1                 1                     48m


Drain Node

$ kubectl drain ip-10-***-180-**.******.compute.internal --ignore-daemonsets


특정 노드를 drain 한 직후의 결과이다. 파란색으로 표시된 pod(PDB 적용)를 봤을 때 우선적으로 하나만 기존의 노드에서 Terminating 되고 있고 다른 노드로 Scheduling 되고 있는 것을 확인할 수 있다.


콘솔 화면에서는 어떨까? drain 한 노드에 위치된 파드들이 evicting 되고 있지만 그 중 두 개의 파드는 실패하게 된다. 

그 이유는 무엇일까. maxUnavailable을 1로 설정한 PDB가 5개로 운영되고 있는 srebgk 파드를 최대 1개의 불가능 상태만을 유지하기 때문이다. 즉, 4개의 Pod는 서비스 정상 상태를 유지 한다는 뜻이다.


극적인 결과를 보여주기 위해 maxUnavailable을 1로 잡았지만, 이런 식으로 설정을 하게 될 경우 스케줄링하는 데 시간이 오래 걸리기 때문에 적당한 비율 값을 설정하는 것을 추천한다. 

5개의 pod를 운영 중이라면 maxUnavailable 값은 3이 좋을 것 같다.

$ kubectl uncordon ip-10-***-180-**.******.compute.internal


Drain으로 Scheduling이 중단된 노드를 uncordon 명령어로 Schedule이 가능하게 복구하는 것도 중요하다. 만약 새로운 노드그룹으로 마이그레이션 중이었다면 uncordon은 필요 없다.

$ kubectl get pods -l app=srebgk -o wide

NAME                                 READY   STATUS    RESTARTS   AGE     IP               NODE                                                NOMINATED NODE   READINESS GATES
srebgk-deployment-575bc5bb86-2mgsf   1/1     Running   0          2m56s   10.***.241.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-nsbhh   1/1     Running   0          2m56s   10.***.249.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-rxrxm   1/1     Running   0          6d10h   10.***.244.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-vc4pm   1/1     Running   0          6d10h   10.***.252.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-wfz89   1/1     Running   0          6h28m   10.***.254.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>


무중단 POD 재배치

이번엔 PodDisruptionBudget에 따라 서비스에 영향도가 얼마나 큰 지, PDB를 이용하면 정말 서비스에 영향 없이 안전하게 재배치가 가능한지 확인해보겠다.

환경 구성

노드는 2대를 운영하고 있으며 가장 Worst case 상황인 한 노드에 모든 POD가 몰려있는 상황으로 테스트를 진행했다.

$ kubectl drain ip-10-***-254-***.******.compute.internal --ignore-daemonsets


$ kubectl get nodes

NAME                                                STATUS                     ROLES    AGE   VERSION
ip-10-***-180-***.******.compute.internal           Ready                      <none>   10d   v1.18.9-eks-d1db3c
ip-10-***-254-***.******.compute.internal           Ready,SchedulingDisabled   <none>   12d   v1.18.9-eks-d1db3c



PDB가 적용되지 않은 환경에서 drain 한 직후부터 파드의 응답 값 결과를 확인해 보면 약 5% 정도의 502, 504 Error가 발생한 것을 볼 수 있다. 

% 단위로 보면 적은 순단 현상이라고 생각할 수 있지만, 크리티컬 한 환경에서의 이 5%는 큰 문제를 초래할 수 있다.

% vegeta report results.* 
                                                        
Requests      [total, rate, throughput]         989, 10.01, 9.59
Duration      [total, attack, wait]             1m39s, 1m39s, 7.278ms
Latencies     [min, mean, 50, 90, 95, 99, max]  5.604ms, 428.506ms, 7.508ms, 38.366ms, 3.272s, 9.404s, 10.044s
Bytes In      [total, mean]                     910452, 920.58
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           95.85%
Status Codes  [code:count]                      200:948  502:30  504:11  
Error Set:
502 Bad Gateway
504 Gateway Timeout


PDB가 적용된 환경에서 drain 테스트를 진행하기 위해서 uncordon을 하여 스케줄링이 가능하도록 복구해 준다.

$ kubectl uncordon ip-10-***-254-***.******.compute.internal 
node/ip-10-***-254-***.******.compute.internal uncordoned


$ kubectl get pods -l app=srebgk -o wide

NAME                                 READY   STATUS    RESTARTS   AGE     IP               NODE                                              NOMINATED NODE   READINESS GATES
srebgk-deployment-575bc5bb86-d985r   1/1     Running   0          6m2s    10.***.179.***   ip-10-***-180-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-k42wc   1/1     Running   0          6m2s    10.***.185.***   ip-10-***-180-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-ls5vw   1/1     Running   0          6m2s    10.***.176.***   ip-10-***-180-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-p5qrw   1/1     Running   0          6m2s    10.***.185.***   ip-10-***-180-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-wwm75   1/1     Running   0          6m2s    10.***.185.***   ip-10-***-180-***.**************.compute.internal   <none>           <none>



drain을 하기 전 모든 pod는 10.***. 180.*** 노드에 쏠려 있는 것을 확인하자.

$ kubectl get pdb

NAME         MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
pdb-srebgk   N/A             3                 3                     27s


$ kubectl drain ip-10-***-180-***.**************.compute.internal --ignore-daemonsets


$ kubectl get pods -l app=srebgk -o wide

NAME                                 READY   STATUS    RESTARTS   AGE     IP               NODE                                                NOMINATED NODE   READINESS GATES
srebgk-deployment-575bc5bb86-5qnv6   1/1     Running   0          2m44s   10.***.***.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-7wqwq   1/1     Running   0          103s    10.***.***.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-9tfv9   1/1     Running   0          2m43s   10.***.***.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-dwpn5   1/1     Running   0          2m44s   10.***.***.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>
srebgk-deployment-575bc5bb86-m6sp6   1/1     Running   0          118s    10.***.***.***   ip-10-***-254-***.**************.compute.internal   <none>           <none>



파드는 10.***. 254.*** 노드로 재 배치된 것을 확인할 수 있다. 그렇다면 응답 값은 어떨까?

% vegeta report results.*                
                                         
Requests      [total, rate, throughput]         2109, 10.00, 10.00
Duration      [total, attack, wait]             3m31s, 3m31s, 8.103ms
Latencies     [min, mean, 50, 90, 95, 99, max]  6.149ms, 9.93ms, 8.087ms, 10.627ms, 22.259ms, 51.807ms, 153.928ms
Bytes In      [total, mean]                     2014095, 955.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:2109  
Error Set:


PDB가 없는 환경에서의 테스트와는 다르게 발생한 Error가 없으며 모두 200을 반환한 것을 확인할 수 있다. 

Pod Disruption Budget이 적용되어 있으면 노드 버전 업그레이드 혹은 커널 업데이트를 서비스 영향도 없이 진행할 수 있다는 것이다.

PodDisruptionBudget을 이용하면 kubernetes의 데이터 플레인 버전 업그레이드 또한 무중단으로 진행할 수 있을 것이다.