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 加密、认证和访问控制。