FDE Capstone: The Warehouse Got the Same Order Three Times
The restore command in the rollback plan had never been run
한국어 원문으로 표시합니다.
목표
WAL 모드 SQLite 를 쓰는 고객 서비스에서 백업 → 증거 → 마이그레이션 → 실패 주입 → 복원 → 백업 시점과 같음 증명을 실제로 돌리고, 그 절차를 migrate.sh·restore.sh·rehearse.sh 로 굳힌다.
왜 중요한가
롤백 계획서의 복구 명령은 대개 실행해 본 적이 없다. 서비스가 떠 있는 채로 cp 한 백업은 integrity_check 가 ok 인데 커밋이 빠져 있고, 강제 종료 뒤 남은 -wal 은 복원한 파일 위에 다시 적용된다. 리허설로 확인하고 숫자로 남기지 않으면, 롤백이 필요한 날 처음 알게 된다.
재료: /opt/lab/p1a-rehearsal/ — liveapp.py(주문 서비스 흉내: start·status·stop·crash), migrations/002_add_currency.sql, migrations/003_customer_unique.sql. 작업 디렉터리는 /root/rehearsal 이다(서비스 DB 는 /root/rehearsal/live.db). 증거 파일은 /root/rehearsal/evidence 에 둔다. 예상 60분이고, 세션이 끝나면 /root/rehearsal 은 사라진다.
단계
liveapp.py start로 서비스를 띄우고, 서비스가 떠 있는 채로 live.db 본파일만 /root/rehearsal/copies/cp-only.db 로 cp 한 뒤, /root/rehearsal/evidence/cp.tsv 에live와cp-only의 행 수·integrity_check 를 적는다.- 서비스가 떠 있는 채로
.backup으로 /root/rehearsal/backups/pre-v2.db,VACUUM INTO로 /root/rehearsal/backups/pre-v2-vacuum.db 를 만들고 evidence/backup.tsv 에file<TAB>rows<TAB>integrity<TAB>journal_mode로 적는다. - 두 백업의 파일 sha256 과
.sha3sum --schema, 행 수·user_version·integrity·시각을 evidence/baseline.json 에 남긴다. migrate.sh DB NNN_이름.sql을 /root/rehearsal/migrate.sh 로 만들고 live.db 에 002 를 적용한다. user_version 이 NNN-1 일 때만 적용하고, 이미 NNN 이면 아무것도 하지 않고 0, 그 밖이면 거절한다.- live.db 에 003 을 적용해 실패시키고 evidence/failure.json 에 종료 코드·user_version·내용 해시 전후·오류를 남긴다. migrate.sh 는 실패하면 흔적이 없어야 한다.
restore.sh BACKUP TARGET을 /root/rehearsal/restore.sh 로 만든다. 백업이 온전하지 않으면 대상에 손대지 않고 거절하고, 남은 -wal 이 있어도 백업과 같은 내용으로 복원해 증명한다.liveapp.py crash로 배포 중 강제 종료를 일으킨 뒤 live.db 를 pre-v2.db 로 복원하고 evidence/restore.json 을 남긴다.rehearse.sh DB WORKDIR UP.sql BROKEN.sql을 /root/rehearsal/rehearse.sh 로 만든다. 백업부터 복원 증명까지 한 번에 돌고 WORKDIR/report.json 을 남기며, 무엇 하나 증명하지 못하면 0 이 아닌 값으로 끝난다.
참고
- 서비스가 떠 있는 동안에는 여러분의 sqlite3 연결이 마지막 연결이 아니어서 -wal 이 유지된다. 서비스를 멈춘 채 live.db 를 열었다 닫으면 체크포인트가 돌아 1단계 조건이 사라진다(
liveapp.py start를 다시 해도 이미 옮겨진 뒤다). 1·2단계는 서비스를 띄운 채로 한다. - 백업 파일은 읽기만 한다:
sqlite3 'file:backups/pre-v2.db?immutable=1' 'PRAGMA integrity_check' - 파일 머리 18번째 바이트(0부터 셈)는 1 이면 롤백 저널, 2 면 WAL 이다:
od -An -tu1 -j18 -N1 파일 - 채점기는 여러분이 띄운 서비스에 기대지 않는다. 4·6·8단계는 새 DB 를 만들어 스크립트를 직접 돌리고, 나머지는 증거 파일을 백업 파일과 liveapp 의 기록(app/events.log)으로 다시 계산해 대조한다. 백업 파일을 지우거나 고치면 앞 단계가 다시 떨어진다.
- 흔한 실수:
sqlite3 db < file.sql로 마이그레이션,-bail없는 BEGIN/COMMIT, 남은 -wal 을 두고 cp 로 복원.
떠 있는 서비스의 DB 를 cp 하면 무엇을 잃나
서비스를 띄우고 live.db 를 /root/rehearsal/copies/cp-only.db 로 cp 한 뒤 /root/rehearsal/evidence/cp.tsv 에 live·cp-only 의 행 수와 integrity_check 를 적는다.
ls -l live.db* 로 본파일 옆에 무엇이 있는지 먼저 보세요. 행 수는 서비스가 떠 있는 동안 live.db 에서 세고, 사본은 immutable URI 로 읽기만 하세요. integrity_check 결과와 행 수가 서로 다른 이야기를 하는지 보는 것이 이 단계의 요점입니다.
.backup 과 VACUUM INTO 로 온라인 백업
서비스가 떠 있는 채로 /root/rehearsal/backups/pre-v2.db(.backup)와 /root/rehearsal/backups/pre-v2-vacuum.db(VACUUM INTO)를 만들고 /root/rehearsal/evidence/backup.tsv 에 적는다.
VACUUM INTO 는 대상 파일이 이미 있으면 실패합니다. journal_mode 는 파일 머리의 바이트로 확인하면 파일을 열지 않고도 알 수 있습니다. 두 백업의 행 수가 1단계의 cp 사본과 어떻게 다른지 비교해 보세요.
복원 뒤 비교할 기준값 남기기
두 백업의 sha256·.sha3sum --schema, pre-v2.db 의 행 수·user_version·integrity·시각을 /root/rehearsal/evidence/baseline.json 에 남긴다.
파일 해시와 내용 해시가 각각 무엇을 증명하는지 생각해 보세요. 두 백업은 파일 해시가 다르고 내용 해시는 같아야 합니다. 시각은 파드 시각으로 %Y-%m-%dT%H:%M:%S 형식입니다. 이 뒤로 백업 파일은 열어서 쓰지 마세요.
판 번호를 지키는 migrate.sh
/root/rehearsal/migrate.sh 를 만들고 live.db 에 002_add_currency.sql 을 적용한다.
판 번호는 파일 이름 앞의 숫자에서 얻습니다. 마이그레이션 파일에는 BEGIN·COMMIT·user_version 이 없으니 스크립트가 감싸야 합니다. 채점기는 다른 번호와 다른 user_version 의 DB 로도 돌려 봅니다.
실패한 마이그레이션은 흔적이 없어야 한다
live.db 에 003_customer_unique.sql 을 적용해 실패시키고 /root/rehearsal/evidence/failure.json 을 남긴다. migrate.sh 는 중간 문장이 실패해도 아무것도 바꾸지 않아야 한다.
실패 전후로 user_version 과 .sha3sum --schema 를 재세요. sqlite3 CLI 가 오류 뒤에 무엇을 하는지, 제약 위반이 트랜잭션 전체를 되돌리는지 문서에서 확인해 보세요. 채점기는 실패 위치가 다른 마이그레이션으로 migrate.sh 를 시험합니다.
남은 -wal 까지 처리하는 restore.sh
/root/rehearsal/restore.sh BACKUP TARGET 을 만든다. 망가진 백업은 대상에 손대기 전에 거절하고, 복원 뒤 내용 해시가 백업과 같음을 확인한다.
대상 옆에 -wal 이 남아 있으면 다음에 열 때 무슨 일이 생기는지 WAL 문서를 다시 보세요. 백업 검사는 대상에 손대기 전에, 증명은 복원한 뒤에. 채점기는 강제 종료로 -wal 이 남은 DB 와 페이지가 망가진 백업을 줍니다.
배포 중 강제 종료된 운영 DB 되돌리기
liveapp.py crash 뒤 restore.sh 로 live.db 를 backups/pre-v2.db 로 복원하고 /root/rehearsal/evidence/restore.json 을 남긴다.
crash 가 끝나면 ls -l live.db* 로 무엇이 남았는지 보세요. 장애의 token 은 app/events.log 의 마지막 crashed 줄에 있습니다. 복원 뒤 hotfix- 로 시작하는 주문이 남았는지도 세어 보세요.
한 번에 도는 리허설과 보고서
/root/rehearsal/rehearse.sh DB WORKDIR UP.sql BROKEN.sql 을 만든다. WORKDIR/backup.db 와 WORKDIR/report.json 을 남기고, 모든 증명이 맞을 때만 0 으로 끝난다.
앞에서 만든 migrate.sh·restore.sh 를 스크립트 위치 기준으로 부르세요. 실패 주입이 실패하지 않은 경우, 실패 뒤 내용 해시가 바뀐 경우, 복원 뒤 백업과 다른 경우 모두 0 이 아니어야 합니다. 채점기는 -wal 에만 커밋이 남은 DB 와 무작위 판 번호로 돌립니다.