深入解析 Apache BookKeeper 系列:第二篇 — 写操作原理

上一篇文章中,我们从组件、线程、读写流程三个方面讲解了 bookie 服务端原理。在这篇文章中,我们将详细介绍写操作是如何通过各组件和线程模型的配合高效写入和快速落盘的。我们尽量还是在架构层面剖析。

本系列文章基于 Apache Pulsar 中配置的 BookKeeper 4.14 版本。

写操作中有很多线程调用 Journal 和 LedgerStorage 的 API。在上一篇文章中,我们已经知道写操作中 Journal 为同步操作,DbLedgerStorage 为异步操作。

图片

图一:各线程是如何处理写操作的

我们知道可以配置多个 Journal 实例和 DbLedgerStorage 实例,每个实例都有自己的线程、队列和缓存。因此当讲到某些线程、缓存和队列的时候,它们可能是并行存在的。

Netty 线程

Netty 线程处理所有的 TCP 连接和这些连接中的所有请求。并将这些写请求转发到写线程池,其中包括要写入的 entry 请求、处理请求结束时的回调、发送响应到客户端。

写线程池

写线程池要做的事情不多,因此不需要很多的线程(默认值是 1)。每个写请求添加 Entry 到 DbLedgerStorage 的 Write Cache,如果成功,则将写请求添加到 Journal 的内存队列(BlockingQueue)中。此时写线程的工作就完成了,剩下的工作就交给其他线程处理。

每个 DbLedgerStorage 实例有两个写缓存,一个是活跃的,一个是空闲的,空闲的这个缓存可以在后台将数据刷到磁盘。当 DbLedgerStorage 需要将数据刷到磁盘时(活跃写缓存写满后),两个写缓存就会发生交换。当空闲状态的写缓存将数据刷到磁盘的同时,可以使用一个空的写缓存继续提供写服务。只要在活跃写缓存被写满之前,将空闲写缓存中的数据刷到磁盘,就不会出现什么问题。

DbLedgerStorage 的刷盘操作可以通过同步线程(Sync Thread)定时执行检查点(checkpoint)机制或通过 DbStorage 线程(DbStorage Thread,每个 DbLedgerStorage 实例对应一个 DbStorage 线程)触发。

如果写线程尝试向写缓存中添加 Entry 时,写缓存已经满了,则写线程将刷盘操作提交到 DbStorage 线程;如果换出的写缓存已经完成了刷盘操作,那么两个写缓存将立即执行交换操作(swap),然后写线程将这个 Entry 添加到新交换出来的写缓存中,这部分的写操作也就完成了。

然而,如果活跃状态的写缓存被写满了,同时交换出的写缓存仍然在刷盘,那么写线程将等待一段时间,最终拒绝写请求。等待写缓存的时间由配置文件中的参数 dbStorage_maxThrottleTimeMs 控制,默认值为 10000(10 秒)。

默认情况下,写线程池中只有一个线程,如果刷盘操作过长的话这将导致写线程阻塞 10 秒钟,这将导致写线程池的任务队列被写请求迅速填满,从而拒绝额外的写请求。这就是 DbLedgerStorage 的背压机制。一旦刷新的写缓存再次能写入之后,写线程池的阻塞状态才会被解除。

写缓存的大小默认为可用直接内存(direct memory)的 25%,可以通过配置文件中的 dbStorage_writeCacheMaxSizeMb 来进行设置。总的可用内存是分配给每个 DbLedgerStorage 实例中的两个写缓存,每个 ledger 目录对应一个 DbLedgerStorage 实例。如果有 2 个 ledger 目录和 1GB 的可用写缓存内存的话,每个 DbLedgerStorage 实例将分配 500MB,其中每个写缓存将分配到 250MB。

DbStorage 线程

每个 DbLedgerStorage 实例都有自己的 DbStorage 线程。当写缓存写满后,该线程负责将数据刷到磁盘。

Sync 线程

这个线程是在 Journal 模块和 DbLedgerStorage 模块之外的。它的工作主要是定期执行检查点,检查点

