在这一部分中,我们将深入研究HerdDB,这是一个分布式数据库,依靠BookKeeper来实现自己的日志,并处理我们在先前文章中讨论的所有问题。
在本系列文章中,我想分享一些基本的架构概念,这些架构概念涉及无共享架构的分布式数据库的可能结构。 在第一部分中,我们了解了如何将数据库设计为复制状态机。 在第二部分中,我们将看到Apache BookKeeper如何通过提供强大的机制来构建数据库的预写日志来帮助我们。
现在,我们将深入研究HerdDB,这是一个分布式数据库,依靠BookKeeper来实现自己的日志并处理我们在先前文章中讨论的所有问题。
为什么使用 HerdDB
我们在EmailSuccess.com上启动了HerdDB,这是一个Java应用程序,它使用SQL数据库存储要传递的电子邮件状态。 EmailSuccess是一个MTA(邮件传输代理),即使使用一台机器,它也可以处理具有数百万条消息的数千个队列。
现在,HerdDB已在其他应用程序中使用,例如Apache Pulsar Manager或CarapaceProxy,我们正在MagNews.com平台中使用它。
最初,EmailSucess使用MySQL,但我们需要一个在Java应用程序的同一进程中运行的数据库,以简化部署和管理,例如SQLLite。
我们还需要一个数据库,该数据库可以跨越多台计算机,并可能利用系统固有的多租户体系结构:几台计算机上的数千个独立队列(通常为1到10)。
所以这里是HerdDB! 用Java编写的分布式可嵌入数据库。 如果您想了解更多有关此数据库的信息,请参考Wiki和可用的文档。
HerdDB数据模型
HerdDB数据库由表空间组成。 每个表空间是一组表,并且独立于其他表空间。 每个表都是一个键值存储,它将键(字节数组)映射到一个值(字节数组)。
为了完全替代MySQL,HerdDB附带了一个内置的高效SQL层,它能够支持SQL数据库中您期望的大多数功能,但需要权衡取舍。 我们将Apache Calcite用于所有SQL解析和执行计划。
使用JDBC驱动程序时,您只能访问SQL API,但是HerdDB还有其他应用程序,例如HerdDB Collections Framework直接使用较低级别的模型。
每个表都有一个SQL模式,该模式描述列,主键,约束和索引。
在一个表空间中,HerdDB支持表和跨多行和多表的事务之间的联接。
对于本文的其余部分,我们将仅讨论底层模型:
- 表空间由表组成
- 表是将二进制键映射到二进制值的字典
- 在表上支持的操作是:INSERT,UPDATE,UPSERT,DELETE,GET和SCAN
- 事务可以访问一个表空间中的多个表和行
数据和元数据
我们有几层数据和元数据。
群集元数据与整个系统有关:
- 节点元数据(发现服务)
- 表空间元数据(分配给它的节点,复制设置)
表空间元数据:
表空间数据:
- 未提交事务的数据
- 临时操作数据
- wal(预写入日志)
表数据:
当HerdDB在集群模式下运行时,我们将集群元数据存储在Apache ZooKeeper上,并将表空间元数据和数据存储在本地磁盘上。 预写日志位于Apache BookKeeper上。
发现服务和网络架构
我们有一组参与集群的机器,我们称它们为节点。每个节点都有一个ID,该ID在集群中唯一地对其进行标识。
对于每个节点,我们将所有有用的信息存储在ZooKeeper上,以查找节点,例如当前的网络地址和支持的协议(TLS可用性)。这样,如果您没有固定的iPhone地址或DNS名称,则可以轻松更改网络地址。
对于每个表空间,我们定义了一组副本节点。每个节点都存储整个表空间的副本。发生这种情况是因为我们支持跨多个记录的查询和事务:您可以通过单个操作访问或修改许多表中的许多记录,这必须非常有效。
选出其中一个节点作为领导者,然后将其他节点命名为跟随者。
客户端向表空间发出操作时,它将使用表空间元数据来定位领导节点,并使用发现服务来定位当前网络地址。所有这些信息都在ZooKeeper上,并在本地缓存。

