Pod 의 자원 사용 제한 - Limit, Request

Created
May 5, 2025
Created by
D
DaEun Kim
Tags
Kubernetes
Property

[수정 이력]

  • 2022.03.24 - 최초 작성
  • 2025.05.05 - CPU request/limit 에 관한 내용 수정
  • 2025.05.06 - CFS 에 관한 내용 수정

쿠버네티스는 기본적으로 도커를 런타임으로 사용하기 때문에 메모리/CPU 사용량 제한을 하지 않으면 파드 하나가 노드의 물리 자원을 모두 소진하는 상황이 발생할 수 있다.

(도커는 —-memory, —-cpus, —-cpu-shares, —-cpu-quota 등의 옵션을 사용하지 않고 컨테이너를 실행할 경우 기본적으로 호스트의 메모리/CPU 를 제한없이 사용한다.)

파드가 사용할 자원을 제한하는 가장 간단한 방법은 spec.containers[].resources.limit 을 정의하는 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: resource-limit-pod
  labels:
    name: resource-limit-pod
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        limits:
          memory: "256MiB"
          cpu: "1000m"

노드 스펙은 AWS t2.medium 이다. (AWS EC2 인스턴스 타입 별 스펙 리스트는 여기 참고.)

하이퍼스레드는 물리적으로 1개인 CPU core가 여러개의 core 처럼 동작하는 것 정도로 이해함.

  • CPU core 2개, 각 코어 당 하이퍼스레드 1개 → vCPU = 2 코어 X 1 하이퍼스레드 = 2 vCPUs
  • 메모리 4GiB ( = 4096 MiB)
💡
쿠버네티스의 CPU 단위는 물리 호스트인지 아니면 물리 호스트 내에서 실행되는 가상 머신인지에 따라 1 물리 CPU 코어 또는 1 가상 코어 에 해당한다. → AWS EC2 를 노드로 사용한다면 vCPU = 1 가상 코어일 듯

resource-limit-pod 파드는 메모리를 최대 256MiB 까지 사용할 수 있고 최대 1 vCPU 를 사용할 수 있다

위 YAML 파일로 파드를 생성하고 파드가 생성된 노드의 상세정보를 조회해봤다.

ubuntu@ip-10-220-12-225:~/k8s-practice$ ku get nodes
NAME               STATUS   ROLES                  AGE   VERSION
ip-10-220-12-225   Ready    control-plane,master   24d   v1.23.4
ip-10-220-6-77     Ready    <none>                 24d   v1.23.4
ubuntu@ip-10-220-12-225:~/k8s-practice$ ku describe nodes ip-10-220-6-77
image

중간에 Non-terminated Pods 항목을 보면 resource-limit-pod 가 전체 CPU time 의 50% 까지 사용할 수 있고, 메모리는 256 / 4096 * 100 = 6% 정도를 사용할 수 있다고 한다.

CFS 와 limit/request 와의 관계

컨테이너의 requests.cpu, limit.cpu 를 설정하는 건 리눅스의 CPU 스케줄러와 연관이 있다.

CPU를 스케줄링 하는 방법으로는 크게 2가지가 있다.

  • Completely Fair Scheduler (CFS)
  • Real-Time scheduler (RT)

리눅스는 기본적으로 CFS 스케줄러를 사용한다.

requests.cpu

CFS 는 각 프로세스에게 부여한 cpu.shares 를 기반으로 하여 CPU 시간을 상대적인 수치로 할당한다. cpu.shares 는 프로세스 간에 CPU 사용 시간의 상대적 비율을 결정하며 컨테이너의 requests.cpu 를 설정하면 해당 컨테이너의 cpu.shares 으로 변환된다.

예를 들어서 프로세스 A에게는 cpu.shares 를 256, 프로세스 B에게는 768 로 셋팅했다고 하자.

AB가 같은 코어를 사용하는 상황에서 두 프로세스들이 CPU 사용을 요청했다면 코어가 단위시간동안 B 요청을 처리하는 시간이 A 요청을 처리하는 시간의 3배가 된다. (단위시간이 1초라고 하면 B 요청을 처리하는 데 0.75초 이면서 단위시간의 75%, A 요청을 처리하는 데 0.25초이면서 단위시간의 25%를 할애하는 것.)

