스케줄링, 선점(Preemption), 축출(Eviction)

쿠버네티스에서, 스케줄링은 kubelet이 파드를 실행할 수 있도록 파드를 노드에 할당하는 것을 말한다. 선점은 우선순위가 높은 파드가 노드에 스케줄될 수 있도록 우선순위가 낮은 파드를 종료시키는 과정을 말한다. 축출은 리소스가 부족한 노드에서 하나 이상의 파드를 사전에 종료시키는 프로세스이다.

쿠버네티스에서, 스케줄링은 kubelet이 파드를 실행할 수 있도록 파드노드에 할당하는 것을 말한다. 선점은 우선순위가 높은 파드가 노드에 스케줄될 수 있도록 우선순위가 낮은 파드를 종료시키는 과정을 말한다. 축출은 리소스가 부족한 노드에서 하나 이상의 파드를 사전에 종료시키는 프로세스이다.

스케줄링

파드 중단(disruption)

파드 중단은 노드에 있는 파드가 자발적 또는 비자발적으로 종료되는 절차이다.

자발적 중단은 애플리케이션 소유자 또는 클러스터 관리자가 의도적으로 시작한다. 비자발적 중단은 의도하지 않은 것이며, 노드의 리소스 부족과 같은 피할 수 없는 문제 또는 우발적인 삭제로 인해 트리거가 될 수 있다.

1 - 쿠버네티스 스케줄러

쿠버네티스에서 스케줄링Kubelet이 파드를 실행할 수 있도록 파드노드에 적합한지 확인하는 것을 말한다.

스케줄링 개요

스케줄러는 노드가 할당되지 않은 새로 생성된 파드를 감시한다. 스케줄러가 발견한 모든 파드에 대해 스케줄러는 해당 파드가 실행될 최상의 노드를 찾는 책임을 진다. 스케줄러는 아래 설명된 스케줄링 원칙을 고려하여 이 배치 결정을 하게 된다.

파드가 특정 노드에 배치되는 이유를 이해하려고 하거나 사용자 정의된 스케줄러를 직접 구현하려는 경우 이 페이지를 통해서 스케줄링에 대해 배울 수 있을 것이다.

kube-scheduler

kube-scheduler는 쿠버네티스의 기본 스케줄러이며 컨트롤 플레인의 일부로 실행된다. kube-scheduler는 원하거나 필요에 따라 자체 스케줄링 컴포넌트를 만들고 대신 사용할 수 있도록 설계되었다.

kube-scheduler는 새로 생성되었거나 아직 예약되지 않은(스케줄링되지 않은) 파드를 실행할 최적의 노드를 선택한다. 파드의 컨테이너와 파드 자체는 서로 다른 요구사항을 가질 수 있으므로, 스케줄러는 파드의 특정 스케줄링 요구사항을 충족하지 않는 모든 노드를 필터링한다. 또는 API를 사용하면 파드를 생성할 때 노드를 지정할 수 있지만, 이는 드문 경우이며 특별한 경우에만 수행된다.

클러스터에서 파드에 대한 스케줄링 요구사항을 충족하는 노드를 실행 가능한(feasible) 노드라고 한다. 적합한 노드가 없으면 스케줄러가 배치할 수 있을 때까지 파드가 스케줄 되지 않은 상태로 유지된다.

스케줄러는 파드가 실행 가능한 노드를 찾은 다음 실행 가능한 노드의 점수를 측정하는 기능 셋을 수행하고 실행 가능한 노드 중에서 가장 높은 점수를 가진 노드를 선택하여 파드를 실행한다. 그런 다음 스케줄러는 바인딩 이라는 프로세스에서 이 결정에 대해 API 서버에 알린다.

스케줄링 결정을 위해 고려해야 할 요소에는 개별 및 집단 리소스 요구사항, 하드웨어 / 소프트웨어 / 정책 제한조건, 어피니티 및 안티-어피니티 명세, 데이터 지역성(data locality), 워크로드 간 간섭 등이 포함된다.

kube-scheduler에서 노드 선택

kube-scheduler는 2단계 작업에서 파드에 대한 노드를 선택한다.

  1. 필터링
  2. 스코어링(scoring)

필터링 단계는 파드를 스케줄링 할 수 있는 노드 셋을 찾는다. 예를 들어 PodFitsResources 필터는 후보 노드가 파드의 특정 리소스 요청을 충족시키기에 충분한 가용 리소스가 있는지 확인한다. 이 단계 다음에 노드 목록에는 적합한 노드들이 포함된다. 하나 이상의 노드가 포함된 경우가 종종 있을 것이다. 목록이 비어 있으면 해당 파드는 (아직) 스케줄링 될 수 없다.

스코어링 단계에서 스케줄러는 목록에 남아있는 노드의 순위를 지정하여 가장 적합한 파드 배치를 선택한다. 스케줄러는 사용 중인 스코어링 규칙에 따라 이 점수를 기준으로 필터링에서 통과된 각 노드에 대해 점수를 지정한다.

마지막으로 kube-scheduler는 파드를 순위가 가장 높은 노드에 할당한다. 점수가 같은 노드가 두 개 이상인 경우 kube-scheduler는 이들 중 하나를 임의로 선택한다.

스케줄러의 필터링 및 스코어링 동작을 구성하는 데 지원되는 두 가지 방법이 있다.

  1. 스케줄링 정책을 사용하면 필터링을 위한 단정(Predicates) 및 스코어링을 위한 우선순위(Priorities) 를 구성할 수 있다.
  2. 스케줄링 프로파일을 사용하면 QueueSort, Filter, Score, Bind, Reserve, Permit 등의 다른 스케줄링 단계를 구현하는 플러그인을 구성할 수 있다. 다른 프로파일을 실행하도록 kube-scheduler를 구성할 수도 있다.

다음 내용

2 - 노드에 파드 할당하기

특정한 노드(들) 집합에서만 동작하거나 특정한 노드 집합에서 동작하는 것을 선호하도록 파드를 제한할 수 있다. 이를 수행하는 방법에는 여러 가지가 있으며 권장되는 접근 방식은 모두 레이블 셀렉터를 사용하여 선택을 용이하게 한다. 보통은 스케줄러가 자동으로 합리적인 배치(예: 자원이 부족한 노드에 파드를 배치하지 않도록 노드 간에 파드를 분배)를 수행하기에 이러한 제약 조건을 설정할 필요는 없다. 그러나, 예를 들어 SSD가 장착된 머신에 파드가 배포되도록 하거나 또는 많은 통신을 하는 두 개의 서로 다른 서비스의 파드를 동일한 가용성 영역(availability zone)에 배치하는 경우와 같이, 파드가 어느 노드에 배포될지를 제어해야 하는 경우도 있다.

쿠버네티스가 특정 파드를 어느 노드에 스케줄링할지 고르는 다음의 방법 중 하나를 골라 사용할 수 있다.

노드 레이블

다른 쿠버네티스 오브젝트와 마찬가지로, 노드도 레이블을 가진다. 레이블을 수동으로 추가할 수 있다. 또한 쿠버네티스도 클러스터의 모든 노드에 표준화된 레이블 집합을 적용한다. 잘 알려진 레이블, 어노테이션, 테인트에서 널리 사용되는 노드 레이블의 목록을 확인한다.

노드 격리/제한

노드에 레이블을 추가하여 파드를 특정 노드 또는 노드 그룹에 스케줄링되도록 지정할 수 있다. 이 기능을 사용하여 특정 파드가 특정 격리/보안/규제 속성을 만족하는 노드에서만 실행되도록 할 수 있다.

노드 격리를 위해 레이블을 사용할 때, kubelet이 변경할 수 없는 레이블 키를 선택한다. 그렇지 않으면 kubelet이 해당 레이블을 변경하여 노드가 사용 불가능(compromised) 상태로 빠지고 스케줄러가 이 노드에 워크로드를 스케줄링하는 일이 발생할 수 있다.

NodeRestriction 어드미션 플러그인은 kubelet이 node-restriction.kubernetes.io/ 접두사를 갖는 레이블을 설정하거나 변경하지 못하도록 한다.

노드 격리를 위해 레이블 접두사를 사용하려면,

  1. 노드 인가자(authorizer)를 사용하고 있는지, 그리고 NodeRestriction 어드미션 플러그인을 활성화 했는지 확인한다.
  2. 노드에 node-restriction.kubernetes.io/ 접두사를 갖는 레이블을 추가하고, 노드 셀렉터에서 해당 레이블을 사용한다. 예: example.com.node-restriction.kubernetes.io/fips=true 또는 example.com.node-restriction.kubernetes.io/pci-dss=true

노드셀렉터(nodeSelector)

nodeSelector는 노드 선택 제약사항의 가장 간단하면서도 추천하는 형태이다. 파드 스펙에 nodeSelector 필드를 추가하고, 타겟으로 삼고 싶은 노드가 갖고 있는 노드 레이블을 명시한다. 쿠버네티스는 사용자가 명시한 레이블을 갖고 있는 노드에만 파드를 스케줄링한다.

노드에 파드 할당에서 더 많은 정보를 확인한다.

어피니티(affinity)와 안티-어피니티(anti-affinity)

nodeSelector 는 파드를 특정 레이블이 있는 노드로 제한하는 가장 간단한 방법이다. 어피니티/안티-어피니티 기능은 표현할 수 있는 제약 종류를 크게 확장한다. 주요 개선 사항은 다음과 같다.

  • 어피니티/안티-어피니티 언어가 더 표현적이다. nodeSelector로는 명시한 레이블이 있는 노드만 선택할 수 있다. 어피니티/안티-어피니티는 선택 로직에 대한 좀 더 많은 제어권을 제공한다.
  • 규칙이 "소프트(soft)" 또는 "선호사항(preference)" 임을 나타낼 수 있으며, 이 덕분에 스케줄러는 매치되는 노드를 찾지 못한 경우에도 파드를 스케줄링할 수 있다.
  • 다른 노드 (또는 다른 토폴로지 도메인)에서 실행 중인 다른 파드의 레이블을 사용하여 파드를 제한할 수 있으며, 이를 통해 어떤 파드들이 노드에 함께 위치할 수 있는지에 대한 규칙을 정의할 수 있다.

어피니티 기능은 다음의 두 가지 종류로 구성된다.

  • 노드 어피니티 기능은 nodeSelector 필드와 비슷하지만 더 표현적이고 소프트(soft) 규칙을 지정할 수 있게 해 준다.
  • 파드 간 어피니티/안티-어피니티 는 다른 파드의 레이블을 이용하여 해당 파드를 제한할 수 있게 해 준다.

노드 어피니티

노드 어피니티는 개념적으로 nodeSelector 와 비슷하며, 노드의 레이블을 기반으로 파드가 스케줄링될 수 있는 노드를 제한할 수 있다. 노드 어피니티에는 다음의 두 종류가 있다.

  • requiredDuringSchedulingIgnoredDuringExecution: 규칙이 만족되지 않으면 스케줄러가 파드를 스케줄링할 수 없다. 이 기능은 nodeSelector와 유사하지만, 좀 더 표현적인 문법을 제공한다.
  • preferredDuringSchedulingIgnoredDuringExecution: 스케줄러는 조건을 만족하는 노드를 찾으려고 노력한다. 해당되는 노드가 없더라도, 스케줄러는 여전히 파드를 스케줄링한다.

파드 스펙의 .spec.affinity.nodeAffinity 필드에 노드 어피니티를 명시할 수 있다.

예를 들어, 다음과 같은 파드 스펙이 있다고 하자.

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - antarctica-east1
            - antarctica-west1
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: registry.k8s.io/pause:2.0

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

  • 노드는 키가 topology.kubernetes.io/zone인 레이블을 갖고 있어야 하며, 레이블의 값이 antarctica-east1 혹은 antarctica-west1여야 한다.
  • 키가 another-node-label-key이고 값이 another-node-label-value인 레이블을 갖고 있는 노드를 선호한다 .

operator 필드를 사용하여 쿠버네티스가 규칙을 해석할 때 사용할 논리 연산자를 지정할 수 있다. In, NotIn, Exists, DoesNotExist, GtLt 연산자를 사용할 수 있다.

NotInDoesNotExist 연산자를 사용하여 노드 안티-어피니티 규칙을 정의할 수 있다. 또는, 특정 노드에서 파드를 쫓아내는 노드 테인트(taint)를 설정할 수 있다.

노드 어피니티를 사용해 노드에 파드 할당에서 더 많은 정보를 확인한다.

노드 어피니티 가중치(weight)

preferredDuringSchedulingIgnoredDuringExecution 어피니티 타입 인스턴스에 대해 1-100 범위의 weight를 명시할 수 있다. 스케줄러가 다른 모든 파드 스케줄링 요구 사항을 만족하는 노드를 찾으면, 스케줄러는 노드가 만족한 모든 선호하는(preferred) 규칙에 대해 합계 계산을 위한 weight 값을 각각 추가한다.

최종 합계는 해당 노드에 대한 다른 우선 순위 함수 점수에 더해진다. 스케줄러가 파드에 대한 스케줄링 판단을 할 때, 총 점수가 가장 높은 노드가 우선 순위를 갖는다.

예를 들어, 다음과 같은 파드 스펙이 있다고 하자.

apiVersion: v1
kind: Pod
metadata:
  name: with-affinity-anti-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
      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
  containers:
  - name: with-node-affinity
    image: registry.k8s.io/pause:2.0

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

스케줄링 프로파일당 노드 어피니티

기능 상태: Kubernetes v1.20 [beta]

여러 스케줄링 프로파일을 구성할 때 노드 어피니티가 있는 프로파일을 연결할 수 있는데, 이는 프로파일이 특정 노드 집합에만 적용되는 경우 유용하다. 이렇게 하려면 다음과 같이 스케줄러 구성에 있는 NodeAffinity 플러그인args 필드에 addedAffinity를 추가한다.

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
  - schedulerName: foo-scheduler
    pluginConfig:
      - name: NodeAffinity
        args:
          addedAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: scheduler-profile
                  operator: In
                  values:
                  - foo

addedAffinity.spec.schedulerNamefoo-scheduler로 설정하는 모든 파드에 적용되며 PodSpec에 지정된 NodeAffinity도 적용된다. 즉, 파드를 매칭시키려면, 노드가 addedAffinity와 파드의 .spec.NodeAffinity를 충족해야 한다.

addedAffinity는 엔드 유저에게 표시되지 않으므로, 예상치 못한 동작이 일어날 수 있다. 스케줄러 프로파일 이름과 명확한 상관 관계가 있는 노드 레이블을 사용한다.

파드간 어피니티와 안티-어피니티

파드간 어피니티와 안티-어피니티를 사용하여, 노드 레이블 대신, 각 노드에 이미 실행 중인 다른 파드 의 레이블을 기반으로 파드가 스케줄링될 노드를 제한할 수 있다.

파드간 어피니티와 안티-어피니티 규칙은 "X가 규칙 Y를 충족하는 하나 이상의 파드를 이미 실행중인 경우 이 파드는 X에서 실행해야 한다(또는 안티-어피니티의 경우에는 "실행하면 안 된다")"의 형태이며, 여기서 X는 노드, 랙, 클라우드 제공자 존 또는 리전 등이며 Y는 쿠버네티스가 충족할 규칙이다.

이러한 규칙(Y)은 레이블 셀렉터 형태로 작성하며, 연관된 네임스페이스 목록을 선택적으로 명시할 수도 있다. 쿠버네티스에서 파드는 네임스페이스에 속하는(namespaced) 오브젝트이므로, 파드 레이블도 암묵적으로 특정 네임스페이스에 속하게 된다. 파드 레이블에 대한 모든 레이블 셀렉터는 쿠버네티스가 해당 레이블을 어떤 네임스페이스에서 탐색할지를 명시해야 한다.

topologyKey를 사용하여 토폴로지 도메인(X)를 나타낼 수 있으며, 이는 시스템이 도메인을 표시하기 위해 사용하는 노드 레이블의 키이다. 이에 대한 예시는 잘 알려진 레이블, 어노테이션, 테인트를 참고한다.

파드간 어피니티 및 안티-어피니티 종류

노드 어피니티와 마찬가지로 파드 어피니티 및 안티-어피니티에는 다음의 2 종류가 있다.

  • requiredDuringSchedulingIgnoredDuringExecution
  • preferredDuringSchedulingIgnoredDuringExecution

