上周末的 TGIP-CN,由我们 StreamNative 高级工程师、温柔暖男李鹏辉带来了关于 Pulsar 性能调优的分享。

首先回顾了最近一周 Pulsar 进展的分享:

2.5.1 新版本发布投票中

 

投票邮件 thread:

https://lists.apache.org/thread.html/r8cc87cb8b6299433d84aa8f89e4817a33f5f4cc0ecde73de009d6d67%40%3Cdev.pulsar.apache.org%3E

 

验证流程:

https://github.com/apache/pulsar/wiki/Release-Candidate-Validation

 

新增 PIP - 61:Advertised multiple addresses

详情参考:
https://github.com/apache/pulsar/wiki/PIP-61%3A-Advertised-multiple-addresses

新增 PIP - 62:Move connectors, adapters and Pulsar Presto to separate repositories

详情参考:
https://github.com/apache/pulsar/wiki/PIP-62%3A-Move-connectors%2C-adapters-and-Pulsar-Presto-to-separate-repositories

此次分享是 Pulsar 性能调优系列的第一部分,大家如果想参考内容的话,可以去 StreamNative 官网下载白皮书报告。

链接参考:
https://streamnative.io/whitepaper/taking-a-deep-dive-into-apache-pulsar-architecture-for-performance-tuning/

接下来将从 Pulsar 一些基本概念入手,带入消息写入-读取的过程,告诉你哪里可以进行性能调优,哪些地方又会出现性能瓶颈等。

因为 Pulsar 里有非常多的组件,如 client、broker 等,这些在之前的 TGIP-CN 里都有介绍。所以今天的性能调优趴,也主要针对 client、broker 和 bookie 这些层面进行分享。


基础概念

Pulsar 最基础的骨架就是由 producer-topic-consumer 这三者进行扩展的。Producer 将消息发送到 topic,consumer 再从 topic 里接收消息。

Pulsar 读写过程的性能调优_分布式

同时 topic 又分为持久化和非持久化,以及需要创建多少个分区等等,这些都是最基础的。在这个层面进行性能调优的话,无非就是考虑 producer 以更大的吞吐量写入到 topic 里,对于 consumer 而言则是以更低的延迟来接收到相关的消息。

Pulsar 读写过程的性能调优_分布式_02

在 broker 层面里,存在一个 managed ledger 库,也就相当于 topic。也就是说每一个 topic 后边都有一个 managed ledger 进行服务存储等。

它会把 topic 里用到的所有 ledger 进行管理,并记录到元数据里。同时在消费订阅层面,也会将其订阅进度、消息签收进度等进行记录。

Pulsar 读写过程的性能调优_大数据_03

在 managed ledger 里,还有一个概念:managed ledger cache,它主要是用来存储 topic 里尾部消息的缓存。即把 broker 层面里新写入的数据提前进行了缓存,这样做有利于多订阅模式下,节省了从读取消息的时间。

Pulsar 读写过程的性能调优_队列_04

我们在之前的博客里也有提到完整的 topic 金字塔模型逻辑,上图也在用模块化的模式进行了表述。

每个 ledger 下会有多个 fragments,当某条消息挂掉时,其实是在 fragment 层面进行调整。而 entry 则是记录了一些写入过程时的映射关系。

Pulsar 读写过程的性能调优_kafka_05

当有 entry 写入时,先把 entry 写入 journal log 里,这些概念在斯杰分享 BookKeeper 内容时也有提到过。感兴趣的可以参考:BookKeeper 的一些概念

在写入 journal log 时,同时也会写入到 memory table 里,这时客户端认定为写入成功。这时会提出一个 check point,当满足 checkpoint 后,会把一段时间内的 journal log 数据放置到 entry log 里。而 index DB 则主要是记录 entry 放置在 entry log 的位置信息。

Pulsar 读写过程的性能调优_java_06

而在 message 层面,则分为 single message 和 batched message。区别在于 client 在发送消息时,是单条进行还是打包批量进行的。开启 batched 后,调动的 broker 数量减少,调用 bookie 次数也减少了,从而可以减少客户端和服务端的cpu使用,提升消息发送和读取的吞吐量。