그런데 가용한 코어가 2개 이상이라면 (가용한 코어 갯수가 프로세스 갯수보다 많다면) CFS는 A, B 에 각각 하나의 코어를 배분하여 AB에 비해 상대적으로 1/3 만큼의 시간을 사용한다고 셋팅되어 있더라도 A는 코어 하나를 온전히 다 점유할 수 있다.

반대로 cpu.shares가 256 인 프로세스 C와 함께 A, B가 하나의 코어를 사용해야 하는 경우 프로세스 B768 / (256 + 256 + 768) = 0.6초 단위시간의 60% 만큼의 시간을 할애받고 프로세스 A, C256 / (256 + 256 + 768) = 0.6초 단위시간의 20% 만큼의 시간을 할애받는다.

즉 프로세스의 cpu.shares는 프로세스 갯수보다 가용한 코어 갯수가 적을 때 의미가 있다.

requests.cpucpu.shares 으로 변환되는 과정은 아래와 같다.

쿠버네티스에서는 1 core = 1000 milicore 으로 간주하고 아래 식을 통해 requests.cpucpu.shares 으로 변환한다.

[requests.cpu 에 명시된 밀리코어] / 1000 * 1024

예를 들어, requests.cpu = 500m(milicore) 인 경우 500m / 1000 * 1024 = 512 cpu shares 와 동일하다.

쿠버네티스는 가용한 코어를 cpu.shares 으로 환산한 값보다 컨테이너들의 cpu.shares 들의 총합이 크지 못하도록 방지하는 방법으로 각 컨테이너의 requests.cpu 를 보장한다.

limits.cpu

쿠버네티스의 spec.containers[].resources.limits.cpu는 CFS의 cpu.cfs_quota_us(스케줄링 주기 내에 프로세스의 최대 CPU 사용 시간)에 영향을 준다.

[참고] 스케줄링 주기 (= cpu.cfs_period_us) 기본적으로 100ms(1/10초)로 설정되며, 이는 kubelet 또는 컨테이너 런타임에 의해 결정된다.

예를 들어, 컨테이너 AB에 각각 limits.cpu100m(0.1 vCPU)와 300m(0.3 vCPU)으로 설정했다고 하자. 단일 코어 환경이라면 컨테이너 AB는 아래와 같이 CPU 시간을 점유하도록 CFS 가 제한한다.

  • A: 100ms 주기에서 최대 10ms(10,000μs) 사용.
  • B: 100ms 주기에서 최대 30ms(30,000μs) 사용.

만약 requests.cpu도 각각 100m300m으로 설정되었다면 CFS 는 cpu.shares 를 1:3 비율로 설정하여 CPU 시간을 분배하려고 하지만 limits.cpu 가 우선하여 컨테이너 A는 최대 10ms, B는 최대 30ms로 제한된다.

참고한 문서

Over Commit

위에 노드 상세정보를 보면 CPU Requests, Memory Requests 항목이 있는데 Request 는 ‘적어도 이 만큼의 자원은 컨테이너에게 보장이 되어야 한다는 것’을 의미한다.

쿠버네티스가 Request 를 지원하는 이유는 오버커밋(Over Commit)을 가능하게 하기 위함이다.

오버커밋은 가상머신 또는 컨테이너에게 사용할 수 있는 자원보다 더 많은 양을 할당함으로써 전체 자원의 사용률 (utilization)을 높이는 방법이다.

image

메모리 1GB를 가지는 서버에 위와 같이 컨테이너 A, B 를 생성했으며 각 컨테이너에게 최소 500MB 를 보장한다고 하자.

이렇게 고정으로 자원을 할당하면 한 가지 단점이 있는데, 컨테이너 A가 자원을 적게 사용하고 있을 때 A의 유휴 자원을 자원이 더 필요한 컨테이너 B에게 줄 수 없다는 것이다. 즉, 자원이 남아 돌아서 사용률이 낮아진다.

애초에 컨테이너를 생성할 때 컨테이너가 사용할 자원의 양을 미리 예측해서 할당하는 것도 방법이 되겠으나 예측에서 벗어나는 경우가 빈번하다.

쿠버네티스에서는 오버커밋(over commit)을 통해 실제로 존재하는 물리 자원보다 더 많은 양을 할당하게 한다.

image

위 예시에서는 실제 존재하는 메모리는 1GB 이기 때문에 최대 750MB를 사용할 수 있는 컨테이너 2개를 생성한다고 해서 1.5GB를 사용할 수 있는 것은 아니다.

