10. Advanced POD Scheduling
이번 장은 파드 스케줄링을 보다 세밀하게 하는 방법을 알아봅니다.
주요 내용
- 서비스 안정성을 위하여 파드 간 실행 가능한 노드를 서로 분리하고 동일한 애플리케이션 파드가 서로 다른 노드에 실행하는 Advanced POD Scheduling 기능을 알아봅니다.
실습 과제
- Taint, Tolerations, Node Affinity 설정으로 특정한 노드에 파드를 실행합니다.
- Pod Anti-Affinity로 동일한 노드에 파드가 실행되지 않도록 합니다.
깃헙 링크
- https://github.com/junghoon2/k8s-class/tree/main/tolerations
- https://github.com/junghoon2/k8s-class/tree/main/affinity
쿠버네티스의 기본 스케줄링 정책은 클러스터의 리소스 활용을 최대화하고, 파드의 가용성을 보장하는 데에 중점을 둡니다. 특별한 요구사항이 없는 경우, Kubernetes는 기본 스케줄링 정책을 사용하여 파드를 임의의 적절한 노드에 자동으로 배치합니다. 스케줄링 기능은 컨트롤 플레인의 스케줄러(Scheduler) 파드가 담당합니다. 스케줄러 파드는 클러스터에서 새로운 파드(Pod)를 어떤 노드(Node)에 배치할지 결정합니다.
EKS는 컨트롤 플레인을 AWS에서 관리하여 Scheduler 파드를 확인할 수 없습니다. 하지만 온프레미스 네이티브 쿠버네티스 환경은 스케줄러 파드를 아래와 같이 확인할 수 있습니다.
$ (⎈ |alooo:kube-system) k get pod --selector component=kube-scheduler -o wide -n kube-system NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kube-scheduler-master1 1/1 Running 61 (12h ago) 126d 192.168.1.101 master1 <none> <none> kube-scheduler-master2 1/1 Running 87 (12h ago) 126d 192.168.1.102 master2 <none> <none> kube-scheduler-master3 1/1 Running 96 (12h ago) 126d 192.168.1.103 master3 <none> <none>
–selector 옵션을 이용하면 특정 Label을 가지는 파드만 지정하여 나타냅니다. 스케줄러는 파드를 배치할 때 노드가 할당 가능한 전체 자원 용량(메모리, CPU 용량 등)과 파드의 자원 요청량(request)을 고려하여 가장 적절한 노드를 선택합니다. 또한 노드의 실행 가능 상태(Healthiness)를 확인하여 이상이 있는 노드에는 파드를 실행하지 않습니다.
주의할 사항은 파드의 자원 요청량은 현재 파드의 실제 자원 사용량이 아닌 매니페스트에 명시한 파드의 자원 요청(request) 용량을 기준입니다. 실제 사용량이 높은데 자원 요청이 낮거나 혹은 사용량이 낮은데 자원 요청은 높은 경우가 있을 수 있습니다. 그러면 노드의 사용 가능한 자원이 부족하여 파드가 OOM(Out Of Memory) 장애가 발생할 수 있습니다. 실제 사용량에 근거하여 적절한 Request 설정이 필요합니다.
쿠버네티스의 ‘Advanced Pod Scheduling’은 파드를 더욱 세부적으로 스케줄링하는 방법을 제공합니다. 기본적으로 쿠버네티스는 위와 같이 파드를 클러스터 내의 어떤 노드에도 배치할 수 있도록 자유롭게 스케줄링합니다. 그러나 특정 상황에서는 파드를 특정 노드에 배치해야 합니다. 이런 경우에 Advanced Pod Scheduling을 사용하여 더욱 세밀한 스케줄링을 제어할 수 있습니다.
1. Taint, Tolerations, Node Affinity 설정
필자는 서비스와 관계된 파드와 실제 서비스와는 관계 없지만 공통 인프라 운영에 관련된 파드, 즉 모니터링, 로그, ArgoCD 등의 파드를 구분하여 서로 다른 노드 그룹에서 실행합니다. 로그 검색 및 모니터링 용도의 파드가 가끔 파드의 자원을 많이 하는데 이 때 실 서비스와 관련된 파드에 영향을 미치지 않기 위해서입니다. 이처럼 명시적으로 특정 파드를 서로 분리하는 경우에 사용하는 옵션이 ‘taint’와 ‘toleration’ 입니다.
‘Taint’(감염) 설정은 특정 노드에 특별한 제약사항을 부여합니다. 노드에 테인트를 설정하면 해당 노드에 파드가 스케줄링되기 전에 테인트와 일치하는 ‘Toleration’(용인, 관용)이 있는 파드만 해당 노드에 파드를 실행됩니다. 이를 통해 특정 노드를 특별한 용도로 사용하거나, 특정 파드를 특정 노드에만 스케줄링하도록 할 수 있습니다.
‘Taint’와 ‘Toleration’ 차이점은 테인트 설정은 노드에 설정하고 톨러레이션 설정은 파드에 설정하는 것 입니다. Taint를 노드에 설정하고 해당 Taint에 해당하는 Toleration 설정을 파드에 지정하면 해당 노드에 다른 파드는 스케줄링되지 않고 Toleration 설정을 가진 파드만 실행할 수 있습니다.
파드가 특정 노드를 선택하여 스케줄링 하려면 ‘nodeSelector(선택)’ 혹은 ‘nodeAffinity(유사, 친근감)’ 설정을 이용합니다. 실무에서 카펜터 Provisioner 설전에 아래와 같이 Taint를 설정하여 파드를 세밀하게 스케줄링합니다. 카펜터 Provisioner 매니페스트 파일에 Taint 설정을 추가하는 예시입니다.
apiVersion: karpenter.sh/v1alpha5 kind: Provisioner metadata: name: webrtc namespace: karpenter spec: providerRef: name: default taints: - effect: NoSchedule key: node value: webrtc labels: node: webrtc (생략)
. spec.taints
Taint 설정은 Key-Value 형식입니다. Key-Value는 임의로 지정할 수 있습니다. 필자는 Key-Value를 node:webrtc로 지정하였습니다. ‘effect: NoSchedule’을 지정하여 Toleration이 node:webrtc를 가지지 않는 파드는 스케줄링되지 못하도록 하였습니다.
다음으로 Taint 설정을 가지는 노드에 파드가 실행하도록 파드 설정에 Toleration과 nodeAffinity 설정을 추가합니다. Toleration은 Taint 설정을 가지는 노드에 배포할 수 있도록 하는 옵션입니다. ‘nodeAffinity’는 파드(Pod)를 특정 노드(Node) 또는 특정 노드 그룹에 스케줄링하는데 사용합니다. nodeAffinity를 사용하면 파드를 특정 노드에 스케줄링할 수 있습니다. nodeSelector 보다 유연한 조건 설정이 가능하고 podAffinity와 유사한 문법이라 nodeSelector 대신 nodeAffinity 설정을 많이 사용합니다. Toleration 설정만 있고 ‘nodeAffinity’가 없으면 파드가 임의의 노드에 스케줄링 될 수 있어 ‘nodeAffinity’ 설정을 톨러레이션 설정과 함께 추가합니다.
‘nodeAffinity’는 노드의 레이블을 기반으로 하며, 파드의 spec 필드에 affinity 블록을 추가하여 정의합니다. 노드 어피니티는 다음과 같은 2가지 옵션을 가질 수 있습니다.
. RequiredDuringSchedulingIgnoredDuringExecution
이 타입은 파드가 반드시(Required) 특정 노드 또는 노드 그룹에 스케줄링되어야 하며, 해당 조건을 만족하지 않으면 파드를 스케줄링하지 않습니다. 단, 이미 해당 노드에 스케줄링되면 노드 어피니티 조건이 변경되어도 파드는 다른 노드로 옮겨지지 않습니다.(Ignored)
. PreferredDuringSchedulingIgnoredDuringExecution
파드가 특정 노드 또는 노드 그룹에 스케줄링되길 선호(Preferred) 하지만, 만족하는 노드가 없는 경우 조건을 만족하지 않아도 파드를 다른 노드에 스케줄링할 수 있습니다. 노드 어피니티가 선호되는 조건이지만 강제성은 없습니다.
그럼 실습으로 알아봅니다.
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-hello namespace: default labels: app: nginx spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginxdemos/hello tolerations: - key: "node" value: "webrtc" effect: "NoSchedule" affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node operator: In values: - webrtc
. tolerations
Taint 설정된 노드에 스케줄링 되도록 Tolerations 설정을 추가합니다. ‘node:webrtc’ 설정으로 ‘node:webrtc’ Taint 설정을 가지는 노드에 배포할 수 있습니다.
. nodeAffinity
레이블이 node:webrtc를 가지는 노드에만 파드를 배포하도록 nodeAffinity 설정을 합니다. 파드가 다른 노드가 아닌 특정 노드에만 스케줄링 되도록 nodeAffinity의 ‘requiredDuringSchedulingIgnoredDuringExecution’ 설정을 추가하였습니다.
그럼, provisioner와 파드를 실행합니다.
$ (⎈ |switch-singapore-test:karpenter) k apply -f taint-provisioner.yaml provisioner.karpenter.sh/webrtc created $ (⎈ |switch-singapore-test:default) k apply -f tolerations-deploy.yml deployment.apps/nginx-hello created
처음 nginx-hello 파드의 상태는 ‘pending’입니다. 왜 그럴까요?
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-hello-558fcbd7bb-rffrz 0/1 Pending 0 3s <none> <none> <none> <none> nginx-hello-558fcbd7bb-x9xdt 0/1 Pending 0 3s <none> <none> <none> <none>
nodeAffinity의 node:webrtc Label을 가지는 노드가 없기 때문입니다. 카펜터가 아직 새로운 노드를 배포하기 전입니다. 조금 기다리면 카펜터 Provisioner가 node:webrtc Label을 가지는 노드를 배포합니다.
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-hello-558fcbd7bb-rffrz 1/1 Running 0 17m 10.110.14.130 ip-10-110-11-102.ap-southeast-1.compute.internal <none> <none> nginx-hello-558fcbd7bb-x9xdt 1/1 Running 0 17m 10.110.2.0 ip-10-110-11-102.ap-southeast-1.compute.internal <none> <none>
이제 nginx-hello 파드가 실행됩니다. 2개의 파드가 동일하게 ip-10-110-11-102-* 호스트네임을 가지는 노드에 실행되었습니다. 해당 노드만 node:webrtc Label을 포함하기 때문입니다.
다음은 임의의 파드를 실행하여 Taint 설정이 된 노드에 파드가 스케줄링 되는지 확인합니다.
$ (⎈ |switch-singapore-test:default) k create deployment nginx --image nginx --replicas 10 deployment.apps/nginx created
Nginx 이미지를 가지는 Nginx 파드 10개를 실행하였습니다.
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-748c667d99-26q6g 1/1 Running 0 51s 10.110.22.225 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-6b977 1/1 Running 0 51s 10.110.28.221 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-85bbt 1/1 Running 0 51s 10.110.19.155 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-8679g 1/1 Running 0 51s 10.110.20.120 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-8hnhv 1/1 Running 0 51s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-cg4w2 1/1 Running 0 51s 10.110.22.21 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-k99b5 1/1 Running 0 51s 10.110.44.197 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-pptxm 1/1 Running 0 51s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-rnk2s 1/1 Running 0 51s 10.110.39.167 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> nginx-748c667d99-ttmhp 1/1 Running 0 51s 10.110.45.96 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> nginx-hello-558fcbd7bb-rffrz 1/1 Running 0 19m 10.110.14.130 ip-10-110-11-102.ap-southeast-1.compute.internal <none> <none> nginx-hello-558fcbd7bb-x9xdt 1/1 Running 0 19m 10.110.2.0 ip-10-110-11-102.ap-southeast-1.compute.internal <none> <none>
10개의 파드를 실행하였는데 파드는 모두 ip-10-110-11-102-* 이 외의 노드에 실행됩니다. Nginx 파드는 Toleration 설정이 없어 Taint 설정이 되어 있는 ip-10-110-11-102-* 노드에 배포될 수 없기 때문입니다.
이처럼 Taint, Toleration, Node Affinity 설정으로 특정 노드 그룹에 필요한 파드만 실행할 수 있습니다. GPU 노드를 사용하거나, 메모리 또는 CPU를 많이 사용하는 파드는 특정 인스턴스 타입(R or C Type)에 배포하여 노드 비용을 절감하는 용도로 사용할 수 있습니다.
실습을 종료하여 파드와 Provisioner 리소스를 삭제합니다.
$ (⎈ |switch-singapore-test:default) k delete deployments.apps nginx nginx-hello deployment.apps "nginx" deleted deployment.apps "nginx-hello" deleted $ (⎈ |switch-singapore-test:default) k delete provisioners.karpenter.sh webrtc provisioner.karpenter.sh "webrtc" deleted
2. Pod Anti-Affinity 설정
동일한 애플리케이션이 여러 개의 파드를 실행하는 경우 특정 노드에 해당 애플리케이션 파드가 중복으로 실행하면 해당 노드에 장애가 발생하면 동일한 서비스의 여러 파드가 종료하므로 장애가 발생할 수 있습니다. 노드 장애와 관련없이 카펜터가 비용 절감을 이유로 특정 노드를 통합(Consolidation)하는 경우 또한 애플리케이션의 모든 파드가 동일 노드에 실행 중이면 서비스가 중지될 수 있습니다.
이러한 현상을 해결하기 위하여 Pod Anti-Affinity(파드 안티-어피니티) 설정을 사용합니다. Pod Anti-Affinity 설정은 특정 레이블을 가진 파드가 같은 노드에 함께 스케줄링되는 것을 방지합니다. 즉, 파드 안티-어피니티를 사용하면 특정 노드에 동시에 여러 파드가 위치하지 않도록 파드를 분산 배치합니다. 이를 통해 서비스의 가용성과 안정성을 향상시킬 수 있습니다. Affinity가 ‘친밀감’ 등의 뜻을 가지므로 Anti-Affinity는 ‘친밀하지 않게’로 이해하면 좀 더 쉽습니다.
파드 안티-어피니티를 사용하는 주요 이유는 다음과 같습니다.
- 고가용성 보장: 서로 다른 노드에 파드를 분산 배치함으로써 노드 또는 랙 수준의 장애에도 파드가 계속 가용성을 유지할 수 있습니다.
- 성능 균형 조정: 동일한 노드에 여러 파드가 위치하면 노드의 리소스를 고르게 사용하기 어려울 수 있습니다. 파드 안티-어피니티를 사용하여 리소스 부하를 더욱 균형 있게 분산합니다.
- 각 파드의 안정성 확보: 서로 다른 파드를 동일한 노드에 스케줄링하면, 자원을 많이 사용하는 특정 파드들이 서로의 리소스 사용에 영향을 미칠 수 있습니다. 안티-어피니티를 사용하면 각 파드가 독립적으로 동작하도록 보장됩니다.
주로 MySQL, MariaDB, Redis 등의 데이터베이스 관련 파드에 필수로 사용합니다. 헬름으로 설치하면 기본 설정에 파드 안티-어피니티 설정이 포함됩니다.
실습으로 자세한 사항을 알아봅니다. 아파치 웹서버(httpd)를 배포합니다.
apiVersion: apps/v1 kind: Deployment metadata: name: httpd labels: app: httpd spec: replicas: 2 selector: matchLabels: app: httpd template: metadata: labels: app: httpd spec: containers: - name: httpd image: httpd affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - httpd topologyKey: kubernetes.io/hostname
. affinity
이전의 nodeAffinity와 동일하게 affinity 필드에 podAntiAffinity 옵션을 추가합니다.
. podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution
동일한 레이블을 가지는 파드는 반드시(required) 같은 노드에 실행되지 않도록 합니다.
. labelSelector
Affinity 적용할 파드을 레이블을 기준으로 선택합니다. 다양한 리소스에 레이블이 사용됩니다.
. topologyKey: kubernetes.io/hostname
파드가 실행되는 노드의 호스트네임을 기준으로 구분합니다. Topology 정보는 클러스터 내의 노드들의 물리적인 배치와 네트워크 연결성을 나타내는데 사용합니다. Topology를 AWS의 zone으로 지정하면 서로 다른 가용존(Availability Zone)에서 실행하도록 지정할 수 있습니다.
Affinity 속성을 가지는 파드를 배포합니다.
$ (⎈ |switch-singapore-test:default) k apply -f affinity-deploy.yml deployment.apps/httpd created
의도한대로 서로 다른 2개의 노드에 배포됩니다.
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES httpd-77d4584879-6l5bp 1/1 Running 0 20s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> httpd-77d4584879-r4l29 1/1 Running 0 20s 10.110.28.221 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none>
그럼, 1개의 파드를 추가하겠습니다. 어떻게 될까요?
현재의 노드는 2개이므로 3개의 파드를 실행할 수 없습니다. 파드는 같은 노드에 실행할 수 없도록 Anti-Affinity 설정이 되어 있습니다.
$ (⎈ |switch-singapore-test:default) k scale deployment httpd --replicas 3 deployment.apps/httpd scaled
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES httpd-77d4584879-6l5bp 1/1 Running 0 2m13s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> httpd-77d4584879-r4l29 1/1 Running 0 2m13s 10.110.28.221 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> httpd-77d4584879-rbchf 0/1 Pending 0 13s <none> <none> <none> <none>
마지막 파드의 상태는 ‘Pending’입니다. 자세한 메시지를 확인합니다.
$ (⎈ |switch-singapore-test:default) k describe pod httpd-77d4584879-rbchf (생략) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 35s default-scheduler 0/2 nodes are available: 2 node(s) didn't match pod anti-affinity rules. preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod..
현재 실행중인 노드는 podAntiAffinity 설정으로 파드가 실행되기 적합하지 않다는 메시지입니다. 파드가 ‘Pending’ 상태이면 카펜터가 해당 파드의 조건을 확인하여 새로운 노드를 실행합니다. 조금 기다리면 카펜터가 새로운 노드를 실행합니다.
$ (⎈ |switch-singapore-test:default) k get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES httpd-77d4584879-6l5bp 1/1 Running 0 8m27s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none> httpd-77d4584879-r4l29 1/1 Running 0 8m27s 10.110.28.221 ip-10-110-24-222.ap-southeast-1.compute.internal <none> <none> httpd-77d4584879-rbchf 1/1 Running 0 6m27s 10.110.47.139 ip-10-110-47-47.ap-southeast-1.compute.internal <none> <none>
이제 3개의 파드가 서로 다른 노드에 실행되었습니다.
eks-node-viewer로 확인하면 가장 마지막에 실행한 노드는 t3.micro, 가장 비용이 저렴한 노드를 실행하였습니다. 실행되는 httpd 파드의 자원 요청량(request)이 설정이 없어 가장 작은 노드에 실행할 수 있기 때문입니다.
참고로 ‘topologySpreadConstraints’ 설정을 사용하면 서로 다른 가용존(AZ)에 배치된 노드에 파드가 배포됩니다. 노드 뿐만 아니라 AZ의 가용성 보장이 필요하면 서로 다른 AZ에 배치된 노드에 배포됩니다. 아래 설정을 사용합니다.
topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: dive-backend-admin-api-prod
커스텀 헬름 차트로 애플리케이션을 배포하는 경우 Pod Anti-Affinity 대신 ‘topologySpreadConstraints’ 설정을 사용하면 동일하게 서로 다른 AZ에 배포된 노드에 파드를 배포 할 수 있습니다.
이제 실습이 종료되어 httpd 디플로이먼트를 삭제합니다.
$ (⎈ |switch-singapore-test:default) k delete deployments.apps httpd deployment.apps "httpd" deleted
이상으로 이번 장에서 파드의 스케줄링을 보다 세밀하게 할 수 있는 Taint, Toleration, Node/Pod Affinity 설정을 실습으로 알아보았습니다.
해당 기술 블로그에 질문이 있으시면 언제든지 문의해 주세요. 직접 답변해 드립니다.
k8sqna@jennifersoft.com
1. 보다 자세한 사항은 kubecost 장에서 실습으로 확인합니다.
2. ‘topologySpreadConstraints’ 설정을 사용하여 서로 다른 가용존(AZ)에 파드가 배포되도록 할 수도 있습니다. 노드 뿐만 아니라 AZ의 가용성을 보장하는 용도로 사용합니다.