운영 문서에서 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.
  • 발견
    1. 부팅이 끝나지 않았다: 4대 중 3대가 부팅 1시간이 지나도 systemctl is-system-running = starting. 그래픽 부팅 스플래시를 내리는 plymouth-quit-wait가 멈춰 multi-user.target에 도달하지 못했다. 서빙·SSH·NFS는 그 전에 다 떠서 겉으로는 정상이었다. head는 직전 부팅에서도 8일간 같은 상태였던 기록이 있다.
    2. 그 결과 GPU 클럭이 노드마다 달라졌다: After=multi-user.target로 걸린 클럭 유닛은 부팅이 끝난 노드에서만 실행된다. 유일하게 부팅이 끝난 node2에서만 1800MHz 고정 유닛이 돌아 node2 GPU가 1768MHz로 묶였다(나머지는 2000MHz 캡). TP=4는 가장 느린 랭크에 맞춰 돈다. head의 2000MHz 캡도 시스템 유닛은 대기 중이었고, 5분마다 도는 사용자 타이머가 대신 걸고 있었다.
    3. CPU 캡은 켜져 있지만 효과가 없다: 10분마다 전 노드 scaling_max_freq를 2.0GHz로 쓰는 타이머가 있지만, 코어는 실측 3.9GHz로 돈다. CPU가 펌웨어 자율 모드(CPPC autonomous)라 이 값을 무시한다.
    4. Prometheus 미기동: 종료 시 SIGTERM 후 10초 안에 안 끝나 SIGKILL(137) → unless-stopped인데도 부팅 후 자동 시작되지 않았다.
    5. 스크립트 자체 버그: 비대화형 실행 시 확인 입력을 중간 명령이 먼저 먹어 버렸다. 확인 게이트가 “취소"로 실패해 아무것도 꺼지지 않았다.

왜 했나

클러스터는 가정에 있고, 원격 접속은 노드 위의 Tailscale에 의존한다. 노드가 꺼지면 원격으로 켤 수단이 없다. 그래서 두 가지를 준비했다.

  • 노드 BIOS의 AC 전원 복구 시 자동 켜짐 활성화 → 전원만 들어오면 부팅한다.
  • 노드별 IoT 플러그 → 원격으로 전원을 끊었다 넣을 수 있다. 플러그 제어 경로는 노드에 의존하지 않아야 한다(노드 위에서 도는 홈 자동화로 플러그를 제어하면 노드가 꺼진 순간 아무것도 못 한다).

여기서 중요한 구분: 플러그 OFF는 반드시 OS 정상 종료 이후다. 켜진 상태로 전원을 끊는 건 여전히 금지(NVMe 쓰기 중이면 특히). 플러그는 “끄는 도구"가 아니라 “다시 켜는 도구"다.

이번 범위는 GPU 노드 4대만. NAS·스위치·공유기는 켜 둔 채였다.

정지 스크립트 설계

node1에서 실행하고, 워커는 SSH로 제어한다. 원칙은 과거 워치독 사고에서 배운 것 하나다: 파괴적 단계 전에 모든 프리체크를 끝낸다.

  1. 실행 위치(node1) 확인, sudo 비밀번호 1회 입력(메모리에만 보관)
  2. 프리체크 — 하나라도 실패하면 아무것도 끄지 않고 종료
    • 워커 3대 SSH·sudo 인증
    • 진행 중 쓰기 작업 없음(HF 다운로드, rsync, docker load/save, apt/dpkg)
  3. 기준선 기록 — 커널, NFS 마운트, IP, RoCE 링크 속도, GPU 클럭·전력, 주요 유닛 상태, 컨테이너 재시작 정책, KV 캐시 크기, health
  4. 확인 문자열 입력(POWEROFF)
  5. 서빙 워치독 disarm → 4랭크 서빙 컨테이너 제거(head 먼저 — API부터 닫는다)
  6. 워커 순차 정지: sync → systemctl poweroff → Tailscale·RoCE 양쪽 ping이 모두 사라질 때까지 대기(상한 240초) → 20초 추가 대기 → 다음 노드. 상한 초과 시 이후 노드는 건드리지 않고 중단
  7. node1 마지막 정지 — 워커가 마운트하고 있는 NFS 핫티어 서버이기 때문

--dry-run(점검·기준선만), --force(진행 중 작업 무시), --rearm(node1 끄기 직전 워치독 재무장 → 부팅 후 서빙 자동 기동) 옵션을 뒀다. 이번엔 재무장하지 않고 부팅 후 점검을 먼저 했다.

타임라인 (KST)

시각이벤트
08:561차 실행 — 확인 단계에서 “사용자 취소"로 종료(아래 발견 4). 아무것도 꺼지지 않음
08:57:492차 실행 시작, 프리체크·기준선 통과
08:57:56워치독 disarm
08:58:204랭크 서빙 컨테이너 제거 완료
08:58:20 → 08:58:38node4 poweroff → 응답 소실 18초
08:58:58 → 08:59:04node3 poweroff → 응답 소실 6초
08:59:24 → 08:59:34node2 poweroff → 응답 소실 10초
08:59:54node1 poweroff
(플러그 OFF → ON)
12:08:52~564노드 부팅(거의 동시에 전원 투입)
12:11워치독 재무장, 서빙 기동 명령
12:20:58KV 캐시 확정
12:25API health 200

정지 자체는 2분이면 끝난다. 복구 시간의 대부분은 서빙 부팅(가중치 로드 + TileLang JIT 컴파일 + CUDA graph 캡처)이다.

