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

dqlite 分布式 SQLite 教程 / 第 3 章:架构深度解析

第 3 章:架构深度解析

本章深入剖析 dqlite 的内部架构,包括 Raft 共识实现、日志复制流程、快照机制和成员变更协议,帮助你理解 dqlite 的工作原理。


3.1 整体架构

dqlite 的架构可以分为四层:

┌─────────────────────────────────────────────────┐
│                  Application Layer               │
│           (Your App / LXD / MicroK8s)           │
├─────────────────────────────────────────────────┤
│                 dqlite Client Layer              │
│        (Connection Pool, Request Router)         │
├─────────────────────────────────────────────────┤
│               dqlite Protocol Layer              │
│     (Binary Protocol, MessagePack Encoding)      │
├──────────────────────┬──────────────────────────┤
│   Raft Consensus     │    SQLite Storage         │
│   ┌──────────────┐   │   ┌─────────────────┐    │
│   │ Leader       │   │   │ WAL Mode        │    │
│   │ Election     │   │   │ Page Cache      │    │
│   │ Log Storage  │   │   │ B-tree          │    │
│   │ Snapshot     │   │   │ Shared Cache    │    │
│   └──────────────┘   │   └─────────────────┘    │
├──────────────────────┴──────────────────────────┤
│              Transport Layer                     │
│          (libuv, TCP, TLS)                       │
└─────────────────────────────────────────────────┘

3.1.1 各层职责

层次 组件 职责
应用层 用户程序 发起 SQL 查询和写入请求
客户端层 连接池 管理到集群节点的连接,路由请求到 Leader
协议层 dqlite 协议 二进制协议编解码(基于 MessagePack)
共识层 C-raft Raft 共识、日志复制、Leader 选举
存储层 SQLite SQL 执行、数据持久化
传输层 libuv 异步网络 I/O、TLS 加密

3.2 Raft 共识实现

dqlite 使用内嵌的 C-raft 库(libraft)实现 Raft 共识协议。

3.2.1 Raft 角色状态机

每个 dqlite 节点在任意时刻处于以下三种角色之一:

                    ┌──────────────┐
          超时       │              │      收到多数票
        ┌──────────▶│  Candidate   │─────────────────┐
        │           │              │                  │
        │           └──────────────┘                  ▼
 ┌──────────────┐                            ┌──────────────┐
 │              │      发现更高任期           │              │
 │   Follower   │◀───────────────────────────│    Leader    │
 │              │                            │              │
 └──────────────┘                            └──────────────┘
        ▲                                          │
        │           ┌──────────────┐               │
        │           │              │               │
        └───────────│  候选人超时   │◀──────────────┘
          发现新     │  或发现新     │    发现更高任期
          Leader     │  Leader      │
                     └──────────────┘
角色 职责 触发条件
Follower 被动接收日志和心跳 启动时默认角色
Candidate 发起选举请求 选举超时未收到心跳
Leader 协调所有写入、发送心跳 赢得多数票选举

3.2.2 Leader 选举流程

时间线:
  t0: Follower 选举超时(150-300ms 随机)
  t1: 转为 Candidate,任期 +1,投自己一票
  t2: 向所有节点发送 RequestVote RPC
  t3: 收到多数票响应 → 成为 Leader
  t4: 开始发送心跳(AppendEntries 空日志)

dqlite 中的选举参数:

参数 默认值 说明
选举超时 150-300ms 随机化以避免选票分裂
心跳间隔 50ms Leader 发送心跳的间隔
最大日志滞后 0 Candidate 日志不能落后于投票者

注意: 在高延迟网络(如跨数据中心)中,可能需要调整选举超时参数以避免频繁的 Leader 切换。

3.2.3 Quorum(法定人数)

Raft 要求多数节点确认才能提交日志:

集群节点数 Quorum(多数派) 可容忍故障数
1 1 0(无冗余)
2 2 0(不推荐)
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3

最佳实践: 始终使用奇数节点(3、5、7)。偶数节点不会增加容错能力,反而增加了 Quorum 开销。


3.3 日志复制(Log Replication)

3.3.1 日志条目结构

每个写操作在 dqlite 中被封装为一个 Raft 日志条目:

