BookKeeper 工作原理

BookKeeper 工作原理

参考文档:

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-1-high-level-6dce62269125

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-2-writes-359ffc17c497

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-3-reads-31637b118bf

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-4-back-pressure-7847bd6d1257

https://mp.weixin.qq.com/s/L0IPBZDEI31mOrvLIwaqAA

BookKeeper是一个高性能的追加写(append-only)存储服务,主要用在Pulsar中,单个BookKeeper节点也叫Bookie。本文试图来解释一些BookKeeper中的架构。

BookKeeper总体架构

BookKeeper是一个单进程服务,多个进程提供对等的服务内容。其总体架构如下,

20220114-BookKeeper 工作原理-2022-01-17-10-20-35.png

最上层是Netty,用来处理网络请求IO。BookKeeper内部主要有2块Entry处理的服务,Journal和DbLedgerStorage。Journal是BookKeeper的WAL,DbLedgerStorage是处理Write Cache,并从后台将Entry数据刷入到Entry Log文件中。

BookKeeper的线程模型

BookKeeper的线程模型如下图,

20220114-BookKeeper 工作原理-2022-01-17-10-20-53.png

不同颜色的SyncThread、DbStorageThread是处理后台任务线程,其他是是处理实时任务的线程。介绍其中几个线程,

  1. Netty Threadpool,Netty线程,主要处理网络IO,消息解析;
  2. Long Pool Threadpool,长轮询线程,当写入线程发生相关写入的时候,触发该线程(没有理解这个线程的具体作用);
  3. Write Threadpool,写线程,处理写入请求的任务;
  4. Read Threadpool,读线程,处理读取请求的任务;
  5. High Priority Threadpool,高优先级线程,对请求添加高优先级标签,主要是Pulsar的Fencing和恢复场景会用到,正常情况下不会用;

值得注意的是,write, read, long poll 和 high priority这4类线程都是OrderedExecutor类的实例,这些线程组成一个大的线程组来提供服务,并与Ledger id绑定。Netty根据Ledger id来分发请求到响应的线程组进行处理。部分线程的默认线程数量,

  • serverNumIOThreads (Netty threads, defaults to 2xCPU threads)
  • numAddWorkerThreads (defaults to 1)
  • numReadWorkerThreads (defaults to 8)
  • numLongPollWorkerThreads (defaults to 0 表示Long Poll读入消息后就提交给读线程)
  • numHighPriorityWorkerThreads (defaults to 8)
  • numJournalCallbackThreads (defaults to 1)

在处理Journal和DbLedger的时候,线程管理的整体结构,如下图,

20220114-BookKeeper 工作原理-2022-01-17-10-21-44.png
20220114-BookKeeper 工作原理-2022-01-17-10-21-57.png

BookKeeper的存储是分开管理的,Journal和DbLedger存储在不同的地方,同时,可以配置多个Journal和多个DbLedger(参数journalDirectories、ledgerDirectories),最大可能利用存储。对每个Journal,专有的Journal线程组实例处理;对每个DbLedger,也有专有的SingleDirectoryDbLedgerStorage线程组实例来处理,包括Write Cache、Read Cache, Ledger Entry Log File、RockDB index file等。

例如,对于一个读请求,Netty根据Ledger id转发到对应的DbLedger实例进行处理,如下,

20220114-BookKeeper 工作原理-2022-01-17-10-22-15.png

对于一个写请求,Netty根据Ledger id转发到对应的Journal和DbLedger实例进行处理,如下,

20220114-BookKeeper 工作原理-2022-01-17-10-22-29.png

BookKeeper写入请求分析

BookKeeper处理写入Entry请求的整体流程如下,

20220114-BookKeeper 工作原理-2022-01-17-10-22-46.png

写入线程

首先,Netty线程收到写入请求,解析,并把请求转给写入线程。写入线程通常只有1个,因为要完成的事情很少。写入线程先把Entry写入Write Cache,然后再发起一个写入Journal的请求进入Entry Log内存队列,就完成了所有工作。整个过程都是内存操作,消耗很少,因此不需要很多线程。

