본문 바로가기
DevOps/Kubernetes

Kubernetes Scheduling - Node Affinity

by 비어원 2024. 5. 2.
728x90

Affinity를 사용하여 파드를 스케줄링할 노드를 찾을 때의 제약조건을 정의할 수 있다. Affinity를 사용하면 이전 시간에 언급한 nodeSelector보다 더 풍부한 방식으로 파드를 실행시키는 노드의 조건을 설정할 수 있다.

 

Affinity에는 크게 NodeAffinity와 Pod간 Affinity가 있다. 이번 글에서는 Node Affinity에 대해 알아보자.

NodeAffinity

Node Affinity는 node label를 기반으로 파드를 스케줄링할 수 있는 노드를 제한할 수 있다. Affinity는 파드의 Spec이기 때문에 Affinity를 사용하면 파드가 노드를 선택하는 개념으로 볼 수 있다. Scheduler에서는 NodeAffinity 플러그인에서 NodeAffinity에 대한 필터링 및 스코어링 기능을 구현한다. Affinity에는 두 가지 유형이 있다.

  • requiredDuringSchedulingIgnoredDuringExecution: 스케줄러는 규칙이 충족되지 않으면 파드를 스케줄링하지 않는다.
  • preferredDuringSchedulingIgnoredDuringExecution: 스케줄러는 해당 규칙이 충족되는 노드를 찾으려고 한다. 만약 일치하지 않는 노드가 없더라도 스케줄러는 파드를 스케줄링한다.

 

requiredDuringSchedulingIgnoredDuringExecution

 

requiredDuringSchedulingIgnoredDuringExecution 유형은 해당 규칙에 충족되는 노드가 있다면 파드를 해당 노드 중 하나로 스케줄링하고, 충족되는 노드가 없다면 파드를 스케줄링하지 않는 유형이다. 이 유형은 Filtering 에서 사용된다. 이 유형을 사용하면 nodeSelector와 거의 비슷한 방식으로 작동하기는 하지만 필터링 연산이 조금 더 풍부하다.

 

 

requiredDuringSchedulingIgnoredDuringExecution으로 이전 시간에서 nodeSelector로 파드를 스케줄링 한 것과 같이 nfs=true인 노드로만 스케줄링 하도록 구성한다면 다음과 같다.

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: nfs
            operator: In
            values:
            - 'true'
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
      - name: contents
        mountPath: /contents
  volumes:
    - name: contents
      persistentVolumeClaim:
        claimName: nginx-nfs-pvc

 

파드를 생성한 후 조회해보면 nfs=true인 노드로 스케줄링 된 것을 확인할 수 있다.

 

Operator

NodeSelector와 NodeAffinity의 가장 큰 차이점은 NodeAffinity는 operator를 지원한다는 점이다. NodeAffinity에서 지원하는 operator는 총 4가지로, In, NotIn, Exists, DoesNotExist이 있다.

 

  • In: 노드의 레이블 key에 대한 값이 values에서 지정한 값들 중 하나일 경우 해당 노드 선택
  • NotIn: 노드의 레이블 key에 대한 값이 values에서 지정한 값들 중 하나일 경우 해당 노드 제외
  • Exists: 노드의 레이블 key에 대한 값이 존재하는 경우 해당 노드 선택, 해당 operator를 사용하는 경우 values는 필요 없다.
  • DoesNotExists: 노드의 레이블 key에 대한 값이 존재하는 경우 해당 노드 제외, 해당 operator를 사용하는 경우 values는 필요 없다.

 

따지고보면 NodeSelector는 NodeAffinity에서의 values가 하나 뿐인In 연산자와 같다. 대신 NodeAffinity는 In 외의 연산자와 여러개의 values 값을 지정하여 노드를 선택할 수 있어서 NodeSelector보다는 더 풍부한 방식으로 노드를 선택할 수 있다.

 

간단한 예시로, 파드를 nfs 레이블이 없는 노드에만 배치하고 싶을 때는 다음과 같이 지정하면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test2
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: nfs
            operator: DoesNotExist
  containers:
  - name: nginx
    image: nginx

 

파드를 생성한 후 조회해보면 nfs 레이블이 없는 노드로 스케줄링 된 것을 확인할 수 있다.

 

Troubleshooting: 리소스가 부족한 경우

만약 nfs=true 인 노드에 파드에게 할당할 리소스가 부족하다면 어떻게 될까? 파드 스케줄링에서 필터링 단계 중, NodeResourcesFit이 있는데 이 단계에서는 파드에 정의된 resources.requests만큼의 리소스를 할당할 수 없는 노드는 파드 스케줄링 대상에서 제외시킨다.

 

실습을 위해 리소스를 충분히 많이 잡아먹는 파드를 하나 생성하고 나서 node-affinity-testresources.requests를 할당한 후 생성해보자.

apiVersion: v1
kind: Pod
metadata:
  name: resource-gangster
spec:
  nodeSelector:
    nfs: "true"
  containers:
  - name: nginx
    image: nginx
    resources:
      requests:
        cpu: '800m'
        memory: 2Gi
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: nfs
            operator: In
            values:
            - 'true'
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
      - name: contents
        mountPath: /contents
    resources:
      requests:
        cpu: 0.5
        memory: 512Mi
  volumes:
    - name: contents
      persistentVolumeClaim:
        claimName: nginx-nfs-pvc

 

