编程 rqlite 深度实战:当轻量级 SQLite 遇上 Raft 共识——从分布式架构到生产级部署的完全指南(2026)

2026-06-22 18:54:55 +0800 CST views 10

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 节点运行三个核心组件:

  1. HTTP Server:接收客户端的读写请求,负责路由、鉴权、参数解析
  2. Raft Node(基于 Hashicorp Raft 实现):管理集群共识、日志复制、Leader 选举
  3. 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-addrHTTP API 监听地址内网端口
-raft-addrRaft 内部通信地址对等节点可达
-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&notify&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_contactLeader 上次联系时间> 2s 可能网络分区
rqlite_raft_leader当前节点是否为 Leader无 Leader 持续 > 10s 告警
rqlite_raft_nodes集群节点数奇数为好,避免分裂
rqlite_raft_log_sizeRaft 日志总大小需要做快照前检查

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,0005.2
批量(10条)~8,0008.1
批量(100条)~15,00025.3
队列写入~30,0001.8
事务(200条)~20,00012.5

7.2 读取吞吐量

查询类型单节点(ops/s)3节点+只读(ops/s)
简单点查(weak)50,000150,000
简单点查(strong)15,00015,000(Leader 瓶颈)
范围查询(weak)20,00060,000
全文搜索8,00024,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 refusedno leader

原因:Leader 切换过程中(通常 200-500ms),集群没有 Leader 服务。客户端连接的是旧 Leader,它已经降级为 Follower。

解决方案

  1. 客户端侧:实现重试机制(前面有示例代码)
  2. 负载均衡侧:使用 /readyz 端点做健康检查,只路由到 status=ready 的节点
# 健康检查端点
curl localhost:4001/readyz
# → ready (Leader 或 Follower 都可服务读请求)

9.2 坑:SELECT 居然返回旧数据

现象:刚 INSERT 完立即 SELECT,结果没查到新数据。特别是通过 Follower 读的时候。

原因:Follower 异步执行 Raft 日志,可能在 Leader 返回写入成功后还没来得及执行。

解决方案

  1. ?level=strong 参数强制强一致性读
  2. 或者应用层做写入后等待一小段时间
  3. 或者始终从 Leader 读取关键数据

9.3 坑:慢查询拖垮集群

现象:一个复杂的 JOIN 查询执行 10 秒,导致 Leader 在此期间无法处理其他请求。

原因:SQLite 在单个连接上执行查询是阻塞的。

解决方案

  1. 设置查询超时(-sqlite-query-timeout=5s
  2. 复杂分析查询移到只读节点
  3. 利用 FTS5 避免 LIKE '%xxx%' 全表扫描

9.4 坑:快照时性能下降

现象:定时观察到写入延迟突然飙升。

原因:Raft 快照生成时 IO 压力较大。

解决方案

  1. 调整快照阈值和间隔,避免高峰期触发
  2. 将快照目录放到单独的 SSD 上
  3. 手动触发快照在低峰期:
# 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,我的建议是:

  1. 先在边缘场景试试:比如做配置中心、缓存、CI/CD 数据库
  2. 理解 Raft 的代价:写入性能有限,设计数据模型时要考虑到
  3. 利用好只读节点:这是 rqlite 最大的架构优势
  4. FTS5 是不错的增量:在高可用集群上做全文搜索,比引入 Elasticsearch 轻太多
  5. 别当主力数据库:rqlite 适合做「轻量级的可靠存储」,不适合做「包含千亿数据的主力 OLTP」

参考资源

复制全文 生成海报 rqlite SQLite Raft 分布式数据库 Go语言

推荐文章

LLM驱动的强大网络爬虫工具
2024-11-19 07:37:07 +0800 CST
Golang在整洁架构中优雅使用事务
2024-11-18 19:26:04 +0800 CST
mysql删除重复数据
2024-11-19 03:19:52 +0800 CST
15 个 JavaScript 性能优化技巧
2024-11-19 07:52:10 +0800 CST
一些高质量的Mac软件资源网站
2024-11-19 08:16:01 +0800 CST
测试文章
2026-06-22 03:28:39 +0800 CST
go命令行
2024-11-18 18:17:47 +0800 CST
JavaScript数组 splice
2024-11-18 20:46:19 +0800 CST
浏览器自动播放策略
2024-11-19 08:54:41 +0800 CST
Go 单元测试
2024-11-18 19:21:56 +0800 CST
mysql时间对比
2024-11-18 14:35:19 +0800 CST
平面设计常用尺寸
2024-11-19 02:20:22 +0800 CST
程序员茄子在线接单