백업을 걸어두고 며칠 뒤, 잘 돌고 있는지 확인해봤다. 잘 돌고 있었다.
스냅샷도 매일 쌓이고, 무결성 검사도 매번 통과했다. 타이머는 계속 성공을 보고했다.
그런데 한 번 도는 데 한 시간 넘게 걸리고 있었다.
구성은 restic + rclone 으로 구글 드라이브에 백업하는 형태다.
서버는 2코어짜리 작은 인스턴스이고, 백업 대상은 200MB 정도다.
0. 증상
실패한 적이 없다. 로그에도 오류가 없다. 그냥 오래 걸린다.
sudo journalctl -u my-backup.service -o short-iso | grep -E "Starting|Finished"
19:03:27 Starting my-backup.service...
20:10:58 Finished my-backup.service.
67분이다. 200MB를 증분 백업하는 데.
1. 단계별로 재보기
스크립트에 단계마다 시각을 찍어두면 범인이 바로 좁혀진다. 이게 없으면
“느리다”에서 더 나아가지 못한다.
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
| 단계 | 시간 |
|---|---|
| DB 덤프 + 파일 복사 | 1초 |
restic backup |
65분 4초 |
forget --prune |
12초 |
check |
2분 |
그리고 restic 이 스스로 남긴 요약이 결정적이었다.
Added to the repository: 1.529 MiB (260.207 KiB stored)
processed 8891 files, 173.070 MiB in 1:05:04
260KB를 올리는 데 65분. 대역폭 문제가 아니다.
2. 손으로 돌려보기
여기서 범위가 크게 좁혀진다. 같은 스크립트를 셸에서 직접 실행했다.
processed 8939 files, 172.468 MiB in 0:20
20초. 3분 전에 systemd 로 돌린 것과 같은 작업, 같은 서버, 같은 시각대다.
같은 시각에 손으로 돌린 것이 20초, systemd 로 돌린 것이 65분이라면
원인은 systemd 실행 환경 안에 있다. 네트워크나 원격 서비스 문제가 아니다.
이 한 번의 비교가 이후의 헛짚기를 대부분 걸러준다.
3. 틀린 가설 두 개
기록해두는 편이 낫겠다. 둘 다 그럴듯했고 둘 다 틀렸다.
가설 1: 구글 드라이브 API 할당량
로그에 Error 403: Quota exceeded ... rateLimitExceeded 가 찍혀 있었다.
rclone 의 기본 OAuth 클라이언트는 전 세계 사용자가 공유해서 할당량이 상시 빠듯하다.
그럴듯했다.
세어보니 그 67분 동안 403 은 4줄, 재시도는 1회였다. 그리고 55분 동안은
로그가 아예 한 줄도 없었다. 재시도로 시간을 보낸 것이 아니었다.
오류 메시지가 눈에 보인다고 그게 병목인 것은 아니다. 횟수를 세봐야 한다.
가설 2: IOSchedulingClass=idle
유닛에 이걸 걸어뒀었다. 2코어를 백업이 독차지하면 사이트가 느려질까 싶어서였다.
I/O 우선순위를 최하로 두면 굶을 수 있으니 의심스러웠다.
지우고 다시 돌렸다. 그대로 느렸다.
이 서버의 디스크 스케줄러를 보면 이유가 나온다.
cat /sys/block/sda/queue/scheduler
# [none] mq-deadline
none 이면 I/O 우선순위가 애초에 반영되지 않는다. 걸어둔 설정이 아무 일도
하지 않고 있었던 것이다.
4. 진짜 원인
systemd 서비스의 환경변수를 들여다봤다.
sudo systemctl show my-backup.service -p Environment
Environment=
비어 있다. 프로세스의 실제 환경도 확인했다.
sudo cat /proc/$(pgrep -f my-backup.sh)/environ | tr '\0' '\n' | grep -E '^(HOME|PATH|USER)='
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
USER=root
HOME 이 없다.
systemd 는 시스템 서비스에
HOME을 넣어주지 않는다.
셸에서는 항상 있으니 없을 거라고 생각하기 어렵다.
5. HOME 이 없으면 restic 이 왜 느려지나
restic 은 저장소의 인덱스와 메타데이터를 ~/.cache/restic 에 캐시한다.
증분 백업이 빠른 이유가 이 캐시다. 이전 스냅샷 정보를 로컬에서 읽고,
바뀐 것만 올린다.
HOME 이 없으면 그 경로를 정하지 못하고 매 실행마다 임시 캐시로 떨어진다.
결과적으로 저장소 메타데이터를 원격에서 통째로 다시 읽는다.
구글 드라이브처럼 작은 요청 하나하나가 느린 백엔드에서 이 차이가 극단적으로 벌어진다.
같은 코드가 로컬 디스크 저장소라면 눈치도 못 챌 만큼 차이가 작을 것이다.
캐시가 살아 있는지는 이렇게 확인한다.
sudo du -sh /root/.cache/restic
# 1.8M /root/.cache/restic ← 쌓이고 있으면 정상
6. 고치기
유닛에 한 줄이다.
[Service]
Type=oneshot
Environment=HOME=/root
ExecStart=/usr/local/bin/my-backup.sh
sudo systemctl daemon-reload
실측 결과:
restic backup 단계 |
|
|---|---|
| 이전 | 65분 4초 |
| 이후 | 1분 9초 |
RESTIC_CACHE_DIR 을 직접 지정해도 된다. HOME 쪽이 rclone 설정 경로까지
같이 해결해줘서 더 간단하다.
7. 곁가지로 얻은 개선
고친 뒤 남은 가장 느린 단계는 forget --prune(약 4분)이었다. 팩 파일을 다시
쓰기 때문에 원래 무거운 작업이다.
보관 정책이 일 7 / 주 4 / 월 6 이라면 매일 돌릴 이유가 없다. 하루 늦게
지워질 뿐이다. 일요일에만 하도록 바꿨다.
if [ "${1:-}" = "--maintenance" ] || [ "$(date +%u)" = "7" ]; then
restic forget --tag daily --host "$(hostname)" \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check
else
log "정리와 검사는 일요일에만 한다"
fi
타이머를 둘로 나누지 않은 이유가 있다. 같은 저장소에 두 작업이 겹치면
restic 잠금이 충돌한다. 한 스크립트 안에서 순서대로 하면 그 걱정이 없다.
최종 결과는 이렇다.
| 전체 소요 | |
|---|---|
| 처음 | 67분 |
HOME 추가 후 |
7분 |
| 정리를 주 1회로 | 22초 (일요일만 약 6분) |
8. 이 문제가 위험한 이유
아무것도 실패하지 않았다.
- 타이머는 매번 성공을 보고했다
- 스냅샷은 매일 쌓였다
- 무결성 검사는 매번 통과했다
- 복구도 실제로 해봤고 잘 됐다
즉 “백업 실패 시 알림” 같은 감시로는 이걸 절대 못 잡는다. 성공했으니까.
배치 작업은 성공/실패만 보면 안 된다. 소요 시간도 같이 봐야 한다.
어제 2분이던 것이 오늘 60분이면, 성공했어도 뭔가 잘못된 것이다.
가장 싼 대비책은 스크립트가 단계마다 시각을 찍는 것이다. 그것만 있으면
“느리다”에서 “이 단계가 느리다”로 바로 넘어간다.
9. 테스트 중에 만든 함정
원인을 찾느라 실행 중인 백업을 systemctl stop 으로 여러 번 끊었다.
그러다 다음 실행이 이렇게 실패했다.
unable to create lock in backend: repository is already locked by PID 2278747
lock was created at 2026-08-31 15:17:52 (9m8s ago)
the `unlock` command can be used to remove stale locks
끊긴 프로세스가 저장소에 잠금을 남긴 것이다. 다른 작업이 정말 돌고 있지 않은지
확인한 뒤 지운다.
sudo HOME=/root restic -r <저장소> unlock
체크리스트
- [ ] systemd 로 도는 작업에
Environment=HOME=...이 있는지 확인 - [ ]
systemctl show <서비스> -p Environment로 실제 환경 보기 - [ ] 스크립트에 단계별 시각 로그 남기기
- [ ]
restic캐시 디렉터리가 쌓이고 있는지 확인 - [ ] 손으로 실행한 시간과 systemd 실행 시간을 같은 시각대에 비교
- [ ] 오류 메시지는 눈에 보이는지가 아니라 횟수로 판단
- [ ]
forget --prune은 보관 정책에 맞춰 주 1회로 - [ ] 실패 알림만이 아니라 소요 시간도 감시 대상에 넣기
이어지는 글
- OCI Always Free 인스턴스는 언제 회수되는가 — 이 백업을 만들게 된 계기
- OCI 무료 서버에 도메인 붙이고 HTTPS 적용하기 — 이 서버를 세운 과정