乐趣区

关于apache:博文推荐|深入解析Apache-BookKeeper-系列第一篇-架构原理

本文翻译自《Apache BookKeeper Internals — Part 1 — High Level》,作者 Jack Vanlightly。

译者简介

王嘉凌 @中国移动云能力核心,挪动云 Pulsar 产品负责人,Apache Pulsar Contributor,沉闷于 Apache Pulsar 等开源我的项目和社区

本系列对于 BookKeeper 的博客心愿帮忙大家了解和把握 BookKeeper 原理和外部逻辑。了解零碎外部运行逻辑是疾速定位并解决生产问题以及开发和批改新性能的基石。在本系列后续文章中,我会将 BookKeeper 各项指标与运行机制相结合,为大家展示高效进行性能问题定位的办法。

BookKeeper 中蕴含很多不同的插件,咱们次要关注 BookKeeper 作为 Apache Pulsar 的存储层应用的场景,不会波及其余一些不相干的插件。同时,咱们次要关注单个 bookie 节点外部的逻辑,对于 BookKeeper 在集群模式下多个节点之间的正本备份和通信协议相干的内容能够参考咱们之前的博客。

咱们次要先关注数据的读写流程,后续咱们也会波及数据回收、数据压缩和 CLI 工具等内容。

如果你看过 BookKeeper 的代码你会发现有些模块具备多个形象实现,咱们不去关注具体每个形象实现的区别(这些区别只和一些 BookKeeper Contributor 无关),而是重点关注解决申请的线程、数据的流向以及线程、数据结构和长久化存储之间的关联。

架构原理视图

咱们能够简略的把一个 BookKeeper 服务端节点(即 bookie)分为三层:顶层是网络通信层(应用 Netty),底层是磁盘 IO 层,中间层蕴含大量的缓存。咱们能够把 Bookie 了解为一个纯正的存储节点,负责尽可能快地写入和读取 ledger entry 数据,以及保障这些数据的平安。

这个视图外部蕴含了多个模块和线程模型,在本篇博客中咱们会逐层剖析和解释它们之间的关系。

组成模块

每个 ledger entry 都会被写入 journal 和 ledger 两个存储模块,Ledger 存储模块是长久化的存储,它有多种实现,在 Pulsar 集群中咱们应用的是 DbLedgerStorage。

Journal 模块保障落盘的数据不会丢并提供低提早的写性能。Entry 数据胜利写入 journal 后会立刻触发同步的写申请的响应告诉客户端 entry 曾经被胜利写入磁盘。DbLedgerStorage 模块则以异步的形式将数据批量刷盘,并在刷盘时对批量数据进行优化,将雷同 ledger 的数据按 entry 进行排序,以便后续可能程序读取。稍后咱们会进行更具体的形容。

读申请只会由 DbLedgerStorage 模块来解决,个别状况下咱们会从读缓存中读到数据。如果在读缓存中没有读取到数据,咱们会从磁盘上读取相应的 entry 数据,同时咱们会预读一些后续的数据并放到读缓存中,这样在进行程序读的时候,后续的 entry 数据就能够间接在读缓存里读到。稍后咱们也会进行具体的形容。

接下来,看看 Journal 和 DbLedgerStorage 两个模块在解决写申请时的外部实现。当咱们从 Netty server 接管到一个写申请,写申请的内容会被封装为一个对象并提交给解决写申请的线程池。这个 entry 数据首先会被传给 DbLedgerStorage 模块并被增加到写缓存(内存缓存),而后传给 journal 模块并被增加到一个内存队列缓存中。journal 和 ledger 模块中的线程会别离从对应的缓存里获取 entry 内容并写入磁盘。当 entry 写入 journal 磁盘后会触发同步的写申请响应。

在理解了有哪些模块之后,咱们还须要理解一下 bookie 的线程模型。每个 bookie 蕴含多个线程池和多个单线程来调用 Journal 和 Ledger Storage 模块的 API 接口。

线程模型

上图简略的展现了 bookie 中蕴含哪些线程和线程池,以及它们之间的通信关系。Netty 线程池负责解决所有的网络申请和响应,而后依据不同的申请类型会提交给 4 个线程池来解决后续逻辑。

Read 线程不受其余线程影响,它们能够独立实现整个读解决。Long Poll 线程则须要期待 Write 线程的写事件告诉。Write 线程则会跨多个线程以同步的形式实现写解决。其余像 Sync 和 DbStorage 线程则会进行异步写解决。

