Certified Kubernetes Application Developer (CKAD) #16 리소스 관리: requests/limits, QoS class, LimitRange
#15 SecurityContext와 Capabilities에서 컨테이너가 어떤 권한으로 돌아가는지를 잡았다면, 이번에는 컨테이너가 얼마만큼의 자원을 쓸 수 있는지를 다룹니다. Pod가 노드의 CPU와 memory를 무제한으로 가져가면 한 워크로드가 노드 전체를 마비시킬 수 있습니다. 쿠버네티스는 컨테이너마다 “이만큼은 보장해 달라(requests)“와 “이 이상은 못 쓴다(limits)“를 선언하게 해서, 스케줄링과 안정성을 동시에 챙깁니다.
이 주제는 CKAD의 가장 큰 도메인인 **Application Environment, Configuration and Security(25%)**에 속하고, 시험에서는 requests/limits를 정확한 단위로 붙이는 작업과 함께 “이 Pod의 QoS class는 무엇인가”, “왜 OOMKilled가 났는가” 같은 판별 문제로 자주 나옵니다. 단위와 동작을 손에 익혀 두면 빠르게 점수를 가져갈 수 있습니다.
requests와 limits: 무엇이 다른가 #
자원 선언은 컨테이너 단위로 spec.containers[].resources 아래에 들어갑니다. 두 키의 의미가 다릅니다.
- requests: 이 컨테이너가 최소한 보장받아야 하는 양입니다. 스케줄러는 노드의 할당 가능 자원에서 이 값을 빼면서 Pod를 배치합니다. 즉 requests는 스케줄링의 기준입니다.
- limits: 이 컨테이너가 써도 되는 상한입니다. 런타임이 이 값을 넘지 못하도록 강제합니다.
requests만 있고 limits가 없으면 상한 없이 노드 여유분까지 쓸 수 있고, limits만 있고 requests가 없으면 쿠버네티스는 requests를 limits와 같은 값으로 간주합니다.
단위: CPU와 memory를 정확히 쓴다 #
CKAD에서 자주 틀리는 부분이 단위입니다. CPU와 memory는 표기 체계가 다릅니다.
| 자원 | 단위 | 의미 |
|---|---|---|
| CPU | 1, 0.5, 500m | 1은 vCPU 1개, 1000m(millicore)이 1코어. 500m은 0.5코어 |
| memory | 128Mi, 1Gi, 512M | Mi/Gi는 2진(1Mi=1024Ki), M/G는 10진(1M=1000K) |
CPU의 m은 millicore이며 500m은 0.5코어와 같습니다. memory에서 Mi(mebibyte)와 M(megabyte)은 다른 값이므로, 시험에서 문제가 Mi로 요구하면 M으로 쓰면 안 됩니다. 보통 Mi와 Gi를 표준으로 씁니다.
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "250m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"이 Pod는 0.25코어와 128Mi를 보장받으며, 최대 0.5코어와 256Mi까지 쓸 수 있습니다. 스케줄러는 requests의 합이 들어갈 자리가 있는 노드를 찾아 배치합니다.
상한을 넘으면 어떻게 되는가 #
CPU와 memory는 limits를 초과했을 때의 동작이 결정적으로 다릅니다. 이 차이가 시험 단골입니다.
- CPU 초과: CPU는 압축 가능(compressible) 자원입니다. limits를 넘으면 컨테이너가 죽지 않고 스로틀(throttle), 즉 할당된 시간만큼만 실행되도록 속도가 제한됩니다. 느려질 뿐 종료되지 않습니다.
- memory 초과: memory는 압축 불가능(incompressible) 자원입니다. limits를 넘으면 커널의 OOM killer가 컨테이너 프로세스를 종료하고, 컨테이너 상태가 OOMKilled로 표시됩니다. restartPolicy에 따라 재시작되며, 계속 초과하면
CrashLoopBackOff로 이어집니다.
OOMKilled가 보이면 원인은 거의 항상 “memory limits가 실제 사용량보다 낮음"입니다. limits를 올리거나 애플리케이션의 memory 사용을 줄이는 두 방향으로 접근합니다.
# 종료 원인 확인
k describe pod resource-demo | grep -A3 "Last State"
# Last State: Terminated
# Reason: OOMKilledQoS class: 세 등급과 eviction #
쿠버네티스는 requests와 limits를 어떻게 지정했는지에 따라 Pod에 Quality of Service(QoS) class를 자동으로 부여합니다. 이 등급은 노드의 자원이 부족해질 때 **누구를 먼저 쫓아내는가(eviction)**를 결정합니다.
| QoS class | 조건 | eviction 우선순위 |
|---|---|---|
| Guaranteed | 모든 컨테이너에 cpu/memory 모두 지정 + requests == limits | 가장 늦게 쫓겨남 |
| Burstable | requests나 limits 중 일부만 지정(Guaranteed 조건 미충족) | 중간 |
| BestEffort | requests도 limits도 전혀 없음 | 가장 먼저 쫓겨남 |
핵심은 다음과 같습니다.
- Guaranteed: 모든 컨테이너가 cpu와 memory 둘 다 지정하고, 각각 requests와 limits가 같은 값일 때입니다. memory가 부족할 때 가장 마지막에 보호받습니다.
- Burstable: requests는 있지만 limits가 더 크거나, 일부 자원만 지정한 경우입니다. requests를 초과해 쓰던 부분이 회수 대상이 됩니다.
- BestEffort: 자원 선언이 전혀 없는 Pod 입니다. 노드 압박 시 가장 먼저 eviction 대상이 됩니다.
Guaranteed가 되는 매니페스트 #
requests와 limits를 cpu, memory 모두 같은 값으로 두면 Guaranteed가 됩니다. 시험에서 “이 Pod를 Guaranteed로 만들라"는 작업이 나오면 이 패턴을 씁니다.
apiVersion: v1
kind: Pod
metadata:
name: guaranteed-demo
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "256Mi"limits만 지정해도 쿠버네티스가 requests를 같은 값으로 채워 주므로 결과는 Guaranteed가 됩니다. 그러나 시험에서는 의도를 분명히 하기 위해 requests와 limits를 모두 명시하는 편이 안전합니다.
QoS class 확인 #
부여된 QoS class는 describe로 바로 보입니다.
k describe pod guaranteed-demo | grep "QoS Class"
# QoS Class: GuaranteedLimitRange: 네임스페이스에 기본값과 한계를 건다 #
개별 Pod마다 requests/limits를 빠뜨리면 BestEffort가 되어 위험합니다. LimitRange는 네임스페이스 안의 컨테이너에 기본값을 채워 주고, 허용 범위의 최소/최대를 강제하는 정책 오브젝트입니다.
default: 컨테이너가 limits를 지정하지 않으면 채워 넣을 기본 limits입니다.defaultRequest: requests를 지정하지 않으면 채워 넣을 기본 requests입니다.min/max: 컨테이너가 지정할 수 있는 값의 하한과 상한입니다. 이 범위를 벗어나는 Pod는 생성이 거부됩니다.
apiVersion: v1
kind: LimitRange
metadata:
name: cpu-mem-limits
namespace: dev
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "250m"
memory: "128Mi"
min:
cpu: "100m"
memory: "64Mi"
max:
cpu: "1"
memory: "512Mi"이 LimitRange가 적용된 dev 네임스페이스에서 requests/limits 없이 Pod를 만들면, 컨테이너는 자동으로 requests 250m/128Mi와 limits 500m/256Mi를 부여받아 Burstable이 됩니다. 반대로 max를 넘는 limits를 적어 제출하면 Pod가 거부됩니다.
k apply -f limitrange.yaml
k describe limitrange cpu-mem-limits -n devResourceQuota: 네임스페이스 총량을 막는다 #
LimitRange가 컨테이너 하나하나의 기본값과 범위를 다룬다면, ResourceQuota는 네임스페이스 전체의 합계를 제한합니다. 한 팀이 클러스터 자원을 독차지하지 못하게 막는 용도입니다. ResourceQuota가 걸린 네임스페이스에서는 모든 Pod가 requests/limits를 반드시 지정해야 하며, 빠뜨리면 생성이 거부됩니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "2"
requests.memory: "2Gi"
limits.cpu: "4"
limits.memory: "4Gi"
pods: "10"이 ResourceQuota는 dev 네임스페이스 안의 모든 Pod가 요청하는 requests 합을 cpu 2코어와 memory 2Gi로, limits 합을 cpu 4코어와 memory 4Gi로, Pod 개수를 10개로 묶습니다. LimitRange와 ResourceQuota를 함께 두면 “기본값 자동 채움 + 총량 상한"이 동시에 걸립니다.
k apply -f resourcequota.yaml
k get resourcequota dev-quota -n dev
# 현재 사용량과 한도를 함께 표시
k describe resourcequota dev-quota -n dev실사용량 확인: k top #
선언한 값이 실제 사용량과 맞는지는 kubectl top으로 봅니다. 이 명령은 클러스터에 metrics-server가 설치되어 있어야 동작합니다.
# Pod별 실제 CPU/memory 사용량
k top pod
k top pod resource-demo -n dev
# 노드 단위 사용량
k top nodek top으로 본 실사용량을 기준으로 requests는 평소 사용량에, limits는 피크 사용량에 맞춰 조정하는 것이 일반적인 튜닝 방향입니다.
시험 포인트 #
- QoS class 판별이 핵심입니다. requests와 limits를 cpu/memory 모두 지정하고 두 값이 같으면 Guaranteed, 일부만 있거나 값이 다르면 Burstable, 둘 다 없으면 BestEffort입니다.
describe의QoS Class줄로 즉시 확인합니다. - OOMKilled = memory limits 초과입니다.
k describe pod의Last State에서Reason: OOMKilled를 읽고, memory limits를 올리거나 사용량을 줄이는 방향으로 해결합니다. CPU 초과는 죽지 않고 스로틀만 걸린다는 점을 혼동하지 않습니다. - 단위를 정확히 씁니다. CPU는
500m=0.5코어, memory는Mi/Gi(2진)와M/G(10진)가 다른 값입니다. 문제가Mi를 요구하면M으로 쓰지 않습니다. - LimitRange의 default vs defaultRequest를 구분합니다.
default는 limits 기본값,defaultRequest는 requests 기본값입니다. - dry-run으로 뼈대를 만든 뒤 resources만 추가하는 흐름이 빠릅니다.
k run app --image=nginx $do > pod.yaml후resources블록을 편집합니다.
리소스 관리는 매니페스트 한 블록으로 끝나지만, QoS와 OOMKilled의 동작을 묻는 판별 문제까지 함께 나오므로 단위와 등급 조건을 손에 익히는 것이 점수로 직결됩니다. 자원 요청과 노드 스케줄링의 더 넓은 맥락은 K8s 중급 #4에서도 다루므로 함께 보면 도움이 됩니다.
정리 #
이번 글에서 잡은 것:
- requests는 스케줄링 기준, limits는 상한입니다. CPU는
m(millicore), memory는Mi/Gi단위로 표기합니다. - CPU 초과는 스로틀, memory 초과는 OOMKilled입니다. CPU는 죽지 않고 느려지지만 memory는 종료됩니다.
- QoS class 3종: Guaranteed(requests==limits 전부 지정), Burstable(일부 지정), BestEffort(미지정). eviction은 BestEffort부터 쫓아냅니다.
- LimitRange는 네임스페이스에 default/defaultRequest와 min/max를 걸고, ResourceQuota는 네임스페이스 총량을 제한합니다.
- 확인은
k describe pod(QoS Class,OOMKilled)와k top pod(실사용량)로 합니다.
다음: Volumes #
자원의 양을 잡았으니, 이제 컨테이너가 데이터를 어디에 두는지를 다룹니다.
#17 Volumes: emptyDir, PVC, projected, ephemeral에서는 Pod 안에서 컨테이너끼리 디렉터리를 공유하는 emptyDir, 영속 저장을 요청하는 PersistentVolumeClaim, ConfigMap과 Secret을 한곳에 모아 마운트하는 projected volume, 그리고 수명이 Pod에 묶이는 ephemeral volume까지 YAML로 직접 붙여 보며 정리하겠습니다.