我们接着上一篇Pulsar存储计算分离架构设计之存储层BookKeeper(上)

继续讲:

六、Bookie的数据读取流程

我们继续跟着上一篇的server端的源码分析上来,直接看 org.apache.bookkeeper.proto.BookieRequestProcessor#processRequest 这个方法,在最新版本的Bookie代码中,读取操作的核心逻辑基于READ_ENTRY操作码(opCode)实现,同时读取处理器(read processor)已迭代升级至V3版本,优化了数据处理的性能和可靠性。我们这里直接跟写入流程一样,读取流程也来分析V3版本的源码。


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_使用场景

processReadRequestV3 方法的核心逻辑见下:

Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_缓存_02

上面代码跟进来,在 org.apache.bookkeeper.bookie.Bookie#readEntry

这段代码的读取流程通过多层委托实现:首先根据LedgerId找到LedgerDescriptor,随后其readEntry方法将请求转交给LedgerStorage接口的getEntry方法,最终由SingleDirectoryDbLedgerStorage#getEntry实际执行(与写入流程共用同一实现类)。整个过程体现了接口抽象与分层解耦的设计思想。

SingleDirectoryDbLedgerStorage#getEntry

Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_使用场景_03

以下是getEntry方法核心要点总结:

  • 特殊处理
  • 当 entryId 为 LAST_ADD_CONFIRMED 时,调用 getLastEntry(ledgerId) 获取最新确认的条目
  • 写缓存访问机制
  • 首先检查 writeCache(当前写入缓存)
  • 然后检查 writeCacheBeingFlushed(正在刷盘的缓存)
  • 使用乐观读锁获取当前写缓存引用,避免阻塞写操作
  • 双重检查机制:
  • 缓存命中时立即返回并记录统计信息
  • 读缓存检查
  • 检查 readCache(读取缓存)是否存在目标条目
  • 命中时记录缓存命中统计并返回结果
  • 主存储读取流程
  • 通过索引 entryLocationIndex 查找条目在日志中的位置
  • 位置为0时表示条目不存在,抛出 NoEntryException
  • 调用 entryLogger.readEntry 从磁盘读取实际数据
  • 缓存更新策略
  • 将从磁盘读取的条目放入 readCache 供后续访问使用
  • 执行预读操作 fillReadAheadCache 加载后续条目到缓存中
  • 性能统计记录
  • 缓存命中:记录 readCacheHitStats
  • 缓存未命中:记录 readCacheMissStats
  • 读取操作成功/失败:记录 readEntryStats

EntryLogger如何读取entry

org.apache.bookkeeper.bookie.EntryLogger#internalReadEntry


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_日志文件_04


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_日志文件_05

Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_缓存_06

主要的逻辑点:

  • 位置解析
  • 从 location 参数中提取 entryLogId(日志文件ID)和 pos(文件内位置)
  • 用于定位具体在哪个日志文件的哪个位置读取数据
  • 文件通道获取与大小读取
  • 从位置 pos - 4 处读取4字节的大小信息
  • 这4字节包含了条目的长度信息
  • 调用 getFCForEntryInternal 获取对应的文件读取通道
  • 通过 readEntrySize 方法读取条目大小信息:
  • 数据读取核心逻辑
  • 如果是当前正在写的日志文件且读取位置接近文件末尾,使用同步方式读取
  • 否则直接从缓冲通道读取
  • 根据读取到的条目大小分配 ByteBuf 缓冲区
  • 调用 readFromLogChannel 从指定位置读取完整的条目数据
  • 读取逻辑优化:
  • 异常处理与完整性检查
  • MissingEntryException:条目不存在
  • MissingLogFileException:日志文件不存在
  • 其他 IO 异常
  • 如果读取的字节数与预期大小不匹配,抛出 NoEntryException
  • 处理多种异常情况:
  • 资源管理
  • 正确设置 ByteBuf 的写索引到数据末尾
  • 读取失败时及时释放已分配的缓冲区资源

七、Bookie的数据读取流程总结

7.1 Bookie的数据读取流程整体架构

读取流程相对于上一篇写入流程来说简单些

在整体架构中,数据读取需依次穿透三级缓存:首先访问最上层缓存,未命中则逐级下沉至次级缓存,若三级缓存均未命中,最终触发磁盘访问。值得注意的是,Broker组件内部还存在独立的内存缓存层,用于拦截高频请求。整个消息获取流程如下图所示:


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_使用场景_07


7.2 Bookie的数据读取流程核心时序图


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_使用场景_08


7.3 组件模块分析

BufferedChannel


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_日志文件_09


