로컬 LLM 추론 서버의 사활 감시를 자동화한 이야기

로컬 LLM 추론 서버의 사활 감시를 자동화한 이야기

4 min read

2026.04 / Tech Blog / BASTION

로컬 LLM 추론 서버 헬스체크 자동화 이야기 #

GPUStack이 “실행 중인 것처럼 보이지만 추론할 수 없는” 상태를 감지합니다. 2단계 헬스체크 + Slack 알림 + Zabbix 연동을 5분 간격으로 실행하는 구조를 만들었습니다.

무엇이 문제였는가 #

이전 글(로컬 LLM으로 인프라 로그를 자동 분석하는 구조를 만든 이야기)에서 소개한 BASTION은 15분마다 인프라 로그를 로컬 LLM(Qwen2.5-14B)으로 자동 분석하는 구조입니다.

이 구조의 단일 장애점은 GPUStack(LLM 추론 서버)입니다. GPUStack이 다운되면 로그 수집은 계속되지만 분석·알림이 모두 중단됩니다. 게다가 중단된 것을 알아차리기까지 시간이 오래 걸립니다. 15분마다의 Slack 알림이 “오지 않는” 것을 사람이 인지하기까지 최악의 경우 몇 시간이 걸립니다.

GPUStack 자체에는 모델 자동 재시작 기능이 있습니다. 프로세스가 크래시하면 자동으로 재시작됩니다. 하지만 이것만으로는 감지할 수 없는 케이스가 있습니다.

GPUStack 자동 재시작으로 감지할 수 없는 케이스:
・API 프로세스는 살아있지만 모델이 OOM으로 언로드된 경우
・API는 200을 반환하지만 모델이 추론 요청에 응답하지 않는 경우
・네트워크상 BASTION 서버에서 GPUStack에 도달할 수 없는 경우

즉, “프로세스의 생존”과 “추론이 실제로 동작하는가”는 별개의 문제입니다. 외부에서 정기적으로 “정말로 추론할 수 있는가”를 확인하는 구조가 필요했습니다.

설계 방침 #

하는 일은 간단합니다. 5분마다 GPUStack의 API를 호출해서 정상이면 아무것도 하지 않고, 이상이면 Slack에 알립니다. 다만 몇 가지 설계상 판단이 있습니다.

2단계 체크 #

헬스체크를 2단계로 나눴습니다.

단계체크 내용무엇을 감지하는가실패 시
CHECK1/v1/models API로 HTTP 요청GPUStack 프로세스 헬스체크Exit 1 (CRITICAL)
CHECK2chat/completions에 실제 추론 요청모델이 실제로 로드되어 응답 가능한지Exit 2 (WARNING)

CHECK1은 통과했지만 CHECK2가 실패하는 케이스가 중요합니다. “API는 살아있지만 모델이 추론할 수 없는” 상태. 이것은 프로세스 모니터링만으로는 절대 잡을 수 없습니다.

CHECK2에서는 "healthcheck"라는 단어 하나를 보내고 max_tokens: 5로 응답을 받습니다. 추론이 동작하는지 확인만 하면 되므로 최소한의 토큰으로 처리합니다.

종료 코드 설계 #

스크립트의 종료 코드를 3가지로 설정했습니다.

종료 코드의미Slack 알림Zabbix 트리거
0정상복구 시에만
1API 다운CRITICAL (빨강)심각한 장애
2모델 다운WARNING (주황)경고

1과 2를 나눈 이유는 대응이 다르기 때문입니다. Exit 1이면 GPUStack 프로세스 자체를 확인해야 합니다. Exit 2라면 GPUStack 대시보드에서 모델 상태를 보면 됩니다. 알림을 본 사람이 다음에 무엇을 해야 할지가 종료 코드만으로도 알 수 있도록 설계했습니다.

연속 알림 억제 #

5분 간격으로 실행하면 GPUStack이 30분간 다운된 경우 같은 CRITICAL 알림이 6번 날아옵니다. 3번째쯤부터는 아무도 Slack을 보지 않게 됩니다.

