Reading

SQLite 正在服务线上,怎么备份才不丢数据

直接 cp 一个正在写入的 SQLite 数据库,等于赌备份时刻恰好没有事务。用 .backup 或 VACUUM INTO 做一致性快照,再配上完整性校验和异地同步。

返回归档
SQLite 正在服务线上,怎么备份才不丢数据 封面图

适用场景:线上跑着一个 SQLite 的小服务(比如这个博客本身),想要可靠的定时备份。直接 cp 数据库文件看起来能用,但在有写入的瞬间拷贝可能得到一份损坏的副本——这篇讲怎么备得对、怎么验证备份真的能恢复。

背景和思路

这个博客的数据层就是一个 qingsong-notes.db 文件,跑在服务器的 Docker volume 里。文件小、依赖少,是 SQLite 的典型舒适区,但备份策略我一开始就想错了:cron 里写一行 cp

问题出在 SQLite 的写入机制上。开启 WAL 模式后,数据同时存在于 .db.db-wal.db-shm 三个文件里;即使是回滚日志模式,写事务进行到一半时拷走的文件也可能是不一致的。cp 一个正在被写入的数据库,等于赌备份时刻恰好没有事务。

SQLite 官方给了两条正路:.backup 命令和 VACUUM INTO,两者都会在数据库层面做一致性快照,不怕并发写入。

方法一:sqlite3 .backup

sqlite3 /app/data/qingsong-notes.db ".backup '/backup/qingsong-notes-$(date +%F).db'"

.backup 使用 SQLite 的在线备份 API,逐页复制并自动处理期间的写入,产出的是一个完整独立的数据库文件。这是最通用的方式。

方法二:VACUUM INTO(顺便瘦身)

VACUUM INTO '/backup/qingsong-notes-2026-07-10.db';
sqlite3 /app/data/qingsong-notes.db "VACUUM INTO '/backup/qingsong-notes-$(date +%F).db'"

VACUUM INTO 在导出的同时做碎片整理,备份文件通常比原库更小。代价是比 .backup 更吃 IO,库大的时候注意错峰。我的库只有几 MB,直接选它。

注意:目标文件必须不存在,否则会报错,脚本里日期戳正好规避了这一点。

放进容器环境里

博客跑在 Docker 里,宿主机 cron 通过 docker compose exec 进容器执行:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR=/opt/qingsong-notes/backups
STAMP=$(date +%F-%H%M)
mkdir -p "$BACKUP_DIR"

docker compose -f /opt/qingsong-notes/docker-compose.yml exec -T qingsong-notes \
  sh -lc "apk add --no-cache sqlite >/dev/null 2>&1 || true; \
          sqlite3 /app/data/qingsong-notes.db \"VACUUM INTO '/app/data/backup-tmp.db'\""

docker compose -f /opt/qingsong-notes/docker-compose.yml cp \
  qingsong-notes:/app/data/backup-tmp.db "$BACKUP_DIR/qingsong-notes-$STAMP.db"

docker compose -f /opt/qingsong-notes/docker-compose.yml exec -T qingsong-notes \
  rm -f /app/data/backup-tmp.db

# 只保留最近 30 份
ls -t "$BACKUP_DIR"/qingsong-notes-*.db | tail -n +31 | xargs -r rm -f

配上 crontab,每天凌晨四点执行:

0 4 * * * /opt/qingsong-notes/backup.sh >> /var/log/qingsong-backup.log 2>&1

如果容器镜像里没有 sqlite3 命令,更干净的做法是在 Dockerfile 里装好,而不是像上面那样运行时补装。

备份必须验证,否则等于没备

备份最大的谎言是「文件存在」。每次备份后至少做完整性检查:

sqlite3 "$BACKUP_DIR/qingsong-notes-$STAMP.db" "PRAGMA integrity_check;"

输出必须是一行 ok。可以把这一步加进脚本,非 ok 就报警(发邮件或者打一条日志再 exit 1 让 cron 通知你)。

更进一步,每季度做一次恢复演练:把备份文件放到本地项目里跑起来,确认文章、标签、媒体记录都在。媒体文件(图片)不在数据库里,要和 media/ 目录一起备份,只备数据库会出现文章在、图挂了的情况——这个坑我在部署脚本里就踩过一次。

异地:别把备份和数据放同一块盘

服务器磁盘挂了,本机备份一起陪葬。最简单的异地方案是 rsync 到另一台机器或本地 NAS:

rsync -az --delete root@101.37.21.147:/opt/qingsong-notes/backups/ ~/backups/qingsong-notes/

想要更实时的连续备份,可以看一眼 Litestream(https://litestream.io/),它以 sidecar 方式把 WAL 持续同步到对象存储,恢复点可以精确到秒级。对个人博客来说每日快照够用了,但知道这个工具的存在很值得。

收尾检查

  • 备份用 .backupVACUUM INTO,绝不直接 cp 在线数据库。
  • 每份备份跑过 PRAGMA integrity_check 且结果为 ok。
  • 媒体目录和数据库一起备份。
  • 至少一份备份在另一台物理机器上。
  • 真正做过一次恢复演练,而不是相信备份「应该没问题」。