在上一篇文章中,咱们从组件、线程、读写流程三个方面解说了 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 关上)