Journal线程

Journal线程在Entry Log内存队列的另一端等待,当有写入线程写入消息时,就把消息中的Entry写入到磁盘。但Journal的写入并不是同步写(fsync),因此只保证写入到了系统缓存中,Journal线程本身并不发起同步写的系统请求。同时,Journal线程周期性的发起强制写入请求,并将请求写入Force Write内存队列。强制写入请求的发起时机有以下几处,

  • 达到预设的最大等待时间(配置journalMaxGroupWaitMSec,默认2 ms)
  • 达到累积写入字节大小上限(配置journalBufferedWritesThreshold,默认512Kb)
  • 达到累积写入Entry条数上限(配置journalBufferedEntriesThreshold,默认0,不启用)
  • 当Entry Log内存队列消费完,从不空到空(配置journalFlushWhenQueueEmpty,默认false)

也就是说,默认启用前2个选项。

注意:Journal线程只有1个,不会出现多个线程同时写一个Entry Log文件的情况。

强制写线程

Force Write线程等待Force Write内存队列的消息,收到请求后,就发起fsync系统调用,强制将数据写入磁盘Journal文件。当数据持久化完成之后,调用Journal Callback线程。

注意:处理强制写请求的时候,可能有新的Entry写入,实际刷盘的时候,刷入的Entry可能比发起请求时的Entry要多。

Journal Callback线程

该线程是回调线程,发送响应给客户端。写入Journal的步骤到此结束。

DbStorage线程

DbStorage线程主要处理,当Write Cache写满之后,将数据写入到Ledger中,一个DbLedgerStorage对应一个DbStorage线程。DbStorage线程是一个很复杂的线程,不仅要负责管理Write Cache,还需要负责Entry存储写入。

Write Cache在内存中有两份,但同时只有一个Write Cache是活跃的(Active Write Cache),用于实时任务的写入;另一个Write Cache是写入存储时候用的(Flushed Write Cache)。当写入存储完成的时候,就把Flushed Write Cache清空。当写入线程发现Active Write Cache已满的时候,就触发DbStorage线程进行写入存储。如果当时Flushed Write Cache是已经清空的,说明之前的写入任务已经完成,DbStorage线程交换两个Write Cache,将空的Write Cache变为Active Write Cache,供写入线程使用;然后,开始执行写入任务,将Flushed Write Cache写入存储。

这是理想情况,如果Active Write Cache已满的时候,Flushed Write Cache尚未清空,说明之前的写入任务还没有完成。此时,不能交换2个Write Cache,写入线程会阻挡写入请求一小段时间,等待写入任务完成。这个时间参数是dbStorage_maxThrottleTimeMs,默认10秒。直到Flushed Write Cache全部写入完成,交换2个Write Cache,写入线程就被释放。

默认情况下,Write Cache的大小设置为可用直接内存的25%(应该就是机器内存的25%),也可以通过参数dbStorage_writeCacheMaxSizeMb来设置。因为Write Cache有2份,所以实际的Write Cache大小会翻倍。假定Write Cache设置为250MB,有2个Ledger目录,每个Ledger有2个Write Cache,总共就有4份Write Cache,共计消耗内存1GB。

当DbLedgerStorage存储写入时,会先按Ledger id和Entry id对所有Entry进行排序,然后将Entry写入Entry Log文件,并将文件的偏移量写入Entry索引,即RocksDB。

DbLedgerStorage存储写入,不仅可以由DbStorage线程完成,也可以由Sync线程在产生检查点时完成。

注意,Entry Log文件中,每次写入的Entry是来自于多个Ledger的,同一个存储中有多个Ledger的数据混杂在一起。经过排序,同一个Ledger的Entry会聚合在一起。在读取的时候,当前Entry的前后是同一个Ledger的概率高。如图,

