할당 자원 관리

개요

파드에 사용할 수 있는 자원 중 가장 직관적인 것 중 하나는 프로세스가 실행되는데 필요한 자원일 것이다.
여기에 사용될 수 있는 자원은 대표적으로 cpu, 메모리, 로컬 스토리지 등이 있다.
여기에 커스텀 자원을 넣어서 확장하는 방법도 존재하는데, 이것 추후에 더 정리하겠다.
이 문서에서는 파드에 어떻게 이 리소스들을 할당할 수 있는지 살펴보자.

양식 작성법

파드에서 실제로 작성하게 되는 양식은 이런 식이다.

spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

보통은 이렇게 각 컨테이너마다 resources 필드를 이용해 자원에 대한 설정을 넣어준다.
이 스펙들을 명시하는 행위는 kube-scheduler가 파드를 스케줄링할 노드를 고를 때, kubelet이 컨테이너를 실행시킬 때 등 다양하게 활용된다.

요청하는 필드에는 requests, limits라는 두 가지 값이 가능하다.
requests는 최소한 이만큼은 컨테이너가 리소스를 받을 수 있어야 한다고 요청하는 필드이다.
limits는 아무리 그래도 이만큼 이상 컨테이너가 리소스를 가져가면 안 된다고 제한하는 필드이다.
그래서 간단하게 최소 얼마, 최대 얼마를 지정하는 거라 생각하면 되겠다.
더 자세하게는 [[#Requests]], [[#Limits]]에서 본다.

각 컨테이너마다 자원에 대한 설정을 하는데 이것들이 각각 가지는 의미가 있긴 하지만 때로는 파드 내 컨테이너의 자원 관련 설정 총합을 활용하는 매커니즘도 동작한다는 걸 유의하자.

파드의 .status에 사용량이 보고된다.

파드 차원 자원 할당

spec:
  resources:
    requests:
      memory: "64Mi"
      cpu: "250m"
    limits:
      memory: "128Mi"
      cpu: "500m"
  containers:
  - name: app
    image: images.my-company.example/app:v4
...

기본 사용 방식은 파드의 컨테이너마다 자원을 할당하는 것이다.
그러나 이런 상황이 있을 수 있다.

이렇게 하면 파드의 전체 사용량을 따져서 값을 계산하게 된다!

이건 Kubernetes v1.32 - Penelope에서 알파인 기능으로 PodLevelResources 피처 게이트를 활성화해야 사용할 수 있다.

Requests

request는 해당 컨테이너가 이만큼은 사용하길 바란다고 요청하는 필드이다.
그렇다고 사실 메모리 50메가 쓰는 컨테이너에 100메가로 요청한다고 해서 강제적으로 그만큼 메모리가 묶여버리는가?
그건 아니다.
이 값은 실제 컨테이너가 얼마나 자원을 쓰는지에 대해서는 일절 관여하지 않는다.

그림 유의사항

지금부터 보이는 그림들은 메모리 자원에 대한 상황을 바탕으로 그려졌다.
cpu와 메모리에 대해서 동작하는 방식이 다른데, 이것은 컴퓨팅 자원에서 조금 더 제대로 다룬다.

requests는 **kube-scheduler가 파드를 스케줄링할 때, 해당 노드에서 사용할 수 있는 자원을 고려하는데 사용**된다. (컨테이너마다 request를 설정한 후 파드 당 총량을 표기하는 예시이다.) 노드에는 할당 가능량이 정해져있는데, 해당 노드에 어떤 파드를 배치할 수 있는지 판단할 때 requests 값을 고려한다는 것이다. 스케줄러는 노드의 자원 최대치를 체크하고 있으며, 스케줄링을 할 때 항상 남은 총량이 컨테이너가 요구하는 양보다 작음을 보장한다.

주의할 것은 실제로 해당 노드가 그만큼의 여유 공간이 확보돼있는지 스케줄러는 상관하지 않는다는 것이다.
위의 예시에서, 파드 1은 사실 500m 만큼의 자원을 쓰고 있다고 쳐보자.
그리고 파드 2는 사실 400m 만큼의 자원을 쓰고 있다.
그렇다면 실제로 해당 노드는 100m 정도만큼의 여유공간만 남아있는 것이다.
파드 3이 실제로 배치되고 나서 자원을 100m 이상 쓰게 된다면, 노드의 실제 가용한 자원을 초과하게 되므로 문제가 발생하게 된다.
이 경우 eviction, OOM kill 등 다양한 자원 정리 해소 매커니즘이 동작하게 될 것이다.
자칫하다간 서비스 장애가 발생할 수 있는 사항이니 세밀하게 제어를 할 필요가 있다.

반대의 상황에서는 운영 비효율이 발생할 수 있다.
파드가 실제로 사용하지도 않을 만큼의 자원을 크게 request해두면 다른 파드들이 배치되기 힘들어진다.
그럼 노드의 자원은 실질적으로 놀게 되는 상황이 발생할 수도 있다.

Limits

limits는 실제로 해당 컨테이너 또는 파드가 사용할 수 있는 최대치를 제한하는 필드이다.
requests와 다르게, 이 필드는 실제 컨테이너가 얼마나 자원을 쓰는지에 대해 관여한다!

limits을 설정하면 kubelet컨테이너 런타임에 이를 전달하고, cgroups에 의해 제한 값이 설정된다. 위의 그림에서 2번 케이스가 바로 limits를 넘기는 파드 혹은 컨테이너가 생겼을 때 발생하는 일이다. 즉, **limits는 실제로 컨테이너 런타임이 컨테이너가 활용할 수 있는 자원의 최대값을 제한하는데 사용**된다.

참고로, 3번 케이스처럼 eviction이 일어나는 케이스에서는 PriorityClass, QoS가 고려된다.
이 중 QoS는 requests와 limits 설정에 따라 클래스가 결정되기에 값을 잘 설정하는 것이 중요하다!

자원 타입

자원 타입 중 CPU, 메모리가 가장 핵심적이고 운영 상 중요하게 다뤄야 하는 자원들이다.
이에 대한 동작 방식을 아는 게 중요한 관계로 컴퓨팅 자원에 따로 담았다.

huge-page

메모리와 관련된 자원 설정으로, 리눅스에서는 huge page 자원을 명시할 수 있다.
이건 커널에 기본 페이지 사이즈보다 더 큰 메모리 블록을 할당하는 기능이다.
hugepages-2Mi: 80Mi 이런 식으로 쓴다.
기본 페이지가 4Kib에서 이렇게 쓸 수 있다.
만약 40 2MiB huge pages (a total of 80 MiB)만큼을 시도하면 할당은 실패할 것이다.
이건 오버커밋할 수 없다.

미숙한 정리

이 개념을 아직 제대로 이해했다고 하기 힘들 것 같다.
그래서 문서에서 본 내용만 대충 적어두고 나중에 조금 더 탐구해보려고 한다.

ephemeral-storage

ephemeral-storage는 노드의 디스크 공간을 의미한다.
단순하게 말하면 그냥 클러스터를 운영하면서 발생하는 모든 디스크 공간을 의미한다.
구체적으로는 노드의 다음 공간들을 의미한다.

spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        ephemeral-storage: "2Gi"
      limits:
        ephemeral-storage: "4Gi"
    volumeMounts:
    - name: ephemeral
      mountPath: "/tmp"
  volumes:
    - name: ephemeral
      emptyDir:
        sizeLimit: 500Mi

이리 되면 4기비 중에서 500메비는 emptyDir로 쓰일 것이다.
스토리지에 대한 값은 기본이 바이트라는 걸 참고하자.

kubelet에서 이러한 로컬 스토리지가 사용되는지도 체크해준다.
대체로 이 공간은 /var/lib/kubelet, /var/logs 이런 곳이 된다.
참고로 이러한 후자의 방법에 대해서는 사용 공간에 대한 제대로 된 추적이 되지 않을 수 있으니 주의하자.
기본적으로 kubelet은 주기적 스캔을 통해 디스크 공간을 추적한다.

꼼수

참고로 kubelet은 지워진 파일의 디스크립터까지 추적하지는 않는다.
가령 어떤 컨테이너가 emptyDir에 있는 파일을 읽고 있다고 쳐보자.
이 파일을 지우더라도, 파일디스크럽터는 남아있고 메모리에 데이터가 남아있으니 컨테이너는 파일 내용물을 볼 수 있는 상태이다.
그래도 이걸 kubelet이 스토리지가 비워지지 않았다고 치지 않는다는 것이다.
이걸 활용할 케이스가 있을진 모르겠다.

Kubernetes v1.31 - Elli 기준 베타인 기능으로, 파일시스템 프로젝트 쿼타라는 방식으로 디스크 사용량 스캔을 하는 방법도 있다고 한다.
image.png
이게 뭔지는 잘 모르겠어서 간단하게 지피티의 도움을 받았다.
간단하게 번역한 내용만 남겨두고, 나중에 조금 더 파헤쳐봐야겠다.

limits 초과시

requests에 대한 동작은 전부 결국 같으므로 구태여 설명하지 않겠다.
만약 limits를 넘는다면 파드는 축출된다.
당연히 파드 안의 한 컨테이너가 이 제한을 넘겨도 파드째로 축출된다.

이 상황은 당연히 kubelet이 노드 스토리지에 대한 측정을 하고 있다는 것을 전제로 한다.
그런데 로컬 스토리지에 대한 리소스 관련 설정이 아예 없는 파드들로만 이뤄진 상황이라면?
이럴 경우에는 별도로 kubelet이 컨테이너나 파드별로 사용량을 측정하지 않으니 축출은 진행되지 않는다.
다만 그러다가 결국 노드 디스크 공간이 부족해져버린다면 최소한 노드에 NoExecute 테인트, 톨러레이션를 걸어버린다.
그러면 이에 대한 테인트, 톨러레이션 대비가 안 된 파드들은 전부 튕겨저 나가버릴 것이다.
초신성 ㄷㄷ

확장 리소스

      resources:
        limits:
          hardware-vendor.example/foo: 2

확장 자원은 kubernetes.io 도메인 바깥의 리소스를 말한다.
이건 쿠버에 내장되지 않은 자원을 넣을 수 있도록 해준다.
클러스터로 GPU 자원을 운용할 때는 이런 방식을 사용한다.

이런 리소스를 사용하기 위해서는 두가지 과정이 필요하다.
일단 관리자가 확장 자원을 게시해야 하고, 그리고 개발자가 파드에서 이 자원을 요청해야 한다.

이 값은 위의 것들과 같이 사용된다.
사용 가능한 양에 대해서는 스케줄러가 파악을 해준다.

유의점

확장 자원은 cpu나 메모리처럼 적당히 값을 잘라서 쓰고 하는 개념이 지원되지 않는다.
그래서 무조건 정수 값으로 떨어지게 설정해야 한다는 것에 유의하자.
또한 확장 자원은 오버커밋(실제 사용 가능한 양보다 많게 사용되는 상황[1])될 수 없다.
그래서 항상 requests와 limits를 동일하게 설정해야 한다.

그럼 이러한 확장 자원을 어떻게 등록하면 될까?
자원의 유형에 따라 노드 레벨과 클러스터 레벨로 두 층위를 또 나뉜다.

노드 레벨 자원 확장

가장 기본적인 방법은 직접 api서버에 PATCH 요청을 날려서 리소스를 등록하는 것이다.

curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--data '[{"op": "add", "path": "/status/capacity/example.com~1foo", "value": "5"}]' \
http://k8s-master:8080/api/v1/nodes/k8s-node-1/status

이런 식으로 하면 노드의 capacity에 해당 값이 추가된다.

~1이 뭐지?

참고로 여기에서 ~1/을 인코딩한 건데, json 방식에서 /을 문자열로 취급하고자 할 때 쓴다.
그래서 example.com/foo가 확장된 자원으로 들어가게 된다.

{
  "status": {
    "capacity": {
      "example.com/foo": "5"
    }
  }
}

이 방식은 그게 실제 사용되는 리소스던 뭐던 임의의 자원을 넣어주는 것을 가능하게 한다.
예를 들면 한 노드에 무조건 파드가 3개만 띄워지게 하고 싶다.
그럴 때 임의의 자원을 3개로 등록을 한 다음에 파드에 이 값을 requests 해버리면 딱 파드 3개만 배치하는 식으로 운용할 수 있다!
(좋은 방식인지는 모르겠지만, 뭐 이런 식으로도 활용할 수 있다 정도..?)
이 방식은 비동기적으로 적용되기 때문에, 바로 노드의 상태값에 반영이 안 될 수 있다.

아니면 GPU, 인피니밴드 등의 실제 노드에 있는 자원을 적절하게 등록해서 사용하는 방법도 있다.
디바이스 플러그인으로, kubelet에서 이를 인식할 수 있도록 직접 구현해서 넣어주면 된다.
관련한 내용은 이 문서[2]를 참고한다.

참고로 kubectl에서 노드에 patch를 하는 방식으로 적용하는 것도 가능하다.
이때 --subresource=status라는 인자를 넣어주지 않으면, 기본적으로 사용자의 status 변경사항은 무시되기 때문에 유의해야 한다.

클러스터 레벨 자원 확장

이건 노드에 묶이지 않는 자원을 관리할 때 사용하는 방식이다.
스케줄러의 기능을 확장하는 툴이나 프로그램에 의해 관리된다.
이 확장자에 대한 정보를 스케줄러 설정에 넣어주면, 스케줄러가 관련 리소스를 요청하는 파드를 스케줄링할 때 해당 확장자에게 스케줄링을 위임해버린다.
설정법은 스케줄러 관련 설정을 참고하자[3].

{
  "kind": "Policy",
  "apiVersion": "v1",
  "extenders": [
    {
      "urlPrefix":"<extender-endpoint>",
      "bindVerb": "bind",
      "managedResources": [
        {
          "name": "example.com/foo",
          "ignoredByScheduler": true
        }
      ]
    }
  ]
}

이렇게 하면 파드 리퀘스트에 example.com/foo가 있을 때 스케줄러에서 스케줄러 확장자로 보내버린다.
ignore 필드 부분을 넣어야 스케줄러가 PodFitResources 부분 검증을 하지 않는다.

PID

프로세스 ID 개수를 제한하는 것이 가능하다.
다만 이건 resource 필드에 요청을 하거나 제한을 걸 수 있는 것은 아니라, 순수 관리자의 영역이라 볼 수 있다.
kubelet 설정으로 넣어주는 식으로 설정하는데, 자세한 건 이 문서[4] 참고.

관련 문서

이름 noteType created
ConfigMap knowledge 2025-01-12
Secret knowledge 2025-01-12
할당 자원 관리 knowledge 2025-01-12
컴퓨팅 자원 knowledge 2025-03-09
Downward API knowledge 2025-03-09
RuntimeClass knowledge 2025-03-19
Dynamic Resource Allocation knowledge 2025-04-18
E-emptyDir 제한 topic/explain 2025-01-16
E-projected 볼륨 - 동적 업데이트, 중복 활용 topic/explain 2025-03-10
T-마운트 전파 Bidirectioal topic/temp 2025-02-28

참고


  1. https://bcho.tistory.com/1291 ↩︎

  2. https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/ ↩︎

  3. https://kubernetes.io/docs/reference/config-api/kube-scheduler-config.v1/ ↩︎

  4. https://kubernetes.io/docs/concepts/policy/pid-limiting/ ↩︎