예를 들어, requiredDuringSchedulingIgnoredDuringExecution 어피니티를 사용하여 서로 통신을 많이 하는 두 서비스의 파드를 동일 클라우드 제공자 존에 배치하도록 스케줄러에게 지시할 수 있다. 비슷하게, preferredDuringSchedulingIgnoredDuringExecution 안티-어피니티를 사용하여 서비스의 파드를 여러 클라우드 제공자 존에 퍼뜨릴 수 있다.

파드간 어피니티를 사용하려면, 파드 스펙에 affinity.podAffinity 필드를 사용한다. 파드간 안티-어피니티를 사용하려면, 파드 스펙에 affinity.podAntiAffinity 필드를 사용한다.

파드 어피니티 예시

다음과 같은 파드 스펙을 가정한다.

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: registry.k8s.io/pause:2.0

이 예시는 하나의 파드 어피니티 규칙과 하나의 파드 안티-어피니티 규칙을 정의한다. 파드 어피니티 규칙은 "하드" requiredDuringSchedulingIgnoredDuringExecution을, 안티-어피니티 규칙은 "소프트" preferredDuringSchedulingIgnoredDuringExecution을 사용한다.

위의 어피니티 규칙은 security=S1 레이블이 있는 하나 이상의 기존 파드의 존와 동일한 존에 있는 노드에만 파드를 스케줄링하도록 스케줄러에 지시한다. 더 정확히 말하면, 만약 security=S1 파드 레이블이 있는 하나 이상의 기존 파드를 실행하고 있는 노드가 zone=V에 하나 이상 존재한다면, 스케줄러는 파드를 topology.kubernetes.io/zone=V 레이블이 있는 노드에 배치해야 한다.

위의 안티-어피니티 규칙은 security=S2 레이블이 있는 하나 이상의 기존 파드의 존와 동일한 존에 있는 노드에는 가급적 파드를 스케줄링하지 않도록 스케줄러에 지시한다. 더 정확히 말하면, 만약 security=S2 파드 레이블이 있는 파드가 실행되고 있는 zone=R에 다른 노드도 존재한다면, 스케줄러는 topology.kubernetes.io/zone=R 레이블이 있는 노드에는 가급적 해당 파드를 스케줄링하지 않야아 한다.

파드 어피니티와 안티-어피니티의 예시에 대해 익숙해지고 싶다면, 디자인 제안을 참조한다.

파드 어피니티와 안티-어피니티의 operator 필드에 In, NotIn, ExistsDoesNotExist 값을 사용할 수 있다.

원칙적으로, topologyKey 에는 성능과 보안상의 이유로 다음의 예외를 제외하면 어느 레이블 키도 사용할 수 있다.

  • 파드 어피니티 및 안티-어피니티에 대해, 빈 topologyKey 필드는 requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution 내에 허용되지 않는다.
  • requiredDuringSchedulingIgnoredDuringExecution 파드 안티-어피니티 규칙에 대해, LimitPodHardAntiAffinityTopology 어드미션 컨트롤러는 topologyKeykubernetes.io/hostname으로 제한한다. 커스텀 토폴로지를 허용하고 싶다면 어드미션 컨트롤러를 수정하거나 비활성화할 수 있다.

labelSelectortopologyKey에 더하여 선택적으로, labelSelector가 비교해야 하는 네임스페이스의 목록을 labelSelectortopologyKey 필드와 동일한 계위의 namespaces 필드에 명시할 수 있다. 생략하거나 비워 두면, 해당 어피니티/안티-어피니티 정의가 있는 파드의 네임스페이스를 기본값으로 사용한다.

네임스페이스 셀렉터

기능 상태: Kubernetes v1.24 [stable]

네임스페이스 집합에 대한 레이블 쿼리인 namespaceSelector 를 사용하여 일치하는 네임스페이스를 선택할 수도 있다. namespaceSelector 또는 namespaces 필드에 의해 선택된 네임스페이스 모두에 적용된다. 빈 namespaceSelector ({})는 모든 네임스페이스와 일치하는 반면, null 또는 빈 namespaces 목록과 null namespaceSelector 는 규칙이 적용된 파드의 네임스페이스에 매치된다.

더 실제적인 유스케이스

파드간 어피니티와 안티-어피니티는 레플리카셋, 스테이트풀셋, 디플로이먼트 등과 같은 상위 레벨 모음과 함께 사용할 때 더욱 유용할 수 있다. 이러한 규칙을 사용하면, 워크로드 집합이 예를 들면 서로 연관된 두 개의 파드를 동일한 노드에 배치하는 것과 같이 동일하게 정의된 토폴로지와 같은 위치에 배치되도록 쉽게 구성할 수 있다.

세 개의 노드로 구성된 클러스터를 상상해 보자. 이 클러스터에서 redis와 같은 인-메모리 캐시를 이용하는 웹 애플리케이션을 실행한다. 또한 이 예에서 웹 애플리케이션과 메모리 캐시 사이의 대기 시간은 될 수 있는 대로 짧아야 한다고 가정하자. 이 때 웹 서버를 가능한 한 캐시와 같은 위치에서 실행되도록 하기 위해 파드간 어피니티/안티-어피니티를 사용할 수 있다.

다음의 redis 캐시 디플로이먼트 예시에서, 레플리카는 app=store 레이블을 갖는다. podAntiAffinity 규칙은 스케줄러로 하여금 app=store 레이블을 가진 복수 개의 레플리카를 단일 노드에 배치하지 않게 한다. 이렇게 하여 캐시 파드를 각 노드에 분산하여 생성한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine

웹 서버를 위한 다음의 디플로이먼트는 app=web-store 레이블을 갖는 레플리카를 생성한다. 파드 어피니티 규칙은 스케줄러로 하여금 app=store 레이블이 있는 파드를 실행 중인 노드에 각 레플리카를 배치하도록 한다. 파드 안티-어피니티 규칙은 스케줄러로 하여금 app=web-store 레이블이 있는 서버 파드를 한 노드에 여러 개 배치하지 못하도록 한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.16-alpine

위의 두 디플로이먼트를 생성하면 다음과 같은 클러스터 형상이 나타나는데, 세 노드에 각 웹 서버가 캐시와 함께 있는 형상이다.

node-1node-2node-3
webserver-1webserver-2webserver-3
cache-1cache-2cache-3

전체적인 효과는 각 캐시 인스턴스를 동일한 노드에서 실행 중인 단일 클라이언트가 액세스하게 될 것 같다는 것이다. 이 접근 방식은 차이(불균형 로드)와 대기 ​​시간을 모두 최소화하는 것을 목표로 한다.

파드간 안티-어피니티를 사용해야 하는 다른 이유가 있을 수 있다. ZooKeeper 튜토리얼에서 위 예시와 동일한 기술을 사용해 고 가용성을 위한 안티-어피니티로 구성된 스테이트풀셋의 예시를 확인한다.

nodeName

nodeName은 어피니티 또는 nodeSelector보다 더 직접적인 형태의 노드 선택 방법이다. nodeName은 파드 스펙의 필드 중 하나이다. nodeName 필드가 비어 있지 않으면, 스케줄러는 파드를 무시하고, 명명된 노드의 kubelet이 해당 파드를 자기 노드에 배치하려고 시도한다. nodeNamenodeSelector 또는 어피니티/안티-어피니티 규칙보다 우선적으로 적용(overrule)된다.

nodeName 을 사용해서 노드를 선택할 때의 몇 가지 제한은 다음과 같다.

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

다음은 nodeName 필드를 사용하는 파드 스펙 예시이다.

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  nodeName: kube-01

위 파드는 kube-01 노드에서만 실행될 것이다.

파드 토폴로지 분배 제약 조건

_토폴로지 분배 제약 조건_을 사용하여 지역(regions), 영역(zones), 노드 또는 사용자가 정의한 다른 토폴로지 도메인과 같은 장애 도메인 사이에서 파드가 클러스터 전체에 분산되는 방식을 제어할 수 있다. 성능, 예상 가용성 또는 전체 활용도를 개선하기 위해 이 작업을 수행할 수 있다.

파드 토폴로지 분배 제약 조건에서 작동 방식에 대해 더 자세히 알아볼 수 있다.

다음 내용

3 - 파드 오버헤드

기능 상태: Kubernetes v1.24 [stable]

노드 상에서 파드를 구동할 때, 파드는 그 자체적으로 많은 시스템 리소스를 사용한다. 이러한 리소스는 파드 내의 컨테이너들을 구동하기 위한 리소스 이외에 추가적으로 필요한 것이다. 쿠버네티스에서, 파드 오버헤드 는 리소스 요청 및 상한 외에도 파드의 인프라에 의해 소비되는 리소스를 계산하는 방법 중 하나이다.

쿠버네티스에서 파드의 오버헤드는 파드의 런타임클래스 와 관련된 오버헤드에 따라 어드미션 이 수행될 때 지정된다.

파드를 노드에 스케줄링할 때, 컨테이너 리소스 요청의 합 뿐만 아니라 파드의 오버헤드도 함께 고려된다. 마찬가지로, kubelet은 파드의 cgroups 크기를 변경하거나 파드의 축출 등급을 부여할 때에도 파드의 오버헤드를 포함하여 고려한다.

파드 오버헤드 환경 설정하기

overhead 필드를 정의하는 RuntimeClass 가 사용되고 있는지 확인해야 한다.

사용 예제

파드 오버헤드를 활용하려면, overhead 필드를 정의하는 런타임클래스가 필요하다. 예를 들어, 가상 머신 및 게스트 OS에 대하여 파드 당 120 MiB를 사용하는 가상화 컨테이너 런타임의 런타임클래스의 경우 다음과 같이 정의 할 수 있다.

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata-fc
handler: kata-fc
overhead:
  podFixed:
    memory: "120Mi"
    cpu: "250m"

kata-fc 런타임클래스 핸들러를 지정하는 워크로드는 리소스 쿼터 계산, 노드 스케줄링 및 파드 cgroup 크기 조정을 위하여 메모리와 CPU 오버헤드를 고려한다.

주어진 예제 워크로드 test-pod의 구동을 고려해보자.

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  runtimeClassName: kata-fc
  containers:
  - name: busybox-ctr
    image: busybox:1.28
    stdin: true
    tty: true
    resources:
      limits:
        cpu: 500m
        memory: 100Mi
  - name: nginx-ctr
    image: nginx
    resources:
      limits:
        cpu: 1500m
        memory: 100Mi

어드미션 수행 시에, 어드미션 컨트롤러는 런타임클래스에 기술된 overhead 를 포함하기 위하여 워크로드의 PodSpec 항목을 갱신한다. 만약 PodSpec이 이미 해당 필드에 정의되어 있으면, 파드는 거부된다. 주어진 예제에서, 오직 런타임클래스의 이름만이 정의되어 있기 때문에, 어드미션 컨트롤러는 파드가 overhead 를 포함하도록 변경한다.

런타임클래스 어드미션 컨트롤러가 변경을 완료하면, 다음의 명령어로 업데이트된 파드 오버헤드 값을 확인할 수 있다.

kubectl get pod test-pod -o jsonpath='{.spec.overhead}'

명령 실행 결과는 다음과 같다.

map[cpu:250m memory:120Mi]

만약 리소스쿼터 항목이 정의되어 있다면, 컨테이너의 리소스 요청의 합에는 overhead 필드도 추가된다.

kube-scheduler 는 어떤 노드에 파드가 기동 되어야 할지를 정할 때, 파드의 overhead 와 해당 파드에 대한 컨테이너의 리소스 요청의 합을 고려한다. 이 예제에서, 스케줄러는 리소스 요청과 파드의 오버헤드를 더하고, 2.25 CPU와 320 MiB 메모리가 사용 가능한 노드를 찾는다.

일단 파드가 특정 노드에 스케줄링 되면, 해당 노드에 있는 kubelet 은 파드에 대한 새로운 cgroup을 생성한다. 기본 컨테이너 런타임이 만들어내는 컨테이너들은 이 파드 안에 존재한다.

만약 각 컨테이너에 대하여 리소스 상한 제한이 걸려있으면 (제한이 걸려있는 보장된(Guaranteed) Qos 또는 향상 가능한(Burstable) QoS), kubelet 은 해당 리소스(CPU의 경우 cpu.cfs_quota_us, 메모리의 경우 memory.limit_in_bytes)와 연관된 파드의 cgroup 의 상한선을 설정한다. 이 상한선은 컨테이너 리소스 상한과 PodSpec에 정의된 overhead 의 합에 기반한다.

CPU의 경우, 만약 파드가 보장형 또는 버스트형 QoS로 설정되었으면, kubelet은 PodSpec에 정의된 overhead 에 컨테이너의 리소스 요청의 합을 더한 값을 cpu.shares 로 설정한다.

다음의 예제를 참고하여, 워크로드에 대하여 컨테이너의 리소스 요청을 확인하자.

kubectl get pod test-pod -o jsonpath='{.spec.containers[*].resources.limits}'

컨테이너 리소스 요청의 합은 각각 CPU 2000m 와 메모리 200MiB 이다.

map[cpu: 500m memory:100Mi] map[cpu:1500m memory:100Mi]

노드에서 측정된 내용과 비교하여 확인해보자.

kubectl describe node | grep test-pod -B2

결과를 보면 2250 m의 CPU와 320 MiB의 메모리가 리소스로 요청되었다. 여기에는 파드 오버헤드가 포함되어 있다.

  Namespace    Name       CPU Requests  CPU Limits   Memory Requests  Memory Limits  AGE
  ---------    ----       ------------  ----------   ---------------  -------------  ---
  default      test-pod   2250m (56%)   2250m (56%)  320Mi (1%)       320Mi (1%)     36m

파드 cgroup 상한 확인하기

워크로드가 실행 중인 노드에서 파드의 메모리 cgroup들을 확인해 보자. 다음의 예제에서, crictl은 노드에서 사용되며, CRI-호환 컨테이너 런타임을 위해서 노드에서 사용할 수 있는 CLI 를 제공한다. 파드 오버헤드 동작을 보여주는 좋은 예이며, 사용자가 노드에서 직접 cgroup들을 확인하지 않아도 된다.

먼저 특정 노드에서 파드의 식별자를 확인해 보자.

# 파드가 스케줄 된 노드에서 이것을 실행
POD_ID="$(sudo crictl pods --name test-pod -q)"

여기에서, 파드의 cgroup 경로를 확인할 수 있다.

# 파드가 스케줄 된 노드에서 이것을 실행
sudo crictl inspectp -o=json $POD_ID | grep cgroupsPath

명령의 결과로 나온 cgroup 경로는 파드의 pause 컨테이너를 포함한다. 파드 레벨의 cgroup은 하나의 디렉터리이다.

  "cgroupsPath": "/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/7ccf55aee35dd16aca4189c952d83487297f3cd760f1bbf09620e206e7d0c27a"

아래의 특정한 경우에, 파드 cgroup 경로는 kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2 이다. 메모리의 파드 레벨 cgroup 설정을 확인하자.

# 파드가 스케줄 된 노드에서 이것을 실행.
# 또한 사용자의 파드에 할당된 cgroup 이름에 맞춰 해당 이름을 수정.
 cat /sys/fs/cgroup/memory/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/memory.limit_in_bytes

예상한 것과 같이 320 MiB 이다.

335544320

관찰성

몇몇 kube_pod_overhead 메트릭은 kube-state-metrics 에서 사용할 수 있어, 파드 오버헤드가 사용되는 시기를 식별하고, 정의된 오버헤드로 실행되는 워크로드의 안정성을 관찰할 수 있다.

다음 내용

4 - 파드 스케줄링 준비성(readiness)

기능 상태: Kubernetes v1.26 [alpha]

파드는 생성되면 스케줄링 될 준비가 된 것으로 간주된다. 쿠버네티스 스케줄러는 모든 Pending 중인 파드를 배치할 노드를 찾기 위한 철저한 조사 과정을 수행한다. 그러나 일부 파드는 오랜 기간 동안 "필수 리소스 누락" 상태에 머물 수 있다. 이러한 파드는 실제로 스케줄러(그리고 클러스터 오토스케일러와 같은 다운스트림 통합자) 불필요한 방식으로 작동하게 만들 수 있다.

파드의 .spec.schedulingGates를 지정하거나 제거함으로써, 파드가 스케줄링 될 준비가 되는 시기를 제어할 수 있다.

