Apache BookKeeper 很复杂吗?你细品

????️阅读本文需 8 分钟

作为 Pulsar 最重要的一个组件,Apache BookKeeper 支撑着 Pulsar 的一致性、容错性等特点,成为众多 Pulsar 用户都想了解的一趴。

所以在第六期的 TGIP-CN 里,由 Apache BookKeeper PMC 主席郭斯杰亲自为大家带来关于 BookKeeper 的一些基本概念、架构以及相关的读写流程。

接下来我们将从三个部分进行一一描述。


 概念  

关于 BookKeeper 的概念,核心就两个元素:Ledger 和 Entry

Ledger 的 API 很简单,创建一个 ledger,进行写数据、关闭或删除等操作。整个过程类似于电脑的文件系统操作。只不过 BookKeeper 在创建时,并没有专门针对一个文件系统去设计,更偏向于设计成「日志存储系统」。

创建 ledger 

Ledger 里更多关注的是元数据的存储,记录 Ledger 的副本信息,比如 - state,代表 ledger 的开关状态 open/close,还有一些代表 ledger 存放位置的复制信息,比如 ensemble / write quorum / ack quorum 等。

在左侧 ledger 里每增加一个 entry 时,就会返回一个 entry ID,这个 ID 是一个从 0 递增的整数。

前边也提到,ledger 里进行元数据存储时,会追加不同属性的 entry 信息。每条 entry 都有自己的元数据—— Entry data,包括上图中的数据指标。Ledger id 和 entry ID 记录的是 entry 的 key,LAC 是指最后一条已确认添加的记录(即 entry ID),用整数标记。Digest 则是记录字节的数据,用于完整性校验。

关闭 ledger 

当增加了足够多的 entry 时,决定关闭这个 ledger,这时也会有相关元数据的记录。相比创建时的元数据,这里的状态就变成了关闭。同时 entry ID 也会根据最后一条 entry 去记录,其余的基本不变。

综上而言,BookKeeper 更像是一个面向「日志存储、可追加负载」的工作场景去设计的工具。


   架构  

熟悉 BookKeeper 的同学都知道, 它的元数据存储—— Metadata store,目前是由 ZooKeeper 进行,当然也支持 ETCD,用来存储 ledger ID 对应的元数据。

而集群中的 bookie 用来存储这些 ledger 对应的 entry,所有的 bookie 会注册到 BookKeeper 上,由客户端去发现并采取相应的操作。BookKeeper 的客户端主要是实现一些与一致性、策略性相关的逻辑。

以上就是 BookKeeper 里三个组件的关联与作用。

很多同学都是从 Pulsar 了解到 BookKeeper 的。Pulsar 里的存储计算和存储分层得益于 broker 和 BookKeeper,上图中的客户端也就是 Pulsar broker。Broker 调用 BookKeeper 客户端去跟 bookie 等相应的 ZooKeeper 去交互。

 Bookie 

我们可以把 bookie 视为一个「Key/Value database」,是一个针对整个 BookKeeper 逻辑进行设计的。Key 指上文提到的 ledger ID + entry ID 构成的标识组件。value 是指存到 ledger 里的 entry,追加和读取操作,都是针对某一个值去运行的。

Bookie 的实现,依靠 journal 和 ledger storage ,这些概念我们也在之前的文章中提到过,详情可参考:Message Lifecycle:Pulsar 里的信息传递究竟是什么样子

Bookie 利用 journal 进行所有写的操作都是。在追加多条 entry(来自不同的 ledger)的过程中,journal 都在发挥着它的持久化作用。这样做的优点是不管 ledger 来自何处,Bookie 只负责按顺序将entry写到journal文件里,不会进行随机访问。

当一个 journal 文件写满后,Bookie 会自动开启一个新的 journal 文件,继续按顺序填补 entry 。


但问题是,用户无法在 journal 里查询某条 entry。所以如果应用到读请求时,就需要「索引」功能来达到更高效的过程。

为了让各组件独立完成任务,没有在 journal 上建立索引功能,而是在 bookie 端维持了一个「write cache」,在内存里进行一个写缓存。在 journal 里运行结束后,会放置到 write cache 里。

经过 write cache 过程后,Bookie对 entry 进行重新排序,按 ledger 的来源划分整理entry,以便确保在缓存变满的过程中,entry 可以按照 ledger 的顺序排队。

当缓存变满后,bookie 会把整个 write cache 冲到磁盘里。Flush 的过程又重新整理了几个目录,用来保留相关的映射关系。一个是 entry log,用来存储 value。同时维护另一个 ledger index,用来记录 entry id 的位置。

默认的 ledger storage 有两类:DB ledger storage 和 Sorted ledger storage。本质上,这两类ledger storage的实现途径是一样的,只是在处理索引存储时不太一样。

所以整个 bookie 的实现就非常简单,没有很复杂的逻辑套用。


  数据流动  

Bookie 操作基本都是在客户端完成和实现的,比如副本复制、读写 entry 等操作。这些有助于确保 BookKeeper 的一致性。

那么,在整个 data flow 的过程中,数据又是如何在客户端里完成的呢?

客户端在创建 ledger 时,会出现 Ensemble、Write Quorum 和 Ack Quorum 这些数据指标。

  • Ensemble —— 用哪几台 bookie 去存储 ledger 对应的 entry

  • Write Quorum ——对于一条 entry,需要存多少副本

  • Ack Quorum —— 在写 entry 时,要等几个 response

默认情况下是(3,3,3),但接下来我们会用(5,3,2)的实例进行讲述,可能会让大家会更清晰的了解。

