8.5 eviction과 우선순위
Pod가 자리를 잃는 경로는 하나가 아니다. kubelet이 노드 자원을 지키려고 실행하는 node-pressure eviction, drain처럼 Eviction API를 통해 요청되는 축출, 스케줄러가 높은 우선순위 Pod의 자리를 만들려고 실행하는 preemption이 각각 다른 규칙으로 움직인다. 여기에 Kubernetes 바깥, 커널의 OOM killer가 한 층 더 아래에서 동작한다. 장애 상황에서 “누가 이 Pod를 종료시켰나"를 추적하려면 이 층들을 구분해야 한다.
node-pressure eviction, kubelet의 자원 방어
kubelet은 노드의 자원 신호를 주기적으로 검사한다. 감시 대상은 memory.available, nodefs.available, imagefs.available, pid.available 같은 신호이고, 신호마다 eviction threshold가 걸려 있다. Linux 기본값 기준으로 hard threshold는 memory.available<100Mi, nodefs.available<10%, imagefs.available<15%, nodefs.inodesFree<5%다. hard threshold에 도달하면 kubelet은 유예 없이 즉시 Pod 회수를 시작하고, 별도로 설정하는 soft threshold는 지정한 유예 기간 동안 초과 상태가 지속될 때만 동작한다.
회수 대상의 순서는 세 기준으로 정해진다. 먼저 실사용량이 requests를 초과한 Pod가 초과하지 않은 Pod보다 앞에 서고, 그다음 Pod priority가 낮은 쪽이 앞에 서며, 마지막으로 requests 대비 초과 폭이 큰 쪽이 앞에 선다. 8.2의 QoS와 연결하면 requests가 0인 BestEffort는 언제나 첫 번째 기준에 걸리므로 사실상 가장 먼저 회수되고, requests 안쪽에서 동작하는 Guaranteed와 Burstable은 마지막까지 남는다.
축출된 Pod는 Failed 상태에 reason Evicted로 남는다. Deployment 같은 controller가 관리하는 Pod라면 대체 Pod가 만들어져 다른 노드로 스케줄링되고, 압박이 감지된 노드에는 pressure condition과 함께 8.4에서 본 memory-pressure, disk-pressure taint가 붙어 새 Pod의 유입을 막는다.
API-initiated eviction과 PodDisruptionBudget
두 번째 경로는 노드에 문제가 없는데도 Pod를 비워야 하는 상황이다. 커널 업그레이드나 노드 교체 전에 실행하는 kubectl drain이 대표적이다. drain은 노드를 cordon해 새 배치를 막은 뒤, 그 노드의 Pod들에 대해 Eviction API를 호출한다. 단순 삭제와의 차이가 이 API에 있다. Eviction API는 요청을 실행하기 전에 PodDisruptionBudget(PDB)을 검사하고, 축출이 PDB를 위반하게 되면 요청을 거부한다. drain은 거부된 Pod에 대해 재시도를 반복하고, 그 사이 다른 노드에서 replica가 회복되면 진행이 이어진다.
PDB는 자발적 중단(voluntary disruption) 중에 유지해야 하는 가용 replica를 선언하는 오브젝트다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: webminAvailable 대신 maxUnavailable로 선언해도 된다. replica 3에 minAvailable: 2면 drain이 동시에 비우는 web Pod는 1개로 제한되어, 여러 노드를 순차로 비우는 작업 중에도 서비스가 최소 정족수를 유지한다.
PriorityClass와 preemption
Pod priority는 두 군데서 쓰인다. 8.1에서 본 스케줄링 큐의 정렬 순서, 그리고 자원이 부족할 때 누구를 내릴지의 판단이다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: db-critical
value: 100000
globalDefault: false
description: "DB 워크로드. 자원 부족 시 마지막까지 유지"Pod에는 spec.priorityClassName: db-critical로 연결한다. filtering에서 후보 노드가 전무하면 스케줄러의 PostFilter 단계가 preemption을 시도한다. 어느 노드에서 이 Pod보다 priority가 낮은 Pod들을 제거하면 자리가 나는지 시뮬레이션하고, 자리가 나는 노드가 있으면 희생 Pod들을 graceful termination으로 종료시킨 뒤 그 노드를 새 Pod의 nominatedNodeName에 기록한다. 낮은 priority Pod를 전부 제거해도 자리가 나지 않는 노드에서는 preemption이 일어나지 않는다. 공식 문서는 preemption이 희생자를 고를 때 PDB를 존중하려 시도하지만 보장하지는 않는다고 명시한다.
배치 순서는 우대받되 남을 쫓아내고 싶지는 않은 워크로드에는 PriorityClass에 preemptionPolicy: Never를 지정한다. 이 Pod는 큐에서 앞서는 이점만 가진 채 자리가 나기를 기다린다. 반대로 priority 값을 남발해 모든 워크로드가 높은 값을 가지면 순서가 다시 평평해져 아무것도 우대받지 못하므로, 값의 층은 소수로 유지한다.
kubelet eviction과 커널 OOM killer의 층위
메모리 부족의 마지막 층은 Kubernetes가 아니라 커널이다. 8.2에서 본 대로 컨테이너 사용량이 자기 memory.max에 닿으면 커널 OOM killer가 프로세스를 종료하고, 노드 전체 메모리가 고갈되면 커널이 노드 수준에서 희생 프로세스를 고른다. kubelet의 eviction은 threshold 기반의 예방적 동작이라, 감시 주기 사이에 발생하는 급격한 메모리 스파이크는 따라잡지 못하고 커널이 먼저 움직이게 된다. kubelet이 QoS에 따라 컨테이너에 설정하는 oom_score_adj는 이 커널 층의 선택에 QoS 순서를 전달하는 장치다.
flowchart TD
A["노드 메모리 압박"] --> B{"어느 층이 먼저 반응?"}
B -->|"threshold 도달"| C["kubelet eviction<br/>Pod 단위 종료"]
B -->|"스파이크가 더 빠름"| D["커널 OOM killer<br/>프로세스 종료"]
C --> E["대체 Pod가<br/>다른 노드로"]
D --> F["OOMKilled 기록<br/>같은 노드 재시작"]
두 층은 결과의 형태도 다르다. eviction은 Pod 단위 사건이라 Pod 상태가 Evicted로 남고 대체 Pod는 다른 노드로 간다. OOM kill은 컨테이너 안 프로세스 단위 사건이라 컨테이너가 OOMKilled로 기록되고, restartPolicy에 따라 같은 노드, 같은 Pod 안에서 재시작된다. 사후 추적에서는 kubectl get pod의 상태와 kubectl describe pod의 Last State를 대조하면 어느 층이 개입했는지 구분된다.
정리
- 자리를 잃는 경로는 셋이다. node-pressure eviction은 requests 초과 여부, priority, 초과 폭 순으로 회수하고, drain의 Eviction API는 PDB의 통제를 받으며, preemption은 높은 priority Pod의 자리를 만든다.
- PDB는 자발적 중단만 지킨다. 노드 장애와 자원 압박에 의한 손실은 분산 선언이 담당한다.
- eviction은 Pod를 다른 노드로 보내는 사건이고 OOM kill은 같은 자리에서 재시작하는 사건이라, 남는 상태(
Evicted와OOMKilled)로 층을 역추적한다.