자기 손상을 스스로 백업한 백업 스크립트

8월 23, 2026 · 노이반
현장의 기록: AI Engineer / Forward Deployed Engineer — 5편
한 줄 요약

자기 출력 디렉토리를 제외하지 않고 디렉토리 트리를 스캔하는 안전 스크립트는 결국 자기 산출물을 입력으로 취급하게 된다 — 동일 경로 가드가 없는 복구 루틴은 자기 자신을 치료법으로 선택할 수 있다.

배경

내 인프라 호스트(이 스택을 Atlas라 부르겠다)의 루트 파일시스템이 디스크 사용률 100%에 도달했다 — 전체 약 935GB 중 가용 공간 0바이트. 데이터베이스 쓰기, git, 체크포인트 파일 등 모든 쓰기 경로가 언제든 실패할 수 있는 상태였다.

처음 살펴본 것

30분마다 실행되는 헬스체크 스크립트가 있었는데, 데이터베이스 파일의 손상 여부를 감시하고 필요하면 백업에서 복구하는 역할이었다. 프로젝트 루트 아래 5MB 이상의 .db 파일을 전부 스캔해서 무결성을 확인하는 방식이었다.

그런데 이 스캔이 몇 달째 정상 백업이 쌓이고 있던 자기 자신의 백업 디렉토리까지 함께 훑고 있었다.

루프가 만들어진 과정

몇 달 전 백업 파일 두 개가 어느 시점엔가 손상돼(database disk image is malformed) 방치돼 있었는데, 아무도 눈치채지 못한 채 그냥 그 자리에 남아있었다. 30분마다 헬스체크는 이렇게 동작했다.

  1. 자기 백업 폴더까지 포함해서 트리 전체를 스캔해 .db 파일을 찾는다.
  2. 손상된 파일 두 개를 발견하고 "복구 필요"로 표시한다.
  3. 복구에 쓸 "최신 안전 백업"을 찾으려 하는데, 후보 탐색 로직에 자기 자신의 경로와 매칭되는 걸 막는 가드가 없어서, 때로는 손상된 파일 자신을 자신의 복구 소스로 선택한다.
  4. 복구를 시도하기 전에 "보존 목적"으로 손상된 파일을 .corrupted.<타임스탬프> 사본으로 먼저 복사한다.
  5. 그런 다음 같은 파일을 자기 자신에게 "수정"이랍시고 복사하는데 — 실제로는 에러(cp: same file)가 나지만, 스크립트의 에러 처리는 이걸 그냥 성공으로 로그를 남겨버린다.
  6. 4번 단계가 다음 주기에 또 실행된다. 그다음도. 아무도 이 스크립트의 출력을 보고 있지 않았기 때문에 30분마다 영원히 반복됐다.
# 버그의 단순화된 구조 — 자기참조 복구를 막는 가드가 없음
find "$WATCH_DIR" -name '*.db' -size +5M   # 자기 백업 디렉토리까지 스캔함

recover_from_backup() {
  local candidate
  candidate=$(find_latest_backup "$db_name")
  cp "$db_path" "${db_path}.corrupted.$(date +%s)"   # "보존" 단계
  cp "$candidate" "$db_path"      # candidate가 db_path 자기 자신일 수 있음!
}

이 문제가 발견됐을 때, 손상 사본 디렉토리에는 8,408개 파일, 579GB가 쌓여 있었다 — 전체 디스크의 60% 이상이, 자기 자신을 향해 작동하는 "안전" 메커니즘이 만들어낸 쓸모없는 중복 쓰레기로 채워져 있었다.

관련 없어 보이지만 함께 문제를 키운 두 번째 원인

별도의 스크립트가 하루 두 번 애플리케이션 디렉토리 전체를 보조 경로로 미러링하고 있었는데, 대형 사고에 대비한 보호 목적으로 만들어진 것이었다. 양쪽 경로에 stat -f를 걸어보니 동일한 물리 논리 볼륨이었다 — "백업" 위치와 원본 데이터가 완전히 같은 하부 스토리지 위에 있었던 것이다. 이 미러링은 정말로 무의미한 중복 41GB를 추가로 만들고 있었을 뿐이다. 진짜 재해 대비 보호는 별도의 물리 디스크나 원격 대상이 필요하지, 같은 디스크 안의 두 번째 디렉토리로는 성립하지 않는다.

조치

  1. 헬스체크의 스캔 경로에서 백업 디렉토리를 완전히 제외하고, 복구 후보 탐색이 자기 자신을 고치려는 경로와 절대 매칭되지 않도록 가드를 추가했다.
  2. 공유 스토리지라는 사실을 확인한 뒤(가정이 아니라 검증으로) 하루 두 번 돌던 미러링 작업을 제거했다.
  3. 손상된 중복 8,408개를 삭제(579GB 회수)하고, 진짜로 손상된 원본 두 개도 제거했다(더 이전의 유효한 백업이 실제 대체 fallback으로 존재함을 확인 후).
  4. git 저장소에 가비지 컬렉션을 돌려 loose object 공간까지 추가로 회수했다.

검증

  • 이전: 전체 935GB 중 931GB 사용, 가용 0(100%).
  • 이후: 전체 935GB 중 301GB 사용, 가용 587GB(34%) — 약 630GB 회수.
  • 패치 후 헬스체크를 수동으로 재실행: 손상 플래그 0건(이전에는 매 주기마다 1건씩 영구적으로 발생하고 있었음).
얻은 교훈

자신의 출력 디렉토리를 제외하지 않고 디렉토리 트리를 스캔하는 "안전" 스크립트는, 언젠가는 반드시 자기 자신의 산출물을 입력으로 취급하게 된다. 그리고 "복구할 좋은 사본을 찾는" 로직에 동일 경로 방지 가드가 없으면, 자기 자신을 치료제로 선택할 수 있다. 이 두 버그 모두, "이 스크립트의 스캔 범위에 자기가 쓰는 폴더가 포함되면 어떻게 되는가"라는 구체적인 질문을 코드 리뷰에서 던지지 않는 한 발견되지 않는다 — 그리고 이건 자기가 감시하는 트리 안에서 읽기와 쓰기를 동시에 하는 모든 예약 작업에 대해 반드시 던져봐야 할 질문이다. 별개로: 백업 대상과 동일한 물리 볼륨에 있는 "백업"은 파일 하나의 실수 삭제 말고는 사실상 아무것도 보호하지 못한다 — 디렉토리 이름만 보고 가정하지 말고 stat -f로 물리적 분리를 직접 검증해야 한다.

Advertisement

첫 댓글을 남겨보세요