有如下几个:

  • ledger 的刷盘操作(长期存储)
  • 标记 Journal 中已经安全的将数据刷到 ledger 盘的位置,通过写入磁盘的 log mark 文件实现。
  • 清理不再需要的、旧的 Journal 文件

这种同步操作可以防止两个不同的线程同时刷盘。

当 DbLedgerStorage 刷盘时,交换出的写缓存会被写入到当前 entry 日志文件中(这里也会有日志切分操作),首先这些 entry 会通过 ledgerId 和 entryId 进行排序,然后将 entry 写入到 entry 日志文件,并将它们的位置写入到 Entry Locations Index。这种写 entry 时的排序是为了优化读操作的性能,我们将在本系列下一篇文章中介绍。

一旦将所有写请求的数据刷到磁盘,则交换出的写缓存就会被清空,以便再次与活跃的写缓存进行交换。

Journal 线程

Journal 线程是一个循环,它从内存队列(BlockingQueue)中获取 entry,并将 entry 写到磁盘,并且周期性的向强制写队列(Force Write queue)添加强制写请求,这会触发 fsync 操作。

Journal 不会为队列中获取的每个 entry 执行 write 系统调用,它会对 entry 进行累计,然后批量的写入磁盘(这就是 BookKeeper 的刷盘方式),这也称为组提交(group commit)。以下几个条件会触发刷盘操作:

  • 达到最大等待时间(通过 journalMaxGroupWaitMSec 配置,默认值为 2ms)
  • 达到最大累计字节数(通过 journalBufferedWritesThreshold 配置,默认值为 512Kb)
  • entry 累计的数量达到最大值(通过 journalBufferedEntriesThreshold 配置,默认值为 0,0 表示不使用该配置)
  • 当队列中最后一个 entry 被取出时,也就是队列由非空变为空(通过 journalFlushWhenQueueEmpty 配置,默认值为 false

每次刷盘都会创建一个强制写请求(Force Write Request),其中包含要刷盘的 entry。

强制写线程

强制写线程是一个循环,循环从强制写队列中获取强制写请求,并对 journal 文件执行 fsync 操作。强制写请求包括要写入的 entry 和这些 entry 请求的回调,以便在持久化到磁盘后,这些 entry 写入请求的回调能被提交到回调线程执行。

Journal 回调线程

这个线程执行写请求的回调,并将响应发送到客户端。

常见问题梳理

  • 写操作的瓶颈通常在 Journal 或 DbLedgerStorage 中的磁盘 IO 上。如果写 Journal 或同步操作(Fsync)太慢的话,那么 Journal 线程和强制写线程(就不能快速地从各自的队列中获取 entry。同样,DbLedgerStorage 刷磁盘太慢,那么 Write Cache 就无法清空,也无法快速的进行互换。
  • 如果 Journal 遇到瓶颈,将导致写线程池的任务队列的任务数量达到容量上限,entry 将阻塞在 Journal 队列中,写线程也将被阻塞。一旦线程池任务队列满了,写操作就会在 Netty 层被拒绝,因为 Netty 线程将无法向写线程池提交更多的写请求。如果你使用了火焰图,你会发现写线程池中的写线程都很繁忙。如果瓶颈在于 DbLedgerStorage,那么 DbLedgerStorage 自身就可以拒绝写操作,在 10 秒(默认情况下)之后,写线程池的资源很快就会被占满,然后导致 Netty 线程拒绝写请求。
  • 如果磁盘 IO 不是瓶颈,而是 CPU 利用率非常高的话,很有可能是因为使用了高性能磁盘,但是 CPU 性能比较低,导致 Netty 线程和其他各种线程处理效率降低。这种情况通过系统的监控指标就能很容易地定位。

总结

本文在 Journal 和 DBLedgerStorage 层面讲解了写操作流程,以及涉及到写操作的线程是如何工作的。在下一篇文章中,我们将介绍读操作。

相关阅读

博文推荐|深入解析 Apache BookKeeper 系列:第一篇 — 架构原理

本文翻译自《Apache BookKeeper Internals — Part 2 — Writes》,作者 Jack Vanlightly。

译者简介

邱峰 @360 技术中台基础架构部中间件产品线成员,主要负责 Pulsar、Kafka 及周边配套服务的开发与维护工作。

图片

转发本文章到朋友圈集赞 30 个,扫码添加 Pulsar Bot 👇🏻👇🏻👇🏻微信凭借朋友圈截图领取👆🏻👆🏻👆🏻技术书籍《深入解析 Apache Pulsar》一本。限量 5 本~先到先得,送完即止!

云原生时代消息队列和流融合系统,提供统一的消费模型,支持消息队列和流两种场景,既能为队列场景提供企业级读写服务质量和强一致性保障,又能为流场景提供高吞吐、低延迟;采用存储计算分离架构,支持大集群、多租户、百万级 Topic、跨地域数据复制、持久化存储、分层存储、高可扩展性等企业级和金融级功能。

GitHub 地址:http://github.com/apache/pulsar/

场景关键词

异步解耦 削峰填谷 跨城同步 消息总线

流存储 批流融合 实时数仓 金融风控

点击阅读英语原文 (使用 VPN 打开)


ApachePulsar
Apache Pulsar 是 Apache 软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息、存储、轻量化...

Apache软件基金会顶级项目,下一代云原生分布式消息系统

192 声望
929 粉丝
0 条评论
推荐阅读
三年前端的2022,如果创造比卷更有趣,那为何创造不能成为主旋律?
up主2019年毕业,找了一份前端工作,一直干到现在。2019年末出现疫情,三年的时间,也差不多与我的职业生涯完全重合了。刚过去的2022年,我也没有避开阳的命运(这病毒是真的强),就12月这一个月时间,几乎全公...

Gomi10阅读 2.1k

封面图
祝贺姜宁连任 2023 Apache 软件基金会董事
在刚刚结束的 ASF Annual Meeting 上,2023 年新任董事会成员诞生了。Apache 软件基金会通过官方 blog 向大家公布了新一任董事的选举成果。

鸣飞4阅读 7.3k评论 1

姜宁 ASF 2022 董事竞选宣言:我希望能够帮助 ASF 打破地域、文化、语言的障碍
在刚刚结束的 ASF Annual Meeting 上,2022 年新任 ASF Member 及董事会成员诞生了。Apache 软件基金会通过官方 blog 向大家公布了新一任董事的选举成果。Apache 软件基金会孵化器导师,ALC Beijing 发起人,华为...

鸣飞4阅读 12.3k

封面图
张晋涛:我的 2022 总结
大家好,我是张晋涛。2022 年已经结束,我每年都会惯例的做个小回顾,今年因为阳了在恢复身体,一直拖到了今天才写。生活在 2022 年初做回顾的时候,觉得 2021 是魔幻的一年,但现在看来 2022 年其实更加魔幻。一...

张晋涛6阅读 1k评论 2

封面图
阿里云被曝 UI 抄袭,复刻 SkyWalking Trace Profiling 页面
2023 年 1 月 3 日,SkyWalking 官网发布消息,称阿里云抄袭了 SkyWalking Trace Profiling 整体页面 UI,包括页面布局、文字和分析任务设置,唯一的区别仅有颜色方案。

鸣飞5阅读 5.3k

权威发布丨2022 中国开源先锋 33 人之心尖上的开源人物
开源社主办的 COSCon'22 中国开源年会上,我们也以「开源站在十字路口」为主论坛主题展开了讨论。面对机遇与挑战并存,分裂与合作共生,风云变幻的世界,我们该何去何从?且看开源先锋们「榜样的力量」!

SegmentFault思否2阅读 2.1k

封面图
15分钟入门23种设计模式:图解,范例和对比
本文力图在15分钟内,通过UML图解、范例和类比,让你对面向对象的23种设计模式形成提纲挈领的认识,从而让我们在面临代码设计问题时更加成竹在胸。本文源代码: UML, Sample Code。

风云信步5阅读 645评论 1

Apache软件基金会顶级项目,下一代云原生分布式消息系统

192 声望
929 粉丝
宣传栏