resources.requests / resources.limits를 명확히 정의해 스케줄링 기준을 제공합니다.requests 값이 노드 적합성 판단의 핵심 지표입니다.requiredDuringSchedulingIgnoredDuringExecution: 하드 필터preferredDuringSchedulingIgnoredDuringExecution: 선호(가중치 기반 점수)예시:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
# 데이터센터/랙 등 환경에 맞는 토폴로지 도메인 값
- key: topology.kubernetes.io/zone
operator: In
values: ["dc-a", "dc-b"]
# 워커 풀 구분(예: compute, storage 등)
- key: node-pool
operator: In
values: ["compute"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference:
matchExpressions:
# 고성능 디스크가 장착된 노드를 선호
- key: disktype
operator: In
values: ["nvme-ssd"]
topologyKey로 공존/분산을 제어합니다.예시(존 단위 분산 강제):
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: my-svc
topologyKey: topology.kubernetes.io/zone
effect: NoSchedule, PreferNoSchedule, NoExecuteoperator: Exists는 value 없이 key만 있어도 매칭됩니다.예시:
# 노드에 테인트 예시:
# kubectl taint nodes node1 accelerator=nvidia:NoSchedule
spec:
tolerations:
- key: "accelerator"
operator: "Equal" # Exists 사용 시 value 생략 가능
value: "nvidia"
effect: "NoSchedule"

QueueSort
파드 우선순위( PriorityClass ), 백오프 등을 반영한 큐에서 파드를 꺼냅니다.
PreFilter
필요한 리소스/제약을 사전 계산합니다(요청량, 토폴로지 스프레드, 어피니티 등 선검사).
Filter (구. Predicates)
조건을 만족하는 노드만 남깁니다.
PostFilter(필요 시 Preemption)
후보 노드가 0개면, 낮은 우선순위 파드를 축출해 공간을 만들 수 있는지 시도합니다(PDB 준수).
Score (구. Priorities)
남은 노드에 0~100 점수를 부여합니다(리소스 균형, 토폴로지 분산, 이미지 지역성 등).
Normalize & Select
점수를 정규화·정렬하고 최고 점수 노드를 선택합니다. (동점이면 무작위 선택)
Reserve
선택한 노드에 임시 예약해 경쟁 상태를 방지합니다.
Permit
외부 승인 훅(플러그인)이 있으면 허용/대기/거부를 결정할 수 있습니다.
PreBind
볼륨 바인딩(예: CSI, WFFC), 크리덴셜/오버헤드 반영 등 바인드 전 준비를 수행합니다.
Bind
파드의 spec.nodeName을 설정해 API 서버에 바인드합니다.
PostBind
후처리를 수행하고 사이클 종료. 이후 Kubelet이 해당 노드에서 컨테이너를 생성합니다.
apiVersion: descheduler/v1alpha1
kind: DeschedulerPolicy
strategies:
RemovePodsViolatingNodeAffinity:
enabled: true
params:
nodeAffinityType:
- requiredDuringSchedulingIgnoredDuringExecution
thresholds(이것보다 모든 지표가 낮으면 underutilized) / targetThresholds(이 중 하나라도 넘으면 overutilized). 지표는 cpu, memory, pods(확장 리소스도 가능)이며, 노드 Allocatable 대비 파드의 requests 비율로 계산requiredDuringSchedulingIgnoredDuringExecution을 만족하지 않거나, 이제는 preferred...를 더 잘 만족하는 노드가 생긴 경우 등스케줄러 설정에서 클러스터 기본 분산 규칙(예: Zone/Hostname 균등)을 지정하고, 워크로드는 필요한 곳만 오버라이드합니다.
namespaceSelector로 크로스 네임스페이스 매칭을 지원합니다.matchLabelKeys / mismatchLabelKeys로 버전 섞임 방지, 테넌트 분리 등 고급 제어가 가능합니다.affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: topology.kubernetes.io/zone
labelSelector:
matchLabels:
app: my-svc
matchLabelKeys: ["version"] # 현재 파드의 version 라벨 값을 따름
spec.schedulingGates로 의존 리소스가 준비될 때까지 스케줄 제외 상태로 두어, 불필요한 스케줄링/오토스케일 동작을 방지하고 자원 효율을 높입니다.
spec:
schedulingGates:
- name: wait.for.dependent.resource
GPU/가속기 등은 ResourceClaim/DeviceClass 기반(DRA)으로 요청(스토리지 PVC와 유사)하는 패턴 전환을 권장합니다.
apiVersion: resource.k8s.io/v1beta1
kind: ResourceClaimTemplate
metadata:
name: gpu-claim-tpl
spec:
spec:
devices:
requests:
- deviceClassName: nvidia-gpu
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dra-example
spec:
replicas: 1
selector:
matchLabels:
app: dra-example
template:
metadata:
labels:
app: dra-example
spec:
resourceClaims:
- name: gpu
source:
resourceClaimTemplateName: gpu-claim-tpl
containers:
- name: app
image: nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["bash","-c","nvidia-smi || sleep 3600"]
descheduler/v1alpha1 → descheduler/v1alpha2로 이전하고 최신 Helm 차트/프로필을 사용합니다.
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: default
plugins:
deschedule:
enabled:
- RemovePodsViolatingNodeAffinity
- RemovePodsViolatingInterPodAntiAffinity
percentageOfNodesToScore(노드 샘플링 비율)로 레이턴시를 제어합니다.NodeResourcesFit.RequestedToCapacityRatio로 빈패킹 vs. 여유 확보 전략을 수치화합니다.kube-reserved, system-reserved에 PID 예약을 포함하는 방안을 검토합니다.런타임 오버헤드는 스케줄링/할당에 포함됩니다. RuntimeClass 사용 시 오버헤드 누락으로 인한 과배치를 방지합니다.
노드 상태 기반 NoExecute 축출이 독립 컨트롤러로 동작합니다. 커스텀 컨트롤러/오퍼레이터는 이 구조를 전제로 설계합니다.
# 버전/노드 확인
k version
k get nodes -o wide 
k create ns team-a
k label ns team-a group=web-tenants --overwrite
k describe ns team-a 
W1=$(kubectl get nodes -o name | grep -E 'worker|control' | sed -n '1p' | cut -d/ -f2)
W2=$(kubectl get nodes -o name | grep -E 'worker|control' | sed -n '2p' | cut -d/ -f2)
k label node "$W1" topology.kubernetes.io/zone=dc-a node-pool=compute disktype=nvme-ssd --overwrite
k label node "$W2" topology.kubernetes.io/zone=dc-b node-pool=compute disktype=nvme-ssd --overwritek get nodes -o name | awk 'NR%2==1{print $0}' \
| xargs -I{} kubectl label {} topology.kubernetes.io/zone=zone-a --overwrite
k get nodes -o name | awk 'NR%2==0{print $0}' \
| xargs -I{} kubectl label {} topology.kubernetes.io/zone=zone-b --overwrite
k get nodes -L topology.kubernetes.io/zone,node-pool,disktype 
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: team-a
spec:
replicas: 3
selector:
matchLabels:
app: web
version: v2
template:
metadata:
labels:
app: web
version: v2
spec:
containers:
- name: app
image: nginx
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: topology.kubernetes.io/zone
matchLabelKeys:
- "version"
namespaceSelector:
matchLabels:
group: web-tenants
labelSelector:
matchLabels:
app: webk apply -f deploy-affinity.yaml
k get pods -n team-a -l app=web -o wide 
```bash
kubectl get pods -A -l app=web -o json \
| jq --argjson nodes "$(kubectl get nodes -o json)" \
--argjson nss "$(kubectl get ns -o json)" '
[ .items[]
| .metadata.namespace as $ns
| select(
($nss.items[] | select(.metadata.name==$ns)
| .metadata.labels["group"]) == "web-tenants"
)
| .spec.nodeName as $n
| {
namespace: $ns,
name: .metadata.name,
version: (.metadata.labels.version // "unknown"),
node: $n,
zone: ( ($nodes.items[] | select(.metadata.name==$n)
| .metadata.labels["topology.kubernetes.io/zone"]) // "unknown" )
}
]
| sort_by(.version, .zone, .namespace, .name)
'
``` 
kubectl kdelete deployments.apps web -n team-aapiVersion: v1
kind: Pod
metadata:
name: gated-pod
namespace: team-a
spec:
schedulingGates:
- name: wait.for.dependency
containers:
- name: app
image: busybox
command:
- sh
- -c
- echo ready && sleep 3600k apply -f pod-gated.yaml
k wait --for=condition=ready -n team-a pod/gated-pod --timeout=60s
k -n team-a get pod gated-pod -o wide 
kubectl -n team-a get pod gated-pod -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].reason}'; echo 
k patch pod gated-pod -n team-a --type=json -p='[{"op":"remove","path":"/spec/schedulingGates"}]' 
k -n team-a get pod gated-pod -o wide -w 
k -n team-a get events \
--field-selector involvedObject.kind=Pod,involvedObject.name=gated-pod -w 
kubectl delete pod gated-pod -n team-ahelm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
helm repo update
helm upgrade --install descheduler descheduler/descheduler \
-n kube-system --create-namespace \
--set schedule="*/30 * * * *"kubectl get pods -n kube-system 
descheduler-policy.yaml)# descheduler-policy.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: descheduler
namespace: kube-system
data:
policy.yaml: |
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: default
pluginConfig:
- name: DefaultEvictor
args:
nodeFit: true
- name: LowNodeUtilization
args:
thresholds: { pods: 16 } # <16% = 저활용 (13~15%도 잡힘)
targetThresholds: { pods: 19 } # >19% = 과밀 (20% 바로 해당)
- name: RemovePodsViolatingNodeAffinity
args:
nodeAffinityType:
- requiredDuringSchedulingIgnoredDuringExecution
plugins:
balance:
enabled: ["LowNodeUtilization"]
deschedule:
enabled: ["RemovePodsViolatingNodeAffinity"]
kubectl apply -f descheduler-policy.yamlkubectl -n kube-system get configmap descheduler -o jsonpath='{.data.policy\.yaml}{"\n"}' 
# 0번을 제외한 나머지 5개 워커 노드 cordon 설정
kubectl cordon k8s-worker-1 k8s-worker-2 k8s-worker-3 k8s-worker-4 k8s-worker-5kubectl get nodes 
kubectl create ns team-a 2>/dev/null || true
cat << 'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: fill
namespace: team-a
spec:
replicas: 60
selector: { matchLabels: { app: fill } }
template:
metadata: { labels: { app: fill } }
spec:
containers:
- name: c
image: nginx
EOF# 1번 확인 방법
kubectl -n team-a get pods -l app=fill -o wide
# 2번 확인 방법
kubectl -n team-a get pods -l app=fill -o custom-columns=NODE:.spec.nodeName --no-headers \
| sort | uniq -c 