이를 방지하기 위해 이전 상태를 파일에 저장하고 상태가 변경된 경우에만 알림하도록 설계했습니다.

상태 전환알림
ok → api_downCRITICAL 알림 발송
ok → model_downWARNING 알림 발송
api_down → okRECOVERED 알림 발송
api_down → api_down알림 없음
model_down → model_down알림 없음

복구 시 RECOVERED 알림을 보내는 것도 중요합니다. CRITICAL을 받은 사람이 “아직 다운인가, 이미 복구됐는가”를 확인하기 위해 대시보드를 열 필요가 없어집니다.

구현 #

셸 스크립트 하나로 완성됩니다. BASTION의 다른 스크립트(summarize.sh, analyze.sh 등)와 같은 디렉토리에 배치하고 cron으로 5분마다 실행합니다.

CHECK1: API 응답 확인 #

MODELS_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
    --max-time 15 \
    -H "Authorization: Bearer ${API_KEY}" \
    "${GPUSTACK_HOST}/v1/models")

GPUStack은 OpenAI 호환 API를 제공하므로 /v1/models 엔드포인트에 GET 요청만 보내면 됩니다. HTTP 200이 반환되면 API가 살아있는 것입니다. 타임아웃은 15초입니다.

CHECK2: 실제 추론 테스트 #

INFERENCE_RESPONSE=$(curl -s --max-time 15 \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "Content-Type: application/json" \
    "${GPUSTACK_HOST}/v1/chat/completions" \
    -d '{"model":"qwen2.5-14b-instruct",
         "messages":[{"role":"user","content":"healthcheck"}],
         "max_tokens":5,"temperature":0}')

응답 JSON에 "choices"가 포함되어 있으면 추론 성공입니다. 포함되어 있지 않으면 모델 다운으로 처리합니다.

상태 저장 #

# 이전 상태 읽기
STATUS_FILE="/tmp/gpustack-health-status"
PREV_STATUS="ok"
[ -f "$STATUS_FILE" ] && PREV_STATUS=$(cat "$STATUS_FILE")

# 현재 상태 저장
echo "ok" > "$STATUS_FILE"      # or "api_down" or "model_down"

파일 하나에 상태를 쓰기만 하면 됩니다. 복잡한 데이터베이스는 필요 없습니다. 재시작해도 처음에는 파일이 없으므로 ok가 기본값이 되고, 이상이 있으면 첫 번째 알림이 전송됩니다.

cron 등록 #

*/5 * * * * bash /opt/oc-seclogs/gpustack-healthcheck.sh >> /var/log/gpustack-healthcheck.log 2>&1

BASTION의 정기 분석(15분 간격)보다 짧은 5분 간격으로 설정했습니다. LLM이 다운된 상태에서 정기 분석이 실행되면 헛돌기 때문에, 그 전에 감지하고 싶었기 때문입니다.

실제로 겪은 문제 #

API 키 인증을 간과했습니다 #

처음 실행했을 때, CHECK1에서 바로 HTTP 401이 반환되었습니다. GPUStack에 API 키 인증이 설정되어 있었는데, curl에 Authorization 헤더를 넣지 않았습니다. 당연한 실수지만, 헬스체크 스크립트를 테스트 환경(인증 없음)에서 작성하고 프로덕션에 그대로 가져가면 발생합니다. .env 파일에서 API 키를 읽어오는 형태로 수정해서 해결했습니다.

CHECK1은 통과하고 CHECK2에서 실패하는 케이스는 실제로 존재합니다 #

GPUStack의 API 프로세스는 실행 중이지만, 모델 로드에 실패한 상태입니다. /v1/models는 200을 반환하는데, chat/completions는 500을 반환합니다. 프로세스 모니터링만으로는 “정상”으로 판정되어 버립니다. 2단계 체크로 한 판단이 옳았다는 것을 운영에서 확인할 수 있었습니다.

Slack 알림 형식에는 “다음에 무엇을 해야 할지”를 포함합니다 #

