노트북 spawn 무한 '시작 중' 진단 가이드 (#2292)
ML 허브 > 개발 > 노트북에서 서버가 '시작 중'에서 멈춰 실제로 열리지 않는 경우의 진단·대응 런북입니다. 대부분 코드 버그가 아니라 싱글유저 Pod이 스케줄/기동되지 못하는 인프라 문제입니다.
증상
- 노트북 카드가 무한 스피너(
시작 중…)에 머물고열기버튼이 끝내 활성화되지 않음. - JupyterHub 는 서버 오브젝트를 만들었으나(
server존재)ready=false상태.- gend-api 의
GET /api/v1/notebooks/{user}는status:"running", ready:false를 반환.
- gend-api 의
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-managerRole 로gendns 의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 으로 고착:
- 싱글유저 Pod 요청 250m CPU. 스케줄 가능한 7개 노드 전부 94~100% CPU 예약 → 최대 여유 220m < 250m → 어디에도 배치 불가.
- 오토스케일러 scale-up 실패가 진짜 병목:
Azure CNI 라 노드/Pod 이 서브넷 IP 를 직접 소비 →code: "SubnetIsFull"message: "Subnet snet-aks with address prefix 10.0.1.0/24does not have enough capacity for 109 IP addresses."
/24(251개) 거의 소진 → 새 노드 추가 불가. nodepool12→3Backoff. 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 사유 노출까지입니다. 아래 실제 인프라 변경은 진단 결과에 따른 후속 작업으로, 정비창과 승인을 거쳐 진행합니다.
- 서브넷 확장 / 신규 서브넷 —
snet-aks를/24→/22로 확장하거나 nodepool 전용 서브넷 분리. IP 여유 확보로 scale-up 재개. (재배치 영향 검토 필요) - Azure CNI Overlay 전환 — Pod IP 를 오버레이 대역에서 할당해 노드 서브넷 IP 소비를 노드당 1개로 축소. 가장 근본적이나 클러스터 재구성 영향 큼.
- gendpool 오토스케일 활성화 — 고정 5노드 → min/max 설정으로 부하 시 자동 확장. 단 서브넷 IP 여유가 선행 조건.
- maxPods 조정 — 노드당 maxPods 를 낮춰 IP 소비를 줄이되 노드 수를 늘리는 트레이드오프.
✅ 해소 (2026-08-08, #3149) — 옵션 1 + 4 를 함께 실행
System 노드풀을 교체해 snet-aks 압박을 풀었다. 서브넷을 확장하는 대신 새
서브넷 + 새 풀 로 간 이유는, maxPods 와 vnetSubnetId 가 노드풀 생성 시점에
고정되어 기존 풀에서는 둘 다 바꿀 수 없기 때문이다.
| 전 | 후 | |
|---|---|---|
| System 풀 | nodepool1 B4as_v2×2, maxPods 110, snet-aks | syspool2 D4as_v4×2, maxPods 50, snet-aks2 |
snet-aks 사용 | 247/251 (98%) | 29/251 (12%) — codexecpool 만 잔류 |
| kubelet | v1.33.8 / v1.34.4 혼재 | v1.34.4 단일 (KubeVersionMismatch 해소) |
maxPods 110 이 사례 A 의 직접 원인이었다 — 노드 1대가 IP 111개를 선점해 /24
한 개를 두 노드가 거의 다 먹었다. 새 풀은 50 으로 낮춰 같은 문제가 재발하지 않는다.
실행 중 걸린 함정 3가지 (다음에 노드풀을 만들 때 그대로 겪는다):
- VM 사이즈는 "지역에 있는가" 와 "패밀리 쿼터가 있는가" 가 다른 축이다.
Standard_D4as_v5는 koreacentral 에 존재하지만 DASv5 패밀리 한도가 0 이라 생성 불가였다.az vm list-sizes만 보면 안 되고az vm list-usage로 그 사이즈가 속한 패밀리를 함께 봐야 한다. →Standard_D4as_v4(DASv4 여유 10). az aks nodepool add는 태그를 상속하지 않는다. 구독의require-tags-resources정책이Owner·Department·CostCenter·Environment4종을 강제해 VMSS 생성이RequestDisallowedByPolicy로 거부됐다. 기존 풀과 동일한--tags를 명시할 것. ★--no-wait로 돌리면provisioningState=Failed만 보이고 사유가 안 보인다 — activity log 에도 안 남았다. 동기 실행해야 CLI 가 정책 이름을 뱉는다.disruptionsAllowed=0인 PDB 가 drain 을 막는다.llmops-litellm이replicas=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