AI 기반 서비스

Today
-
Total
-
← 목록으로

DGX Spark 셧다운 2일차 - LLM 과부하와 WOL 자동 복구 시스템 구축

이창연•2026년 1월 24일

DGX Spark 셧다운 2일차 - 진짜 범인을 찾아서

또 꺼져 있다

2026년 1월 24일 아침. 또 DGX Spark가 꺼져 있었다.

어제 PM2 무한 재시작 문제를 해결했는데? 분명 PM2 서비스에 재시작 제한도 걸고, 상태도 확인했는데?

miniPC의 watchdog 로그를 확인해보니 01:25부터 CraftBay MCP 헬스체크가 실패하고 있었다. 새벽 1시대에 또 셧다운된 것이다.

용의자 비교: 그제 vs 어제

항목1/22→23 (그제)1/23→24 (어제)
PM2 재시작 횟수66,000회+0회
셧다운 시각01:1201:24
마지막 작업PM2 무한 루프???

PM2는 더 이상 범인이 아니었다. 재시작 카운터가 모두 0이었다.

그런데... 둘 다 새벽 1시대에 죽었다. 이건 우연이 아니다.

새벽 1시에 뭐가 돌지?

$ crontab -l | grep "0 1"
0 1 * * * /home/lisyoen/projects/polilog/scripts/run-analysis.sh

PoliLog 분석 스크립트가 매일 새벽 1시에 실행되고 있었다.

이 스크립트가 하는 일:

  • 295명의 국회의원
  • 30개 키워드
  • 최대 8,850회의 QwQ-32B LLM 호출
  • 각 호출당 120초 타임아웃

로그가 말해주는 진실

정상 실행 시 분석 로그 크기

약 200KB 이상

그제 분석 로그

739 bytes (중단됨)

어제 분석 로그

1,157 bytes (중단됨)

분석 시작하고 얼마 안 돼서 죽은 것이다.

로그 내용을 보니:

[1] 강기윤 - 예산 (47건)
Error: This operation was aborted

LLM 호출 중 시스템이 뻗어버렸다.

범인: QwQ-32B 과부하

32B 파라미터 모델에 연속으로 무거운 요청을 때려박으니:

  1. GPU 메모리 풀 가동
  2. 시스템 리소스 고갈
  3. SBSA Watchdog 10초 타임아웃 트리거
  4. 강제 전원 차단

그제는 PM2 무한 루프가 시스템을 약하게 만들어놓은 상태에서 LLM이 마무리를 했고, 어제는 LLM 혼자서 시스템을 죽였다.

해결책 1: 분석 스크립트에 숨 좀 쉬게 해주기

기존 설정:

// LLM 호출 후 2초 대기
await new Promise(r => setTimeout(r, 2000));

수정된 설정:

const DELAY_BETWEEN_CALLS = 10000;  // 10초 대기
const BATCH_SIZE = 10;               // 10건마다
const BATCH_REST_SECONDS = 60;       // 60초 휴식

// 기본 딜레이
await sleep(DELAY_BETWEEN_CALLS);

// 배치 휴식
if (llmCalls % BATCH_SIZE === 0) {
  console.log(`[휴식] ${llmCalls}건 처리 완료, 60초 대기...`);
  await sleep(BATCH_REST_SECONDS * 1000);
}

분석 시간이 길어지지만, 서버가 죽는 것보단 낫다.

해결책 2: 스케줄 변경

# 기존: 새벽 1시
0 1 * * * /home/lisyoen/projects/polilog/scripts/run-analysis.sh

# 변경: 새벽 3시
0 3 * * * /home/lisyoen/projects/polilog/scripts/run-analysis.sh

데이터 동기화(00:10)가 끝나고 충분히 시간을 두고 시작.

해결책 3: WOL 자동 복구 시스템

아무리 조심해도 서버는 죽을 수 있다. 죽으면 자동으로 살리자.

아키텍처

miniPC (watchdog)
    │
    ├─ 매 분 헬스체크
    │   └─ curl https://mcp.craftbay.io/health
    │
    ├─ 실패 감지 시
    │   └─ ping 192.168.50.150 (Spark IP)
    │
    └─ ping 실패 = 전원 꺼짐
        └─ WOL Magic Packet 전송

WOL 스크립트 (Python)

#!/usr/bin/env python3
"""Wake-on-LAN for DGX Spark"""
import socket

SPARK_MAC = "4c:bb:47:29:b1:81"
BROADCAST = "192.168.50.255"

def send_wol(mac_address):
    mac_bytes = bytes.fromhex(mac_address.replace(":", ""))
    magic_packet = b'\xff' * 6 + mac_bytes * 16
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.sendto(magic_packet, (BROADCAST, 9))
    sock.close()
    print(f"WOL packet sent to {mac_address}")

if __name__ == "__main__":
    send_wol(SPARK_MAC)

Watchdog 로직

# CraftBay MCP 헬스체크 실패 시
if ! health_check "$CRAFTBAY_HEALTH_URL"; then
    # Spark 전원 상태 확인
    if ! ping -c 1 -W 3 "$SPARK_IP" > /dev/null 2>&1; then
        log "WARN: Spark 서버 ping 실패 - 전원 꺼짐 추정"
        
        # WOL 전송
        send_telegram "⚡ Spark 전원 꺼짐 감지, WOL 전송 중..."
        python3 wol-spark.py
    fi
fi

실제 테스트: WOL이 진짜 되나?

말만 하면 안 된다. 직접 테스트해봤다.

# Spark에서 종료
$ sudo poweroff

30초 후, miniPC에서:

$ python3 wol-spark.py
WOL packet sent to 4c:bb:47:29:b1:81

$ sleep 30 && ping -c 3 192.168.50.150
PING 192.168.50.150 (192.168.50.150) 56(84) bytes of data.
64 bytes from 192.168.50.150: icmp_seq=1 ttl=64 time=1.31 ms
64 bytes from 192.168.50.150: icmp_seq=2 ttl=64 time=0.104 ms
64 bytes from 192.168.50.150: icmp_seq=3 ttl=64 time=0.397 ms

WOL 성공! 🎉

DGX Spark이 전원 꺼진 상태에서 Magic Packet을 받고 자동으로 부팅됐다.

최종 방어 체계

단계방어책효과
1차LLM 호출 딜레이 (10초)과부하 방지
2차배치 휴식 (10건/60초)GPU 쿨다운
3차스케줄 변경 (01시→03시)충돌 회피
4차WOL 자동 복구죽어도 부활

교훈

  1. LLM 서빙은 생각보다 무겁다 - 32B 모델에 연속 요청은 위험
  2. 같은 시간대에 반복되면 cron을 의심하라
  3. Watchdog은 양날의 검 - 시스템을 보호하지만, 과부하 시 강제 종료
  4. 자동 복구 시스템은 필수 - 서버는 언제든 죽을 수 있다

이제 새벽에 Spark이 죽어도 miniPC가 1분 안에 깨운다. 아침에 일어나서 전원 버튼 누르러 갈 일은 없을 거다. 아마도.


다음 편: 3일 연속 셧다운 - 이번엔 또 뭐지? (안 나왔으면 좋겠다)