这几个Channel类在BookKeeper中构成了一个层次化的缓冲I/O通道体系,各自有不同的用途和特点:

  • BufferedChannelBase(基础类)
  • 含义:所有缓冲通道的基类,提供基本的缓冲功能
  • 使用场景:作为其他缓冲通道类的共同基类,提供通用功能
  • BufferedChannel(通用缓冲通道)
  • 用于一般的文件读写操作
  • 作为其他专用通道的基础实现
  • 含义:标准的缓冲读写通道,支持读写操作
  • 使用场景:
  • SlowBufferedChannel(慢速缓冲通道)
  • 测试环境中的故障模拟
  • 性能基准测试
  • 验证系统在慢速存储下的行为
  • 含义:专门用于模拟慢速存储设备的通道,主要用于测试
  • 使用场景:
  • BufferedReadChannel(缓冲读通道)
  • Bookie读取日志文件条目
  • 优化读密集型操作的性能
  • 含义:专门优化用于读操作的缓冲通道
  • 使用场景:
  • BufferedLogChannel(日志缓冲通道)
  • Bookie写入entry日志文件
  • 管理journal日志文件
  • 处理日志文件的生命周期
  • 含义:专门用于BookKeeper日志文件的缓冲通道
  • 使用场景:

EntryLogger


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_使用场景_10


  • EntryLogger(条目日志记录器)
  • Bookie正常运行时的条目读写
  • Journal和EntryLogger的日常操作
  • 含义:标准的条目日志管理器,负责条目的读写操作
  • 使用场景:
  • ReadOnlyEntryLogger(只读条目日志记录器)
  • 数据恢复过程
  • 备份和归档操作
  • 只读副本或镜像节点
  • 垃圾回收(Garbage Collection)过程中的日志扫描
  • 含义:专门用于只读操作的条目日志记录器
  • 使用场景:
  • SlowEntryLogger(慢速条目日志记录器)
  • 含义:用于模拟慢速日志操作的记录器,主要用于测试和调试

LedgerStorage


Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_缓存_11


  • LedgerStorage(账本存储接口)
  • 含义:账本存储的顶层接口,定义了所有账本存储实现必须提供的基本操作
  • 使用场景:作为所有账本存储实现的统一接口
  • CompactableLedgerStorage(可压缩账本存储)
  • 含义:支持垃圾回收和压缩操作的账本存储抽象类
  • 使用场景:需要定期清理过期数据的存储场景
  • SortedLedgerStorage(排序账本存储)
  • 含义:基于排序索引的账本存储实现
  • 使用场景:需要有序访问条目或范围查询的场景
  • InterleavedLedgerStorage(交错账本存储)
  • 含义:交错存储多个账本条目的实现,是BookKeeper最常用的存储格式
  • 使用场景:BookKeeper默认的生产环境存储实现
  • SlowInterleavedLedgerStorage(慢速交错账本存储)
  • 含义:模拟慢速存储的交错账本存储实现,用于测试
  • 使用场景:系统性能测试、容错能力验证、慢速存储环境模拟
  • SlowSortedLedgerStorage(慢速排序账本存储)
  • 含义:模拟慢速存储的排序账本存储实现,用于测试
  • 使用场景:排序存储相关的性能测试、系统稳定性和容错性验证

八、存储层BookKeeper架构总结

Bookie的存储架构设计围绕高性能读写优化展开,通过三级存储结构实现吞吐量与延迟的平衡。

Journal层作为第一道屏障,采用顺序混合写入模式,不同Topic的消息被交织写入独立挂载的磁盘,以此实现低延迟写入(写完即返回)。但混合存储的特性导致其难以支持单个Topic的顺序读取和消息回溯,存在显著的读放大问题。

EntryLog层通过二次转储解决Journal的读取缺陷:写入时按LedgerId+EntryId排序,使同一Ledger的数据物理相邻,提升局部性读取效率。每条消息写入后会记录其物理偏移量(Offset),但该Offset缺乏业务语义——无法直接反映消息在Topic中的逻辑顺序,而消费端恰恰依赖这种逻辑序列进行位点管理。

为此引入索引层构建LedgerId+EntryId到物理Offset的映射关系。Bookie同时支持RocksDB(Pulsar默认选用)和自研KV引擎存储该映射,通过用户态缓存(优于PageCache)降低容器化环境中多POD间的IO干扰,确保读写性能稳定。

整个架构通过分层设计实现写入高吞吐与读取低延时的统一:Journal保障写入速度,EntryLog优化数据局部性,索引层弥合物理存储与逻辑消费的鸿沟,配合用户态缓存减少抖动。

Pulsar 存储计算分离架构设计之存储层 BookKeeper(下)_缓存_12

Apache Pulsar 作为一个高性能、分布式的发布-订阅消息系统,正在全球范围内获得越来越多的关注和应用。如果你对分布式系统、消息队列或流处理感兴趣,欢迎加入我们!

Github: 

https://github.com/apache/pulsar