Infra

[백업] restic DB 백업 용량 최소화 복원 자동검증

jykim23 2026. 7. 8. 23:48
반응형

restic PostgreSQL 백업 스냅샷 증분 용량 복원 검증

부제: 6GB 백업을 월 0.9GB로 줄이고 매일 복원까지 확인한 이야기

DB를 백업하려고 보니 덩치가 컸다.
그런데 뜯어보니 대부분은 굳이 백업할 필요가 없는 데이터였다. 그 이야기다.

문제

이 서비스는 LangGraph로 대화 상태를 checkpoint에 저장한다.
agent가 추론 턴을 한 번 돌 때마다 그 시점의 전체 상태를 스냅샷으로 남긴다. 과거 step으로 되돌아가는 time-travel을 위해서다.
그러다 보니 한 스레드에서 대화가 100번 오가면 step 버전이 100개 쌓인다.

checkpoint 테이블만 약 6GB였는데, 그중 98%가 이 step 이력이었다.
재해 복구 관점에서 진짜 필요한 건 각 스레드의 최신 상태 하나뿐이다. 나머지 옛 step은 되감기 같은 고급 기능에나 쓰인다.

결정 — 최신 checkpoint 하나 + 그게 가리키는 것만

열쇠는 checkpoint 안의 channel_versions 맵이었다.
이 맵은 지금 상태를 이루는 blob이 어떤 것인지 정확히 가리킨다. 부모 checkpoint가 없어도 이 맵만 있으면 현재 상태를 온전히 복원할 수 있다.

실제로 확인해 봤다.
한 스레드의 과거 checkpoint 76개를 다 지우고 최신 1개만 남겼는데, 3.3만 토큰짜리 긴 대화가 디테일까지 그대로 복원됐다.

그래서 백업을 둘로 쪼갰다.

-- 스레드별 최신 checkpoint 1개
SELECT DISTINCT ON (thread_id, checkpoint_ns) ...
FROM checkpoints
ORDER BY thread_id, checkpoint_ns, checkpoint_id DESC;

-- 그 checkpoint가 channel_versions로 가리키는 blob만

비즈니스 데이터(users·threads·messages 등)는 pg_dump --exclude-table-data='checkpoint*'로 따로 뜬다.
이 조합으로 6GB가 checkpoint 281MB + 비즈니스 77MB로 줄었다. 대화 연속성은 그대로 두고.

restic — 매시간 백업인데 왜 안 터지나

두 번째 문제는 저장 방식이었다.
매시간 백업을 뜨는데, 대부분의 스레드는 그 사이 아무 일도 없다. 실측해 보니 14분 간격으로 뜬 두 덤프가 94.8% 같았다.

gzip으로 그냥 쌓으면 답이 없다.
평문 덤프가 648MB인데 매시간 × 24 × 7일이면 100GB를 넘는다. gzip은 1바이트만 달라져도 압축 결과가 통째로 달라져서, "같은 조각"을 찾아 재활용할 수가 없다.

그래서 restic을 썼다.
restic은 내용 기반으로 파일을 조각내서(content-defined chunking) 바뀐 조각만 저장한다. 압축과 암호화도 저장소에 내장돼 있다.
결과가 확연했다. 첫 백업 648MB가 55MB로 들어갔고, 이후로는 회당 3~7MB씩만 늘었다. 7일에 168번을 떠도 0.9GB 정도다. gzip으로 압축해 쌓는 예전 방식이면 22GB쯤 됐을 걸 25배 가까이 줄인 셈이다.

백업은 복원으로 증명한다

백업이 조용히 깨져 있으면, 정작 필요할 때 알게 된다. 그건 너무 늦다.
그래서 매일 새벽에 복원을 한 번씩 돌린다.

  • restic check로 저장소 무결성을 본다
  • 최신 스냅샷을 임시 위치에 복원한다
  • 버리는 임시 DB에 적재한다
  • users가 있는지, checkpoint가 있는지, 스레드마다 최신 하나만 남았는지(중복 0) 확인한다
  • 임시 DB는 drop

장애가 나서 복구하는 게 아니라, 평상시에 미리 복원을 해보는 것이다.
"백업이 있다"는 믿음 대신 "어제도 복원됐다"는 사실을 매일 갱신한다.

dedup의 대가 — 월마다 저장소를 갈아탄다

restic은 스냅샷끼리 조각을 공유한다. 덕분에 용량이 작지만, 뒤집으면 조각 하나가 상하면 그 조각을 쓰던 여러 스냅샷이 같이 무너질 수 있다.
gzip 같은 독립 full 백업은 이런 연쇄 실패가 없는 대신 용량이 터진다.

그래서 저장소를 월 단위(repo/YYYY-MM)로 나눴다.
매달 새 저장소로 넘어가면서 dedup 체인을 끊는다. 이러면 조각 손상이 번지는 범위가 최대 한 달로 제한된다.
매달 첫 백업이 full이 되는 비용을 치르는 대신, 재해가 나도 한 달치만 걱정하면 된다.

정리

  • 백업 전에 "이게 다 필요한가"부터 본다. 여기선 6GB의 98%가 되감기용 이력이었다.
  • checkpoint의 channel_versions처럼 "현재 상태를 가리키는 참조"만 있으면 과거 전부를 들고 갈 필요가 없다.
  • 반복 백업은 dedup(restic)으로 시계열 중복을 지운다. gzip은 1바이트 변화에 약해 dedup이 안 먹는다.
  • 백업은 매일 복원해봐야 증명된다. dedup을 쓰면 월 단위 저장소 분리로 손상 범위를 끊는다.

용량을 줄인 것보다, 확률적으로 상할 수 있는 지점마다 방어선을 하나씩 둔 게 핵심이었다.

반응형