SUMMARY

Docker는 메모리 한계를 넘으면 컨테이너를 종료하고 거기서 끝이다. Kubernetes는 종료 후 restartPolicy에 따라 다시 살리고, 그마저도 백오프로 간격을 벌린다. 더 큰 차이는 requests/limits라는 이중 구조로 스케줄링과 QoS까지 다룬다는 점이다.

💬 이슈

둘의 리소스 관리 방식이 다르다는 건 알고 있었는데, 막상 운영 중에 파드가 죽거나 스로틀링에 걸리면 매번 “이게 Docker 때랑 뭐가 달랐지” 하고 헷갈렸다. 그래서 한번 정리해둔다.

핵심은 메모리 한계를 넘겼을 때 컨테이너(또는 파드)의 생명주기가 어떻게 관리되느냐다. 여기서 두 플랫폼이 갈린다.

🧗 해결

1. Docker

1-1. 메모리 한계 초과

Docker는 컨테이너가 설정된 메모리 한계를 넘으면, 커널의 OOM Killer(Out of Memory Killer)가 해당 컨테이너 안의 프로세스를 종료시킨다.

# 메모리 한계를 512MB로 설정한 컨테이너
# 이 안에서 512MB를 넘게 쓰면 커널 OOM Killer가 프로세스를 종료한다.
docker run -m 512m --name test-container nginx

여기서 중요한 건 Docker는 컨테이너를 멈출 뿐 알아서 다시 띄우지 않는다는 점이다. 죽었을 때 다시 실행되게 하려면 --restart 정책을 직접 붙여야 한다.

1-2. CPU 한계 초과

CPU는 메모리와 다르다. 종료가 아니라 스로틀링(Throttling)이다. 한계에 닿으면 컨테이너를 죽이는 대신 CPU 사용을 강제로 조인다.

# CPU를 0.5 코어로 제한한다.
# 이 컨테이너는 CPU를 아무리 써도 0.5 코어만큼의 성능만 낸다.
docker run --cpus="0.5" --name cpu-limited nginx

컨테이너가 죽지는 않지만 응답이 느려지는 식으로 성능이 떨어진다.

2. Kubernetes

2-1. 메모리 한계 초과

Kubernetes도 메모리 한계를 넘으면 파드를 종료한다. 커널 OOM Killer가 도는 건 Docker와 같다. 다른 건 그다음이다. restartPolicy에 따라 자동으로 다시 시작하고, 파드의 기본값이 Always라 별도 설정 없이도 재시작된다.

# memory limit을 512Mi로 설정한 파드
apiVersion: v1
kind: Pod
metadata:
  name: memory-limited-pod
spec:
  containers:
  - name: app
    image: nginx
    resources:
      limits:
        memory: "512Mi"
      requests:
        memory: "256Mi"
  restartPolicy: Always  # 파드 기본값이 Always다. OOMKilled 시 재시작된다.

파드가 OOMKilled 되면 restartPolicy에 따라 다시 뜬다. 이때 무한 재시작으로 노드에 부하가 쏠리지 않도록, 재시작 간격을 지수적으로 늘리는 백오프(Backoff)가 걸린다.

IMPORTANT

자동 재시작은 일시적인 메모리 급증을 넘기는 데는 유용하다. 하지만 메모리 누수처럼 근본 원인이 있으면 파드가 뜨자마자 다시 죽길 반복하는 CrashLoopBackOff에 빠진다. 재시작이 잦으면 문제를 가리는 게 아니라 문제가 있다는 신호로 봐야 한다.

2-2. requests와 limits

Kubernetes 리소스 관리의 또 다른 축은 requestslimits로 나뉜 이중 구조다.

  • requests: 스케줄링 기준이 되는 최소 보장량. 노드는 파드들의 requests 합이 노드 용량을 넘지 않는 선에서 파드를 받는다.
  • limits: 컨테이너가 최대로 쓸 수 있는 상한. 이 값을 넘으면 앞서 본 대로 종료되거나(메모리) 스로틀링된다(CPU).

Docker의 리소스 제한이 사실상 상한 하나였다면, Kubernetes는 requests라는 스케줄링 축이 하나 더 있는 셈이다. 이 덕에 단순 제한을 넘어 스케줄링과 서비스 품질(QoS) 관리가 가능해진다.

2-3. 노드 리소스 부족과 파드 축출

개별 파드가 아니라 노드 전체의 리소스가 부족해지면, Kubernetes는 파드 축출(Pod Eviction)로 노드를 지킨다. 어떤 파드를 먼저 내보낼지는 대체로 QoS 클래스 순서를 따른다(실제로는 파드 우선순위, requests 대비 사용량도 함께 본다).

  1. BestEffort: requestslimits가 하나도 없는 파드. 가장 먼저 쫓겨난다.
  2. Burstable: requestslimits는 있지만 Guaranteed 조건은 못 채운 파드. BestEffort 다음이다.
  3. Guaranteed: 모든 컨테이너에 requestslimits가 같은 값으로 잡힌 파드. 가장 마지막까지 남는다.

3. 차이 정리

구분DockerKubernetes
메모리 초과 시컨테이너 종료파드 종료 후 자동 재시작
재시작 정책직접 설정 (--restart)기본 제공 (restartPolicy)
리소스 계층단일 (limits)이중 (requests + limits)
관리 단위개별 컨테이너클러스터 전체 파드
축출 정책없음QoS 기반 우선순위 축출

4. 값을 잡을 때

  • Docker: 메모리 사용 패턴을 보고 여유를 둬서 limit을 잡는다. 그리고 --restart=unless-stopped 같은 재시작 정책은 웬만하면 붙여둔다. 안 붙이면 죽고 나서 아무도 안 살린다.
  • Kubernetes: requests는 평상시 실사용량에 가깝게, limits는 순간 트래픽(Burst)을 받아줄 만큼 여유 있게 잡는 편이 낫다.

NOTE

requests를 너무 낮게 잡으면 한 노드에 파드가 몰려 서로 자원을 갉아먹는 ‘노이지 네이버(Noisy Neighbor)‘가 생긴다. 반대로 너무 높게 잡으면 스케줄 가능한 파드 수가 줄어 노드가 텅텅 비게 된다. 결국 이 사이 어디쯤을 찾는 게 일이다.

✅ 확인

동작을 직접 보고 싶으면 이렇게 확인한다.

  • Docker: 메모리를 초과하는 프로세스를 돌리면 컨테이너가 Exited로 바뀐다. docker ps -a로 보인다.
  • Kubernetes: kubectl get pods -w로 OOMKilled된 파드가 재시작되는 과정을 본다. 반복해서 죽으면 CrashLoopBackOff까지 넘어간다.

🔗 참고