GPU Operator 와 타임슬라이싱 · GPU 파드가 안 뜬다 — 원인을 순서대로 좁힌다 · 이론
같은 메시지, 다른 원인 — GPU 장애 분류표
한 줄 요약
"GPU 파드가 안 뜬다" 는 신고를 받으면 먼저 파드 오브젝트가 있는가 없는가로 두 갈래를 가르고, 있다면 Pending 인지 Running 인지로 다시 가른 뒤, 노드 status 와 파드 spec 을 대조해 일곱 가지 원인 중 하나로 좁힌다. 스케줄러가 남기는 문장은 세 가지 서로 다른 원인에서 글자 하나 다르지 않게 똑같이 나오므로, 메시지를 읽는 것만으로는 끝나지 않는다.
왜 이게 필요했나
GPU 클러스터 운영에서 가장 자주 오는 문의는 늘 같은 한 문장이다. "제 잡이 안 돌아가요." 그런데 이 한 문장 뒤에 있는 실제 상태는 최소한 이만큼 다르다.
- 파드를 만들려고 했는데
kubectl apply가 에러를 냈다. 파드는 존재하지 않는다. - 파드는 만들어졌는데 몇 시간째
Pending이다. - 파드는
Running인데 안에서nvidia-smi가 장치를 못 찾는다.
세 가지는 조사해야 할 자리가 전혀 다르다. 첫째는 어드미션(쿼터·RuntimeClass·정책), 둘째는 스케줄러, 셋째는 파드 spec 과 노드의 런타임이다. 그런데 신고자는 이 셋을 구분해 주지 않는다. 그래서 받은 쪽이 첫 3분에 갈래를 정하는 절차를 갖고 있어야 한다. 그 절차가 없으면 device plugin 로그를 한 시간 뒤진 다음에야 "아, 파드가 애초에 안 만들어졌네" 를 알게 된다.
한 걸음 더 나쁜 사정이 있다. 쿠버네티스 스케줄러는 실패 이유를 친절하게 남기는 편인데, 확장 자원에 대해서는 그 친절이 원인을 가리는 쪽으로 작동한다. 아래 세 상황에서 나오는 문장이 똑같다.
- 그 노드에
nvidia.com/gpu자체가 광고되어 있지 않다(device plugin 이 죽었거나 아예 없다). capacity에는 4장이 있는데allocatable이 0 이다(드라이버 검증 실패, 노드가 GPU 를 내놓지 못하는 중).- 광고도 멀쩡하고 자리도 있는데 이미 다른 파드가 전부 쥐고 있다.
세 경우 모두 Insufficient nvidia.com/gpu 다. 원인은 각각 "플러그인을 살려라", "노드를 고쳐라", "자리를 기다리거나 늘려라" 로 완전히 다른데 메시지는 구분을 못 해 준다. 이 한 가지 사실이 이 모듈이 존재하는 이유다.
증상을 가르는 첫 질문 두 개
질문 1 — 파드 오브젝트가 있는가.
kubectl get pod <이름> -n <네임스페이스> -o json없다면 스케줄러는 이 사건과 아무 상관이 없다. 오브젝트는 API 서버의 어드미션을 통과해야 생기는데, GPU 관련해서 어드미션이 막는 대표적인 둘이 ResourceQuota 초과와 없는 RuntimeClass 참조다. 둘 다 kubectl apply 를 한 사람의 터미널에만 에러가 찍히고 클러스터에는 아무 흔적도 남지 않는다. 신고자가 그 화면을 닫아 버렸다면 같은 매니페스트를 다시 적용해 에러를 재현하는 것이 가장 빠르다.
질문 2 — spec.nodeName 이 비어 있는가.
kubectl get pod <이름> -n <네임스페이스> -o jsonpath='{.spec.nodeName}'비어 있으면 스케줄링 문제다. 채워져 있는데 문제가 있다면 그것은 노드 위의 문제(런타임 핸들러, 드라이버, 요청을 안 한 매니페스트)다. kubectl get pod 의 STATUS 칸이 아니라 이 필드를 보는 이유는, Pending 이라는 한 단어가 "노드를 못 골랐다" 와 "노드는 골랐는데 컨테이너를 못 만든다" 를 둘 다 덮기 때문이다.
원인이 어느 칸에서 드러나는가
| 원인 | 결정적 증거가 있는 곳 | 보는 명령 |
| --- | --- | --- |
| 쿼터 초과 | 파드가 없음. 생성 시 에러 문구 | kubectl get resourcequota -n <ns> -o json |
| RuntimeClass 없음 | 파드가 없음. 생성 시 에러 문구 | kubectl get runtimeclass |
| 요청을 안 함 | spec.containers[].resources.limits 에 nvidia.com/gpu 가 없음 | kubectl get pod -o json |
| 라벨 불일치 | spec.nodeSelector 를 만족하는 노드가 0개 | kubectl get nodes --show-labels |
| 테인트 | 후보 노드의 spec.taints 를 파드의 tolerations 가 못 견딤 | kubectl get node -o json |
| 자원 미광고 | 후보 노드의 status.capacity 에 자원 이름 자체가 없음 | kubectl get node -o json |
| allocatable 0 | capacity 는 양수인데 status.allocatable 이 0 | 같은 출력의 다른 칸 |
| 자리 소진 | allocatable 은 양수인데 그 노드의 파드 요청 합이 그만큼 | kubectl get pods -A -o json 을 합산 |
이 표의 쓸모는 "무엇을 보는가" 가 아니라 "어떤 순서로 보는가" 에 있다. 순서는 위에서 아래다. 앞의 것이 참이면 뒤의 것은 볼 필요가 없고, 순서를 지키지 않으면 엉뚱한 결론이 난다. 예를 들어 nodeSelector 가 아무 노드와도 안 맞는 파드는 테인트도 자원도 검사할 대상이 애초에 없다. 그런데 "테인트를 지웠는데도 안 뜬다" 같은 조치를 먼저 해 버리면 멀쩡한 클러스터 설정만 망가진다.
마지막 줄(자리 소진)이 유일하게 계산을 요구한다. 노드 오브젝트만 봐서는 알 수 없고, 그 노드에 배치된 파드들을 모아 GPU 요청을 더해야 한다. 여기서 한 가지 함정이 있다. Succeeded 나 Failed 로 끝난 파드는 자리를 쥐고 있지 않으므로 합산에서 빼야 한다. 빼지 않으면 실제로는 비어 있는 노드를 "꽉 찼다" 고 잘못 판정한다.
확장 자원이라서 생기는 사정
nvidia.com/gpu 는 cpu·memory 와 달리 kubelet 이 스스로 세지 못하는 확장 자원이다. 공식 문서는 확장 자원의 규칙을 둘로 못 박는다. 오버커밋을 지원하지 않고, requests 와 limits 가 같아야 하며, 값은 정수여야 한다. 그래서 GPU 요청을 읽을 때는 limits 만 봐도 충분하다. 반대로 말하면 반 장이나 1.5장 같은 값은 존재할 수 없고, 모자란 만큼만 받는 식의 동작도 없다. 요청은 통째로 만족되거나 파드가 기다리거나 둘 중 하나다.
그리고 확장 자원은 "장치가 있는가" 와 무관하게 성립한다. 노드 status 에 숫자만 적혀 있으면 스케줄러는 그 노드로 파드를 보낸다. 반대로 카드가 여덟 장 꽂혀 있어도 status 에 숫자가 없으면 스케줄러에게 그 노드는 GPU 가 없는 노드다. 조사할 때 물리적 사실이 아니라 노드 오브젝트가 스케줄러의 유일한 진실이라는 것을 붙잡고 있어야 한다.
현장에서 만나는 모습
첫째, kubectl describe 만 읽고 원인을 단정하는 습관이 가장 비싸다. 앞서 본 대로 세 원인이 같은 문장을 낸다. 특히 야간에 "Insufficient nvidia.com/gpu" 를 보고 자동으로 노드를 늘리는 스크립트를 걸어 둔 조직이 있었는데, 실제 원인은 노드 하나의 allocatable 이 0 으로 떨어진 것이었다. 노드를 늘려도 그 노드는 계속 0 이었고, 비용만 늘었다.
둘째, 사람이 판정하면 순서를 지키지 못한다. 그래서 이 판정은 도구로 굳혀 두는 편이 낫다. 입력은 파드 하나, 출력은 원인 한 단어. 이런 도구가 있으면 1차 대응자가 신고를 받은 자리에서 곧장 "이건 쿼터입니다" 라고 답할 수 있고, 판정 기준이 사람마다 달라지지 않는다. 이 모듈의 실습이 만드는 것이 정확히 그 도구다.
셋째, 출력은 한 단어여야 쓸모가 있다. 문장을 내면 사람이 다시 읽어야 하고, 자동화에 물릴 수 없다. no-gpu-node·allocatable-zero·exhausted 처럼 다음 행동이 하나로 정해지는 어휘를 골라 두면 그대로 알림 라우팅 키가 된다. 원인 어휘를 정하는 일이 실은 운영 절차를 정하는 일이다.
넷째, 이 환경의 정직한 한계. 실습 클러스터에는 GPU 도 device plugin 도 없다. 그래서 "장치 플러그인이 죽었다" 는 상태는 플러그인을 죽여서가 아니라 노드 status 에 자원을 안 적는 것으로 만든다. 이것은 흉내가 아니라 같은 상태다 — 실제 장애에서도 스케줄러가 보는 것은 status 뿐이기 때문이다. 다만 "파드는 Running 인데 컨테이너 안에서 장치가 안 보인다" 의 뒷부분은 컨테이너가 실제로 돌지 않으므로 확인할 수 없다. 그 갈래는 파드 spec 에 자원 요청이 없다는 사실까지만 판정한다.
참고 문서
- 노드에 확장 자원 광고하기: https://kubernetes.io/docs/tasks/administer-cluster/extended-resource-node/
- 컨테이너 자원 관리(확장 자원 규칙): https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- 테인트와 톨러레이션: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
- 리소스 쿼터: https://kubernetes.io/docs/concepts/policy/resource-quotas/
- 파드 디버깅: https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/
- GPU Operator 문제 해결: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/troubleshooting.html
다음 실습에서 할 것
kwok 이 띄운 진짜 스케줄러 위에 일곱 가지 상태를 하나씩 만들어 놓는다. 광고가 멀쩡한 노드, 자원 이름 자체가 없는 노드, capacity 만 있고 allocatable 이 0 인 노드를 세우고, 거기에 각각 파드를 꽂아 세 원인이 같은 문장을 내는 것을 직접 본다. 테인트만 다른 파드와 nodeSelector 만 다른 파드를 더하고, 어드미션에서 거부되어 오브젝트조차 생기지 않는 두 가지(RuntimeClass·쿼터)를 겪는다. 마지막으로 kubectl get -o json 만 보고 원인을 한 단어로 답하는 분류기를 만들어 일곱 파드 전부에 물려 본다.