Kubernetes Resource Management

Kubernetes Resource Management란? 리소스 요청과 제한의 핵심

Kubernetes 클러스터에서 “이 Pod가 갑자기 메모리를 다 써버려서 같은 노드의 다른 Pod가 OOMKilled됐다”, “스케줄러가 Pod를 배치할 노드를 찾지 못해 Pending 상태가 계속된다” — 이런 운영 사고의 근본 원인은 리소스 관리 미흡입니다.

Kubernetes의 리소스 관리는 requests(요청)limits(제한)이라는 두 축으로 동작합니다. 이 두 값이 스케줄링, QoS 클래스, OOM 우선순위, Eviction을 결정하며, LimitRangeResourceQuota로 네임스페이스 수준의 거버넌스를 구현합니다.

이 글에서는 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

⚠️ 함정: 128M128Mi는 약 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의 defaultdefaultRequest를 반드시 함께 설정해야 합니다. 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의 차이를 이해하고, 실사용량 데이터를 기반으로 사이징하는 것이 안정적인 클러스터 운영의 핵심입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux