백업 스크립트를 새로 짜면서 맨 위에 set -e를 넣었다. 어디서 실패하면 거기서 멈추니까 안전하다고 생각했다. 새벽에 돌았고 아침에 보니 어제 백업본이 없었다. 새 백업도 없었다.
Table of contents
Open Table of contents
지운 뒤에 멈췄다
문제의 부분은 이랬다.
#!/bin/bash
set -e
rm -f $BACKUP_DIR/dump-*.sql
mysqldump -u backup -p"$PASS" shopdb > $BACKUP_DIR/dump-$DATE.sql
gzip $BACKUP_DIR/dump-$DATE.sql
mysqldump 가 실패했고 그 앞의 rm -f 는 성공했다. set -e 는 실패 지점에서 멈췄지만 멈추기 전에 지운 것은 그대로 지워진 상태로 남는다. 옛 백업도 없고 새 백업도 없는 자리가 그렇게 만들어졌다.
set -e 는 명령이 실패하면 스크립트를 종료한다. 그게 전부다. 실패 지점 이후를 실행하지 않을 뿐 이미 실행한 것은 건드리지 않는다.
이름 때문에 안전장치처럼 느껴지는데 실제로 하는 일은 불완전한 상태에서 손을 떼는 것이다. ROLLBACK 같은 것이 파일과 시스템 설정에는 없다. 지운 것은 지워진 것이다.
안 잡는 자리도 여럿이다. 직접 돌려 보니 파이프라인 왼쪽이 실패해도 안 멈추고 if 조건과 || 왼쪽과 함수 안에서 난 실패도 그냥 지나간다. set -o pipefail 을 함께 켜야 파이프라인 실패가 잡힌다.
백업 스크립트가 정확히 그 모양이 되기 쉽다. mysqldump ... | gzip > $FILE 로 쓰면 mysqldump 가 죽어도 gzip 이 0 으로 끝나므로 파이프라인 전체는 성공이다.
더 위험한 경우
같은 함정이 네트워크 설정에서 더 크게 터진다.
set -e
route del default
route add default gw 10.0.0.1
앞이 성공하고 뒤가 실패하면 그 서버는 기본 경로가 없는 상태로 남는다. 밖으로 나가는 통신이 전부 끊긴다. ssh 로 접속해 이걸 돌리고 있었다면 그 순간 연결이 끊기고 다시 못 들어간다. 안전하게 멈춘 결과가 서버를 못 쓰게 만든다.
부분 실패가 시스템을 불완전한 상태로 만드는 작업에서는 이 설정에 기대지 않기로 했다. route 와 rm 과 chmod 와 mount 가 거기 들어간다.
지우는 자리를 뒤로 보냈다
백업 스크립트는 명령마다 결과를 보는 방식으로 고쳤다.
mysqldump -u backup -p"$PASS" shopdb > "$NEW.tmp"
if [ $? -ne 0 ]; then
echo "덤프 실패, 기존 백업 유지" >&2
rm -f "$NEW.tmp"; exit 1
fi
if [ ! -s "$NEW.tmp" ]; then
echo "덤프가 비었다, 기존 백업 유지" >&2
rm -f "$NEW.tmp"; exit 1
fi
mv "$NEW.tmp" "$NEW"
gzip "$NEW"
find $BACKUP_DIR -name 'dump-*.sql.gz' -mtime +7 -delete
바뀐 것이 셋이다. rm 을 뒤로 보내 새 것이 확보되기 전에는 옛 것을 안 건드린다. .tmp 로 받고 마지막에 mv 하므로 받다가 죽으면 임시 파일만 남는다. 그리고 test -s 로 파일이 비었는지를 따로 본다. 오래된 것 정리는 find -mtime +7 -delete 로 맨 끝에 뒀다.
종료 코드가 성공이어도 실패일 수 있다
세 번째가 이 일에서 제일 오래 남았다. > $FILE 은 셸이 명령보다 먼저 처리하므로 mysqldump 가 한 글자도 못 쓰고 죽어도 빈 파일은 이미 만들어져 있다. 종료 코드만 보면 실패를 성공으로 읽는다.
명령이 0 으로 끝났다는 것은 그 명령이 자기 일을 마쳤다는 뜻이지 내가 원한 상태가 됐다는 뜻이 아니다. cp 도 잘못된 파일을 성실히 복사하고 0 으로 끝난다. mkdir -p 도 이미 있는 디렉터리에 0 을 준다. ln -s 는 대상이 없어도 링크를 만든다.
그래서 명령의 성공이 아니라 결과를 확인해야 한다. 백업이면 test -s 로 크기를 보고 gzip -t 로 압축이 풀리는지 본다. 복사면 양쪽 md5sum 을 본다.
확인이 한 단계 더 있다. 백업 파일이 있는 것과 그 파일로 복구가 되는 것은 다른 얘기라 월 1회 다른 서버에서 실제로 풀어 넣어 보기로 했다.
정리
set -e는 트랜잭션이 아니다. 멈출 뿐 이미 한 것을 안 되돌린다- 파이프라인 왼쪽과
if조건과||왼쪽과 함수 안의 실패는 안 잡는다 mysqldump | gzip은set -o pipefail없이는 실패가 묻힌다- 부분 실패가 시스템을 못 쓰게 만드는 작업에서는 기대지 않는다
rm을 먼저 두지 않는다. 새 것을 확보한 뒤에 옛 것을 지운다.tmp로 만들고 마지막에mv하면 중간 상태가 안 남는다> $FILE은 셸이 먼저 처리하므로 빈 파일이 이미 만들어져 있다- 종료 코드 0 이 원한 상태가 됐다는 뜻은 아니다
test -s와gzip -t로 결과물을 따로 확인한다