새 노드에 파드를 올리거나 매니페스트를 손본 직후, 파드가 Pending에서 한참을 안 넘어가는 상황은 운영하다 보면 주기적으로 만난다. 급하면 노드부터 하나 더 붙이고 싶어지는데, 원인을 안 보고 노드를 늘렸다가 문제가 taint나 PVC 쪽이라 그대로였던 적이 있다. 그래서 파드가 안 뜰 때는 손보다 눈이 먼저 가야 한다.
결론부터 말하자면, kubectl describe pod를 실행해 맨 아래 Events의 FailedScheduling 한 줄을 읽으면 원인 대부분이 갈린다. 스케줄러가 왜 노드를 못 골랐는지를 그 메시지에 그대로 적어주기 때문이다. 리소스 부족, taint 불일치, nodeAffinity 불일치, PVC 바인딩 실패가 서로 다른 문구로 나오므로, 문구만 매칭하면 5분 안에 원인을 특정할 수 있다.
1. Pending의 의미와 스케줄러의 역할
파드가 Pending이라는 건 아직 어느 노드에도 배치되지 않았다는 뜻이다. 쿠버네티스에는 kube-scheduler라는 컴포넌트가 있는데, 새로 생긴 파드를 보고 클러스터의 모든 노드를 훑어 조건에 맞는 노드를 하나 고르는 일을 한다. 이 배치에 성공해야 파드가 노드에 붙고, kubelet이 컨테이너를 띄우기 시작한다.
스케줄러가 어느 노드도 고르지 못하면 파드는 계속 Pending에 남고, 그때마다 파드에 FailedScheduling 이벤트를 남긴다. 이 이벤트는 노드 몇 개 중 몇 개가 왜 탈락했는지를 짧게 요약한다. 예를 들어 노드 3개짜리 클러스터에서 CPU가 부족하면 0/3 nodes are available: 3 Insufficient cpu 같은 식이다.
그러니 진단의 출발점은 항상 이 이벤트다. 로그를 뒤지거나 노드에 SSH로 들어가기 전에, 파드 이벤트부터 읽는다.
2. 먼저 이벤트를 읽는다
파드 상태를 확인하고, 해당 파드를 describe한다.
$ kubectl get pod web-0 NAME READY STATUS RESTARTS AGE web-0 0/1 Pending 0 2m14s $ kubectl describe pod web-0
출력이 길지만 볼 곳은 맨 아래 Events 한 곳이다.
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m default-scheduler 0/3 nodes are available: 3 Insufficient cpu.
preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.
메시지가 비어 있고 파드가 방금 만들어졌다면 스케줄러가 아직 돌기 전일 수 있으니 몇 초 뒤 다시 본다. 이벤트가 안 보이면 이벤트 보존 시간(기본 1시간)이 지났을 수 있는데, 그럴 땐 kubectl get events --sort-by=.lastTimestamp로 네임스페이스 이벤트를 다시 확인한다.
아래에서는 이 FailedScheduling 문구별로 원인과 확인 명령, 해결을 차례로 살펴본다.
3. Insufficient cpu / Insufficient memory - 리소스 부족
메시지에 Insufficient cpu 또는 Insufficient memory가 있으면, 파드가 요청한 리소스(requests)를 담을 여유가 있는 노드가 없다는 뜻이다. 여기서 기준은 노드에 실제로 남은 여유가 아니라, 이미 배치된 파드들의 requests 합계다. 실제 사용량이 낮아도 requests가 다 예약돼 있으면 탈락한다.
확인은 노드의 할당 현황을 보는 것이 정확하다.
$ kubectl describe node ip-10-0-1-23 ... Allocatable: cpu: 1930m memory: 3420Mi ... Allocated resources: Resource Requests Limits -------- -------- ------ cpu 1800m (93%) 2 (103%) memory 2900Mi (84%) 3Gi (89%)
여기서 Allocatable cpu가 1930m인데 이미 1800m이 requests로 예약돼 있다. 내 파드가 requests.cpu: 500m을 달라고 하면 130m밖에 안 남았으니 들어갈 자리가 없다. kubectl top nodes로 실제 사용률을 같이 보면 예약과 실사용의 차이가 눈에 들어온다.
해결은 세 갈래다. 파드의 requests를 실제 필요에 맞게 낮추거나, 놀고 있는 파드를 정리하거나, 노드를 늘린다. 오토스케일러를 쓰는 클러스터라면 requests가 과하게 잡혀 있어 스케일아웃이 계속 트리거되는 경우도 흔하니, 노드부터 늘리기 전에 requests가 현실적인지부터 본다.
4. untolerated taint - taint/toleration 불일치
taint는 노드에 "이런 파드는 받지 않겠다"고 붙이는 표식이고, toleration은 파드 쪽에서 "그 표식을 견딜 수 있다"고 선언하는 짝이다. 전용 노드(GPU 노드, 컨트롤플레인 등)를 특정 파드에만 열어줄 때 쓴다. 파드에 맞는 toleration이 없으면 그 노드는 후보에서 빠진다.
이때 이벤트는 이렇게 나온다.
Events:
Warning FailedScheduling 1m default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) had untolerated taint {gpu: true}.
메시지가 어떤 taint 때문인지 키까지 찍어준다. 노드에 걸린 taint는 이렇게 확인한다.
$ kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints NAME TAINTS ip-10-0-1-23 [map[effect:NoSchedule key:gpu value:true]] ip-10-0-1-24 [map[effect:NoSchedule key:gpu value:true]]
taint effect는 세 가지다. NoSchedule은 toleration 없는 새 파드를 안 받고(기존 파드는 유지), PreferNoSchedule은 가급적 피하는 약한 버전, NoExecute는 toleration 없는 파드를 즉시 쫓아낸다.
파드가 그 노드에 들어가야 한다면 매니페스트에 toleration을 추가한다. 키, effect가 같아야 하고 operator에 따라 값 비교 방식이 갈린다.
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
여기서 자주 걸리는 함정 하나. toleration은 "그 노드에 갈 수 있다"는 허가일 뿐 "그 노드로 보내라"는 지시가 아니다. 특정 노드에만 확실히 보내려면 toleration에 더해 nodeAffinity나 nodeSelector로 목적지를 지정해야 한다. 반대로 노드의 taint가 실수로 붙은 거라면 kubectl taint nodes ip-10-0-1-23 gpu=true:NoSchedule-처럼 끝에 하이픈을 붙여 제거한다.
5. node affinity/selector 불일치
파드에 nodeSelector나 nodeAffinity를 걸면 특정 라벨을 가진 노드에만 배치된다. 그 라벨을 가진 노드가 없으면 후보가 0이 되어 Pending에 남는다. 이벤트는 이렇게 나온다.
Events: Warning FailedScheduling 30s default-scheduler 0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.
파드가 요구하는 라벨과 노드가 실제로 가진 라벨을 맞춰본다.
$ kubectl get pod web-0 -o jsonpath='{.spec.nodeSelector}'
{"disktype":"ssd"}
$ kubectl get nodes -l disktype=ssd
No resources found
파드는 disktype=ssd 노드를 찾는데 그런 라벨을 가진 노드가 없다. 오타(ssd vs SSD)이거나, 라벨을 안 붙였거나, 해당 노드가 빠졌을 수 있다. 노드에 라벨을 붙여준다.
$ kubectl label node ip-10-0-1-23 disktype=ssd node/ip-10-0-1-23 labeled
requiredDuringSchedulingIgnoredDuringExecution로 건 affinity는 조건을 만족하는 노드가 없으면 이렇게 Pending으로 끝난다. 반드시 특정 노드여야 하는 게 아니라면 preferredDuringScheduling...으로 완화하는 편을 검토한다.
6. unbound PersistentVolumeClaims - PVC 바인딩 실패
파드가 볼륨으로 PVC를 참조하는데 그 PVC가 아직 실제 볼륨(PV)에 연결되지 않았으면, 스케줄러는 파드를 노드에 붙이지 못한다. 이벤트 문구는 다음과 같다.
Events: Warning FailedScheduling 45s default-scheduler 0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims.
이때는 파드가 아니라 PVC 상태를 본다.
$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-web Pending standard 1m $ kubectl describe pvc data-web ... Events: Warning ProvisioningFailed 20s persistentvolume-controller storageclass.storage.k8s.io "standard" not found
여기서는 standard라는 StorageClass가 없어서 동적 프로비저닝이 실패했다. kubectl get storageclass로 클러스터에 실제로 있는 StorageClass 이름을 확인하고, PVC의 storageClassName을 맞춘다. 기본 StorageClass가 지정돼 있으면 필드를 비워도 된다.
StorageClass는 맞는데도 PVC가 Pending이면, 정적 PV를 쓰는 환경에서 조건에 맞는 PV가 없거나(용량, accessModes 불일치), 클라우드 프로비저너가 특정 존에만 볼륨을 만드는데 파드가 다른 존으로 스케줄되려는 topology 문제일 수 있다. describe pvc의 이벤트가 대개 어느 쪽인지 알려준다.
7. 5분 진단 순서 정리
파드가 Pending일 때 다음 순서로 움직이면 원인 특정까지 오래 걸리지 않는다.
kubectl describe pod <name>으로 Events의FailedScheduling문구를 읽는다.Insufficient cpu/memory면kubectl describe node와kubectl top nodes로 requests 예약 현황을 본다.untolerated taint면 노드 taint를 보고 toleration을 추가하거나 taint를 제거한다.didn't match Pod's node affinity/selector면 파드가 요구하는 라벨과 노드 라벨을 대조한다.unbound ... PersistentVolumeClaims면kubectl describe pvc로 StorageClass와 프로비저닝 이벤트를 확인한다.
이벤트에 아무 문구도 없이 그냥 안 뜨는 경우도 있다. 스케줄러 파드 자체가 죽었거나, 노드가 전부 NotReady거나, 파드가 hostPort를 잡아 자리가 극단적으로 좁아진 상황이다. 그럴 땐 kubectl get nodes와 컨트롤플레인 상태부터 확인한다. 핵심은 하나다. 노드를 늘리기 전에 이벤트를 먼저 읽는다.