┌────────────────────────────────────────┐
│          Raft Log Entry                 │
├──────────────┬─────────────────────────┤
│ Term (8B)    │ Leader 的任期号          │
│ Index (8B)   │ 日志条目的全局唯一索引    │
│ Type (1B)    │ 条目类型                 │
│ Data (var)   │ SQL 命令(MessagePack)  │
└──────────────┴─────────────────────────┘
字段 类型 说明
Term uint64 创建此条目时的 Leader 任期
Index uint64 单调递增的日志序号
Type uint8 条目类型(命令、配置变更等)
Data bytes 序列化的 SQL 操作

3.3.2 写入流程详解

一次完整的写入操作经历以下步骤:

  Client          Leader         Follower 1      Follower 2
    │               │                │               │
    │─── Open ──────▶│               │               │
    │◀── OK ────────│                │               │
    │               │                │               │
    │─── Exec ──────▶│               │               │
    │  "INSERT ..."  │               │               │
    │               │               │               │
    │               │── AppendEntries ──▶│           │
    │               │── AppendEntries ────────────── ▶│
    │               │                │               │
    │               │◀─ Success ─────│               │
    │               │◀─ Success ─────────────────────│
    │               │               │               │
    │               │  [Quorum 达成]  │               │
    │               │               │               │
    │               │── Apply to ──▶│               │
    │               │   SQLite      │               │
    │               │               │               │
    │◀── Result ────│               │               │
    │               │               │               │

步骤分解

  1. 客户端发送写请求 → Leader 节点
  2. Leader 创建日志条目 → 追加到本地日志
  3. Leader 广播 AppendEntries → 所有 Follower
  4. Follower 验证并存储日志 → 返回确认
  5. 多数节点确认 → 标记为已提交(Committed)
  6. 应用到 SQLite → 所有节点执行 SQL
  7. 返回结果 → 客户端

3.3.3 AppendEntries RPC

字段 说明
term Leader 当前任期
leaderId Leader 的节点 ID
prevLogIndex 前一条日志的索引(用于一致性检查)
prevLogTerm 前一条日志的任期
entries[] 待复制的日志条目
leaderCommit Leader 已提交的最高日志索引

Follower 接收 AppendEntries 的处理逻辑:

收到 AppendEntries:
  1. 如果 term < 本地 term → 拒绝
  2. 如果 prevLogIndex 处的条目不匹配 → 拒绝(日志不一致)
  3. 追加/覆盖日志条目
  4. 更新 commitIndex = min(leaderCommit, 最新条目索引)
  5. 对已提交的条目执行 Apply(应用到 SQLite)

3.3.4 日志压缩

随着写入的增加,Raft 日志会不断增长。dqlite 通过 快照(Snapshot)机制压缩日志:

日志状态:
  Index: 1    2    3    4    5    6    7    8    9
          ▲                                  ▲
          │                                  │
       lastSnapshotIndex                 lastLogIndex
          │                                  │
       [已快照,可删除]     [活跃日志,保留]

快照后:
  Index:         5    6    7    8    9
  Snapshot: [包含 1-4 的完整状态]
                 ▲
                 │
              新的 lastSnapshotIndex

3.4 快照机制(Snapshot)

3.4.1 何时触发快照

dqlite 在以下条件下触发快照:

条件 默认阈值 说明
日志条目数量 1024 自上次快照后的日志条目数
日志大小 无硬限制 与数据库大小相关

配置示例(通过 C API):

/* 设置快照阈值 */
struct raft_configuration config;
config.trailing_entries = 1024;  /* 保留的最近日志条目数 */

3.4.2 快照创建流程

Leader 创建快照流程:

1. 暂停 Apply(停止应用新日志)
2. 调用 SQLite checkpoint(将 WAL 写入主数据库)
3. 复制 SQLite 数据库文件作为快照
4. 记录快照对应的 lastLogIndex 和 lastLogTerm
5. 删除 lastSnapshotIndex 之前的日志条目
6. 恢复 Apply

Follower 接收快照流程:

1. 收到 InstallSnapshot RPC
2. 保存快照数据到临时文件
3. 替换本地 SQLite 数据库
4. 丢弃快照之前的所有日志
5. 更新 lastSnapshotIndex
6. 恢复正常日志接收

3.4.3 InstallSnapshot RPC

字段 说明
term Leader 当前任期
leaderId Leader 的节点 ID
lastSnapshotIndex 快照包含的最后一个日志索引
lastSnapshotTerm 该索引对应的任期
data 快照数据(SQLite 数据库文件内容)
offset 数据偏移量(支持分片传输)
done 是否为最后一个分片

