目录
目录
简介
A scalable, fault-tolerant, and low-latency storage service optimized for real-time workloads。BookKeeper 的定位是一个可用于实时场景下的高扩展性、强容错、低延迟的存储服务,它相当于把底层的存储层系统服务化(BookKeeper 是更底层的存储服务,类似于 Kafka 的存储层)。这样可以使得依赖于 BookKeeper 实现的分布式存储系统(包括分布式消息队列)在设计时可以只关注其应用层和功能层的内容,存储层比较难解决的问题像一致性、容错等,BookKeeper 已经实现了,从这个层面看,BookKeeper 确实解决业内的一些问题,而且 BookKeeper (Ledger 化,Ledger 相当于 Kafka segment)天生适合云上部署,未来还是有很大潜力的
-
低延迟多副本复制:Quorum Parallel Replication;
-
持久化:所有操作保证在刷盘后才 ack;
-
强一致性:可重复读的一致性(Repeatable Read Consistency);
-
读写高可用;
-
读写分离。
背景
BookKeeper由yahoo于2009年创建,并在2011年开源。
BookKeeper是一个可靠的日志流记录系统,用于将系统产生的日志(也可以是其他数据)记录在BookKeeper集群上,由BookKeeper这个第三方Storage保证数据存储的可靠和一致性。典型场景是系统写write-ahead log,即先把log写到BookKeeper上,再对log做处理,比如将log写到内存的数据结构中。BookKeeper同时适用于任何单点写入并要求保证高性能和数据不丢失(Strong Durabilty Guarantees)的场景。
BookKeeper诞生于Hadoop2.0的namenode HA。在Hadoop中,出于故障恢复的考虑,Namenode在对它的记录做修改前都会先将本条修改的日志写到磁盘上。但是这里有一个潜在问题,当Namenode发生故障时,很可能连本地磁盘也不能访问,这时之前的记录的日志也就没用了。基于上述考虑,可以将Namenode的日志信息保存在一个可靠的外部Storage中。最初业界通过NFS这样的Share Storage来实现日志同步。之所以选择NFS,一方面因为可以很方便地实现数据共享,另外一方面是因为NFS相对稳定成熟。虽然如此,NFS也有缺点不能满足HDFS的在线存储业务:网络单点及其存储节点单点。为了满足共享日志的高可用性,社区引入了BookKeeper。除此之外还有默认的HA方案:QJM。
架构
Apache BookKeeper主要包括三部分
-
客户端 (client)
-
数据存储节点 (Bookie)
-
元数据存储 Service Discovery(ZooKeeper)
Bookies 在启动的时候向 ZooKeeper 注册节点,Client 通过 ZooKeeper 发现可用的 Bookie。
组件介绍
BookKeeper是一种服务,它提供日志条目流的持久存储 - 也就是称为Ledger的记录序列。 BookKeeper在多个服务器上复制存储的条目
Basic terms
in Bookkeeper:
-
each unit of a log is an entry (aka record) 日志的每个单位都是一个条目(也就是记录)
-
individual servers storing ledgers of entries are called bookies 存储ledgers的各个服务器称为bookies
BookKeeper设计用于可靠且适应各种故障,Bookies可能会崩溃,损坏数据或丢弃数据,但只要有足够的bookies在整体中表现正常,整个服务就会表现正常(Quorum机制)
Entries
Entry包含写入Ledger的实际数据以及一些重要的元数据。BookKeeper entry 是写入Ledger的字节序列。每个条目都包含以下字段:
Field | Java type | Description |
---|---|---|
Ledger number | long | 已写入Entry的Ledger的ID |
Entry number | long | entry的唯一ID |
Last confirmed (LC) | long | 最后确认的entry的ID |
Data | byte[] | 由客户端直接写入的真实的数据 |
Authentication code | byte[] | 消息验证代码,包括entry中的所有其他字段 |
Ledgers
Ledger是BookKeeper中的基本存储单元。
Ledgers是entries序列,而每个entry是一个字节序列。条目将写入Ledger:
-
sequentially, and 顺序地
-
at most once. 最多一次的
这意味着Ledger只有附加语义。一旦将entry写入Ledger,就无法修改条目。确定正确的写入顺序是客户端应用程序的责任。
Clients and APIs
BookKeeper客户端有两个主要角色:它们创建和删除Ledger,它们从 Ledger 中读取 entry 并将 entry 写入 Ledger。
BookKeeper为Ledger交互提供了较低级别和较高级别的API。目前有两个API可用于与BookKeeper交互
-
The ledger API is a lower-level API that enables you to interact with ledgers directly.
-
The DistributedLog API is a higher-level API that enables you to use BookKeeper without directly interacting with ledgers.
通常,您应该根据您需要对Ledger语义进行多少细粒度控制来选择API。这两个API也可以在单个应用程序中使用
Bookies
Bookies是处理Ledger的单独的BookKeeper服务器(更具体地说,是Ledger的片段)。 Bookies作为整体的一部分
Bookie是一个单独的BookKeeper存储服务器。Bookie存储Ledger的碎片,而不是整个Ledger(为了表现)。对于任何给定的Ledger,ensemble 是存储Entry的一组Bookie
每当 Entry 被写入 Ledger 时,这些Entry就会在整个集合中顺序化(写入一组 Bookie 而不是所有 Bookie )。
Motivation
BookKeeper的最初动机来自Hadoop生态系统。 在Hadoop分布式文件系统(HDFS)中,名为NameNode的特殊节点以可靠的方式记录所有操作,这确保在崩溃的情况下可以进行恢复。
然而,NameNode仅作为BookKeeper的初始灵感。 BookKeeper的应用程序远远超出了这一范围,基本上包括任何需要基于附加的存储系统的应用程序。 BookKeeper为此类应用程序提供了许多优势:
-
高效的写入(高写入)
-
通过在bookies集合中复制消息来实现高容错性(高容错)
-
通过顺序化进行写入操作的高吞吐量(可以根据需要设置多个Bookies) (高吞吐)
Metadata storage
BookKeeper需要元数据存储服务来存储与Ledger和可用bookie相关的信息。 BookKeeper目前使用ZooKeeper执行此任务和其他任务。
zookeeper存储的信息格式:
Data management in bookies
Bookies以日志结构的方式管理数据,使用三种类型的文件实现: journals 、entry logs 、index files
事项 | 描述 |
---|---|
Journals | 日志文件包含BookKeeper事务日志。 在对Ledger进行任何更新之前,bookie会确保将描述更新的事务写入非易失性存储。 一旦bookie启动或旧的日志文件达到日志文件大小阈值,就会创建一个新的日志文件。 |
Entry logs | Entry log管理从 BookKeeper 客户端收到的entry写入请求。 来自不同 Ledger 的 entry按顺序聚合和写入,而它们的偏移量作为指针保存在 Ledger cache 中以便快速查找。 一旦bookie启动或旧的Entry log文件达到文件大小阈值,就会创建一个新的entry log文件。 垃圾收集器线程一旦与任何活动Ledger关联,旧Entry log文件将被删除。 |
index files | 为每个Ledger创建一个索引文件,该文件包含一个标题和几个固定长度的索引页,用于记录存储在条目日志文件中的数据的偏移量。 由于更新索引文件会引入随机磁盘I / O索引文件由后台运行的同步线程延迟更新。 这确保了更新的快速性能。 在索引页面持久保存到磁盘之前,会将它们收集在分类帐缓存中以进行查找 |
Ledger cache | Ledger索引页 缓存在内存池中,这样可以更有效地管理磁盘头调度。 |
Adding entries | 当客户端指示bookie将entry写入Ledger时,该条目将执行以下步骤以持久保存在磁盘上: 出于性能原因,Entry日志缓冲内存中的条目并批量提交它们,而Ledger缓存将索引页保存在内存中并且延迟刷新。 在下面的数据刷新部分中更详细地描述了该过程。 |
Data flush | Ledger索引页在两种场景下会被刷新到索引文件:
除了刷新索引页面之外,同步线程还负责滚动日志文件,以防日志文件占用太多磁盘空间。同步线程中的数据刷新流程如下
如果bookie在将LastLogMark保存到磁盘之前已经崩溃,它仍然包含日志文件,其中包含可能未保留索引页的条目。 因此,当这个bookie重新启动时,它会检查日志文件以恢复这些条目,并且数据不会丢失。 使用上述数据刷新机制,当bookie关闭时,同步线程可以安全地跳过数据刷新。 但是,在条目记录器中,它使用缓冲通道批量写入条目,并且在关闭时可能会在缓冲通道中缓冲数据。 该bookie需要确保入口日志在关闭期间刷新其缓冲的数据。 否则,条目日志文件会因部分条目而损坏 |
Data compaction
在bookies中,不同 Ledger 的 entry 在entry log文件中交错。 bookie运行垃圾收集器线程以删除未关联的Entry log文件以回收磁盘空间。如果给定的条目日志文件包含尚未删除的Ledger的entry,则永远不会删除entry log 文件,并且永远不会回收占用的磁盘空间。为了避免这种情况,bookie服务器压缩垃圾收集器线程中的Entry log文件以回收磁盘空间。
有两种不同频率的压实:轻微压缩和主压缩。轻微压缩和主要压缩之间的差异在于它们的阈值和压缩间隔。
-
垃圾收集阈值是那些未删除的Ledger占用的entry log文件的大小百分比。默认的轻微压缩阈值为0.2,而主要压缩阈值为0.8。
-
垃圾收集间隔是运行压缩的频率。默认的次要压缩间隔为1小时,而主要压缩阈值为1天。
如果阈值或间隔设置为小于或等于零,则禁用压缩。
垃圾收集器线程中的数据压缩流程如下:
-
线程扫描Entry log文件以获取其Entry Log元数据,该元数据记录包含entry log 及其相应百分比的Ledger列表。
-
使用正常的垃圾收集流程,一旦bookie确定已删除Ledger,将从Entry log 元数据中删除Ledger,并减少Entry log的大小。
-
如果Entry log 文件的剩余大小达到指定阈值,则entry log 中的存活的 Ledger 将复制到新的entry log文件中。
-
复制完所有有效记录后,将删除旧的entry log文件。
ZooKeeper metadata
BookKeeper需要ZooKeeper安装来存储Ledger元数据。无论何时构造BookKeeper客户端对象,都需要将ZooKeeper服务器列表作为参数传递给构造函数,如下所示:
String zkConnectionString = "127.0.0.1:2181";
BookKeeper bkClient = new BookKeeper(zkConnectionString);
更多的关于BookKeeper Java 客户端的信息, 请戳 this guide.
Ledger manager
Ledger管理器处理分类帐的元数据(存储在ZooKeeper中)。
BookKeeper提供两种类型的分类帐管理器:the flat ledger manager 和 the hierarchical ledger manager ,两个 Ledger管理器 都扩展了AbstractZkLedgerManager抽象类。
the flat ledger manager 是默认设置,几乎适用于所有用例。the hierarchical ledger manager 更适合管理大量BookKeeper Ledger(> 50,000)。
Flat ledger manager
flat ledger manager在FlatLedgerManager类中实现,它将所有ledgers的元数据存储在单个ZooKeeper路径的子节点中。flat ledger manager 创建顺序节点,以确保Ledger ID的唯一性,并为L. Bookie服务器的所有节点加上前缀,以便在散列映射中管理它们自己的活跃的Ledger,以便很容易地找到哪些Ledger 已从ZooKeeper中删除,然后对它们进行垃圾收集。
flat ledger manager的垃圾回收工作如下:
-
所有现存的ledger都来自ZooKeeper (zkActiveLedgers)
-
所有当前活跃在 Bookie 内部的 ledgers都被取走(bkActiveLedgers)
-
当前活跃的ledgers被循环遍历,以确定哪些ledgers当前不存在于ZooKeeper中。然后这些垃圾被收集起来。
-
flat ledger manager 将 Ledgers 的元数据存储在两级znode中。
Hierarchical ledger manager
在HierarchicalLedgerManager类中实现的 The Hierarchical ledger manager 首先使用EPHEMERAL_SEQUENTIAL znode从ZooKeeper获取全局唯一ID。 由于ZooKeeper的序列计数器的格式为%10d(10位填充0填充,例如<path> 0000000001),因此The Hierarchical ledger manager 将生成的ID拆分为3部分:
{level1 (2 digits)}{level2 (4 digits)}{level3 (4 digits)}
这三个部分用于形成存储 Ledger 元数据的实际 Ledger 节点路径:
{ledgers_root_path}/{level1}/{level2}/L{level3}
例如,Ledger 0000000001分为00、0000和00001三个部分,并存储在znode /{ledgers_root_path}/00/0000/L0001中。每个znode可以有多达10,000个Ledger,这避免了子列表大于ZooKeeper包的最大大小的问题(这是最初促使创建 The Hierarchical ledger manager 的限制)。
Ledger核心详解
Apache BookKeeper 提供的三个核心特性:I/O 分离、并行复制和容易理解的一致性模型。它们能够很好地满足我们对于持久化、多副本和一致性的要求。
在 Apache BookKeeper 中,读写操作的单元叫做 Ledger。Ledger 是一组追加有序的记录。
客户端可以创建一个 Ledger,然后进行追加写操作。每个 Ledger 会被赋予全局唯一的 ID。读者可以根据 Ledger ID,打开 Ledger 进行读操作。
Ledger Creation
客户端在创建 Ledger 的时候,从 Bookie Pool 里面按照指定的数据放置策略挑选出一定数量的 Bookie,构成一个 Ensemble。
Write Entries
-
每条被追加的记录在写者(Writer)会被赋予从 0 开始有序递增的序号,称为 Entry ID。
-
每条 Entry 会被并行地发送给 Ensemble 里面的所有 Bookies。并且所有 Entry的发送以流水线的方式进行。也就是意味着发送第 N + 1 条记录的写请求不需要等待发送第 N 条记录的写请求返回。
-
对于每条 Entry 的写操作而言,当它收到 Ensemble 里面大多数 Bookie 的确认后,Client 认为这条记录已经持久化到这个 Ensemble 中,并且有大多数副本,它就可以返回确认给 Application。
-
写记录的发送可以乱序,但是确认 (Acknowledge) 则会按照 Entry ID 的顺序进行有序确认。从而实现日志的严格有序性。
Ensemble Change
如果 Ensemble 里面的存活的 Bookies 不能构成大多数,Client 会进行一个 Ensemble Change。
Ensemble Change 将从 Bookie Pool 中根据数据放置策略挑选出额外的 Bookie 用来取代那些不存活的 Bookie (图中粉色方块)。通过 Ensemble Change 操作,Apache BookKeeper 保证写操作的高可用性。
一致性模型
理解 Apache BookKeeper 的读操作之前,需要先说明一下 Apache BookKeeper 的一致性模型。
对于 Writer 而言,write 不断地添加记录。每个记录会被 writer 赋予一个严格递增的 ID。所有的追加操作都是异步的。也就是写第二条记录不用等写第一条记录返回。所有写成功的操作按照 ID 递增顺序 Ack 回 writer。
Consistency
对于 Writer 而言,write 不断地添加记录。每个记录会被 writer 赋予一个严格递增的 ID。所有的追加操作都是异步的。也就是写第二条记录不用等写第一条记录返回。所有写成功的操作按照 ID 递增顺序 Ack 回 writer。
伴随着写成功的 Acknowledges,writer 不断地更新一个指针叫做 Last-Add-Confirmed (LAC)。所有 Entry ID 小于等于 LAC 的记录保证持久化并复制到大多数副本上。
而在 LAC 和 LAP (Last-Add-Pushed) 之间的记录就是已经发送到 Bookies 但是尚未被确认写成功的。
所有的 Readers 都可以安全地读取 Entry ID 小于或者等于 LAC 的记录,从而保证 reader 不会读到尚未被确认 (acknowledged) 的记录,从而保证了读者之间的一致性。
在写方面,BookKeeper 并不进行任何主动的选主 (leader election) 操作。相反地,它提供了内置的 fencing 机制,防止出现多个写者的状态,从而保证写者的一致性。
Apache BookKeeper 没有将很复杂的一致性机制捆绑在一起。写者和读者之间也没有很复杂的协同机制。所有的一致性的协调就是通过这个 LAC 指针 (Last Add Confirmed)。这样的做法,可以使得扩展写者和扩展读者相互分离。
读操作
理解了 Apache BookKeeper 的一致性模型之后,我们再回来看它的读操作。
在 Apache BookKeeper中,主要有两种读操作:一种是读指定的 Entry(图(a)),另外一种是读 LAC (图(b))。
-
读指定的 Entry
因为 Entry 追加之后不再被修改,那么在图 (a) 中,客户端可以到任意一个副本读取相应的 Entry。为了保证低延时(获得平滑的 p999), 我们使用了一个叫 Speculative Read 的机制。读请求首先发送给第一个副本,在指定 timeout 的时间内,如果没有收到 reponse,则发送读请求给第二个副本,然后同时等待第一个和第二个副本。谁第一个返回,即读取成功。通过有效的 Speculative Read,我们很大程度减小了 p999 延时的 spikes,达到可预测的低延时。
-
读取 LAC
这是读者跟写者之间的 Catch-Up 操作,保证读者读取到最新的数据。因此,它是采用的是 Quorum Read 的做法:从所有 Bookies 读取最新的 LAC,然后等待大多数的答复。
Read Entries 和 Read LAC 构成了Reader的核心操作。为了进一步降低延时,可以将两种操作进行合并,形成 “Long Poll Read”(图(c)). 客户端发送 Long Poll 请求并在 Bookie 等待最新的 LAC 的更新,一旦写者更新了 LAC,Bookie 返回更新后的 LAC 以及相应的 Entry。这样可以有效地节省多轮网络交互。同时对于 Long Poll Read,我们仍然采用 Speculative 机制,保证平滑的的可预测的 p999 延时。
主要api
http://bookkeeper.apache.org/docs/latest/api/ledger-api/
相关参考
http://bookkeeper.apache.org/docs/latest/getting-started/concepts/
http://matt33.com/2018/10/19/bk-cluster-install-and-use/#BookKeeper-%E7%AE%80%E4%BB%8B