草庐IT

翟佳:高可用、强一致、低延迟——BookKeeper的存储实现

DataFunTalk 2023-03-28 原文

分享嘉宾:翟佳 StreamNative 联合创始人

编辑整理:张晓伟 美团点评

出品平台:DataFunTalk


导读:多数读者们了解BookKeeper是通过Pulsar,实际上BookKeeper在数据库和存储场景都有着非常广泛的应用。BookKeeper是Pulsar的底层存储,Pulsar有着广泛数据入口,Pulsar跟Kafka及各类MQ(RabbitMQ、ACTIVEMQ)的较大区别是Pulsar是统一的云原生消息流平台,不但是分布式系统,而且做了存算分离,可以让用户在云的环境下,体验到云原生的优势,例如随意扩缩容、数据灵活迁移复制等。希望通过本文,让大家对Pulsar底层的BookKeeper有更深入的了解。

今天的介绍会围绕下面四点展开:

  • BookKeeper的简介
  • BookKeeper的特性
  • BookKeeper存储介质的演进
  • BookKeeper的社区资源

--

01 BookKeeper的简介

1. 业务场景需求的统一

Pulsar里有很重要的概念是“统一”,这个统一的特性是由BookKeeper支持实现的。这里的统一是指需求的统一,在消息场景下,用户场景分两类:

第一类是线上业务场景,例如1984年诞生的IBM MQ到现在的各类开源MQ解决的是线上业务场景,这些MQ的服务质量会对业务服务质量有着直接的影响,所以这类需求对数据质量,例如对数据持久性、数据延迟、消费模型的灵活性有较强的要求。

第二类是大数据场景,例如2010年左右随着实时计算的广泛使用,Kafka的这种高带宽和高吞吐使用需求。

由于面向场景不同、技术栈不同,这两种场景在业务上又同时存在,给业务带来不同的基础设施API、不同的使用方式、不同系统的运维成本等问题。所以Pulsar针对这些问题,做了两层API的统一:既兼容MQ的并发消费模型,提供比较好的服务质量,同时通过底层存储层抽象,可以提供很高的吞吐和带宽,这就是我们要介绍的Apache BookKeeper项目。

2. Apache BookKeeper简介

很多服务里都有日志,例如MySQL的binlog和HDFS的namenode的editlog,都是对日志的一个抽象,而BookKeeper就是把这个抽象变成了一个分布式的服务,摆脱了对单机容量瓶颈的限制,把日志变成了可无限扩展的服务。BookKeeper使用packet source协议和ZooKeeper的zap协议,通过log append only的方式实现了低延迟和高吞吐。在APCP里选择CP,而availability是通过多副本并发的方式提供高可用,BookKeeper有着低延迟、高吞吐、持久化、数据的强一致性、服务的高可用、单节点可以存储很多日志、IO隔离等优势,针对这些特性在后文会展开介绍。

3. BookKeeper的诞生

BookKeeper也是Apache的一个项目,同样是由雅虎捐献诞生,原本是为了应对雅虎开源HDFS里元数据存储的需求。

下图是字节跳动技术文章的一个图,主要是呈现在字节跳动如何用BookKeeper支撑元数据服务,支撑起EB级别的HDFS集群。这个集群DN有好几万台,需要很多NameNode,就需要一个可以保障active/standby/observer NameNode之间强一致性的日志服务,单机容量瓶颈下很难支撑这么大的体量时,引入了一个分布式的日志服务,这就是BookKeeper诞生的场景。随着HDFS大规模的问题开始出现之后,BookKeeper成为了HDFS在HA上的刚需需求,例如在EMC内部的HDFS集群,也是用的BookKeeper来做NameNode的editlog服务。BookKeeper是一种分布式场景下很常见的复制状态机的实现(通过复制log,保持各个节点状态机的同步,A节点持久化log后,把log同步到B节点,在B节点进行log的一个重放,让B节点达到跟A节点一样的一个状态)。由于在HDFS场景中,保存的只是NN的变更日志,所以算是元数据的元数据,对数据一致性、对吞吐、对时延的要求自然极高。

4. BookKeeper使用案例

BookKeeper也有局限性,是append only的一个抽象变成了分布式服务,相对而言比较底层。所以用户多是一些比较大的互联网公司或其他有大数据量的需求的用户,这些用户会在BookKeeper之上做一些二次开发,例如Pulsar在BookKeeper之上做了一层broker服务,对BookKeeper的每个分片做一些管理然后将其作为数据的存储服务。