kubectl uncordon k8s-worker-1 k8s-worker-2 k8s-worker-3 k8s-worker-4 k8s-worker-5
kubectl get nodes 
kubectl -n kube-system create job --from=cronjob/descheduler descheduler-manual-$(date +%s)kubectl -n kube-system logs -f $(
kubectl -n kube-system get pod -l app.kubernetes.io/name=descheduler \
--sort-by=.metadata.creationTimestamp -o name | tail -n1
)
kubectl get events.events.k8s.io -n team-a --sort-by=.eventTime \
| grep -E 'fill-|Killing|SuccessfulCreate' | tail -n 50 

kubectl -n team-a get pods -l app=fill -o custom-columns=NODE:.spec.nodeName --no-headers \
| sort | uniq -c 
kubectl delete deployments.apps fill -n team-a
kubectl delete configmap descheduler -n kube-systemtopologySpreadConstraints를 넣지 않아도,스케줄러의 기본 분산 규칙이 적용되어 존/노드에 고르게 퍼지는지 확인합니다kubectl create ns tps-demo# tps-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: tps-demo
spec:
replicas: 30
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
# 의도적으로 topologySpreadConstraints 미설정 (기본 분산이 적용되어야 함)
containers:
- name: pause
image: registry.k8s.io/pause:3.9
kubectl apply -f tps-deploy.yaml# 각 Pod가 어느 노드/존에 갔는지 보기
kubectl -n tps-demo get pod -o wide -L topology.kubernetes.io/zone
# 상세 정보
kubectl -n tps-demo get pod -o json \
| jq -r '.items[] | [.metadata.name, .spec.nodeName] | @tsv' \
| while IFS=$'\t' read -r pod node; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
printf "%-28s %-14s %s\n" "$pod" "$node" "$zone"
done
# 존별 파드 갯수
kubectl -n tps-demo get pod -o json \
| jq -r '.items[].spec.nodeName' \
| xargs -I{} kubectl get node {} -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}' \
| sort | uniq -c➜ 5 kubectl -n tps-demo get pod -o wide -L topology.kubernetes.io/zone
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES ZONE
web-cdb4bbd86-2x649 1/1 Running 0 3s 10.244.9.10 k8s-worker-5 <none> <none>
web-cdb4bbd86-58ghm 1/1 Running 0 3s 10.244.9.11 k8s-worker-5 <none> <none>
web-cdb4bbd86-6h255 1/1 Running 0 3s 10.244.5.12 k8s-worker-2 <none> <none>
web-cdb4bbd86-8t5hf 1/1 Running 0 3s 10.244.8.9 k8s-worker-4 <none> <none>
web-cdb4bbd86-9fc55 1/1 Running 0 3s 10.244.4.13 k8s-worker-1 <none> <none>
web-cdb4bbd86-9hxw2 1/1 Running 0 3s 10.244.7.9 k8s-worker-3 <none> <none>
web-cdb4bbd86-blqlf 1/1 Running 0 3s 10.244.5.9 k8s-worker-2 <none> <none>
web-cdb4bbd86-bn6zh 1/1 Running 0 3s 10.244.8.12 k8s-worker-4 <none> <none>
web-cdb4bbd86-ccff2 1/1 Running 0 3s 10.244.4.10 k8s-worker-1 <none> <none>
web-cdb4bbd86-clk7f 1/1 Running 0 3s 10.244.7.10 k8s-worker-3 <none> <none>
web-cdb4bbd86-d78gm 1/1 Running 0 3s 10.244.5.11 k8s-worker-2 <none> <none>
web-cdb4bbd86-dndn4 1/1 Running 0 3s 10.244.4.12 k8s-worker-1 <none> <none>
web-cdb4bbd86-g8wcn 1/1 Running 0 3s 10.244.8.10 k8s-worker-4 <none> <none>
web-cdb4bbd86-gtjzz 1/1 Running 0 3s 10.244.7.8 k8s-worker-3 <none> <none>
web-cdb4bbd86-nn7kv 1/1 Running 0 3s 10.244.9.8 k8s-worker-5 <none> <none>
web-cdb4bbd86-p2czr 1/1 Running 0 3s 10.244.3.12 k8s-worker-0 <none> <none>
web-cdb4bbd86-p72m2 1/1 Running 0 3s 10.244.9.9 k8s-worker-5 <none> <none>
web-cdb4bbd86-pgqhl 1/1 Running 0 3s 10.244.3.9 k8s-worker-0 <none> <none>
web-cdb4bbd86-psn2q 1/1 Running 0 3s 10.244.3.11 k8s-worker-0 <none> <none>
web-cdb4bbd86-rgmmf 1/1 Running 0 3s 10.244.3.10 k8s-worker-0 <none> <none>
web-cdb4bbd86-rqrtk 1/1 Running 0 3s 10.244.8.11 k8s-worker-4 <none> <none>
web-cdb4bbd86-sb8wn 1/1 Running 0 3s 10.244.3.13 k8s-worker-0 <none> <none>
web-cdb4bbd86-sr6z4 1/1 Running 0 3s 10.244.9.7 k8s-worker-5 <none> <none>
web-cdb4bbd86-srdm2 1/1 Running 0 3s 10.244.7.11 k8s-worker-3 <none> <none>
web-cdb4bbd86-swl7c 1/1 Running 0 3s 10.244.5.8 k8s-worker-2 <none> <none>
web-cdb4bbd86-t87vj 1/1 Running 0 3s 10.244.5.10 k8s-worker-2 <none> <none>
web-cdb4bbd86-v4kl5 1/1 Running 0 3s 10.244.7.12 k8s-worker-3 <none> <none>
web-cdb4bbd86-w59gh 1/1 Running 0 3s 10.244.8.13 k8s-worker-4 <none> <none>
web-cdb4bbd86-wgn77 1/1 Running 0 3s 10.244.4.11 k8s-worker-1 <none> <none>
web-cdb4bbd86-wtxhh 1/1 Running 0 3s 10.244.4.9 k8s-worker-1 <none> <none>
➜ 5åå➜ 5 kubectl -n tps-demo get pod -o json \
| jq -r '.items[] | [.metadata.name, .spec.nodeName] | @tsv' \
| while IFS=$'\t' read -r pod node; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
printf "%-28s %-14s %s\n" "$pod" "$node" "$zone"
done
web-cdb4bbd86-2x649 k8s-worker-5 zone-a
web-cdb4bbd86-58ghm k8s-worker-5 zone-a
web-cdb4bbd86-6h255 k8s-worker-2 zone-b
web-cdb4bbd86-8t5hf k8s-worker-4 zone-b
web-cdb4bbd86-9fc55 k8s-worker-1 zone-a
web-cdb4bbd86-9hxw2 k8s-worker-3 zone-a
web-cdb4bbd86-blqlf k8s-worker-2 zone-b
web-cdb4bbd86-bn6zh k8s-worker-4 zone-b
web-cdb4bbd86-ccff2 k8s-worker-1 zone-a
web-cdb4bbd86-clk7f k8s-worker-3 zone-a
web-cdb4bbd86-d78gm k8s-worker-2 zone-b
web-cdb4bbd86-dndn4 k8s-worker-1 zone-a
web-cdb4bbd86-g8wcn k8s-worker-4 zone-b
web-cdb4bbd86-gtjzz k8s-worker-3 zone-a
web-cdb4bbd86-nn7kv k8s-worker-5 zone-a
web-cdb4bbd86-p2czr k8s-worker-0 zone-b
web-cdb4bbd86-p72m2 k8s-worker-5 zone-a
web-cdb4bbd86-pgqhl k8s-worker-0 zone-b
web-cdb4bbd86-psn2q k8s-worker-0 zone-b
web-cdb4bbd86-rgmmf k8s-worker-0 zone-b
web-cdb4bbd86-rqrtk k8s-worker-4 zone-b
web-cdb4bbd86-sb8wn k8s-worker-0 zone-b
web-cdb4bbd86-sr6z4 k8s-worker-5 zone-a
web-cdb4bbd86-srdm2 k8s-worker-3 zone-a
web-cdb4bbd86-swl7c k8s-worker-2 zone-b
web-cdb4bbd86-t87vj k8s-worker-2 zone-b
web-cdb4bbd86-v4kl5 k8s-worker-3 zone-a
web-cdb4bbd86-w59gh k8s-worker-4 zone-b
web-cdb4bbd86-wgn77 k8s-worker-1 zone-a
web-cdb4bbd86-wtxhh k8s-worker-1 zone-akubectl -n tps-demo get pod -o json \
| jq -r '.items[].spec.nodeName' \
| xargs -I{} kubectl get node {} -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}' \
| sort | uniq -c
15 zone-a
15 zone-bk delete deploy web -n tps-demo # 30개 naked pod 생성
for i in $(seq 1 30); do
cat <<'YAML' | sed "s/NAME/p${i}/" | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: NAME
namespace: tps-demo
labels: { app: naked }
spec:
containers: [{ name: c, image: registry.k8s.io/pause:3.9 }]
YAML
done
# 각 Pod가 어느 노드/존에 갔는지 보기
kubectl -n tps-demo get pod -o wide -L topology.kubernetes.io/zone
# 상세 정보
kubectl -n tps-demo get pod -o json \
| jq -r '.items[] | [.metadata.name, .spec.nodeName] | @tsv' \
| while IFS=$'\t' read -r pod node; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
printf "%-28s %-14s %s\n" "$pod" "$node" "$zone"
done
# 존별 파드 갯수
kubectl -n tps-demo get pod -o json \
| jq -r '.items[].spec.nodeName' \
| xargs -I{} kubectl get node {} -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}' \
| sort | uniq -c
p1 k8s-worker-2 zone-b
p10 k8s-worker-3 zone-a
p11 k8s-worker-5 zone-a
p12 k8s-worker-2 zone-b
p13 k8s-worker-1 zone-a
p14 k8s-worker-5 zone-a
p15 k8s-worker-2 zone-b
p16 k8s-worker-3 zone-a
p17 k8s-worker-0 zone-b
p18 k8s-worker-4 zone-b
p19 k8s-worker-2 zone-b
p2 k8s-worker-0 zone-b
p20 k8s-worker-3 zone-a
p21 k8s-worker-4 zone-b
p22 k8s-worker-5 zone-a
p23 k8s-worker-1 zone-a
p24 k8s-worker-0 zone-b
p25 k8s-worker-0 zone-b
p26 k8s-worker-1 zone-a
p27 k8s-worker-3 zone-a
p28 k8s-worker-2 zone-b
p29 k8s-worker-4 zone-b
p3 k8s-worker-3 zone-a
p30 k8s-worker-5 zone-a
p4 k8s-worker-4 zone-b
p5 k8s-worker-5 zone-a
p6 k8s-worker-1 zone-a
p7 k8s-worker-4 zone-b
p8 k8s-worker-0 zone-b
p9 k8s-worker-1 zone-a
➜ 5 kubectl -n tps-demo get pod -o json \
| jq -r '.items[].spec.nodeName' \
| xargs -I{} kubectl get node {} -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}' \
| sort | uniq -c
15 zone-a
15 zone-bkubectl delete namespace tps-demov1alpha2). 기본 비활성이라 feature gate와 -runtime-config로 켜야 보입니다.v1beta1로 바뀌고 DeviceClass/devices: 문법 등장(기본은 꺼져 있고 명시 활성 필요).v1)로 승격.kubectl api-versions | grep resource.k8s.io
kubectl api-resources | egrep 'Resource(Class|ClaimTemplate|Claim|ClassParameters|Slice)'➜ 5 kubectl api-versions | grep resource.k8s.io
resource.k8s.io/v1
➜ 5 kubectl api-resources | egrep 'Resource(Class|ClaimTemplate|Claim|ClassParameters|Slice)'
resourceclaims resource.k8s.io/v1 true ResourceClaim
resourceclaimtemplates resource.k8s.io/v1 true ResourceClaimTemplate
resourceslices resource.k8s.io/v1 false ResourceSlice/etc/kubernetes/manifests/kube-apiserver.yaml 에 command args에 추가 적용- --feature-gates=DynamicResourceAllocation=true
- --runtime-config=resource.k8s.io/v1alpha2=true 
/etc/kubernetes/manifests/kube-scheduler.yaml/etc/kubernetes/manifests/kube-controller-manager.yaml- --feature-gates=DynamicResourceAllocation=true 
/var/lib/kubelet/config.yamlfeatureGates:
DynamicResourceAllocation: true
# 적용후 재시작
sudo systemctl daemon-reload
sudo systemctl restart kubelet 
kubectl api-versions | grep resource.k8s.io
kubectl api-resources | egrep 'Resource(Class|ClaimTemplate|Claim|ClassParameters|Slice)' 
# 네임스페이스 생성
kubectl create namespace dra-tutorial
# (v1.34) DeviceClass 생성
cat <<'YAML' | kubectl apply -f -
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: gpu.example.com
spec:
selectors:
- cel:
# 드라이버가 광고한 디바이스만 매칭(예: 예제 드라이버)
expression: "device.driver == 'gpu.example.com'"
YAML
# 서비스어카운트 / 클러스터롤 / 바인딩 / 우선순위클래스
kubectl apply --server-side -f https://k8s.io/examples/dra/driver-install/serviceaccount.yaml
kubectl apply --server-side -f https://k8s.io/examples/dra/driver-install/clusterrole.yaml
kubectl apply --server-side -f https://k8s.io/examples/dra/driver-install/clusterrolebinding.yaml
kubectl apply --server-side -f https://k8s.io/examples/dra/driver-install/priorityclass.yaml
# 예제 DRA 드라이버(모의 GPU) DaemonSet 배포
kubectl apply --server-side -f https://k8s.io/examples/dra/driver-install/daemonset.yaml
# 확인
kubectl -n dra-tutorial get pod -l app.kubernetes.io/name=dra-example-driver
여기서 막힘apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClass
metadata:
name: nvidia-gpu
driverName: gpu.example.com# claim-template.yaml (v1.34)
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: gpu-claim-tpl
namespace: team-a
spec:
spec:
devices:
requests:
- name: gpu-claim
exactly:
deviceClassName: gpu.example.com
# 선택: 드라이버가 노출한 속성으로 필터링
selectors:
- cel:
# 예제: 10Gi 이상 메모리를 가진 모의 GPU
expression: "device.capacity['gpu.example.com'].memory.compareTo(quantity('10Gi')) >= 0"
# deploy-uses-dra.yaml (v1.34)
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-app
namespace: team-a
spec:
replicas: 1
selector:
matchLabels: { app: gpu-app }
template:
metadata:
labels: { app: gpu-app }
spec:
# 1) Pod 레벨에서 Claim(템플릿/기존 Claim) 제공
resourceClaims:
- name: my-gpu
resourceClaimTemplateName: gpu-claim-tpl
containers:
- name: app
image: nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["bash","-lc"]
args: ["nvidia-smi || (echo 'no driver'; sleep 3600)"]
resources:
requests:
cpu: "1"
memory: "1Gi"
# 2) 컨테이너 레벨에서 해당 Claim을 명시적으로 참조
claims:
- name: my-gpu
# 리소스 적용
kubectl apply -f claim-template.yaml
kubectl apply -f deploy-uses-dra.yaml# 1) DeviceClass/ResourceSlice 가용 여부
kubectl get deviceclasses
kubectl get resourcesliceskubectl -n team-a get pods -l app=gpu-app
kubectl -n team-a get resourceclaim
kubectl -n team-a describe resourceclaim $(kubectl -n team-a get rc -o name)kubectl -n team-a describe pod -l app=gpu-app | grep -E "ResourceClaim|Allocated|Warning|Failed|device" -nkubectl delete deploy gpu-app -n team-a
kubectl delete resourceclaimtemplates.resource.k8s.io gpu-claim-tpl -n team-a
kubectl delete deviceclass gpu.example.com
# 예제 드라이버 정리
kubectl delete -n dra-tutorial daemonset dra-example-driver-kubeletplugin
kubectl delete namespace dra-tutorial
kubectl delete clusterrole dra-example-driver-role
kubectl delete clusterrolebinding dra-example-driver-role-binding
kubectl delete priorityclass dra-driver-high-prioritypercentageOfNodesToScore): https://kubernetes.io/docs/concepts/scheduling-eviction/scheduler-perf-tuning/namespaceSelector KEP: https://github.com/kubernetes/enhancements/blob/master/keps/sig-scheduling/2249-pod-affinity-namespace-selector/README.mdmatchLabelKeys/mismatchLabelKeys 소개: https://kubernetes.io/blog/2024/08/16/matchlabelkeys-podaffinity/안녕하세요~ 오늘 작성해주신 블로그 실습글 너무 좋았습니다.
Q1. 실습에서 Pod Anti-Affinity와 Node Affinity를 동시에 사용했는데, 이 두 설정이 충돌하는 경우도 있을것 같은데, 적용 원칙같은게 있을까요?
Q2. 실습에서 schedulingGates를 사용해 파드 스케줄링을 지연시켰는데, 실제 운영 환경에서 어떤 상황에 유용한가요?
추가적으로 문의 주신 내용에 대해서 답변을 드리겠습니다.
우선 제가 직접 운영해본적이 없어.. 주변 동료분들과 GPT를 이용하여 정리하였습니다.
분명 Descheduler를 잘못 사용하게 되면 문제가 발생합니다. 위험 요소는 다음과 같습니다.
그럼에도 불구하고 다음과 같은 경우에는 사용하는것이 권장 됩니다.
RemoveDuplicates).Low/HighNodeUtilization).다음, DeScheduler를 Cronjob을 이용하여 정기적으로 실행하는 경우와, 일시적으로 실행하는 경우는 다음과 같이 분류를 할수 있습니다.
PodLifeTime 등(상태성/로컬스토리지는 제외).[정책/옵션 가드레일]
DefaultEvictor.nodeFit: true → 수용 가능한 노드가 있을 때만 축출.maxNoOfPodsToEvictPerNode / PerNamespace / Total은 보수적으로 시작.evictDaemonSetPods=false, evictLocalStoragePods=false, evictSystemCriticalPods=false 유지.maxUnavailable/minAvailable 점검).namespaces.include/exclude, labelSelector, nodeSelector로 대상 축소(카나리 적용).-v=4 이상으로 사유 로깅.[메트릭/관측 (Prometheus 권장 지표)]
:10258/metrics):pods_evicted_total: 축출 수(증가율 알람 권장).loop_duration_seconds: 사이클 소요 시간(p95 이상 증가 시 알람).strategy_duration_seconds: 전략별 소요.scheduler_pending_pods: 스케줄 대기 누적.scheduler_schedule_attempts_total: 스케줄 시도 추이.scheduler_preemption_attempts_total/_victims: 프리엠션 신호.rate(pods_evicted_total[5m]) 급증 & scheduler_pending_pods 동반 상승 → 배포/리밸런스 충돌 가능.loop_duration_seconds{quantile="0.95"} 상승 → 정책 과도·대상 과대 가능.nodeFit:true + 축출 한도 보수.pods_evicted_total/loop_duration_seconds + 스케줄러 지표로 충돌 신호 감시.taint 적용 → GPU 파드만 tolerations로 통과resources.limits.nvidia.com/gpu](<N> 선언nodeSelector/affinity로 타겟팅(A100/H100 등)topologySpreadConstraints로 분산 보조single-numa-node) + Memory Manager로 메모리/장치 지역성 맞추기
안녕하세요. 작성해주신 글 잘 읽었습니다. 중간에 스케줄러 프로파일 & 성능 튜닝 설명해주신 부분에서 프로파일 다중화에 대해 언급해주신 부분을 보았습니다. 혹시 해당 부분은 ConfigMap을 통해서 Scheduler의 설정을 변경해주는 것을 의미하는 것인지, 아니면 다른 방식이 더 있는 것인지요?