20220114-BookKeeper 工作原理-2022-01-17-10-24-29.png

Sync线程

Sync线程是一个守护线程,不在Journal线程池和DbLedgerStorage线程池中,主要负责定期生成检查点(checkpoint)。检查点要完成以下任务,

  1. 将Ledger数据刷入存储;
  2. 标记刷入磁盘的Journal位置(日志标记),并持久化,表示这个位置之前的Entry都已经安全的写入存储了。这个过程是通过写磁盘上一个单独的日志标记文件来完成的;
  3. 清理不在需要的旧Journal文件;

写入瓶颈

通常情况下,BookKeeper的瓶颈都是磁盘IO造成的,但Journal IO瓶颈和DbLedgerStorage IO瓶颈的现象是不一样的。

如果是Journal的瓶颈,会发现写入线程很平稳的拒绝某些请求,同时,如果有火焰图工具的话,可以看到写入线程非常繁忙。如果是DbLedgerStorage的瓶颈,会发现写入线程拒绝所有写入请求,等待10秒(默认)后就开始正常接收请求,然后发生拥堵,又开始拒绝请求。

当然,也可能是CPU瓶颈,不太常见,一般情况都是IO是瓶颈。即使发生了,也不难发现,CPU占用率很高,直接增加CPU资源处理Netty请求即可。

BookKeeper读取请求分析

BookKeeper的读取请求,主要是由DbLedgerStorage的getEntry(long ledgerId, long entryId)方法完成。架构图如下,

20220114-BookKeeper 工作原理-2022-01-17-10-25-05.png

读取的流程如下,

  1. 检查Write Cache中是否有数据,有则返回;
  2. 检查Read Cache中是否有数据,有则返回,无则说明数据在磁盘上;
  3. 从Entry索引(RocksDB)中获取到Entry的位置(哪个文件的哪个偏移量);
  4. 根据文件和偏移量,定位特定的Entry;
  5. 执行预读取;
  6. 将预读取的Entry加载到Read Cache;
  7. 返回当前Entry;

预读取的假设是,读取了当前Entry,也很可能会读取之后的Entry,因此预先加载这些Entry到内存,也因为写入的时候,相同Ledger的数据被排序放在了一起,因此,预读取是磁盘的顺序读,性能较好。预读取的边界是,

  1. 达到单次预读取数量上限,默认1000,Pulsar场景;
  2. 读到当前文件结束;
  3. 读到另一个Ledger的Entry;

Read Cache每个DbLedgerStorage上有1个,默认是可用直接内存的25%。

关于读取的一些其他问题

Broker粘滞读取,也就是说,Broker如果在某个Bookie上读取到了数据,那么下次统一客户端的读取请求还是发送到同一个Bookie。同样是基于邻近读取的假设。如果没有粘滞的话,可能每个Bookie上都需要预读取加载相同的数据。这也说明,Broker在读取Bookie数据的时候并不是对等对待每个Bookie的。Broker粘滞读取需要由 Broker 来完成实现。

Read Cache有多个分段(Segment),每个Segment的数据结构是一个环形队列(Ring Buffer)。内存预先分配,新的Entry覆盖旧的Entry,每个Cache有索引指向对应位置,以方便查找。

Read Cache缓存抖动,主要出现在Read Cache大小不足的时候。假设Read Cache可以容纳2000个Entry,读请求先读取Ledger A,预读取1000个Entry在Cache中;又读取Ledger B,预读取1000个Entry在Cache中,此时Read Cache已满;再读取Ledger C,预读取1000个Entry在Cache中,覆盖掉Ledger A的1000个;此时A又来读取下一个Entry,缓存无法命中,继续预读取;如果之后,A、B、C依次读取,那么一次Cache也无法命中,性能急剧下降。造成这样现象的原因是Read Cache大小不足,所有的缓存类应用都有类似现象。解决方案有,增大Read Cache,或者降低预读取的上限(如改为预读取500条即可)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容