类似的还有Twitter和Salesforce。Twitter的技术栈是构建在实时计算上的,在Twitter内部,BookKeeper是作为很重要的基础设施,不但有类似Pulsar的服务eventbus,还有其他使用场景例如搜索、广告、Stream computing, 以及作为类似KV存储的Manhattan Database的元数据服务,这些场景都用到了BookKeeper。在规模上,Twitter BookKeeper有两个集群,每个集群约有1500个节点,每天有17PB的数据,每秒1.5万亿的records。而在Salesforce的使用背景,是Salesforce想去掉对Oracle的依赖,所以自研了类似Amazon Aurora的NewSQL Database,其内部很多跟元数据相关或有一致性要求的服务都是通过BookKeeper来满足的,并且也有部分后端场景将BookKeeper作为存储服务去用。

--

02 BookKeeper的特性

1. BookKeeper基本概念

  • Ledger

可以理解为BookKeeper是会计,Ledger是账本,每个账本是记录信息的一个单元,写完之后转为closed状态(只读),最新的账本是打开状态(openLedger),以append only的方式持续存储数据。

  • Fragment

可以理解为BookKeeper内部维护的一个以append only的方式添加的数据组。

Fragment之下就是用户以append only的方式追加的一条条数据。

2. BookKeeper的节点对等架构

openLedger时有3个参数:Ensemble选择几个节点存储这个账本,Write Quorum控制数据写几个副本(并发写,不同于Kafka或HDFS,BookKeeper没有数据节点之间主从同步的关系,把数据同步的协调者从服务端移到了客户端),Ack Quorum控制等几个副本返回ack。

以下图为例openLedger(5,3,2),在保存这个账本时,选择了5个节点,但是只写3个副本,等2个副本来返回ack。第一个参数一般可用于调整并发度,因为写3个副本是通过轮转的方式写入,例如第1个record是写1-3节点,第2个record写2-4节点,第3个record写3-5节点,第4个record写4-5和1节点这样轮转。这种方式即便3个副本,也可以把5个节点都用起来。

这几个参数便捷的特性可让用户通过机架感知、机房感知、resource感知等各种方式进行灵活设置。当选好节点后节点之间的排序就已完成,每个record会带个index,index和节点已有绑定关系,例如index为1的,都放在123上,为2的都放在234上。通过这种方式可以让我们知道每个节点存了哪些消息,当某个节点宕机,根据这个节点的位置信息,把对应record还在哪些节点上有副本的信息找出来进行多对多的恢复。这么做的另一个好处是不用再维护元数据信息,只需要有每个节点记录index信息,在openLedger时记录好每个节点的顺序即可。

openLedger(5,3,2)数据存储结构就是下图中右边的结构,如果选择Ensemble=3,Write Quorum=3,数据存储结构就是下图左边的结构:

综上,用户可以通过Ensemble来调整读写带宽,通过Write Quorum调整强一致性的控制,通过Ack Quorum权衡在有较多副本时也可以有较低的长尾时延(但一致性就可能有一定的损失)。

3. BookKeeper可用性

  • 读的高可用

读的访问是对等的,任意一个节点返回就算读成功。这个特性可以把延迟固定在一个阈值内,当遇到网络抖动或坏节点,通过延迟的参数避障。例如读的延迟时间2ms,读节点3超过2ms,就会并发地读节点4,任意一个节点返回就算读成功,如下图Reader部分。

  • 写的高可用

在openLedger时会记录每个节点的顺序,假如写到5节点宕机,会做一次元数据的变更,从这个时间开始,先进行数据恢复,同时新的index中会把5节点变为6节点,如下图x节点替换5节点:

4. BookKeeper一致性

BookKeeper底层节点对等设计让写入数据的Writer成为了协调者,Writer来保存数据是否存储成功的状态,例如节点是否出现问题、副本够不够、切换Fragment时要不要做数据恢复、在写入过程中出现宕机时,通过fencing的方式防止脑裂等。所以,Writer维护了2个index:LastAddPushed和LastAddConfirmed。

LastAddPushed会随消息ID递增,LastAddConfirmed则记录最后一个连续的消息成功写入了(例如Ack Quorum为2,有2个成功返回了即为成功),但因为返回顺序不一定与消息顺序一致,例如123个消息,3的消息先返回了,2的还未返回,按连续的规则就是2不是3。

5. BookKeeper与Raft的对比

在底层原理上,Raft与BookKeeper有很多类似的地方,Raft每个数据写入的组织形式是term,跟BookKeeper的segment类似,每个term也会选择一组节点存储数据,然后不断往后追加数据,通过数据节点之间的协同保证数据一致性。

