知识广场

按学科筛选:计算机科学 / 数据库 / SQLite
清除筛选

«计算机科学 / 数据库 / SQLite» 分类下共 2 篇帖子

litestream 把 SQLite 实时复制到 S3 / Backblaze(秒级 RPO 备份)

## 起因 我们的小型生产服务用 SQLite(部署简单 + 性能够 + 单文件备份)。 但默认备份方案最多每天 cron `sqlite3 .backup` + scp 异地。两次备份之间 有 24 小时 RPO(最坏丢一天数据)。 `litestream` 是开源工具,把 SQLite 的 WAL 日志实时增量复制到 S3 兼容 存储。RPO 降到秒级,无需改 application 代码。 ## 解决方案 ### 装 ```bash # 二进制安装 LITESTREAM_VERSION=0.3.13 curl -fsSL https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-v${LITESTREAM_VERSION}-linux-amd64.tar.gz \ | sudo tar xz -C /usr/local/bin litestream litestream version ``` 或 Docker:`docker run litestream/litestream`。 ### 配置 `/etc/litestream.yml`: ```yaml dbs: - path: /srv/knowledge/db.sqlite3 replicas: - type: s3 bucket: my-backups path: knowledge endpoint: https://s3.us-east-005.backblazeb2.com region: us-east-005 access-key-id: ${B2_KEY_ID} secret-access-key: ${B2_APP_KEY} retention: 720h # 保留 30 天 snapshot-interval: 24h # 每天全量 snapshot ``` litestream 工作原理: 1. application 写 `db.sqlite3` → SQLite 写 WAL 2. litestream 后台读 WAL 增量帧 3. 每个 WAL 帧立刻上传到 S3(默认 10s 内) 4. 定期(默认 24h)做一次全量 snapshot 5. retention 期外的 snapshot + WAL 自动清理 ### systemd service ```ini # /etc/systemd/system/litestream.service [Unit] Description=Litestream Requires=network.target After=network.target [Service] User=trio Group=trio EnvironmentFile=/etc/litestream.env ExecStart=/usr/local/bin/litestream replicate -config /etc/litestream.yml Restart=on-failure RestartSec=5s StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target ``` `/etc/litestream.env`: ```bash B2_KEY_ID=00abc... B2_APP_KEY=K00... ``` ```bash sudo systemctl enable --now litestream journalctl -u litestream -f # 看到 "replicating wal frames..." ``` ### 验证副本 ```bash litestream snapshots /srv/knowledge/db.sqlite3 # replica generation index size created # s3 abc123 5 12345678 2026-05-24T03:30:00Z litestream wal /srv/knowledge/db.sqlite3 | head # replica generation index offset size ``` ### 灾难还原 服务器全没了,新机器恢复: ```bash # 装 litestream + 复制 /etc/litestream.yml + .env 到新机器 litestream restore -o /srv/knowledge/db.sqlite3 \ s3://my-backups/knowledge # 从最新 snapshot + 重放 WAL 到最新状态 # 几秒-几分钟(取决数据量) # 启动 application sudo systemctl start knowledge ``` 时间点恢复: ```bash litestream restore -timestamp 2026-05-20T10:00:00Z \ -o /tmp/db-at-may-20.sqlite3 \ s3://my-backups/knowledge ``` 恢复到任意 30 天内的时间点。 ## 效果 - RPO 从 24h → 10s - 灾难还原 = 一行 `litestream restore` - B2 存储费用:30 天保留 ~2 GB DB 约 $0.10/月 - application 零改动:litestream 在后台独立进程跑 ## 与其它方案对比 | | cron `.backup` + scp | litestream | PG streaming replica | |---|---|---|---| | RPO | 24h | ~10s | ~ms | | 单机部署 | ✅ | ✅ | 需主备 | | 时间点恢复 | ❌ | ✅ | ✅ | | 同步开销 | 0(备份时刻才有) | 极低(WAL 增量) | 中(流复制) | | 成本 | 几乎 0 | 存储费 + 几乎免费 | 备机 + 网络 | SQLite + litestream 在中小规模生产是性价比最高方案之一。 ## 与之前装的 daily backup 共存 之前章节装的 `knowledge-backup.timer` 是每日全量本机存档;litestream 是实时异地复制。两者互补: - 本地 daily 用于"刚才误删一条记录" 快速回滚 - litestream 用于"机器全没了" 灾难恢复 不冲突,都开就好。 ## 性能影响 litestream 读 WAL 是只读操作,对 application 几乎无影响。 WAL 上传是后台 + 异步,application 写延迟不变。 CPU 占用通常 < 1%,内存 < 50 MB。 ## 踩过的坑 1. **SQLite 必须 WAL 模式**:litestream 依赖 WAL。 ```sql PRAGMA journal_mode = WAL; ``` 不是 WAL 时 litestream 报错。Django 我们已经在 settings 里开了。 2. **不能两个 litestream 同时复制同一个 DB**:会写坏。一个机器一个 实例。 3. **同时开 cron `.backup`**:`.backup` 命令本身不影响 WAL,安全; 但 `VACUUM` / `VACUUM INTO` 会重写 DB 让 litestream 失效,需要 重新 init。 4. **B2 / R2 等 S3-兼容 endpoint URL 易写错**:每个区域 endpoint 不同。出错时 `litestream replicate` 报 "no such bucket"。 curl 测试一下访问。 5. **首次 restore 时 generation 不匹配**:DB 被本地写过新数据, litestream WAL 链断了。生产里数据始终从备份 restore,本地不要 手动改。

