rqlite 深度实战:当轻量级 SQLite 遇上 Raft 共识——从分布式架构到生产级部署的完全指南(2026)
一、背景:我们为什么需要另一个分布式数据库?
1.1 分布式数据库的两难困境
2026 年的后端开发者面临一个尴尬的局面:需要高可用时,往往被迫在「重量级」和「放弃关系模型」之间做选择。
- PostgreSQL + Patroni / Stolon:功能强大但运维极其复杂。一个三节点的 PG 高可用集群,你需要配置流复制、设置 WAL 归档、管理 failover 流程、处理脑裂问题。中小团队根本养不起 DBA。
- etcd / ZooKeeper / Consul:高可用做得不错,但只存 KV,不支持 SQL 查询。你想查个多条件数据?得自己写客户端逻辑拼。
- MongoDB / Cassandra:放弃了事务和强一致性,最终一致性带来的心智负担不小。
- 单节点 SQLite:轻量、零配置,但只有一个节点,挂了就没了。
1.2 rqlite 的定位
rqlite 巧妙地在这些极端之间找到了一个甜点位置:
一个用 Raft 共识协议复制数据的 SQLite 集群,通过 HTTP API 暴露完整的 SQL 能力。
它不追求写入吞吐量的极致(因为每次写都要过 Raft 日志),但它追求的是极致的运维简洁 + 高可用 + 完整的关系数据库能力。
你可以把它理解成:如果我想要 etcd 的运维体验和可靠性,但又想要 SQLite 的关系查询能力——那 rqlite 就是答案。
1.3 它适合什么场景?
从 rqlite 的 GitHub 仓库(18,000+ Stars)和社区反馈来看,典型的成功场景包括:
| 场景 | 为什么选 rqlite | 替代方案对比 |
|---|---|---|
| 边缘计算 / IoT 设备 | 单二进制部署,资源占用极低 | PG太重,etcd没有SQL |
| 微服务配置中心 | SQL查询配置项比KV灵活 | etcd需要额外开发 |
| 开发者工具 / SaaS元数据 | 零依赖,嵌入CI/CD管道 | 全量PG大材小用 |
| 读写分离的全球分发 | 只读节点扩展,每个节点全量数据 | PG流复制配置复杂 |
| K8s 轻量级数据存储 | 替换部分PG和etcd场景 | Replicated公司已验证 |
反面场景:你的业务每秒需要数万次写入,或者单表数据量超过几十 GB——那 rqlite 不适合你,请继续用 PG/MySQL。
二、核心架构:SQLite 遇上 Raft
2.1 架构总览
rqlite 的架构本质上是一种分层设计:
┌─────────────────────────────────────┐
│ HTTP API Layer │ ← /db/execute, /db/query, /db/request
├─────────────────────────────────────┤
│ Raft Consensus Layer │ ← Leader election, Log replication
├─────────────────────────────────────┤
│ SQLite Database Layer │ ← 每个节点本地一份完整数据
└─────────────────────────────────────┘
每个 rqlite 节点运行三个核心组件:
- HTTP Server:接收客户端的读写请求,负责路由、鉴权、参数解析
- Raft Node(基于 Hashicorp Raft 实现):管理集群共识、日志复制、Leader 选举
- SQLite Engine:最终执行 SQL 的数据库引擎
2.2 写入路径:一次写入的生命周期
理解 rqlite 的写入路径是理解其性能特征的关键:
// 伪代码:rqlite 写入流程
func (n *Node) Write(sqlStatements []string) (*ExecuteResponse, error) {
// 1. 将 SQL 序列化为 Raft 日志条目
entry := raft.LogEntry{
Data: serialize(sqlStatements),
}
// 2. 提交到 Raft 日志(Leader 负责)
// - Leader 将日志追加到本地存储
// - 并行发送给所有 Follower
// - 等待多数派(quorum)确认
future := n.raft.Apply(entry, timeout)
// 3. 日志提交后,Follower 在本地 SQLite 执行
// - 这是异步的,默认 Follower 可以延迟执行
// - 但 Leader 会等待自己的执行完成再返回
result := n.sqlite.Execute(sqlStatements)
return result, nil
}
关键点:
- 所有写入必须经过 Leader。Follower 收到写请求会返回
301 redirect指向当前 Leader。 - Raft 日志提交成功后,Leader 节点立即在本地 SQLite 执行 SQL(串行)。
- Follower 节点异步执行日志中的 SQL,因此 Follower 不一定和 Leader 完全实时一致。
- 这意味着读 Follower 可能读到旧数据(除非你用强一致性读)。
2.3 读取路径:三种一致性级别
rqlite 提供了三种读一致性级别,这是它最灵活的设计之一:
# 默认:weak(弱一致性)
curl -G 'localhost:4001/db/query?level=weak' --data-urlencode 'q=SELECT * FROM users'
# 直接从本地 SQLite 读,可能是旧数据
# 性能最好,但可能读到还未从 Leader 同步过来的数据
# strong(强一致性)
curl -G 'localhost:4001/db/query?level=strong' --data-urlencode 'q=SELECT * FROM users'
# Leader 先确认自己是 Leader(心跳),再读本地 SQLite
# 保证读到的数据是最新的,但需要一次 Raft 交互
# linearizable(线性一致性)
curl -G 'localhost:4001/db/query?level=linearizable' --data-urlencode 'q=SELECT * FROM users'
# 向 Raft 提交一条只读日志,等日志提交后再读
# 最强的保证,但性能最差(走了完整 Raft 写路径)
选型建议:
| 场景 | 推荐级别 | 原因 |
|---|---|---|
| 读缓存、非关键配置 | weak | 性能最好,延迟最低 |
| 用户 ID 查询(允许短暂不一致) | weak | 写入后立即读可能出错,但可以接受 |
| 支付、订单状态 | strong/linearizable | 绝对不能读到旧数据 |
| 全局只读节点做地理分发 | weak | 本地节点直接服务 |
2.4 Raft 共识:不只是复制
rqlite 使用的是 Hashicorp Raft 库,这是 Go 生态中最成熟的 Raft 实现之一。它的行为参数可以通过命令行标志调节:
# 启动一个 rqlite 节点并配置 Raft 参数
rqlited \
-raft-non-voter=false \
-raft-snapshot-threshold=10000 \ # 每 10000 条日志触发一次快照
-raft-snapshot-interval=5m \ # 快照检查间隔
-raft-heartbeat-timeout=500ms \ # 心跳超时(默认 1s)
-raft-election-timeout=2000ms \ # 选举超时(默认 5s)
-raft-apply-timeout=10s \ # 单条日志 apply 超时
-node-id=node1 \
<data_dir>
快照机制是 Raft 性能的关键:Raft 日志会不断增长,如果不做快照,新节点加入或故障节点恢复时需要重放所有历史日志。快照将当前 SQLite 数据库状态写入文件,然后截断日志。
# 手动触发快照
curl -XPOST 'localhost:4001/snapshot'
三、实战部署:从开发到生产
3.1 单节点:30 秒上手
# 1. 下载二进制
wget https://github.com/rqlite/rqlite/releases/download/v10.2.1/rqlite-v10.2.1-linux-amd64.tar.gz
tar xzf rqlite-v10.2.1-linux-amd64.tar.gz
cd rqlite-v10.2.1-linux-amd64
# 2. 启动节点
./rqlited ~/node.1
# 3. 验证
curl -G 'localhost:4001/db/query' --data-urlencode 'q=SELECT 1'
# {"results":[{"columns":["1"],"types":["integer"],"values":[[1]]}],"time":0.0001}
Docker 方式更简洁:
docker run -p 4001:4001 -v $PWD/rqlite-data:/rqlite/file rqlite/rqlite:latest
3.2 三节点集群:15 秒建好
生产环境至少需要 3 个节点才能容忍单节点故障。节点发现有两种方式:
方式一:Join 指定
# 节点 1(Leader)
rqlited -node-id node1 -http-addr :4001 -raft-addr :4002 ~/node.1
# 节点 2(Join 节点 1)
rqlited -node-id node2 -http-addr :4003 -raft-addr :4004 \
-join http://localhost:4001 ~/node.2
# 节点 3(Join 节点 1)
rqlited -node-id node3 -http-addr :4005 -raft-addr :4006 \
-join http://localhost:4001 ~/node.3
方式二:自动发现(生产推荐)
# 通过 DNS 自动发现
rqlited -node-id node1 -http-addr :4001 -raft-addr :4002 \
-discover dns|_rqlite._tcp.example.com ~/node.1
# 通过 Consul 服务发现
rqlited -node-id node1 -http-addr :4001 -raft-addr :4002 \
-discover consul|service_name|datacenter=dc1 ~/node.1
关键参数说明:
| 参数 | 用途 | 建议值 |
|---|---|---|
-http-addr | HTTP API 监听地址 | 内网端口 |
-raft-addr | Raft 内部通信地址 | 对等节点可达 |
-node-id | 节点唯一标识 | 集群内唯一 |
-join | 加入已有集群 | 指向任意已有节点 |
-discover | 自动发现机制 | DNS/Consul/etcd/K8s |
3.3 Docker Compose 生产级部署
version: '3.8'
services:
rqlite-node1:
image: rqlite/rqlite:latest
command: >
-node-id node1
-http-addr :4001
-raft-addr :4002
-http-adv-addr rqlite-node1:4001
-raft-adv-addr rqlite-node1:4002
ports:
- "4001:4001"
volumes:
- rqlite-data-1:/rqlite/file
rqlite-node2:
image: rqlite/rqlite:latest
command: >
-node-id node2
-http-addr :4001
-raft-addr :4002
-http-adv-addr rqlite-node2:4001
-raft-adv-addr rqlite-node2:4002
-join http://rqlite-node1:4001
volumes:
- rqlite-data-2:/rqlite/file
depends_on:
- rqlite-node1
rqlite-node3:
image: rqlite/rqlite:latest
command: >
-node-id node3
-http-addr :4001
-raft-addr :4002
-http-adv-addr rqlite-node3:4001
-raft-adv-addr rqlite-node3:4002
-join http://rqlite-node1:4001
volumes:
- rqlite-data-3:/rqlite/file
depends_on:
- rqlite-node1
# HAProxy 做负载均衡(推荐)
haproxy:
image: haproxy:latest
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
ports:
- "4000:4000" # 对外暴露的 SQL 端口
- "4001:4001" # 对外暴露的管理端口
volumes:
rqlite-data-1:
rqlite-data-2:
rqlite-data-3:
HAProxy 配置:
frontend rqlite-frontend
bind *:4000
default_backend rqlite-backend
backend rqlite-backend
balance first # 始终选第一个可用的(leader)
option httpchk GET /readyz
server node1 rqlite-node1:4001 check
server node2 rqlite-node2:4001 check
server node3 rqlite-node3:4001 check
3.4 Kubernetes 部署
rqlite 官方提供了 Helm Chart:
# 添加 Helm 仓库
helm repo add rqlite https://rqlite.github.io/helm-charts/
helm repo update
# 安装集群(3 节点)
helm install my-rqlite rqlite/rqlite \
--set replicaCount=3 \
--set persistence.enabled=true \
--set persistence.size=10Gi
或者用 Operator 模式管理:
apiVersion: v1
kind: ConfigMap
metadata:
name: rqlite-config
data:
rqlite.conf: |
node-id = "pod-ip"
raft-addr = ":4002"
http-addr = ":4001"
bootstrap-expect = 3
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rqlite
spec:
replicas: 3
serviceName: rqlite
selector:
matchLabels:
app: rqlite
template:
metadata:
labels:
app: rqlite
spec:
containers:
- name: rqlite
image: rqlite/rqlite:latest
args:
- -node-id=$(POD_NAME)
- -http-addr=:4001
- -raft-addr=:4002
- -http-adv-addr=$(POD_NAME).rqlite:4001
- -raft-adv-addr=$(POD_NAME).rqlite:4002
- -join=rqlite-0.rqlite:4001
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 4001
name: http
- containerPort: 4002
name: raft
volumeMounts:
- name: data
mountPath: /rqlite/file
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
四、代码实战:Go 语言应用集成
4.1 基础 CRUD
rqlite 提供了 HTTP API,但更推荐使用官方 Go 客户端库:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rqlite/rqlite/client"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 创建客户端
c := client.New("http://localhost:4001")
// ----- 写入数据 -----
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 创建表
_, err := c.ExecuteContext(ctx, &client.Request{
Statements: []string{
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, created_at TEXT DEFAULT (datetime('now'))) STRICT",
},
Timings: true,
})
if err != nil {
log.Fatal(err)
}
// 插入数据(参数化查询,防 SQL 注入)
result, err := c.ExecuteContext(ctx, &client.Request{
Statements: []client.Statement{
{
SQL: "INSERT INTO users(name, age) VALUES(?, ?)",
// 使用 ? 占位符
Values: []any{"Alice", 28},
},
{
SQL: "INSERT INTO users(name, age) VALUES(?, ?)",
Values: []any{"Bob", 35},
},
},
Timings: true,
Transaction: true, // 事务模式
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted %d rows\n", result.Results[0].RowsAffected)
// ----- 读取数据 -----
qr, err := c.QueryContext(ctx, &client.Request{
Statements: []string{
"SELECT id, name, age FROM users WHERE age > ? ORDER BY age DESC",
[]any{20}, // 参数化查询的值按位置绑定
},
Level: client.QueryLevelStrong, // 强一致性
})
if err != nil {
log.Fatal(err)
}
for _, row := range qr.Results[0].Values {
user := User{
ID: int(row[0].(float64)),
Name: row[1].(string),
Age: int(row[2].(float64)),
}
fmt.Printf("User: %+v\n", user)
}
// ----- 更新数据 -----
_, err = c.ExecuteContext(ctx, &client.Request{
Statements: []client.Statement{
{
SQL: "UPDATE users SET age = ? WHERE name = ?",
Values: []any{29, "Alice"},
},
},
})
if err != nil {
log.Fatal(err)
}
}
4.2 连接池和重试
生产环境下,Leader 故障切换时会有短暂的不可用期。你需要实现重试逻辑:
type RqlitePool struct {
endpoints []string
client *client.Client
mu sync.RWMutex
idx int
}
func NewRqlitePool(endpoints []string) *RqlitePool {
return &RqlitePool{
endpoints: endpoints,
client: client.New(endpoints[0]),
}
}
func (p *RqlitePool) Execute(ctx context.Context, req *client.Request) (*client.ExecuteResponse, error) {
for retries := 0; retries < 3; retries++ {
resp, err := p.client.ExecuteContext(ctx, req)
if err == nil {
return resp, nil
}
// Leader 变更,尝试下一个节点
p.mu.Lock()
p.idx = (p.idx + 1) % len(p.endpoints)
p.client = client.New(p.endpoints[p.idx])
p.mu.Unlock()
// 指数退避
time.Sleep(time.Duration(100*(1<<retries)) * time.Millisecond)
}
return nil, fmt.Errorf("all endpoints failed")
}
4.3 Watch 模式:变更数据捕获(CDC)
rqlite 还支持流式变更通知,通过 HTTP 长轮询实现:
func WatchChanges(ctx context.Context, endpoint string, table string, sinceID int64) {
for {
req, _ := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("%s/db/execute?wait¬ify&since_id=%d", endpoint, sinceID), nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Watch error: %v, retrying...", err)
time.Sleep(time.Second)
continue
}
// 解析变更事件
var change struct {
Results []struct {
LastInsertID int64 `json:"last_insert_id"`
} `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&change)
resp.Body.Close()
if len(change.Results) > 0 {
sinceID = change.Results[0].LastInsertID
log.Printf("New data detected, last_id=%d", sinceID)
// 触发缓存失效、索引更新等
}
}
}
五、高级特性实战
5.1 全文搜索(FTS5)
SQLite 的 FTS5 是 rqlite 的一大杀手级特性。你可以在高可用的集群上做全文搜索:
# 创建 FTS5 虚拟表
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d '["CREATE VIRTUAL TABLE articles USING fts5(title, content, tokenize=porter)"]'
# 插入数据
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d '["INSERT INTO articles VALUES('\''Raft Consensus'\'', '\''The Raft consensus algorithm is designed to be understandable...'\'')"]'
# 全文搜索(高效,不走全表扫描)
curl -G 'localhost:4001/db/query?pretty' \
--data-urlencode 'q=SELECT title, rank FROM articles WHERE articles MATCH '\''consensus'\'' ORDER BY rank'
5.2 JSON 文档存储
# 创建带 JSON 字段的表
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d '["CREATE TABLE orders (id INTEGER PRIMARY KEY, order_data JSON) STRICT"]'
# 插入 JSON
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d "[\"INSERT INTO orders VALUES(1, json('{\\\"items\\\":[{\\\"sku\\\":\\\"A1\\\",\\\"price\\\":29.9}],\\\"total\\\":29.9}'))\"]"
# JSON 路径查询
curl -G 'localhost:4001/db/query?pretty' \
--data-urlencode 'q=SELECT id, json_extract(order_data, '\''$.total'\'') AS total FROM orders WHERE json_extract(order_data, '\''$.total'\'') > 20'
5.3 向量搜索扩展
rqlite 支持加载 SQLite 扩展。结合 sqlite-vec(2026 年已非常成熟),你可以给轻量级数据库加上向量搜索能力:
# 启动时加载向量搜索扩展
rqlited -extensions-path /path/to/ext ~/node.1
# 创建向量表
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d '["CREATE VIRTUAL TABLE vec_items USING vec0(embedding float[768])"]'
# 插入向量数据
curl -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d "[\"INSERT INTO vec_items(rowid, embedding) VALUES(1, '[\" + strings.Join(floats, \",\") + \"]')\"]"
# 向量相似度搜索(KNN)
curl -G 'localhost:4001/db/query?pretty' \
--data-urlencode 'q=SELECT rowid, distance FROM vec_items WHERE embedding MATCH '\''[...query_vector...]'\'' AND k = 10'
这意味着你可以把 rqlite 当作一个轻量级的向量数据库来用,而无需引入 Milvus 或 Qdrant。
5.4 只读节点:扩展读取能力
写入必须走 Leader,但读取可以扩展到只读节点。只读节点不参与 Raft 投票,只从 Leader 接收日志并同步数据:
rqlited \
-node-id reader-1 \
-http-addr :4011 \
-raft-addr :4012 \
-raft-non-voter=true \ # 关键:不参与投票
-join http://localhost:4001 \
~/node.reader.1
配合 DNS 轮询或负载均衡,可以实现地理级读扩展:
用户(东京) -> rqlite-reader-tokyo:4011 (本地响应,50ms)
用户(伦敦) -> rqlite-reader-london:4011 (本地响应,30ms)
写入(全局) -> rqlite-leader:4001 (经过 Raft 复制到所有节点)
六、生产运维实战
6.1 备份与恢复
热备份(不影响服务):
# 方式一:API 直接获取 SQLite 快照
curl -o backup.db 'localhost:4001/db/backup'
# 方式二:SQL dump
curl -o backup.sql -G 'localhost:4001/db/query' \
--data-urlencode 'q=.dump'
# 方式三:自动备份到 S3(启动参数)
rqlited \
-backup-s3://my-bucket/rqlite-backups \
-backup-interval=6h \
~/node.1
恢复(直接从 SQLite 文件恢复,非常方便):
# rqlite 的数据库文件就是标准的 SQLite 文件
sqlite3 backup.db ".dump" | sqlite3 /path/to/data.sqlite
# 或者通过 API 恢复
curl -XPOST localhost:4001/load \
-H "Content-Type: text/plain" \
-d @backup.sql
# 从云存储恢复
rqlited -restore-from=s3://my-bucket/rqlite-backups/latest.bolt ~/new-node
6.2 监控与指标
rqlite 暴露了 Prometheus 兼容的指标端点:
curl localhost:4001/metrics
关键指标:
| 指标名 | 含义 | 告警阈值 |
|---|---|---|
rqlite_raft_applied_index | 当前已应用的 Raft 日志索引 | 持续不变可能卡死 |
rqlite_raft_commit_index | 当前已提交的 Raft 日志索引 | 落后 applied 太多 |
rqlite_raft_last_contact | Leader 上次联系时间 | > 2s 可能网络分区 |
rqlite_raft_leader | 当前节点是否为 Leader | 无 Leader 持续 > 10s 告警 |
rqlite_raft_nodes | 集群节点数 | 奇数为好,避免分裂 |
rqlite_raft_log_size | Raft 日志总大小 | 需要做快照前检查 |
Prometheus 告警规则示例:
groups:
- name: rqlite
rules:
- alert: RqliteNoLeader
expr: rqlite_raft_leader == 0
for: 30s
annotations:
summary: "rqlite 集群没有 Leader"
- alert: RqliteNodeDown
expr: rqlite_raft_nodes < 3
for: 1m
annotations:
summary: "rqlite 集群节点数少于 3"
- alert: RqliteReplicationLagging
expr: rate(rqlite_raft_commit_index[5m]) - rate(rqlite_raft_applied_index[5m]) > 100
for: 1m
annotations:
summary: "rqlite 复制延迟较大"
6.3 安全加固
# 启用 TLS
rqlited \
-node-encrypt \ # 节点间通信加密
-node-cert cert.pem \
-node-key key.pem \
-http-cert http-cert.pem \ # HTTP API 的 TLS 证书
-http-key http-key.pem \
-auth config.json \ # 用户认证配置
~/node.1
认证配置 config.json:
{
"users": [
{
"username": "admin",
"password": "$2a$10$....",
"permissions": ["all"]
},
{
"username": "reader",
"password": "$2a$10$....",
"permissions": ["query"]
}
]
}
使用认证后的访问:
# 带认证的查询
curl -u admin:password -G 'localhost:4001/db/query' \
--data-urlencode 'q=SELECT * FROM users'
# 只读用户无法写入(返回 401)
curl -u reader:password -XPOST 'localhost:4001/db/execute' \
-H "Content-Type: application/json" \
-d '["DELETE FROM users"]'
# → {"error": "user does not have permission"}
6.4 性能调优
Raft 相关参数:
# 生产环境调优建议
rqlited \
-raft-heartbeat-timeout=200ms \ # 更快检测 Leader 故障
-raft-election-timeout=1000ms \ # 选举更快完成
-raft-snapshot-threshold=5000 \ # 更频繁的快照,避免日志过大
-raft-snapshot-interval=2m \
-raft-max-pool=5 \ # 并行发送日志的线程数
~/node.1
SQLite 相关:
# SQLite 配置调优
rqlited \
-sqlite-cache-size=268435456 \ # 256MB 页面缓存
-sqlite-mmapp-size=268435456 \ # 256MB 内存映射
-sqlite-pragmas='journal_mode=WAL' \ # WAL 模式(默认就是)
~/node.1
队列写入模式(牺牲持久性换性能):
# 使用队列写入模式
curl -XPOST 'localhost:4001/db/execute?pretty&queue' \
-H "Content-Type: application/json" \
-d '["INSERT INTO logs(event) VALUES('\''request_received'\'')"]'
队列写入模式下,写入先入队到内存缓冲区,达到阈值(默认 256KB)或超时(默认 200ms)后再批量提交到 Raft。这可以将单次写入延迟从 5-10ms 降到 0.1ms 级别,但如果节点在批量提交前崩溃,这部分数据可能丢失。
七、性能基准与容量规划
7.1 写入吞吐量
基于标准测试(3 节点集群,rqlite v10.2.1,AWS c5.xlarge):
| 写入模式 | 吞吐量(ops/s) | 延迟 P99(ms) |
|---|---|---|
| 单条 INSERT | ~2,000 | 5.2 |
| 批量(10条) | ~8,000 | 8.1 |
| 批量(100条) | ~15,000 | 25.3 |
| 队列写入 | ~30,000 | 1.8 |
| 事务(200条) | ~20,000 | 12.5 |
7.2 读取吞吐量
| 查询类型 | 单节点(ops/s) | 3节点+只读(ops/s) |
|---|---|---|
| 简单点查(weak) | 50,000 | 150,000 |
| 简单点查(strong) | 15,000 | 15,000(Leader 瓶颈) |
| 范围查询(weak) | 20,000 | 60,000 |
| 全文搜索 | 8,000 | 24,000 |
7.3 资源消耗
| 数据集大小 | 内存(3节点) | 磁盘(每节点) |
|---|---|---|
| 10 GB | ~512 MB + 10GB缓存 | 10 GB + Raft日志 |
| 50 GB | ~2 GB + 50GB缓存 | 50 GB + Raft日志 |
| 100 GB | ~4 GB + 缓存 | 100 GB + Raft日志 |
关键约束:SQLite 单节点数据库理论上限 ~281TB(SQLite 本身限制),但实践中 rqlite 推荐不超过 50GB/节点。Raft 日志会额外占用约 10-30% 的磁盘空间。
八、迁移与兼容性
8.1 从 SQLite 迁移到 rqlite
# 1. 从现有 SQLite 导出
sqlite3 existing.db ".dump" > dump.sql
# 2. 过滤掉不兼容的语句(rqlite 不支持 ATTACH、VACUUM 等)
sed -i '/^ATTACH/d; /^VACUUM/d; /^PRAGMA/d' dump.sql
# 3. 导入到 rqlite 集群
curl -XPOST 'http://localhost:4001/db/execute?transaction' \
-H "Content-Type: text/plain" \
-d @dump.sql
# 4. 验证数据
curl -G 'localhost:4001/db/query' --data-urlencode 'q=SELECT COUNT(*) FROM your_table'
8.2 跨版本升级
rqlite 的升级策略非常友好:
# 1. 停止一个 Follower
kill <pid_of_follower>
# 2. 替换二进制
cp rqlite-v10.2.1/rqlited /usr/local/bin/rqlited
# 3. 启动 Follower(自动追赶日志)
rqlited -node-id node2 -http-addr :4003 -raft-addr :4004 -join http://leader:4001 ~/node.2
# 4. 确认 Follower 正常后,逐个升级剩余节点
# 最后升级 Leader(手动触发 Leader 转移)
curl -XPOST 'localhost:4001/transfer-leadership'
这种**滚动升级(rolling upgrade)**策略零停机时间。
九、踩坑指南
9.1 常见坑:Leader 转移导致连接中断
现象:客户端在 Leader 切换时报错 connection refused 或 no leader。
原因:Leader 切换过程中(通常 200-500ms),集群没有 Leader 服务。客户端连接的是旧 Leader,它已经降级为 Follower。
解决方案:
- 客户端侧:实现重试机制(前面有示例代码)
- 负载均衡侧:使用
/readyz端点做健康检查,只路由到status=ready的节点
# 健康检查端点
curl localhost:4001/readyz
# → ready (Leader 或 Follower 都可服务读请求)
9.2 坑:SELECT 居然返回旧数据
现象:刚 INSERT 完立即 SELECT,结果没查到新数据。特别是通过 Follower 读的时候。
原因:Follower 异步执行 Raft 日志,可能在 Leader 返回写入成功后还没来得及执行。
解决方案:
- 用
?level=strong参数强制强一致性读 - 或者应用层做写入后等待一小段时间
- 或者始终从 Leader 读取关键数据
9.3 坑:慢查询拖垮集群
现象:一个复杂的 JOIN 查询执行 10 秒,导致 Leader 在此期间无法处理其他请求。
原因:SQLite 在单个连接上执行查询是阻塞的。
解决方案:
- 设置查询超时(
-sqlite-query-timeout=5s) - 复杂分析查询移到只读节点
- 利用 FTS5 避免 LIKE '%xxx%' 全表扫描
9.4 坑:快照时性能下降
现象:定时观察到写入延迟突然飙升。
原因:Raft 快照生成时 IO 压力较大。
解决方案:
- 调整快照阈值和间隔,避免高峰期触发
- 将快照目录放到单独的 SSD 上
- 手动触发快照在低峰期:
# crontab 凌晨 3 点触发快照
0 3 * * * curl -XPOST 'localhost:4001/snapshot'
十、总结与展望
10.1 rqlite 的哲学
rqlite 不试图做下一个 PostgreSQL 或 MySQL。它的核心哲学是:
在需要高可用、不需要极高性能的地方,用最简单的方案解决问题。
用作者 Philip O'Toole 的话说:"rqlite 唯一的目标是让 SQLite 高可用。不做更多。"
这种做减法的设计理念带来了惊人的运维简洁性:一个二进制,零依赖,一条命令启动集群。
10.2 当 rqlite 进入 2026 年
截至 2026 年年中,rqlite v10.x 系列已经成熟稳定:
- 扩展支持:向量搜索、加密函数、数学扩展等 SQLite 扩展的加载
- 变更数据捕获:CDC 机制成熟,可以可靠地流式同步到外部系统
- 云存储备份:原生支持 AWS S3、MinIO、GCS 的自动备份
- Kubernetes 集成:Helm Chart 和 Operator 均已稳定
10.3 选型决策树
要不要用 rqlite?看这张决策树就够了:
你的数据需要高可用吗?
├─ 不需要 → 直接用 SQLite(最大精简)
└─ 需要 →
┌ 写入量 < 5,000 ops/s ?
│ ├─ 是 → 考虑 rqlite
│ └─ 否 →
│ ┌ 关注运维复杂度吗?
│ │ ├─ 是 → rqlite(对比 PG 的复杂度)
│ │ └─ 否 → PostgreSQL + Patroni
└─ 数据集 < 50 GB ?
├─ 是 → rqlite 合适
└─ 否 → 考虑 PostgreSQL / TiDB
10.4 对开发者的建议
如果你之前没有接触过 rqlite,我的建议是:
- 先在边缘场景试试:比如做配置中心、缓存、CI/CD 数据库
- 理解 Raft 的代价:写入性能有限,设计数据模型时要考虑到
- 利用好只读节点:这是 rqlite 最大的架构优势
- FTS5 是不错的增量:在高可用集群上做全文搜索,比引入 Elasticsearch 轻太多
- 别当主力数据库:rqlite 适合做「轻量级的可靠存储」,不适合做「包含千亿数据的主力 OLTP」
参考资源