본문으로 건너뛰기

노트북 spawn 무한 '시작 중' 진단 가이드 (#2292)

ML 허브 > 개발 > 노트북에서 서버가 '시작 중'에서 멈춰 실제로 열리지 않는 경우의 진단·대응 런북입니다. 대부분 코드 버그가 아니라 싱글유저 Pod이 스케줄/기동되지 못하는 인프라 문제입니다.

증상

  • 노트북 카드가 무한 스피너(시작 중…)에 머물고 열기 버튼이 끝내 활성화되지 않음.
  • JupyterHub 는 서버 오브젝트를 만들었으나(server 존재) ready=false 상태.
    • gend-api 의 GET /api/v1/notebooks/{user}status:"running", ready:false 를 반환.

UI 가 표시하는 사유 (#2292 이후)

gend-api 는 ready=false 인 서버에 대해 싱글유저 Pod jupyter-<user>best-effort·fail-open 으로 읽어(services/notebook_diagnostics.py) 사유를 응답에 실어 보냅니다(pending, pending_reason, pending_message). UI 는 무한 스피너 대신 시작 지연 — <사유> 배지와 전체 메시지를 노출합니다.

pending_reason (k8s)UI 친화 문구근본 원인
Unschedulable (+ Insufficient cpu)클러스터 용량 부족 (CPU)노드 CPU 예약 포화
Unschedulable (+ Insufficient memory)클러스터 용량 부족 (메모리)노드 메모리 예약 포화
Unschedulable (기타)스케줄 불가 — 가용 노드 없음taint/affinity/PVC 미바인딩 등
ErrImageNeverPull / ImagePullBackOff / ErrImagePull노트북 이미지를 가져오지 못함싱글유저 이미지가 노드/레지스트리에 없음
CrashLoopBackOff / CreateContainerConfigError컨테이너 시작 실패잘못된 설정/시크릿/엔트리포인트
Pending (신호 없음)시작 대기 중 (Pending)스케줄 직전 단계

RBAC: gend-api ServiceAccount 는 gend-serving-manager Role 로 gend ns 의 pods get/list/watch 권한을 이미 보유합니다(KServe 진단용). 본 진단에 신규 RBAC 나 인프라 변경은 불필요합니다.

진단 절차

# 1) 멈춘 싱글유저 Pod 상태
kubectl get pod jupyter-<user> -n gend

# 2) 스케줄 실패 사유 (Pending 인 경우)
kubectl get pod jupyter-<user> -n gend \
-o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason} {.message}{"\n"}{end}'

# 3) 컨테이너 waiting 사유 (ErrImageNeverPull / ImagePullBackOff 등)
kubectl get pod jupyter-<user> -n gend \
-o jsonpath='{.status.containerStatuses[0].state.waiting.reason}'

# 4) 노드 가용 용량 (CPU/메모리 예약률)
kubectl describe nodes | grep -A5 "Allocated resources"

# 5) (클라우드) 오토스케일러 scale-up 실패 이벤트
kubectl get events -n kube-system --field-selector reason=FailedScaleUp

사례 A — prod aks-genos-prod 용량/서브넷 IP 고갈 (2026-06-23)

jupyter-demo, jupyter-dwmyoung Pod 이 Pending 으로 고착:

  1. 싱글유저 Pod 요청 250m CPU. 스케줄 가능한 7개 노드 전부 94~100% CPU 예약 → 최대 여유 220m < 250m → 어디에도 배치 불가.
  2. 오토스케일러 scale-up 실패가 진짜 병목:
    code: "SubnetIsFull"
    message: "Subnet snet-aks with address prefix 10.0.1.0/24
    does not have enough capacity for 109 IP addresses."
    Azure CNI 라 노드/Pod 이 서브넷 IP 를 직접 소비 → /24(251개) 거의 소진 → 새 노드 추가 불가. nodepool1 2→3 Backoff. gendpool 은 오토스케일 비활성(고정 5노드).

사례 B — gend_dev 싱글유저 이미지 부재 (온프렘)

jupyter-<user>ErrImageNeverPull 로 고착. infra/jupyterhub/values.yaml 의 싱글유저 이미지가 gend-singleuser:latest + pullPolicy: Never(Kind 전용)라, 온프렘 노드에는 해당 이미지가 없어 컨테이너가 기동되지 못함.

  • 해결: infra/jupyterhub/Dockerfile.singleuser 빌드 → Harbor(localhost:31500/gend/) 푸시 → values 의 singleuser.image + profileList 이미지 경로를 Harbor 경로로 교체.

Remediation 옵션 (인프라 — 별도 정비창 + 승인 필요)

본 #2292 스코프는 UX 사유 노출까지입니다. 아래 실제 인프라 변경은 진단 결과에 따른 후속 작업으로, 정비창과 승인을 거쳐 진행합니다.

  1. 서브넷 확장 / 신규 서브넷snet-aks/24/22 로 확장하거나 nodepool 전용 서브넷 분리. IP 여유 확보로 scale-up 재개. (재배치 영향 검토 필요)
  2. Azure CNI Overlay 전환 — Pod IP 를 오버레이 대역에서 할당해 노드 서브넷 IP 소비를 노드당 1개로 축소. 가장 근본적이나 클러스터 재구성 영향 큼.
  3. gendpool 오토스케일 활성화 — 고정 5노드 → min/max 설정으로 부하 시 자동 확장. 단 서브넷 IP 여유가 선행 조건.
  4. maxPods 조정 — 노드당 maxPods 를 낮춰 IP 소비를 줄이되 노드 수를 늘리는 트레이드오프.

✅ 해소 (2026-08-08, #3149) — 옵션 1 + 4 를 함께 실행

System 노드풀을 교체snet-aks 압박을 풀었다. 서브넷을 확장하는 대신 새 서브넷 + 새 풀 로 간 이유는, maxPodsvnetSubnetId 가 노드풀 생성 시점에 고정되어 기존 풀에서는 둘 다 바꿀 수 없기 때문이다.

System 풀nodepool1 B4as_v2×2, maxPods 110, snet-akssyspool2 D4as_v4×2, maxPods 50, snet-aks2
snet-aks 사용247/251 (98%)29/251 (12%) — codexecpool 만 잔류
kubeletv1.33.8 / v1.34.4 혼재v1.34.4 단일 (KubeVersionMismatch 해소)

maxPods 110 이 사례 A 의 직접 원인이었다 — 노드 1대가 IP 111개를 선점해 /24 한 개를 두 노드가 거의 다 먹었다. 새 풀은 50 으로 낮춰 같은 문제가 재발하지 않는다.

실행 중 걸린 함정 3가지 (다음에 노드풀을 만들 때 그대로 겪는다):

  1. VM 사이즈는 "지역에 있는가" 와 "패밀리 쿼터가 있는가" 가 다른 축이다. Standard_D4as_v5 는 koreacentral 에 존재하지만 DASv5 패밀리 한도가 0 이라 생성 불가였다. az vm list-sizes 만 보면 안 되고 az vm list-usage그 사이즈가 속한 패밀리를 함께 봐야 한다. → Standard_D4as_v4(DASv4 여유 10).
  2. az aks nodepool add 는 태그를 상속하지 않는다. 구독의 require-tags-resources 정책이 Owner·Department·CostCenter·Environment 4종을 강제해 VMSS 생성이 RequestDisallowedByPolicy 로 거부됐다. 기존 풀과 동일한 --tags 를 명시할 것. ★ --no-wait 로 돌리면 provisioningState=Failed 만 보이고 사유가 안 보인다 — activity log 에도 안 남았다. 동기 실행해야 CLI 가 정책 이름을 뱉는다.
  3. disruptionsAllowed=0 인 PDB 가 drain 을 막는다. llmops-litellmreplicas=1 + minAvailable=1 이라 필연적으로 0이었다. 무상태 Deployment 면 임시 2 replica → drain → 1 로 복원이 안전·가역적이다.

노드풀을 바꾸면 같이 봐야 하는 것: infra/weaviate/networkpolicy.yaml 의 ipBlock 이 노드풀 서브넷을 하드코딩한다. 안 고치면 그 대역에 linkerd control plane 이 스케줄되는 순간 weaviate 가 CrashLoopBackOff 된다 (#3201 에서 실제로 누락된 채 지나갈 뻔했다). CI 가드 scripts/test_netpol_nodepool_cidr.py 가 막지만, 운영자는 --from-azure 로 라이브 대조도 할 수 있다. 5. 싱글유저 자원 요청 완화 — CPU guarantee 하향(예: 250m→100m)으로 스케줄 성공률 향상. 단 과다 점유 위험이 있어 본 이슈에서는 비스코프.

관련

  • 진단 코드: apps/api/src/gend_api/services/notebook_diagnostics.py
  • 응답 enrich: apps/api/src/gend_api/routers/notebook.py (_enrich_pending)
  • UI 배지: ui/src/components/notebooks/NotebooksPage.tsx
  • 이슈: #2292