SUMMARY
Docker는 메모리 한계를 넘으면 컨테이너를 종료하고 거기서 끝난다. Kubernetes는 종료 후에
restartPolicy에 따라 파드를 다시 띄우며, 재시작 간격도 백오프로 벌린다. 더 큰 차이는requests/limits라는 이중 구조로 스케줄링과 QoS까지 관리한다는 점이다.
1. 이슈
두 플랫폼의 리소스 관리 방식이 다르다는 것은 알고 있었지만, 막상 운영 중에 파드가 종료되거나 스로틀링에 걸리면 매번 “이게 Docker 때랑 뭐가 달랐지” 하고 헷갈렸다. 그래서 한 번 정리해둔다.
핵심은 메모리 한계를 넘겼을 때 컨테이너(또는 파드)의 생명주기가 어떻게 관리되는가이다. 여기서 두 플랫폼이 갈린다.
2. 해결
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 리소스 관리의 또 다른 축은 requests와 limits로 나뉘는 이중 구조다.
- requests: 스케줄링 기준이 되는 최소 보장량이다. 노드는 파드들의
requests합이 노드 용량을 넘지 않는 범위에서 파드를 배치한다. - limits: 컨테이너가 최대로 사용할 수 있는 상한이다. 이 값을 넘으면 앞서 본 대로 메모리에서는 종료되고 CPU에서는 스로틀링된다.
Docker의 리소스 제한이 상한 하나였다면, Kubernetes에는 requests라는 스케줄링 축이 하나 더 존재하는 셈이다. 덕분에 단순 제한을 넘어서 스케줄링과 서비스 품질(QoS) 관리까지 가능해진다.
2-3. 노드 리소스 부족과 파드 축출
개별 파드가 아니라 노드 전체의 리소스가 부족해지면, Kubernetes는 파드 축출(Pod Eviction)로 노드를 보호한다. 어떤 파드를 먼저 내보낼지는 대체로 QoS 클래스 순서를 따른다(실제로는 파드 우선순위와 requests 대비 실제 사용량도 함께 고려한다).
- BestEffort:
requests와limits가 하나도 없는 파드다. 가장 먼저 축출된다. - Burstable:
requests나limits는 있지만 Guaranteed 조건은 충족하지 못하는 파드다. BestEffort 다음으로 축출된다. - Guaranteed: 모든 컨테이너에
requests와limits가 같은 값으로 설정된 파드다. 가장 마지막까지 남는다.
3. 차이 정리
| 구분 | Docker | Kubernetes |
|---|---|---|
| 메모리 초과 시 | 컨테이너 종료 | 파드 종료 후 자동 재시작 |
| 재시작 정책 | 직접 설정 (--restart) | 기본 제공 (restartPolicy) |
| 리소스 계층 | 단일 (limits) | 이중 (requests + limits) |
| 관리 단위 | 개별 컨테이너 | 클러스터 전체 파드 |
| 축출 정책 | 없음 | QoS 기반 우선순위 축출 |
4. 값을 잡을 때
- Docker: 메모리 사용 패턴을 확인하고 여유를 두어 limit을 정한다. 그리고
--restart=unless-stopped같은 재시작 정책은 웬만하면 함께 설정해둔다. 설정하지 않으면 컨테이너가 종료된 후에 자동으로 다시 띄워 주는 주체가 없다. - Kubernetes:
requests는 평상시 실사용량에 가깝게,limits는 순간 트래픽(Burst)을 흡수할 만큼 여유 있게 잡는 편이 낫다.
NOTE
requests를 너무 낮게 잡으면 한 노드에 파드가 몰려서 서로 자원을 잠식하는 ‘노이지 네이버(Noisy Neighbor)’ 상황이 생긴다. 반대로 너무 높게 잡으면 스케줄 가능한 파드 수가 줄어서 노드 활용률이 낮아진다. 결국 이 사이 어디쯤을 찾는 것이 운영자의 몫이다.
3. 확인
동작을 직접 확인하고 싶으면 아래 방법을 사용한다.
- Docker: 메모리를 초과하는 프로세스를 실행하면 컨테이너 상태가
Exited로 바뀐다.docker ps -a로 확인할 수 있다. - Kubernetes:
kubectl get pods -w로 OOMKilled된 파드가 재시작되는 과정을 관찰한다. 종료가 반복되면CrashLoopBackOff까지 진행된다.