AI 블로그 완성기 (6) - 첫 서버 다운....
AI 요약
블로그의 AI 챗봇이 원인으로 홈서버가 다운되었고, 멀티턴 대화로 인해 GPU 사용량이 증가하여 전력 피크 발생했다. 이를 해결하기 위해 글로벌 동시 요청 제한을 적용하고 히스토리 축소와 최대 출력 토큰 줄이는 조치를 취했으며, Ollama 환경변수를 수정하여 모델 VRAM 사용 시간을 줄였다. 또한 GPU 전력 제한을 통해 서버의 과부하를 방지했다.
AI 블로그 완성기 (6) - 첫 서버 다운....
정확히 이유를 짐작하기는 어렵지만, 홈서버가 처음으로 다운이 되었습니다... 아마도 블로그에 달아둔 AI 챗봇(Qwen3:8B + Ollama)이 원인이 된거 같습니다. 멀티턴 대화의 컨텍스트 누적 + 동시 요청 무제한 + GPU 전력 피크가 겹쳐 발생한 문제였고, 동시 요청 제한·히스토리 축소·GPU 전력 캡 등 몇 가지 조치를 취했습니다.
무슨 일이 있었나
블로그에 방문자용 RAG 챗봇을 운영하고 있습니다. Ollama 위에 Qwen3:8B 모델을 올려서 RTX 4070 SUPER GPU로 추론하는 구조입니다.
어느 날, 사용 전력이 급등하면서 홈서버가 갑자기 다운됐습니다. 로그를 살펴보니 다운 직전에 챗봇 사용량이 평소보다 많았습니다. 멀티턴 채팅이 원인일 수 있겠다는 생각이 들어 코드를 뜯어보기 시작했습니다.
원인 분석
챗봇 요청 하나가 GPU에서 어떤 작업을 하는지 정리해보면 이렇습니다.
| 단계 | GPU 사용 | 설명 |
|---|---|---|
| RAG 쿼리 임베딩 | nomic-embed-text 1회 | 가벼움, ~100ms |
| 청크 유사도 비교 | CPU only | 178개 청크 cosine similarity |
| LLM 추론 (Qwen3:8B) | GPU 풀로드 | num_ctx=4096, num_predict=512 |
문제는 멀티턴 대화에서 컨텍스트가 누적된다는 점이었습니다.
// 변경 전: 첫 메시지 + 마지막 5턴(10개 메시지) 유지
const trimmedHistory = rawHistory.length <= 6
? rawHistory
: [rawHistory[0], ...rawHistory.slice(-5)];
시스템 프롬프트(~1,100 토큰) + RAG 컨텍스트(~1,200 토큰) + 히스토리(최대 12개 메시지) + 사용자 질문을 합치면 4,096 토큰 윈도우를 거의 꽉 채웁니다. 컨텍스트가 길수록 추론 시간은 2-3배 늘어나고, GPU가 그만큼 오래 풀로드 상태를 유지합니다.
여기에 글로벌 동시 요청 제한이 없었습니다. IP당 분당 5회 제한은 있었지만, 여러 사용자가 동시에 질문하면 Ollama가 큐잉하며 GPU를 쉬지 않고 돌립니다.
방문자 A: 5턴 대화 (각 2-5초 GPU 추론)
방문자 B: 동시에 질문 (큐 대기 → GPU 연속 가동)
방문자 C: 또 질문
→ GPU가 수분간 100% 풀로드 → 전력 220W 연속 피크 → PSU 과부하
위험 요인을 정리하면 이렇습니다.
| 요인 | 위험도 | 근거 |
|---|---|---|
| 멀티턴 컨텍스트 누적 | 높음 | 토큰 꽉 채우면 추론 2-3배 느려짐 |
| 동시 사용자 무제한 | 높음 | GPU 연속 풀로드 |
| 글로벌 rate limit 부재 | 높음 | IP 제한만 있고 전체 제한 없음 |
| 모델 VRAM 상시 점유 | 중간 | 12GB 중 5GB 상시 사용 |
해결: 6가지 조치
1. 글로벌 동시 요청 제한 (가장 중요)
let activeRequests = 0;
const MAX_CONCURRENT = 2;
export async function POST(request: Request) {
if (activeRequests >= MAX_CONCURRENT) {
return Response.json({ error: "Server busy" }, { status: 503 });
}
activeRequests++;
// ... 추론 후 activeRequests-- (스트림 완료 시점에)
}
3번째 동시 요청부터 즉시 503을 반환합니다. GPU가 2개 이상 요청을 동시에 처리할 일이 없어졌습니다.
2. 히스토리 축소
// 변경 후: 첫 메시지 + 마지막 1.5턴(3개 메시지)만 유지
const trimmedHistory = rawHistory.length <= 4
? rawHistory
: [rawHistory[0], ...rawHistory.slice(-3)];
최대 히스토리를 12개 → 4개로 줄였습니다. 포트폴리오 Q&A는 긴 대화가 필요 없어서 품질 영향이 거의 없습니다.
3. 최대 출력 토큰 축소
num_predict를 512 → 384로 줄여 응답당 GPU 점유 시간을 ~25% 감소시켰습니다. 3-5문장 답변에는 384 토큰이면 충분합니다.
4-5. Ollama 환경변수
# docker-compose.yml
ollama:
environment:
- OLLAMA_KEEP_ALIVE=2m # 유휴 2분 후 모델 VRAM 해제
- OLLAMA_MAX_QUEUE=3 # 대기열 3개 초과 시 거부
채팅을 안 쓸 때는 VRAM을 해제하고, 과도한 큐잉도 방지합니다.
6. GPU 전력 제한
sudo nvidia-smi -pl 150 # TDP 220W → 150W
물리적으로 전력 피크를 차단합니다. 추론 속도는 ~15% 감소하지만, 서버가 다운되는 것보다 낫습니다. systemd 서비스로 등록해 재부팅 후에도 자동 적용되도록 했습니다.
프론트엔드 대응
503 응답을 받으면 사용자에게 친절한 메시지를 보여줍니다.
status === 503
? "서버가 바쁩니다. 잠시 후 다시 시도해주세요"
: "응답을 받을 수 없습니다"
정리
| 조치 | 효과 |
|---|---|
| 동시 요청 제한 (MAX_CONCURRENT=2) | GPU 연속 풀로드 방지 |
| 히스토리 축소 (12→4 메시지) | 추론 시간 단축 |
| num_predict 축소 (512→384) | 응답당 GPU 시간 25% 감소 |
| OLLAMA_KEEP_ALIVE=2m | 유휴 시 VRAM 해제 |
| OLLAMA_MAX_QUEUE=3 | 과도한 큐잉 방지 |
| GPU 전력 캡 (150W) | 물리적 전력 피크 차단 |
로컬 LLM을 프로덕션에서 돌리면 "모델 품질"이 아니라 "리소스 관리"가 진짜 과제라는 걸 체감했습니다. 클라우드 API는 돈으로 해결하지만, 로컬은 하드웨어 한계를 직접 체감해야 하는 것 같습니다... 그게 더 재밌기도 하고요:)
댓글 (0)
아직 댓글이 없습니다.