在数据结构上,Raft在保存数据时有Entry,Entry除了带本身的index还会带上last committed index,与BookKeeper中的LastAddConfirmed较为类似,只是BK是通过Writer来协调数据在不同节点的一致性,Raft有leader来协调数据在不同节点的一致性。


6. BookKeeper的IO读写分离

下图是每个数据节点的数据流转过程,数据写入时,Writer通过append only方式写入到Journal,Journal在把数据写到内存的同时会按一定频率(默认1ms或500 byte)把数据持久化到Journal Device里,写完后会告诉Writer这个节点写入成功了(持久化到磁盘是默认配置)。

Journal在数据写入时有写到内存中,接下来在内存中做排序(用于解决如果按写的顺序读会导致分区随机性强的问题),然后把数据刷到数据盘中。读的时候,如果读最新的数据,可以直接从内存里返回,如果读历史数据,也只去读数据盘,不会对Journal Device写入有影响。这样针对有读瓶颈或写瓶颈的用户,可以把Journal Disk或Ledger Disk换成SSD盘,提升性能,并且防止读写的互相干扰。

--

03 BookKeeper的存储介质演变

1. BookKeeper的Disk演进

在演进过程中,因为顺序读的情况比较多,所以读的部分变化不大,但在写的这部分经历了HDD到SSD再到NVMe SSD,再到现在部分用户换成了Intel的PMem的过程(如下图所示)。在换到NVMe SSD时,部分用户通过多目录的方式可以把SSD的IO带宽打的很满。

2. PMEM在BookKeeper上的应用

PMem的特性非常匹配Journal Disk,单块PMem可以达到3-4GB的带宽,不但能提供高带宽吞吐而且可持久化。PMem容量相比SSD比较小、相比内存又比较大,在刚推出时单条128GB,有着GB级别的吞吐和ns级别的延迟。

高吞吐低容量的PMem非常适合Journal持久话刷盘的需求,例如宕机后,需要对没刷到磁盘的这部分数据做恢复,需要Journal做replay log重放,由于只是增量日志而非全量数据,所以并不需要很大的容量,正好和PMem容量不大相匹配。而且,PMem的寿命会比SSD的寿命长一些,例如在每天同样写入量下SSD可能只能用1年而PMem预计可以使用4-5年。

雅虎(现在是Verizon Media)有实际通过PMEem优化BookKeeper的案例,在只增加5%的单机成本情况下提升了5倍的带宽吞吐和低于5ms的时延保障(BookKeeper社区与Intel正在合作做性能测试,预计未来会产出白皮书说明)。

在雅虎案例中用10台Pulsar(底层是用PMem做Journal Disk的BookKeeper)替换了33台Kafka,比原Kafka方案成本降低了一半,产出的对比结果如下:

--

04 社区资源

  • 团队构成

由Apache Pulsar核心研发团队创立,同时有Apache Pulsar和Apache BookKeeper项目管理委员会(PMC)主席,有6名Apache Pulsar PMC成员和3名Apache BookKeeper PMC成员,有约20名 Apache Committer。

  • 里程碑

公司成立于2019年,2020年发布商业化产品StreamNative Cloud,目前有50+付费客户,覆盖金融、IoT、互联网、制造多个行业。

  • 优势

是社区和代码的构建维护者,有全球最专业的Pulsar设计开发、运维、管理团队的7*24小时服务,提供开箱即用的云服务和咨询培训服务。



今天的分享就到这里,谢谢大家。


分享嘉宾:


本文首发于微信公众号“DataFunTalk”。

有关翟佳:高可用、强一致、低延迟——BookKeeper的存储实现的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby-on-rails - 如何使辅助方法在 Rails 集成测试中可用? - 2

    我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel

  4. ruby - Rack:如何将 URL 存储为变量? - 2

    我正在编写一个简单的静态Rack应用程序。查看下面的config.ru代码:useRack::Static,:urls=>["/elements","/img","/pages","/users","/css","/js"],:root=>"archive"map'/'dorunProc.new{|env|[200,{'Content-Type'=>'text/html','Cache-Control'=>'public,max-age=6400'},File.open('archive/splash.html',File::RDONLY)]}endmap'/pages/search.

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. ruby - Ruby gsub 替换中的行为不一致? - 2

    两个gsub产生不同的结果。谁能解释一下为什么?代码也可在https://gist.github.com/franklsf95/6c0f8938f28706b5644d获得.ver=9999str="\tCFBundleDevelopmentRegion\n\ten\n\tCFBundleVersion\n\t0.1.190\n\tAppID\n\t000000000000000"putsstr.gsub/(CFBundleVersion\n\t.*\.).*()/,"#{$1}#{ver}#{$2}"puts'--------'putsstr.gsub/(CFBundleVersio

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