[TOC]
本篇文章主要聚焦于 BookKeeper 内核的实现机制上,会从 BookKeeper 的基本概念、架构、读写一致性实现、读写分离实现、容错机制等方面来讲述,因为我并没有看过 BookKeeper 的源码,所以这里的讲述主要还是从原理、方案实现上来介绍,具体如何从解决方案落地到具体的代码实现,有兴趣的可以去看下 BookKeeper 的源码实现。
正如 Apache BookKeeper 官网介绍的一样:A scalable, fault-tolerant, and low-latency storage service optimized for real-time workloads。BookKeeper 的定位是一个可用于实时场景下的高扩展性、强容错、低延迟的存储服务。Pulsar-Cloud Native Messaging & Streaming - 示说网 中也做了一个简单总结:
如下图所示,一个 Log/Stream/Topic 可以由下面的部分组成(图片来自 Pulsar-Cloud Native Messaging & Streaming)。
关于 Fragment,它是 Ledger 的物理组成单元,也是最小的物理存储单元,在以下两种情况下会创建新的 Fragment:
Apache BookKeeper 的架构如下图所示,它主要由三个组件构成:客户端 (client)、数据存储节点 (Bookie) 和元数据存储 Service Discovery(ZooKeeper),Bookies 在启动的时候向 ZooKeeper 注册节点,Client 通过 ZooKeeper 发现可用的 Bookie。
这里,我们可以看到 BookKeeper 架构属于典型的 slave-slave 架构,zk 存储其集群的 meta 信息(zk 虽是单点,但 zk 目前的高可用还是很有保障的),这种模式的好处显而易见,server 端变得非常简单,所有节点都是一样的角色和处理逻辑,能够这样设计的主要原因是其副本没有 leader 和 follower 之分,这是它与一些常见 mq(如:kafka、RocketMQ)系统的典型区别,每种设计都有其 trade-off,BeekKeeper 从设计之初就是为了高可靠而设计。
Apache BookKeeper 是一个高可靠的分布式存储系统,存储层的实现是其核心,对一个存储系统来说,关键的几点实现,无非是:一致性如何保证、IO 如何优化、高可用如何实现等,这小节就让我们揭开其神秘面纱。
Ledger 是 BookKeeper 的基本存储抽象单元,这里先看下一个 Ledger 是如何创建的,这里会介绍一些关于 Ledger 存储层的一些重要概念(图片来自 Pulsar-Cloud Native Messaging & Streaming)。
Ledger 是一组追加有序的记录,它是由 Client 创建的,然后由其进行追加写操作。每个 Ledger 在创建时会被赋予全局唯一的 ID,其他的 Client 可以根据 Ledger ID,对其进行读取操作。创建 Ledger 及 Entry 写入的相关过程如下:
这里引入了三个重要的概念,它们也是 BookKeeper 一致性的基础:
从上面 Ensemble、Qw、Qa 的概念可以得到以下这些推论:
对于分布式存储系统,为了高可用,多副本是其通用的解决方案,但也带来了一致性的问题,这里就看下 Apache BookKeeper 是如何解决其带来的一致性问题的。
在介绍其读写一致性之前,先看下 BK 的一致性模型(图片来自 Twitter高性能分布式日志系统架构解析)。
对于 Write 操作而言,writer 不断添加记录,每条记录会被 writer 赋予一个严格递增的 id,所有的追加操作都是异步的,也就是说:第二条记录不用等待第一条记录返回结果。所有写成功的操作都会按照 id 递增顺序返回 ack 给 writer。(图片来自 Twitter高性能分布式日志系统架构解析)。
伴随着写成功的 ack,writer 不断地更新一个指针叫做 Last-Add-Confirm(LAC),所有 Entry id 小于等于 LAC 的记录保证持久化并复制到大多数副本上,而 LAC 与 LAP(Last-Add-Pushed)之间的记录就是已经发送到 Bookie 上但还未被 ack 的数据。
所有的 Reader 都可以安全读取 Entry ID 小于或者等于 LAC 的记录,从而保证 reader 不会读取未确认的数据,从而保证了 reader 之间的一致性(图片来自 Twitter高性能分布式日志系统架构解析)。
从上面的介绍中,也可以看出,对于 BK 的多个副本,其并没有 leader 和 follower 之分,因此,BK 并不会进行相应的选主(leader election)操作,并且限制每个 Ledger 只能被一个 Writer 写,BK 通过 Fencing 机制来防止出现多个 Writer 的状态,从而保证写的一致性。
下面来看下 BK 存储层一个很重要的设计,那就是读写分离机制。在论文 Durability with BookKeeper 中,关于读写分离机制的介绍如下所示(图片来自 Durability with BookKeeper):
e
A bookie uses two devices, ideally in separate physical disks:
上面是论文中关于 BK 读写分离机制实现的介绍,我当时在看完上面的记录之后,脑海中有以下疑问:
带着这些疑问,接下来来分析其实现(图片来自 Pulsar-Cloud Native Messaging & Streaming):
Journal Device 分析:
Ledger Device 的实现:
了解完 BK 的一致性模型和读写分离机制之后,这里来看下 BK 的读写流程。
了解完 BK 的一致性模型和读写分离机制之后,这里来看下 BK 的读写流程。
Entry 写入流程
这里以一个例子来说明,假设 E 是3,Qw 和 Qa 是2,那么 Entry 写入如下图(图片来自 Durability with BookKeeper):
这里依然以一个例子做说明,例子是紧接着上面的示例,如下图所示(图片来自 Durability with BookKeeper):
如何想要读取 id 为1的那条 Entry 应该怎么做?
这种机制会导致,读取数据时可能需要从多个 Bookie 获取数据,需要并发访问多个 Bookie,性能会变差,极端情况会有这个问题。
BK 怎么处理长尾效应的问题(长尾效应指的是某台机器上某段或者某条数据读取得比较慢,进而影响了整体的效率)?
Fencing 机制在前面已经简单介绍过了,它目的主要是为了保证写的一致性,严格保证一个 Ledger 只能被一个 Writer 来写。
Fencing 怎么触发呢?
一个 Ledger 正常关闭后,会在其 Metadata 中存储 the last entry 的信息,所以正常关闭一个 Ledger 是非常重要的(Ledger 一旦关闭,其就是不可变的,读取的时候可以从任意一个 Bookie 上读取,而不需要再取 care 这个 Ledger 的 LAC 信息),否则可能会出现这样一种情况:
由于 Writer 挂了(Ledger 未正常关闭),导致部分数据写入成功,实际上这个条消息并不满足 Qw(可能满足了 Qa),会导致不同 Reader 读取的结果不一致!如下图所示:
解决方案就是: Log Recovery,正常关闭这个 Ledger,并将 The Last Entry 及状态更新到 metadata 中。
Log Recovery 怎么实现呢?通常有两种方案:
很明显,第二种方案是比较合理的恢复速度更快。
当一个 Bookie 故障时:
Bk 提供自动和手动两种方式:两种方式的复制协议是一样的;自动恢复是 BK 内部自动触发,手动过程需要手动干预,这里重点介绍自动过程:
AutoRecoveryMain 还有 Replicator Worker 线程会复制相应的 Fragment 到自己的 Ledger 上,如果复制后满足 Fully Replicated,那么就从 zk 的节点中删除这个任务;
每个 Bookie 在发现任务时会尝试锁定,如果无法锁定就会执行后面的任务。如果获得锁,那么:
如果 Ledgers 仍然存在副本数不足的 Fragment,则释放锁。如果所有 Fragment 都已经Fully Replicated,则从 /underreplicated 删除重复复制任务。
简单来说,Fencing机制用于防止有多个writer(pulsar中即为broker)同时写同一个topic/partition
什么时候会出现多个writer同时写同一个topic呢?在pulsar中,当zk检测到有一个broker1挂掉了,那么会把该broker1拥有的topic所有权转移到另一个broker2。如果broker1实际上没挂掉(类似出现脑裂的情况),那么会出现broker1、broker2同时写同一个topic,对于broker1写入完成的数据,由于topic已经给broker2接管了,在broker2看来并不知道broker1写入了数据,就会出现写入数据的不一致。
Broker crash,或 Broker 与 ZK 出现网络分区导致脑裂,需进行 partition ownership 转移。
Broker2 更新 ledger_X 的 metadata,将其置为 CLOSE 状态,再创建新 ledger,继续处理 Client 的写请求。