[Kubernetes in Action] Managing pods' computational resources

2022. 2. 12. 14:02DevOps/Docker & Kubernetes

 

 

 

Requesting Resources for a pod's containers

파드를 생성할 때, 각각의 컨테이너가 얼마만큼의 CPU 리소스와 메모리를 사용할지를 명시할 수 있습니다. 쿠버네티스에서 CPU와 메모리 자원은 컨테이너 단위로 명시되며, 파드의 리소스는 파드 내의 컨테이너에서 사용되는 리소스들의 합으로 정의됩니다. 리소스는 requestlimit을 통해서 설정할 수 있습니다. request는 컨테이너가 필요로 하는(최소) CPU와 메모리의 양을 설정하는 것이고, limit은 컨테이너가 최대로 사용할 수 있는 CPU와 메모리의 양을 설정하는 것을 의미합니다. 

 

 

Creating pods with resource requests

request를 갖는 파드는 다음과 같이 생성할 수 있습니다. 

apiVersion: v1
kind: Pod
metadata:
  name: requests-pod
spec:
  containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      requests:
        cpu: 200m           
        memory: 10Mi

 

CPU 리소스 중, 200m이라는 것은 200 millicore(밀리 코어)를 의미합니다. 1000밀리 코어는 하나의 CPU를 온전하게 사용한다는 의미이므로 200m는 하나의 CPU 중 1/5의 타임을 점유하여 사용한다는 의미입니다. Memory 리소스는 10Mi로 설정되었는데 이는 10 메비 바이트(Mebibyte)의 메모리를 할당하였음을 의미합니다. (자세한 내용은 공식문서를 참고해주세요)

 

 

mebibyte or MiB is the newer unit of measurement of size.
1MB=1000KB or 1024 KB
1 MiB = only and only 1024KB

 

 

request는 반드시 설정해야 하는 것은 아닌데, request를 설정하지 않았다는 의미는, 이 컨테이너가 "임의로" 자원을 할당받아도 된다는 것을 의미합니다. 따라서 항상 실행시간을 보장받지 않아도 되는 Batch Job의 경우, request를 설정하지 않아도 되지만, User Request를 받아야 하는 프로세스의 경우에는 적합하지 않습니다.

When you don’t specify a request for CPU, you’re saying you don’t care how much CPU time the process running in your container is allotted. In the worst case, it may not get any CPU time at all (this happens when a heavy demand by other processes exists on the CPU).

 

실제로 파드를 생성하고 top 명령어를 통해 CPU Consumption을 확인해 볼 수 있습니다. 

kubectl exec -it requests-pod -- top

 

 

여기서의 CPU 사용률은 11.7%의 가용률을 보이는데, 이는 실행환경의 CPU 코어가 6개라서 그런 것이고, 실제로 해당 화면에서 키보드의 "1"을 입력하고 나면 하나의 CPU에서 100% 정도의 가용율(usr 60%, sys 40%)을 사용하고 있음을 확인할 수 있습니다. 즉, request의 CPU 설정은 최소 조건을 의미하므로 여유분이 있을 경우, 컨테이너는 제한 없이 원하는 만큼 자원을 사용할 수 있게 됩니다. 제한을 줄 수 있는 방법도 있는데 이는 아래에서 설명할 Limit을 통해 가능합니다.

 

 

 

Understanding how resource requests affect scheduling

request를 설정하게 되면, 컨테이너가 필요로 하는 "최소 자원"을 설정하는 것이 됩니다. 이는 파드 스케줄링에도 영향을 주게 되는데 최소 자원 조건을 만족시키지 못하는 노드에는 해당 컨테이너가 들어있는 파드를 스케줄링해서는 안되기 때문입니다.

 

 

Understanding how the scheduler determines if a pod can fit on a node

스케줄러는 파드를 띄울 노드를 선택할 때 스케줄링 당시에 소비되고 있는 자원들의 총량이 아닌 해당 노드에 존재하는 파드의 requests를 확인한 후, 이들의 총합을 기준으로 선택합니다. 스케줄링 당시에 자원을 적게 소모한다고 해서 스케줄러가 해당 노드에 추가로 스케줄링을 해버리면 나중에 해당 노드에 있는 파드의 컨테이너가 request만큼의 자원을 사용하려 했을 때 자원이 부족해지는 현상이 나타날 수 있기 때문입니다. 따라서 항상 request를 기준으로 스케줄링을 진행하게 됩니다.

 

