Apache BookKeeper 的 ledger(账本)是其核心数据存储单元,底层存储机制结合了日志追加(append-only)、分布式存储和容错设计。Ledger 的数据存储在 Bookie 节点的磁盘上,具体实现涉及 Journal(日志)和 Ledger Storage(账本存储)两个部分。以下是 ledger 底层存储数据的详细机制:
Ledger 存储的整体架构
- 分布式存储:
- 一个 ledger 的数据分布在多个 Bookie 节点上(由 ensembleSize 定义,例如 3 个节点)。
- 每个 Bookie 负责存储 ledger 的一部分或全部数据(取决于 writeQuorum 配置)。
- 两阶段存储:
- Journal:实时记录写入操作的事务日志,确保数据持久化。
- Ledger Storage:长期存储账本数据,优化读取性能。
- 文件系统:
- 数据直接存储在 Bookie 节点的本地文件系统中(例如 ext4、XFS),没有额外的数据库层。
底层存储的实现细节
1. Journal(日志)
- 作用:
- Journal 是 ledger 数据写入的第一步,用于保证数据持久性和一致性。
- 每次写入条目(entry)时,先追加到 Journal,确保即使系统崩溃也能恢复。
- 存储位置:
- 配置项 journalDirectory 指定路径(例如 /bookkeeper/journal)。
- 每个 Bookie 节点独立维护自己的 Journal。
- 文件结构:
- Journal 由多个日志文件组成,按时间或大小滚动(rollover)。
- 文件名格式:journal.<timestamp>(例如 journal.1698765432100)。
- 每个文件是一个顺序追加的二进制文件。
- 写入过程:
- 客户端发送条目到 Bookie。
- Bookie 将条目序列化为二进制格式,包含:
- Ledger ID:账本标识。
- Entry ID:条目序列号。
- Data:实际数据内容。
- 追加到当前 Journal 文件。
- 可配置 journalSyncData=true(默认),调用 fsync 强制刷盘,确保数据持久化。
- 返回确认(ACK)给客户端。
- 性能优化:
- Journal 使用顺序写入,适合高吞吐量。
- 建议将 journalDirectory 放在高速磁盘(例如 SSD)上。
2. Ledger Storage(账本存储)
- 作用:
- Journal 确认后,数据异步写入 Ledger Storage,用于长期存储和读取。
- 存储位置:
- 配置项 ledgerDirectories 指定路径(例如 /bookkeeper/ledgers)。
- 可以配置多个目录(例如 /disk1/ledgers, /disk2/ledgers),分散 I/O 负载。
- 文件结构:
- Ledger 数据按 ledger 分片存储,目录结构:
-
/bookkeeper/ledgers/ ├── current/ # 当前活跃的账本文件 │ ├── 00000001.log # Ledger ID 1 的数据文件 │ ├── 00000002.log # Ledger ID 2 的数据文件 ├── recovered/ # 崩溃恢复后的文件 └── compacted/ # 压缩后的文件(可选)
- 每个 .log 文件对应一个 ledger,包含该 ledger 的所有条目。
- 写入过程:
- Journal 写入成功后,条目放入内存缓冲区(EntryLogger)。
- 缓冲区满或达到刷新间隔(ledgerStorage_flushInterval)时,异步写入 .log 文件。
- 数据按 Entry ID 顺序存储,文件格式为二进制。
- 索引:
- 为了快速定位条目,BookKeeper 维护一个索引。
- 配置项 indexDirectories 指定路径(默认与 ledgerDirectories 相同)。
- 默认使用文件系统索引(FileInfo),可选配置 RocksDB(dbStorage_rocksDB_* 参数)提高性能。
- 索引记录每个 Entry ID 在 .log 文件中的偏移量。
3. 数据分布
- Ensemble:
- 一个 ledger 的数据分布在 ensembleSize 个 Bookie 上。
- 例如,ensembleSize=3,数据可能存储在 bookie1、bookie2、bookie3。
- Write Quorum:
- 每次写入,数据完整存储在 writeQuorumSize 个 Bookie 上。
- 如果 writeQuorum < ensembleSize,不同条目可能分布在不同的 Bookie 子集。
- 副本:
- 每个条目在多个 Bookie 上有副本(由 writeQuorum 控制),提供容错性。
数据写入的完整流程
以 ensembleSize=3, writeQuorum=3, ackQuorum=2 为例:
- 客户端:
- 创建 ledger,分配 Ledger ID=1,选择 bookie1、bookie2、bookie3 作为 ensemble。
- 发送条目 entry1 到 3 个 Bookie。
- Bookie:
- bookie1:写入 /journal/journal.<timestamp>,返回 ACK。
- bookie2:写入 /journal/journal.<timestamp>,返回 ACK。
- bookie3:写入 /journal/journal.<timestamp>,返回 ACK(可能稍慢)。
- 客户端收到 2 个 ACK(满足 ackQuorum=2),写入成功。
- 异步存储:
- 每个 Bookie 将 entry1 从 Journal 移到 /ledgers/current/00000001.log。
- 更新索引,记录 entry1 的偏移量。
数据读取
- 读取流程:
- 客户端指定 Ledger ID 和 Entry ID。
- Bookie 从索引查找条目位置。
- 从 .log 文件读取数据返回。
- 容错:
- 如果某个 Bookie 不可用,客户端从其他副本读取(需要至少 ackQuorum 个副本可用)。
存储特性
- 追加式存储:
- Ledger 只支持追加写入(append-only),不支持修改或删除。
- 删除 ledger 需要关闭并通过 ZooKeeper 删除元数据。
- 纠删码(Erasure Coding):
- 默认不使用纠删码,而是完整副本存储。
- 可通过配置启用纠删码(实验性功能),减少存储开销。
- 持久性:
- Journal 的 fsync 保证写入持久化。
- Ledger Storage 异步写入,依赖 Journal 恢复一致性。
崩溃恢复
- Journal 回放:
- Bookie 重启时,检查 Journal 文件,恢复未写入 Ledger Storage 的条目。
- 恢复后,数据移到 recovered/ 目录。
- 一致性:
- 只要 ackQuorum 个 Bookie 存活,数据不会丢失。
性能优化
- 分离存储:
- 将 journalDirectory 和 ledgerDirectories 放在不同磁盘(例如 SSD 和 HDD),提高 I/O 性能。
- 批量写入:
- Journal 支持批量 fsync,减少磁盘同步开销。
- 索引优化:
- 使用 RocksDB 替代默认文件索引,加速查找。
总结
Ledger 的底层存储机制:
- Journal:顺序写入事务日志,保证持久性,存储在 journalDirectory。
- Ledger Storage:异步存储账本数据,分布在 ledgerDirectories 的 .log 文件中。
- 索引:记录条目偏移量,存储在 indexDirectories。
- 分布式:数据按 ensembleSize 分布在多个 Bookie,副本数由 writeQuorum 控制。
这种设计结合了高吞吐量(顺序写入)、低延迟(异步存储)和容错性(多副本),非常适合分布式日志存储需求。你的 Go Demo 数据最终存储在 3 个 Bookie 的 Journal 和 Ledger 文件中,具体路径取决于 Docker Compose 的卷配置