rqlite 完全指南 / 第 7 章:备份与恢复
第 7 章:备份与恢复
掌握 rqlite 的备份策略、恢复流程以及自动备份方案。
7.1 备份概述
rqlite 提供两种备份格式,各有适用场景:
| 备份格式 | 说明 | 文件大小 | 恢复速度 | 兼容性 |
|---|---|---|---|---|
| SQL dump | SQL 文本导出 | 较大 | 较慢 | 通用 SQLite |
| 二进制备份 | SQLite 原始数据文件 | 小 | 快 | 仅 rqlite |
备份原理
┌─────────────────────────────────────────────┐
│ 备份流程 │
│ │
│ 1. 客户端请求 /db/backup │
│ 2. rqlite 在 Leader 上创建 SQLite 快照 │
│ 3. 导出为 SQL dump 或二进制格式 │
│ 4. 通过 HTTP 流式返回给客户端 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 恢复流程 │
│ │
│ 1. 客户端发送 SQL dump 到 /db/load │
│ 2. rqlite 将 SQL 通过 Raft 日志复制到集群 │
│ 3. 所有节点应用 SQL 语句 │
│ 4. 数据恢复完成 │
└─────────────────────────────────────────────┘
重要: 备份始终从 Leader 节点获取,确保数据一致性。
7.2 手动备份
7.2.1 SQL Dump 备份
# SQL dump 格式备份
curl -s 'localhost:4001/db/backup' -o backup.sql
# 查看备份文件大小
ls -lh backup.sql
# 查看备份内容(前几行)
head -20 backup.sql
SQL dump 示例内容:
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL, age INTEGER DEFAULT 0);
INSERT INTO users VALUES(1,'zhangsan','[email protected]',28);
INSERT INTO users VALUES(2,'lisi','[email protected]',32);
CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product TEXT NOT NULL, quantity INTEGER, price REAL, status TEXT DEFAULT 'pending');
INSERT INTO orders VALUES(1,1,'笔记本',2,5999.0,'completed');
COMMIT;
7.2.2 二进制备份
# 二进制格式备份(更小、更快)
curl -s 'localhost:4001/db/backup?fmt=binary' -o backup.db
# 查看文件信息
file backup.db
ls -lh backup.db
7.2.3 备份格式选择
| 考量因素 | SQL Dump | 二进制备份 |
|---|---|---|
| 可读性 | ✅ 可直接查看 | ❌ 二进制 |
| 跨版本兼容 | ✅ 更好 | ⚠️ 可能不兼容 |
| 备份速度 | 较慢 | 快 |
| 恢复速度 | 较慢(逐条执行) | 快 |
| 文件大小 | 较大 | 小 |
| 可移植性 | ✅ 可导入任何 SQLite | 限 rqlite/SQLite |
建议: 生产环境使用二进制备份用于快速恢复,同时保留 SQL dump 用于灾难恢复和数据迁移。
7.3 恢复数据
7.3.1 从 SQL dump 恢复
# 恢复 SQL dump
curl -XPOST 'localhost:4001/db/load' \
-H 'Content-Type: text/plain' \
--data-binary @backup.sql
注意: 恢复操作会通过 Raft 日志复制到集群所有节点,保证数据一致性。
7.3.2 从二进制备份恢复
# 停止 rqlite 集群所有节点
systemctl stop rqlited
# 将备份文件复制到数据目录(每个节点都需操作)
cp backup.db /var/lib/rqlite/data/db.sqlite
# 清除 Raft 日志(需要重新建立集群)
rm -rf /var/lib/rqlite/data/raft
# 重启集群
systemctl start rqlited
# 重新建立集群(第一个节点无需 -join,其余节点加入)
7.3.3 恢复到新集群
# 1. 启动新集群的 Leader 节点
rqlited -node-id=new-node1 -disco-mode=off /tmp/new-cluster/node1 &
# 2. 加载备份
curl -XPOST 'localhost:4001/db/load' \
-H 'Content-Type: text/plain' \
--data-binary @backup.sql
# 3. 添加更多节点
rqlited -node-id=new-node2 \
-join=http://localhost:4001 \
/tmp/new-cluster/node2 &
rqlited -node-id=new-node3 \
-join=http://localhost:4001 \
/tmp/new-cluster/node3 &
7.4 自动备份方案
7.4.1 Cron 定时备份脚本
#!/bin/bash
# /opt/rqlite/scripts/backup.sh
# 自动备份脚本,建议通过 cron 定期执行
# 配置
RQLITE_HOST="${RQLITE_HOST:-localhost:4001}"
BACKUP_DIR="${BACKUP_DIR:-/var/backup/rqlite}"
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/rqlite/backup.log"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
}
# 检查 Leader 是否可用
leader_check=$(curl -s -o /dev/null -w "%{http_code}" \
"http://$RQLITE_HOST/status/leader" --connect-timeout 5)
if [ "$leader_check" != "200" ]; then
log "ERROR: Leader not available at $RQLITE_HOST (HTTP $leader_check)"
exit 1
fi
# SQL dump 备份
log "Starting SQL dump backup..."
sql_file="$BACKUP_DIR/rqlite_${DATE}.sql"
if curl -s "http://$RQLITE_HOST/db/backup" -o "$sql_file"; then
sql_size=$(du -h "$sql_file" | cut -f1)
log "SQL dump backup OK: $sql_file ($sql_size)"
else
log "ERROR: SQL dump backup failed"
exit 1
fi
# 二进制备份
log "Starting binary backup..."
bin_file="$BACKUP_DIR/rqlite_${DATE}.db"
if curl -s "http://$RQLITE_HOST/db/backup?fmt=binary" -o "$bin_file"; then
bin_size=$(du -h "$bin_file" | cut -f1)
log "Binary backup OK: $bin_file ($bin_size)"
else
log "ERROR: Binary backup failed"
exit 1
fi
# 清理过期备份
log "Cleaning up backups older than $BACKUP_RETENTION_DAYS days..."
deleted=$(find "$BACKUP_DIR" -name "rqlite_*" -mtime +$BACKUP_RETENTION_DAYS -delete -print | wc -l)
log "Deleted $deleted old backup files"
# 压缩当天的备份
gzip -f "$sql_file"
log "Compressed: ${sql_file}.gz"
log "Backup completed successfully"
配置 crontab:
# 每天凌晨 3 点执行备份
0 3 * * * /opt/rqlite/scripts/backup.sh
# 每 6 小时执行备份
0 */6 * * * /opt/rqlite/scripts/backup.sh
7.4.2 增量备份思路
rqlite 不直接支持增量备份,但可以通过以下方式实现类似效果:
#!/bin/bash
# incremental-backup.sh — 基于 WAL 文件的增量备份
DB_DIR="/var/lib/rqlite/data"
WAL_FILE="$DB_DIR/db.sqlite-wal"
LAST_BACKUP_MARKER="/var/backup/rqlite/.last_backup"
# 检查 WAL 文件是否有变化
if [ -f "$LAST_BACKUP_MARKER" ]; then
last_time=$(cat "$LAST_BACKUP_MARKER")
wal_mtime=$(stat -c %Y "$WAL_FILE" 2>/dev/null)
if [ "$wal_mtime" -le "$last_time" ]; then
echo "WAL unchanged, skipping backup"
exit 0
fi
fi
# 执行完整备份
/opt/rqlite/scripts/backup.sh
# 更新标记
date +%s > "$LAST_BACKUP_MARKER"
7.4.3 远程备份到 S3
#!/bin/bash
# s3-backup.sh — 备份到 S3 兼容存储
RQLITE_HOST="${RQLITE_HOST:-localhost:4001}"
S3_BUCKET="${S3_BUCKET:-my-rqlite-backups}"
S3_PREFIX="${S3_PREFIX:-rqlite}"
DATE=$(date +%Y%m%d_%H%M%S)
TEMP_DIR=$(mktemp -d)
# 备份
curl -s "http://$RQLITE_HOST/db/backup?fmt=binary" -o "$TEMP_DIR/rqlite_${DATE}.db"
# 压缩
gzip "$TEMP_DIR/rqlite_${DATE}.db"
# 上传到 S3
aws s3 cp "$TEMP_DIR/rqlite_${DATE}.db.gz" "s3://$S3_BUCKET/$S3_PREFIX/" \
--storage-class STANDARD_IA
# 清理
rm -rf "$TEMP_DIR"
# 清理 S3 上 30 天前的备份
aws s3 ls "s3://$S3_BUCKET/$S3_PREFIX/" | while read -r line; do
file_date=$(echo "$line" | awk '{print $1}')
file_name=$(echo "$line" | awk '{print $4}')
if [[ $(date -d "$file_date" +%s 2>/dev/null) -lt $(date -d "30 days ago" +%s) ]]; then
aws s3 rm "s3://$S3_BUCKET/$S3_PREFIX/$file_name"
fi
done
7.5 灾难恢复演练
7.5.1 恢复流程检查表
| 步骤 | 操作 | 验证 |
|---|---|---|
| 1 | 确认备份文件完整性 | 检查文件大小和修改时间 |
| 2 | 准备新的数据目录 | 确保磁盘空间充足 |
| 3 | 启动单节点并加载备份 | 确认数据正确 |
| 4 | 添加 Follower 节点 | 确认集群状态正常 |
| 5 | 验证数据完整性 | 执行数据校验查询 |
| 6 | 切换流量到新集群 | 监控错误率 |
7.5.2 完整恢复演练脚本
#!/bin/bash
# restore-drill.sh — 灾难恢复演练脚本
BACKUP_FILE="$1"
DRILL_DIR="/tmp/rqlite-drill-$(date +%Y%m%d)"
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup-file>"
exit 1
fi
echo "=== rqlite 灾难恢复演练 ==="
echo "备份文件: $BACKUP_FILE"
echo "演练目录: $DRILL_DIR"
echo ""
# 清理
rm -rf "$DRILL_DIR"
mkdir -p "$DRILL_DIR"/{node1,node2,node3}
# 步骤 1: 启动临时节点
echo "Step 1: 启动临时节点..."
rqlited -node-id=drill-node1 \
-http-addr=:15001 -raft-addr=:15002 \
-disco-mode=off \
"$DRILL_DIR/node1" &
LEADER_PID=$!
sleep 3
# 步骤 2: 加载备份
echo "Step 2: 加载备份数据..."
if [[ "$BACKUP_FILE" == *.sql ]] || [[ "$BACKUP_FILE" == *.sql.gz ]]; then
if [[ "$BACKUP_FILE" == *.gz ]]; then
gunzip -c "$BACKUP_FILE" | curl -s -XPOST 'localhost:15001/db/load' \
-H 'Content-Type: text/plain' --data-binary @-
else
curl -s -XPOST 'localhost:15001/db/load' \
-H 'Content-Type: text/plain' --data-binary @"$BACKUP_FILE"
fi
else
echo "Binary restore requires node restart, copying..."
kill $LEADER_PID 2>/dev/null
wait $LEADER_PID 2>/dev/null
cp "$BACKUP_FILE" "$DRILL_DIR/node1/db.sqlite"
rqlited -node-id=drill-node1 \
-http-addr=:15001 -raft-addr=:15002 \
-disco-mode=off \
"$DRILL_DIR/node1" &
LEADER_PID=$!
sleep 3
fi
# 步骤 3: 验证数据
echo "Step 3: 验证数据..."
tables=$(curl -s -G 'localhost:15001/db/query' \
--data-urlencode 'q=SELECT name FROM sqlite_master WHERE type="table"' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['results'][0].get('values',[])))")
echo " 发现 $tables 张表"
# 步骤 4: 启动 Follower 节点
echo "Step 4: 启动 Follower 节点..."
rqlited -node-id=drill-node2 \
-http-addr=:15011 -raft-addr=:15012 \
-join=http://localhost:15001 \
"$DRILL_DIR/node2" &
sleep 3
rqlited -node-id=drill-node3 \
-http-addr=:15021 -raft-addr=:15022 \
-join=http://localhost:15001 \
"$DRILL_DIR/node3" &
sleep 3
# 步骤 5: 验证集群
echo "Step 5: 验证集群状态..."
node_count=$(curl -s 'localhost:15001/nodes' | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(len(data.get('nodes', [])))
")
echo " 集群节点数: $node_count"
# 步骤 6: 测试写入和复制
echo "Step 6: 测试数据复制..."
curl -s -XPOST 'localhost:15001/db/execute' \
-H 'Content-Type: application/json' \
-d '[["CREATE TABLE IF NOT EXISTS drill_test (id INTEGER PRIMARY KEY, ts DATETIME DEFAULT CURRENT_TIMESTAMP)"]]' > /dev/null
curl -s -XPOST 'localhost:15001/db/execute' \
-H 'Content-Type: application/json' \
-d '[["INSERT INTO drill_test (id) VALUES (1)"]]' > /dev/null
# 从 Follower 读取验证
result=$(curl -s -G 'localhost:15011/db/query' \
--data-urlencode 'q=SELECT * FROM drill_test' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['results'][0].get('values',[])))")
if [ "$result" = "1" ]; then
echo " 数据复制: ✅ 通过"
else
echo " 数据复制: ❌ 失败"
fi
# 清理
echo ""
echo "=== 恢复演练完成 ==="
echo "清理演练环境..."
kill $LEADER_PID 2>/dev/null
pkill -f "drill-node" 2>/dev/null
rm -rf "$DRILL_DIR"
echo "演练环境已清理"
7.6 备份策略建议
| 环境 | 备份频率 | 保留天数 | 存储位置 | 格式 |
|---|---|---|---|---|
| 开发 | 每天 | 7 | 本地磁盘 | SQL dump |
| 测试 | 每天 | 14 | 本地磁盘 | SQL dump |
| 生产 | 每 6 小时 | 30 | S3 + 本地 | 二进制 + SQL |
| 金融 | 每小时 | 90 | 多区域 S3 | 二进制 + SQL |
7.7 本章小结
| 要点 | 内容 |
|---|---|
| 备份格式 | SQL dump(通用)和二进制(快速) |
| 备份来源 | 始终从 Leader 获取以保证一致性 |
| 自动备份 | 使用 cron + 脚本定期执行 |
| 恢复方式 | /db/load(SQL)或直接替换数据文件(二进制) |
| 灾难恢复 | 定期演练,保存多份备份在不同位置 |
上一章:第 6 章:集群管理 下一章:第 8 章:安全配置