1. 개요
백업 구조가 이랬다. CronJob 이 매일 pg_dump 를 떠서 Longhorn PVC 에 둔다. Longhorn RecurringJob 이 그 PVC 를 매일 외부 NFS 로 백업한다. 매니페스트 주석에 “외부 사본이 한 벌 더 나간다” 고 적혀 있었다. 실측하니 외부 사본은 0벌 이었다. 이 글은 왜 그랬는지, 어떻게 고쳤는지, 그리고 백업이라 부르려면 무엇을 확인해야 하는지다.
2. 핵심 내용
2-1. RecurringJob 은 attached 볼륨만 본다
Longhorn 의 RecurringJob(snapshot/backup)은 볼륨이 attached 상태 일 때만 실행한다. detached 볼륨은 건너뛴다. 문서에 있는 동작인데, 덤프 볼륨의 생명주기와 만나면 이렇게 된다.
- CronJob 파드가 뜬다 → PVC attach → 덤프 30초 → 파드 종료 → detach
- 하루 중 볼륨이 attached 인 시간: 약 30초
- RecurringJob 실행 시각에 attached 일 확률: 0 에 가깝다
RecurringJob 은 매일 돌면서 “attached 볼륨 없음” 으로 정상 종료했다. 에러가 아니다. 그래서 알림도 없었다.
2-2. holder 파드
전역 설정으로 detached 볼륨도 백업하게 할 수는 있다(allow-recurring-job-while-volume-detached). 하지만 그러면 Longhorn 이 백업 때마다 볼륨을 attach/detach 한다. 이 볼륨만의 문제를 전역 설정으로 푸는 것도 마음에 안 들었다.
대신 pause 컨테이너 하나가 볼륨을 읽기 전용으로 상시 마운트 한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgdump-holder
spec:
replicas: 1
template:
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.9
volumeMounts:
- name: dump
mountPath: /dump
readOnly: true
volumes:
- name: dump
persistentVolumeClaim:
claimName: pgdumppause 컨테이너는 메모리 수 MB 다. 볼륨이 항상 attached 이니 RecurringJob 이 본다. CronJob 은 podAffinity 로 holder 와 같은 노드에 묶는다. Longhorn 볼륨은 RWO 라 다른 노드에서 동시에 attach 할 수 없다.
2-3. 덤프 파일 자체를 검증한다
외부 사본 문제를 잡으면서 덤프 스크립트도 다시 봤다. 이전엔 pg_dump > file 이 전부였다. 지금은:
TMP="$DIR/.tmp-$STAMP.dump"
pg_dump -Fc "$DB" > "$TMP"
# 1KB 미만이면 실패로 간주 (빈 덤프, 인증 실패 시 빈 파일)
[ "$(stat -c %s "$TMP")" -ge 1024 ] || { echo "dump too small"; exit 1; }
# 아카이브 목차를 읽을 수 있어야 한다
pg_restore --list "$TMP" > /dev/null || { echo "dump unreadable"; exit 1; }
# 검증 통과 후에만 정식 이름으로
mv "$TMP" "$DIR/$STAMP.dump"- 크기 하한: 인증 실패나 연결 끊김으로 빈 파일이 생기는 경우를 잡는다.
pg_restore --list: 아카이브 포맷이 깨졌으면 여기서 걸린다. 복원까지 하지 않아도 목차는 읽어 본다.- 임시 파일 → rename: 쓰는 도중 죽어도 반쪽 파일이 정식 이름으로 남지 않는다.
- 정리를 덤프 앞에: 디스크가 꽉 차서 덤프가 실패하는 것보다 옛 덤프를 먼저 지우는 게 낫다. 최소 보존 개수는 둔다.
activeDeadlineSeconds:concurrencyPolicy: Forbid인데 Job 이 매달리면 다음 실행이 영원히 건너뛴다. 상한을 둔다.- DB 접속 대기 루프: NetworkPolicy IPSet 지연 때문에 뜨자마자 붙으면 거부된다.
2-4. 되읽어야 백업이다
마지막으로 복원 리허설. 덤프를 빈 PostgreSQL 에 pg_restore 하고 테이블별 행 수를 운영과 비교했다. 두 프로젝트에서 각각 8개·11개 테이블이 일치했다. 이걸 하기 전까지 “백업이 있다” 는 주석의 주장이었다.
비슷한 교훈이 다른 곳에도 있었다. 비밀번호 관리자 메모에 YAML 을 붙여 두고 백업이라 생각했는데, 되읽으니 줄바꿈이 뭉개져 복원이 안 됐다. 한 줄 JSON 으로 다시 보관하고 되읽어 대조했다. 되읽어 보기 전까지 백업은 백업이 아니다. 주석은 더더욱 아니다.
2-5. 실패 도메인을 분리한다
DB 데이터 PVC 는 local-path(노드 로컬 디스크), 덤프 PVC 는 longhorn(레플리카). DB 가 있는 노드 디스크가 죽어도 덤프는 다른 노드 레플리카에 있다. 그리고 덤프 볼륨은 Longhorn 백업으로 NFS 에 한 벌 더. 세 곳이 서로 다른 실패 도메인이다.
DB 파드는 nodeSelector 로 워커 하나에 고정했다. 이유는 local-path 다. 파드가 다른 노드에 스케줄되면 PVC 가 그 노드에 새로 만들어져 빈 DB 로 뜬다. 데이터가 사라진 게 아니라 다른 노드에 남아 있는데, 앱은 빈 DB 를 보고 정상 기동한다. 그 상태로 트래픽을 받으면 새 데이터가 빈 DB 에 쓰인다. 고정이 없으면 이 경로가 열려 있다.
3. 마무리
요약
- Longhorn RecurringJob 은 detached 볼륨을 건너뛴다. 정상 종료라 알림도 없다.
- pause 컨테이너로 볼륨을 상시 attach 하는 holder 패턴. CronJob 은 같은 노드에 묶는다.
- 덤프는 크기 하한 +
pg_restore --list+ 임시 파일 rename. 정리는 덤프 앞에.- 되읽어 보기 전까지 백업은 백업이 아니다.
local-pathDB 는 노드에 고정한다. 안 하면 빈 DB 로 뜨는 경로가 열려 있다.