Profile picture

[k8s] 쿠버네티스 스케줄러 (NodeName, NodeSelector, NodeAffinity)

JaehyoJJAng2023년 04월 18일

📕 머리말

image

특정한 노드 집합에서만 동작하거나 선호하도록 파드를 제한할 수 있다. 보통은 쿠버네티스 스케줄러가 자동으로 합리적인 배치를 수행하기 때문에 이러한 제약 조건을 설정할 필요가 없지만, 특정한 상황(특정 노드에 파드가 배포되어야 할 이유가 있다던지 등)이 있을 때 사용한다.


파드를 특정 노드에 배포하기(Affinity) 위한 제약조건으로는 여러가지 방법이 존재한다.

  • Node Labels에 매칭되는 nodeSelector 필드
  • Affinity와 Anti-Affinty
  • nodeName 필드
  • Pod topology 분재 제약 조건

📜 Node Labels

다른 쿠버네티스 오브젝트와 마찬가지로 노드도 Label을 가질 수 있고 수동으로 추가도 가능하다.
뿐만 아니라 클러스터의 모든 노드들은 생성 후 표준화된 Label이 자동으로 달리게 되는데, 아래처럼 master 노드를 describe 명령어로 확인해보면 알 수 있듯이 Node Label들이 kubernetes.io와 k8s.io 네임스페이스 아래에 정의 되어있다.


Annotations는 Label과 비슷하게 리소스의 metadata를 표시하는 방법이지만 Label은 리소스를 태깅하고 그룹하는데 쓰는데에 비해, Annotations는 조금 더 부가적인 설명(설명,버전 등)를 제공하는 느낌이라고 볼 수 있겠다.

$ kubectl describe node/k8s-master | grep -A14 'Name:'
Name:               k8s-master
Roles:              control-plane
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-master
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/control-plane=
                    node.kubernetes.io/exclude-from-external-load-balancers=
Annotations:        csi.volume.kubernetes.io/nodeid: {"csi.tigera.io":"k8s-master"}
                    kubeadm.alpha.kubernetes.io/cri-socket: unix:///var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    projectcalico.org/IPv4Address: 192.168.56.30/24
                    projectcalico.org/IPv4VXLANTunnelAddr: 20.108.82.192
                    volumes.kubernetes.io/controller-managed-attach-detach: true

📋 nodeSelector

위와 같은 Label을 Selector를 통해 지정하고 특정할 수 있다. 특정 오브젝트가 실행될 노드를 선택하기 위해 사용하는 방법 중 하나이며 가장 간단하면서도 추천하는 형태이다.
nodeSelector 필드를 추가하고 타겟으로 삼고싶은 노드가 갖고 있는 Node Label을 명시한다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
spec:
  containers:
    - name: container
      image: docker.io/nginx:latest
  nodeSelector:
    kubernetes.io/hostname: k8s-node1

위와 같이 key-value 쌍으로 라벨을 지정해주면 kubernetes.io/hostname이 k8s-node1인 노드에만 파드가 배포된다.

nodeSelector.kubernetes.io/hostanme 키는 노드의 Labels를 조회하면 확인 가능하다.

$ kubectl describe node/<node_이름> | grep -A4 "Labels"
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-node1
                    kubernetes.io/os=linux

📋 nodeName

nodeName은 Affinity랑 nodeSelector보다 더 직접적인 노드 선택 방법이다.

apiVersion: v1
kind: Pod
metadata:
  name: pod
spec:
  containers:
    - name: nginx
      image: docker.io/nginx:latest
  nodeName: k8s-node1

위 파드는 무조건 k8s-node1 이라는 노드에서만 실행된다.

  • 만약 명명된 노드가 없는 경우, 파드가 실행되지 않고 자동으로 삭제될 수 있다.
  • 만약 명명된 노드에 파드를 수용할 수 있는 리소스가 없는 경우 파드 생성이 실패된다. 그 이유는 다음과 같이 표시된다.
    • OutOfMemory, OutOfcpu
  • 클라우드 환경의 노드 이름은 항상 예측 가능하거나 안정적이지 않다.

📋 nodeAffinity

