분명 콘솔에서는 인스턴스가 초록불(RUNNING)인데, 왜 내 API는 에러를 뿜으며 멈춰 있을까요?
배포 당일, 대시보드에FAILED가 찍힌 채 절대 바뀌지 않던 그 순간 식은땀 흘렸던 기억으로 이 글을 씁니다.
같은 자리에서 발을 동동 구르고 있을 당신에게, 이 글이 바로 그날의 저를 구해줄 가이드가 되길 바랍니다.
1. 비싼 GPU 아끼려다 마주친 함정 — 온디맨드의 딜레마
먼저 공감부터 하고 가죠. 도면 분석 같은 GPU 서비스를 만들면 누구나 같은 계산을 합니다.
VLM 모델 하나가 21GB, EasyOCR 서버까지 띄우려면 RTX 4090급 이상이 필요하고,
시간당 $0.34~$0.44가 24시간 내내 나갑니다. 하루 $8~$10, 한 달이면 약 $245.
그런데 실제 요청은 하루 몇 차례뿐. 이건 그냥 두면 안 되죠.
디스크는 그대로 두니 모델을 다시 받을 필요도 없고, GPU 과금만 멈추면 됩니다.
계산대로라면 월 $25 내외, 90% 절감. 저도 그 계산에 설렜습니다.
그런데 막상 만들어서 Resume 버튼을 눌렀더니, 이런 화면이 저를 반겼습니다.
Pod: RUNNING / Provisioning: FAILED / Step: 원격 스크립트 실행 오류 (code 1)
인프라는 살아 있는데 서비스 상태는 영구 FAILED. 새로고침을 백 번 눌러도 그대롭니다.
"내가 뭘 잘못했지? 콘솔은 초록불인데?" — 그 당혹스러움, 저도 겪었습니다.
결론부터 말하면, 당신이 잘못한 게 아닙니다. Resume 경로에는 처음 설계할 때 보이지 않던
함정 세 개가 숨어 있었을 뿐입니다.
2. 전체 그림: 깨우고, 쓰고, 재우는 온디맨드 라이프사이클
본론에 들어가기 전, 시스템 전체 흐름을 한 장으로 그려둘게요.
이 그림을 머릿속에 넣고 읽으면, 뒤의 함정들이 "아, 저기서 터지는 거구나" 하고 보일 겁니다.
분석 요청이 들어오면 깨우고(resume), 끝나고 5분(300초)이 지나면 다시 재우는 것.
동시에 여러 요청이 들어와도 기동은 단 한 번만 일어나도록 잠금(
asyncio.Lock)을 걸고,작업이 끝나든 실패하든
finally에서 종료를 등록해 타이머 예약이 절대 빠지지 않게 했습니다.10번 중 1~2번이 FAILED에 고착되면, 운영자가 직접 SSH로 들어가 스크립트를 재실행해야 하고,
그 사이 Pod은 RUNNING 과금 상태로 방치됩니다. 즉 resume 신뢰성이 곧 돈입니다.
그래서 다음 장의 트러블슈팅은 단순한 버그 수정이 아니라, 이 아키텍처의 존립 조건이었습니다.
3. 그날의 식은땀: RUNNING인데 FAILED — 함정 세 개 파헤치기
자, 이제 당신이 보고 있을 그 화면을 하나씩 뜯어봅시다.
원인은 세 겹이었고, 서로 맞물려 있었습니다.
함정 1. "설치돼 있는데 왜 또 설치하다 터지지?" — 셋업 스크립트의 조기 실패
set -e 모드로 실행됩니다. 하나라도 실패하면 즉시 전체 중단.문제는 "실패"의 정의였습니다.
grep이 매칭을 못 찾았을 때 내뱉는 exit 1,apt-get·pip·pgrep 계열이 툭하면 반환하는 비-0 종료 코드 —이런 것들은 진짜 실패가 아니라 그냥 "정보"에 가까운데, 스크립트는 전부 치명상으로 받아들였습니다.
더 억울한 건, Ollama나 EasyOCR이 이미 설치돼 있는데도 매번 재설치를 시도했다는 겁니다.
멀쩡한 걸 또 설치하다 터지는 꼴. "분명 어제는 됐는데?" 싶으셨다면, 바로 이 함정입니다.
Ollama 바이너리가 있으면 설치 단계를 통째로 건너뛰고,
EasyOCR이 import되면 pip install을 생략하고,
모델 파일이 디스크에 있으면 다운로드를 건너뜁니다.
그리고 조건 검사 명령들은 실패해도 스크립트가 죽지 않게 방어막을 쳐주세요.
이것만으로 resume 시간이 수십 분 단축되고, 실패 지점 자체가 사라집니다.
저는 이걸 "Fast-Path"라고 불렀습니다. 말 그대로 지름길이거든요.
함정 2. "Resume인데 왜 처음부터 다시 받지?" — 무조건 재설치의 늪
stop→resume은 디스크가 보존됩니다. 21GB짜리 모델이 그대로 남아 있어요.
그런데도 매번 Ollama 재설치, 수 분짜리 pip 설치, 수십 분짜리 모델 다운로드를
처음부터 반복하고 있었다면 믿으시겠어요? 저희가 그랬습니다.
각 단계는 시간 낭비일 뿐 아니라 새로운 실패 지점이 됩니다.
다운로드 중에 네트워크가 한 번만 흔들려도 전체가 실패하니까요.
"재가동인데 왜 신규 설치처럼 오래 걸리지?" 싶으셨다면, 이 함정을 의심하세요.
"이미 있으면 건너뛴다"는 원칙 하나로 resume이 가벼워집니다.
그리고 프로비저닝 시작 전에 "혹시 이미 전부 살아 있니?"를 먼저 확인하세요.
Ollama·모델·OCR이 다 살아 있으면 재설치 과정을 통째로 생략하고 즉시 READY.
이 사전 점검 하나로 stop→resume 사이클이 완전히 달라집니다.
함정 3. "한 번 FAILED면 영원히 FAILED?" — 좀비 상태의 저주
그
FAILED 상태가 인메모리에 저장되는데, 이후 백그라운드에서Ollama와 EasyOCR이 멀쩡히 기동돼도 조회 API는 저장된
FAILED만 계속 반환했습니다.실제 상태와 무관한 "좀비 상태"죠. polling을 백 번 해도 바뀌지 않는 이유가 여기 있었습니다.
"Pod이 중지되었습니다" 문구가 재가동 후에도 남아서,
[GPU: RUNNING] [준비 중] Pod이 중지되었습니다라는 모순이 화면에 떴던 겁니다.사용자는 시스템을 못 믿고 재기동 버튼을 연타하고, 중복 기동은 새로운 경합과 과금을 낳는 악순환.
저도 이 화면 앞에서 한참을 멍하니 있었습니다.
매 조회마다 실제 서비스에 직접 물어보고(Ollama·OCR 살아 있니? 모델 올라왔니?),
그 결과로 상태를 덮어쓰는 겁니다. Pod이 RUNNING인데 과거 FAILED가 남아 있으면
진행 중 상태로 되돌리고, 전부 정상이면 즉시 READY로.
쓰는 쪽(상태가 바뀔 때 초기화)과 읽는 쪽(조회할 때 실시간 확인) 양쪽에서 고치는 게 정석입니다.
한쪽만 고치면 좀비가 다른 쪽에서 다시 살아납니다. 경험담입니다.
4. 회복의 순간: 자가 치유(Self-Healing) 아키텍처
"빠르게 건너뛰고(Fast-Path), 실시간으로 확인하고(Readiness), 틀렸으면 고쳐라(Self-Healing)."
가장 뿌듯했던 순간을 말씀드릴게요. 같은 상황에서 상태 API가 이렇게 답하기 시작한 순간입니다.
Provisioning: PROVISIONING / Step: AI 서비스 가동 및 모델 다운로드 중... (17% - 3.6GB/21.0GB)
고정된 "로딩 중…"이 아니라 살아있는 숫자. 원격 모델 디렉토리 크기를 주기적으로 재서
전체 21GB 대비 진행률을 계산한 겁니다. 이 숫자가 보이기 시작하면 운영자의 마음이 놓입니다.
"아, 멈춘 게 아니라 받고 있구나." 불필요한 재시도 버튼 연타가 사라지는 순간이죠.
다운로드 중이라면 숫자로 보여주세요. 그게 운영자를 살립니다.
그리고 자가 치유의 전체 흐름은 이렇습니다. 조회가 들어오면 과거 기록을 그대로 내주는 게 아니라,
실제 서비스에 매번 확인하고, 모델 디렉토리 크기를 재고, 그 결과로 상태를 덮어씁니다.
과거 FAILED가 남아 있어도 실제가 살아 있으면 진행 중으로 되돌리고,
전부 정상이면 READY로. 스크립트가 비정상 종료했어도 실제 서비스가 멀쩡하면 복구해줍니다.
"스크립트 실패 = 서비스 실패"라는 잘못된 등식을 끊은 것, 그게 자가 치유의 본질입니다.
5. 다시는 안 당하려면: TDD로 함정을 테스트에 가두기
함정의 재현 방법을 테스트로 남기지 않으면, 반드시 다시 당합니다.
그래서 이런 테스트를 가장 먼저 썼습니다. "Pod은 RUNNING인데 과거 FAILED가 남아 있고,
실제로는 일부 서비스만 살아 있는 상황"을 가짜(Mock)로 재현하고,
조회 결과가 FAILED에서 진행 중 상태로 회복되는지를 단언하는 테스트입니다.
장애 상황 자체를 테스트로 가두는 거죠. Red(실패 재현) → Green(고쳐서 통과) → Refactor(다듬기),
이 순서 그대로요.
결과는 RunPod 관련 53개 테스트 통과, 전체 백엔드 150개 테스트 통과.
"여러 요청이 동시에 와도 기동은 1번만", "5분 지나면 자동 중지", "새 요청 오면 타이머 취소"까지
전부 테스트에 가둬뒀습니다. 이제 누군가 코드를 고치다가 자가 치유를 깨뜨리면 테스트가 먼저 비명을 지릅니다.
(덧붙여, 글 속 Pod ID·키는 전부 마스킹 처리했습니다. 실제 값은 절대 올리지 마세요!)
6. 같은 길을 가는 당신에게 — 교훈 다섯 가지
- 원격 스크립트는 방어적으로 짜세요.
grep한 줄의 exit 1 때문에 전체 셋업이 죽는 허무함을 겪지 마시길.
조건 검사는 실패해도 죽지 않게, 설치는 "이미 있으면 건너뛰기"를 원칙으로. - Resume과 신규 생성을 구분하세요. 디스크가 살아 있는데 처음부터 다시 설치하는 건
시간·비용·실패율을 세 배로 불리는 지름길입니다. 재가동 경로는 가볍게. - 캐시된 상태와 실시간 진실을 분리하세요. 과거 기록을 그대로 반환하면 좀비 상태가 됩니다.
조회할 때마다 실제에 물어보고 덮어쓰는 자가 치유를 넣으세요. - 진행률은 숫자로 보여주세요. "로딩 중…"이 아니라 실측 진행률을.
운영자의 불안이 사라지면 불필요한 재시도도 사라집니다. - 당했던 함정은 테스트로 남기세요. 장애 재현 테스트가 가장 싼 회귀 방지책입니다.
다음 사람이 같은 식은땀을 흘리지 않게, 당신의 오늘을 테스트에 기록해주세요.
분명 콘솔은 초록불인데 API는 멈춰 있던 그 순간, 저도 "나만 이런가" 싶었습니다.
하지만 아니었습니다. 이건 Resume 아키텍처를 만드는 사람이라면 누구나 밟는 함정이었고,
빠르게 건너뛰고, 실시간으로 확인하고, 틀리면 고치는 구조로 해결되는 문제였습니다.
이 글이 당신의 그날 밤을 조금이라도 짧게 해주길 바랍니다. 화이팅입니다.
서비스명·키·ID는 일반화/마스킹 처리되었습니다.