Kubernetes Resource Management란? 리소스 요청과 제한의 핵심
Kubernetes 클러스터에서 “이 Pod가 갑자기 메모리를 다 써버려서 같은 노드의 다른 Pod가 OOMKilled됐다”, “스케줄러가 Pod를 배치할 노드를 찾지 못해 Pending 상태가 계속된다” — 이런 운영 사고의 근본 원인은 리소스 관리 미흡입니다.
Kubernetes의 리소스 관리는 requests(요청)와 limits(제한)이라는 두 축으로 동작합니다. 이 두 값이 스케줄링, QoS 클래스, OOM 우선순위, Eviction을 결정하며, LimitRange와 ResourceQuota로 네임스페이스 수준의 거버넌스를 구현합니다.
이 글에서는 requests/limits의 동작 원리부터 QoS 클래스 3가지, CPU 스로틀링 메커니즘, 메모리 OOMKill 우선순위, LimitRange 기본값 주입, ResourceQuota 테넌트 격리, 그리고 HPA와의 상호작용까지 운영 수준에서 완전히 다룹니다.
requests와 limits: 스케줄링과 런타임의 두 가지 관점
Pod 스펙에서 리소스를 지정하는 두 필드는 완전히 다른 시점에 사용됩니다:
| 필드 | 사용 시점 | 동작 | 초과 시 |
|---|---|---|---|
requests |
스케줄링 시 | 노드 배치 기준. “최소한 이만큼은 보장해라” | – |
limits |
런타임 시 | 컨테이너 실행 중 상한선. “절대 이 이상 쓸 수 없다” | CPU: 스로틀링 / Memory: OOMKill |
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- name: app
image: web-app:latest
resources:
requests:
cpu: "250m" # 0.25 CPU 코어 보장
memory: "256Mi" # 256 MiB 메모리 보장
limits:
cpu: "500m" # 최대 0.5 CPU 코어
memory: "512Mi" # 최대 512 MiB (초과 시 OOMKill)
CPU 단위 이해
| 표기 | 의미 | 설명 |
|---|---|---|
1 |
1 vCPU 코어 | AWS 1 vCPU, GCP 1 Core |
500m |
0.5 코어 | m = millicores (1000m = 1 코어) |
100m |
0.1 코어 | CFS 스케줄러로 100ms 중 10ms 사용 |
메모리 단위 이해
| 표기 | 의미 | 주의 |
|---|---|---|
128Mi |
128 × 2²⁰ bytes (≈134MB) | Mi = Mebibyte (2진수) |
128M |
128 × 10⁶ bytes (=128MB) | M = Megabyte (10진수) |
1Gi |
1 × 2³⁰ bytes (≈1.07GB) | Gi = Gibibyte |
⚠️ 함정: 128M과 128Mi는 약 5% 차이가 납니다. 일관성을 위해 항상 Mi/Gi(2진수)를 사용하세요.
QoS(Quality of Service) 클래스: Pod 우선순위의 핵심
Kubernetes는 requests/limits 설정에 따라 자동으로 3가지 QoS 클래스를 부여합니다. 이 클래스가 노드 압박(Node Pressure) 시 어떤 Pod가 먼저 제거되는지를 결정합니다.
| QoS 클래스 | 조건 | Eviction 우선순위 | 적합한 워크로드 |
|---|---|---|---|
| Guaranteed | 모든 컨테이너의 CPU/Memory requests == limits | 가장 마지막에 제거 | DB, 핵심 서비스 |
| Burstable | requests < limits (부분 설정 포함) | requests 대비 실사용량 비율로 결정 | 일반 웹 서비스 |
| BestEffort | requests/limits 모두 미설정 | 가장 먼저 제거 | 배치, 실험 워크로드 |
# Guaranteed: requests == limits
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m" # requests와 동일
memory: "512Mi" # requests와 동일
# Burstable: requests < limits
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1" # requests보다 큼
memory: "1Gi" # requests보다 큼
# BestEffort: 아무것도 설정 안 함
resources: {} # 또는 아예 생략
QoS 확인 방법:
kubectl get pod web-app -o jsonpath='{.status.qosClass}'
# Burstable
CPU 스로틀링: limits 초과 시 무슨 일이 벌어지는가
CPU는 압축 가능한 리소스(compressible resource)입니다. limits를 초과하면 프로세스가 죽지 않고 스로틀링(throttling)됩니다.
Linux CFS(Completely Fair Scheduler)는 cpu.cfs_period_us(기본 100ms)와 cpu.cfs_quota_us를 사용합니다:
| CPU limits | cfs_quota_us | 의미 |
|---|---|---|
| 500m | 50000 | 100ms 중 50ms만 CPU 사용 가능 |
| 1 | 100000 | 100ms 중 100ms 사용 (1코어 전체) |
| 2 | 200000 | 100ms 중 200ms 사용 (2코어분) |
⚠️ CPU 스로틀링의 숨겨진 위험: 멀티스레드 애플리케이션(Java, Node.js 등)에서 cpu.limit=1로 설정하면, 여러 스레드가 100ms quota를 빠르게 소진하여 나머지 시간은 전부 스로틀링됩니다. 응답 지연(latency spike)의 주범입니다.
# 스로틀링 발생 여부 확인
kubectl exec web-app -- cat /sys/fs/cgroup/cpu/cpu.stat
# nr_periods 12345
# nr_throttled 678 ← 스로틀링 발생 횟수
# throttled_time 123456789 ← 총 스로틀링 시간 (ns)
# cgroup v2 환경
kubectl exec web-app -- cat /sys/fs/cgroup/cpu.stat
실무 권장: 많은 팀이 CPU limits를 아예 설정하지 않는 전략을 채택합니다. requests만 설정하면 스케줄링은 보장하면서 스로틀링 없이 유휴 CPU를 활용할 수 있습니다. Google의 내부 권장 사항도 이와 같습니다.
메모리 OOMKill: limits 초과 시의 치명적 결과
메모리는 압축 불가능한 리소스(incompressible resource)입니다. limits를 초과하면 컨테이너가 즉시 종료(OOMKill)됩니다.
# OOMKilled 확인
kubectl describe pod web-app
# ...
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137 ← 128 + 9 (SIGKILL)
# 노드 레벨 OOM 이벤트 확인
kubectl get events --field-selector reason=OOMKilling
OOMKill 우선순위: oom_score_adj
노드의 메모리가 부족할 때 Linux OOM Killer는 oom_score가 높은 프로세스를 먼저 종료합니다:
| QoS 클래스 | oom_score_adj | OOMKill 순서 |
|---|---|---|
| BestEffort | 1000 | 1순위 (가장 먼저) |
| Burstable | 2~999 (requests 비율 기반) | 2순위 |
| Guaranteed | -997 | 3순위 (가장 마지막) |
핵심: Burstable Pod 간에는 requests 대비 실사용량 비율이 높은 Pod가 먼저 제거됩니다. 예: requests=256Mi인데 400Mi 사용 중인 Pod가, requests=512Mi인데 600Mi 사용 중인 Pod보다 먼저 제거됩니다.
LimitRange: 네임스페이스 레벨 기본값과 제한
LimitRange는 네임스페이스 내 개별 컨테이너/Pod의 리소스에 기본값을 주입하고 상한/하한을 강제합니다.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: dev
spec:
limits:
# 컨테이너 레벨 제한
- type: Container
default: # limits 미설정 시 자동 주입
cpu: "500m"
memory: "512Mi"
defaultRequest: # requests 미설정 시 자동 주입
cpu: "100m"
memory: "128Mi"
max: # 허용 최대값
cpu: "2"
memory: "4Gi"
min: # 허용 최소값
cpu: "50m"
memory: "64Mi"
maxLimitRequestRatio: # limits/requests 비율 제한
cpu: "4" # CPU limits는 requests의 4배까지만
memory: "4" # Memory limits는 requests의 4배까지만
# Pod 레벨 제한 (모든 컨테이너 합산)
- type: Pod
max:
cpu: "4"
memory: "8Gi"
LimitRange 동작 시점과 주의사항
| 시나리오 | 동작 |
|---|---|
| requests/limits 모두 미설정 | default와 defaultRequest 모두 자동 주입 |
| limits만 설정 | requests = limits로 자동 설정 (Guaranteed 됨!) |
| requests만 설정 | default limits 주입 |
| 설정값이 min 미만 또는 max 초과 | Admission 거부 (Pod 생성 실패) |
⚠️ 함정: LimitRange는 이미 실행 중인 Pod에는 적용되지 않습니다. 새로 생성되는 Pod에만 적용됩니다. 또한 limits만 설정한 Pod에 requests가 자동으로 limits와 동일하게 설정되어 의도치 않게 Guaranteed QoS가 될 수 있습니다.
ResourceQuota: 네임스페이스 전체 리소스 총량 제한
ResourceQuota는 네임스페이스 내 모든 Pod/리소스의 총합을 제한합니다. 멀티테넌트 클러스터에서 팀 간 리소스 공정 분배에 필수적입니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
# 컴퓨트 리소스 총량
requests.cpu: "8" # 모든 Pod의 CPU requests 합계 ≤ 8코어
requests.memory: "16Gi" # 모든 Pod의 Memory requests 합계 ≤ 16Gi
limits.cpu: "16" # 모든 Pod의 CPU limits 합계 ≤ 16코어
limits.memory: "32Gi" # 모든 Pod의 Memory limits 합계 ≤ 32Gi
# 오브젝트 수 제한
pods: "50" # 최대 50개 Pod
services: "20" # 최대 20개 Service
configmaps: "30"
secrets: "30"
persistentvolumeclaims: "10"
services.loadbalancers: "2" # LoadBalancer 서비스 제한 (비용 관리)
services.nodeports: "5"
ResourceQuota가 활성화되면 requests/limits가 필수!
⚠️ 중요: 네임스페이스에 CPU/Memory ResourceQuota가 설정되면, 해당 네임스페이스의 모든 Pod에 반드시 requests/limits를 명시해야 합니다. 미설정 시 Pod 생성이 거부됩니다.
# ResourceQuota 사용량 확인
kubectl describe resourcequota dev-quota -n dev
# Name: dev-quota
# Resource Used Hard
# -------- ---- ----
# limits.cpu 4200m 16
# limits.memory 8Gi 32Gi
# pods 12 50
# requests.cpu 2100m 8
# requests.memory 4Gi 16Gi
이 때문에 LimitRange의 default와 defaultRequest를 반드시 함께 설정해야 합니다. LimitRange가 없으면 개발자가 requests/limits를 누락할 때마다 Pod 생성이 실패합니다.
QoS별 ResourceQuota: 우선순위 기반 할당
# Guaranteed QoS Pod만 허용하는 쿼타
apiVersion: v1
kind: ResourceQuota
metadata:
name: guaranteed-only
namespace: critical
spec:
hard:
pods: "20"
scopeSelector:
matchExpressions:
- scopeName: PriorityClass
operator: In
values: ["high-priority"]
# BestEffort Pod는 별도 제한
apiVersion: v1
kind: ResourceQuota
metadata:
name: besteffort-limit
namespace: dev
spec:
hard:
pods: "10"
scopes:
- BestEffort # BestEffort QoS Pod만 카운트
requests/limits 사이징 전략: 실무 가이드
전략 1: Burstable (대부분의 웹 서비스)
# requests: P50 사용량 기준
# limits: P99 피크 사용량 기준
resources:
requests:
cpu: "200m" # 평균 사용량
memory: "256Mi" # 안정 상태 메모리
limits:
cpu: "1" # 스파이크 대응 (또는 CPU limits 생략)
memory: "512Mi" # 피크 메모리 + 20% 여유
전략 2: Guaranteed (데이터베이스, 핵심 서비스)
# 안정적 성능이 필수인 워크로드
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "2" # requests와 동일
memory: "4Gi" # requests와 동일
전략 3: CPU limits 없음 (Google SRE 권장)
# CPU 스로틀링 방지, 유휴 CPU 활용 극대화
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
# cpu 생략! → 노드의 여유 CPU 자유 사용
memory: "512Mi" # 메모리 limits는 반드시 설정
주의: CPU limits를 생략하면 HPA의 CPU utilization 계산에서 requests 기준 퍼센티지가 사용됩니다. requests를 정확하게 설정해야 HPA가 올바르게 동작합니다.
실제 사용량 기반 사이징: VPA와 kubectl top
# 실시간 리소스 사용량 확인
kubectl top pods -n production
# NAME CPU(cores) MEMORY(bytes)
# web-app-1 125m 198Mi
# web-app-2 89m 187Mi
# api-server 342m 456Mi
# 노드별 리소스 현황
kubectl top nodes
# NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
# node-1 2450m 61% 12Gi 75%
# node-2 1890m 47% 9Gi 56%
# 노드의 할당 가능 리소스 vs 실제 요청량
kubectl describe node node-1 | grep -A 5 "Allocated"
# Allocated resources:
# Resource Requests Limits
# cpu 3200m (80%) 6400m (160%)
# memory 8Gi (50%) 16Gi (100%)
Overcommit 비율: 위 예시에서 CPU Limits 합계(160%)가 노드 용량을 초과합니다. 이는 의도된 오버커밋이며, 모든 Pod가 동시에 limits까지 사용하지 않는다는 가정에 기반합니다. Burstable 전략의 핵심입니다.
Eviction과 Node Pressure: 리소스 부족 시 동작
kubelet은 노드 리소스가 임계치를 넘으면 Pod를 퇴거(Evict)시킵니다:
| Signal | 기본 임계치 | 동작 |
|---|---|---|
| memory.available | < 100Mi | MemoryPressure → BestEffort Pod부터 Evict |
| nodefs.available | < 10% | DiskPressure → 컨테이너 이미지/로그 정리 |
| imagefs.available | < 15% | 미사용 이미지 삭제 |
| pid.available | < 100 | PIDPressure |
Eviction 순서: BestEffort → Burstable (requests 초과 사용량 기준) → Guaranteed. 이것이 Taints & Tolerations와 결합되어 노드 안정성을 보장합니다.
실무 안티패턴 5가지
1. requests/limits 미설정
# ❌ BestEffort → 첫 번째 Eviction 대상
containers:
- name: app
image: my-app:latest
# resources 없음!
# ✅ 최소한 requests는 반드시 설정
containers:
- name: app
image: my-app:latest
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
2. requests를 너무 크게 설정 (리소스 낭비)
# ❌ 실제 100m 사용하면서 2코어 예약 → 1900m 낭비
resources:
requests:
cpu: "2" # 실사용량의 20배!
memory: "4Gi" # 실사용량의 8배!
# ✅ 실사용량 기반 + 약간의 여유
resources:
requests:
cpu: "150m" # P50 사용량 + 50% 버퍼
memory: "256Mi"
3. 메모리 limits 미설정
CPU limits는 생략해도 되지만, 메모리 limits는 반드시 설정해야 합니다. 메모리 누수가 있는 컨테이너가 노드 전체를 죽일 수 있습니다.
4. init 컨테이너 리소스 미고려
# Pod의 effective requests = max(initContainers) vs sum(containers)
spec:
initContainers:
- name: migrate
resources:
requests:
cpu: "1" # 마이그레이션에 CPU 필요
memory: "1Gi"
containers:
- name: app
resources:
requests:
cpu: "200m"
memory: "256Mi"
# effective requests: cpu=1 (init가 더 큼), memory=1Gi
5. LimitRange 없이 ResourceQuota만 설정
ResourceQuota가 있으면 requests/limits가 필수인데, LimitRange로 기본값을 제공하지 않으면 개발자가 매번 수동 설정해야 합니다.
완전한 네임스페이스 리소스 거버넌스 예시
---
apiVersion: v1
kind: Namespace
metadata:
name: team-backend
labels:
team: backend
---
# 1. 기본값 주입 + 개별 컨테이너 제한
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-backend
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "8Gi"
min:
cpu: "50m"
memory: "64Mi"
---
# 2. 네임스페이스 총량 제한
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-backend
spec:
hard:
requests.cpu: "16"
requests.memory: "32Gi"
limits.cpu: "32"
limits.memory: "64Gi"
pods: "100"
services: "30"
persistentvolumeclaims: "20"
services.loadbalancers: "3"
정리: 리소스 관리 체크리스트
| 항목 | 체크 |
|---|---|
| 모든 프로덕션 Pod에 requests/limits 설정 | ☐ |
| 메모리 limits는 반드시 설정 (CPU limits는 선택) | ☐ |
| 핵심 서비스는 Guaranteed QoS (requests == limits) | ☐ |
| LimitRange로 네임스페이스 기본값 주입 | ☐ |
| ResourceQuota로 팀별 총량 제한 | ☐ |
| kubectl top으로 실사용량 기반 사이징 | ☐ |
| CPU 스로틀링 모니터링 (nr_throttled) | ☐ |
| init 컨테이너 리소스 고려 | ☐ |
| Mi/Gi 단위 일관 사용 (M/G 아닌) | ☐ |
| 오버커밋 비율 정기 점검 | ☐ |
Kubernetes 리소스 관리는 단순히 숫자를 넣는 것이 아니라, 스케줄링 보장(requests), 런타임 보호(limits), QoS 우선순위, 네임스페이스 거버넌스(LimitRange + ResourceQuota)가 유기적으로 연결된 시스템입니다. 특히 CPU 스로틀링과 메모리 OOMKill의 차이를 이해하고, 실사용량 데이터를 기반으로 사이징하는 것이 안정적인 클러스터 운영의 핵심입니다.