개념적으로 nodeSelector와 비슷하지만 선택 로직에 대한 조금 더 많은 제어권을 제공한다.

  • "소프트(soft)" 또는 "선호사항(preference)"을 나타내어 매치되는 노드를 찾지 못한 경우에도 파드를 스케줄링 할 수 있음
  • Pod 간 Affinity / Anti-Affinity는 다른 Pod의 레이블을 이용하여 해당 파드를 제한할 수 있게 해줌.

  • requiredDuringSchedulingIgnoredDuringExecution : 필수로 반드시 지켜야하는
    • 규칙이 만족되지 않으면 스케줄러가 파드를 스케줄링 X
    • nodeSelector와 유사하지만, 좀 더 표현적인 문법을 제공한다.
  • preferredDuringSchedulingIgnoredDuringExecution : 필수는 아니지만 가급적인
    • 스케줄러는 조건을 만족하는 노드를 가급적이면 찾으려고 노력.
    • 해당되는 노드가 없더라도, 다른 노드에 파드를 스케줄링한다.
  • Operator로 key 값이 values에 어떻게 포함되는지에 대한 다양한 연산 표현방식을 제공한다.
    • In, NotIn, Exists, DoesNotExist, Gt, Lt
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - k8s-node1
                - k8s-node2
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1
          preference:
            matchExpressions:
              - key: kubernetes.io/hostname
                operator: In
                values:
                  - k8s-node1
  containers:
    - name: container
      image: docker.io/nginx:latest

위 예시에서는 다음 규칙이 적용된다.

  • 노드는 키가 kubernetes.io/hostname인 레이블을 가지고 있어야 하며, 레이블의 값이 k8s-node1 또는 k8s-node2이어야 한다.
  • 키가 kubernetes.io/hostname이고 값이 k8s-node1인 레이블을 갖고 있는 노드를 선호한다.

🔺 Affinity weight (어피니티 가중치)

preferredDuringSchedulingIgnoredDuringExecution 어피니티 타입 인스턴스에 비해 1-100 범위의 weight를 명시할 수 있다. 선호하는 노드를 가중치로 표현하여 계산하며 스케줄러는 점수가 가장 높은 노드에 우선적으로 배포한다.

      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1
          preference:
            matchExpressions:
              - key: label-1
                operator: In
                values:
                  - key-1
        - weight: 50
          preference:
            matchExpressions:
              - key: label-2
                operator: In
                values:
                  - key-2

각각 label-1:key-1 레이블, label-2:key-2 레이블이 붙은 노드가 각각 있다면, 스케줄러는 각 노드의 weight를 확인한 뒤 해당 노드에 대한 다른 점수에 가중치를 더하고, 최종 점수가 가장 높은 노드에 해당 파드를 스케줄링한다.


📋 podAffinity

목표

[Required Pod Anti Affinity Test]
2개의 Worker Node가 존재할 경우 Deployment에서 replicas를 3으로 설정하여 3개의 Nginx Pod를
Required Pod Anti Affinity로 서로 다른 Worker Node에 배포되도록 했을 때 결과가 어떻게 나오는지 확인


[Preferred Pod Anti Affinity Test]
2개의 Worker Node가 존재할 경우 Deployment에서 replicas를 3으로 설정하여 3개의 MySQL Pod를
Preferred Pod Anti Affinity로 서로 다른 Worker Node에 배포되도록 했을 때 결과가 어떻게 나오는지 확인


[Preferred Pod Affinity Test]
MySQL Pod는 이미 생성됐던 Nginx Pod가 있는 노드에만 생성되도록 설정


1. podAffinity 설정된 Nginx Pod 생성

[예제 nginx-deployment.yaml]

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-affinity
  labels:
    app: nginx
spec:
  replicas: 3 # 3개의 파드 생성
  selector:
    matchLabels:
      app: nginx-pod
  template:
    metadata:
      labels:
        app: nginx-pod # 생성되는 Pod의 Label을 app=tomcat-pod로 설정
    spec:
      containers:
        - name: tomcat
          image: docker.io/nginx:latest
          ports:
            - containerPort: 80
              protocol: TCP
      affinity: # Affinity 사용
        # Anti Affinit로 서로 다른 Worker Node에 배포되도록 설정
        podAntiAffinity: # 종류는 Pod Anti Affinity로 설정하였음.
          requiredDuringSchedulingIgnoredDuringExecution: # Required Pod Anti Affinity로 설정
            - labelSelector:
                matchExpressions:
                  # podAntiAffinity를 적용할 Pod의 Key를 지정
                  # 해당 deployment로 생성될 Pod들이 서로 다른 Worker Node에 생성되어야 하므로
                  # Deployment에서 생성되는 Pod의 Label을 넣어줘야 함.
                  - key: app
                    values:
                      - nginx-pod # 마찬가지로 적용할 Pod의 value를 넣음.
                    operator: In
                # Pod Anti Affinity를 적용할 Worker Node 찾기
                # topologyKey는 Worker Node Label의 Key를 확인해서
                # 설정된 Key(kubernetes.io/hostname)이 Worker Node에 존재하면 Pod Anti Affinity가 적용된다.
                # 해당 key의 value 값은 확인 안함.
              topologyKey: kubernetes.io/hostname

