운영 문서에서 IP·호스트명·계정·자격증명을 제거한 버전. 노드는 node1~node4로 표기(node1이 head, NFS 핫티어 서버). 측정은 2026-09-29 08:56 ~ 13:20 KST. 이 클러스터 한 구성의 결과이며 일반화하지 말 것.
요약
- 목적: 정전·계획 정지 후 “사람 없이 다시 올라오는가"를 실제로 확인. 이전까지 재부팅은 사고(자동 커널 업데이트, 네트워크 공사)로만 겪었다.
- 방식: 정지 스크립트로 node4 → node3 → node2 → node1 순차
poweroff→ IoT 플러그로 전원 차단·재투입 → BIOS의 AC 복구 자동 켜짐 → 워치독으로 서빙 기동. - 결과: 정지 약 2분, 전원 투입부터 API health 200까지 약 16분(그중 서빙 부팅 14분). 4노드 동시 부팅에도 NFS 핫티어 마운트·RoCE 200G·MTU·관리망 IP·KV 캐시 크기가 전부 정지 전과 같았다. Xid 0, OOM 0.
- 발견
- 부팅이 끝나지 않았다: 4대 중 3대가 부팅 1시간이 지나도
systemctl is-system-running=starting. 그래픽 부팅 스플래시를 내리는plymouth-quit-wait가 멈춰multi-user.target에 도달하지 못했다. 서빙·SSH·NFS는 그 전에 다 떠서 겉으로는 정상이었다. head는 직전 부팅에서도 8일간 같은 상태였던 기록이 있다. - 그 결과 GPU 클럭이 노드마다 달라졌다:
After=multi-user.target로 걸린 클럭 유닛은 부팅이 끝난 노드에서만 실행된다. 유일하게 부팅이 끝난 node2에서만 1800MHz 고정 유닛이 돌아 node2 GPU가 1768MHz로 묶였다(나머지는 2000MHz 캡). TP=4는 가장 느린 랭크에 맞춰 돈다. head의 2000MHz 캡도 시스템 유닛은 대기 중이었고, 5분마다 도는 사용자 타이머가 대신 걸고 있었다. - CPU 캡은 켜져 있지만 효과가 없다: 10분마다 전 노드
scaling_max_freq를 2.0GHz로 쓰는 타이머가 있지만, 코어는 실측 3.9GHz로 돈다. CPU가 펌웨어 자율 모드(CPPC autonomous)라 이 값을 무시한다. - Prometheus 미기동: 종료 시 SIGTERM 후 10초 안에 안 끝나 SIGKILL(137) →
unless-stopped인데도 부팅 후 자동 시작되지 않았다. - 스크립트 자체 버그: 비대화형 실행 시 확인 입력을 중간 명령이 먼저 먹어 버렸다. 확인 게이트가 “취소"로 실패해 아무것도 꺼지지 않았다.
- 부팅이 끝나지 않았다: 4대 중 3대가 부팅 1시간이 지나도
왜 했나
클러스터는 가정에 있고, 원격 접속은 노드 위의 Tailscale에 의존한다. 노드가 꺼지면 원격으로 켤 수단이 없다. 그래서 두 가지를 준비했다.
- 노드 BIOS의 AC 전원 복구 시 자동 켜짐 활성화 → 전원만 들어오면 부팅한다.
- 노드별 IoT 플러그 → 원격으로 전원을 끊었다 넣을 수 있다. 플러그 제어 경로는 노드에 의존하지 않아야 한다(노드 위에서 도는 홈 자동화로 플러그를 제어하면 노드가 꺼진 순간 아무것도 못 한다).
여기서 중요한 구분: 플러그 OFF는 반드시 OS 정상 종료 이후다. 켜진 상태로 전원을 끊는 건 여전히 금지(NVMe 쓰기 중이면 특히). 플러그는 “끄는 도구"가 아니라 “다시 켜는 도구"다.
이번 범위는 GPU 노드 4대만. NAS·스위치·공유기는 켜 둔 채였다.
정지 스크립트 설계
node1에서 실행하고, 워커는 SSH로 제어한다. 원칙은 과거 워치독 사고에서 배운 것 하나다: 파괴적 단계 전에 모든 프리체크를 끝낸다.
- 실행 위치(node1) 확인, sudo 비밀번호 1회 입력(메모리에만 보관)
- 프리체크 — 하나라도 실패하면 아무것도 끄지 않고 종료
- 워커 3대 SSH·sudo 인증
- 진행 중 쓰기 작업 없음(HF 다운로드, rsync,
docker load/save, apt/dpkg)
- 기준선 기록 — 커널, NFS 마운트, IP, RoCE 링크 속도, GPU 클럭·전력, 주요 유닛 상태, 컨테이너 재시작 정책, KV 캐시 크기, health
- 확인 문자열 입력(
POWEROFF) - 서빙 워치독 disarm → 4랭크 서빙 컨테이너 제거(head 먼저 — API부터 닫는다)
- 워커 순차 정지:
sync→systemctl poweroff→ Tailscale·RoCE 양쪽 ping이 모두 사라질 때까지 대기(상한 240초) → 20초 추가 대기 → 다음 노드. 상한 초과 시 이후 노드는 건드리지 않고 중단 - node1 마지막 정지 — 워커가 마운트하고 있는 NFS 핫티어 서버이기 때문
--dry-run(점검·기준선만), --force(진행 중 작업 무시), --rearm(node1 끄기 직전 워치독 재무장 → 부팅 후 서빙 자동 기동) 옵션을 뒀다. 이번엔 재무장하지 않고 부팅 후 점검을 먼저 했다.
타임라인 (KST)
| 시각 | 이벤트 |
|---|---|
| 08:56 | 1차 실행 — 확인 단계에서 “사용자 취소"로 종료(아래 발견 4). 아무것도 꺼지지 않음 |
| 08:57:49 | 2차 실행 시작, 프리체크·기준선 통과 |
| 08:57:56 | 워치독 disarm |
| 08:58:20 | 4랭크 서빙 컨테이너 제거 완료 |
| 08:58:20 → 08:58:38 | node4 poweroff → 응답 소실 18초 |
| 08:58:58 → 08:59:04 | node3 poweroff → 응답 소실 6초 |
| 08:59:24 → 08:59:34 | node2 poweroff → 응답 소실 10초 |
| 08:59:54 | node1 poweroff |
| (플러그 OFF → ON) | |
| 12:08:52~56 | 4노드 부팅(거의 동시에 전원 투입) |
| 12:11 | 워치독 재무장, 서빙 기동 명령 |
| 12:20:58 | KV 캐시 확정 |
| 12:25 | API health 200 |
정지 자체는 2분이면 끝난다. 복구 시간의 대부분은 서빙 부팅(가중치 로드 + TileLang JIT 컴파일 + CUDA graph 캡처)이다.
부팅 후 점검 — 대부분 그대로 돌아왔다
| 항목 | 결과 |
|---|---|
| 커널 | 정지 전과 동일(head만 한 단계 낮은 커널인 기존 불일치도 그대로) |
| NFS 핫티어 | 워커 3대 모두 자동 마운트. 4대가 동시에 부팅해 head의 NFS export가 늦게 떴는데도, 이전 사고 후 넣어 둔 마운트 재시도 유닛이 처리했다. 실제 재부팅에서 처음 검증 |
| RoCE | 8링크 전부 200G, MTU 9000 |
| 관리망 IP | 전 노드 정지 전과 동일(공유기를 켜 둔 덕에 DHCP 임대 유지) |
| 서빙 | KV 캐시 2,681,851 토큰(정지 전과 동일), 부팅 14분 |
| 커널 로그 | Xid 0, OOM 0. 한 워커에서 부팅 중 KV 할당 구간 NVRM NO_MEMORY 4건 — 이 클러스터에서 부팅 중 정상적으로 보이는 패턴 |
발견 1 — 4대 중 3대는 부팅이 끝나지 않았다
GPU 클럭 캡을 확인하려고 동시 16요청 부하를 걸고 4노드 클럭을 샘플링했다.
| 노드 | GPU 클럭(부하 중) | clocks_event_reasons |
|---|---|---|
| node1 | 1982 MHz | 0x0 |
| node2 | 1768~1774 MHz | 0x0 |
| node3 | 1989 MHz | 0x0 |
| node4 | 1976 MHz | 0x0 |
node2만 낮다. 스로틀 사유가 0이므로 열·전력 제한이 아니라 설정으로 고정된 것이다. 정지 전 기준선에서 node2는 1969MHz였다. 원인을 따라가다 더 큰 문제가 나왔다.
node1: is-system-running=starting multi-user.target=inactive
node2: is-system-running=running multi-user.target=active
node3: is-system-running=starting multi-user.target=inactive
node4: is-system-running=starting multi-user.target=inactive
부팅 1시간이 지났는데 세 노드는 아직 “부팅 중"이다. 대기 중인 작업 목록에서 실제로 돌고 있는 건 하나뿐이었다.
plymouth-quit-wait.service start running (12:09:03부터 1시간 넘게)
plymouth는 부팅 스플래시 화면이다. 노드의 기본 타깃이 graphical.target이고 GNOME 디스플레이 매니저가 뜨는데, 모니터 없는 서버에서 스플래시 → 디스플레이 매니저 인계가 끝나지 않아 plymouth-quit-wait가 계속 기다린다. 이게 끝나야 multi-user.target에 도달한다. node2만 23초 만에 인계가 끝났다. 왜 node2만 달랐는지는 확인하지 못했다.
이 상태가 무서운 이유는 겉으로 멀쩡하다는 것이다. 네트워크, SSH, Tailscale, NFS 마운트, 도커, 서빙은 전부 multi-user.target 이전 단계에서 뜬다. 모니터링도 다 초록이다. 다만 After=multi-user.target으로 정의된 유닛은 영원히 실행되지 않는다. 로컬 콘솔 로그인(getty)도 안 뜬다.
head의 이전 부팅 기록을 보니 같은 일이 이미 있었다. 9월 10일 부팅 후 plymouth-quit-wait가 9월 18일에야 끝났고, 바로 그 순간 대기하던 클럭 유닛들이 줄줄이 실행됐다. 8일 동안 아무도 몰랐다.
클럭 설정이 네 겹이었다
부팅이 끝나느냐에 따라 실행 여부가 갈리는 유닛이 섞여 있어서, 노드마다 결과가 달라졌다.
| 메커니즘 | 설정 | 위치 | 실행 조건 | 이번 부팅 결과 |
|---|---|---|---|---|
gpu-clock-cap | 0~2000MHz | 워커 3대 | 드라이버 로드 후 | 3대 모두 실행 |
set-gpu-clock | 0~2000MHz | head | After=multi-user.target | 대기 중(미실행) |
| 5분 주기 사용자 타이머 | 0~2000MHz | head | 부팅 2분 후부터 | 실행 — head 캡은 이것 덕분 |
nvidia-clamp | 1800MHz 고정 | 4대 전부 | After=multi-user.target | node2만 실행, 나머지 대기 |
2000MHz 상한은 열·전력 캠페인에서 “무손실로 전력 45% 절감"을 확인한 설정이다. 1800MHz 고정 유닛은 이후 열 완화 목적으로 추가된 것인데, 최소=최대로 묶어 유휴 시에도 클럭이 내려가지 않는다. 두 설정이 공존하면 결과는 “마지막으로 실행된 것"이 되고, 그 순서는 부팅이 언제 끝나느냐에 달려 있다.
영향은 두 가지다.
- 성능: TP=4 디코드 스텝은 가장 느린 랭크에 맞춰진다. node2 하나가 1768MHz면 나머지 세 노드의 여유는 버려진다.
- 예측 불가능성: head에서
plymouth가 며칠 뒤에라도 끝나면 그 순간 1800MHz 고정이 실행되고, 5분 뒤 타이머가 다시 2000MHz 상한으로 되돌린다. 같은 노드의 클럭이 운영 중에 조용히 바뀔 수 있다.
이 문제는 가동 중 점검으로는 보이지 않는다. 재부팅이라는 사건이 있어야만 발현된다. 이 클러스터에서 부팅 시 클럭 캡이 안 걸린 사례는 이번이 두 번째다(이전엔 워커 유닛의 의존 관계 문제였다).
발견 2 — CPU 캡은 쓰이고 있지만 아무것도 막지 않는다
CPU도 확인했다. sysfs에는 전 노드 모든 코어가 이렇게 보인다.
scaling_driver = cppc_cpufreq
scaling_max_freq = 2000000 (2.0 GHz)
cpuinfo_max_freq = 3900000 (3.9 GHz, X925 코어)
auto_select = 1
max_perf = 4004000
head의 사용자 타이머가 10분마다 전 노드 코어의 scaling_max_freq를 2.0GHz로 다시 쓰고 있었다. 그런데 실제 코어 클럭은 달랐다.
| 조건 | 측정 코어 | 실측 평균 클럭 |
|---|---|---|
| 서빙 부하 중(vLLM 워커·엔진 스레드) | X925 클러스터 | 3.4 ~ 3.8 GHz |
| 단일 코어 busy loop 3초 | X925 코어 2개 | 3.89 / 3.91 GHz |
cpuinfo_avg_freq(하드웨어 활동 카운터 기반) 기준이다. auto_select = 1은 CPPC 자율 모드 — 성능 수준을 OS가 아니라 펌웨어가 min_perf~max_perf 범위에서 고른다. 이 모드에서는 scaling_max_freq가 사실상 무시된다. CPU 캡은 켜져 있는 것처럼 보이지만 걸려 있지 않다.
여기엔 운영 의도 문제도 있다. 열·전력 캠페인의 결론은 “CPU 캡은 속도만 잃는 함정, 걸지 않는다"였는데, 그 뒤에 CPU 캡을 유지하는 타이머가 추가됐다. 지금은 효과가 없어서 결과적으로 캠페인 결론대로 돌고 있지만, 누군가 자율 모드를 끄는 순간 CPU 캡이 실제로 걸린다. 의도를 하나로 정해야 한다.
발견 3 — 재시작 정책을 믿으면 안 된다
head의 Prometheus 컨테이너만 부팅 후 떠 있지 않았다. 로그를 보면 정지 시 SIGTERM을 받고 graceful 종료를 시작했지만 도커의 기본 대기(10초) 안에 끝나지 않아 SIGKILL(종료 코드 137)로 끝났다. 재시작 정책은 unless-stopped였는데 부팅 후 재시작 시도 자체가 없었다. 왜 그렇게 판정됐는지는 확정하지 못했다.
수동 docker start 후 WAL 재생까지 정상이었고 데이터 손실은 없었다. 대책은 두 방향이다.
- 정지 스크립트에서 poweroff 전에
docker stop -t 60 prometheus명시 - 부팅 후 점검 항목에 “떠 있어야 할 컨테이너 목록” 포함
반대로 벤치마크용으로 11일째 떠 있던 컨테이너(restart=no)는 당연히 돌아오지 않았다. 재부팅은 이런 “임시로 띄워 놓고 잊은 것"을 정리하는 효과도 있다.
발견 4 — 비대화형 실행과 stdin
스크립트는 원래 사람이 터미널에서 치도록 만들었다. 원격에서 비밀번호와 확인 문자열을 stdin으로 넣어 돌렸더니, 기준선을 기록하는 도중 로컬 명령 하나가 stdin을 읽어 POWEROFF를 삼켰다. 확인 단계는 빈 입력을 받고 “사용자 취소"로 종료했다.
결과적으로는 설계가 맞게 동작했다. 확인 게이트가 파괴적 단계 앞에 있었고, 입력이 틀리면 진행이 아니라 중단으로 실패했다. 수정은 stdin이 필요 없는 모든 명령(SSH 포함)에 </dev/null을 붙이는 것.
성능 한 줄
복구 직후 코드 프롬프트(completions, 400토큰, temperature 0) 단일 요청 두 번: 49.6 / 50.3 tok/s. 벤치마크 글의 한국어 실프롬프트 수치와는 측정 방식이 달라 직접 비교할 수 없고, 이번엔 정지 전 같은 조건의 측정값도 없다. node2의 1768MHz 고정이 이 수치에 얼마나 영향을 줬는지는 캡을 바로잡은 뒤 같은 조건으로 재측정해야 알 수 있다.
시사점
- “서빙이 떴다"는 “부팅이 끝났다"가 아니다. 이번 가장 큰 발견이다. 서빙·네트워크·스토리지는 전부 부팅 중간 단계에서 뜨기 때문에, 부팅이 영영 끝나지 않아도 모든 대시보드가 초록이다. 부팅 후 점검 첫 줄은
systemctl is-system-running이어야 하고, 이 값이running이 아니면 알림을 보내야 한다. - 헤드리스 서버는 헤드리스로 부팅시킨다. 모니터 없는 GPU 노드의 기본 타깃이
graphical.target이고 디스플레이 매니저와 부팅 스플래시가 떠 있었다. 기본 타깃을multi-user.target으로 바꾸거나 스플래시를 끄면 이 계열의 멈춤이 원천적으로 사라진다. - 하드웨어 설정 유닛은
After=multi-user.target에 걸지 않는다. 부팅의 “끝"에 의존하면 부팅이 안 끝날 때 영영 실행되지 않는다. 실제 의존 대상(nvidia-persistenced.service등)을 지정한 워커의 2000MHz 캡 유닛은 부팅이 끝나지 않은 노드에서도 정확히 실행됐다. - 같은 대상을 설정하는 메커니즘은 하나만. GPU 클럭 하나에 시스템 유닛 두 개, 사용자 타이머 하나, 고정 클럭 유닛 하나가 겹쳐 있었다. 누가(사람 / 어떤 에이전트) 어떤 의도로 추가했는지 유닛 파일에 남기고, 새로 추가할 때는 기존 것을 먼저 찾는다.
- sysfs에 써진 값과 하드웨어가 따르는 값은 다를 수 있다. CPU
scaling_max_freq가 대표적이다. 캡은 부하를 건 실측으로 확인한다(GPU는clocks_event_reasons까지 같이 본다). - 재시작 정책은 보장이 아니다. “떠 있어야 할 것"의 목록을 명시하고 부팅 후 자동으로 대조한다. 정지 시 느리게 종료되는 컨테이너는 대기 시간을 늘려 명시적으로 멈춘다.
- 재부팅 연습은 정기적으로 할 가치가 있다. 18일 연속 가동 동안 쌓인 설정은 가동 중 어떤 점검으로도 드러나지 않았다. 이번 발견 중 1~3번은 모두 재부팅이 있어야만 보이는 것이다.
- 복구 시간의 대부분은 인프라가 아니라 모델이다. 정지 2분, OS 부팅 수십 초, 서빙 14분. MTTR을 줄이려면 JIT 캐시 유지나 가중치 로드 경로를 봐야 한다.
- 전원 복구 경로는 노드와 독립이어야 한다. BIOS 자동 켜짐 + 노드별 플러그 + 노드에 의존하지 않는 플러그 제어. 이 조합이면 사람이 현장에 없어도 정지·재기동 사이클을 돌릴 수 있다. 다만 이제 짧은 정전에도 노드가 스스로 켜지므로, 워치독을 무장해 둘지(자동 서빙) 해제해 둘지(점검 후 수동 기동)를 의식적으로 정해 둬야 한다.
- 안전 게이트는 “틀리면 멈추는” 방향으로. 프리체크는 파괴적 단계 전에 전부, 확인 입력이 기대와 다르면 중단. 이번에 스크립트 버그가 있었는데도 사고가 나지 않은 이유다.
다음 할 일
- 기본 부팅 타깃을
multi-user.target으로 변경(또는 부팅 스플래시 비활성화) → 다음 재부팅에서 4대 모두running도달 확인 - GPU 클럭 메커니즘을 2000MHz 상한 하나로 통일: 1800MHz 고정 유닛 제거, head 시스템 유닛의 의존 관계 수정, node2 클럭 원복 후 같은 조건으로 재측정
- CPU 캡 의도 결정: 캠페인 결론대로 두지 않을 거면 10분 주기 타이머 제거, 걸 거면 자율 모드에서 통하는 방식으로
- 부팅 후 점검 스크립트:
is-system-running, 커널, 마운트, RoCE, GPU 클럭(부하 중), 필수 컨테이너 - 정지 스크립트에 느린 컨테이너 명시 정지 추가
- head 커널을 워커와 통일할지 결정(현재 한 단계 차이), 관리망 DHCP 고정 할당