SQLite 启用 WAL 模式 + 调几个 pragma 让并发写不再串行

SQLite 默认 journal_mode 是 `delete`,写操作要拿整库锁,多写者 直接互相 block。改 WAL(Write-Ahead Logging)后: - 读不阻塞写 - 写不阻塞读 - 多个读者并发 - 单写者(这个仍是 SQLite 的硬限制) 对中小型应用(< 10k QPS)足够把它当生产数据库用。 ## 启用 WAL ```python import sqlite3 conn = sqlite3.connect('app.db') conn.execute('PRAGMA journal_mode = WAL;') conn.execute('PRAGMA synchronous = NORMAL;') conn.execute('PRAGMA busy_timeout = 5000;') conn.execute('PRAGMA cache_size = -64000;') # 64MB cache conn.execute('PRAGMA foreign_keys = ON;') conn.execute('PRAGMA temp_store = MEMORY;') conn.execute('PRAGMA mmap_size = 134217728;') # 128 MB mmap ``` 各 pragma 含义: | pragma | 作用 | 建议值 | |---|---|---| | `journal_mode = WAL` | 改为 Write-Ahead Logging | WAL | | `synchronous = NORMAL` | WAL 同步级(默认 FULL 更安全但慢) | NORMAL | | `busy_timeout` | 等锁的毫秒数(超时返回 SQLITE_BUSY) | 5000 | | `cache_size` | 负数表示 KB,正数表示页数;建议负数 | -64000 (64MB) | | `foreign_keys` | 默认 **关闭** —— 必须显式开 | ON | | `temp_store` | 临时表放内存还是磁盘 | MEMORY | | `mmap_size` | 用 mmap 而不是 read() 读 | 128MB | 每次新连接都要重设这些 pragma(除了 journal_mode 是持久化的)。 ## WAL 后会出现的文件 ``` app.db app.db-wal # WAL log app.db-shm # 共享内存映射 ``` 备份这 3 个文件都要在一致快照里,最好用 `VACUUM INTO` 或 SQLite Online Backup API。 不要简单 `cp app.db backup.db`,可能拿到不一致状态。 ```python # 一致备份: src = sqlite3.connect('app.db') dst = sqlite3.connect('backup.db') with dst: src.backup(dst) ``` ## checkpoint:把 WAL 合并回主库 WAL 不断追加,会越来越大。SQLite 默认 1000 页时自动 checkpoint, 但繁忙系统可能赶不上。手动控制: ```python conn.execute('PRAGMA wal_autocheckpoint = 1000;') # 默认值 conn.execute('PRAGMA wal_checkpoint(PASSIVE);') # 立刻做一次(不阻塞读写) conn.execute('PRAGMA wal_checkpoint(TRUNCATE);') # 完全清空 WAL(要短暂独占) ``` 低峰期跑一次 `TRUNCATE` 比较好。 ## 真实并发测试 ```python # 5 个线程并发读 + 1 个线程写 import threading, sqlite3, time DB = 'test.db' def setup(): c = sqlite3.connect(DB) c.execute('PRAGMA journal_mode = WAL') c.execute('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)') for i in range(10000): c.execute('INSERT INTO t (v) VALUES (?)', (f'val{i}',)) c.commit(); c.close() setup() def reader(): c = sqlite3.connect(DB) c.execute('PRAGMA busy_timeout = 5000') for _ in range(1000): c.execute('SELECT count(*) FROM t').fetchone() c.close() def writer(): c = sqlite3.connect(DB) c.execute('PRAGMA busy_timeout = 5000') for i in range(1000): c.execute('INSERT INTO t (v) VALUES (?)', (f'w{i}',)) c.commit() c.close() threads = [threading.Thread(target=reader) for _ in range(5)] + [threading.Thread(target=writer)] t0 = time.time() for t in threads: t.start() for t in threads: t.join() print(f'done in {time.time()-t0:.2f}s') ``` WAL 模式比 delete 模式通常快 3-10 倍(取决于 IO)。 ## SQLITE_BUSY 的根因 + 处理 WAL 模式下还可能 BUSY: 1. **两个写者同时持有写锁**:第二个收到 BUSY。`busy_timeout` 让 SQLite 自动重试 5 秒 2. **DDL 时被读者卡住**:CREATE / ALTER 需要独占,正在 SELECT 的连接会阻塞 DDL 3. **WAL checkpoint(TRUNCATE) 时被读者卡住**:同上 应用层正确做法:捕获 `sqlite3.OperationalError` 重试,或者用 `busy_timeout` 让驱动层重试。 ## Django 配置 ```python # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;', 'timeout': 20, # busy_timeout (秒) }, } } ``` 或用一个 connection signal 设更多 pragma: ```python from django.db.backends.signals import connection_created def _set_pragmas(sender, connection, **kwargs): if connection.vendor == 'sqlite': with connection.cursor() as cur: cur.execute('PRAGMA journal_mode=WAL;') cur.execute('PRAGMA synchronous=NORMAL;') cur.execute('PRAGMA temp_store=MEMORY;') cur.execute('PRAGMA cache_size=-64000;') cur.execute('PRAGMA mmap_size=134217728;') connection_created.connect(_set_pragmas) ``` ## 什么时候放弃 SQLite - 写 QPS > 1000:单写者是瓶颈 - 多机器需要共享 DB:SQLite 是单机的 - 字段需要 jsonb 索引等高级 PG 特性 - 复杂查询性能:SQLite 优化器较 PG 简单 否则 SQLite 比 PG/MySQL 部署简单一个数量级,单文件备份,绝对值得。 ## 踩过的坑 - WAL 文件不被压缩;WAL 不 checkpoint 时膨胀到几 GB 还在变大。监控 `app.db-wal` 大小,超过 100MB 跑一次 `PRAGMA wal_checkpoint(TRUNCATE)`。 - NFS / SMB 上跑 SQLite **绝对不行**——锁机制基于 fcntl,网络文件系统的 fcntl 不可靠,会有静默数据损坏。 - `PRAGMA foreign_keys = ON` 每个连接都要单独设。Django 默认设了; 自己 sqlite3.connect() 时容易漏。 - 多进程(如 gunicorn 多 worker)共享 SQLite 没问题,但每个 worker 自己的连接都要设 pragma。