오버커밋은 어떤 컨테이너가 자원을 적게 사용하고 있을 때 그 컨테이너의 유휴 자원을 다른 컨테이너가 사용할 수 있게 해서 자원의 사용률을 높이는 것이다.

이 때 컨테이너가 사용할 수 있는 최대 자원의 양을 limit으로 설정한다. 위의 예시에서는 컨테이너 A, B의 메모리 limit750MB으로 설정되어 있다.

대신에 컨테이너 A가 500MB를 이미 점유하고 있는데 컨테이너 B가 750MB를 사용 할 수는 없다. 컨테이너 A의 request500MB 로 설정되어 있기 때문이다.

만약 A 컨테이너에 트래픽이 증가해서 A 컨테이너의 메모리 사용율이 올라가고 A,B 컨테이너의 총 사용량이 1GB 를 넘어서려고 한다면, 우선순위가 더 낮은 컨테이너를 종료시킨다.

정리하면

  • request : 컨테이너에게 보장하는 최소 자원의 양
  • limit : 유휴 자원이 있을 때 request 에 설정된 자원의 양을 포함하여 최대로 점유가능한 자원의 양

request는 각 컨테이너에게 할당하는 최소한의 양이므로 request 의 총합이 노드의 총 자원의 양을 초과할 수 없다. 그래서 쿠버네티스 스케줄러는 파드를 생성할 때 파드 내에 컨테이너들의 총 request 만큼 자원이 놀고 있는 노드에게 파드를 생성한다. 즉, 파드를 생성할 때 기준은 limit 이 아닌 request 이다.

CPU Request & Limit

파드 내에서 CPU request/limit 설정이 실제로 어떻게 동작하는지 알아보자.

1. CPU 자원이 충분할 때

image

위의 그림처럼 컨테이너 A, B, C 의 request 가 1000m, 1000m, 500m 으로 되어있다면 각각 A,B 컨테이너는 기본적으로 C 컨테이너와 비교해 2배 많은 CPU time 을 보장받는다.

2. CPU 자원이 모자를 때

image

위의 예시의 경우 pod1 의 CPU request 를 만족하는 노드가 없을 경우 pod1은 노드를 할당받지 못하고 pending 으로 남아있게 된다.

쿠버네티스는 가용한 코어를 cpu.shares 으로 환산한 값보다 컨테이너들의 cpu.shares 들의 총합이 크지 못하도록 방지하는 방법으로 각 컨테이너의 requests.cpu 를 보장한다.

예를 들어

  1. 위의 경우 가용한 코어를 cpu.shares 으로 환산하면 3 core X 1024 = 3072
  2. 이미 생성되어 있는 컨테이너 Dcpu.shares = 1000 / 1000 X 1024 = 1024
  3. 새로 생성하려는 컨테이너들의 cpu.shares 총합 = (1000 + 1000 + 500) / 1000 X 1024 = 2560
  4. 1024 + 2560 = 3584 → 가용한 코어를 환산한 3072 보다 크므로 pod1 은 생성되지 못하도록 막음

3. CPU limit 에 도달했을 때

image

컨테이너 B 에서 limit 을 초과하도록 CPU 를 사용한다면 컨테이너 B 에는 쓰로틀(throttle) 이 발생한다.

이 때 컨테이너 B 의 성능이 일시적으로 줄어들 수 있다.

4. 다른 컨테이너와 CPU 경합이 발생할 때

image

B 컨테이너는 자신의 CPU limit 내에서 CPU 를 사용하며 돌아가고 있고 컨테이너 A는 자신의 CPU request 를 다 쓰지 않고 있다고 하자.

컨테이너 A에 트래픽이 급증하여 자신의 CPU limit 에 준하는 자원을 필요로 할 때, 컨테이너 B에는 쓰로틀(throttle)이 발생하여 CPU time 이 줄어들고, 컨테이너 B 의 성능도 줄어든다.

3, 4번이 가능한 이유는 쿠버네티스에서는 CPU를 compressible resource 으로 간주하기 때문이다. 이는 파드 내에 컨테이너가 자신의 CPU request 보다 많은 CPU를 사용하여 다른 컨테이너와 경합이 발생하더라도 쓰로틀을 걸어서 컨테이너의 CPU 점유시간을 조절하는 것을 뜻한다.

반면에 메모리는 incompressible resource 으로 간주해서 메모리 경합이 발생하면 우선순위가 낮은 컨테이너가 종료된다.

참고자료