(5,3,2) 代表了对于一个 ledger ,会挑 5 台 bookie 去存储所有的 entry。所以当 entry 0 生成时,可以根据曲线模型计算出应该放置到哪台 bookie。比如 E0 对应 B1,B2,B3,那 E1 就对应 B2,B3,B4,以此类推。

虽然总体是需要 5 台 bookie,但是每条 entry 只会占用 3 台 bookie 去存放,并只需等待其中的 2 台 bookie 给出应答即可。

 LastAddConfirm 

在第一部分中,我们提到 LAC(LastAddConfirm)。它是由 LAP(LastAddPush) 延伸而来是根据客户端进行维护并发布的最后一条 entry id,从 0 开始递增。所以 LAC 是由应答确认回来的最后一条 entry id 构成,如下图右侧显示。

对于 ledger而言, 所谓的消息顺序,不是存储 bookie 的文件位置,,而是客户端发出entry的顺序。

所以 LAC 以前的 entry ID (比它本身小的)都已确认过,它其实是一致性的边界,LAC 之前和之后的都是一致的。

同时 LAC 作为 bookie 的元数据,可以根据此来判断 entry 的确认与否。这样做的好处是,LAC 不会受限于一个集中的元数据管理,可以分散存储在存储节点。

 Ensemble change 

当其中的某个 bookie 挂掉时,客户端会进行一个 ensemble change 的操作,用新的 bookie 替换挂掉的旧 bookie。比如 当bookie 4 挂掉时,可以使用 bookie 6 进行替换。

整个过程,只要有新的存储节点出现,就会保证不会中断读写操作是,即过程中随时补新。

Ensemble change 对应到元数据存储,即对元数据的修改。之前的 E0-E6 是写在 B1~B5 上,E7 以后写在了 B1、B2、B3、B6、B5 上。这样就可以通过元数据的方式,看到数据到底存储在那个bookie上。

 Speculative read 

写的过程搞定了,那读的过程自然就轻松了。有了 LAC后,消息的读取变得更加方便,通过一个 entry ID 即可找到对应的 bookie。当你要读一条 entry 时,没有中间所谓顺序的过程。这样做的好处就是,读取可以并行实施,提高灵活性。

另外一个优点就是,对于一些主从系统,如果读 bookie1 时反应速度过慢,其实可以再发起一个请求,去读 bookie2。因为 LAC 的加持,可以保证更随意、更翻边的返回数据请求。

在这种情况下,可以利用 speculative read 来设置等待时间,比如 100 秒时第一台还没有返回请求,可以立马切换到第二台进行读取。这点保证了 BookKeeper 的低延时特性,不依赖于物理存储。


  本期 Q&A  

1. 会存在多客户端写 journal 的并发竞争吗?

首先,不管是 bookie 的客户端还是服务端都是 Netty 的,客户端连接 

bookie 都是利用一个复用的网络通道,写不同 ledger 的时候都是往这个链路去发,它收到的只是的一个个的 key ,然后 append 到 journal 里,journal 

的写并没有竞争的发生。

2. 后面会有归并 compact 吗?需不需要全局有序?

并没有把一个 ledger compact 到同一个 file 去,其实这是一个局部有序,

因为局部有序已经足够了。对于大部分扫描操作,局部有序扫了一部分 

entry, 就可以跳到下一段 entry。这个跳跃的过程开销比较小,同时开销会被平摊到扫描部分。所以没有什么性能损耗,不需要全局有序。

3. 当 journal 配置不同目录,如何保证不同 journal 目录之间流量的负载均衡?

目前是把一个 ledger id 映射到各个 journal thread 里,这个 ledger 对应的 entry 都会归到一个 ledger 里面进去。

4. 请求是在 journal 刷盘后返回还是等顺序刷盘后返回?

JournalSinkData = True 时,是刷盘后返回;JournalSinkData = False 时,写表示到文件系统,即不等刷盘就返回。

5. Ledger 元数据是保存在 ZooKeeper 吗?

是的。

6. 客户端在写 journal 之前就已经将 entry 写到 write cache了吗?

目前的实现方式是客户端在写 journal 之前先把 entry 丢到 write cache 里,然后再去写 journal。

7. Ledger 中 entry id 用满 2^64 会创建新的 ledger 吗?

基本不会写满,因为会很快 close ledger。

8. Journal directory 和  Ledger directory 怎么规划?

Journal directory 根据磁盘带宽来进行规划,由 bookie 所需承受的带宽决定。推荐 SSD,也可以用 HDD;Ledger directry 考虑带宽+容量。

9. 挂一个节点时,Fragment 内存副本同步是否造成大量 IO,会影响 BookKeeper 的整体吞吐性能吗?

Fragment 的内部同步是由 AutoRecovery 决定的。内部控制并发复制的数量,所以可以来控制带宽的占用。

10. ZooKeeper 元数据会出现大量修改吗?

这个取决于 AutoRecovery 的修复速度,其实这是一个并发的过程。修复时候会先修复数据。


   总结   

以上就是从概念和架构层面,对 BookKeeper 进行了一些系统的描述。同时利用拓扑图的表现形式,将数据传输的细节也进行了一些讲解。

如果想回顾完整直播内容,可直接观看下方视频:

关于本次所有的拓扑图和思维导图,大家都可以在这个页面进行下载:
https://github.com/streamnative/tgip-cn/tree/master/episodes/006

也可以点击「阅读原文」直接进入页面下载吼。

 往期直播内容回顾:

如果你觉得 TGIP-CN 的直播不错

也可以帮我们多多转发宣传~

❤️

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值