처음에는 “GPUStack API가 응답하지 않습니다”라고만 표시했지만, 알림을 받은 사람(=저 자신이지만)이 다음에 할 일은 매번 같습니다. 그래서 알림 본문에 확인 명령어(curl -s http://<endpoint>/v1/models)를 포함하도록 했습니다. 알림을 보고 터미널에 복사 붙여넣기하면 바로 상황을 확인할 수 있습니다.

Zabbix 연동 #

cron에 의한 Slack 알림은 즉시성이 높지만, “지난 1개월의 가동률은 어땠는가” “응답 시간의 트렌드는 어떤가”와 같은 장기적인 관점이 부족합니다. 이 부분은 Zabbix의 전문 분야이므로, UserParameter를 등록해서 Zabbix에서도 모니터링하고 있습니다.

UserParameter #

Zabbix Agent의 설정 파일에 3개의 UserParameter를 추가했습니다.

내용데이터 타입
gpustack.health헬스체크 스크립트의 종료 코드(0/1/2)정수
gpustack.response_timeAPI 응답 시간(밀리초)정수
gpustack.models모델 목록 JSON텍스트

gpustack.health는 헬스체크 스크립트 자체를 호출하고, 종료 코드를 반환합니다. cron과 Zabbix에서 같은 스크립트를 재사용함으로써, 판정 로직의 중복을 피하고 있습니다.

트리거 #

3단계의 트리거를 설정했습니다.

심각도조건의미
심각한 장애gpustack.health = 1API 다운. LLM 추론이 완전히 중단
경고gpustack.health = 2API는 살아있지만 모델이 추론 불가능
정보gpustack.response_time > 5000API 응답에 5초 이상. GPU 고부하 징후

3번째 “응답 지연” 트리거는, CRITICAL이 되기 전의 예조 감지로 기능합니다. 응답 시간이 점점 늘어나는 그래프가 보이면, OOM이 발생하기 전에 모델의 배치 사이즈나 동시 요청 수를 조정할 수 있습니다.

cron과 Zabbix의 역할 분담 #

cron(5분 간격): 이상 발생 시 Slack으로 즉시 알림. 간단하고 확실.
Zabbix(5분 간격): 히스토리 저장, 응답 시간 그래프, 에스컬레이션, 장애 이력.

둘 다 실행함으로써 이중화를 확보. cron이 작동하지 않아도 Zabbix가 감지하고, Zabbix가 다운되어도 cron이 알림.

운영 결과 #

운영을 시작한 후, GPUStack의 모델이 응답하지 않게 되는 케이스를 2회 감지했습니다. 둘 다 CHECK1은 통과하고 CHECK2에서 감지(Exit 2). GPUStack의 대시보드를 확인하니 모델이 “Error” 상태가 되어 있었습니다. GPUStack의 자동 재시작 기능이 모델을 재로드하고, 몇 분 후 RECOVERED 알림이 와서 복구를 확인했습니다.

만약 헬스체크가 없었다면, 다음 정기 분석(최대 15분 후)이 헛돌고, Slack에 알림이 오지 않는 것을 사람이 알아차릴 때까지 더 시간이 걸렸을 것입니다.

정리 #

로컬 LLM 추론 서버(GPUStack)의 생사 모니터링을, 셸 스크립트+cron+Zabbix로 자동화했습니다.

설계상의 포인트는 3가지입니다. 2단계 체크(API 응답과 실제 추론을 나누어 감지 정확도를 높임), 연속 알림 억제(상태 변화 시에만 알림하여 피로를 방지), cron과 Zabbix의 병용(즉시 알림과 장기 트렌드의 양립).

셸 스크립트 1개로 완결되는 구조이므로, GPUStack에 한정되지 않고 vLLM이나 Ollama 등 OpenAI 호환 API를 제공하는 LLM 서버라면 같은 접근법으로 모니터링할 수 있습니다.

BASTION은 폐쇄 환경에서 AI 보안 모니터링을 실현하는 서비스입니다.

BASTION 서비스 페이지
문의하기

Updated on 2026년 6월 9일

What are your feelings

  • Happy
  • Normal
  • Sad

©2020 BESTNET.LLC . All Rights Reserved.