更多基础概念细节,可参考文末视频回放 12:20-30:00 时间段。


消息写入

消息写入是由 producer 发起并写入到 topic 里,topic 可能是单个 topic 或 partitioned topic。

Pulsar 读写过程的性能调优_java_07

Partitioned topic 可以让 producer 将消息写到不同的 partition 里去,这样 producer 在分发时,就需要选择投放到哪个 partition。具体概念和介绍,我们之前在《Message Lifecycle》篇文章也提到过,大家可以点击查看。

Pulsar 读写过程的性能调优_kafka_08

在 producer 层面,有个 pending queue 组件。消息会先放置到内存的队列里,它会持续不断的往 broker 去推送,broker 给到回应后继续给到客户端,把对应 queue 里的数据给清理掉。如果 queue 满了,Pulsar 的默认行为是 producer 会被阻塞发送,用户也可以修改为抛出异常的行为。

如果队列过大,当前一批 response 返回时,就会清理掉已经消耗的消息队列,实现不冲突式的叠加,减少被 block 的时间。当然 queue 本身是以内存为代价的,queue 越大需要的内存就越多。在实际应用中,就需要多方位考虑了。

Pulsar 读写过程的性能调优_大数据_09

如果结合上边提到的 batched message,则呈现效果就如上图所示。这时候就需要考虑一个 batched message 的大小,它越大所需内存也是直线上升的。

前边我们提到在 producer 端对吞吐量进行性能优化,在客户端层面还有一个优化考虑:压缩。

Pulsar 现在支持四种压缩的方式:LZ4、ZLIB、ZSTD、SNAPPY。但不同的压缩方式对版本的支持不太一样。

Pulsar 读写过程的性能调优_大数据_10

Batched message 是由 client 本地进行压缩消息的, 可以减轻 broker/bookie 端的一些网络带宽、磁盘使用等。

Pulsar 读写过程的性能调优_分布式_11

所以在消息写入端,producer 将消息写入到 topic,然后 broker 并行去写 bookies。这时就需要在 broker 端考虑消息持久化配置的选项。

Pulsar 读写过程的性能调优_分布式_12

配置的是你要用的 bookie 资源,比如你需要用多少台 bookie 去服务 topic、数据副本有多少、有多少 bookie 写入成功等。

副本越多,broker 端并行去写的数量就多。相应地,网络带宽的出口就要更大,这时需要根据数据的容灾能力和使用情况进行权衡。

所以 3-3-2 模式是可以在写入时比 3-3-3 有更低的延迟,由于可以容忍掉在同一时刻写入最慢的 bookie,这样的话延迟方面就会有不错的表现。

Pulsar 读写过程的性能调优_java_13

在 bookie 端处理消息写入时,会需要一个叫“request processor”组件,它会处理所有写入的 entry,再逐一放置到 journal 和 memory table 里。当完成上述过程后,request 过程基本结束,最后 flush entry log 就是通过异步模式进行后续处理。

????写入过程性能调优部分参考

Broker 端

1. managedLedgerDefaultEnsembleSize

2. managedLedgerDefaultWriteQuorum

3. managedLedgerDefaultAckQuorum

4. managedLedgerNumWorkerThreads

5. numIOThreads

6. Dorg.apache.bookkeeper.conf.readsystemproperties=true -DnumIOThreads=8

Bookie 端

1. Journal Directories

2. Ledger Directories

3. Journal sync data

4. Journal group commit

5. Write cache

6. Flush interval

7. Add worker threads and max pending add requests

8. Journal pagecache flush interval

更多关于写入过程的细节,可参考文末视频回放 30:00-70:00 时间段。


读取过程

消息读取其实跟写入差不多,也有一个 queue 相关,叫 consumer receiver queue。跟前边的 pending queue 的实现原理类似,避免 consumer 在当前队列里没有可读取的消息。

Pulsar 读写过程的性能调优_java_14