Understanding how the scheduler uses pods' requests when selecting the best node for a pod

스케줄러가 새로운 파드를 생성할 노드를 선택할 때 조건에 맞는, 즉 요청한 자원을 제공할 수 있는 노드가 여러 개 있다면 다음과 같은 방법으로 우선순위를 정할 수 있습니다.

 

  • LeastRequestedPriority
    • 여유 자원이 많은 노드를 선호하는 방법. 
    • 여러 개의 노드를 비교적 여유롭게 사용할 수 있다.
  • MostRequestedPriority
    • 여유 자원이 적은 노드를 선호하는 방법.
    • 클러스터를 운영하는데 최소한의 노드를 사용할 수 있다. (비용 절감 등의 목적으로)

 

Inspecting a Node's capacity

다음과 같은 명령어를 사용하면 사용하고 있는 노드의 자원 양을 확인할 수 있습니다. 

kubectl describe nodes

 

 

 

Understanding how CPU requests affect CPU time Sharing

별도의 limit을 설정하지 않는다면 파드 내의 컨테이너들을 request이상으로 해당 노드의 리소스를 사용할 수 있습니다. 만약 하나의 노드에 여러 개의 파드들이 스케줄 되어 있고, 각각의 파드들이 별도의 limit이 없이 request만 가지고 있다면 잔여 리소스들은 여러 개의 파드들의 request의 비율에 맞게 분배됩니다. 예를 들어 아래의 예제와 같이 2개의 파드가  각각 200m, 1000m의 CPU를 요청했다면 남은 CPU time(800m)은 각각 1:5의 비율로 비례 배분되어 추가적으로 사용할 수 있게 됩니다. 

 

 

 

Limiting Resources Available to a container

limit을 사용하면 노드에서 파드(파드 내의 컨테이너)가 사용 가능한 "최대" 자원을 제한할 수 있습니다.

Setting a hard limit for the amount of resources a container can use

CPU는 Compressible(압축 가능)한 자원입니다. 따라서 컨테이너에서 사용하는 CPU 사용량의 총합이 100%를 넘어가게 되더라도 프로세스가 실행을 멈추지는 않고 CPU 자원이 Throttle 됩니다. 하지만 Memory는 Incompressible 한 자원이기 때문에 컨테이너에서 사용하는 메모리 사용량의 총합이 100%를 넘어가게 되면 할당 자체가 되지 않고 프로세스가 실행할 수 없게 됩니다. 이경우 OOM(Out Of Memory) 상태라고 하며, CrashLoopBackOff 상태에 빠지게 됩니다.

 

이러한 특성 때문에 메모리의 경우 limit을 적절하게 설정하여 하나의 파드가 메모리를 지나치게 사용해서 다른 파드들의 실행해 영향을 주지 않도록 주의해야 합니다. 앞서 이야기한 것처럼, 스케줄러는 스케줄 시점의 자원이 아닌 requests를 보고 남은 자원을 판단하기 때문에 메모리가 실제 사용량보다 많은 경우 스케줄링이 되어도 실제로는 메모리 부족으로 실행되지 않을 수 있습니다.

 

Limit 리소스는 다음과 같이 생성할 수 있으며, limit을 생성할 때 별도로 request를 생성하지 않으면, limit과 동일한 값으로 request가 설정됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: limited-pod
spec:
  containers:
  - image: busybox
    command: ["dd", "if=/dev/zero", "of=/dev/null"]
    name: main
    resources:
      limits:                  
        cpu: 1
        memory: 20Mi

 

resource요청과는 다르게, limit은 노드에 할당 가능한 자원의 제약을 받지 않습니다. 즉, 노드에서 할당 가능한 100%의 자원 그 이상으로 limit을 설정할 수 있다는 의미입니다. CPU의 경우 위에서 설명한 것처럼 Throttling이 되지만 메모리의 경우 노드에서 할당 가능한 메모리 이상의 자원을 사용하게 될 경우 OOMKilled가 됩니다. (메모리가 부족해서 파드가 killed 됩니다.) 또한 한번 Limit을 설정하고 나면, 해당 limit의 제약을 받는 컨테이너가 허용된 것보다 많은 메모리를 사용하려고 하는 경우 해당 컨테이너는 Killed 됩니다.

 

 

