Certified Kubernetes Administrator (CKA) #15 리소스 관리: requests/limits, QoS, LimitRange, ResourceQuota

#14 Scheduling 2에서 taints/tolerations와 PriorityClass로 어떤 Pod를 어느 노드에 어떤 우선순위로 둘지를 정했습니다. 이번 글은 한 걸음 더 들어가, 그 Pod가 노드의 cpu와 memory를 얼마나 예약하고 얼마까지 쓸 수 있는지를 다룹니다. requests/limits로 개별 컨테이너의 자원을 묶고, QoS 클래스로 노드가 압박받을 때 누가 먼저 쫓겨나는지를 이해하며, LimitRange와 ResourceQuota로 네임스페이스 단위의 운영 정책을 강제하겠습니다.

이 주제는 Workloads and Scheduling도메인의 마지막 조각이면서, 트러블슈팅에서 자주 만나는 OOMKilled와 Pending의 근본 원인이기도 합니다. 자원 설정을 모르면 #22 Troubleshooting 1의 OOM,Pending 문제를 끝까지 추적할 수 없습니다.

requests와 limits: 예약과 상한 #

쿠버네티스에서 컨테이너의 자원은 두 값으로 표현됩니다. requests는 예약이고 limits는 상한입니다. 이 둘을 명확히 구분하지 못하면 스케줄링과 트러블슈팅이 모두 흔들립니다.

  • requests: 이 컨테이너가 정상 동작에 최소로 필요로 하는 양입니다. scheduler는 노드의 할당 가능한(allocatable) 자원에서 requests 합계가 들어갈 자리가 있는 노드에만 Pod를 배치합니다. 즉 requests는 스케줄링 결정의 기준입니다.
  • limits: 이 컨테이너가 쓸 수 있는 최대 양입니다. 런타임이 cgroup으로 이 상한을 강제합니다. limits를 넘으려 하면 cpu는 스로틀되고, memory는 프로세스가 죽습니다.

scheduler는 limits가 아니라 requests만 보고 배치한다는 점이 핵심입니다. limits 합계는 노드 용량을 넘어도 배치되며, 이를 **오버커밋(overcommit)**이라고 합니다.

단위 표기 #

cpu와 memory는 단위 표기가 다릅니다.

자원표기의미
cpu1, 0.5, 500m1은 vCPU 1코어, 500m은 0.5코어(millicore)
memory128Mi, 256Mi, 1GiMi=메비바이트(1024², 2^20), Gi=기비바이트

cpu는 millicore 단위로 잘게 쪼갤 수 있어 250m은 0.25코어를 뜻합니다. memory는 Mi,Gi(2진 접두사)와 M,G(10진 접두사)가 다르므로, 운영에서는 혼동을 피하기 위해 Mi,Gi로 통일하는 편이 안전합니다.

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: "250m"      # 0.25코어 예약
          memory: "128Mi"  # 128Mi 예약
        limits:
          cpu: "500m"      # 0.5코어 상한
          memory: "256Mi"  # 256Mi 상한

위 컨테이너는 0.25코어와 128Mi를 예약받고, 부하가 몰리면 0.5코어와 256Mi까지 쓸 수 있습니다. requests와 limits 사이의 구간이 버스트 여유입니다.

CPU 스로틀 vs memory OOMKilled #

limits를 초과했을 때의 동작은 cpu와 memory가 근본적으로 다릅니다. 이 차이가 시험과 운영에서 가장 자주 헷갈리는 지점입니다.

자원limits 초과 시 동작결과
cpu스로틀(throttle). cgroup이 CPU 시간을 깎음프로세스는 죽지 않고 느려짐
memoryOOMKilled. 커널 OOM killer가 프로세스 종료컨테이너 재시작(restartPolicy에 따름)

cpu는 압축 가능한(compressible) 자원입니다. 한 컨테이너가 cpu limit에 닿으면 커널이 그 컨테이너에 할당하는 CPU 시간을 줄일 뿐, 프로세스를 죽이지는 않습니다. 그래서 cpu limit이 너무 낮으면 앱이 죽는 대신 응답이 느려지는 증상으로 나타납니다.

memory는 압축 불가능한(incompressible) 자원입니다. 이미 할당된 메모리는 회수할 방법이 없으므로, 컨테이너가 memory limit을 넘으면 커널 OOM killer가 그 프로세스를 종료합니다. 이때 kubectl describe pod의 상태가 OOMKilled로 찍히고, restartPolicy에 따라 재시작과 CrashLoopBackOff로 이어집니다.

# OOMKilled 확인
k describe pod web | grep -A3 "Last State"
#   Last State:     Terminated
#     Reason:       OOMKilled
#     Exit Code:    137

종료 코드 137(=128+9, SIGKILL)이 보이면 메모리 초과를 의심합니다. 해결은 memory limit을 올리거나 앱의 메모리 사용을 줄이는 쪽입니다. 이 패턴은 #22 Troubleshooting 1에서 다시 다루겠습니다.