파드 스케줄링게이트(schedulingGates) 구성하기

스케줄링게이트(schedulingGates) 필드는 문자열 목록을 포함하며, 각 문자열 리터럴은 파드가 스케줄링 가능한 것으로 간주되기 전에 충족해야 하는 기준으로 인식된다. 이 필드는 파드가 생성될 때만 초기화할 수 있다(클라이언트에 의해 생성되거나, 어드미션 중에 변경될 때). 생성 후 각 스케줄링게이트는 임의의 순서로 제거될 수 있지만, 새로운 스케줄링게이트의 추가는 허용되지 않는다.

stateDiagram-v2 s1: 파드 생성 s2: 파드 스케줄링 게이트됨 s3: 파드 스케줄링 준비됨 s4: 파드 실행 if: 스케줄링 게이트가 비어 있는가? [*] --> s1 s1 --> if s2 --> if: 스케줄링 게이트 제거됨 if --> s2: 아니오 if --> s3: 네 s3 --> s4 s4 --> [*]

사용 예시

파드가 스케줄링 될 준비가 되지 않았다고 표시하려면, 다음과 같이 하나 이상의 스케줄링 게이트를 생성하여 표시할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  schedulingGates:
  - name: foo
  - name: bar
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.6

파드가 생성된 후에는 다음 명령을 사용하여 상태를 확인할 수 있다.

kubectl get pod test-pod

출력은 파드가 SchedulingGated 상태임을 보여준다.

NAME       READY   STATUS            RESTARTS   AGE
test-pod   0/1     SchedulingGated   0          7s

다음 명령을 실행하여 schedulingGates 필드를 확인할 수도 있다.

kubectl get pod test-pod -o jsonpath='{.spec.schedulingGates}'

출력은 다음과 같다.

[{"name":"foo"},{"name":"bar"}]

스케줄러에게 이 파드가 스케줄링 될 준비가 되었음을 알리기 위해, 수정된 매니페스트를 다시 적용하여 schedulingGates를 완전히 제거할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.6

schedulingGates가 지워졌는지 확인하려면 다음 명령을 실행하여 확인할 수 있다.

kubectl get pod test-pod -o jsonpath='{.spec.schedulingGates}'

출력은 비어 있을 것이다. 그리고 다음 명령을 실행하여 최신 상태를 확인할 수 있다.

kubectl get pod test-pod -o wide

test-pod가 CPU/메모리 리소스를 요청하지 않았기 때문에, 이 파드의 상태는 이전의 SchedulingGated에서 Running으로 전환됐을 것이다.

NAME       READY   STATUS    RESTARTS   AGE   IP         NODE  
test-pod   1/1     Running   0          15s   10.0.0.4   node-2

가시성(Observability)

스케줄링을 시도했지만 스케줄링할 수 없다고 클레임되거나 스케줄링할 준비가 되지 않은 것으로 명시적으로 표시된 파드를 구분하기 위해 scheduler_pending_pods 메트릭은 ”gated"라는 새로운 레이블과 함께 제공된다. scheduler_pending_pods{queue="gated"}를 사용하여 메트릭 결과를 확인할 수 있다.

다음 내용

5 - 파드 토폴로지 분배 제약 조건

사용자는 토폴로지 분배 제약 조건 을 사용하여 지역(region), 존(zone), 노드 및 기타 사용자 정의 토폴로지 도메인과 같이 장애 도메인으로 설정된 클러스터에 걸쳐 파드가 분배되는 방식을 제어할 수 있다. 이를 통해 고가용성뿐만 아니라 효율적인 리소스 활용의 목적을 이루는 데에도 도움이 된다.

클러스터-수준 제약을 기본값으로 설정하거나, 개별 워크로드마다 각각의 토폴로지 분배 제약 조건을 설정할 수 있다.

동기(motivation)

최대 20 노드로 이루어진 클러스터가 있고, 레플리카 수를 자동으로 조절하는 워크로드를 실행해야 하는 상황을 가정해 보자. 파드의 수는 2개 정도로 적을 수도 있고, 15개 정도로 많을 수도 있다. 파드가 2개만 있는 상황에서는, 해당 파드들이 동일 노드에서 실행되는 것은 원치 않을 것이다. 단일 노드 장애(single node failure)로 인해 전체 워크로드가 오프라인이 될 수 있기 때문이다.

이러한 기본적 사용 뿐만 아니라, 고가용성(high availability) 및 클러스터 활용(cluster utilization)으로부터 오는 장점을 워크로드가 누리도록 하는 고급 사용 예시도 존재한다.

워크로드를 스케일 업 하여 더 많은 파드를 실행함에 따라, 중요성이 부각되는 다른 요소도 존재한다. 3개의 노드가 각각 5개의 파드를 실행하는 경우를 가정하자. 각 노드는 5개의 레플리카를 실행하기에 충분한 성능을 갖고 있다. 하지만, 이 워크로드와 통신하는 클라이언트들은 3개의 서로 다른 데이터센터(또는 인프라스트럭처 존(zone))에 걸쳐 존재한다. 이제 단일 노드 장애에 대해서는 덜 걱정해도 되지만, 지연 시간(latency)이 증가하고, 서로 다른 존 간에 네트워크 트래픽을 전송하기 위해 네트워크 비용을 지불해야 한다.

정상적인 동작 중에는 각 인프라스트럭처 존에 비슷한 수의 레플리카가 스케줄되고, 클러스터에 문제가 있는 경우 스스로 치유하도록 설정할 수 있다.

파드 토폴로지 분배 제약 조건은 이러한 설정을 할 수 있도록 하는 선언적인 방법을 제공한다.

topologySpreadConstraints 필드

파드 API에 spec.topologySpreadConstraints 필드가 있다. 이 필드는 다음과 같이 쓰인다.

---
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  # 토폴로지 분배 제약 조건을 구성한다.
  topologySpreadConstraints:
    - maxSkew: <integer>
      minDomains: <integer> # 선택 사항이며, v1.25에서 베타 기능으로 도입되었다.
      topologyKey: <string>
      whenUnsatisfiable: <string>
      labelSelector: <object>
      matchLabelKeys: <list> # 선택 사항이며, v1.25에서 알파 기능으로 도입되었다.
      nodeAffinityPolicy: [Honor|Ignore] # 선택 사항이며, v1.26에서 베타 기능으로 도입되었다.
      nodeTaintsPolicy: [Honor|Ignore] # 선택 사항이며, v1.26에서 베타 기능으로 도입되었다.
  ### 파드의 다른 필드가 이 아래에 오게 된다.

kubectl explain Pod.spec.topologySpreadConstraints 명령을 실행하거나 파드에 관한 API 레퍼런스의 스케줄링 섹션을 참조해서 이 필드에 대해 좀 더 알아볼 수 있다.

분배 제약 조건 정의

하나 또는 다중 topologySpreadConstraint 를 정의하여, kube-scheduler가 어떻게 클러스터 내에서 기존 파드와의 관계를 고려하며 새로운 파드를 배치할지를 지시할 수 있다. 각 필드는 다음과 같다.

  • maxSkew 는 파드가 균등하지 않게 분산될 수 있는 정도를 나타낸다. 이 필드는 필수이며, 0 보다는 커야 한다. 이 필드 값의 의미는 whenUnsatisfiable 의 값에 따라 다르다.

    • whenUnsatisfiable: DoNotSchedule을 선택했다면, maxSkew는 대상 토폴로지에서 일치하는 파드 수와 전역 최솟값(global minimum) (적절한 도메인 내에서 일치하는 파드의 최소 수, 또는 적절한 도메인의 수가 minDomains보다 작은 경우에는 0)
      사이의 최대 허용 차이를 나타낸다. 예를 들어, 3개의 존에 각각 2, 2, 1개의 일치하는 파드가 있으면, maxSkew는 1로 설정되고 전역 최솟값은 1로 설정된다.
    • whenUnsatisfiable: ScheduleAnyway를 선택하면, 스케줄러는 차이(skew)를 줄이는 데 도움이 되는 토폴로지에 더 높은 우선 순위를 부여한다.
  • minDomains 는 적합한(eligible) 도메인의 최소 수를 나타낸다. 이 필드는 선택 사항이다. 도메인은 토폴로지의 특정 인스턴스 중 하나이다. 도메인의 노드가 노드 셀렉터에 매치되면 그 도메인은 적합한 도메인이다.

    • minDomains 값을 명시하는 경우, 이 값은 0보다 커야 한다. minDomainswhenUnsatisfiable: DoNotSchedule일 때에만 지정할 수 있다.
    • 매치되는 토폴로지 키의 적합한 도메인 수가 minDomains보다 적으면, 파드 토폴로지 스프레드는 전역 최솟값을 0으로 간주한 뒤, skew 계산을 수행한다. 전역 최솟값은 적합한 도메인 내에 매치되는 파드의 최소 수이며, 적합한 도메인 수가 minDomains보다 적은 경우에는 0이다.
    • 매치되는 토폴로지 키의 적합한 도메인 수가 minDomains보다 크거나 같으면, 이 값은 스케줄링에 영향을 미치지 않는다.
    • minDomains를 명시하지 않으면, 분배 제약 조건은 minDomains가 1이라고 가정하고 동작한다.
  • topologyKey노드 레이블의 키(key)이다. 이 키와 동일한 값을 가진 레이블이 있는 노드는 동일한 토폴로지에 있는 것으로 간주된다.
    토폴로지의 각 인스턴스(즉, <키, 값> 쌍)를 도메인이라고 한다. 스케줄러는 각 도메인에 균형잡힌 수의 파드를 배치하려고 시도할 것이다. 또한, 노드가 nodeAffinityPolicy 및 nodeTaintsPolicy의 요구 사항을 충족하는 도메인을 적절한 도메인이라고 정의한다.

  • whenUnsatisfiable 는 분산 제약 조건을 만족하지 않을 경우에 파드를 처리하는 방법을 나타낸다.

    • DoNotSchedule(기본값)은 스케줄러에 스케줄링을 하지 말라고 알려준다.
    • ScheduleAnyway는 스케줄러에게 차이(skew)를 최소화하는 노드에 높은 우선 순위를 부여하면서, 스케줄링을 계속하도록 지시한다.
  • labelSelector 는 일치하는 파드를 찾는 데 사용된다. 이 레이블 셀렉터와 일치하는 파드의 수를 계산하여 해당 토폴로지 도메인에 속할 파드의 수를 결정한다. 자세한 내용은 레이블 셀렉터를 참조한다.

  • matchLabelKeys 는 분배도(spreading)가 계산될 파드를 선택하기 위한 파드 레이블 키 목록이다. 키는 파드 레이블에서 값을 조회하는 데 사용되며, 이러한 키-값 레이블은 labelSelector와 AND 처리되어 들어오는 파드(incoming pod)에 대해 분배도가 계산될 기존 파드 그룹의 선택에 사용된다. 파드 레이블에 없는 키는 무시된다. null 또는 비어 있는 목록은 labelSelector와만 일치함을 의미한다.

    matchLabelKeys를 사용하면, 사용자는 다른 리비전 간에 pod.spec을 업데이트할 필요가 없다. 컨트롤러/오퍼레이터는 다른 리비전에 대해 동일한 label키에 다른 값을 설정하기만 하면 된다. 스케줄러는 matchLabelKeys를 기준으로 값을 자동으로 가정할 것이다. 예를 들어 사용자가 디플로이먼트를 사용하는 경우, 디플로이먼트 컨트롤러에 의해 자동으로 추가되는 pod-template-hash로 키가 지정된 레이블을 사용함으로써 단일 디플로이먼트에서 서로 다른 리비전을 구별할 수 있다.

        topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              matchLabelKeys:
                - app
                - pod-template-hash
    
  • nodeAffinityPolicy는 파드 토폴로지의 스프레드 스큐(spread skew)를 계산할 때 파드의 nodeAffinity/nodeSelector를 다루는 방법을 나타낸다. 옵션은 다음과 같다.

    • Honor: nodeAffinity/nodeSelector와 일치하는 노드만 계산에 포함된다.
    • Ignore: nodeAffinity/nodeSelector는 무시된다. 모든 노드가 계산에 포함된다.

    옵션의 값이 null일 경우, Honor 정책과 동일하게 동작한다.

  • nodeTaintsPolicy는 파드 토폴로지의 스프레드 스큐(spread skew)를 계산할 때 노드 테인트(taint)를 다루는 방법을 나타낸다. 옵션은 다음과 같다.

    • Honor: 테인트가 없는 노드, 그리고 노드가 톨러레이션이 있는 들어오는 파드(incoming pod)를 위한 테인트가 설정된 노드가 포함된다.
    • Ignore: 노드 테인트는 무시된다. 모든 노드가 포함된다.

    옵션의 값이 null일 경우, Ignore 정책과 동일하게 동작한다.

파드에 2개 이상의 topologySpreadConstraint가 정의되어 있으면, 각 제약 조건은 논리 AND 연산으로 조합되며, kube-scheduler는 새로운 파드의 모든 제약 조건을 만족하는 노드를 찾는다.

노드 레이블

토폴로지 분배 제약 조건은 노드 레이블을 이용하여 각 노드가 속한 토폴로지 도메인(들)을 인식한다. 예를 들어, 노드가 다음과 같은 레이블을 가질 수 있다.

  region: us-east-1
  zone: us-east-1a

각각 다음과 같은 레이블을 갖는 4개의 노드로 구성된 클러스터가 있다고 가정한다.

NAME    STATUS   ROLES    AGE     VERSION   LABELS
node1   Ready    <none>   4m26s   v1.16.0   node=node1,zone=zoneA
node2   Ready    <none>   3m58s   v1.16.0   node=node2,zone=zoneA
node3   Ready    <none>   3m17s   v1.16.0   node=node3,zone=zoneB
node4   Ready    <none>   2m43s   v1.16.0   node=node4,zone=zoneB

그러면 클러스터는 논리적으로 다음과 같이 보이게 된다.

graph TB subgraph "zoneB" n3(Node3) n4(Node4) end subgraph "zoneA" n1(Node1) n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4 k8s; class zoneA,zoneB cluster;

일관성(consistency)

그룹 내의 모든 파드에는 동일한 파드 토폴로지 분배 제약 조건을 설정해야 한다.

일반적으로, 디플로이먼트와 같은 워크로드 컨트롤러를 사용하는 경우, 파드 템플릿이 이 사항을 담당한다. 여러 분배 제약 조건을 혼합하는 경우, 쿠버네티스는 해당 필드의 API 정의를 따르기는 하지만, 동작이 복잡해질 수 있고 트러블슈팅이 덜 직관적이게 된다.

동일한 토폴로지 도메인(예: 클라우드 공급자 리전)에 있는 모든 노드가 일관적으로 레이블되도록 하는 메카니즘이 필요하다. 각 노드를 수동으로 레이블하는 것을 피하기 위해, 대부분의 클러스터는 topology.kubernetes.io/hostname와 같은 잘 알려진 레이블을 자동으로 붙인다. 이용하려는 클러스터가 이를 지원하는지 확인해 본다.

토폴로지 분배 제약 조건 예시

예시: 단수 토폴로지 분배 제약 조건

foo:bar 가 레이블된 3개의 파드가 4개 노드를 가지는 클러스터의 node1, node2 및 node3에 각각 위치한다고 가정한다.

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class zoneA,zoneB cluster;

신규 파드가 기존 파드와 함께 여러 존에 걸쳐 균등하게 분배되도록 하려면, 다음과 같은 매니페스트를 사용할 수 있다.

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

위의 매니페스트에서, topologyKey: zone이 의미하는 것은 zone: <any value>로 레이블된 노드에 대해서만 균등한 분배를 적용한다는 뜻이다(zone 레이블이 없는 노드는 무시된다). whenUnsatisfiable: DoNotSchedule 필드는 만약 스케줄러가 신규 파드에 대해 제약 조건을 만족하는 스케줄링 방법을 찾지 못하면 이 신규 파드를 pending 상태로 유지하도록 한다.

만약 스케줄러가 이 신규 파드를 A 존에 배치하면 파드 분포는 [3, 1]이 된다. 이는 곧 실제 차이(skew)가 2(3-1)임을 나타내는데, 이는 maxSkew: 1을 위반하게 된다. 이 예시에서 제약 조건과 상황을 만족하려면, 신규 파드는 B 존의 노드에만 배치될 수 있다.

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

