클러스터 자원이 부족할 때 모든 Pod를 같은 순서로 실행해야 할까? 보통은 아니다. 결제 요청을 처리하는 API와 나중에 실행해도 되는 리포트 Job이 CPU와 메모리를 두고 경쟁한다면, 결제 API를 먼저 실행해야 한다.
Kubernetes에서 이 의도를 표현하는 도구가 PriorityClass다. 다만 여기서 꼭 구분해야 할 것이 있다.
| 개념 | 하는 일 |
|---|---|
| Pod priority | 아직 배치되지 않은 Pod의 대기열 순서를 바꾼다. |
| preemption | 높은 우선순위 Pod를 배치할 자리가 없을 때, 낮은 우선순위 Pod를 내보내 자리를 만들 수 있다. |
PriorityClass를 설정했다고 항상 기존 Pod가 종료되는 것은 아니다. 먼저 대기열 순서가 달라지고, 조건을 만족하는 Node가 전혀 없을 때에만 preemption이 검토된다.
PriorityClass로 중요도 정하기
PriorityClass는 namespace에 속하지 않는 리소스다. 이름과 정수 값의 매핑을 한 번 정의하고, Pod가 priorityClassName으로 참조한다. 값이 클수록 우선순위가 높다. 이는 Pod Priority and Preemption 공식 문서에 명시되어 있다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: business-critical
value: 100000
globalDefault: false
description: "User-facing checkout and payment APIs only"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch
value: 1000
globalDefault: false
description: "Interruptible batch workloads"
이 값은 서비스의 절대적인 등급표가 아니다. 같은 클러스터에서 어떤 워크로드가 다른 워크로드보다 먼저 자원을 받아야 하는지에 대한 운영 약속이다. 그래서 999999999처럼 큰 값을 각 팀이 임의로 쓰기 시작하면 우선순위 체계가 금방 무너진다. 보통은 플랫폼 팀이 몇 단계만 정의하고, 각 단계의 사용 범위를 문서화하는 편이 낫다.
API를 배치 Job보다 먼저 실행하기
평소에는 report-job이 유휴 자원을 활용해 실행된다. 그런데 트래픽 장애로 checkout-api의 replica를 빠르게 늘려야 하는 상황을 생각해 보자.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
spec:
replicas: 4
selector:
matchLabels:
app.kubernetes.io/name: checkout-api
template:
metadata:
labels:
app.kubernetes.io/name: checkout-api
spec:
priorityClassName: business-critical
containers:
- name: api
image: example.registry/checkout-api:1.0
resources:
requests:
cpu: "1"
memory: 1Gi
apiVersion: batch/v1
kind: Job
metadata:
name: nightly-report
spec:
template:
spec:
priorityClassName: batch
restartPolicy: Never
containers:
- name: report
image: example.registry/report-worker:1.0
resources:
requests:
cpu: "1"
memory: 1Gi
새 checkout-api Pod가 Pending 상태라면 scheduler는 우선 이 Pod를 batch Pod보다 먼저 처리한다. 다른 Node에 여유가 있으면 기존 Pod에는 영향 없이 그냥 배치된다. 여유가 전혀 없다면 scheduler는 낮은 우선순위 Pod 일부를 제거하면 API Pod가 들어갈 수 있는 Node가 있는지 찾아본다.
이 과정이 preemption이다. 자리를 내주는 Pod(victim Pod)는 종료 준비 시간을 받을 수 있다. 그래서 preemption 이벤트가 생겨도 새 API Pod가 바로 Running 상태가 되지 않을 수 있다.
preemption으로 해결할 수 없는 문제
preemption은 높은 우선순위의 Pending Pod가 있고, 낮은 우선순위 Pod를 제거하면 특정 Node에 배치할 수 있을 때만 가능하다. 즉 아래와 같은 문제는 preemption으로 해결되지 않을 수 있다.
- 높은 우선순위 Pod의 request 자체가 어느 Node 용량보다 크다.
- required node affinity나 taint/toleration 조건을 만족하는 Node가 없다.
- 낮은 우선순위 Pod를 모두 제거해도 anti-affinity 조건 때문에 배치할 수 없다.
- 다른 Node의 Pod를 함께 비워야만 조건이 풀리는 cross-node preemption 상황이다. 기본 scheduler는 이를 수행하지 않는다.
특히 Pod가 자신보다 낮은 우선순위 Pod에 pod affinity를 걸고 있으면, 그 Pod를 제거해서 자리를 만드는 순간 affinity가 깨진다. Kubernetes 공식 문서도 이 경우에는 낮은 우선순위 Pod가 아니라 같거나 더 높은 우선순위 Pod를 대상으로 affinity를 두는 방식을 권장한다.
기존 Pod를 종료하지 않고 순서만 앞당기기
모든 높은 우선순위 작업이 다른 Pod를 내보내도 되는 것은 아니다. 데이터 분석 Job처럼 “다른 대기 Job보다 앞서 실행하되, 이미 돌아가는 Job을 끊고 싶지는 않다”는 요구도 있다.
preemptionPolicy: Never를 둔 PriorityClass가 여기에 맞는다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: analyst-queue
value: 10000
preemptionPolicy: Never
globalDefault: false
description: "Queue ahead of batch jobs, but never interrupt running Pods"
이 class를 사용하는 Pod는 낮은 우선순위 Pod보다 대기열에서 먼저 시도된다. 하지만 자리가 없으면 기존 Pod를 축출하지 않고 충분한 자원이 자연스럽게 생길 때까지 기다린다. 반대로 자신보다 더 높은 우선순위 Pod에게 preemption될 수는 있다.
PDB가 있으면 Pod를 지킬 수 있을까?
PodDisruptionBudget(PDB)는 관리자가 계획한 중단 상황에서 동시에 멈춰도 되는 복제본 수를 제한한다. preemption에서도 scheduler는 가능하면 PDB를 지키는 Pod를 먼저 고른다. 하지만 반드시 지켜지는 것은 아니다. 높은 우선순위 Pod를 배치할 다른 방법이 없으면 PDB를 위반하고 낮은 우선순위 Pod를 제거할 수 있다.
따라서 PDB는 preemption을 막는 방화벽이 아니다. 중요한 서비스라면 PDB와 함께 다음도 확인해야 한다.
| 확인할 것 | 이유 |
|---|---|
| PriorityClass를 쓸 수 있는 namespace와 워크로드 | 모든 팀이 높은 priority를 쓰는 것을 막기 위해 |
| low-priority Job의 재시도 방식과 checkpoint | 축출돼도 복구 가능한지 확인하기 위해 |
terminationGracePeriodSeconds | victim이 너무 오래 자리를 잡지 않도록 하기 위해 |
| resource request 크기 | API Pod 하나를 위해 실제로 얼마나 비워야 하는지 알기 위해 |
이벤트로 동작 확인하기
높은 우선순위 Pod가 안 올라오면 아래 순서로 확인한다.
kubectl describe pod <pending-pod>
kubectl get pod <pending-pod> -o jsonpath='{.status.nominatedNodeName}{"\n"}'
kubectl get events --sort-by=.lastTimestamp
preemption이 일어난 경우 nominatedNodeName은 scheduler가 victim을 비워 두려고 잡아 둔 Node를 보여 줄 수 있다. 다만 그것이 최종 배치 Node라는 보장은 없다. victim이 종료되는 사이 더 높은 우선순위 Pod가 오거나 다른 Node가 비면 scheduler가 다른 결정을 할 수 있다.
핵심 정리
PriorityClass는 “이 Pod가 중요하다”는 표식이 아니라, 자원이 부족할 때 어떤 약속을 지킬지 결정하는 운영 정책이다.
- priority는 Pending Pod의 대기열 순서를 정한다.
- preemption은 낮은 우선순위 Pod를 제거해 자리를 만들 수 있지만, 모든 스케줄링 제약을 해결하지는 못한다.
preemptionPolicy: Never는 기존 작업을 끊지 않으면서 대기열 우선순위만 높이고 싶을 때 쓴다.- scheduler는 preemption 중에 가능한 한 PDB를 지키지만 이를 보장하지는 않는다. 중요한 서비스 보호를 PDB 하나에만 맡기면 안 된다.
이로써 Pod 배치를 다룰 때 자주 만나는 선택, 분산, 자원 예산, 우선순위를 한 바퀴 살펴봤다. 실제 운영에서는 한 설정만 따로 두기보다 workload의 중요도와 장애 경계, request를 함께 설계하는 것이 중요하다.