Understanding how apps in containers see limits

limit에 대해서 한 가지 주의할 점은 컨테이너와 limit 리소스 와의 관계입니다. 실행 중인 컨테이너는 자신이 떠있는 파드의 limit 값을 모르기 때문에 컨테이너 안의 프로세스가 메모리를 확인하고 사용할 때는 자신이 실행 중인 노드의 전체 메모리를 보게 됩니다. 이러한 이유로 컨테이너화 된 애플리케이션이 메모리를 확인하고 이 여유 메모리 값에 따라서 메모리를 할당하게 되면, limit과 관계없이 더 많은 메모리를 할당하게 됩니다. 

 

CPU의 경우에도 컨테이너는 자신이 속한 파드의 limit값을 모르기 때문에 자신이 실행 중인 노드의 CPU를 보게 됩니다. 물론 CPU는 Compressible 하기 때문에 자신이 속한 파드의 limit보다 더 많은 CPU를 사용한다 하더라도 프로세스가 종료되지는 않지만, CPU Core의 개수에 따라 Worker Thread를 여러 개 생성하는 경우 실제로는 CPU Limit의 제한을 받아(컨테이너는 해당 limit에 대해 모르지만) Threading의 효과를 볼 수 없게 됩니다. 이러한 경우는 쿠버네티스의 Downward API를 사용해서 파드에서 CPU, 메모리의 Limit값을 컨테이너에 넘겨주어 해결해야 합니다.

 

Understanding pod QoS classes

앞서 살펴본 것처럼, limit을 설정하면 특정 파드가 사용할 수 있는 자원의 최대 범위를 설정할 수 있고, limit의 범위 자체는 노드에서 제공할 수 있는 것(100%) 이상을 설정할 수 있습니다. 이러한 특징 때문에 특정 파드의 컨테이너 안에서 동작하는 애플리케이션이 limit을 초과할 수 있으며, 이 limit이 노드에서 제공할 수 있는 자원을 초과하게 되면 특정 파드를 kill 해야 합니다. (특히 메모리의 경우) 따라서 쿠버네티스에서는 이러한 kill에 대해 어떠한 우선순위를 가지고 어떤 파드를 kill 할 것인지를  QoS(Quality of Service) 클래스를 통해 내부적으로 정해두고 있습니다. 정의되는 QoS 클래스는 다음과 같습니다.

 

  • BestEffort (the lowest priority)
  • Burstable
  • Guaranteed (the highest priority)

 

Defining the QoS class for a pod

QoS 클래스는 따로 설정하는 것은 아니며, 파드의 requests/limits 값으로부터 자동으로 도출됩니다. 도출된 QoS 클래스는 파드의 정의에 포함되며 kubectl describe pod 명령어를 통해 어떤 QoS 클래스가 도출되었는지를 확인할 수 있습니다.

 

  • BestEffort class
    • requests/limits가 설정되지 않은 컨테이너가 하나라도 존재하는 경우 pod에게 부여됩니다
    • 파드가 사용할 수 있는 리소스에 대해 어떠한 보장도 하지 않습니다. Starvation이 발생할 수도 있고, 노드 Capacity를 초과하는 경우 가장 먼저 kill 됩니다.
  • Guaranteed class
    • 모든 컨테이너의 requests와 limits 값이 일치하는 파드에게 부여됩니다.
    • CPU와 메모리 모두 requests와 limits가 모든 컨테이너에 부여되어야 하고, 그 값이 같아야 합니다.
    • 해당 파드 내의 컨테이너는 요청한 만큼 자원을 받고 limit 이상 그 자원을 받지는 못합니다.
  • Burstable class
    • 위의 두 클래스에 속하지 않는 경우 이 클래스에 속하게 됩니다.

 

위의 예시에서 파드는 limit만 설정한 파드였고, 별도의 request를 설정하지 않은 경우 limit에 설정된 값이 자동으로 request에도 할당되기 때문에 Guaranteed Class에 정의를 만족해서 해당 클래스가 부여된 것을 확인할 수 있습니다. 실제로 limit, request 두 값을 다르게 설정한 파드의 경우 다음과 같이 BestEffort Class가 부여됩니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - name: main
    image: curlimages/curl
    imagePullPolicy: IfNotPresent
    command: ["sleep", "9999999"]
    resources:
      requests:
        cpu: 1
        memory: 10Mi
      limits:
        cpu: 1
        memory: 20Mi

 

 