또는

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n3 n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

사용자는 파드 스펙을 조정하여 다음과 같은 다양한 요구사항을 충족할 수 있다.

  • maxSkew 를 더 큰 값(예: 2)으로 변경하여 신규 파드가 A 존에도 배치할 수 있도록 한다.
  • topologyKeynode로 변경하여 파드가 존이 아닌 노드에 걸쳐 고르게 분산되도록 한다. 위의 예시에서, 만약 maxSkew1로 유지한다면, 신규 파드는 오직 node4에만 배치될 수 있다.
  • whenUnsatisfiable: DoNotSchedulewhenUnsatisfiable: ScheduleAnyway로 변경하면 신규 파드가 항상 스케줄링되도록 보장할 수 있다(다른 스케줄링 API를 충족한다는 가정 하에). 그러나, 매칭되는 파드의 수가 적은 토폴로지 도메인에 배치되는 것이 선호된다. (이 선호도는 리소스 사용 비율과 같은 다른 내부 스케줄링 우선순위와 함께 정규화된다는 것을 알아두자.)

예시: 다중 토폴로지 분배 제약 조건

이 예시는 위의 예시에 기반한다. foo:bar 가 레이블된 3개의 파드가 4개 노드를 가지는 클러스터의 node1, node2 그리고 node3에 각각 위치한다고 가정한다.

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

사용자는 2개의 토폴로지 분배 제약 조건을 사용하여 노드 및 존 기반으로 파드가 분배되도록 제어할 수 있다.

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  - maxSkew: 1
    topologyKey: node
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

이 경우에는, 첫 번째 제약 조건에 부합하려면, 신규 파드는 오직 B 존에만 배치될 수 있다. 한편 두 번째 제약 조건에 따르면 신규 파드는 오직 node4 노드에만 배치될 수 있다. 스케줄러는 모든 정의된 제약 조건을 만족하는 선택지만 고려하므로, 유효한 유일한 선택지는 신규 파드를 node4에 배치하는 것이다.

예시: 상충하는 토폴로지 분배 제약 조건

다중 제약 조건이 서로 충돌할 수 있다. 3개의 노드를 가지는 클러스터 하나가 2개의 존에 걸쳐 있다고 가정한다.