3.4.4 快照与 WAL 的关系

dqlite 使用 SQLite 的 WAL(Write-Ahead Logging)模式,快照过程中需要处理 WAL 文件:

快照前:
  db.sqlite     (主数据库文件)
  db.sqlite-wal (WAL 文件,包含未合并的变更)
  db.sqlite-shm (共享内存文件)

SQLite Checkpoint:
  WAL 内容 → 合并到 db.sqlite
  WAL 文件 → 清空或截断

快照内容:
  db.sqlite     (包含所有已提交数据)
  (WAL 通常为空或只有最近的小量变更)

注意: 快照操作会导致短暂的 I/O 峰值和可能的写入暂停。在生产环境中,建议监控快照频率和持续时间。


3.5 成员变更(Membership Changes)

集群运行过程中可能需要增加或移除节点。dqlite 支持 单步成员变更(Single-Step Membership Change)。

3.5.1 成员变更类型

操作 说明 风险
添加节点 新节点加入集群
移除节点 节点离开集群 中(确保 Quorum)
替换节点 旧节点被新节点替代 高(需谨慎)

3.5.2 添加节点流程

添加节点 Node 4 到 {1, 2, 3} 集群:

1. 管理员发起 AddNode(4) 请求 → Leader
2. Leader 创建配置变更日志条目
3. 复制到所有现有节点并提交
4. Leader 开始向 Node 4 发送日志
5. Node 4 从头开始接收日志(或接收快照)
6. Node 4 完成同步后正式成为集群成员

配置序列:
  C_old: {1, 2, 3}       Quorum: 2
  C_new: {1, 2, 3, 4}    Quorum: 3

3.5.3 移除节点流程

从 {1, 2, 3, 4} 集群中移除 Node 4:

1. 管理员发起 RemoveNode(4) 请求 → Leader
2. Leader 创建配置变更日志条目
3. 复制到所有节点(包括 Node 4)并提交
4. Leader 停止向 Node 4 发送日志
5. Node 4 转为独立节点(或关闭)

配置序列:
  C_old: {1, 2, 3, 4}    Quorum: 3
  C_new: {1, 2, 3}        Quorum: 2

警告: 不要同时进行多个成员变更。确保每次变更完成且稳定后,再进行下一次。同时变更可能导致 Quorum 不可达,造成集群不可用。

3.5.4 安全成员变更的最佳实践

规则 说明
一次只变更一个节点 避免 Quorum 混乱
新节点先完成同步 确认新节点日志最新后再变更
不要移除 Leader 先将 Leader 转移到其他节点
奇数节点原则 保持 3、5、7 个节点
变更期间避免写入 降低不一致风险

3.5.5 节点替换场景

当一个节点永久失效需要替换时:

# 场景:Node 2 永久失效,需要用 Node 5 替换

# 步骤 1:移除旧节点
dqlite-remove-node --id 2

# 步骤 2:准备新节点
# 在新机器上启动 Node 5(数据目录为空)

# 步骤 3:添加新节点
dqlite-add-node --id 5 --address "192.168.1.105:9001"

# 步骤 4:等待同步
# Node 5 会通过 InstallSnapshot 或日志重放完成数据同步

3.6 二进制协议(dqlite Wire Protocol)

dqlite 使用自定义二进制协议进行节点间和客户端-节点通信。

3.6.1 协议概述

特性 说明
编码格式 MessagePack
传输层 TCP
默认端口 9001
连接模型 每客户端一个连接
认证 可选 TLS + 客户端证书

3.6.2 消息格式

┌──────────────────────────────────────────┐
│              Message Frame               │
├──────────┬──────────┬────────────────────┤
│ Type (1) │ Words (4)│ Body (variable)    │
├──────────┼──────────┼────────────────────┤
│ uint8    │ uint32   │ MessagePack 编码   │
└──────────┴──────────┴────────────────────┘

3.6.3 请求类型

类型代码 名称 说明
0x01 Open 打开数据库
0x02 Exec 执行 SQL 语句
0x03 Query 执行查询
0x04 ExecSQL 执行 SQL(简版)
0x05 QuerySQL 执行查询(简版)
0x06 Interrupt 中断当前操作
0x07 Add 添加节点
0x08 Assign 分配角色
0x09 Remove 移除节点
0x0a Dump 导出数据库
0x0b Cluster 获取集群信息
0x0c Transfer Leader 转移