High Priority 线程池用来解决带有 high priority 标识的读写申请。通常蕴含 fencing 操作(对 journal 进行写操作)以及 recovery 相干的读写操作。在集群稳固的状态下,这个线程池基本上会处于闲暇状态。

线程之间通过以下形式进行通信:

  • 解决申请提交给另一个线程或者线程池(Java executors),每个 executor 有本人的 task 队列,解决申请会放入 task 队列中期待被执行。
  • 利用 blocking queues 之类的内存队列,一个线程将申请封装为 task 对象后增加到这个队列,另一个线程从队列中获取 task 对象并执行。
  • 利用缓存,一个线程将数据增加到写缓存(write cache),另一个线程从写缓存(write cache)中读取数据并写到磁盘。

在咱们深刻理解这些线程和模块的具体解决和交互逻辑之前,咱们先看一下计算(线程)和 IO(Journal / Ledger Storage)是如何实现并发解决的。

并行处理和程序保障

BookKeeper 反对计算和磁盘 IO 的并行处理。计算的并行处理通过线程池来实现,磁盘 IO 的并行处理则是通过将磁盘 IO 扩散到不同的磁盘目录来实现(每个磁盘目录能够挂载到不同的磁盘卷)。

Write, read, long poll 和 high priority 这四个线程池都是 OrderedExecutor 类的实例,这个类会依据须要读写的 entry 所属的 ledgerId 来调配负责解决的线程。

依据 ledgerId 来调配执行线程的形式使得咱们可能在进行并行处理的同时,还能保障针对同一个 ledger 的解决是按程序执行的。每个线程都有单独的 task 队列,从而保障提交到这个线程的解决可能按程序被执行。

配置线程数的参数如下:

  • serverNumIOThreads (Netty 线程, 默认为 2xCPU 外围数)
  • numAddWorkerThreads (默认为 1)
  • numReadWorkerThreads (默认为 8)
  • numLongPollWorkerThreads (默认为 0,示意长轮询读解决提交到读线程池)
  • numHighPriorityWorkerThreads (默认为 8)
  • numJournalCallbackThreads (默认为 1)

对于磁盘 IO,咱们能够通过将 Journal 和 Ledger 目录设为多个磁盘目录来实现磁盘 IO 操作的并行处理。

每个独自的 journal 目录都会创立一个独立的 Journal 实例,每个 Journal 实例蕴含独立的线程模型来进行写磁盘和回调写解决响应的操作。

咱们能够在 journalDirectories 配置多个 journal 磁盘目录。

对应每个 ledger 磁盘目录,DbLedgerStorage 会创立一个 SingleDirectoryDbLedgerStorage 实例,每个实例蕴含一个写缓存、一个读缓存、DbStorage 线程、一组 ledger entry logs 文件和 RocksDB 索引文件。各实例之间相互独立,不会共享缓存和文件。

咱们能够通过 ledgerDirectories 来配置多个 ledger 目录。

为了不便浏览,在本文前面将 SingleDirectoryDbLedgerStorage 简称为 DbLedgerStorage。

一次申请由哪个线程和组件来解决,取决于线程池的大小以及 journal 和 ledger 目录的数量。

默认状况下,写线程池只有 1 个线程。咱们在后续博客里会介绍,这个线程池没有太多的解决须要实现。

这样的并发解决架构使得 bookie 在具备多核 CPU 和多块磁盘的大型服务器上运行时,能够同时进步计算和磁盘 IO 的并发解决能力来进步性能。

当然,咱们晓得给 BookKeeper 扩容最简略的形式还是减少 bookie 节点的数量,因为 BookKeeper 自身具备弹性扩容的个性。

线程命名规定

如果你拉取一下 bookie 过程的堆栈信息,你会看到带有以下前缀的线程和线程池:

  • bookie-io (Netty 线程)
  • BookieReadThreadPool-OrderedExecutor
  • BookieWriteThreadPool-OrderedExecutor
  • BookieJournal-3181 (应用默认端口的状况)
  • ForceWriteThread
  • bookie-journal-callback
  • SyncThread
  • db-storage

总结

在本篇博客中咱们从线程和组件的角度介绍了 bookie 的架构,理解了 bookie 的申请是如何调度并交由这些线程和组件解决的。在本系列下一篇博客中,咱们会具体介绍写申请具体是如何在这些线程和组件中解决的。

关注 公众号「Apache Pulsar」,获取干货与动静

👇🏻 退出 Apache Pulsar 中文交换群 👇🏻

退出移动版