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

dqlite 分布式 SQLite 教程 / 第 7 章:性能优化

第 7 章:性能优化

本章介绍 dqlite 的性能特征、优化策略,包括批量写入、读优化、同步策略、日志压缩和容量规划。


7.1 性能特征概览

7.1.1 dqlite 性能基准

以下数据基于典型硬件(4 核 CPU、SSD、千兆网络),仅供参考:

操作类型延迟吞吐量说明
单条 INSERT0.5-2ms500-2000 ops/s经过 Raft 复制
批量 INSERT(事务)2-10ms5000-20000 ops/s100 条/事务
单条 SELECT0.01-0.1ms10000-50000 ops/s本地读取
复杂 JOIN0.1-5ms1000-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平衡安全和性能
日志收集NORMALdqlite 已有 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_threshold1024写入频繁时增大到 4096
snapshot_trailing2048新节点加入时增大以减少全量快照

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-50MBGC 和 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 性能指标参考表

指标
单条写延迟> 10ms1-5ms< 1ms
批量写吞吐< 1000/s1000-5000/s> 5000/s
单条读延迟> 1ms0.1-1ms< 0.1ms
读吞吐< 5000/s5000-20000/s> 20000/s
内存使用> 500MB50-200MB< 50MB

本章小结

优化领域关键措施预期收益
批量写入事务批处理、预编译语句10-100x 吞吐提升
读优化Follower 读、索引优化减轻 Leader 负载
同步策略NORMAL 模式(dqlite 场景)2-3x 写性能提升
日志压缩调整快照阈值减少 I/O 峰值
网络优化同机房部署、TCP 调优降低写延迟

下一章

第 8 章:安全配置 — 学习如何为 dqlite 集群配置 TLS 加密、认证和访问控制。