0
点赞
收藏
分享

微信扫一扫

深入解析 Apache BookKeeper 系列:第三篇——读取原理

本文翻译自《Apache BookKeeper Internals — Part 3 — Reads》,作者 Jack Vanlightly。
译者:王伦辉,开源爱好者,贵州本土开发者。

本系列基于为 Apache Pulsar 配置的 BookKeeper 4.14。

在​​上一篇文章​​中,我们讨论了每次从 Netty 层到文件 IO 的写入路径,以及所有涉及的线程和组件。在这篇文章中,我们将对读取路径做同样的解析。读取请求被提交到要执行的读线程池,并且可以有多个线程来完成这项工作(因此它是一个池)。默认情况下,长轮询读取也提交给读线程轮询,但可以配置为在单独的长轮询线程池上运行。

执行读取时,选定的读线程调用 DbLedgerStorage getEntry(long ledgerId, long entryId) 方法,并且读取是同步的。

深入解析 Apache BookKeeper 系列:第三篇——读取原理_python

图 1. 读取过程仅进入 DbLedgerStorage 并同步执行。

读请求的处理顺序如下:

  1. 1. 检查写缓存,如果缓存命中则返回 entry。
  2. 2. 如果未命中写缓存,则检查读缓存;如果缓存命中则返回 entry。如果读缓存未命中,表明该 entry 仅存在于磁盘上。
  3. 3. 使用 Entry Location Index (RocksDB) 获取 entry 在磁盘上的位置。索引将返回一个位置(entry 日志文件和在文件中的偏移量)。
  4. 4. 在指定的偏移处读取指定的 entry 日志文件。
  5. 5. 执行预读。
  6. 6. 将所有预读 entry 加载到读缓存中。
  7. 7. 返回 entry。

预读操作涉及从 entry 日志文件中读取,从下一个 entry 开始一直读取 entry,直到出现以下情况之一:

  1. 1. 达到批量预读大小限制(默认为 1000 个 entry,由 Pulsar 配置);
  2. 2. 到达文件末尾;
  3. 3. 到达不同 Ledger 的 entry。

默认情况下,读缓存的总大小(每个 DbLedgerStorage 实例有一个读缓存)是可用直接内存的 25%。因此,对于两个 Ledger 目录和 2GB 的总读缓存,每个 DbLedgerStorage 实例都会获得 1GB 的读缓存。

预读(Readahead) 是高效的,因为相同 Ledger 的 entry 位于磁盘上的连续块中,这是由于在刷盘写入文件之前首先按 Ledger 和 entry id 对 entry 进行了排序。这意味着预读不需要位置索引并且读取是有序的。

在客户端使用粘滞读取(sticky read)对性能很有帮助,因为来自给定 Bookie 客户端的所有读都将发送到单个 Bookie,这种操作会很好地利用这种预读。如果读分散在不同的 Bookie 中,则会降低预读操作的功效。例如,如果一个客户端(一个 Pulsar Broker)在三个 Bookie 之间随机分配 0-99 个 entry 的读取,那么每个 Bookie 最终都会覆盖大部分范围的预读,但每个只会处理大约 ⅓ 的读,同时我们已经在 Bookie 中将每个 entry 的副本加载了 3 次。

除此之外,还可能发生其他状况,例如缓存抖动(cache thrashing),其中读缓存的有效性会降低,甚至会影响到性能。

读缓存

每个读缓存由一个或多个大小不超过 1GB 的分片(segment)组成,在逻辑上可以视为单个环形缓冲区(ring buffer)。作为环形缓冲区,内存会预先分配,新添加的 entry 最终会覆盖旧 entry。每个缓存都有一个它包含的 entry 的索引,用于快速查找和检索。

读缓存抖动

如果读缓存太小而无法满足预读操作的需求,entry 将在被读取之前不断从读缓存中逐出,这种情况即缓存抖动。缓存抖动会导致先前已从磁盘读取的 entry 被再次读取以放置在缓存中。

为了避免读缓存抖动,Bookie 的读缓存需要足够大,以便容纳当前所有 Pulsar 消费者的预读。当一个 Topic 或跨 Topic 的不同位置有许多 Pulsar 消费者且这些位置在 Broker 自己的缓存之外时,BookKeeper 可能会收到大量读负载。在这些场景中,Pulsar 必须向 Bookie 发送读请求,最坏的情况是 entry 和预读完全不相交,并且这些不相交预读的总大小超过了缓存的容量。

深入解析 Apache BookKeeper 系列:第三篇——读取原理_python_02

图 2. 客户端(Pulsar Broker 中的消费者对象)在使用前互相驱逐预读 entry

当需求数据大小超过缓存大小时,越来越多未被读取的 entry 从缓存中被驱逐,因此必须从磁盘重新读取。

深入解析 Apache BookKeeper 系列:第三篇——读取原理_数据库_03

图 3. 需要多少读缓存和最终从磁盘读取(和重新读取)每个 entry 的平均次数之间的关系。以上数字基于真实生产事件。

可以通过以下方式消除或减少这种读缓存抖动:

  • • 增加读缓存的可用内存量;
  • • 减少批量读缓存大小。

总结

如果我们忽略整体上高 CPU 利用率这一点,那么读取路径中瓶颈的原因可能是磁盘 IO。简单的情况是底层存储卷不够快,无法满足需求。另一个原因可能是没有足够的读线程将卷推到其极限。最后是读缓存抖动,这会导致磁盘 IO 数量急剧上升,以至于存储卷达到达到限制的速度比原来更快。

在下一篇文章[1]中,我们将看看 BookKeeper 用于保护自己免于过载的背压机制。




举报

相关推荐

0 条评论