3.6.4 连接生命周期

Client                          dqlite Node
  │                                 │
  │──── TCP Connect ─────────────── ▶│
  │                                 │
  │──── Handshake (client ID) ───── ▶│
  │◀──── Handshake (server ID) ─────│
  │                                 │
  │──── Open (database name) ────── ▶│
  │◀──── OK (database ID) ──────────│
  │                                 │
  │──── Exec (SQL) ─────────────── ▶│
  │◀──── Result ────────────────────│
  │                                 │
  │──── Query (SQL) ────────────── ▶│
  │◀──── Rows ──────────────────────│
  │                                 │
  │──── Close ───────────────────── ▶│
  │                                 │

3.7 共享缓存模式

dqlite 使用 SQLite 的 共享缓存(Shared Cache)模式来优化多连接场景下的内存使用。

3.7.1 共享缓存 vs 普通模式

特性 普通模式 共享缓存模式
缓存共享 每连接独立缓存 所有连接共享缓存
内存使用 高(每个连接一份) 低(共享一份)
锁粒度 表级锁 页级锁(WAL 模式)
并发读 支持 支持
并发写 串行 串行(单 Writer)

3.7.2 dqlite 的内部连接管理

┌─────────────────────────────────────┐
│            dqlite 节点               │
│  ┌───────────────────────────────┐  │
│  │     连接池 (Connection Pool)  │  │
│  │  ┌────────┐ ┌────────┐       │  │
│  │  │Conn 1  │ │Conn 2  │ ...   │  │
│  │  └───┬────┘ └───┬────┘       │  │
│  │      │          │             │  │
│  │  ┌───▼──────────▼──────────┐  │  │
│  │  │   Shared Cache          │  │  │
│  │  │   (SQLite Shared Cache) │  │  │
│  │  └───────────┬─────────────┘  │  │
│  │              │                 │  │
│  │  ┌───────────▼─────────────┐  │  │
│  │  │   SQLite Storage        │  │  │
│  │  │   (WAL Mode)            │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

3.8 数据持久化

3.8.1 文件结构

dqlite 数据目录中的文件:

data/
├── 1                    # Raft 日志数据库(SQLite 格式)
├── 1-wal                # Raft 日志的 WAL 文件
├── <db-name>            # 用户数据库文件
├── <db-name>-wal        # 用户数据库的 WAL 文件
└── <db-name>-shm        # 共享内存文件
文件 说明
1 Raft 元数据和日志存储(SQLite 格式)
<db-name> 用户创建的数据库(如 test.db
-wal Write-Ahead Log 文件
-shm 共享内存索引文件

3.8.2 数据持久化保证

保证 说明
已提交数据不丢失 日志被多数节点持久化后才提交
崩溃恢复 SQLite WAL 支持自动恢复
快照一致性 快照是某个时刻的完整数据库副本

注意: 必须确保数据目录所在的文件系统支持 fsync。不建议将 dqlite 数据放在 tmpfs 或网络文件系统(NFS)上。


3.9 故障模型

3.9.1 dqlite 能处理的故障

故障类型 影响 处理方式
Follower 崩溃 短暂不可用 Leader 继续服务,节点恢复后自动同步
Leader 崩溃 短暂不可写 Follower 触发选举,新 Leader 接管
网络分区 少数派不可用 多数派继续服务
节点慢 写入延迟增加 自动降级为 Follower

3.9.2 dqlite 不能处理的故障

故障类型 影响 解决方案
多数节点同时故障 集群不可用 等待节点恢复
数据文件损坏 需要从备份恢复 定期备份
拜占庭故障 不保证正确性 Raft 是 CFT 算法,非 BFT

本章小结

要点 说明
架构层次 应用层 → 客户端层 → 协议层 → 共识层 + 存储层 → 传输层
Raft 角色 Follower、Candidate、Leader
写入流程 Client → Leader → 日志复制 → Quorum 确认 → Apply
日志压缩 通过快照机制,定期清除旧日志
成员变更 单步变更,一次只改一个节点
通信协议 自定义二进制协议,基于 MessagePack

下一章

第 4 章:基本操作 — 学习如何创建数据库、执行 SQL 操作和管理连接。