QoS 클래스: 누가 먼저 쫓겨나는가 #

노드의 memory가 부족해지면 kubelet은 Pod를 **eviction(축출)**해 자원을 회수합니다. 이때 어떤 Pod를 먼저 내보낼지는 QoS(Quality of Service) 클래스가 결정합니다. QoS는 사용자가 직접 지정하는 값이 아니라, requests와 limits를 어떻게 설정했는지에서 자동으로 도출됩니다.

QoS 클래스조건eviction 우선순위
Guaranteed모든 컨테이너가 cpu,memory의 requests와 limits를 가지며, 각각 requests == limits가장 나중(보호됨)
Burstable하나 이상의 컨테이너에 requests가 있으나 Guaranteed 조건 미달중간
BestEffort어떤 컨테이너에도 requests/limits가 전혀 없음가장 먼저(우선 축출)

노드가 memory 압박을 받으면 kubelet은 BestEffort → Burstable → Guaranteed 순서로 Pod를 축출합니다. 즉 자원을 명확히 예약한 Pod일수록 보호받습니다. 운영의 함의는 분명합니다. 중요한 워크로드일수록 requests와 limits를 명시해 Guaranteed에 가깝게 두어야 노드 압박 상황에서 살아남습니다.

Guaranteed를 만드는 법 #

Guaranteed는 모든 컨테이너에서 requests와 limits가 완전히 같을 때 부여됩니다.

spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: "500m"
          memory: "256Mi"
        limits:
          cpu: "500m"      # requests와 동일
          memory: "256Mi"  # requests와 동일

limits만 지정하고 requests를 생략하면, 쿠버네티스가 requests를 limits와 같은 값으로 자동 보정합니다. 그래서 limits만 적어도 Guaranteed가 될 수 있습니다. QoS 클래스는 다음으로 확인합니다.

k get pod web -o jsonpath='{.status.qosClass}'
# Guaranteed

LimitRange: 네임스페이스 기본값과 경계 #

개별 Pod마다 requests/limits를 일일이 적게 하는 것은 운영에서 비현실적이고, 누락하면 BestEffort가 되어 위험합니다. LimitRange는 네임스페이스 단위로 컨테이너의 기본값과 허용 범위를 강제하는 객체입니다.

필드역할
default컨테이너가 limits를 생략하면 채워 넣을 기본 limit
defaultRequest컨테이너가 requests를 생략하면 채워 넣을 기본 request
min허용하는 최소값(이보다 작으면 생성 거부)
max허용하는 최대값(이보다 크면 생성 거부)
apiVersion: v1
kind: LimitRange
metadata:
  name: container-limits
  namespace: dev
spec:
  limits:
    - type: Container
      default:           # limits 미지정 시 채울 값
        cpu: "500m"
        memory: "256Mi"
      defaultRequest:    # requests 미지정 시 채울 값
        cpu: "250m"
        memory: "128Mi"
      min:               # 허용 최소
        cpu: "100m"
        memory: "64Mi"
      max:               # 허용 최대
        cpu: "1"
        memory: "1Gi"

이 LimitRange가 적용된 dev 네임스페이스에서는, resources를 비운 채 Pod를 만들어도 자동으로 defaultRequestdefault가 채워집니다. 또 max1Gi를 넘는 memory limit을 요청하면 API server가 생성을 거부합니다. LimitRange는 만드는 시점에 검증하므로, 적용 전에 이미 떠 있던 Pod에는 소급되지 않습니다.

k apply -f limitrange.yaml
k describe limitrange container-limits -n dev

ResourceQuota: 네임스페이스 총량 제한 #

LimitRange가 컨테이너 하나의 경계라면, ResourceQuota네임스페이스 전체의 총량을 제한합니다. 한 팀이 클러스터 자원을 독식하지 못하도록 네임스페이스 단위로 상한을 거는 멀티테넌시 운영의 핵심 도구입니다.

ResourceQuota는 크게 두 가지를 제한합니다.

  • 자원 총합: 네임스페이스 안 모든 Pod의 cpu,memory requests/limits 합계
  • 객체 수: Pod, Service, ConfigMap, Secret, PVC 등의 개수
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: dev
spec:
  hard:
    # 자원 총합
    requests.cpu: "4"          # requests cpu 합계 최대 4코어
    requests.memory: "8Gi"     # requests memory 합계 최대 8Gi
    limits.cpu: "8"            # limits cpu 합계 최대 8코어
    limits.memory: "16Gi"     # limits memory 합계 최대 16Gi
    # 객체 수
    pods: "20"                 # Pod 최대 20개
    services: "5"
    configmaps: "10"
    persistentvolumeclaims: "4"

중요한 제약이 하나 있습니다. ResourceQuota가 requests.*limits.*를 제한하는 네임스페이스에서는, 모든 컨테이너가 해당 requests/limits를 반드시 지정해야 합니다. 지정하지 않으면 쿼터 집계가 불가능하므로 Pod 생성이 거부됩니다. 그래서 ResourceQuota는 LimitRange와 짝으로 쓰는 것이 정석입니다. LimitRange가 기본값을 채워 주면, requests/limits를 깜빡한 Pod도 쿼터 검증을 통과합니다.

