第 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 中提问。