Kill Priority

BestEffort가 가장 먼저 Kill 되며, Burstable, Guaranteed 순으로 Kill 됩니다.

 

Understanding which process gets killed when memory is low

QoS 순서대로 Kill이 일어나지만, QoS가 같은 파드가 여러 개 있는 경우 OOM Score (Out Of Memory Score)를 계산해서 가장 높은 값을 갖는 파드를 Kill 하게 됩니다. OOM Score는 프로세스가 가지고 있는 메모리 중 사용 가능한 메모리의 비율과 Fixed OOM score adjustment가 고려됩니다.

 

 

Setting default requests and limits for pods per namespace

 

위에서 살펴본 것처럼 requests/limits를 명시적으로 설정하는 것이 좋습니다. 하지만 매 파드마다 이를 설정하는 것은 번거로운 작업이므로 쿠버네티스에서는 이러한 requests/limits를 Namespace 단위로 설정할 수 있는 기능인 LimitRange 리소스를 제공합니다. LimitRange를 설정하면 해당 네임스페이스에 속한 파드들에 기본적으로 minimum & maximum limit이 설정되며 기본적으로 request 값을 설정하지 않은 파드에게도 기본적으로 request를 설정해줍니다. 

 

 

Creating a LimitRange object

LimitRange 리소스는 다음과 같이 생성할 수 있습니다. 해당 리소스를 네임스페이스에 생성하고 나면 여기에 명시된 Range를 벗어난 파드의 생성 요청 시에 해당 요청이 Reject 됩니다.

apiVersion: v1
kind: LimitRange
metadata:
  name: example
spec:
  limits:
  - type: Pod           
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi
  - type: Container        
    defaultRequest:        
      cpu: 100m
      memory: 10Mi
    default:               
      cpu: 200m
      memory: 100Mi
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi
    maxLimitRequestRatio:    
      cpu: 4
      memory: 10
  - type: PersistentVolumeClaim 
    min:
      storage: 1Gi
    max:
      storage: 10Gi

 

Limiting the total resources available in a namespace

LimitRange를 사용하면 특정 파드에 대한 설정을 네임스페이스 단위로 관리하는 리소스이기 때문에 여러 파드에 할당된 리소스들의 총합을 관리할 수는 없습니다. 클러스터 관리자 입장에서는 파드 단위가 아닌 네임스페이스 별로 사용하는 리소스의 총량을 관리할 필요가 있고, 이를 위해 쿠버네티스에서는 ResourceQuota 리소스를 제공합니다.

 

Introducing the ResourceQuota Object

ResourceQuota Object를 생성해두면 파드가 생성되었을 때 네임스페이스에서 사용 가능한 자원 양을 초과하는지를 ResourceQuota Admission Control Plugin에서 확인합니다. 

 

 

apiVersion: v1
kind: ResourceQuota
metadata:
  name: cpu-and-mem
spec:
  hard:
    requests.cpu: 400m
    requests.memory: 200Mi
    limits.cpu: 600m
    limits.memory: 500Mi

 

ResourceQuota는 requests/limits 뿐 아니라 PVC용량, 오브젝트 개수 등도 네임스페이스 단위로 제한할 수 있게 됩니다.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: storage-objects
spec:
  hard:
    # storage
    requests.storage: 500Gi
    ssd.storageclass.storage.k8s.io/requests.storage: 300Gi
    standard.storageclass.storage.k8s.io/requests.storage: 1Ti
    # objects
    pods: 10
    replicationcontrollers: 5
    secrets: 10
    configmaps: 10
    persistentvolumeclaims: 5
    services: 5
    services.loadbalancers: 1
    services.nodeports: 2
    ssd.storageclass.storage.k8s.io/persistentvolumeclaims: 2

 

 

Reference

https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

 

Resource Management for Pods and Containers

When you specify a Pod, you can optionally specify how much of each resource a container needs. The most common resources to specify are CPU and memory (RAM); there are others. When you specify the resource request for containers in a Pod, the kube-schedul

kubernetes.io

 

 

 

반응형