- Published on
EKS Pod Eviction Loop - PDB·우선순위·Spot 정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스가 아닌 EKS(EC2 워커) 환경에서 Pod eviction loop(파드가 Evicted → 다른 노드로 이동 → 다시 Evicted… 반복)는 생각보다 자주 발생합니다. 특히 Spot 노드를 섞어 비용을 줄이거나, 서비스 가용성을 위해 PDB(PodDisruptionBudget) 를 촘촘히 걸어둔 경우, 그리고 노드/파드 우선순위(PriorityClass) 를 도입한 경우에 “각각은 맞는 설정인데 조합이 위험한” 상태가 됩니다.
이 글에서는 EKS에서 eviction loop가 만들어지는 전형적인 메커니즘을 설명하고, PDB·우선순위·Spot을 함께 쓸 때의 안전한 설계/운영 체크리스트와 예시 매니페스트를 제공합니다.
eviction loop란 무엇이고, 왜 EKS에서 더 잘 보이나
Kubernetes에서 eviction은 크게 두 갈래로 발생합니다.
- 자원 압박 기반 eviction: 노드의
memory/disk/pid압박으로 kubelet이 파드를 축출(Evicted)합니다. - 의도된/비의도된 disruption: 노드 드레인(drain), 노드 종료(Spot interruption), 업그레이드/스케일인 등으로 파드가 다른 곳으로 이동합니다.
EKS에서 loop가 잘 보이는 이유는 다음이 겹치기 때문입니다.
- Managed Node Group/Cluster Autoscaler/Karpenter가 노드를 자주 교체하거나 스케일 인/아웃을 수행
- Spot은 예고 후(보통 2분) 종료되며, 종료가 연쇄적으로 발생할 수 있음
- PDB가 drain을 막아 “노드는 죽는데 파드는 못 나감” 같은 교착을 만들거나, 반대로 “파드는 나갔는데 다시 들어갈 곳이 없음”을 만들 수 있음
- PriorityClass/Preemption이 낮은 우선순위 파드를 계속 밀어내며 재시도 루프를 유발
또한 eviction loop는 종종 사용자에게 “서비스 503/504”로 관측됩니다. 파드가 Running으로 보이더라도 readiness/endpoint 반영이 깨지면 트래픽이 실패할 수 있으니, 증상이 HTTP 오류라면 아래 글도 함께 참고하면 좋습니다.
먼저: “Evicted”의 종류를 구분하는 빠른 진단
loop를 잡기 전에 어떤 eviction인지부터 분리해야 합니다.
1) 이벤트로 1차 분류
kubectl get pods -A --field-selector=status.phase=Failed
kubectl describe pod -n <ns> <pod>
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
Reason: Evicted+ 메시지에The node was low on resource: memory→ 자원 압박 evictionPreempted/preemption관련 이벤트 → 우선순위/선점 루프- 노드가
NotReady/종료/드레인 이벤트와 맞물림 → Spot/스케일인/업그레이드 disruption
2) 노드 상태/압박 확인
kubectl describe node <node-name> | egrep -n "Conditions|MemoryPressure|DiskPressure|PIDPressure|Allocatable|Allocated" -n
kubectl top node
kubectl top pod -A --sort-by=memory | tail -n 20
여기서 Allocatable 대비 Allocated가 과도하거나, 특정 파드가 메모리를 폭증시키면 eviction이 반복됩니다. (LLM/RAG/벡터검색 등 메모리 민감 워크로드라면 OOM/eviction이 엮이기 쉽습니다.)
PDB가 eviction loop를 만드는 전형적인 패턴
PDB는 “자발적(voluntary) disruption”을 제어합니다. 즉, kubectl drain, 노드 업그레이드, 오토스케일러의 안전한 축출 같은 상황에서 동시에 몇 개까지 내려도 되는지를 제한합니다.
문제는 다음 두 가지입니다.
PDB가 너무 빡빡해서 drain이 막힘
- 오토스케일러/업그레이드가 노드를 비우지 못하고 재시도
- Spot은 기다려주지 않으므로 노드는 죽고 파드는 강제 종료 → 다른 노드로 재스케줄 → 다시 같은 상황 반복
PDB가 의미 없는 형태로 설정됨
minAvailable: 1인데 레플리카가 1개뿐인 서비스 → 사실상 “절대 축출 불가”- HPA/오토스케일과 결합 시, 스케일 인 과정에서 계속 충돌
PDB 설계의 현실적인 기준
- 레플리카가 2 이상인 stateless 서비스:
maxUnavailable: 1이 운영 친화적 - 레플리카가 1인 서비스: PDB를 걸면 “업그레이드/드레인”이 막힐 확률이 큼. 대신 가용성 목표를 먼저 바꾸거나(최소 2개), 예외적으로 유지보수 윈도우에만 조정
PDB 예시 (권장 패턴)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: prod
spec:
maxUnavailable: 1
selector:
matchLabels:
app: api
위험 패턴 (레플리카 1 + minAvailable 1)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: singleton-pdb
namespace: prod
spec:
minAvailable: 1
selector:
matchLabels:
app: singleton
이 경우 노드 교체/드레인 시도가 계속 실패하고, Spot 종료와 결합하면 “안 나가려는 파드 + 죽어버리는 노드” 조합으로 루프가 심해집니다.
PriorityClass/Preemption이 만드는 “밀어내기 루프”
우선순위는 좋은 도구지만, 잘못 주면 낮은 우선순위 파드가 계속 쫓겨나며 재스케줄을 반복합니다.
전형적인 시나리오:
- Spot 노드가 대거 종료 → 남은 노드에 파드가 몰림
- 높은 우선순위 파드가 스케줄되어야 해서, 낮은 우선순위 파드가 Preempted
- 낮은 우선순위 파드는 다시 스케줄하려 하지만, 계속 자리가 없음 → Pending/Preempted 반복
우선순위 설계 팁
- “진짜로” 중요한 워크로드만 높은 우선순위를 부여
- 배치/비동기/재시도 가능한 워크로드는 낮은 우선순위 + Spot에 격리
- 시스템 파드(
kube-system)와 경쟁하지 않도록 리소스 요청/제한을 현실적으로 설정
PriorityClass 예시
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: prod-critical
value: 100000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "Customer-facing critical workloads"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-low
value: 1000
preemptionPolicy: Never
globalDefault: false
description: "Batch jobs that should not preempt others"
preemptionPolicy: Never를 배치에 주면, 배치가 다른 파드를 밀어내며 루프를 강화하는 상황을 줄일 수 있습니다.
Spot이 eviction loop를 증폭시키는 이유와 대응
Spot은 “언제든” 회수될 수 있고, 회수는 종종 동시다발적입니다. 따라서 다음을 반드시 분리해야 합니다.
- Spot에만 있어도 되는 것 vs 온디맨드에 있어야 하는 것
- 중단 허용(stateless, idempotent, 재처리 가능) vs 중단 민감(stateful, 세션/락, 단일 리더)
1) 노드 레이블/테인트로 워크로드 분리
Spot 노드에 테인트를 걸고, 해당 워크로드만 toleration을 주는 방식이 가장 단순하고 효과적입니다.
Spot 노드(예: Karpenter/노드그룹) 테인트 예시
# 노드에 적용되는 설정 예시(개념)
# key=spot, effect=NoSchedule
Spot 전용 워크로드 toleration + nodeSelector
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-worker
namespace: prod
spec:
replicas: 5
selector:
matchLabels:
app: batch-worker
template:
metadata:
labels:
app: batch-worker
spec:
priorityClassName: batch-low
tolerations:
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
lifecycle: spot
containers:
- name: worker
image: public.ecr.aws/docker/library/busybox:1.36
command: ["sh", "-c", "echo working; sleep 3600"]
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
핵심은 중요 서비스가 Spot으로 흘러들어가지 않게 “기본값”을 온디맨드로 두는 것입니다.
2) terminationGracePeriodSeconds + SIGTERM 처리
Spot 종료는 2분 통지로 끝나는 경우가 많아, 종료 훅이 길면 의미가 없습니다. 반대로 너무 짧으면 요청 처리 중 끊깁니다.
- HTTP API: readiness를 빨리 내려 트래픽을 차단하고, 짧은 grace로 종료
- 워커: 체크포인트/재시도 설계가 되어 있다면 짧게
파드가 Terminating에서 오래 끌리면 드레인/재스케줄이 더 꼬일 수 있습니다. 종료가 잘 안 끝나는 문제는 아래 글의 디버깅 흐름이 도움이 됩니다.
preStop + readiness 전환 예시
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: prod
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: nginx:1.25
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
failureThreshold: 1
preStop에서 짧게 대기해 커넥션 드레인을 유도하고, readiness가 빠르게 내려가도록 설정합니다.
“PDB + Spot + 우선순위” 조합에서 안전한 운영 체크리스트
아래 항목 중 2~3개만 어긋나도 eviction loop가 생길 수 있습니다.
1) 가용성 목표를 숫자로 고정
- 서비스별 최소 레플리카(예: 2/3/5)
- AZ 분산(Topology Spread/anti-affinity)
- PDB는 그 목표를 반영
2) PDB는 maxUnavailable 중심으로 단순화
minAvailable은 레플리카 변동(HPA)에서 직관이 깨질 때가 많음- 레플리카 2 이상이면
maxUnavailable: 1이 운영 난이도가 낮음
3) 우선순위는 “층”을 적게
prod-critical/default/batch-low정도로 3단이면 충분한 경우가 많음- 중요 서비스에만 높은 우선순위, 배치는
preemptionPolicy: Never고려
4) Spot은 격리하고, 중요한 건 온디맨드에 고정
- 테인트/톨러레이션으로 강제
- 중요한 서비스가 Spot으로 스케줄되는 “우연”을 제거
5) 리소스 requests를 현실적으로
- requests가 낮으면 한 노드에 과밀 배치 → 메모리 압박 eviction
- requests가 너무 높으면 스케줄 불가 → Pending 루프
마무리: 루프를 끊는 실전 접근 순서
eviction loop를 끊을 때는 “설정 하나”로 해결하려고 하기보다, 다음 순서가 빠릅니다.
- 이벤트로 eviction 유형 분류(자원/선점/종료)
- Spot 종료가 섞여 있으면 워크로드 격리(테인트/톨러레이션) 부터 적용
- PDB를
maxUnavailable중심으로 재설계(특히 레플리카 1 서비스는 재검토) - PriorityClass를 최소 계층으로 정리하고, 배치의 preemption을 제한
- 마지막으로 requests/limits와 노드 타입(메모리, 디스크)을 조정
이 흐름대로 정리하면 “Spot 비용 절감”과 “업그레이드/스케일링 안정성”을 동시에 얻으면서도, Evicted/Preempted 루프를 상당 부분 제거할 수 있습니다.