Cloudera AI Inference 의 모델 엔드포인트를 운영하면서 자주 부딪히는 두 가지를 정리한다. 하나는 파드를 지우면 모델을 다시 내려받느라 복구가 오래 걸리는 문제이고, 다른 하나는 CAI 세션이 모델 엔드포인트가 쓰던 GPU 를 가져가면서 엔드포인트가 죽는 문제다.
NIM_CACHE_PATH=/mnt/serving 은 EmptyDir 볼륨이고 파드 수명 동안만 유지된다. 파드를 삭제하면 EmptyDir 이 사라지고 storage-initializer init 컨테이너가 모델을 다시 내려받아 복구가 느리다. 컨테이너만 재시작하면 init 컨테이너가 다시 돌지 않아 캐시가 남고 가중치 로드만 하면 되므로 훨씬 빠르다.
컨테이너 안에서 PID 1 을 죽이려 해도 소용이 없다. PID 1 은 핸들러가 없는 시그널을 커널이 무시하며, 비대화식 bash 는 SIGTERM 트랩을 걸지 않는다. 대신 PID 1 의 직계 자식을 종료하면 start_server.sh 의 wait 가 풀리면서 스크립트가 끝나고, kubelet 이 restartPolicy: Always 에 따라 같은 파드 안에서 컨테이너만 새로 띄운다.
POD=<endpoint-pod>
NS=serving-default
kubectl get pod $POD -n $NS \
-o jsonpath='{.status.containerStatuses[?(@.name=="kserve-container")].restartCount}{"\n"}'
kubectl exec $POD -n $NS -c kserve-container -- sh -c 'kill -TERM $(pgrep -P 1)'
30~60초 뒤에도 그대로면 강제 종료한다.
kubectl exec $POD -n $NS -c kserve-container -- sh -c 'kill -KILL $(pgrep -P 1)'
pgrep -P 1 은 vLLM 계열이든 Triton 계열이든 동일하게 동작하므로 이미지별 분기가 필요 없다. 결과는 AGE 가 리셋되지 않고 RESTARTS 만 올라가는 것으로 확인한다.
kubectl get pod $POD -n $NS -w
kubectl logs -n $NS $POD -c kserve-container --previous | tail -50
노드에 접속할 수 있으면 crictl 로 컨테이너만 멈추는 방법이 가장 확실하다. istio-proxy 와 queue-proxy 는 그대로 살아 있다.
kubectl get pod -n serving-default <pod> -o wide
export PATH=$PATH:/var/lib/rancher/rke2/bin
export CONTAINER_RUNTIME_ENDPOINT=unix:///run/k3s/containerd/containerd.sock
crictl pods --name <pod> -q
crictl ps -p <pod-id> --name kserve-container -q
crictl stop -t 60 <container-id>
헬스체크 실패 시 kubectl delete pod 하던 워치독은 한 줄 치환만으로 컨테이너 재기동으로 바꿀 수 있다.
kubectl exec "$POD" -n "$NS" -c kserve-container -- sh -c 'kill -TERM $(pgrep -P 1)' >/dev/null 2>&1
컨테이너만 재시작해도 파드 phase 는 계속 Running 이라 다음 루프의 파드 조회에 그대로 잡히고 헬스체크가 이어진다. 다만 GPU 메모리 미해제나 캐시 손상은 컨테이너 재시작으로 풀리지 않으므로, 연속 실패 시 파드 삭제로 폴백하는 카운터를 두는 편이 안전하다.
FAIL_CNT=$((FAIL_CNT + 1))
log "FAIL $POD code=$CODE (fail=$FAIL_CNT) -> restart"
kubectl logs "$POD" -n "$NS" --all-containers --tail=-1 \
> "$LOGDIR/dead_$(date '+%Y%m%d-%H%M%S')_${POD}.log" 2>&1
if [ "$FAIL_CNT" -lt 3 ]; then
if OUT=$(kubectl exec "$POD" -n "$NS" -c kserve-container -- \
sh -c 'kill -TERM $(pgrep -P 1)' 2>&1); then
log "container restart signal sent: $POD"
else
log "container restart failed: $OUT -> fallback to pod delete"
kubectl delete pod "$POD" -n "$NS" --wait=false >/dev/null 2>&1
FAIL_CNT=0
fi
else
log "3 consecutive fails -> delete pod $POD"
kubectl delete pod "$POD" -n "$NS" --wait=false >/dev/null 2>&1
FAIL_CNT=0
fi
정상(200) 분기에서 FAIL_CNT=0 초기화를 빠뜨리지 않는다. 로그는 죽이기 전에 먼저 확보한다.
모델 엔드포인트가 GPU 를 점유 중인데 CAI 세션이 같은 GPU 를 잡으면서 엔드포인트가 죽는 것은 정상 동작이 아니다. nvidia.com/gpu 는 원래 배타적 할당이라 세션은 Pending 으로 대기해야 한다. 원인 후보는 셋이다.
GPU time-slicing 이 켜져 있으면 물리 GPU 1장이 N개로 광고돼 세션과 모델이 같은 GPU 에 동시 스케줄된다. 타임슬라이싱은 MIG 와 달리 메모리와 장애를 격리하지 않는다. 우선순위 선점이 원인이면 모델 파드 이벤트에 Preempted 가 찍히고, 노드 자원 압박이면 Evicted 가 찍힌다.
kubectl describe node <gpu-node> | grep -A5 -i "Allocatable\|nvidia.com/gpu"
kubectl -n <workbench-ns> describe pod <model-pod>
kubectl -n <workbench-ns> get events --sort-by=.lastTimestamp | grep -i "evict\|preempt\|oom"
kubectl -n gpu-operator get cm -o yaml | grep -i -A10 "timeSlicing\|replicas"
nvidia-smi # 해당 노드에서
nvidia-smi 에 두 프로세스가 함께 보이면 time-slicing 이 원인으로 확정된다. 우선순위 가설은 다음 두 명령으로 1분 안에 판별할 수 있다. 값이 같으면 스케줄러는 선점을 시도하지 않으므로 가설을 버린다.
kubectl -n <workbench-ns> get pod \
-o custom-columns='NAME:.metadata.name,PC:.spec.priorityClassName,PRIO:.spec.priority'
kubectl -n <workbench-ns> get events --field-selector reason=Preempted
CAI 워크로드 파드는 대개 priorityClassName 을 설정하지 않아 전부 priority 0 으로 뜬다. Preempted 이벤트가 없고 모델 파드 종료 사유가 OOMKilled 이거나 로그에 CUDA out of memory 가 있으면 time-slicing 이 범인이다.
대응은 운영 엔드포인트용 GPU 노드와 세션용 GPU 노드를 taint·label 기반 노드 그룹으로 분리하는 것이 가장 확실하다. 공유가 꼭 필요하면 time-slicing 대신 MIG 로 메모리까지 격리하거나 최소한 failRequestsGreaterThanOne: true 로 의미를 명확히 한다. 선점이 원인이면 모델 배포 파드에 더 높은 PriorityClass 를 부여한다.
2&>1 은 잘못된 문법이다. bash 가 1 이라는 이름의 파일로 리다이렉트한다. 올바른 형태는 2>&1 이다.2>/dev/null 로 에러를 버리면 exec 실패 사유를 알 수 없다. 변수에 담아 실패했을 때만 로그에 남기는 편이 낫다.pgrep -P 1 결과가 비면 kill 이 인자 부족 오류를 낸다. 위 형태면 그 메시지도 로그에 남는다.sleep 900 은 파드 재생성 대기 기준이라 컨테이너 재시작에는 과하다. 컨테이너 재기동 경로만 180초 정도로 줄이면 복구 감지가 빨라진다.curl -H "Authorization: Bearer ${LLM_TOKEN}" 형태로만 참조한다. 이미 노출된 키는 폐기·재발급한다.