dqlite 分布式 SQLite 教程 / 第 7 章:性能优化
第 7 章:性能优化
本章介绍 dqlite 的性能特征、优化策略,包括批量写入、读优化、同步策略、日志压缩和容量规划。
7.1 性能特征概览
7.1.1 dqlite 性能基准
以下数据基于典型硬件(4 核 CPU、SSD、千兆网络),仅供参考:
| 操作类型 | 延迟 | 吞吐量 | 说明 |
|---|
| 单条 INSERT | 0.5-2ms | 500-2000 ops/s | 经过 Raft 复制 |
| 批量 INSERT(事务) | 2-10ms | 5000-20000 ops/s | 100 条/事务 |
| 单条 SELECT | 0.01-0.1ms | 10000-50000 ops/s | 本地读取 |
| 复杂 JOIN | 0.1-5ms | 1000-5000 ops/s | 取决于数据量 |
7.1.2 性能瓶颈分析
写入路径:
Client → 网络 → Leader → Raft 日志 → 复制到 Follower → Quorum 确认 → Apply
├── 网络延迟 ────────────┤
│ ├── Raft 共识 ──────────┤
│ │ ├── SQLite Apply ─┤
│ │ │ │
└── 通常 0.5-5ms 总延迟 ──────────────────────────────────────────┘
读取路径:
Client → 网络 → 本地 SQLite 查询 → 返回
│ │
└── 通常 0.01-0.1ms(本地读取)─────┘
| 瓶颈 | 影响 | 优化方向 |
|---|
| 网络延迟 | 写入延迟的主要组成部分 | 同机房部署、优化网络 |
| Raft 复制 | 需要多数节点确认 | 减少节点数(3→3, 5→3 不可行) |
| SQLite 写入 | WAL 模式下受限于 fsync | 使用 SSD、调整同步策略 |
| 日志大小 | 大量日志影响恢复速度 | 快照压缩 |
7.2 批量写入优化
7.2.1 事务批处理
最重要的优化手段:将多条写操作放在同一个事务中。
// ❌ 差:每条 INSERT 一个事务(每条都触发 Raft 复制)
func badInsert(db *sql.DB, records []Record) error {
for _, r := range records {
_, err := db.Exec("INSERT INTO logs (msg) VALUES (?)", r.Msg)
if err != nil {
return err
}
}
return nil
}
// 1000 条记录 = 1000 次 Raft 复制 ≈ 1000-5000ms
// ✅ 好:所有 INSERT 在一个事务中(一次 Raft 复制)
func goodInsert(db *sql.DB, records []Record) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO logs (msg) VALUES (?)")
if err != nil {
return err
}
defer stmt.Close()
for _, r := range records {
if _, err := stmt.Exec(r.Msg); err != nil {
return err
}
}
return tx.Commit()
}
// 1000 条记录 = 1 次 Raft 复制 ≈ 2-10ms
7.2.2 批量大小建议
| 批量大小 | 优势 | 劣势 | 推荐场景 |
|---|
| 1(不批处理) | 无 | 延迟最高 | 不推荐 |
| 10-50 | 平衡 | 需管理部分失败 | 实时性要求高的场景 |
| 100-500 | 高吞吐 | 原子性风险增大 | 日志收集、批量导入 |
| 1000+ | 最高吞吐 | 长事务、内存占用大 | 大批量数据迁移 |
7.2.3 流式批量写入
func streamInsert(ctx context.Context, db *sql.DB, ch <-chan Record, batchSize int) error {
batch := make([]Record, 0, batchSize)
flush := func() error {
if len(batch) == 0 {
return nil
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx,
"INSERT INTO records (data, timestamp) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
for _, r := range batch {
if _, err := stmt.ExecContext(ctx, r.Data, r.Timestamp); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
batch = batch[:0] // 重置切片
return nil
}
for {
select {
case record, ok := <-ch:
if !ok {
return flush() // 通道关闭,刷新最后一批
}
batch = append(batch, record)
if len(batch) >= batchSize {
if err := flush(); err != nil {
return err
}
}
case <-ctx.Done():
return ctx.Err()
}
}
}
7.2.4 预编译语句复用
// ❌ 差:每次执行都编译 SQL
func badQuery(db *sql.DB) {
for i := 0; i < 1000; i++ {
db.Exec("INSERT INTO test (val) VALUES (?)", i)
}
}
// ✅ 好:预编译一次,多次执行
func goodQuery(db *sql.DB) {
stmt, err := db.Prepare("INSERT INTO test (val) VALUES (?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for i := 0; i < 1000; i++ {
stmt.Exec(i)
}
}
7.3 读优化
7.3.1 Follower 读取
默认情况下,所有请求(包括读)都发送到 Leader。dqlite 支持配置 Follower 参与读取以分担 Leader 压力:
| 读模式 | 一致性 | 性能 | 适用场景 |
|---|
| Leader 读 | 线性一致性 | 中 | 需要最新数据的场景 |
| Follower 读 | 最终一致性 | 高 | 可容忍短暂数据陈旧 |
// Follower 读取配置(通过 go-dqlite driver option)
drv, err := driver.New(nodeStore,
driver.WithDialFunc(func(ctx context.Context, address string) (net.Conn, error) {
// 自定义连接逻辑,可以选择连接到 Follower
return net.Dial("tcp", address)
}),
)
7.3.2 查询优化
// ❌ 差:SELECT * 返回所有列
rows, _ := db.Query("SELECT * FROM users WHERE status = 'active'")
// ✅ 好:只查询需要的列
rows, _ := db.Query("SELECT id, name, email FROM users WHERE status = 'active'")
// ✅ 好:使用 LIMIT 限制返回行数
rows, _ := db.Query("SELECT id, name FROM users ORDER BY created_at DESC LIMIT 100")
// ✅ 好:使用索引覆盖查询
// 创建覆盖索引
db.Exec("CREATE INDEX idx_users_status_name ON users(status, name)")
// 查询只需扫描索引
rows, _ = db.Query("SELECT name FROM users WHERE status = 'active'")
7.3.3 索引策略
// 创建有效的索引
_, err := db.Exec(`
-- 常用查询索引
CREATE INDEX IF NOT EXISTS idx_orders_customer_date
ON orders(customer_id, created_at DESC);
-- 覆盖索引(包含查询所需的所有列)
CREATE INDEX IF NOT EXISTS idx_orders_covering
ON orders(customer_id, status)
INCLUDE (total, created_at);
-- 部分索引(只索引符合条件的行)
CREATE INDEX IF NOT EXISTS idx_orders_active
ON orders(customer_id)
WHERE status = 'active';
`)
索引使用原则:
| 原则 | 说明 |
|---|
| 为 WHERE 条件列创建索引 | 加速过滤 |
| 为 ORDER BY 列创建索引 | 避免排序 |
| 为 JOIN 列创建索引 | 加速连接 |
| 避免过度索引 | 每个索引增加写入开销 |
| 使用 EXPLAIN 验证 | 确认索引被使用 |
// 查看查询计划
rows, _ := db.Query("EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = ?")
// 输出应包含 "USING INDEX" 以确认使用了索引
7.3.4 连接池优化
db := sql.OpenDB(drv)
// 根据场景调整连接池
db.SetMaxOpenConns(10) // 最大连接数
db.SetMaxIdleConns(5) // 空闲连接数
db.SetConnMaxLifetime(0) // 连接不过期
db.SetConnMaxIdleTime(5 * time.Minute) // 空闲连接 5 分钟后关闭
7.4 同步策略
SQLite 的同步(synchronous)策略影响数据安全性和写入性能。
7.4.1 SQLite 同步模式
| 模式 | 说明 | 安全性 | 性能 |
|---|
FULL | 每次写操作后 fsync | 最高 | 最低 |
NORMAL | 关键时刻 fsync | 高 | 中 |
OFF | 从不 fsync | 低(可能丢数据) | 最高 |
注意: dqlite 有自己的日志持久化机制(Raft 日志),因此 SQLite 层面的同步策略选择需要与 Raft 持久化结合考虑。
7.4.2 PRAGMA 配置建议
// 优化 SQLite PRAGMA(在 dqlite 内部或通过连接设置)
pragmas := map[string]string{
// 同步策略 - NORMAL 在 dqlite 场景下足够安全
// 因为 Raft 日志已经提供了额外的持久化保证
"synchronous": "NORMAL",
// WAL 模式(dqlite 默认使用)
"journal_mode": "WAL",
// 缓存大小(页数,负值为 KB)
"cache_size": "-8000", // 8MB
// 临时表存储位置
"temp_store": "MEMORY",
// 内存映射 I/O(适用于数据量 < 内存的情况)
"mmap_size": "268435456", // 256MB
// 页面大小
"page_size": "4096",
// WAL 自动检查点阈值
"wal_autocheckpoint": "1000",
}
for k, v := range pragmas {
_, err := db.Exec(fmt.Sprintf("PRAGMA %s = %s", k, v))
if err != nil {
log.Printf("Warning: PRAGMA %s failed: %v", k, err)
}
}
7.4.3 同步策略选择指南
| 场景 | 推荐策略 | 理由 |
|---|
| 金融交易 | FULL | 不能丢数据 |
| 一般业务 | NORMAL | 平衡安全和性能 |
| 日志收集 | NORMAL | dqlite 已有 Raft 保护 |
| 缓存数据 | 可考虑 OFF | 数据可重建 |
7.5 日志压缩与快照优化
7.5.1 快照参数调整
/* C API: 调整快照参数 */
/* 快照阈值:日志条目数量超过此值时触发快照 */
dqlite_node_set_snapshot_threshold(node, 1024); /* 默认 1024 */
/* 快照后保留:快照完成后保留的最近日志条目数 */
dqlite_node_set_snapshot_trailing(node, 2048); /* 默认 2048 */
| 参数 | 默认值 | 调优建议 |
|---|
snapshot_threshold | 1024 | 写入频繁时增大到 4096 |
snapshot_trailing | 2048 | 新节点加入时增大以减少全量快照 |
7.5.2 快照对性能的影响
快照过程中的性能影响:
正常写入: ────────────────────────────────────▶
│ │
快照创建: ├────────────┤
│ I/O 峰值 │
│ 写入暂停 │
└────────────┘
~100ms-1s (取决于数据库大小)
7.5.3 减少快照开销
| 方法 | 说明 |
|---|
| 增大快照阈值 | 减少快照频率 |
| 使用 SSD | 快照是 I/O 密集操作 |
| 减小数据库大小 | 删除不必要的数据 |
| 预分配磁盘空间 | 避免快照时的文件扩展 |
7.6 内存优化
7.6.1 SQLite 缓存
// 设置缓存大小
db.Exec("PRAGMA cache_size = -16000") // 16MB
// 查看缓存使用情况
var cacheSize int
db.QueryRow("PRAGMA cache_size").Scan(&cacheSize)
fmt.Printf("Cache size: %d pages\n", cacheSize)
7.6.2 内存使用估算
| 组件 | 内存使用 | 说明 |
|---|
| SQLite 缓存 | cache_size × page_size | 默认约 2MB |
| Raft 日志 | 条目数 × 平均条目大小 | 通常 < 10MB |
| 网络缓冲 | 连接数 × 缓冲区大小 | 每连接 ~64KB |
| Go 运行时 | ~10-50MB | GC 和 goroutine |
| 总计 | ~50-100MB | 典型单节点 |
7.7 网络优化
7.7.1 网络参数调优
# Linux 系统级优化
# 增大 TCP 缓冲区
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
# 启用 TCP 快速打开
sysctl -w net.ipv4.tcp_fastopen=3
# 调整 TCP 拥塞控制
sysctl -w net.ipv4.tcp_congestion_control=bbr
7.7.2 网络拓扑优化
推荐部署拓扑:
同机房部署(延迟 < 1ms):
┌─────────────┐
│ 机房 A │
│ Node 1 │
│ Node 2 │
│ Node 3 │
└─────────────┘
延迟:0.1-0.5ms,吞吐最优
同城市跨机房(延迟 1-5ms):
┌──────┐ ┌──────┐ ┌──────┐
│机房 A│ │机房 B│ │机房 C│
│Node 1│ │Node 2│ │Node 3│
└──────┘ └──────┘ └──────┘
延迟:1-5ms,可接受
跨城市(延迟 > 20ms):
❌ 不推荐用于 dqlite
Raft 选举和复制对延迟敏感
7.8 性能测试
7.8.1 基准测试代码
package bench
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
)
func BenchmarkSingleInsert(b *testing.B) {
db := setupDB(b)
defer db.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := db.Exec("INSERT INTO bench (val) VALUES (?)", i)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBatchInsert(b *testing.B) {
batchSize := 100
db := setupDB(b)
defer db.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx, _ := db.Begin()
stmt, _ := tx.Prepare("INSERT INTO bench (val) VALUES (?)")
for j := 0; j < batchSize; j++ {
stmt.Exec(i*batchSize + j)
}
stmt.Close()
tx.Commit()
}
}
func BenchmarkSelect(b *testing.B) {
db := setupDB(b)
defer db.Close()
// 预填充数据
tx, _ := db.Begin()
stmt, _ := tx.Prepare("INSERT INTO bench (val) VALUES (?)")
for i := 0; i < 10000; i++ {
stmt.Exec(i)
}
stmt.Close()
tx.Commit()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var val int
db.QueryRow("SELECT val FROM bench WHERE id = ?", i%10000+1).Scan(&val)
}
}
func BenchmarkConcurrentRead(b *testing.B) {
db := setupDB(b)
defer db.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var count int
db.QueryRow("SELECT COUNT(*) FROM bench").Scan(&count)
}
})
}
7.8.2 性能指标参考表
| 指标 | 差 | 中 | 优 |
|---|
| 单条写延迟 | > 10ms | 1-5ms | < 1ms |
| 批量写吞吐 | < 1000/s | 1000-5000/s | > 5000/s |
| 单条读延迟 | > 1ms | 0.1-1ms | < 0.1ms |
| 读吞吐 | < 5000/s | 5000-20000/s | > 20000/s |
| 内存使用 | > 500MB | 50-200MB | < 50MB |
本章小结
| 优化领域 | 关键措施 | 预期收益 |
|---|
| 批量写入 | 事务批处理、预编译语句 | 10-100x 吞吐提升 |
| 读优化 | Follower 读、索引优化 | 减轻 Leader 负载 |
| 同步策略 | NORMAL 模式(dqlite 场景) | 2-3x 写性能提升 |
| 日志压缩 | 调整快照阈值 | 减少 I/O 峰值 |
| 网络优化 | 同机房部署、TCP 调优 | 降低写延迟 |
下一章
→ 第 8 章:安全配置 — 学习如何为 dqlite 集群配置 TLS 加密、认证和访问控制。