k apply -f resourcequota.yaml
k describe resourcequota team-quota -n dev
# Resource          Used   Hard
# --------          ----   ----
# requests.cpu      1500m  4
# requests.memory   3Gi    8Gi
# pods              7      20

UsedHard에 닿으면 그 자원을 더 요구하는 Pod는 forbidden 오류로 거부됩니다. 쿼터 초과로 생성이 막혔는데 원인을 모르면 트러블슈팅이 길어지므로, Pending이나 생성 실패를 만나면 describe resourcequota로 먼저 사용량을 확인하는 습관이 도움이 됩니다.

운영 관점: 네임스페이스 정책 세트 #

실무에서 새 팀 네임스페이스를 만들 때는 다음 세 가지를 한 묶음으로 적용합니다.

  1. Namespace: 격리 단위를 만듭니다.
  2. LimitRange: 컨테이너 기본값과 min/max를 강제해, requests 누락으로 인한 BestEffort와 과도한 단일 컨테이너를 막습니다.
  3. ResourceQuota: 네임스페이스 총 자원과 객체 수에 상한을 걸어, 한 팀이 클러스터를 독식하지 못하게 합니다.

이 세 객체가 함께 있어야 멀티테넌시가 안정적으로 굴러갑니다. LimitRange 없이 ResourceQuota만 걸면 requests를 적지 않은 Pod가 전부 거부되어 개발자가 혼란을 겪고, ResourceQuota 없이 LimitRange만 걸면 개별 컨테이너는 적정해도 네임스페이스 총량이 무한정 늘어납니다.

이 자원 정책의 기초 개념은 쿠버네티스 중급 #4에서 requests/limits와 QoS를 입문 관점으로 먼저 정리했으니, 개념이 흐릿하면 함께 보면 도움이 됩니다.

시험 포인트 #

CKA에서 이 주제는 다음 형태로 자주 나옵니다.

  • requests/limits 추가: 기존 Deployment나 Pod에 cpu,memory의 requests/limits를 명시하라는 작업. k edit이나 매니페스트 수정으로 처리하며, 단위 표기(m, Mi, Gi)를 정확히 적는 것이 채점 포인트입니다.
  • OOMKilled 진단: Pod가 반복 재시작하는 원인을 찾는 트러블슈팅. describe에서 OOMKilled와 종료 코드 137을 확인하고 memory limit을 올립니다.
  • LimitRange 생성: 네임스페이스에 default/defaultRequest/min/max를 가진 LimitRange를 만들라는 작업. type: Container를 빠뜨리지 않습니다.
  • ResourceQuota 생성: 네임스페이스에 자원 총합이나 객체 수 쿼터를 거는 작업. requests.cpu처럼 점(.)이 들어간 키 표기를 정확히 씁니다.
  • QoS 클래스 확인: k get pod <name> -o jsonpath='{.status.qosClass}'로 클래스를 확인하거나, 특정 QoS가 되도록 requests/limits를 맞추는 작업.

빠른 생성 명령도 함께 익혀 두면 시간을 아낍니다.

# requests/limits 지정해 Pod 생성
k run web --image=nginx \
  --requests='cpu=250m,memory=128Mi' \
  --limits='cpu=500m,memory=256Mi'

# 네임스페이스에 ResourceQuota 빠르게 생성
k create quota team-quota -n dev \
  --hard=requests.cpu=4,requests.memory=8Gi,pods=20

정리 #

이번 글에서 잡은 것:

  • requests는 예약, limits는 상한. scheduler는 requests만 보고 배치하며, limits 합계가 노드 용량을 넘는 오버커밋이 가능합니다.
  • cpu는 스로틀, memory는 OOMKilled. cpu는 압축 가능한 자원이라 느려질 뿐 죽지 않고, memory는 압축 불가능해 limit 초과 시 종료(코드 137)됩니다.
  • QoS 3종. Guaranteed(requests==limits),Burstable,BestEffort 순으로 보호 강도가 높으며, eviction은 BestEffort부터 일어납니다.
  • LimitRange. 네임스페이스 컨테이너 단위로 default,defaultRequest,min,max를 강제합니다.
  • ResourceQuota. 네임스페이스 총 자원과 객체 수를 제한하며, LimitRange와 짝으로 써야 안정적입니다.

다음: Storage 1 #

자원의 cpu와 memory를 묶었으니, 다음은 데이터를 담을 스토리지입니다.

#16 Storage 1: Volume types, PV, PVC 정적 프로비저닝에서는 emptyDir,hostPath 같은 Volume 타입부터, PersistentVolume과 PersistentVolumeClaim으로 스토리지를 선언적으로 요청하고 바인딩하는 정적 프로비저닝까지, 그리고 accessModes와 reclaim policy가 무슨 차이를 만드는지 직접 매니페스트로 다루겠습니다.

X