파드를 생성해보면 Pending 상태로 머물러있는데, 이벤트를 조회해보면 다음과 같다.

 

대충 해석하자면, 노드 두 대 중 한 대는 Affinity에서 걸리고, 나머지 한 대는 Affinity에 의해 선택되었지만 파드에게 할당할 CPU와 메모리가 부족하기 때문에 파드를 실행시킬 노드가 없어서 파드 스케줄링에 실패했다는 것이다. 필터링 단계에서 모든 노드가 선택을 못받았기 때문에 파드는 Pending 상태로 머물게 된다.

 

 

이런 에러가 발생한다면 nodeAffinity를 지우거나, 어피니티를 만족시키는 노드의 리소스를 증설하거나, 어피니티를 만족시키는 노드를 추가하거나, 어피니티를 만족시키는 노드에서 필요없는 파드를 축출시켜 가용 리소스를 확보해야 한다.

 

preferredDuringSchedulingIgnoredDuringExecution

preferredDuringSchedulingIgnoredDuringExecution 유형은 해당 규칙에 충족되는 노드가 있다면, 규칙에 충족되는 노드에게 가중치를 부여하는 유형이다. requiredDuringSchedulingIgnoredDuringExecution과 가장 큰 차이점은 preferredDuringSchedulingIgnoredDuringExecution 은 규칙에 충족되는 노드가 없어도 파드를 다른 노드로 스케줄링 할 수 있다는 것이다. 그래서 이 유형은 Scoring 에서 사용된다.

 

preferredDuringSchedulingIgnoredDuringExecution 유형에서는 1부터 100까지의 weight를 부여할 수 있다. 스케줄러는 파드의 다른 모든 스케줄링 요구사항을 충족하는 노드를 찾으면 스케줄러는 노드가 충족하는 모든 기본 설정 규칙을 반복하고 해당 표현식에 대한 가중치 값을 합계에 더한다. 최종 합계는 노드의 다른 우선순위 함수의 점수에 더해진다. 스케줄러가 파드에 대한 스케줄링 결정을 내릴 때 총 점수가 가장 높은 노드가 우선순위를 가진다.

 

 

예시

CPU 연산을 자주하는 애플리케이션이 있고, 노드 중 CPU 성능이 좋은 노드가 있다고 가정하자. 그러면 해당 애플리케이션은 CPU 성능이 좋은 노드로 스케줄링되면 성능이 유리해진다.

 

 

해당 애플리케이션을 preferredDuringSchedulingIgnoredDuringExecution 으로 스케줄링 해보도록 하자.

 

일단 먼저, CPU 성능이 좋은 노드에 cpu=fast 라는 레이블을 추가하자.

$ kubectl labels no k8s-wn1 cpu=fast

 

그 다음 preferredDuringSchedulingIgnoredDuringExecution을 사용하여 NodeAffinity를 구성하여 파드를 생성해보자.

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test3
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 50
          preference:
            matchExpressions:
              - key: cpu
                operator: In
                values:
                  - fast
  containers:
  - name: nginx
    image: nginx
    resources:
      requests:
        cpu: '0.5'
        memory: '512Mi'

 

파드를 조회해보면 cpu=fast인 노드로 스케줄링 된 것을 확인할 수 있다.

 

cpu=fast 노드에 리소스가 없다면?

 

만약 cpu=fast 레이블이 있는 노드에 리소스가 없다면 어떻게 될까? 이전에 만들어둔 resource-gangster 파드를 생성한 후 node-affinity-test3을 생성해보자.

apiVersion: v1
kind: Pod
metadata:
  name: resource-gangster
spec:
  nodeSelector:
    cpu: "fast"
  containers:
  - name: nginx
    image: nginx
    resources:
      requests:
        cpu: '800m'
        memory: 2Gi

 

이번에는 파드가 cpu=fast가 아닌 노드로 스케줄링 되었다.

 

앞서 말했지만 preferredDuringSchedulingIgnoredDuringExecution은 어피니티의 조건을 만족시키는 노드에게 우선순위를 매기는 것일 뿐, 조건을 만족시키지 않는 노드를 배제하는 것이 아니기 때문에 어피니티의 조건을 만족시키는 노드에서 다른 이유로 스케줄링을 할 수 없는 경우에도 조건을 만족시키지 않는 노드로 스케줄링을 할 수 있다.

 

resource-gangster 파드가 없는 경우에는 두 노드 모두 가용 리소스가 충분하기 때문에 필터링에서 모두 통과하고, preferredDuringSchedulingIgnoredDuringExecution 에 의해 k8s-wn1 노드가 가중치를 더 많이 받아 파드 스케줄링에 우선순위가 높아져 최종적으로 파드가 k8s-wn1 노드에 배치가 된다.

 

 

하지만 resource-gangster 파드가 k8s-wn1 노드에 스케줄링되어 k8s-wn1 노드에 가용 리소스가 없다면, 필터링 단계에서 k8s-wn1 노드가 제외되지만 k8s-wn2노드는 제외되지 않고, NodeAffinity에 의해 가중치를 받는 노드가 없기 때문에 가중치가 없어도 k8s-wn2노드로 파드가 스케줄링된다.

728x90

댓글