强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

rqlite 完全指南 / 第 7 章:备份与恢复

第 7 章:备份与恢复

掌握 rqlite 的备份策略、恢复流程以及自动备份方案。


7.1 备份概述

rqlite 提供两种备份格式,各有适用场景:

备份格式说明文件大小恢复速度兼容性
SQL dumpSQL 文本导出较大较慢通用 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','zs@example.com',28);
INSERT INTO users VALUES(2,'lisi','ls@example.com',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 小时30S3 + 本地二进制 + SQL
金融每小时90多区域 S3二进制 + SQL

7.7 本章小结

要点内容
备份格式SQL dump(通用)和二进制(快速)
备份来源始终从 Leader 获取以保证一致性
自动备份使用 cron + 脚本定期执行
恢复方式/db/load(SQL)或直接替换数据文件(二进制)
灾难恢复定期演练,保存多份备份在不同位置

上一章:第 6 章:集群管理 下一章:第 8 章:安全配置