当进行消费数据时进程过半,此时会跟 broker 继续索要数据。如果组件内有当前队列一半的数据,consumer 则可以继续处理,这时同步的 broker 里也会分发更多消息。这里也需要考虑其内存因素。

Pulsar 读写过程的性能调优_java_15

在消息读取时,过程刚好与写入相反,即从 bookie-broker-topic 的过程。在前边内容里我们提到了 managed ledgre cache,当 broker 里已经有缓存时,可以直接从 cache 读取,不需要再从 broker 进行读取了。

Pulsar 读写过程的性能调优_java_16

在读取消息时,broker 端会有一个叫 dispatcher 的组件。它主要是从 managed  ledger 读取消息并分发给 consumer,根据不同的订阅模式进行不同情况的分发。

Pulsar 读写过程的性能调优_分布式_17

比如在 Key_Shared 订阅模式里,dispatcher 需要拿到 message key、计算 key hash 值、根据哈希值进行分组化等。

一批读取的消息越多,group by 的效果就越好,要计算 consumer 的次数就越少。那么这里就需要考虑「性能调优」的部分,比如怎么提升 batch 效果等。

????读取过程性能调优参考

Bookie 端

1. dbStorage_rocksDB_blockCacheSize

2. dbStorage_readAheadCacheMaxSizeMb

3. dbStorage_readAheadCacheBatchSize

4. Read worker threads

Broker 端

1. Managed ledger cache

2. Dispatcher max read batch size

3. Bookkeeper sticky reads

更多关于读取过程的细节,可以参考文末视频 70:00-80:00 时间段。


Q&A 汇总

1. 分区主题和普通主题大概会在哪个版本合并统一起来,因为普通主题就是只有一个分区的分区主题。

没有合并的计划,目前可以在 broker 端的配置 allowAutoTopicCreationType=partitioned

将自动创建的 topic 修改为 partitioned topic 以及自动创建是使用的 partition 数量。

2. go 和 cpp 的客户端是不是也会从主仓库分开呢?

目前 native go 的 client 已经在单独的仓库。

https://github.com/apache/pulsar-client-go.

Cgo client 和 cpp client 目前不会和主仓库分开。可以订阅 Pulsar 邮件列表以获取 PIP-62 的进展。

3. 消息过滤有计划支持不?

目前支持在 reader 种使用 key hash range 的方式来读取消息。如果不能满足需求,欢迎在 https://github.com/apache/pulsar/issues 提交 feature request 或者 pull request。

4. 使用 Key_Shared 是否可以实现类型同步请求的模式,发送端 A1 send to B,B 回的消息只会被 A1 接收。

目前 Key_Shared 默认是 hash range 自动切分的方式来分配给 consumer 一个 hash range。

可以考虑使用 fixed hash range 的方式将 consumer 和 hash range 进行绑定。

5. Key_Shared 在 consumer 数量变化的时候,同一个 key 的消息是可能发给不同的 consumer 吗?

目前在默认的 key hash range auto split 的模式下是会的,目前已经在优化这部分来保证同一个 key 的跨 consumer 的分发顺序。

Fixed hash range 是一个 consumer 负责一个固定的 hash range,这种方式不会,当然使用的时候需要给 consumer 去 assgin hash range。

6. KeyBasedBatcherBuilder 会重新选择分区吗?

是在一个 partition 内部的行为,不会跨 partition 进行 message batch。

7. 看到有很多 preAllocate 硬盘的参数,为什么?

BookKeeper 这边有两个(journalPreAllocSizeMB, entryLogFilePreallocationEnabled), 前者是为了写到磁盘的物理地址尽量连续,后者是是否预创建 entry log。


总结

本次分享,主要从基础概念入手,通过将读与写的过程拆分中,点出一些可以进行性能调优/出现性能瓶颈的部分。

如需了解更多技术细节,可以查看下方视频回顾。也可以点击「阅读原文」查看所有 TGIP-CN 直播回放合集。

本周末的 TGIP-CN 将会大家带来关于「Protocol Handler & Kafka-on-Pulsar」的相关内容,这两天将会发出报名链接,敬请期待吧!