graph BT subgraph "zoneB" p4(Pod) --> n3(Node3) p5(Pod) --> n3 end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n1 p3(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3,p4,p5 k8s; class zoneA,zoneB cluster;

만약 two-constraints.yaml (위 예시의 매니페스트)을 클러스터에 적용하면, mypod 파드가 Pending 상태로 유지되는 것을 볼 수 있을 것이다. 이러한 이유는, 첫 번째 제약 조건을 충족하려면 mypod 파드는 B 존에만 배치될 수 있지만, 두 번째 제약 조건에 따르면 mypod 파드는 node2 노드에만 스케줄링될 수 있기 때문이다. 두 제약 조건의 교집합이 공집합이므로, 스케줄러는 파드를 배치할 수 없다.

이 상황을 극복하기 위해, maxSkew 값을 증가시키거나, 제약 조건 중 하나를 whenUnsatisfiable: ScheduleAnyway 를 사용하도록 수정할 수 있다. 상황에 따라, 기존 파드를 수동으로 삭제할 수도 있다. 예를 들어, 버그픽스 롤아웃이 왜 진행되지 않는지 트러블슈팅하는 상황에서 이 방법을 활용할 수 있다.

노드 어피니티(Affinity) 및 노드 셀렉터(Selector)와의 상호 작용

스케줄러는 신규 파드에 spec.nodeSelector 또는 spec.affinity.nodeAffinity가 정의되어 있는 경우, 부합하지 않는 노드들을 차이(skew) 계산에서 생략한다.

예시: 토폴로지 분배 제약조건과 노드 어피니티

5개의 노드를 가지는 클러스터가 A 존에서 C 존까지 걸쳐 있다고 가정한다.

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;
graph BT subgraph "zoneC" n5(Node5) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n5 k8s; class zoneC cluster;

그리고 C 존은 파드 배포에서 제외해야 한다는 사실을 사용자가 알고 있다고 가정한다. 이 경우에, 다음과 같이 매니페스트를 작성하여, mypod 파드가 C 존 대신 B 존에 배치되도록 할 수 있다. 유사하게, 쿠버네티스는 spec.nodeSelector 필드도 고려한다.

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: zone
            operator: NotIn
            values:
            - zoneC
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

암묵적 규칙

여기에 주목할만한 몇 가지 암묵적인 규칙이 있다.

  • 신규 파드와 동일한 네임스페이스를 갖는 파드만이 매칭의 후보가 된다.

  • topologySpreadConstraints[*].topologyKey 가 없는 노드는 무시된다. 이것은 다음을 의미한다.

    1. 이러한 노드에 위치한 파드는 maxSkew 계산에 영향을 미치지 않는다. 위의 예시에서, node1 노드는 "zone" 레이블을 가지고 있지 않다고 가정하면, 파드 2개는 무시될 것이고, 이런 이유로 신규 파드는 A 존으로 스케줄된다.
    2. 신규 파드는 이런 종류의 노드에 스케줄 될 기회가 없다. 위의 예시에서, 잘못 타이핑된 zone-typo: zoneC 레이블을 갖는 node5 노드가 있다고 가정하자(zone 레이블은 설정되지 않음). node5 노드가 클러스터에 편입되면, 해당 노드는 무시되고 이 워크로드의 파드는 해당 노드에 스케줄링되지 않는다.
  • 신규 파드의 topologySpreadConstraints[*].labelSelector가 자체 레이블과 일치하지 않을 경우 어떻게 되는지 알고 있어야 한다. 위의 예시에서, 신규 파드의 레이블을 제거해도, 제약 조건이 여전히 충족되기 때문에 이 파드는 B 존의 노드에 배치될 수 있다. 그러나, 배치 이후에도 클러스터의 불균형 정도는 변경되지 않는다. 여전히 A 존은 foo: bar 레이블을 가지는 2개의 파드를 가지고 있고, B 존도 foo: bar 레이블을 가지는 1개의 파드를 가지고 있다. 만약 결과가 예상과 다르다면, 워크로드의 topologySpreadConstraints[*].labelSelector를 파드 템플릿의 레이블과 일치하도록 업데이트한다.

클러스터 수준의 기본 제약 조건

클러스터에 대한 기본 토폴로지 분배 제약 조건을 설정할 수 있다. 기본 토폴로지 분배 제약 조건은 다음과 같은 경우에만 파드에 적용된다.

  • .spec.topologySpreadConstraints 에 어떠한 제약 조건도 정의되어 있지 않는 경우.
  • 서비스, 레플리카셋(ReplicaSet), 스테이트풀셋(StatefulSet), 또는 레플리케이션컨트롤러(ReplicationController)에 속해있는 경우.

기본 제약 조건은 스케줄링 프로파일내의 플러그인 인자 중 하나로 설정할 수 있다. 제약 조건은 위에서 설명한 것과 동일한 API를 이용하여 정의되는데, 다만 labelSelector는 비워 두어야 한다. 셀렉터는 파드가 속한 서비스, 레플리카셋, 스테이트풀셋, 또는 레플리케이션 컨트롤러를 바탕으로 계산한다.

예시 구성은 다음과 같다.

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 1
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

내장 기본 제약 조건

기능 상태: Kubernetes v1.24 [stable]

파드 토폴로지 분배에 대해 클러스터 수준의 기본 제약을 설정하지 않으면, kube-scheduler는 다음과 같은 기본 토폴로지 제약이 설정되어 있는 것처럼 동작한다.

defaultConstraints:
  - maxSkew: 3
    topologyKey: "kubernetes.io/hostname"
    whenUnsatisfiable: ScheduleAnyway
  - maxSkew: 5
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: ScheduleAnyway

또한, 같은 동작을 제공하는 레거시 SelectorSpread 플러그인은 기본적으로 비활성화되어 있다.

클러스터에 기본 파드 분배 제약 조건을 사용하지 않으려면, PodTopologySpread 플러그인 구성에서 defaultingTypeList 로 설정하고 defaultConstraints 를 비워두어 기본값을 비활성화할 수 있다.

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints: []
          defaultingType: List

파드어피니티(PodAffinity) 및 파드안티어피니티(PodAntiAffinity)와의 비교

쿠버네티스에서, 파드간 어피니티 및 안티 어피니티는 파드가 다른 파드에 서로 어떤 연관 관계를 지니며 스케줄링되는지를 제어하며, 이는 파드들이 서로 더 밀집되도록 하거나 흩어지도록 하는 것을 의미한다.

podAffinity
파드를 끌어당긴다. 조건이 충족되는 토폴로지 도메인에는 원하는 수의 파드를 얼마든지 채울 수 있다.
podAntiAffinity
파드를 밀어낸다. 이를 requiredDuringSchedulingIgnoredDuringExecution 모드로 설정하면 각 토폴로지 도메인에는 하나의 파드만 스케줄링될 수 있다. 반면 preferredDuringSchedulingIgnoredDuringExecution로 설정하면 제약 조건이 강제성을 잃게 된다.

더 세밀한 제어를 위해, 토폴로지 분배 제약 조건을 지정하여 다양한 토폴로지 도메인에 파드를 분배할 수 있고, 이를 통해 고 가용성 또는 비용 절감을 달성할 수 있다. 이는 또한 워크로드의 롤링 업데이트와 레플리카의 원활한 스케일링 아웃에 도움이 될 수 있다.

더 자세한 내용은 파드 토폴로지 분배 제약 조건에 대한 개선 제안의 동기(motivation) 섹션을 참고한다.

알려진 제한사항

  • 파드가 제거된 이후에도 제약 조건이 계속 충족된다는 보장은 없다. 예를 들어 디플로이먼트를 스케일링 다운하면 그 결과로 파드의 분포가 불균형해질 수 있다.

    Descheduler와 같은 도구를 사용하여 파드 분포를 다시 균형있게 만들 수 있다.

  • 테인트된(tainted) 노드에 매치된 파드도 계산에 포함된다. 이슈 80921을 본다.

  • 스케줄러는 클러스터가 갖는 모든 존 또는 다른 토폴로지 도메인에 대한 사전 지식을 갖고 있지 않다. 이 정보들은 클러스터의 기존 노드로부터 획득된다. 이로 인해 오토스케일된 클러스터에서 문제가 발생할 수 있는데, 예를 들어 노드 풀(또는 노드 그룹)이 0으로 스케일 다운되고, 클러스터가 다시 스케일 업 되기를 기대하는 경우, 해당 토폴로지 도메인은 적어도 1개의 노드가 존재하기 전에는 고려가 되지 않을 것이다.

    이를 극복하기 위해, 파드 토폴로지 분배 제약 조건과 전반적인 토폴로지 도메인 집합에 대한 정보를 인지하고 동작하는 클러스터 오토스케일링 도구를 이용할 수 있다.

다음 내용

6 - 테인트(Taints)와 톨러레이션(Tolerations)

노드 어피니티노드 셋을 (기본 설정 또는 어려운 요구 사항으로) 끌어들이는 파드의 속성이다. 테인트 는 그 반대로, 노드가 파드 셋을 제외시킬 수 있다.

톨러레이션 은 파드에 적용된다. 톨러레이션을 통해 스케줄러는 그와 일치하는 테인트가 있는 파드를 스케줄할 수 있다. 톨러레이션은 스케줄을 허용하지만 보장하지는 않는다. 스케줄러는 그 기능의 일부로서 다른 매개변수를 고려한다.

테인트와 톨러레이션은 함께 작동하여 파드가 부적절한 노드에 스케줄되지 않게 한다. 하나 이상의 테인트가 노드에 적용되는데, 이것은 노드가 테인트를 용인하지 않는 파드를 수용해서는 안 된다는 것을 나타낸다.

개요

kubectl taint를 사용하여 노드에 테인트을 추가한다. 예를 들면 다음과 같다.

kubectl taint nodes node1 key1=value1:NoSchedule

node1 노드에 테인트을 배치한다. 테인트에는 키 key1, 값 value1 및 테인트 이펙트(effect) NoSchedule 이 있다. 이는 일치하는 톨러레이션이 없으면 파드를 node1 에 스케줄할 수 없음을 의미한다.

위에서 추가했던 테인트를 제거하려면, 다음을 실행한다.

kubectl taint nodes node1 key1=value1:NoSchedule-

PodSpec에서 파드에 대한 톨러레이션를 지정한다. 다음의 톨러레이션은 위의 kubectl taint 라인에 의해 생성된 테인트와 "일치"하므로, 어느 쪽 톨러레이션을 가진 파드이던 node1 에 스케줄 될 수 있다.

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
tolerations:
- key: "key1"
  operator: "Exists"
  effect: "NoSchedule"

톨러레이션을 사용하는 파드의 예는 다음과 같다.

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "example-key"
    operator: "Exists"
    effect: "NoSchedule"

지정하지 않으면 operator 의 기본값은 Equal 이다.

톨러레이션은, 키와 이펙트가 동일한 경우에 테인트와 "일치"한다. 그리고 다음의 경우에도 마찬가지다.

  • operatorExists 인 경우(이 경우 value 를 지정하지 않아야 함), 또는
  • operatorEqual 이고 valuevalue 로 같다.

위의 예는 NoScheduleeffect 를 사용했다. 또는, PreferNoScheduleeffect 를 사용할 수 있다. 이것은 NoSchedule 의 "기본 설정(preference)" 또는 "소프트(soft)" 버전이다. 시스템은 노드의 테인트를 허용하지 않는 파드를 배치하지 않으려고 시도 하지만, 필요하지는 않다. 세 번째 종류의 effect 는 나중에 설명할 NoExecute 이다.

동일한 노드에 여러 테인트를, 동일한 파드에 여러 톨러레이션을 둘 수 있다. 쿠버네티스가 여러 테인트 및 톨러레이션을 처리하는 방식은 필터와 같다. 모든 노드의 테인트로 시작한 다음, 파드에 일치하는 톨러레이션이 있는 것을 무시한다. 무시되지 않은 나머지 테인트는 파드에 표시된 이펙트를 가진다. 특히,

  • NoSchedule 이펙트가 있는 무시되지 않은 테인트가 하나 이상 있으면 쿠버네티스는 해당 노드에 파드를 스케줄하지 않는다.
  • NoSchedule 이펙트가 있는 무시되지 않은 테인트가 없지만 PreferNoSchedule 이펙트가 있는 무시되지 않은 테인트가 하나 이상 있으면 쿠버네티스는 파드를 노드에 스케쥴하지 않으려고 시도 한다
  • NoExecute 이펙트가 있는 무시되지 않은 테인트가 하나 이상 있으면 파드가 노드에서 축출되고(노드에서 이미 실행 중인 경우), 노드에서 스케줄되지 않는다(아직 실행되지 않은 경우).

예를 들어, 이와 같은 노드를 테인트하는 경우는 다음과 같다.

kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule

그리고 파드에는 두 가지 톨러레이션이 있다.

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"

이 경우, 세 번째 테인트와 일치하는 톨러레이션이 없기 때문에, 파드는 노드에 스케줄 될 수 없다. 그러나 세 번째 테인트가 파드에서 용인되지 않는 세 가지 중 하나만 있기 때문에, 테인트가 추가될 때 노드에서 이미 실행 중인 경우, 파드는 계속 실행할 수 있다.

일반적으로, NoExecute 이펙트가 있는 테인트가 노드에 추가되면, 테인트를 용인하지 않는 파드는 즉시 축출되고, 테인트를 용인하는 파드는 축출되지 않는다. 그러나 NoExecute 이펙트가 있는 톨러레이션은 테인트가 추가된 후 파드가 노드에 바인딩된 시간을 지정하는 선택적 tolerationSeconds 필드를 지정할 수 있다. 예를 들어,

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  tolerationSeconds: 3600

이것은 이 파드가 실행 중이고 일치하는 테인트가 노드에 추가되면, 파드는 3600초 동안 노드에 바인딩된 후, 축출된다는 것을 의미한다. 그 전에 테인트를 제거하면, 파드가 축출되지 않는다.

유스케이스 예시

테인트 및 톨러레이션은 파드를 노드에서 멀어지게 하거나 실행되지 않아야 하는 파드를 축출할 수 있는 유연한 방법이다. 유스케이스 중 일부는 다음과 같다.

  • 전용 노드: 특정 사용자들이 독점적으로 사용하도록 노드 셋을 전용하려면, 해당 노드에 테인트를 추가(예: kubectl taint nodes nodename dedicated=groupName:NoSchedule)한 다음 해당 톨러레이션을 그들의 파드에 추가할 수 있다(사용자 정의 [어드미션 컨트롤러] (/docs/reference/access-authn-authz/admission-controllers/)를 작성하면 가장 쉽게 수행할 수 있음). 그런 다음 톨러레이션이 있는 파드는 테인트된(전용) 노드와 클러스터의 다른 노드를 사용할 수 있다. 노드를 특정 사용자들에게 전용으로 지정하고 그리고 그 사용자들이 전용 노드 사용하려면, 동일한 노드 셋에 테인트와 유사한 레이블을 추가해야 하고(예: dedicated=groupName), 어드미션 컨트롤러는 추가로 파드가 dedicated=groupName 으로 레이블이 지정된 노드에만 스케줄될 수 있도록 노드 어피니티를 추가해야 한다.

  • 특별한 하드웨어가 있는 노드: 작은 서브셋의 노드에 특별한 하드웨어(예: GPU)가 있는 클러스터에서는, 특별한 하드웨어가 필요하지 않는 파드를 해당 노드에서 분리하여, 나중에 도착하는 특별한 하드웨어가 필요한 파드를 위한 공간을 남겨두는 것이 바람직하다. 이는 특별한 하드웨어가 있는 노드(예: kubectl taint nodes nodename special=true:NoSchedule 또는 kubectl taint nodes nodename special=true:PreferNoSchedule)에 테인트를 추가하고 특별한 하드웨어를 사용하는 파드에 해당 톨러레이션을 추가하여 수행할 수 있다. 전용 노드 유스케이스에서와 같이, 사용자 정의 어드미션 컨트롤러를 사용하여 톨러레이션를 적용하는 것이 가장 쉬운 방법이다. 예를 들어, 확장된 리소스를 사용하여 특별한 하드웨어를 나타내고, 확장된 리소스 이름으로 특별한 하드웨어 노드를 테인트시키고 ExtendedResourceToleration 어드미션 컨트롤러를 실행하는 것을 권장한다. 이제, 노드가 테인트되었으므로, 톨러레이션이 없는 파드는 스케줄되지 않는다. 그러나 확장된 리소스를 요청하는 파드를 제출하면, ExtendedResourceToleration 어드미션 컨트롤러가 파드에 올바른 톨러레이션을 자동으로 추가하고 해당 파드는 특별한 하드웨어 노드에서 스케줄된다. 이렇게 하면 이러한 특별한 하드웨어 노드가 해당 하드웨어를 요청하는 파드가 전용으로 사용하며 파드에 톨러레이션을 수동으로 추가할 필요가 없다.

  • 테인트 기반 축출: 노드 문제가 있을 때 파드별로 구성 가능한 축출 동작은 다음 섹션에서 설명한다.

테인트 기반 축출

기능 상태: Kubernetes v1.18 [stable]

앞에서 우리는 노드에서 이미 실행 중인 파드에 영향을 주는 NoExecute 테인트 이펙트를 다음과 같이 언급했다.

  • 테인트를 용인하지 않는 파드는 즉시 축출된다.
  • 톨러레이션 명세에 tolerationSeconds 를 지정하지 않고 테인트를 용인하는 파드는 계속 바인딩된다.
  • tolerationSeconds 가 지정된 테인트를 용인하는 파드는 지정된 시간 동안 바인딩된 상태로 유지된다.

노드 컨트롤러는 특정 컨디션이 참일 때 자동으로 노드를 테인트시킨다. 다음은 빌트인 테인트이다.

  • node.kubernetes.io/not-ready: 노드가 준비되지 않았다. 이는 NodeCondition Ready 가 "False"로 됨에 해당한다.
  • node.kubernetes.io/unreachable: 노드가 노드 컨트롤러에서 도달할 수 없다. 이는 NodeCondition Ready 가 "Unknown"로 됨에 해당한다.
  • node.kubernetes.io/memory-pressure: 노드에 메모리 할당 압박이 있다.
  • node.kubernetes.io/disk-pressure: 노드에 디스크 할당 압박이 있다.
  • node.kubernetes.io/pid-pressure: 노드에 PID 할당 압박이 있다.
  • node.kubernetes.io/network-unavailable: 노드의 네트워크를 사용할 수 없다.
  • node.kubernetes.io/unschedulable: 노드를 스케줄할 수 없다.
  • node.cloudprovider.kubernetes.io/uninitialized: "외부" 클라우드 공급자로 kubelet을 시작하면, 이 테인트가 노드에서 사용 불가능으로 표시되도록 설정된다. 클라우드-컨트롤러-관리자의 컨트롤러가 이 노드를 초기화하면, kubelet이 이 테인트를 제거한다.

노드가 축출될 경우, 노드 컨트롤러 또는 kubelet은 NoExecute 이펙트로 관련 테인트를 추가한다. 장애 상태가 정상으로 돌아오면 kubelet 또는 노드 컨트롤러가 관련 테인트를 제거할 수 있다.

이 기능을 tolerationSeconds 와 함께 사용하면, 파드에서 이러한 문제 중 하나 또는 둘 다가 있는 노드에 바인딩된 기간을 지정할 수 있다.

예를 들어, 로컬 상태가 많은 애플리케이션은 네트워크 분할의 장애에서 네트워크가 복구된 후에 파드가 축출되는 것을 피하기 위해 오랫동안 노드에 바인딩된 상태를 유지하려고 할 수 있다. 이 경우 파드가 사용하는 톨러레이션은 다음과 같다.

tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

데몬셋 파드는 tolerationSeconds 가 없는 다음 테인트에 대해 NoExecute 톨러레이션를 가지고 생성된다.

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/not-ready

이렇게 하면 이러한 문제로 인해 데몬셋 파드가 축출되지 않는다.

컨디션(condition)을 기준으로 노드 테인트하기

컨트롤 플레인은 노드 컨트롤러를 이용하여 노드 컨디션에 대한 NoSchedule 효과를 사용하여 자동으로 테인트를 생성한다.

스케줄러는 스케줄링 결정을 내릴 때 노드 컨디션을 확인하는 것이 아니라 테인트를 확인한다. 이렇게 하면 노드 컨디션이 스케줄링에 직접적인 영향을 주지 않는다. 예를 들어 DiskPressure 노드 컨디션이 활성화된 경우 컨트롤 플레인은 node.kubernetes.io/disk-pressure 테인트를 추가하고 영향을 받는 노드에 새 파드를 할당하지 않는다. MemoryPressure 노드 컨디션이 활성화되면 컨트롤 플레인이 node.kubernetes.io/memory-pressure 테인트를 추가한다.

새로 생성된 파드에 파드 톨러레이션을 추가하여 노드 컨디션을 무시하도록 할 수 있다. 또한 컨트롤 플레인은 BestEffort 이외의 QoS 클래스를 가지는 파드에 node.kubernetes.io/memory-pressure 톨러레이션을 추가한다. 이는 쿠버네티스가 Guaranteed 또는 Burstable QoS 클래스를 갖는 파드(메모리 요청이 설정되지 않은 파드 포함)를 마치 그 파드들이 메모리 압박에 대처 가능한 것처럼 다루는 반면, 새로운 BestEffort 파드는 영향을 받는 노드에 할당하지 않기 때문이다.

데몬셋 컨트롤러는 다음의 NoSchedule 톨러레이션을 모든 데몬에 자동으로 추가하여, 데몬셋이 중단되는 것을 방지한다.

  • node.kubernetes.io/memory-pressure
  • node.kubernetes.io/disk-pressure
  • node.kubernetes.io/pid-pressure (1.14 이상)
  • node.kubernetes.io/unschedulable (1.10 이상)
  • node.kubernetes.io/network-unavailable (호스트 네트워크만 해당)

이러한 톨러레이션을 추가하면 이전 버전과의 호환성이 보장된다. 데몬셋에 임의의 톨러레이션을 추가할 수도 있다.

다음 내용

7 - 스케줄러 성능 튜닝

기능 상태: Kubernetes 1.14 [beta]

kube-scheduler는 쿠버네티스의 기본 스케줄러이다. 그것은 클러스터의 노드에 파드를 배치하는 역할을 한다.

파드의 스케줄링 요건을 충족하는 클러스터의 노드를 파드에 적합한(feasible) 노드라고 한다. 스케줄러는 파드에 대해 적합한 노드를 찾고 기능 셋을 실행하여 해당 노드의 점수를 측정한다. 그리고 스케줄러는 파드를 실행하는데 적합한 모든 노드 중 가장 높은 점수를 가진 노드를 선택한다. 이후 스케줄러는 바인딩 이라는 프로세스로 API 서버에 해당 결정을 통지한다.

본 페이지에서는 상대적으로 큰 규모의 쿠버네티스 클러스터에 대한 성능 튜닝 최적화에 대해 설명한다.

큰 규모의 클러스터에서는 스케줄러의 동작을 튜닝하여 응답 시간 (새 파드가 빠르게 배치됨)과 정확도(스케줄러가 배치 결정을 잘 못하는 경우가 드물게 됨) 사이에서의 스케줄링 결과를 균형 잡을 수 있다.

kube-scheduler 의 percentageOfNodesToScore 설정을 통해 이 튜닝을 구성 한다. 이 KubeSchedulerConfiguration 설정에 따라 클러스터의 노드를 스케줄링할 수 있는 임계값이 결정된다.

임계값 설정하기

percentageOfNodesToScore 옵션은 0과 100 사이의 값을 허용한다. 값 0은 kube-scheduler가 컴파일 된 기본값을 사용한다는 것을 나타내는 특별한 숫자이다. percentageOfNodesToScore 를 100 보다 높게 설정해도 kube-scheduler는 마치 100을 설정한 것처럼 작동한다.

값을 변경하려면, kube-scheduler 구성 파일을 편집한 다음 스케줄러를 재시작한다. 대부분의 경우, 구성 파일은 /etc/kubernetes/config/kube-scheduler.yaml 에서 찾을 수 있다.

이를 변경한 후에 다음을 실행해서

kubectl get pods -n kube-system | grep kube-scheduler

kube-scheduler 컴포넌트가 정상인지 확인할 수 있다.

노드 스코어링(scoring) 임계값

스케줄링 성능을 향상시키기 위해 kube-scheduler는 실행 가능한 노드가 충분히 발견되면 이를 찾는 것을 중단할 수 있다. 큰 규모의 클러스터에서는 모든 노드를 고려하는 고지식한 접근 방식에 비해 시간이 절약된다.

클러스터에 있는 모든 노드의 정수 백분율로 충분한 노두의 수에 대한 임계값을 지정한다. kube-scheduler는 이 값을 노드의 정수 값(숫자)로 변환 한다. 스케줄링 중에 kube-scheduler가 구성된 비율을 초과 할만큼 충분히 실행 가능한 노드를 식별한 경우, kube-scheduler는 더 실행 가능한 노드를 찾는 검색을 중지하고 스코어링 단계를 진행한다.

스케줄러가 노드 탐색을 반복(iterate)하는 방법 은 이 프로세스를 자세히 설명한다.

기본 임계값

임계값을 지정하지 않으면 쿠버네티스는 100 노드 클러스터인 경우 50%, 5000 노드 클러스터인 경우 10%를 산출하는 선형 공식을 사용하여 수치를 계산한다. 자동 값의 하한선은 5% 이다.

즉, percentageOfNodesToScore 를 명시적으로 5보다 작게 설정하지 않은 경우 클러스터가 아무리 크더라도 kube-scheduler는 항상 클러스터의 최소 5%를 스코어링을 한다.

스케줄러가 클러스터의 모든 노드에 스코어링을 하려면 percentageOfNodesToScore 를 100으로 설정 한다.

예시

아래는 percentageOfNodesToScore를 50%로 설정하는 구성 예시이다.

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
  provider: DefaultProvider

...

percentageOfNodesToScore: 50

percentageOfNodesToScore 튜닝

percentageOfNodesToScore는 1과 100 사이의 값이어야 하며 기본값은 클러스터 크기에 따라 계산된다. 또한 100 노드로 하드 코딩된 최솟값도 있다.

이 값을 세팅할 때 중요하고 자세한 사항은, 클러스터에서 적은 수의 노드에 대해서만 적합성을 확인하면, 주어진 파드에 대해서 일부 노드의 점수는 측정이되지 않는다는 것이다. 결과적으로, 주어진 파드를 실행하는데 가장 높은 점수를 가질 가능성이 있는 노드가 점수 측정 단계로 조차 넘어가지 않을 수 있다. 이것은 파드의 이상적인 배치보다 낮은 결과를 초래할 것이다.

percentageOfNodesToScore 를 매우 낮게 설정해서 kube-scheduler가 파드 배치 결정을 잘못 내리지 않도록 해야 한다. 스케줄러의 처리량에 대해 애플리케이션이 중요하고 노드 점수가 중요하지 않은 경우가 아니라면 백분율을 10% 미만으로 설정하지 말아야 한다. 즉, 가능한 한 모든 노드에서 파드를 실행하는 것이 좋다.

스케줄러가 노드 탐색을 반복(iterate)하는 방법

이 섹션은 이 특징의 상세한 내부 방식을 이해하고 싶은 사람들을 위해 작성되었다.

클러스터의 모든 노드가 파드 실행 대상으로 고려되어 공정한 기회를 가지도록, 스케줄러는 라운드 로빈(round robin) 방식으로 모든 노드에 대해서 탐색을 반복한다. 모든 노드가 배열에 나열되어 있다고 생각해보자. 스케줄러는 배열의 시작부터 시작하여 percentageOfNodesToScore에 명시된 충분한 수의 노드를 찾을 때까지 적합성을 확인한다. 그 다음 파드에 대해서는, 스케줄러가 이전 파드를 위한 노드 적합성 확인이 마무리된 지점인 노드 배열의 마지막 포인트부터 확인을 재개한다.

만약 노드들이 다중의 영역(zone)에 있다면, 다른 영역에 있는 노드들이 적합성 확인의 대상이 되도록 스케줄러는 다양한 영역에 있는 노드에 대해서 탐색을 반복한다. 예제로, 2개의 영역에 있는 6개의 노드를 생각해보자.

영역 1: 노드 1, 노드 2, 노드 3, 노드 4
영역 2: 노드 5, 노드 6

스케줄러는 노드의 적합성 평가를 다음의 순서로 실행한다.

노드 1, 노드 5, 노드 2, 노드 6, 노드 3, 노드 4

모든 노드를 검토한 후, 노드 1로 돌아간다.

다음 내용

8 - 리소스 빈 패킹(bin packing)

kube-scheduler의 스케줄링 플러그인 NodeResourcesFit에는, 리소스의 빈 패킹(bin packing)을 지원하는 MostAllocatedRequestedToCapacityRatio라는 두 가지 점수 산정(scoring) 전략이 있다.

MostAllocated 전략을 사용하여 빈 패킹 활성화하기

MostAllocated 전략은 리소스 사용량을 기반으로 할당량이 많은 노드를 높게 평가하여 노드에 점수를 매긴다. 각 리소스 유형별로 가중치를 설정하여 노드 점수에 미치는 영향을 조정할 수 있다.

NodeResourcesFit 플러그인에 대한 MostAllocated 전략을 설정하려면, 다음과 유사한 스케줄러 설정을 사용한다.

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- pluginConfig:
  - args:
      scoringStrategy:
        resources:
        - name: cpu
          weight: 1
        - name: memory
          weight: 1
        - name: intel.com/foo
          weight: 3
        - name: intel.com/bar
          weight: 3
        type: MostAllocated
    name: NodeResourcesFit

기타 파라미터와 기본 구성에 대한 자세한 내용은 NodeResourcesFitArgs에 대한 API 문서를 참조한다.

RequestedToCapacityRatio을 사용하여 빈 패킹 활성화하기

RequestedToCapacityRatio 전략은 사용자가 각 리소스에 대한 가중치와 함께 리소스를 지정하여 용량 대비 요청 비율을 기반으로 노드의 점수를 매길 수 있게 한다. 이를 통해 사용자는 적절한 파라미터를 사용하여 확장된 리소스를 빈 팩으로 만들 수 있어 대규모의 클러스터에서 부족한 리소스의 활용도를 향상시킬 수 있다. 이 전략은 할당된 리소스의 구성된 기능에 따라 노드를 선호하게 한다. NodeResourcesFit점수 기능의 RequestedToCapacityRatio 동작은 scoringStrategy필드를 이용하여 제어할 수 있다. scoringStrategy 필드에서 requestedToCapacityRatioresources라는 두 개의 파라미터를 구성할 수 있다. requestedToCapacityRatio파라미터의 shape를 사용하면 utilizationscore 값을 기반으로 최소 요청 혹은 최대 요청된 대로 기능을 조정할 수 있게 한다. resources 파라미터는 점수를 매길 때 고려할 리소스의 name 과 각 리소스의 가중치를 지정하는 weight 로 구성된다.

다음은 requestedToCapacityRatio 를 이용해 확장된 리소스 intel.com/foointel.com/bar 에 대한 빈 패킹 동작을 설정하는 구성의 예시이다.

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- pluginConfig:
  - args:
      scoringStrategy:
        resources:
        - name: intel.com/foo
          weight: 3
        - name: intel.com/bar
          weight: 3
        requestedToCapacityRatio:
          shape:
          - utilization: 0
            score: 0
          - utilization: 100
            score: 10
        type: RequestedToCapacityRatio
    name: NodeResourcesFit

kube-scheduler 플래그 --config=/path/to/config/file 을 사용하여 KubeSchedulerConfiguration 파일을 참조하면 구성이 스케줄러에 전달된다.

기타 파라미터와 기본 구성에 대한 자세한 내용은 NodeResourcesFitArgs에 대한 API 문서를 참조한다.

점수 기능 튜닝하기

shapeRequestedToCapacityRatio 기능의 동작을 지정하는 데 사용된다.

shape:
 - utilization: 0
   score: 0
 - utilization: 100
   score: 10

위의 인수는 utilization 이 0%인 경우 score 는 0, utilization 이 100%인 경우 10으로 하여, 빈 패킹 동작을 활성화한다. 최소 요청을 활성화하려면 점수 값을 다음과 같이 변경해야 한다.

shape:
  - utilization: 0
    score: 10
  - utilization: 100
    score: 0

resources 는 기본적으로 다음과 같이 설정되는 선택적인 파라미터이다.

resources:
  - name: cpu
    weight: 1
  - name: memory
    weight: 1

다음과 같이 확장된 리소스를 추가하는 데 사용할 수 있다.

resources:
  - name: intel.com/foo
    weight: 5
  - name: cpu
    weight: 3
  - name: memory
    weight: 1

weight 파라미터는 선택 사항이며 지정되지 않은 경우 1로 설정 된다. 또한, weight 는 음수로 설정할 수 없다.

용량 할당을 위해 노드에 점수 매기기

이 섹션은 이 기능 내부의 세부적인 사항을 이해하려는 사람들을 위한 것이다. 아래는 주어진 값의 집합에 대해 노드 점수가 계산되는 방법의 예시이다.

요청된 리소스는 다음과 같다.

intel.com/foo : 2
memory: 256MB
cpu: 2

리소스의 가중치는 다음과 같다.

intel.com/foo : 5
memory: 1
cpu: 3

FunctionShapePoint {{0, 0}, {100, 10}}

노드 1의 사양은 다음과 같다.

Available:
  intel.com/foo: 4
  memory: 1 GB
  cpu: 8

Used:
  intel.com/foo: 1
  memory: 256MB
  cpu: 1

노드 점수는 다음과 같다.

intel.com/foo  = resourceScoringFunction((2+1),4)
               = (100 - ((4-3)*100/4)
               = (100 - 25)
               = 75                       # requested + used = 75% * available
               = rawScoringFunction(75) 
               = 7                        # floor(75/10) 

memory         = resourceScoringFunction((256+256),1024)
               = (100 -((1024-512)*100/1024))
               = 50                       # requested + used = 50% * available
               = rawScoringFunction(50)
               = 5                        # floor(50/10)

cpu            = resourceScoringFunction((2+1),8)
               = (100 -((8-3)*100/8))
               = 37.5                     # requested + used = 37.5% * available
               = rawScoringFunction(37.5)
               = 3                        # floor(37.5/10)

NodeScore   =  (7 * 5) + (5 * 1) + (3 * 3) / (5 + 1 + 3)
            =  5

노드 2의 사양은 다음과 같다.

Available:
  intel.com/foo: 8
  memory: 1GB
  cpu: 8
Used:
  intel.com/foo: 2
  memory: 512MB
  cpu: 6

노드 점수는 다음과 같다.

intel.com/foo  = resourceScoringFunction((2+2),8)
               =  (100 - ((8-4)*100/8)
               =  (100 - 50)
               =  50
               =  rawScoringFunction(50)
               = 5

Memory         = resourceScoringFunction((256+512),1024)
               = (100 -((1024-768)*100/1024))
               = 75
               = rawScoringFunction(75)
               = 7

cpu            = resourceScoringFunction((2+6),8)
               = (100 -((8-8)*100/8))
               = 100
               = rawScoringFunction(100)
               = 10

NodeScore   =  (5 * 5) + (7 * 1) + (10 * 3) / (5 + 1 + 3)
            =  7

다음 내용

9 - 파드 우선순위(priority)와 선점(preemption)

기능 상태: Kubernetes v1.14 [stable]

파드우선순위 를 가질 수 있다. 우선순위는 다른 파드에 대한 상대적인 파드의 중요성을 나타낸다. 파드를 스케줄링할 수 없는 경우, 스케줄러는 우선순위가 낮은 파드를 선점(축출)하여 보류 중인 파드를 스케줄링할 수 있게 한다.

우선순위와 선점을 사용하는 방법

우선순위와 선점을 사용하려면 다음을 참고한다.

  1. 하나 이상의 프라이어리티클래스를 추가한다.

  2. 추가된 프라이어리티클래스 중 하나에 priorityClassName이 설정된 파드를 생성한다. 물론 파드를 직접 생성할 필요는 없다. 일반적으로 디플로이먼트와 같은 컬렉션 오브젝트의 파드 템플릿에 priorityClassName 을 추가한다.

이 단계에 대한 자세한 내용은 계속 읽어보자.

프라이어리티클래스

프라이어리티클래스는 프라이어리티클래스 이름에서 우선순위의 정수 값으로의 매핑을 정의하는 네임스페이스가 아닌(non-namespaced) 오브젝트이다. 이름은 프라이어리티클래스 오브젝트의 메타데이터의 name 필드에 지정된다. 값은 필수 value 필드에 지정되어 있다. 값이 클수록, 우선순위가 높다. 프라이어리티클래스 오브젝트의 이름은 유효한 DNS 서브 도메인 이름이어야 하며, system- 접두사를 붙일 수 없다.

프라이어리티클래스 오브젝트는 10억 이하의 32비트 정수 값을 가질 수 있다. 일반적으로 선점하거나 축출해서는 안되는 중요한 시스템 파드에는 더 큰 숫자가 예약되어 있다. 클러스터 관리자는 원하는 각 매핑에 대해 프라이어리티클래스 오브젝트를 하나씩 생성해야 한다.

프라이어리티클래스에는 globalDefaultdescription 두 개의 선택적인 필드도 있다. globalDefault 필드는 이 프라이어리티클래스의 값을 priorityClassName 이 없는 파드에 사용해야 함을 나타낸다. 시스템에서 globalDefaulttrue 로 설정된 프라이어리티클래스는 하나만 존재할 수 있다. globalDefault 가 설정된 프라이어리티클래스가 없을 경우, priorityClassName 이 없는 파드의 우선순위는 0이다.

description 필드는 임의의 문자열이다. 이 필드는 이 프라이어리티클래스를 언제 사용해야 하는지를 클러스터 사용자에게 알려주기 위한 것이다.

PodPriority와 기존 클러스터에 대한 참고 사항

  • 이 기능없이 기존 클러스터를 업그레이드 하는 경우, 기존 파드의 우선순위는 사실상 0이다.

  • globalDefaulttrue 로 설정된 프라이어리티클래스를 추가해도 기존 파드의 우선순위는 변경되지 않는다. 이러한 프라이어리티클래스의 값은 프라이어리티클래스를 추가한 후 생성된 파드에만 사용된다.

  • 프라이어리티클래스를 삭제하면, 삭제된 프라이어리티클래스의 이름을 사용하는 기존 파드는 변경되지 않고 남아있지만, 삭제된 프라이어리티클래스의 이름을 사용하는 파드는 더 이상 생성할 수 없다.

프라이어리티클래스 예제

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "이 프라이어리티클래스는 XYZ 서비스 파드에만 사용해야 한다."

비-선점 프라이어리티클래스

기능 상태: Kubernetes v1.24 [stable]

preemptionPolicy: Never 를 가진 파드는 낮은 우선순위 파드의 스케줄링 대기열의 앞쪽에 배치되지만, 그 파드는 다른 파드를 축출할 수 없다. 스케줄링 대기 중인 비-선점 파드는 충분한 리소스가 확보되고 스케줄링될 수 있을 때까지 스케줄링 대기열에 대기한다. 다른 파드와 마찬가지로, 비-선점 파드는 스케줄러 백오프(back-off)에 종속된다. 이는 스케줄러가 이러한 파드를 스케줄링하려고 시도하고 스케줄링할 수 없는 경우, 더 적은 횟수로 재시도하여, 우선순위가 낮은 다른 파드를 미리 스케줄링할 수 있음을 의미한다.

비-선점 파드는 다른 우선순위가 높은 파드에 의해 축출될 수 있다.

preemptionPolicy 는 기본값으로 PreemptLowerPriority 로 설정되어, 해당 프라이어리티클래스의 파드가 우선순위가 낮은 파드를 축출할 수 있다(기존의 기본 동작과 동일). preemptionPolicyNever 로 설정된 경우, 해당 프라이어리티클래스의 파드는 비-선점될 것이다.

예제 유스케이스는 데이터 과학 관련 워크로드이다. 사용자는 다른 워크로드보다 우선순위가 높은 잡(job)을 제출할 수 있지만, 실행 중인 파드를 축출하여 기존의 작업을 삭제하지는 않을 것이다. 클러스터 리소스가 "자연스럽게" 충분히 사용할 수 있게 되면, preemptionPolicy: Never 의 우선순위가 높은 잡이 다른 대기 중인 파드보다 먼저 스케줄링된다.

비-선점 프라이어리티클래스 예제

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-nonpreempting
value: 1000000
preemptionPolicy: Never
globalDefault: false
description: "이 프라이어리티클래스는 다른 파드를 축출하지 않는다."

파드 우선순위

프라이어리티클래스가 하나 이상 있으면, 그것의 명세에서 이들 프라이어리티클래스 이름 중 하나를 지정하는 파드를 생성할 수 있다. 우선순위 어드미션 컨트롤러는 priorityClassName 필드를 사용하고 우선순위의 정수 값을 채운다. 프라이어리티클래스를 찾을 수 없으면, 파드가 거부된다.

다음의 YAML은 이전 예제에서 생성된 프라이어리티클래스를 사용하는 파드 구성의 예이다. 우선순위 어드미션 컨트롤러는 명세를 확인하고 파드의 우선순위를 1000000으로 해석한다.

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  priorityClassName: high-priority

스케줄링 순서에 대한 파드 우선순위의 영향

파드 우선순위가 활성화되면, 스케줄러가 우선순위에 따라 보류 중인 파드를 주문하고 보류 중인 파드는 스케줄링 대기열에서 우선순위가 낮은 다른 보류 중인 파드보다 우선한다. 결과적으로, 스케줄링 요구 사항이 충족되는 경우 우선순위가 더 낮은 파드보다 우선순위가 높은 파드가 더 빨리 스케줄링될 수 있다. 이러한 파드를 스케줄링할 수 없는 경우, 스케줄러는 계속 진행하고 우선순위가 낮은 다른 파드를 스케줄링하려고 한다.

선점

파드가 생성되면, 대기열로 이동하여 스케줄링을 기다린다. 스케줄러가 대기열에서 파드를 선택하여 노드에 스케줄링하려고 한다. 파드의 지정된 모든 요구 사항을 충족하는 노드가 없으면, 보류 중인 파드에 대해 선점 로직이 트리거된다. 보류 중인 파드를 P라 하자. 선점 로직은 P보다 우선순위가 낮은 하나 이상의 파드를 제거하면 해당 노드에서 P를 스케줄링할 수 있는 노드를 찾는다. 이러한 노드가 발견되면, 하나 이상의 우선순위가 낮은 파드가 노드에서 축출된다. 파드가 축출된 후, P는 노드에 스케줄링될 수 있다.

사용자 노출 정보

파드 P가 노드 N에서 하나 이상의 파드를 축출할 경우, 파드 P의 상태 nominatedNodeName 필드는 노드 N의 이름으로 설정된다. 이 필드는 스케줄러가 파드 P에 예약된 리소스를 추적하는 데 도움이 되고 사용자에게 클러스터의 선점에 대한 정보를 제공한다.

파드 P는 반드시 "지정된 노드"로 스케줄링되지는 않는다. 스케줄러는 다른 노드에 스케줄링을 시도하기 전에 항상 "지정된 노드"부터 시도한다. 피해자 파드가 축출된 후, 그것은 정상적(graceful)으로 종료되는 기간을 갖는다. 스케줄러가 종료될 피해자 파드를 기다리는 동안 다른 노드를 사용할 수 있게 되면, 스케줄러는 파드 P를 스케줄링하기 위해 다른 노드를 사용할 수 있다. 그 결과, 파드 스펙의 nominatedNodeNamenodeName 은 항상 동일하지 않다. 또한, 스케줄러가 노드 N에서 파드를 축출했지만, 파드 P보다 우선순위가 높은 파드가 도착하면, 스케줄러가 노드 N에 새로운 우선순위가 높은 파드를 제공할 수 있다. 이러한 경우, 스케줄러는 파드 P의 nominatedNodeName 을 지운다. 이렇게하면, 스케줄러는 파드 P가 다른 노드에서 파드를 축출할 수 있도록 한다.

선점의 한계

선점 피해자의 정상적인 종료

파드가 축출되면, 축출된 피해자 파드는 정상적인 종료 기간을 가진다. 피해자 파드는 작업을 종료하고 빠져나가는 데(exit) 많은 시간을 가진다. 그렇지 않으면, 파드는 강제종료(kill) 된다. 이 정상적인 종료 기간은 스케줄러가 파드를 축출하는 지점과 보류 중인 파드(P)를 노드(N)에서 스케줄링할 수 있는 시점 사이의 시간 간격을 만든다. 그 동안, 스케줄러는 보류 중인 다른 파드를 계속 스케줄링한다. 피해자 파드가 빠져나가거나 종료되면, 스케줄러는 보류 대기열에서 파드를 스케줄하려고 한다. 따라서, 일반적으로 스케줄러가 피해자 파드를 축출하는 시점과 파드 P가 스케줄링된 시점 사이에 시간 간격이 있다. 이러한 차이를 최소화하기 위해, 우선순위가 낮은 파드의 정상적인 종료 기간을 0 또는 작은 수로 설정할 수 있다.

PodDisruptionBudget을 지원하지만, 보장하지 않음

Pod Disruption Budget(PDB)은 애플리케이션 소유자가 자발적 중단에서 동시에 다운된 복제된 애플리케이션의 파드 수를 제한할 수 있다. 쿠버네티스는 파드를 선점할 때 PDB를 지원하지만, PDB를 따르는 것이 최선의 노력이다. 스케줄러는 선점에 의해 PDB를 위반하지 않은 피해자 파드를 찾으려고 하지만, 그러한 피해자 파드가 발견되지 않으면, 선점은 여전히 발생하며, PDB를 위반하더라도 우선순위가 낮은 파드는 제거된다.

우선순위가 낮은 파드에 대한 파드-간 어피니티

이 질문에 대한 답변이 '예'인 경우에만 노드가 선점 대상으로 간주된다. "대기 중인 파드보다 우선순위가 낮은 모든 파드가 노드에서 제거되면, 보류 중인 파드를 노드에 스케줄링할 수 있습니까?"

보류 중인 파드가 노드에 있는 하나 이상의 우선순위가 낮은 파드에 대한 파드-간 어피니티를 가진 경우에, 우선순위가 낮은 파드가 없을 때 파드-간 어피니티 규칙을 충족할 수 없다. 이 경우, 스케줄러는 노드의 파드를 축출하지 않는다. 대신, 다른 노드를 찾는다. 스케줄러가 적합한 노드를 찾거나 찾지 못할 수 있다. 보류 중인 파드를 스케줄링할 수 있다는 보장은 없다.

이 문제에 대한 권장 솔루션은 우선순위가 같거나 높은 파드에 대해서만 파드-간 어피니티를 생성하는 것이다.

교차 노드 선점

보류 중인 파드 P가 노드 N에 스케줄링될 수 있도록 노드 N이 선점 대상으로 고려되고 있다고 가정한다. 다른 노드의 파드가 축출된 경우에만 P가 N에서 실행 가능해질 수 있다. 예를 들면 다음과 같다.

  • 파드 P는 노드 N에 대해 고려된다.
  • 파드 Q는 노드 N과 동일한 영역의 다른 노드에서 실행 중이다.
  • 파드 P는 파드 Q(topologyKey: topology.kubernetes.io/zone)와 영역(zone) 전체의 안티-어피니티를 갖는다.
  • 영역에서 파드 P와 다른 파드 간의 안티-어피니티에 대한 다른 경우는 없다.
  • 노드 N에서 파드 P를 스케줄링하기 위해, 파드 Q를 축출할 수 있지만, 스케줄러는 교차-노드 선점을 수행하지 않는다. 따라서, 파드 P가 노드 N에서 스케줄링할 수 없는 것으로 간주된다.

파드 Q가 노드에서 제거되면, 파드 안티-어피니티 위반이 사라지고, 파드 P는 노드 N에서 스케줄링될 수 있다.

수요가 충분하고 합리적인 성능의 알고리즘을 찾을 경우 향후 버전에서 교차 노드 선점의 추가를 고려할 수 있다.

문제 해결

파드 우선순위와 선점은 원하지 않는 부작용을 가질 수 있다. 다음은 잠재적 문제의 예시와 이를 해결하는 방법이다.

파드가 불필요하게 선점(축출)됨

선점은 우선순위가 높은 보류 중인 파드를 위한 공간을 만들기 위해 리소스 압박을 받고 있는 클러스터에서 기존 파드를 제거한다. 실수로 특정 파드에 높은 우선순위를 부여하면, 의도하지 않은 높은 우선순위 파드가 클러스터에서 선점을 유발할 수 있다. 파드 우선순위는 파드 명세에서 priorityClassName 필드를 설정하여 지정한다. 그런 다음 우선순위의 정수 값이 분석되어 podSpecpriority 필드에 채워진다.

문제를 해결하기 위해, 해당 파드가 우선순위가 낮은 클래스를 사용하도록 priorityClassName 을 변경하거나, 해당 필드를 비워둘 수 있다. 빈 priorityClassName 은 기본값이 0으로 해석된다.

파드가 축출되면, 축출된 파드에 대한 이벤트가 기록된다. 선점은 클러스터가 파드에 대한 리소스를 충분히 가지지 못한 경우에만 발생한다. 이러한 경우, 선점은 보류 중인 파드(선점자)의 우선순위가 피해자 파드보다 높은 경우에만 발생한다. 보류 중인 파드가 없거나, 보류 중인 파드의 우선순위가 피해자 파드와 같거나 낮은 경우 선점이 발생하지 않아야 한다. 그러한 시나리오에서 선점이 발생하면, 이슈를 올리기 바란다.

파드가 축출되었지만, 선점자는 스케줄링되지 않음

파드가 축출되면, 요청된 정상적인 종료 기간(기본적으로 30초)이 주어진다. 이 기간 내에 대상 파드가 종료되지 않으면, 강제 종료된다. 모든 피해자 파드가 사라지면, 선점자 파드를 스케줄링할 수 있다.

선점자 파드가 피해자 파드가 없어지기를 기다리는 동안, 동일한 노드에 적합한 우선순위가 높은 파드가 생성될 수 있다. 이 경우, 스케줄러는 선점자 대신 우선순위가 높은 파드를 스케줄링한다.

이것은 예상된 동작이다. 우선순위가 높은 파드는 우선순위가 낮은 파드를 대체해야 한다.

우선순위가 높은 파드는 우선순위가 낮은 파드보다 우선함

스케줄러가 보류 중인 파드를 실행할 수 있는 노드를 찾으려고 한다. 노드를 찾지 못하면, 스케줄러는 임의의 노드에서 우선순위가 낮은 파드를 제거하여 보류 중인 파드를 위한 공간을 만든다. 우선순위가 낮은 파드가 있는 노드가 보류 중인 파드를 실행할 수 없는 경우, 스케줄러는 선점을 위해 우선순위가 높은 다른 노드(다른 노드의 파드와 비교)를 선택할 수 있다. 피해자 파드는 여전히 선점자 파드보다 우선순위가 낮아야 한다.

선점할 수 있는 여러 노드가 있는 경우, 스케줄러는 우선순위가 가장 낮은 파드 세트를 가진 노드를 선택하려고 한다. 그러나, 이러한 파드가 위반될 PodDisruptionBudget을 가지고 있고 축출된 경우 스케줄러는 우선순위가 높은 파드를 가진 다른 노드를 선택할 수 있다.

선점을 위해 여러 개의 노드가 존재하고 위의 시나리오 중 어느 것도 적용되지 않는 경우, 스케줄러는 우선순위가 가장 낮은 노드를 선택한다.

파드 우선순위와 서비스 품질 간의 상호 작용

파드 우선순위와 QoS 클래스는 상호 작용이 거의 없고 QoS 클래스를 기반으로 파드 우선순위를 설정하는 데 대한 기본 제한이 없는 두 개의 직교(orthogonal) 기능이다. 스케줄러의 선점 로직은 선점 대상을 선택할 때 QoS를 고려하지 않는다. 선점은 파드 우선순위를 고려하고 우선순위가 가장 낮은 대상 세트를 선택하려고 한다. 우선순위가 가장 높은 파드는 스케줄러가 선점자 파드를 스케줄링할 수 없거나 우선순위가 가장 낮은 파드가 PodDisruptionBudget 으로 보호되는 경우에만, 우선순위가 가장 낮은 파드를 축출 대상으로 고려한다.

kubelet은 우선순위를 사용하여 파드의 노드-압박(node-pressure) 축출 순서를 결정한다. 사용자는 QoS 클래스를 사용하여 어떤 파드가 축출될 것인지 예상할 수 있다. kubelet은 다음의 요소들을 통해서 파드의 축출 순위를 매긴다.

  1. 기아(starved) 리소스 사용량이 요청을 초과하는지 여부
  2. 파드 우선순위
  3. 요청 대비 리소스 사용량

더 자세한 내용은 kubelet 축출을 위한 파드 선택을 참조한다.

kubelet 노드-압박 축출은 사용량이 요청을 초과하지 않는 경우 파드를 축출하지 않는다. 우선순위가 낮은 파드가 요청을 초과하지 않으면, 축출되지 않는다. 요청을 초과하는 우선순위가 더 높은 다른 파드가 축출될 수 있다.

다음 내용

10 - 노드-압박 축출

노드-압박 축출은 kubelet이 노드의 자원을 회수하기 위해 파드를 능동적으로 중단시키는 절차이다.

kubelet은 클러스터 노드의 메모리, 디스크 공간, 파일시스템 inode와 같은 자원을 모니터링한다. 이러한 자원 중 하나 이상이 특정 소모 수준에 도달하면, kubelet은 하나 이상의 파드를 능동적으로 중단시켜 자원을 회수하고 고갈 상황을 방지할 수 있다.

노드-압박 축출 과정에서, kubelet은 축출할 파드의 PodPhaseFailed로 설정함으로써 파드가 종료된다.

노드-압박 축출은 API를 이용한 축출과는 차이가 있다.

kubelet은 이전에 설정된 PodDisruptionBudget 값이나 파드의 terminationGracePeriodSeconds 값을 따르지 않는다. 소프트 축출 임계값을 사용하는 경우, kubelet은 이전에 설정된 eviction-max-pod-grace-period 값을 따른다. 하드 축출 임계값을 사용하는 경우, 파드 종료 시 0s 만큼 기다린 후 종료한다(즉, 기다리지 않고 바로 종료한다).

실패한 파드를 새로운 파드로 교체하는 워크로드 리소스(예: 스테이트풀셋(StatefulSet) 또는 디플로이먼트(Deployment))가 파드를 관리하는 경우, 컨트롤 플레인이나 kube-controller-manager가 축출된 파드를 대신할 새 파드를 생성한다.

kubelet은 축출 결정을 내리기 위해 다음과 같은 다양한 파라미터를 사용한다.

  • 축출 신호
  • 축출 임계값
  • 모니터링 간격

축출 신호

축출 신호는 특정 시점에서 특정 자원의 현재 상태이다. kubelet은 노드에서 사용할 수 있는 리소스의 최소량인 축출 임계값과 축출 신호를 비교하여 축출 결정을 내린다.

kubelet은 다음과 같은 축출 신호를 사용한다.

축출 신호설명
memory.availablememory.available := node.status.capacity[memory] - node.stats.memory.workingSet
nodefs.availablenodefs.available := node.stats.fs.available
nodefs.inodesFreenodefs.inodesFree := node.stats.fs.inodesFree
imagefs.availableimagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFreeimagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
pid.availablepid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc

이 표에서, 설명 열은 kubelet이 축출 신호 값을 계산하는 방법을 나타낸다. 각 축출 신호는 백분율 또는 숫자값을 지원한다. kubelet은 총 용량 대비 축출 신호의 백분율 값을 계산한다.

memory.available 값은 free -m과 같은 도구가 아니라 cgroupfs로부터 도출된다. 이는 free -m이 컨테이너 안에서는 동작하지 않고, 또한 사용자가 node allocatable 기능을 사용하는 경우 자원 부족에 대한 결정은 루트 노드뿐만 아니라 cgroup 계층 구조의 최종 사용자 파드 부분에서도 지역적으로 이루어지기 때문에 중요하다. 이 스크립트는 kubelet이 memory.available을 계산하기 위해 수행하는 동일한 단계들을 재현한다. kubelet은 메모리 압박 상황에서 메모리가 회수 가능하다고 가정하므로, inactive_file(즉, 비활성 LRU 목록의 파일 기반 메모리 바이트 수)을 계산에서 제외한다.

kubelet은 다음과 같은 파일시스템 파티션을 지원한다.

  1. nodefs: 노드의 메인 파일시스템이며, 로컬 디스크 볼륨, emptyDir, 로그 스토리지 등에 사용된다. 예를 들어 nodefs/var/lib/kubelet/을 포함한다.
  2. imagefs: 컨테이너 런타임이 컨테이너 이미지 및 컨테이너 쓰기 가능 레이어를 저장하는 데 사용하는 선택적 파일시스템이다.

kubelet은 이러한 파일시스템을 자동으로 검색하고 다른 파일시스템은 무시한다. kubelet은 다른 구성은 지원하지 않는다.

아래의 kubelet 가비지 수집 기능은 더 이상 사용되지 않으며 축출로 대체되었다.

기존 플래그새로운 플래그이유
--image-gc-high-threshold--eviction-hard 또는 --eviction-soft기존의 축출 신호가 이미지 가비지 수집을 트리거할 수 있음
--image-gc-low-threshold--eviction-minimum-reclaim축출 회수도 동일한 작업을 수행
--maximum-dead-containers-오래된 로그들이 컨테이너의 컨텍스트 외부에 저장된 이후로 사용되지 않음
--maximum-dead-containers-per-container-오래된 로그들이 컨테이너의 컨텍스트 외부에 저장된 이후로 사용되지 않음
--minimum-container-ttl-duration-오래된 로그들이 컨테이너의 컨텍스트 외부에 저장된 이후로 사용되지 않음

축출 임계값

kubelet이 축출 결정을 내릴 때 사용하는 축출 임계값을 사용자가 임의로 설정할 수 있다.

축출 임계값은 [eviction-signal][operator][quantity] 형태를 갖는다.

  • eviction-signal에는 사용할 축출 신호를 적는다.
  • operator에는 관계연산자를 적는다(예: < - 미만)
  • quantity에는 1Gi와 같이 축출 임계값 수치를 적는다. quantity에 들어가는 값은 쿠버네티스가 사용하는 수치 표현 방식과 맞아야 한다. 숫자값 또는 백분율(%)을 사용할 수 있다.

예를 들어, 노드에 총 10Gi의 메모리가 있고 1Gi 아래로 내려갔을 때 축출이 시작되도록 만들고 싶으면, 축출 임계값을 memory.available<10% 또는 memory.available<1Gi 형태로 정할 수 있다. 둘을 동시에 사용할 수는 없다.

소프트 축출 임계값과 하드 축출 임계값을 설정할 수 있다.

소프트 축출 임계값

소프트 축출 임계값은 관리자가 설정하는 유예 시간(필수)과 함께 정의된다. kubelet은 유예 시간이 초과될 때까지 파드를 제거하지 않는다. 유예 시간이 지정되지 않으면 kubelet 시작 시 오류가 반환된다.

kubelet이 축출 과정에서 사용할 수 있도록, '소프트 축출 임계값'과 '최대 허용 파드 종료 유예 시간' 둘 다를 설정할 수 있다. '최대 허용 파드 종료 유예 시간'이 설정되어 있는 상태에서 '소프트 축출 임계값'에 도달하면, kubelet은 두 유예 시간 중 작은 쪽을 적용한다. '최대 허용 파드 종료 유예 시간'을 설정하지 않으면, kubelet은 축출된 파드를 유예 시간 없이 즉시 종료한다.

소프트 축출 임계값을 설정할 때 다음과 같은 플래그를 사용할 수 있다.

  • eviction-soft: 축출 임계값(예: memory.available<1.5Gi)의 집합이며, 지정된 유예 시간동안 이 축출 임계값 조건이 충족되면 파드 축출이 트리거된다.
  • eviction-soft-grace-period: 축출 유예 시간의 집합이며, 소프트 축출 임계값 조건이 이 유예 시간동안 충족되면 파드 축출이 트리거된다.
  • eviction-max-pod-grace-period: '최대 허용 파드 종료 유예 시간(단위: 초)'이며, 소프트 축출 임계값 조건이 충족되어 파드를 종료할 때 사용한다.

하드 축출 임계값

하드 축출 임계값에는 유예 시간이 없다. 하드 축출 임계값 조건이 충족되면, kubelet은 고갈된 자원을 회수하기 위해 파드를 유예 시간 없이 즉시 종료한다.

eviction-hard 플래그를 사용하여 하드 축출 임계값(예: memory.available<1Gi)을 설정할 수 있다.

kubelet은 다음과 같은 하드 축출 임계값을 기본적으로 설정하고 있다.

  • memory.available<100Mi
  • nodefs.available<10%
  • imagefs.available<15%
  • nodefs.inodesFree<5% (리눅스 노드)

이러한 하드 축출 임계값의 기본값은 매개변수가 변경되지 않은 경우에만 설정된다. 어떤 매개변수의 값을 변경한 경우, 다른 매개변수의 값은 기본값으로 상속되지 않고 0으로 설정된다. 사용자 지정 값을 제공하려면, 모든 임계값을 각각 제공해야 한다.

축출 모니터링 시간 간격

kubelet은 housekeeping-interval에 설정된 시간 간격(기본값: 10s)마다 축출 임계값을 확인한다.

노드 컨디션

kubelet은 하드/소프트 축출 임계값 조건이 충족되어 노드 압박이 발생했다는 것을 알리기 위해, 설정된 유예 시간과는 관계없이 노드 컨디션을 보고한다.

kubelet은 다음과 같이 노드 컨디션과 축출 신호를 매핑한다.

노드 컨디션축출 신호설명
MemoryPressurememory.available노드의 가용 메모리 양이 축출 임계값에 도달함
DiskPressurenodefs.available, nodefs.inodesFree, imagefs.available, 또는 imagefs.inodesFree노드의 루트 파일시스템 또는 이미지 파일시스템의 가용 디스크 공간 또는 inode의 수가 축출 임계값에 도달함
PIDPressurepid.available(리눅스) 노드의 가용 프로세스 ID(PID)가 축출 임계값 이하로 내려옴

kubelet은 --node-status-update-frequency에 설정된 시간 간격(기본값: 10s)마다 노드 컨디션을 업데이트한다.

노드 컨디션 진동(oscillation)

경우에 따라, 노드의 축출 신호값이 사전에 설정된 유예 시간 동안 유지되지 않고 소프트 축출 임계값을 중심으로 진동할 수 있다. 이로 인해 노드 컨디션이 계속 truefalse로 바뀌며, 잘못된 축출 결정을 야기할 수 있다.

이러한 진동을 방지하기 위해, eviction-pressure-transition-period 플래그를 사용하여 kubelet이 노드 컨디션을 다른 상태로 바꾸기 위해 기다려야 하는 시간을 설정할 수 있다. 기본값은 5m이다.

노드-수준 자원 회수하기

kubelet은 최종 사용자 파드를 축출하기 전에 노드-수준 자원 회수를 시도한다.

DiskPressure 노드 컨디션이 보고되면, kubelet은 노드의 파일시스템을 기반으로 노드-수준 자원을 회수한다.

imagefs가 있는 경우

컨테이너 런타임이 사용할 전용 imagefs 파일시스템이 노드에 있으면, kubelet은 다음 작업을 수행한다.

  • nodefs 파일시스템이 축출 임계값 조건을 충족하면, kubelet은 종료된 파드와 컨테이너에 대해 가비지 수집을 수행한다.
  • imagefs 파일시스템이 축출 임계값 조건을 충족하면, kubelet은 모든 사용중이지 않은 이미지를 삭제한다.

imagefs가 없는 경우

노드에 nodefs 파일시스템만 있고 이것이 축출 임계값 조건을 충족한 경우, kubelet은 다음 순서로 디스크 공간을 확보한다.

  1. 종료된 파드와 컨테이너에 대해 가비지 수집을 수행한다.
  2. 사용중이지 않은 이미지를 삭제한다.

kubelet 축출을 위한 파드 선택

kubelet이 노드-수준 자원을 회수했음에도 축출 신호가 임계값 아래로 내려가지 않으면, kubelet은 최종 사용자 파드 축출을 시작한다.

kubelet은 파드 축출 순서를 결정하기 위해 다음의 파라미터를 활용한다.

  1. 파드의 자원 사용량이 요청량을 초과했는지 여부
  2. 파드 우선순위
  3. 파드의 자원 요청량 대비 자원 사용량

결과적으로, kubelet은 다음과 같은 순서로 파드의 축출 순서를 정하고 축출을 수행한다.

  1. BestEffort 또는 Burstable 파드 중 자원 사용량이 요청량을 초과한 파드. 이 파드들은 파드들의 우선순위, 그리고 자원 사용량이 요청량을 얼마나 초과했는지에 따라 축출된다.
  2. Guaranteed, Burstable 파드 중 자원 사용량이 요청량보다 낮은 파드는 우선순위에 따라 후순위로 축출된다.

Guaranteed 파드는 모든 컨테이너에 대해 자원 요청량과 제한이 명시되고 그 둘이 동일할 때에만 보장(guaranteed)된다. 다른 파드의 자원 사용으로 인해 Guaranteed 파드가 축출되는 일은 발생하지 않는다. 만약 시스템 데몬(예: kubelet, journald)이 system-reserved 또는 kube-reserved 할당을 통해 예약된 것보다 더 많은 자원을 소비하고, 노드에는 요청량보다 적은 양의 자원을 사용하고 있는 Guaranteed / Burstable 파드만 존재한다면, kubelet은 노드 안정성을 유지하고 자원 고갈이 다른 파드에 미칠 영향을 통제하기 위해 이러한 파드 중 하나를 골라 축출해야 한다. 이 경우, 가장 낮은 Priority를 갖는 파드가 선택된다.

inodesPIDs에 대한 요청량은 정의하고 있지 않기 때문에, kubelet이 inode 또는 PID 고갈 때문에 파드를 축출할 때에는 파드의 Priority를 이용하여 축출 순위를 정한다.

노드에 전용 imagefs 파일시스템이 있는지 여부에 따라 kubelet이 파드 축출 순서를 정하는 방식에 차이가 있다.

imagefs가 있는 경우

nodefs로 인한 축출의 경우, kubelet은 nodefs 사용량(모든 컨테이너의 로컬 볼륨 + 로그)을 기준으로 축출 순서를 정한다.

imagefs로 인한 축출의 경우, kubelet은 모든 컨테이너의 쓰기 가능한 레이어(writable layer) 사용량을 기준으로 축출 순서를 정한다.

imagefs가 없는 경우

nodefs로 인한 축출의 경우, kubelet은 각 파드의 총 디스크 사용량(모든 컨테이너의 로컬 볼륨 + 로그 + 쓰기 가능한 레이어)을 기준으로 축출 순서를 정한다.

최소 축출 회수량

경우에 따라, 파드를 축출했음에도 적은 양의 자원만이 회수될 수 있다. 이로 인해 kubelet이 반복적으로 축출 임계값 도달을 감지하고 여러 번의 축출을 수행할 수 있다.

--eviction-minimum-reclaim 플래그 또는 kubelet 설정 파일을 이용하여 각 자원에 대한 최소 회수량을 설정할 수 있다. kubelet이 자원 부족 상황을 감지하면, 앞서 설정한 최소 회수량에 도달할때까지 회수를 계속 진행한다.

예를 들어, 다음 YAML은 최소 회수량을 정의하고 있다.

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
  memory.available: "500Mi"
  nodefs.available: "1Gi"
  imagefs.available: "100Gi"
evictionMinimumReclaim:
  memory.available: "0Mi"
  nodefs.available: "500Mi"
  imagefs.available: "2Gi"

이 예제에서, 만약 nodefs.available 축출 신호가 축출 임계값 조건에 도달하면, kubelet은 축출 신호가 임계값인 1Gi에 도달할 때까지 자원을 회수하며, 이어서 축출 신호가 1.5Gi에 도달할 때까지 최소 500Mi 이상의 자원을 회수한다.

유사한 방식으로, kubelet은 imagefs.available 축출 신호가 102Gi에 도달할 때까지 imagefs 자원을 회수한다.

모든 자원에 대해 eviction-minimum-reclaim의 기본값은 0이다.

노드 메모리 부족 시의 동작

kubelet의 메모리 회수가 가능하기 이전에 노드에 메모리 부족(out of memory, 이하 OOM) 이벤트가 발생하면, 노드는 oom_killer에 의존한다.

kubelet은 각 파드에 설정된 QoS를 기반으로 각 컨테이너에 oom_score_adj 값을 설정한다.

서비스 품질(Quality of Service)oom_score_adj
Guaranteed-997
BestEffort1000
Burstablemin(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

노드가 OOM을 겪기 전에 kubelet이 메모리를 회수하지 못하면, oom_killer가 노드의 메모리 사용률 백분율을 이용하여 oom_score를 계산하고, 각 컨테이너의 실질 oom_score를 구하기 위해 oom_score_adj를 더한다. 그 뒤 oom_score가 가장 높은 컨테이너부터 종료시킨다.

이는 곧, 스케줄링 요청에 비해 많은 양의 메모리를 사용하면서 QoS가 낮은 파드에 속한 컨테이너가 먼저 종료됨을 의미한다.

파드 축출과 달리, 컨테이너가 OOM으로 인해 종료되면, kubelet이 컨테이너의 RestartPolicy를 기반으로 컨테이너를 다시 실행할 수 있다.

추천 예시

아래 섹션에서 축출 설정에 대한 추천 예시를 소개한다.

스케줄 가능한 자원과 축출 정책

kubelet에 축출 정책을 설정할 때, 만약 어떤 파드 배치가 즉시 메모리 압박을 야기하기 때문에 축출을 유발한다면 스케줄러가 그 파드 배치를 수행하지 않도록 설정해야 한다.

다음 시나리오를 가정한다.

  • 노드 메모리 용량: 10Gi
  • 운영자는 시스템 데몬(커널, kubelet 등)을 위해 메모리 용량의 10%를 확보해 놓고 싶어 한다.
  • 운영자는 시스템 OOM 발생을 줄이기 위해 메모리 사용률이 95%인 상황에서 파드를 축출하고 싶어한다.

이것이 실현되도록, kubelet이 다음과 같이 실행된다.

--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi

이 환경 설정에서, --system-reserved 플래그는 시스템 용으로 1.5Gi 메모리를 확보하는데, 이는 총 메모리의 10% + 축출 임계값에 해당된다.

파드가 요청량보다 많은 메모리를 사용하거나 시스템이 1Gi 이상의 메모리를 사용하여, memory.available 축출 신호가 500Mi 아래로 내려가면 노드가 축출 임계값에 도달할 수 있다.

데몬셋(DaemonSet)

파드 우선 순위(Priority)는 파드 축출 결정을 내릴 때의 주요 요소이다. kubelet이 DaemonSet에 속하는 파드를 축출하지 않도록 하려면 해당 파드의 파드 스펙에 충분히 높은 priorityClass를 지정한다. 또는 낮은 priorityClass나 기본값을 사용하여 리소스가 충분할 때만 DaemonSet 파드가 실행되도록 허용할 수도 있다.

알려진 이슈

다음 섹션에서는 리소스 부족 처리와 관련된 알려진 이슈에 대해 다룬다.

kubelet이 메모리 압박을 즉시 감지하지 못할 수 있음

기본적으로 kubelet은 cAdvisor를 폴링하여 일정한 간격으로 메모리 사용량 통계를 수집한다. 해당 타임 윈도우 내에서 메모리 사용량이 빠르게 증가하면 kubelet이 MemoryPressure를 충분히 빠르게 감지하지 못해 OOMKiller가 계속 호출될 수 있다.

--kernel-memcg-notification 플래그를 사용하여 kubelet의 memcg 알림 API가 임계값을 초과할 때 즉시 알림을 받도록 할 수 있다.

사용률(utilization)을 극단적으로 높이려는 것이 아니라 오버커밋(overcommit)에 대한 합리적인 조치만 원하는 경우, 이 문제에 대한 현실적인 해결 방법은 --kube-reserved--system-reserved 플래그를 사용하여 시스템에 메모리를 할당하는 것이다.

active_file 메모리가 사용 가능한 메모리로 간주되지 않음

리눅스에서, 커널은 활성 LRU 목록의 파일 지원 메모리 바이트 수를 active_file 통계로 추적한다. kubelet은 active_file 메모리 영역을 회수할 수 없는 것으로 취급한다. 임시 로컬 스토리지를 포함하여 블록 지원 로컬 스토리지를 집중적으로 사용하는 워크로드의 경우 파일 및 블록 데이터의 커널 수준 캐시는 최근에 액세스한 많은 캐시 페이지가 active_file로 계산될 가능성이 있음을 의미한다. 활성 LRU 목록에 이러한 커널 블록 버퍼가 충분히 많으면, kubelet은 이를 높은 자원 사용 상태로 간주하고 노드가 메모리 압박을 겪고 있다고 테인트를 표시할 수 있으며, 이는 파드 축출을 유발한다.

자세한 사항은 https://github.com/kubernetes/kubernetes/issues/43916를 참고한다.

집중적인 I/O 작업을 수행할 가능성이 있는 컨테이너에 대해 메모리 제한량 및 메모리 요청량을 동일하게 설정하여 이 문제를 해결할 수 있다. 해당 컨테이너에 대한 최적의 메모리 제한량을 추정하거나 측정해야 한다.

다음 내용

11 - API를 이용한 축출(API-initiated Eviction)

API를 이용한 축출은 축출 API를 사용하여 생성된 Eviction 오브젝트로 파드를 정상 종료한다.

축출 API를 직접 호출하거나, 또는 kubectl drain 명령과 같이 API 서버의 클라이언트를 사용하여 프로그램적인 방법으로 축출 요청을 할 수 있다. 이는 Eviction 오브젝트를 만들며, API 서버로 하여금 파드를 종료하도록 만든다.

API를 이용한 축출은 사용자가 설정한 PodDisruptionBudgetsterminationGracePeriodSeconds 값을 준수한다.

API를 사용하여 Eviction 오브젝트를 만드는 것은 정책 기반의 파드 DELETE 동작을 수행하는 것과 비슷한 효과를 낸다.

축출 API 호출하기

각 언어 별 쿠버네티스 클라이언트를 사용하여 쿠버네티스 API를 호출하고 Eviction 오브젝트를 생성할 수 있다. 이를 실행하려면, 아래의 예시를 참고하여 POST 호출을 수행한다.

{
  "apiVersion": "policy/v1",
  "kind": "Eviction",
  "metadata": {
    "name": "quux",
    "namespace": "default"
  }
}

{
  "apiVersion": "policy/v1beta1",
  "kind": "Eviction",
  "metadata": {
    "name": "quux",
    "namespace": "default"
  }
}

또는 다음 예시와 같이 curl 또는 wget으로 API에 접근하여 축출 동작을 시도할 수도 있다.

curl -v -H 'Content-type: application/json' https://your-cluster-api-endpoint.example/api/v1/namespaces/default/pods/quux/eviction -d @eviction.json

API를 이용한 축출의 동작

API를 사용하여 축출을 요청하면, API 서버는 인가 확인(admission checks)를 수행하고 다음 중 하나로 응답한다.

  • 200 OK: 축출 요청이 허용되었고, Eviction 서브리소스가 생성되었고, (마치 파드 URL에 DELETE 요청을 보낸 것처럼) 파드가 삭제되었다.
  • 429 Too Many Requests: 현재 설정된 PodDisruptionBudget 때문에 축출이 현재 허용되지 않는다. 또는 API 요청 속도 제한(rate limiting) 때문에 이 응답을 받았을 수도 있다.
  • 500 Internal Server Error: 잘못된 환경 설정(예: 여러 PodDisruptionBudget이 하나의 동일한 파드를 참조함)으로 인해 축출이 허용되지 않는다.

축출하려는 파드가 PodDisruptionBudget이 설정된 워크로드에 속하지 않는다면, API 서버는 항상 200 OK를 반환하고 축출을 허용한다.

API 서버가 축출을 허용하면, 파드는 다음과 같이 삭제된다.

  1. API 서버 내 Pod 리소스의 삭제 타임스탬프(deletion timestamp)가 업데이트되며, 이 타임스탬프에 명시된 시각이 경과하면 API 서버는 해당 Pod 리소스를 종료 대상으로 간주한다. 또한 설정된 그레이스 시간(grace period)이 Pod 리소스에 기록된다.
  2. 로컬 파드가 실행되고 있는 노드의 kubeletPod가 종료 대상으로 표시된 것을 감지하고 로컬 파드의 그레이스풀 셧다운을 시작한다.
  3. kubelet이 파드를 종료하는 와중에, 컨트롤 플레인은 엔드포인트엔드포인트슬라이스 오브젝트에서 파드를 삭제한다. 이 결과, 컨트롤러는 파드를 더 이상 유효한 오브젝트로 간주하지 않는다.
  4. 파드의 그레이스 시간이 만료되면, kubelet이 로컬 파드를 강제로 종료한다.
  5. kubelet이 API 서버에 Pod 리소스를 삭제하도록 지시한다.
  6. API 서버가 Pod 리소스를 삭제한다.

문제가 있어 중단된 축출 트러블슈팅하기

일부 경우에, 애플리케이션이 잘못된 상태로 돌입하여, 직접 개입하기 전에는 축출 API가 429 또는 500 응답만 반환할 수 있다. 이러한 현상은, 예를 들면 레플리카셋이 애플리케이션을 서비스할 파드를 생성했지만 새 파드가 Ready로 바뀌지 못하는 경우에 발생할 수 있다. 또는 마지막으로 축출된 파드가 긴 종료 그레이스 시간을 가진 경우에 이러한 현상을 목격할 수도 있다.

문제가 있어 중단된 축출을 발견했다면, 다음 해결책 중 하나를 시도해 본다.

  • 이 문제를 발생시키는 자동 동작(automated operation)을 중단하거나 일시 중지한다. 해당 동작을 재시작하기 전에, 문제가 있어 중단된 애플리케이션을 조사한다.
  • 잠시 기다린 뒤, 축출 API를 사용하는 것 대신 클러스터 컨트롤 플레인에서 파드를 직접 삭제한다.

다음 내용