- plan.md: Phase 0 (CI/CD) + 4 시나리오 (No Fix / tini only / tini+preStop / Full Fix) - scripts/: observe, node-observe, gpu-pod-trace (cgroup v1), prestop - scenario-1~4/: Dockerfile + worker py + Pod manifest - .gitea/workflows/: ci.yaml (matrix 3 이미지 build), cd.yaml (v* tag → Helm OCI) - charts/gpu-zombie-test/: Helm chart (4 Pod template + helper) - argocd-app.yaml: Harbor OCI chart auto sync Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .gitea/workflows | ||
| charts/gpu-zombie-test | ||
| results | ||
| scenario-1-nofix | ||
| scenario-2-tini-only | ||
| scenario-3-tini-prestop | ||
| scenario-4-fullfix | ||
| scripts | ||
| .gitignore | ||
| README.md | ||
| argocd-app.yaml | ||
| plan.md | ||
README.md
GPU Zombie / Orphan Process & Orphaned CUDA Context 재현·해결 실험
Kubernetes 위에서 GPU 워크로드가 예기치 않게 종료되거나 signal handler가 잘못 짜였을 때 발생하는 좀비 프로세스 / 고아 프로세스 / orphaned CUDA context / ghost GPU memory 문제를 단계적으로 재현하고, 각 수정안(tini, preStop hook, code-level signal handler)이 어디까지 문제를 해결하는지 정량적으로 검증하는 실험 프로젝트.
실험 설계 전문은
plan.md참조. 이 README는 프로젝트 전체 개요와 사용법.
배경 — 왜 이 실험이 필요한가
GPU Pod 하나가 죽어도 GPU 메모리가 해제되지 않거나, nvidia-smi에는 프로세스가 안 보이는데
GPU 메모리는 잡혀 있는 현상(ghost context)은 현업에서 자주 관찰된다. 원인은 대체로:
- PID 1 문제 — 컨테이너의 PID 1이
bash같은 non-init 프로세스면waitpid(-1)을 호출하지 않아 좀비가 쌓임 - 고아 프로세스 — 부모가
wait()없이 종료하면 자식은 PID 1로 reparent → PID 1이 bash면 계속 살아남음 - Signal 전달 실패 —
kubectl delete pod→ SIGTERM → PID 1이 자식에게 전달 안 함 → gracePeriod 경과 후 SIGKILL - Orphaned CUDA context — 프로세스가 SIGKILL로 급사하면 CUDA driver가 미처 context를 정리하지 못해 GPU 메모리 잔존
- Ghost context —
nvidia-smi --query-compute-apps에는 안 잡히는데/dev/nvidia*에 fd가 걸려 있거나 메모리만 점유된 상태
이 실험은 점진적 수정안의 효과 범위를 시나리오별로 분리해 측정하는 게 목표다.
실험 환경
| 항목 | 값 |
|---|---|
| 노드 | gpu-4 (A100 SXM4 80GB × 4) |
| 컨테이너 런타임 | containerd |
| cgroup | v1 (systemd slice 형식) |
| K8s namespace | gpu-zombie-test |
| 컨테이너 레지스트리 | harbor.cone-chain.net/aipf |
| CI/CD | Gitea Actions + Harbor + ArgoCD |
| 관리 권한 | gpu-4 노드 SSH + sudo 가능 |
시나리오 개요
총 4개 시나리오, 3개 이미지, 4개 Pod.
(시나리오 2·3은 동일 이미지 :tini를 공유하며 Pod manifest의 preStop hook 유무로만 차이)
| # | 이름 | PID 1 | preStop | Worker 코드 | 이미지 태그 | Pod 이름 |
|---|---|---|---|---|---|---|
| 1 | No Fix | bash |
❌ | SIGTERM 무시 | :nofix |
gpu-zombie-nofix |
| 2 | tini only | tini |
❌ | SIGTERM 무시 | :tini |
gpu-zombie-tini |
| 3 | tini + preStop | tini |
✅ | SIGTERM 무시 | :tini (재사용) |
gpu-zombie-tini-prestop |
| 4 | Full Fix | tini |
✅ | SIGTERM handler + CUDA cleanup | :fullfix |
gpu-zombie-fullfix |
각 시나리오가 검증하는 가설
시나리오 1 — No Fix
PID 1이 bash면 좀비/고아/orphaned CUDA context가 모두 발생한다.
- 부모 프로세스가
wait()없이 종료 → 자식(gpu_worker)은 PPID=1 고아 - bash는 좀비 수거 안 함 → 좀비 잔존
- worker를
kill -9→ nvidia-smi에서 프로세스 사라져도 GPU 메모리 잔존 (orphaned context) - Pod 삭제 후에도 GPU 메모리가 잔존할 수 있음 → 노드 재부팅 필요 가능
시나리오 2 — tini only
tini가 PID 1이면 좀비는 수거되지만, 코드에 signal handler가 없으면 CUDA cleanup은 여전히 실패한다.
- tini의
waitpid(-1)덕분에 좀비는 사라짐 ✅ - 그러나
kill -TERM을 worker가 무시하므로 kubectl delete 시 gracePeriod 후 SIGKILL → orphaned context 여전히 발생
시나리오 3 — tini + preStop
preStop hook이 SIGTERM을 보내도, 코드가 SIGTERM을 무시하면 결국 preStop timeout → SIGKILL fallback 경로를 타고 orphaned context는 여전히 발생한다.
- Pod 삭제 → preStop hook 실행 (
/usr/local/bin/prestop.sh) - prestop.sh가 GPU 점유 PID에 SIGTERM 전송 → 20초 대기 → worker 무반응 → SIGKILL fallback
- 결과적으로 시나리오 2와 거의 동일 (단지 timing만 제어됨)
시나리오 4 — Full Fix
tini + preStop + 코드 signal handler + 충분한 gracePeriod가 모두 갖춰지면 좀비·고아·orphaned context 모두 없다.
- worker가 SIGTERM handler에서
tensors.clear()→torch.cuda.empty_cache()→torch.cuda.synchronize()→ exit - preStop은 동일한 prestop.sh를 호출하지만 worker가 graceful하게 응답하므로 SIGKILL fallback 경로 미트리거
- Pod 삭제 후 GPU 메모리 완전 해제, ghost context 없음
최종 비교표 (예상 결과)
| 항목 | S1 | S2 | S3 | S4 |
|---|---|---|---|---|
| 좀비 수거 | ❌ | ✅ | ✅ | ✅ |
| 고아 GPU 점유 | ❌ 잔존 | ❌ 잔존 | ⚠️ SIGKILL fallback | ✅ graceful |
| SIGTERM → CUDA cleanup | ❌ | ❌ (handler 없음) | ❌ (handler 없음) | ✅ |
| kill -9 후 orphaned ctx | ❌ 발생 | ❌ 발생 | ❌ 발생 가능 | N/A |
| Pod 삭제 후 GPU 메모리 | ❌ 잔존 가능 | ⚠️ 불확실 | ⚠️ 불확실 | ✅ 해제 |
| 노드 재부팅 필요 | 가능 | 가능 | 가능 | 불필요 |
디렉토리 구조
process_error/
├── README.md # 이 문서
├── plan.md # 실험 설계 전문 (1500+ 줄)
├── .claude/
│ └── settings.json # Claude Code 권한 설정 (이 프로젝트 한정)
│
├── .gitea/
│ └── workflows/
│ ├── ci.yaml # 시나리오 이미지 빌드/푸시
│ └── cd.yaml # Helm chart 패키징/푸시
│
├── scripts/ # 공통 관찰·운영 스크립트
│ ├── observe.sh # 컨테이너 내부 관찰 (좀비/고아/GPU 상태)
│ ├── node-observe.sh # 노드 레벨 GPU 관찰
│ ├── gpu-pod-trace.sh # GPU PID → Container → Pod 역추적 (노드 root 실행)
│ └── prestop.sh # 시나리오 3·4 preStop hook 공용 스크립트
│
├── scenario-1-nofix/ # 시나리오 1 — bash PID 1
│ ├── Dockerfile
│ ├── parent.py # 자식 spawn 후 wait() 없이 종료
│ ├── gpu_worker.py # SIGTERM 무시, GPU ~2GB 점유
│ └── pod.yaml # manual apply용 (CI/CD 미사용 시)
│
├── scenario-2-tini-only/ # 시나리오 2 — tini PID 1
│ ├── Dockerfile
│ ├── parent.py
│ ├── gpu_worker.py # 시나리오 1과 동일
│ └── pod.yaml
│
├── scenario-3-tini-prestop/ # 시나리오 3 — tini + preStop
│ └── pod.yaml # 이미지는 scenario-2 재사용
│
├── scenario-4-fullfix/ # 시나리오 4 — 전부 적용
│ ├── Dockerfile
│ ├── parent.py
│ ├── gpu_worker_fixed.py # SIGTERM handler + CUDA cleanup
│ └── pod.yaml
│
├── charts/
│ └── gpu-zombie-test/ # Helm chart (4개 pod template)
│ ├── Chart.yaml
│ ├── values.yaml # scenario enable 토글
│ └── templates/
│ ├── _helpers.tpl
│ ├── pod-nofix.yaml
│ ├── pod-tini.yaml
│ ├── pod-tini-prestop.yaml
│ └── pod-fullfix.yaml
│
├── argocd-app.yaml # ArgoCD Application 정의
│
└── results/ # 시나리오별 실험 결과
├── scenario-1/
│ ├── observe-after-spawn.log
│ ├── observe-after-kill.log
│ ├── gpu-pod-trace.log
│ ├── nvidia-smi-before-delete.log
│ ├── nvidia-smi-after-delete.log
│ └── notes.md
├── scenario-2/
├── scenario-3/
├── scenario-4/
└── comparison.md # 최종 비교 분석
현재 상태:
plan.md,README.md,.claude/settings.json만 존재. 나머지는plan.md의 설계대로 Phase 0부터 순차 생성 예정.
실험 흐름 — 어떻게 동작하는가
Phase 0 — CI/CD 환경 구성
- Gitea repo 생성 (
soo-rnd/gpu-zombie-test) + Secrets 등록 (REGISTRY_URL/USER/PASSWORD) - K8s namespace + pull secret 사전 생성
.gitea/workflows/ci.yaml작성 —scenario-*/변경 시 matrix로 3개 이미지를 Harbor에 빌드·푸시.gitea/workflows/cd.yaml작성 —v*tag push 시 Helm chart를 Harbor OCI에 푸시charts/gpu-zombie-test/Helm chart 작성 — 4개 Pod templateargocd-app.yamlapply — 이후 chart 버전 업 시 ArgoCD가 자동 sync
[코드 변경]
├─ scenario-*/ 변경 → CI 트리거
│ └─ 3개 이미지 build → Harbor push (:nofix, :tini, :fullfix + sha 태그)
│
└─ git tag v0.1.x → CD 트리거
└─ Helm chart package → Harbor OCI push
└─ ArgoCD 자동 sync
└─ gpu-zombie-test ns에 4개 Pod 배포
Phase 1~2-C — 시나리오 실행 사이클
각 시나리오는 다음 공통 흐름을 따름:
# 1. 코드 수정 → push → CI 자동 빌드 확인 (Gitea Actions UI)
# 2. Pod 재생성으로 새 이미지 pull (imagePullPolicy: Always)
kubectl delete pod gpu-zombie-<scenario> -n gpu-zombie-test
# → ArgoCD selfHeal로 즉시 재배포
# 3. Pod 진입 & 실험
kubectl exec -it gpu-zombie-<scenario> -n gpu-zombie-test -- bash
# 4. 컨테이너 내부 관찰
observe.sh # 좀비/고아/GPU 상태
# 5. 노드에서 역추적 (별도 터미널)
ssh gpu-4
bash /path/to/gpu-pod-trace.sh
# 6. 특정 액션 수행 후 재관찰
# - parent.py 실행 후 고아 생성
# - kill -9 <worker_pid>로 orphaned context 유발
# - kubectl delete pod로 preStop 동작 확인
# 7. 결과 로그 저장 → results/scenario-<N>/
관찰 도구 — 무엇을 어떻게 확인하는가
scripts/observe.sh (컨테이너 내부)
컨테이너 안에서 실행. 다음 8가지를 한 번에 점검:
- PID 1 cmdline (bash vs tini)
- 전체 프로세스 트리 (
ps auxf) - 좀비 프로세스 (
STAT=Z) - 고아 프로세스 (PPID=1)
- GPU 메모리 상태 (
nvidia-smi --query-gpu) - GPU 사용 프로세스 (
nvidia-smi --query-compute-apps) /dev/nvidia*fd 보유 프로세스 (fuser)- Orphaned GPU context 종합 체크 (nvidia-smi ∩ fuser ∩ /proc 생존 확인)
scripts/node-observe.sh (gpu-4 노드 root)
노드 레벨에서 GPU 전체 상태 + ghost context 감지.
scripts/gpu-pod-trace.sh (gpu-4 노드 root) ⭐ 핵심
cgroup v1 + containerd + systemd slice 포맷에 맞춰 작성된 GPU PID → Pod 역추적 스크립트.
/proc/<pid>/cgroup파싱 → container ID 추출 →crictl inspect→ Pod name/namespace/container name- nvidia-smi에 안 잡히는 숨은 프로세스(
fuser로만 감지)도 추적 - Ghost context (프로세스 0개인데 GPU 메모리 점유) 별도 경고
출력 예시:
PID STATE GPU_MEM NSMI QOS POD NAMESPACE CONTAINER
12345 R 2048 MiB Y besteffort gpu-zombie-nofix gpu-zombie-test worker
scripts/prestop.sh (Pod preStop hook)
시나리오 3·4의 이미지에 /usr/local/bin/prestop.sh로 포함.
- GPU 점유 PID 수집 (nvidia-smi + fuser)
- SIGTERM 전송 → 최대 20초 대기
- 미종료 시 SIGKILL fallback (시나리오 3에서 트리거)
결과는 어디서 확인하는가
1. CI/CD 동작 확인
| 항목 | 위치 |
|---|---|
| CI 빌드 로그 | https://gitea.inje-private.com/soo-rnd/gpu-zombie-test/actions |
| 푸시된 이미지 | https://harbor.cone-chain.net/harbor/projects/aipf/repositories/gpu-zombie-test |
| 푸시된 Helm chart | Harbor 동 project OCI artifacts |
| ArgoCD sync 상태 | https://argocd.gpulive.cloud → Application gpu-zombie-test |
2. Pod & 클러스터 상태
# 배포된 Pod 확인
kubectl get pods -n gpu-zombie-test
# 특정 Pod 상세 (events, restarts, container status)
kubectl describe pod gpu-zombie-<scenario> -n gpu-zombie-test
# Pod 로그 (parent.py, gpu_worker.py 출력 + preStop 로그)
kubectl logs gpu-zombie-<scenario> -n gpu-zombie-test
# Pod 삭제 시 preStop 로그를 실시간으로 보려면
kubectl logs -f gpu-zombie-<scenario> -n gpu-zombie-test &
kubectl delete pod gpu-zombie-<scenario> -n gpu-zombie-test
3. GPU 상태 (노드에서)
ssh ubuntu@gpu-4
sudo -i
# 한 눈에 보기
watch -n 1 'nvidia-smi'
# 전체 진단
bash /path/to/scripts/node-observe.sh
# PID → Pod 역추적
bash /path/to/scripts/gpu-pod-trace.sh
4. 실험 결과 파일 (results/)
각 시나리오별로 다음 파일들을 저장:
| 파일 | 내용 |
|---|---|
observe-after-spawn.log |
parent.py 실행 직후 observe.sh 출력 |
observe-after-kill.log |
kill -9 <worker> 직후 observe.sh 출력 — orphaned context 확인 |
gpu-pod-trace.log |
노드에서 gpu-pod-trace.sh 출력 — PID→Pod 매핑 |
nvidia-smi-before-delete.log |
Pod 삭제 직전 GPU 상태 |
nvidia-smi-after-delete.log |
Pod 삭제 직후 GPU 상태 (메모리 잔존 여부) |
notes.md |
해당 시나리오 특이사항, 예상과 다른 결과, 타이밍 이슈 등 |
5. 최종 비교 — results/comparison.md
모든 시나리오 완료 후, 4개 결과를 한 표로 정리하고 각 수정안의 효과를 정량화. plan.md의 "Phase 3: 결과 비교" 템플릿 사용.
Quick Start
전제: plan.md 기준 Phase 0 (CI/CD 환경) 완료 상태.
# 1. 클러스터 접근 확인
kubectl config current-context
kubectl get ns gpu-zombie-test
# 2. 배포된 Pod 확인 (ArgoCD가 sync 완료한 상태)
kubectl get pods -n gpu-zombie-test
# NAME READY STATUS ...
# gpu-zombie-nofix 1/1 Running
# gpu-zombie-tini 1/1 Running
# gpu-zombie-tini-prestop 1/1 Running
# gpu-zombie-fullfix 1/1 Running
# 3. 시나리오 1부터 순차 실험 — plan.md Phase 1 참조
kubectl exec -it gpu-zombie-nofix -n gpu-zombie-test -- bash
# (컨테이너 내부)
python3 parent.py &
sleep 10
observe.sh
# 4. 노드에서 역추적
ssh ubuntu@gpu-4
sudo bash gpu-pod-trace.sh
# 5. 결과 저장 (로컬)
mkdir -p results/scenario-1
kubectl exec gpu-zombie-nofix -n gpu-zombie-test -- observe.sh > results/scenario-1/observe-after-spawn.log
# 6. 다음 시나리오 전 GPU 초기화 확인
nvidia-smi
# (필요 시 nvidia-smi --gpu-reset 또는 노드 재부팅)
주의사항
-
실험은
gpu-4노드를 독점한다 — 다른 GPU 워크로드가 있으면 간섭 가능. 실험 전nvidia-smi로 깨끗한 상태 확인. -
시나리오 간 GPU 초기화 필수 — orphaned/ghost context가 누적되면 후속 시나리오 결과가 오염된다. 정리 안 되면
nvidia-smi --gpu-reset시도, 안 되면 gpu-4 노드 재부팅. -
nvidia-smi --gpu-reset은 해당 GPU를 쓰는 다른 컨텍스트가 있으면 실패 — 모든 GPU pod 정리 후 시도. -
prestop.sh는 이미지 2개(:tini,:fullfix)에만 포함 — 시나리오 1의:nofix이미지에는 없음. -
시나리오 2와 3은 같은 이미지를 쓴다 — scenario-3는 별도 이미지를 빌드하지 않고
:tini재사용. CI matrix에 scenario-3가 없는 것은 의도된 것.
참고 문서
- 설계 전문:
plan.md— 모든 스크립트·매니페스트·체크리스트 포함 (1500+ 줄) - Claude Code 권한:
.claude/settings.json— 이 프로젝트 한정 권한 - CI/CD 참고 템플릿:
/home/ubuntu/soo/vLLM/A100_npu/— 동일 Gitea/Harbor/ArgoCD 스택 사용 중