부팅 후 점검 — 대부분 그대로 돌아왔다

항목결과
커널정지 전과 동일(head만 한 단계 낮은 커널인 기존 불일치도 그대로)
NFS 핫티어워커 3대 모두 자동 마운트. 4대가 동시에 부팅해 head의 NFS export가 늦게 떴는데도, 이전 사고 후 넣어 둔 마운트 재시도 유닛이 처리했다. 실제 재부팅에서 처음 검증
RoCE8링크 전부 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
node11982 MHz0x0
node21768~1774 MHz0x0
node31989 MHz0x0
node41976 MHz0x0

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-cap0~2000MHz워커 3대드라이버 로드 후3대 모두 실행
set-gpu-clock0~2000MHzheadAfter=multi-user.target대기 중(미실행)
5분 주기 사용자 타이머0~2000MHzhead부팅 2분 후부터실행 — head 캡은 이것 덕분
nvidia-clamp1800MHz 고정4대 전부After=multi-user.targetnode2만 실행, 나머지 대기

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 고정이 이 수치에 얼마나 영향을 줬는지는 캡을 바로잡은 뒤 같은 조건으로 재측정해야 알 수 있다.

시사점

  1. “서빙이 떴다"는 “부팅이 끝났다"가 아니다. 이번 가장 큰 발견이다. 서빙·네트워크·스토리지는 전부 부팅 중간 단계에서 뜨기 때문에, 부팅이 영영 끝나지 않아도 모든 대시보드가 초록이다. 부팅 후 점검 첫 줄은 systemctl is-system-running이어야 하고, 이 값이 running이 아니면 알림을 보내야 한다.
  2. 헤드리스 서버는 헤드리스로 부팅시킨다. 모니터 없는 GPU 노드의 기본 타깃이 graphical.target이고 디스플레이 매니저와 부팅 스플래시가 떠 있었다. 기본 타깃을 multi-user.target으로 바꾸거나 스플래시를 끄면 이 계열의 멈춤이 원천적으로 사라진다.
  3. 하드웨어 설정 유닛은 After=multi-user.target에 걸지 않는다. 부팅의 “끝"에 의존하면 부팅이 안 끝날 때 영영 실행되지 않는다. 실제 의존 대상(nvidia-persistenced.service 등)을 지정한 워커의 2000MHz 캡 유닛은 부팅이 끝나지 않은 노드에서도 정확히 실행됐다.
  4. 같은 대상을 설정하는 메커니즘은 하나만. GPU 클럭 하나에 시스템 유닛 두 개, 사용자 타이머 하나, 고정 클럭 유닛 하나가 겹쳐 있었다. 누가(사람 / 어떤 에이전트) 어떤 의도로 추가했는지 유닛 파일에 남기고, 새로 추가할 때는 기존 것을 먼저 찾는다.
  5. sysfs에 써진 값과 하드웨어가 따르는 값은 다를 수 있다. CPU scaling_max_freq가 대표적이다. 캡은 부하를 건 실측으로 확인한다(GPU는 clocks_event_reasons까지 같이 본다).
  6. 재시작 정책은 보장이 아니다. “떠 있어야 할 것"의 목록을 명시하고 부팅 후 자동으로 대조한다. 정지 시 느리게 종료되는 컨테이너는 대기 시간을 늘려 명시적으로 멈춘다.
  7. 재부팅 연습은 정기적으로 할 가치가 있다. 18일 연속 가동 동안 쌓인 설정은 가동 중 어떤 점검으로도 드러나지 않았다. 이번 발견 중 1~3번은 모두 재부팅이 있어야만 보이는 것이다.
  8. 복구 시간의 대부분은 인프라가 아니라 모델이다. 정지 2분, OS 부팅 수십 초, 서빙 14분. MTTR을 줄이려면 JIT 캐시 유지나 가중치 로드 경로를 봐야 한다.
  9. 전원 복구 경로는 노드와 독립이어야 한다. BIOS 자동 켜짐 + 노드별 플러그 + 노드에 의존하지 않는 플러그 제어. 이 조합이면 사람이 현장에 없어도 정지·재기동 사이클을 돌릴 수 있다. 다만 이제 짧은 정전에도 노드가 스스로 켜지므로, 워치독을 무장해 둘지(자동 서빙) 해제해 둘지(점검 후 수동 기동)를 의식적으로 정해 둬야 한다.
  10. 안전 게이트는 “틀리면 멈추는” 방향으로. 프리체크는 파괴적 단계 전에 전부, 확인 입력이 기대와 다르면 중단. 이번에 스크립트 버그가 있었는데도 사고가 나지 않은 이유다.

다음 할 일

  • 기본 부팅 타깃을 multi-user.target으로 변경(또는 부팅 스플래시 비활성화) → 다음 재부팅에서 4대 모두 running 도달 확인
  • GPU 클럭 메커니즘을 2000MHz 상한 하나로 통일: 1800MHz 고정 유닛 제거, head 시스템 유닛의 의존 관계 수정, node2 클럭 원복 후 같은 조건으로 재측정
  • CPU 캡 의도 결정: 캠페인 결론대로 두지 않을 거면 10분 주기 타이머 제거, 걸 거면 자율 모드에서 통하는 방식으로
  • 부팅 후 점검 스크립트: is-system-running, 커널, 마운트, RoCE, GPU 클럭(부하 중), 필수 컨테이너
  • 정지 스크립트에 느린 컨테이너 명시 정지 추가
  • head 커널을 워커와 통일할지 결정(현재 한 단계 차이), 관리망 DHCP 고정 할당