服务器节点不互相通信,但是所有更新都通过BookKeeper进行。
仅在跟随者节点进行引导且其本地数据不足以从BookKeeper恢复时,才需要服务器到服务器的通信。
写步骤
写入操作遵循以下流程:
- 客户端找到表空间的领导节点(元数据在本地缓存)
- 客户端建立连接(连接被池化)
- 客户端发送写请求
- 节点解析SQL并计划执行操作
- 验证操作并为执行做好准备(行级锁,表空间检查点锁,约束验证,新值的计算……)
- 日志条目已排队,可以写入日志。
- BookKeeper将entry发送到法定数量的Bookies(写入法定bookies大小配置参数)
- 配置的Bookies数量(ack quorum size配置参数)确认已写入,BookKeeper客户端唤醒。
- 该操作的效果将应用于表的内存副本中的本地
- 执行清理操作(释放行级别锁,表空间检查点锁…)
- 该写操作已确认给客户端
这些步骤大多数都是异步的,因此可以提高吞吐量。
跟随者节点一直在跟踪日志:它们侦听BookKeeper的新entry,并将相同的更改应用于表的内存副本中的本地
我们正在使用长轮询读取模式以节省资源。 请检查BookKeeper文档中的ReadHandle#readLastAddConfirmedAndEntry。
切换到新的领导者
BookKeeper保证追随者最终将与表的最新版本保持最新,但我们必须将故事的其余部分全部实现。
我们有多个节点。 一个节点是领导者,另一个是跟随者。 但是,如何保证领导者只有一个节点呢? 我们必须处理网络分区。
对于每个表空间,我们在ZooKeeper中存储一个描述所有这些元数据的结构(表空间元数据),尤其是:
我们没有深入研究HerdDB中leader选举的工作方式。 让我们关注保证系统一致性的机制。
上面的结构对客户和系统管理很有用,但是我们需要另一个数据结构,该数据结构包含构成日志的当前Ledger集合,并且该结构也将成为领导力实施的另一个关键。
我们具有LedgersInfo结构:
- 生成日志的分类帐列表(activeledgers)
- 表空间历史记录中的第一个分类帐的ID(firstledgerid)
领导者节点仅保持一个总账保持打开状态,这始终是lactiveledgers列表末尾的总账。
BookKeeper保证领导者是唯一可以写入日志的领导者,因为分类账只能从一个客户端写入一次。
每个关注者节点都使用LedgerInfo在BookKeeper上查找数据。
当新的跟随者节点启动时,它将检查第一个ledger的ID。 如果第一个ledger仍在活动ledger列表中,则只需读取第一个到最新的分类帐序列即可执行恢复。
如果此ledger不再存在于活动ledger列表中,则它必须找到该ledger并下载完整的数据快照。
将跟随者节点提升为领导者角色时,它将执行两个步骤:
- 它将使用“recovery”标志打开活动ledger列表中的所有ledger,这将依次隔离当前的ledger
- 这将打开一个新的ledger进行写入
- 它将其添加到活动ledger列表中
对LedgersInfo的所有写操作都是使用ZooKeeper内置的比较并设置功能执行的,这保证了只有一个节点可以强制其领导。
如果有两个并发的新领导者试图将其自己的ledger ID附加到列表中,则其中一个将导致对ZooKeeper的写入失败,并且将使引导失败。
检查点
HerdDB无法永远保留所有ledger,因为它们将阻止Bookies回收空间,因此我们必须在可能的情况下将其删除。
每个节点(领导者或跟随者)定期执行检查点操作:
在检查点期间,服务器将其自身的数据本地副本和日志的当前位置合并到日志中:从该时间点开始,直到该位置的日志部分将无用,可以将其删除。
但是在集群模式下,您不能天真地这样做:
- 您不能只删除ledger的一部分,而只能删除整个ledger
- 追随者仍在跟踪日志,领导者无法删除尚未使用并在每个其他节点上建立检查点的珍贵数据
- 追随者不允许更改日志(只有领导者可以触摸LedgerInfo结构)
当前的HerdDB方法是使用一个配置参数来定义ledger的最长生存时间。 在那之后,所有在领导者节点检查点期间无用的旧ledger都将被丢弃。
如果您的追随者人数很少(通常是两个)并且已经启动并正在运行,则这种方法效果很好,目前大多数HerdDB安装就是这种情况。 预期跟随者节点的关闭时间不会超过日志生存时间。
通过这种方式,如果发生这种情况,引导跟随者可以连接到领导者,然后下载最近检查点的快照。
通常,由很少计算机组成的HerdDB集群可容纳数十到数百个表空间,每个节点是某些表空间的领导者,而其他节点则是跟随者。
事务
BookKeeper必须确认每个操作,然后才能将其应用于本地内存并将响应返回给客户端。 BookKeeper将写入内容发送给仲裁中的每个Bookie,然后等待。 这可能很慢!
在事务内,客户希望只有当事务成功提交后,事务操作的结果才会自动应用于表。
无需等待bookies确认属于事务的每个写入,您只需等待并检查最终提交操作的写入结果,因为BookKeeper保证所有写入均能持久且成功地持久保存。

这并不像您期望的那样简单,例如,您必须处理以下事实:客户端可能在事务上下文中发送长时间运行的操作,并由于某些应用程序级别超时而发出回滚命令:必须将回滚写入到 事务的所有其他操作之后的日志,否则跟随者(以及自我恢复过程中的领导节点本身)将看到一系列奇怪的事件:“开始事务”,“操作”,“回滚事务”以及事务的其他操作那已经不存在了。
回滚新Ledgers
如果领导者没有遇到麻烦并且永远不会重新启动,则它只能保持打开一个ledger并继续对其进行写入。
实际上,这不是一个好主意,因为该ledger将无限制地增长,并且使用BookKeeper不能删除一个分类帐的一部分,而只能删除完整的ledger,因此Bookies将无法回收空间。
在HerdDB中,在配置了一定数量的字节之后,我们将滚动一个新的ledger。这样,您可以在连续写入日志的情况下快速回收空间。
但是BookKeeper保证仅在处理单个ledger时适用。它保证只有当所有其他ID小于该条目ID的条目都已被成功写入时,才会向写入者确认每次写入。
启动新ledger时,必须等待并检查发布到前一个ledger(或至少最后一个ledger)的所有写入的结果。
您还可以看看Apache DistributedLog,它是Apache BookKeeper的高级API,它解决了我在本文中讨论的许多问题。
总结
我们已经看到了BookKeeper的实际应用程序,以及如何使用它来实现分布式数据库的预写日志。 Apache BookKeeper和Apache ZooKeeper提供了处理数据和元数据一致性所需的所有工具。 处理异步操作可能很棘手,您将不得不处理很多极端情况。 您还必须设计日志并让其回收磁盘空间,而又不会阻止跟随者节点的正确行为。
HerdDB仍然是一个年轻的项目,但是它正在关键任务应用程序的生产中运行,社区和产品的增长与新用户提出的用例一样多。
参考资料
https://streamnative.io/blog/tech/2020-05-12-distributed-database-bk3/