dqlite 分布式 SQLite 教程 / 第 10 章:生产最佳实践
第 10 章:生产最佳实践
本章总结 dqlite 在生产环境中的最佳实践,包括技术选型决策、容量规划、监控告警、运维 SOP 和常见陷阱。
10.1 何时选择 dqlite
10.1.1 决策树
需要分布式数据库?
│
├── 否 → 直接用 SQLite
│
└── 是 → 数据量多大?
│
├── > 10GB → 考虑 PostgreSQL / CockroachDB
│
└── ≤ 10GB → 需要 HTTP API?
│
├── 是 → 考虑 rqlite
│
└── 否 → 需要嵌入式?
│
├── 是 → 平台是 Linux?
│ │
│ ├── 是 → ✅ 选择 dqlite
│ │
│ └── 否 → 考虑 rqlite 或 etcd
│
└── 否 → 考虑 rqlite / Consul / etcd
10.1.2 dqlite 适用场景详细分析
| 场景 |
评分 |
说明 |
| 容器管理器/运行时 |
⭐⭐⭐⭐⭐ |
LXD 验证的最佳场景 |
| Kubernetes 数据存储 |
⭐⭐⭐⭐ |
MicroK8s 使用,适合中小规模 |
| 边缘计算/IoT |
⭐⭐⭐⭐ |
轻量级,资源占用小 |
| 嵌入式 Linux 设备 |
⭐⭐⭐⭐ |
无外部依赖 |
| 配置管理 |
⭐⭐⭐ |
可用,但 etcd 可能更成熟 |
| Web 应用后端 |
⭐⭐ |
rqlite 更合适(HTTP API) |
| 高吞吐数据管道 |
⭐ |
不适合,考虑 Kafka + 数据库 |
| 多数据中心 |
⭐ |
不适合,考虑 CockroachDB |
10.1.3 与替代方案对比
| 维度 |
dqlite |
etcd |
Consul |
rqlite |
| 存储模型 |
关系型 (SQL) |
KV |
KV |
关系型 (SQL) |
| 查询能力 |
完整 SQL |
无 |
有限 |
完整 SQL |
| 嵌入式 |
✅ |
❌ |
❌ |
❌ |
| 依赖 |
libuv |
无 |
无 |
无 |
| 数据量上限 |
~10GB |
~8GB |
~10GB |
~10GB |
| 成熟度 |
高 |
最高 |
高 |
高 |
| 复杂度 |
中 |
低 |
中 |
低 |
10.2 容量规划
10.2.1 节点规格建议
| 规模 |
节点数 |
CPU |
内存 |
存储 |
网络 |
| 开发测试 |
1 |
1 核 |
512MB |
1GB SSD |
任意 |
| 小型生产 |
3 |
2 核 |
1GB |
10GB SSD |
千兆 |
| 中型生产 |
3 |
4 核 |
4GB |
50GB SSD |
千兆 |
| 大型生产 |
5 |
8 核 |
8GB |
100GB SSD |
万兆 |
10.2.2 数据容量评估
存储空间计算:
数据库大小 = 原始数据 + 索引 + WAL 缓冲 + Raft 日志
示例计算:
原始数据: 1GB
索引 (约 30%): 0.3GB
WAL 缓冲: 0.1GB
Raft 日志 (2x): 0.2GB (快照后保留)
安全余量 (20%): 0.32GB
─────────────────────
总计: ~2GB
磁盘建议: 原始数据 × 3 倍
10.2.3 性能容量计算
写入容量(3 节点集群):
单条写 QPS ≈ 1000-2000 (取决于硬件和网络)
批量写 QPS ≈ 5000-20000 (100 条/事务)
读取容量:
单节点读 QPS ≈ 10000-50000
3 节点分散读 ≈ 30000-150000
连接数:
推荐 10-50 个连接(SQLite 写入串行)
并发事务:
写事务: 串行(1 个并发)
读事务: 并发(受限于连接数)
10.2.4 容量规划表
| 指标 |
公式 |
示例 |
| 所需磁盘 |
数据量 × 3 |
10GB 数据 → 30GB 磁盘 |
| 所需内存 |
max(1GB, 数据量 × 0.1 + 256MB) |
10GB 数据 → 1.25GB |
| 所需 CPU |
基础 1 核 + 每 1000 QPS 加 1 核 |
5000 QPS → 6 核 |
| 网络带宽 |
写入 QPS × 平均事务大小 × 节点数 |
1000 × 1KB × 3 = 3MB/s |
10.3 监控告警
10.3.1 关键监控指标
| 类别 |
指标 |
阈值 |
告警级别 |
| 可用性 |
节点存活数 |
< Quorum |
P0 (严重) |
| 可用性 |
Leader 是否存在 |
无 Leader > 5s |
P0 |
| 性能 |
写延迟 P99 |
> 10ms |
P1 (警告) |
| 性能 |
读延迟 P99 |
> 5ms |
P1 |
| 性能 |
写 QPS |
> 容量 80% |
P2 (关注) |
| 存储 |
磁盘使用率 |
> 80% |
P1 |
| 存储 |
数据库大小 |
> 容量 70% |
P1 |
| Raft |
日志滞后 |
> 1000 条 |
P1 |
| Raft |
快照耗时 |
> 30s |
P2 |
| 网络 |
节点间延迟 |
> 10ms |
P1 |
10.3.2 Prometheus 监控配置
# prometheus/alert-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: dqlite-alerts
spec:
groups:
- name: dqlite
rules:
# 节点存活检查
- alert: DqliteNodeDown
expr: up{job="dqlite"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "dqlite 节点 {{ $labels.instance }} 不可用"
description: "节点已离线超过 1 分钟"
# Leader 缺失
- alert: DqliteNoLeader
expr: dqlite_raft_leader_id == 0
for: 5s
labels:
severity: critical
annotations:
summary: "dqlite 集群无 Leader"
# 写延迟过高
- alert: DqliteHighWriteLatency
expr: histogram_quantile(0.99, dqlite_write_latency_seconds_bucket) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "dqlite 写延迟 P99 > 10ms"
# 磁盘空间
- alert: DqliteDiskSpaceHigh
expr: (dqlite_disk_used_bytes / dqlite_disk_total_bytes) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "dqlite 磁盘使用率 > 80%"
# Raft 日志滞后
- alert: DqliteRaftLogLag
expr: dqlite_raft_log_entries - dqlite_raft_commit_index > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "Raft 日志滞后 > 1000 条"
10.3.3 Grafana Dashboard
关键面板建议:
┌─────────────────────────────────────────────────────────────┐
│ dqlite Dashboard │
├─────────────┬─────────────┬─────────────┬──────────────────┤
│ 节点状态 │ Leader 状态 │ 集群大小 │ 当前任期 │
│ ● 3/3 │ ● Node 1 │ ● 3 │ ● 42 │
├─────────────┴─────────────┴─────────────┴──────────────────┤
│ 写延迟 (P50/P95/P99) │
│ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.1ms / 4.5ms / 8.2ms│
├─────────────────────────────────────────────────────────────┤
│ QPS (写入/读取) │
│ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇ 写: 1.2K 读: 8.5K │
├──────────────┬──────────────┬──────────────────────────────┤
│ 磁盘使用率 │ 数据库大小 │ Raft 日志条目数 │
│ ▓▓▓▓▓░░░ 45% │ 2.3GB │ 1,234 │
├──────────────┴──────────────┴──────────────────────────────┤
│ 快照状态 │
│ 上次快照: 2 分钟前 │ 耗时: 1.2s │ 日志已清理: 1024 条 │
└─────────────────────────────────────────────────────────────┘
10.4 运维 SOP
10.4.1 日常巡检
#!/bin/bash
# daily-check.sh - dqlite 日常巡检脚本
set -euo pipefail
echo "=== dqlite Daily Check - $(date) ==="
# 1. 检查节点存活
echo -e "\n[1] Node Health:"
for node in "127.0.0.1:9001" "127.0.0.1:9002" "127.0.0.1:9003"; do
if nc -z -w2 ${node/:/ } 2>/dev/null; then
echo " ✓ $node - UP"
else
echo " ✗ $node - DOWN"
fi
done
# 2. 检查 Leader
echo -e "\n[2] Leader Status:"
# 通过客户端查询集群信息
echo " (执行集群状态查询...)"
# 3. 检查磁盘使用
echo -e "\n[3] Disk Usage:"
for dir in /var/lib/dqlite/node{1,2,3}; do
if [ -d "$dir" ]; then
usage=$(du -sh "$dir" 2>/dev/null | cut -f1)
echo " $dir: $usage"
fi
done
# 4. 检查日志
echo -e "\n[4] Recent Errors:"
journalctl -u dqlite --since "24 hours ago" --priority=err --no-pager | tail -5
# 5. 检查数据库大小
echo -e "\n[5] Database Size:"
find /var/lib/dqlite -name "*.db" -exec ls -lh {} \;
echo -e "\n=== Check Complete ==="
10.4.2 备份 SOP
#!/bin/bash
# backup.sh - dqlite 备份脚本
set -euo pipefail
BACKUP_DIR="/backup/dqlite/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
echo "Starting dqlite backup to $BACKUP_DIR..."
# 方式 1:使用 dqlite dump 工具
dqlite-dump \
--address "127.0.0.1:9001" \
--output "$BACKUP_DIR/dump.sql"
# 方式 2:复制数据文件(需要先暂停写入或使用一致性快照)
# 停止一个 Follower 节点,复制其数据目录
# systemctl stop dqlite-node-3
# cp -r /var/lib/dqlite/node3/* "$BACKUP_DIR/"
# systemctl start dqlite-node-3
# 压缩
gzip "$BACKUP_DIR/dump.sql"
# 上传到远程存储
# aws s3 sync "$BACKUP_DIR" "s3://backups/dqlite/$(basename $BACKUP_DIR)/"
# 清理 30 天前的备份
find /backup/dqlite -maxdepth 1 -mtime +30 -exec rm -rf {} \;
echo "Backup completed: $BACKUP_DIR"
10.4.3 恢复 SOP
#!/bin/bash
# restore.sh - dqlite 恢复脚本
set -euo pipefail
BACKUP_FILE="$1"
TARGET_ADDRESS="${2:-127.0.0.1:9001}"
if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
echo "=== dqlite Restore Procedure ==="
echo "Backup: $BACKUP_FILE"
echo "Target: $TARGET_ADDRESS"
echo ""
echo "WARNING: This will overwrite existing data!"
read -p "Continue? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "Aborted."
exit 0
fi
# 1. 停止所有节点
echo "[1] Stopping all dqlite nodes..."
# systemctl stop dqlite-node-1 dqlite-node-2 dqlite-node-3
# 2. 清空数据目录
echo "[2] Clearing data directories..."
# rm -rf /var/lib/dqlite/node1/* /var/lib/dqlite/node2/* /var/lib/dqlite/node3/*
# 3. 在第一个节点恢复数据
echo "[3] Restoring data..."
# dqlite-restore --backup "$BACKUP_FILE" --data-dir /var/lib/dqlite/node1
# 4. 启动第一个节点
echo "[4] Starting first node..."
# systemctl start dqlite-node-1
# 5. 等待节点就绪
echo "[5] Waiting for node to be ready..."
# sleep 5
# 6. 重新初始化集群
echo "[6] Re-initializing cluster..."
# dqlite-add-node --leader 127.0.0.1:9001 --id 2 --address 127.0.0.1:9002
# dqlite-add-node --leader 127.0.0.1:9001 --id 3 --address 127.0.0.1:9003
# 7. 启动其他节点
echo "[7] Starting remaining nodes..."
# systemctl start dqlite-node-2 dqlite-node-3
echo ""
echo "Restore completed. Please verify data integrity."
10.4.4 滚动重启 SOP
#!/bin/bash
# rolling-restart.sh - 滚动重启 dqlite 集群
set -euo pipefail
NODES=("dqlite-node-1" "dqlite-node-2" "dqlite-node-3")
for node in "${NODES[@]}"; do
echo "=== Restarting $node ==="
# 1. 确认集群健康(当前节点不是唯一 Leader)
echo "[1] Checking cluster health..."
# ... 检查逻辑 ...
# 2. 如果是 Leader,先转移
echo "[2] Transferring leadership if needed..."
# ... 转移逻辑 ...
# 3. 停止节点
echo "[3] Stopping $node..."
systemctl stop "$node"
# 4. 等待集群稳定
echo "[4] Waiting for cluster to stabilize..."
sleep 5
# 5. 启动节点
echo "[5] Starting $node..."
systemctl start "$node"
# 6. 等待节点加入集群
echo "[6] Waiting for node to rejoin..."
sleep 10
# 7. 验证节点状态
echo "[7] Verifying node status..."
# ... 验证逻辑 ...
echo "=== $node restarted successfully ==="
echo ""
done
echo "All nodes restarted."
10.5 常见陷阱与避坑指南
10.5.1 部署陷阱
| 陷阱 |
后果 |
正确做法 |
| 使用偶数节点 |
不增加容错,浪费资源 |
始终使用 3、5、7 节点 |
| 跨机房部署 |
Raft 对延迟敏感 |
同机房或同城部署 |
| 使用 NFS 存储 |
文件锁问题,数据损坏 |
使用本地 SSD |
| 不做备份 |
数据丢失无法恢复 |
定期备份 + 验证恢复 |
| 不同步时钟 |
选举异常,日志不一致 |
使用 NTP 同步 |
10.5.2 开发陷阱
| 陷阱 |
后果 |
正确做法 |
| 每条 INSERT 一个事务 |
性能极差 |
批量操作在同一事务中 |
| 不处理 BUSY 错误 |
偶发失败 |
实现指数退避重试 |
| 读取不走 Leader |
数据陈旧 |
确认一致性需求 |
| 不使用参数绑定 |
SQL 注入风险 |
始终使用 ? 参数 |
| 连接池配置不当 |
连接泄漏或过少 |
合理配置 MaxOpenConns |
10.5.3 运维陷阱
| 陷阱 |
后果 |
正确做法 |
| 不监控集群状态 |
故障不可感知 |
配置告警规则 |
| 不测试恢复流程 |
真正恢复时手忙脚乱 |
定期演练恢复 |
| 一次性变更多个节点 |
Quorum 混乱 |
一次只变更一个 |
| 不检查磁盘空间 |
写入失败 |
监控磁盘使用率 |
| 不更新版本 |
错过安全补丁 |
跟踪版本发布 |
10.6 性能调优速查表
| 优化项 |
配置 |
预期收益 |
| 批量写入 |
100-500 条/事务 |
10-100x |
| 预编译语句 |
Prepare + Execute |
2-3x |
| 同步策略 NORMAL |
PRAGMA synchronous=NORMAL |
2-3x |
| 内存缓存 |
PRAGMA cache_size=-16000 |
读性能提升 |
| 内存映射 |
PRAGMA mmap_size=256MB |
大数据集读提升 |
| WAL 自动检查点 |
PRAGMA wal_autocheckpoint=1000 |
控制 WAL 大小 |
| SSD 存储 |
使用 NVMe SSD |
显著降低 I/O 延迟 |
| 同机房部署 |
节点延迟 < 1ms |
降低写延迟 |
10.7 故障排查速查表
| 症状 |
可能原因 |
排查步骤 |
解决方案 |
| 写入超时 |
Leader 不可达 |
1. 检查集群状态 2. 检查网络 |
恢复 Leader 或网络 |
| 读取数据陈旧 |
读了 Follower |
1. 确认读一致性需求 |
配置 Leader 读 |
| 集群无法启动 |
配置不一致 |
1. 对比各节点配置 |
统一配置 |
| 节点反复选举 |
网络不稳定 |
1. 检查节点间延迟 |
改善网络 |
| 磁盘空间不足 |
数据增长 |
1. 检查数据库大小 |
清理数据或扩容 |
| 内存占用高 |
缓存配置过大 |
1. 检查 PRAGMA cache_size |
减小缓存 |
| 快照失败 |
I/O 错误 |
1. 检查磁盘健康 |
修复磁盘 |
10.8 生产检查清单
10.8.1 上线前检查
| 检查项 |
状态 |
说明 |
| □ 节点数为奇数 |
|
3、5、7 |
| □ TLS 已启用 |
|
双向认证 |
| □ 防火墙已配置 |
|
仅允许必要端口 |
| □ 数据目录权限正确 |
|
专用用户运行 |
| □ 备份策略已实施 |
|
定期备份 + 异地存储 |
| □ 监控已部署 |
|
Prometheus + Grafana |
| □ 告警规则已配置 |
|
节点故障、Leader 丢失等 |
| □ 日志收集已配置 |
|
ELK / Loki |
| □ 时钟同步 |
|
NTP 已配置 |
| □ 存储类型正确 |
|
SSD,非 NFS |
| □ 容量规划完成 |
|
磁盘、内存、CPU |
| □ 恢复流程已测试 |
|
定期演练 |
| □ 滚动更新流程已验证 |
|
无停机更新 |
| □ 文档已更新 |
|
运维手册 |
10.8.2 定期检查(每月)
| 检查项 |
动作 |
| 数据库大小增长趋势 |
预测何时需要扩容 |
| 备份恢复测试 |
验证备份可用性 |
| 证书有效期 |
提前 30 天续期 |
| 安全补丁 |
更新到最新版本 |
| 性能基准测试 |
确认性能未退化 |
本章小结
| 实践领域 |
关键要点 |
| 技术选型 |
dqlite 适合嵌入式 Linux 场景,数据量 < 10GB |
| 容量规划 |
磁盘 3 倍数据量,内存 = 数据量 × 0.1 + 256MB |
| 监控 |
节点存活、Leader 状态、延迟、磁盘、Raft 日志 |
| 备份 |
每日备份 + 异地存储 + 定期恢复演练 |
| 避坑 |
奇数节点、同机房、SSD、NTP、批量事务 |
全书完
恭喜你完成了 dqlite 分布式 SQLite 教程的全部 10 章学习!
快速回顾
进一步学习
如有问题,欢迎在 dqlite GitHub Issues 中提问。