[생성 결과]
podAntiAffinity 설정으로 인해 Pod가 서로 다른 Worker Node에 생성되는 것을 확인할 수 있음.
replicas을 3으로 설정해서 총 3개의 Pod가 생성되었는데 Worker Node가 2개뿐이므로 각각 k8s-node1, k8s-node2에 하나씩 생성되고
나머지 하나는 생성될 Worker Node가 없기 때문에 Pending 상태가 됨.

$ kubectl get pods
NAME                                   READY   STATUS    RESTARTS   AGE
deployment-affinity-6cb8c4fb84-cjk7n   1/1     Running   0          37m
deployment-affinity-6cb8c4fb84-dl6mc   1/1     Running   0          37m
deployment-affinity-6cb8c4fb84-zdnpw   0/1     Pending   0          37m

2. podAntiAffinity와 podAffinity 설정된 MySQL Pod 생성

[예제 mysql-deployment.yaml]

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-mysql
  labels:
    app: mysql
spec:
  replicas: 3 # 3개의 파드가 생성
  selector:
    matchLabels:
      app: mysql-pod
  template:
    metadata:
      labels: 
        app: mysql-pod
    spec:
      containers:
        - name: mysql
          image: docker.io/mysql:latest
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: secret
      affinity:
        # Anti Affinity로 서로 다른 Worker Node에 배포되도록 설정
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    values:
                      - mysql-pod
                    operator: In
              topologyKey: kubernetes.io/hostname
        # 상위에서 생성했던 nginx pod가 있는 worker node에만 mysql이 생성되도록 설정
        podAffinity: # 이전에 생성했던
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app # 상위에서 생성했던 nginx pod 의 Label 값을 넣어줌
                    values:
                      - nginx-pod
                    operator: In
              topologyKey: kubernetes.io/hostname

[생성 결과]
PodAntiAffinity 설정으로 인해
Pod가 서로 다른 Worker Node에 생성되는것을 확인할 수 있었음. replicas를 3으로 설정해서 총 3개의 Pod가 생성되었는데
Worker Node가 2개뿐이므로 각각 k8s-node1, k8s-node2에 하나씩 생성되었고
나머지 하나는 생성될 worker node가 없어서 pending 상태가 되었음.

$ kubectl get pods | grep 'mysql'
deployment-mysql-7c674b44dc-n8rg8      1/1     Running   0          38s
deployment-mysql-7c674b44dc-snnxp      1/1     Running   0          38s
deployment-mysql-7c674b44dc-tmnk6      0/1     Pending   0          38s

PodAffinity 설정으로 인해
nginx pod가 동작 중인 worker node에만
mysql Pod가 배포된 것을 확인할 수 있었다.

$ kubectl get pods -o wide | grep 'affinity'
deployment-affinity-6cb8c4fb84-8lh9c   1/1     Running   0          9m8s    20.111.156.102   k8s-node1   <none>           <none>
deployment-affinity-6cb8c4fb84-pcnkm   0/1     Pending   0          9m8s    <none>           <none>      <none>           <none>
deployment-affinity-6cb8c4fb84-tmqhp   1/1     Running   0          9m8s    20.109.131.63    k8s-node2   <none>           <none>

$ kubectl get pods -o wide | grep 'mysql'
deployment-mysql-7c674b44dc-n8rg8      1/1     Running   0          2m42s   20.111.156.104   k8s-node1   <none>           <none>
deployment-mysql-7c674b44dc-snnxp      1/1     Running   0          2m42s   20.109.131.2     k8s-node2   <none>           <none>
deployment-mysql-7c674b44dc-tmnk6      0/1     Pending   0          2m42s   <none>           <none>      <none>           <none>

3. nginx pod가 없는 상태에서 podAffinity 설정된 MySQL Pod 생성

[결과]
PodAffinity 설정으로 인해
nginx pod가 생성되어있는 Worker node가 없으므로
어떠한 Worker Node에도 MySQL Pod가 생성이 안됨.

$ kubectl get pods | grep 'mysql'
deployment-mysql-7c674b44dc-bg7nn   0/1     Pending   0          21s
deployment-mysql-7c674b44dc-fbt6t   0/1     Pending   0          21s
deployment-mysql-7c674b44dc-xn965   0/1     Pending   0          21s

Loading script...