强曰为道

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

第 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-raftRaft 共识、日志复制、Leader 选举
存储层SQLiteSQL 执行、数据持久化
传输层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随机化以避免选票分裂
心跳间隔50msLeader 发送心跳的间隔
最大日志滞后0Candidate 日志不能落后于投票者

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

3.2.3 Quorum(法定人数)

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

集群节点数Quorum(多数派)可容忍故障数
110(无冗余)
220(不推荐)
321
431
532
642
743

最佳实践: 始终使用奇数节点(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)  │
└──────────────┴─────────────────────────┘
字段类型说明
Termuint64创建此条目时的 Leader 任期
Indexuint64单调递增的日志序号
Typeuint8条目类型(命令、配置变更等)
Databytes序列化的 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

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

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

字段说明
termLeader 当前任期
leaderIdLeader 的节点 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 请求类型

类型代码名称说明
0x01Open打开数据库
0x02Exec执行 SQL 语句
0x03Query执行查询
0x04ExecSQL执行 SQL(简版)
0x05QuerySQL执行查询(简版)
0x06Interrupt中断当前操作
0x07Add添加节点
0x08Assign分配角色
0x09Remove移除节点
0x0aDump导出数据库
0x0bCluster获取集群信息
0x0cTransferLeader 转移

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        # 共享内存文件
文件说明
1Raft 元数据和日志存储(SQLite 格式)
<db-name>用户创建的数据库(如 test.db
-walWrite-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 操作和管理连接。