아무도 안 볼 때도 돌아가게 — 백업 복구 훈련, 휴대폰 알림, 그리고 재부팅이 반으로 줄인 KV cache
이 노드에는 백업이 하나도 없었습니다. 백업을 만들고, 클라우드에서 실제로 복구해 보고, 일부러 장애를 내서 휴대폰 알림을 확인하고, 재부팅을 했습니다. 재부팅 후 vLLM의 KV cache가 절반이 되어 있었습니다.
이 글의 독자: 지난 글에서 이어집니다. 리눅스 운영 경험이 많지 않은 개발자를 기준으로 썼습니다. 저도 우분투는 이 프로젝트로 처음 제대로 써 봤습니다.
들어가며 — 목록을 적어 보니
기능이 쌓이자, 노드를 “운영”하고 있는지 스스로 점검해 봤습니다. 목록은 짧고 아팠습니다.
- 백업: 없음. 대화 기록, 올린 문서, 사용자 계정 전부 디스크 하나에만 있음
- 알림: 없음. 무언가 죽어도 제가 브라우저를 열어 볼 때까지 모름
- 수집 안 되는 지표: 리랭커, 디스크, 메모리, 컨테이너 상태
- 로그: 컨테이너 11개 전부 크기 제한 없음. 언젠가 디스크를 다 채움
- 마지막 재부팅: 리랭커를 추가하기 전
목표는 한 문장으로 정했습니다.
몇 주 동안 아무도 안 봐도 돌아간다. 무언가 고장 나거나 데이터가 위험하면, 몇 분 안에 내 휴대폰이 안다. 복구 절차는 전부 적혀 있고, 한 번씩은 실제로 해 봤다.
마지막 문장이 핵심입니다. 해 보지 않은 복구 절차는 절차가 아니라 희망입니다.
Step 1 — 백업부터: 유일하게 되돌릴 수 없는 위험 (결정 1)
순서는 위험의 크기로 정했습니다. 알림이 없으면 늦게 알 뿐이지만, 백업이 없으면 영원히 잃습니다.
밤 03:30, 로컬 백업. PostgreSQL 이미지로 백업 컨테이너를 하나 띄우고(서버와 같은 버전의 pg_dump를 쓰려고), 모든 데이터베이스를 덤프합니다.
- 쓰는 동안은
.partial이름으로, 완전히 끝나야 진짜 이름으로 바꿉니다. 중간에 끊긴 파일을 “최신 백업”으로 착각하지 않게. - 14일치 보관, 파일은 소유자만 읽을 수 있게.
- 첫 백업: 6.8MB, 1초.
밤 04:00, 클라우드 복사본. 집에 불이 나면 로컬 백업도 같이 탑니다. restic으로 노드에서 암호화한 뒤 Backblaze B2 버킷에 올립니다. 클라우드 업체는 암호화된 덩어리만 봅니다. 일별 14개, 주별 8개, 월별 12개를 남기고, 올릴 때마다 저장소 무결성 검사(restic check)를 돌립니다.
암호화 비밀번호는 비밀번호 관리자에 따로 보관합니다. 노드가 통째로 사라졌을 때 필요한 비밀번호가 그 노드에만 있다면, 백업은 열 수 없는 상자입니다.
Step 2 — 복구 훈련: 백업은 복구해 봐야 백업이다
백업 파일이 생겼다고 끝이 아닙니다. 그래서 복구 훈련 스크립트를 만들었습니다.
- 버리는 용도의 PostgreSQL을 하나 새로 띄운다 (운영 DB는 건드리지 않음)
- 백업을 거기에 복구한다
- 테이블 11개의 행 개수를 운영 DB와 비교한다
- 복구된 문서 조각으로 벡터 검색이 실제로 되는지 확인한다
- 버린다
로컬 백업으로 3.8초 만에 통과했습니다. 그다음은 클라우드에서 했습니다. 스냅샷을 내려받아 복호화하고(3초), 로컬 덤프와 바이트 단위로 같은지 비교하고, 같은 훈련을 돌렸습니다. 통과.
이제 “백업이 있다”가 아니라 “클라우드에서 복구해서 행 개수까지 맞는 걸 확인했다”고 말할 수 있습니다.
Step 3 — 알림: 내 휴대폰으로 (결정 2)
알림은 텔레그램 봇으로 보냅니다. 노드에서 밖으로 나가는 HTTPS 요청만 있으면 되고, 들어오는 포트를 열 필요가 없습니다.
알림 규칙은 Grafana에서 관리하고, 전부 코드로 저장소에 있습니다. 화면에서 클릭해서 만든 규칙은 언젠가 사라지거나, 왜 그렇게 설정했는지 아무도 모르게 됩니다. 처음 13개를 만들었습니다(지금은 16개).
| 분류 | 규칙 |
|---|---|
| 서비스 | 지표 수집이 끊김, 컨테이너가 꺼짐, 재시작 반복, 메모리 초과로 강제 종료 |
| 품질 | 게이트웨이 에러율, 첫 글자까지 p95 > 10초, 리랭커 대신 임베딩 순서로 답한 횟수, 문서 처리가 멈춤 |
| 하드웨어 | GPU 메모리, GPU 온도, 디스크, 호스트 메모리 |
| 데이터 | 백업이 너무 오래됨 (지표가 아예 사라져도 알림) |
마지막 줄의 괄호가 중요합니다. 백업 스크립트가 고장 나서 “마지막 성공 시각” 지표를 아예 안 쓰게 되면, 많은 알림 규칙은 “데이터 없음 = 문제없음”으로 처리합니다. 고장 난 백업이 조용한 백업처럼 보이는 겁니다. 그래서 이 규칙은 지표가 사라진 것도 알림으로 취급합니다.
그리고 모든 규칙에는 런북 항목이 하나씩 있습니다. 새벽에 알림을 받은 제가 “이게 무슨 뜻이고, 무엇부터 보고, 어떻게 고치는지”를 그 자리에서 읽을 수 있게.
일부러 고장 내기
규칙을 만들었으면 울리는지 봐야 합니다. 일부러 고장을 냈습니다.
- Tika 컨테이너와 GPU 지표 수집기를 멈춤
- 로컬 백업 시각을 이틀 전으로 조작
결과: “백업이 너무 오래됨”이 30초 만에, “서비스 다운”과 “컨테이너 꺼짐” 두 개가 3.5분 안에 텔레그램으로 왔습니다. 원래대로 돌리자 1분 안에 전부 “해결됨”이 왔습니다.
Step 4 — 재부팅 훈련, 그리고 절반이 된 KV cache
마지막은 진짜 재부팅이었습니다. 정전이나 커널 업데이트 후에도 사람 손 없이 다 살아나야 합니다.
1
sudo reboot
다 살아났습니다. 모든 서비스가 healthy, 지표 수집 대상 8개 전부 정상, 남은 알림 없음, RAG 질문에 리랭커가 정렬한 출처까지. 그런데 확인하는 과정에서 두 가지를 발견했습니다.
발견 1: 시작 순서
리랭커와 채팅 vLLM이 동시에 시작하면, 둘이 GPU 메모리를 동시에 잡으려다 한쪽이 실패할 수 있습니다. Docker Compose의 depends_on은 compose up을 칠 때의 순서만 정해 주고, 재부팅 후 Docker가 컨테이너를 되살릴 때는 순서를 보장하지 않습니다. 그래서 리랭커의 시작 스크립트가 채팅 vLLM의 /health가 응답할 때까지 직접 기다리게 바꿨습니다. 실제로 5초 간격으로 16번 기다렸습니다.
발견 2: KV cache가 18,139 토큰?
vLLM은 시작할 때 VRAM을 측정해서 KV cache 크기를 정합니다. 평소에는 37,449 토큰이었습니다. 그런데 어떤 시작에서는 18,139 토큰, 거의 절반이었습니다. 동시에 들고 있을 수 있는 대화량이 절반이 된다는 뜻입니다.
처음엔 시작 순서 탓인 줄 알았습니다. 아니었습니다. 차이는 컴파일 캐시였습니다.
- vLLM은 처음 시작할 때 모델을 GPU용으로 컴파일하고, 결과를 캐시에 저장합니다.
- 새 컨테이너에는 그 캐시가 없어서 컴파일을 다시 합니다.
- 그런데 vLLM은 “모델을 돌리는 데 필요한 메모리”를 재는 시점에, 컴파일 자체가 쓰는 메모리까지 같이 재 버립니다(1.32 GiB vs 평소 0.25 GiB).
- 그만큼 KV cache에 줄 몫이 줄어듭니다.
캐시를 볼륨으로 빼서 컨테이너가 새로 만들어져도 남게 했습니다. 이제 모든 시작이 5초(이전 63초)이고, 매번 37k 토큰을 받습니다.
그리고 그 상태로 부하를 걸었습니다. 3,000토큰짜리 요청 16개를 동시에. KV cache 사용률 94.5%, GPU 메모리 최고 7.5 / 8GB, 메모리 초과 없음.
재부팅 훈련의 목적은 “다시 켜지는지”였는데, 진짜 수확은 “켜질 때마다 같은 상태로 켜지는지”였습니다. 같은 설정, 다른 결과는 숨은 상태(여기서는 캐시)가 있다는 신호입니다.
Step 5 — 작은 위생들
- 로그 크기 제한. Compose 파일 맨 위에 공통 설정 하나(
json-file, 10MB × 3개)를 두고 16개 서비스 전부에 붙였습니다. 데몬 설정이 아니라 Compose에 둔 이유는 저장소에 남고, sudo가 필요 없어서입니다. - “51GB 회수 가능”. Docker가 이렇게 알려 줘서 깜짝 놀랐는데, 지금 안 돌고 있는 vLLM과 Ollama 이미지였습니다(필요한 것). 진짜 안 쓰는 것만 지웠습니다(1.4GB). 정기적인 자동 정리는 두지 않았습니다. 디스크 85% 알림이 있으니, 울리면 그때 손으로.
- Keycloak 시작 25초 → 2.2초. 단일 노드인데 클러스터 합류를 20초씩 기다리고 있었습니다. 최적화 빌드 + 로컬 캐시로 바꿨습니다.
- 런북. 알림별 의미 / 먼저 볼 것 / 고치는 법, 엔진 전환, 백업 복구(훈련과 실전), 키 폐기, 그리고 “데스크톱을 써야 할 땐 리랭커부터 끌 것”.
Step 6 — 그 뒤에 실제로 있었던 일
이 단계를 끝낸 뒤, 저는 노드를 일부러 끄기도 했습니다. 그러면서 운영 장치들이 실제로 일을 했습니다.
- 재부팅 직후 DNS가 아직 안 올라온 상태에서 돈 데이터 수집 작업들이 실패했고, 알림이 그걸 잡았습니다. 이제 실패한 작업은 하루를 기다리지 않고 한 시간 뒤에 다시 시도합니다.
- 노드가 꺼져 있던 밤에는 백업이 그냥 건너뛰어졌습니다. 03:30에 꺼져 있었으니 당연한 일인데, 백업 시각 지표를 보고서야 알았습니다. 이제 켜질 때 마지막 백업이 하루보다 오래됐으면 바로 하나를 뜨고, 클라우드 복사도 5분 뒤에 따라갑니다.
두 경우 모두 처음 설계할 때는 생각하지 못한 상황이었습니다. 그리고 둘 다 기록(실행 이력과 백업 시각 지표)이 알려 줬습니다. 운영 장치의 가치는 예상한 고장을 막는 것보다, 예상하지 못한 고장을 보이게 만드는 데 있었습니다.
정리 — 오늘 얻은 것
만든 것:
- 밤마다 로컬 백업 + 암호화된 클라우드 복사본, 둘 다 실제로 복구해 봄
- 코드로 관리하는 알림 → 텔레그램, 규칙마다 런북, 일부러 고장 내서 확인
- 재부팅 후 사람 손 없이 전부 정상, 시작할 때마다 같은 KV cache
배운 것:
- 해 보지 않은 복구 절차는 희망이다
- “데이터 없음”을 “문제없음”으로 읽는 알림은 고장 난 백업을 숨긴다
- 같은 설정에서 다른 결과가 나오면, 숨은 상태를 찾을 것
다음 글
이제 플랫폼은 혼자 돌아갑니다. 그럼 그 위에서 무엇을 돌릴까요?
다음 글은 이 플랫폼 위에 올린 첫 번째 애플리케이션 이야기입니다. 매일 뉴스와 공시를 모아서 “어느 회사가 어느 회사와 무슨 관계인지”를 출처가 달린 그래프로 만드는 이슈 링커. 그리고 4B 모델의 정확도가 기준에 못 미쳤을 때 내린 결정, 마지막으로 이걸 가족에게 열어 준 이야기까지 적겠습니다.