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:12 | 01: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 파라미터 모델에 연속으로 무거운 요청을 때려박으니:
- GPU 메모리 풀 가동
- 시스템 리소스 고갈
- SBSA Watchdog 10초 타임아웃 트리거
- 강제 전원 차단
그제는 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 자동 복구 | 죽어도 부활 |
교훈
- LLM 서빙은 생각보다 무겁다 - 32B 모델에 연속 요청은 위험
- 같은 시간대에 반복되면 cron을 의심하라
- Watchdog은 양날의 검 - 시스템을 보호하지만, 과부하 시 강제 종료
- 자동 복구 시스템은 필수 - 서버는 언제든 죽을 수 있다
이제 새벽에 Spark이 죽어도 miniPC가 1분 안에 깨운다. 아침에 일어나서 전원 버튼 누르러 갈 일은 없을 거다. 아마도.
다음 편: 3일 연속 셧다운 - 이번엔 또 뭐지? (안 나왔으면 좋겠다)