本文翻译自 Flipkart 博客《Tuning Apache Pulsar Cluster》,讲述 Flipkart 如何调优 Apache Pulsar 集群的性能,并给出了详尽的调优指南。作者:Anshul Singh,译者:傅腾
注:为了行为顺畅,有部分调整。翻译不易,有任何问题,都可以小窗公众号私信,或者直接联系 Pulsar Bot :)
这是一篇对 Apache Pulsar 集群做调优的指南。
Apache Pulsar 是一个开源的、分布式的消息和事件流平台,旨在实现高性能、可扩展性和可靠性。它是数据流处理方案 Apache Kafka 的一个替代方案,但具有许多内置的重要功能,如支持多租户、水平扩展和地理复制。
在 Flipkart 中,许多微服务使用 Pulsar 集群来处理各种流式和批处理用例。所有用户事件、订单事件(点击产品、添加到购物车、下订单等)、推荐、广告促销、支付对账等都会产生大量数据。我们必须优化处理所有这些用例的 Pulsar 集群,以实现性能优化,并不受非功能性阻塞因素的影响(如亚毫秒级消息处理支持、持久性和可用性)。这也是一篇解释我们如何管理故障容错和高可用性的文章。
在这篇博客文章中,我们将探讨一些关键的考虑因素和高效的方法来调优 Pulsar 集群,再保障性能的同时还能确保高可用性。
我们从 Pulsar 中想要什么?
Pulsar 完美适用于发布-订阅模型、事件驱动架构、实时数据处理或作为微服务通信通道。调整集群配置以更好地支持这些要求并更好地利用资源是很重要的。以下是一些相对于默认配置可以尝试改进的方面:
更好的吞吐量和 Ops
实时处理的延迟更低
可扩展性和可维护性
容错性和弹性
以下是我们在优化 Pulsar 集群后能够实现的一些结果。
3+ Gbps 吞吐量
生产延迟(p999)< 50毫秒
高扇出(fanout)支持(单个主题上的订阅量超过1000个)
支持 50K 个活跃 Topic
这一切都在一个 Pulsar 集群中处理。令人兴奋,对吧?
Pulsar 架构一览
这是关于 Pulsar 基本架构的参考链接:https://Pulsar.apache.org/docs/3.0.x/concepts-architecture-overview/
我们可以将组件分为以下几个部分:
Pulsar Cluster
Broker :处理层,负责管理和提供主题。它还处理消息、路由和与 Pulsar 客户端的连接。
bookkeeper :存储层支持持久化消息存储、数据持久化以及数据在多个存储节点之间的复制。这使得集群具有弹性。
Zookeeper:协调层,还负责管理 Pulsar 集群中的元数据和配置的策略。
Pulsar Client
生产者(Producer):将消息发布到 Pulsar 的主题(流)中
消费者(Consumer):订阅 Pulsar 主题的接收者
让我们看看在 Flipkart 中如何对各个组件做卓有成效的优化。
调优 Pulsar 集群的方法论
我们会对集群进行多种类型的负载测试,如红线测试(redline tests)、浸泡测试(soak tests)等,以验证系统在生产负载下在扩展性、速度和整体稳定性方面的表现是否良好。
我们遵循以下过程进行调优:
基准线:使用当前的设置和配置建立一个基准线。这是评估调优变化影响的参考点。
捕获指标:记录用于参考的指标。工具
https://github.com/lawrenceching/metricdump
可以提供用于后续比较和记录的指标。需要捕获的一些重要指标(后文附)。识别瓶颈:监控 broker/bookie/zookeeper的dashboards,以识别瓶颈(包括CPU/MEM资源利用率、N/W带宽、应用配置、存储I/O、元数据存储性能、身份验证/授权开销等)。除了上述记录的指标外,还可以查看Topic 的
stats
和stats internal
。更多详细信息:https://Pulsar.apache.org/docs/next/administration-stats/监控假设性变化:针对瓶颈的原因提出一个假设,并在出现任何困惑的情况下查看 Pulsar 代码库。尝试通过配置或部署更改来解决这些问题。
测试工作负载:再次测试相同的工作负载,以进行公平比较,计算变化的影响。
比较结果:将结果与之前创建的基准进行比较。如果结果是有利的,则接受它。
重复 :)
重点指标:
JVM指标 [jvm_*]:用于观察资源使用情况、GC暂停情况等。
Broker managedLedgerCache指标 [Pulsar_ml_cache_*] 和 bookkeeper 读/写缓存指标 [bookie_write_cache_*, bookie_read_cache_*]
Broker Loadbalancer指标 [Pulsar_lb_*]
延迟相关指标:
客户观察到的。
Broker 发布延迟 [Pulsar_broker_publish_latency]
Broker 观察到的写入延迟 [Pulsar_storage_ledger_write_latency_*, Pulsar_storage_write_latency_*]
bookkeeper 观察到的延迟(journal, ledger read/write 延迟)
PV持久卷指标和实际磁盘延迟
工作负载(吞吐量进/出,速率进/出,主题/订阅数量,产生的积压等)。有关其他指标的详细信息,请参阅:https://Pulsar.apache.org/docs/next/reference-metrics/
按组件调优
现在让我们来看看每个组件的关键调优点是什么,以及它们如何影响整体的行为。
Broker
调优 Pulsar brokers 需要调整许多配置。让我们来看一下其中几个重要的配置。
我们在Broker中的主要优化是:
Managed Ledger 的策略和缓存
负载均衡和 Bundle 配置
限流
JVM GC 调优
Managed Ledger
Managed Ledger 是存储主题消息的底层数据结构。
Managed Ledger 策略(EnsembleSize, WriteQuorum, AckQuorum)
不同的使用场景有不同的数据存储策略,主要可分为:
易失性(Volatile/in-memory):数据主要存储在内存中,不会持久化到磁盘。
软持久性(Soft/async disk write):数据异步写入磁盘。
强持久性(Hard/sync disk write):数据同步写入磁盘。
如何在数据持久性(durability)和延迟(latency)之间做出权衡?
耐久性越强,延迟影响越大。在 Pulsar 中,有许多配置的组合(包括控制持久性的 bookkeeper 配置,journal/ledger 如何接受请求,延迟写入等)。我们在 brokers 中配置的一个这样的配置是(EnsembleSize,WriteQuorum和AckQuorum)的三元组。
这个三元组(EnsembleSize、WriteQuorum和AckQuorum)是什么?
这个三元组控制 Broker 如何使用 bookkeeper 集群。它告诉 bookkeeper 客户端有关数据副本数量以及何时在 bookkeeper 集群中考虑成功的持久写入。
EnsembleSize定义了在 Ledger 中存储消息时要考虑的 bookies 数量。我们可以使用 managedLedgerDefaultEnsembleSize 配置来更改此大小,默认为2。
WriteQuorum定义了在 Ledger 中存储消息的复制因子。我们可以使用 managedLedgerDefaultWriteQuorum 配置来更改此值,默认值为2。
AckQuorum定义了从 bookies 需要的最小确认数量,以将消息确认为“已持久化”。
控制保证副本的数量:我们可以通过 managedLedgerDefaultAckQuorum 配置来控制所需的确认数量,默认情况下为 2。
以下是一些有帮助的场景示例,可以帮助您更好地理解调优要求:
强持久化场景:为了提供持久性和容错性,常见的组合是将 AckQuorum 和 WriteQuorum 设置为相同,并且 EnsembleSize 大于或等于 WriteQuorum。这确保每个写操作都得到了最少数量的 bookies 的确认,并在足够数量的节点上进行了复制。
低的延迟场景:为了在一定程度上牺牲持久性以实现较低的延迟,我们可以将“AckQuorum”和“WriteQuorum”设置为较低的值,相对于“Ensemble size”来说。较低的“AckQuorum”可以实现更快的确认,而“EnsembleSize”可以根据容错需求进行调整。
定制持久化和性能:根据具体使用情况,我们可以定制化 AckQuorum、WriteQuorum 和 EnsembleSize,以在持久化、性能和资源利用之间取得平衡。这需要仔细考虑诸如消息重要性、预期工作负载、期望的容错能力和可用资源等因素。
Managed Ledger 缓存
Broker 在 Pulsar 客户端和存储层( bookkeeper 集群)之间管理了一个缓存层。如果要求消费消息的实时性,重要的是调优 Broker 中的缓存属性,以减少对 bookkeeper 磁盘的读取。
缓存负责存储由 Broker 管理的 Ledger 中的主题中的尾部消息,称为 Managed Ledger 缓存。在当前 Broker 的实现中(直到v2.11),这将占用 Broker 分配到的JVM直接内存(direct memory)。
如果工作负载是可预先确定的,在考虑到集群的整体内存消耗后,可以为缓存分配额外的内存,以支持实时场景的使用。
我们可以使用 Pulsar_ml_cache_*
指标来观察缓存命中率。如果缓存命中率较低,增加内存可以帮助改善。
下图显示了针对实时消费者错误的缓存调优示例,其中缓存命中率降至0.6,反而增加了对 bookkeeper 的读取请求。
以下示例展示了一个良好的缓存调优,其中缓存命中率保持在 > 0.95,适用于实时使用。
以下的Broker 配置可以用来调优 managedLedgerCache 的性能:
managedLedgerCacheSizeMB=配置用于缓存 Ledger 元数据和索引条目的缓存大小。增加缓存大小可以通过减少磁盘I/O来提高读取性能。默认情况下,它占用可用直接内存的1/5。
managedLedgerCacheCopyEntries=false指定在将条目有效载荷插入缓存时是否制作副本。
managedLedgerCacheEvictionWatermark=0.9当触发驱逐时,将缓存级别降低到此阈值。其浮点值为[0-1]。0.9表示驱逐缓存的10%。
managedLedgerCacheEvictionIntervalMs=10调整managed Ledger 缓存清理频率。
managedLedgerCacheEvictionTimeThresholdMillis=1000指定条目在被驱逐之前可以在缓存中保留的时间长度。
managedLedgerMaxEntriesPerLedger=50000指定在创建新的 Ledger 之前,单个 Ledger 中存储的最大条目(消息)数量。该属性会影响每个 Ledger 文件的大小,并且与硬件相关。
负载均衡和 Bundle 配置
在 Pulsar 中,主题在命名空间还有一层逻辑分组。这些主题在物理上被分组并称为 bundles。因此,一个命名空间可以有多个 bundles,一个 bundles 可以有多个主题。brokers 获取 bundles 的所有权而不是每个主题的所有权。这有助于负载均衡。
当一个 Bundle 变得过大或接收到大量的传入消息时,它会被分割成多个较小的 Bundle 或子 Bundle 。Bundle 的分割和负载均衡密切相关,因为 Bundle 分割的过程有助于负载均衡。然后,这些子 Bundle 可以以负载均衡的方式分布在 Broker 之间。所有这些都是基于 Broker 的负载和可用资源动态发生的。
当一个 Bundle 被拆分时,为了做出有根据的负载均衡决策,负载均衡器考虑以下因素:
每个 brokers 的当前负载和 bundles 分布
活跃的生产者和消费者数量
消息吞吐量
资源利用
负载均衡器可以将 bundles 重新分配给负载较低的 brokers ,以确保负载的整体均衡分布。
当我们正确调整 Bundle 拆分和负载均衡时,资源利用率在负载下将达到最佳状态。这还有助于防止集群中出现任何热点,并允许在分布式环境中进行可扩展和高效的消息处理。
这是一个例子,其中一个 Broker 有一个热点和瓶颈。
上图显示了在对负载均衡器和 Bundle 配置进行更改之前的延迟。
在将负载均衡算法从 ThresholdShedder LB 算法更改为 UniformLoadShedder LB 算法后,由于它更适合我们的使用情况。
通过新的配置,每个 Broker 处理的 Bundle 数量大致相同,而之前的情况并非如此,因为一个 Broker 处理的 Bundle 数量是另一个 Broker 的3倍,导致延迟较高,出现瓶颈。
有关 bundle 和负载均衡属性的更多信息,请参阅 Pulsar 文档:https://Pulsar.apache.org/docs/3.0.x/administration-load-balance/
限流
限流允许您调节消息的流量,防止过载情况并保持系统稳定。在生产端和消费端都对客户端进行限流非常重要,因为一个表现不佳的客户端(生产或消费的QPS/吞吐量非常高)可能导致整个集群的性能下降。
相关情景:
系统稳定性:如果生产者或消费者突然产生大量的流量,可能对资源的使用超出预期(CPU、内存、磁盘IOPS、网络带宽、高GC等),导致 Pulsar 集群拥堵和潜在的服务中断。
防止背压:限流作为一种机制,对消息流应用背压。当特定组件(如消费者)无法跟上消息的生产速率时,它可以请求减慢消息的消费速率。这样可以防止消息积压和潜在的数据丢失,确保系统在其容量限制内运行。
可以使用集群配置或资源策略在不同层级上应用限流。
broker 级别:在 Broker 配置中,
dispatchThrottlingRate*
配置可以用于限制发送给消费者的吞吐量,并主动防止任何突然的消息峰值导致系统不稳定。主题级别:可以使用 Pulsar 管理命令设置命名空间策略,以对该命名空间内的所有主题应用速率限制。参考:https://Pulsar.apache.org/docs/next/admin-api-namespaces/#configure-dispatch-throttling-for-topics
订阅级别:同样,可以使用命名空间策略来限制命名空间下主题的所有订阅。可以使用 Pulsar 管理命令来应用相同的策略。参考:https://Pulsar.apache.org/docs/next/admin-api-namespaces/#configure-dispatch-throttling-for-subscription
示例:
限制客户端之前
限制客户端之后
在上述运行中,超过 100 个 Open Messaging Benchmark 客户端运行以消除可能存在的客户端瓶颈。这么多的客户端能够通过发出过多的请求轻松地降低集群的性能。
发布延迟上的尖峰表示消费者拉取消息以更快速度清除积压,超过了允许的限制,导致延迟恶化。应用调度速率限制器后,延迟尖峰得到了减少。
Pulsar 提供了两种类型的速率限制器——基于轮询和精确速率限制器。在集群中应用严格的限流规则后,使用精确速率限制器可以将上述延迟限制在不超过200毫秒的范围内。要启用它,请将 preciseTopicPublishRateLimiterEnable 标志设置为 True。
其他优化
JVM GC调优
GC算法:Pulsar 中的默认算法是 G1GC,适用于大多数用例。然而,我们建议尝试不同的 GC 算法。例如,对于低延迟的 GC,ZGC 是一个很好的算法,但与G1GC相比,它使用更多的CPU周期。
堆(Heap)大小:堆大小过小会导致 GC 暂停的频率非常高,而堆大小过大会导致巨大的 GC 暂停时间,从而增加延迟峰值。为了计算出最佳的堆大小,可以先猜测初始内存使用量,然后从那里进行调整,观察对堆大小影响最小的情况。
性能分析(Profiling):为了优化垃圾回收(GC),更好地对应用程序进行性能分析,了解内存使用模式,并使用GC日志观察 GC 行为。在负载下进行内存分析可以清楚地了解内存使用模式。
GC 调优是一个独立的话题,这是另一个时间的讨论,但我会分享一个有用的观点,在生产环境中,当为 broker/bookie/zookeeper 进程分配了专用内存时,将 xmx
和 xms
设置为相同的值。
参考:https://developer.jboss.org/thread/149559[1]
Bookkeeper
Bookkeeper 将消息存储为 Ledger 中的条目。这些 Ledger 被映射到称为 managedLedgers 的特定主题。关于提高 bookkeeper 集群性能或支持特定类型的工作负载的一些因素如下:
Journal磁盘
在将消息存储到 Ledger 之前,消息会被追加到 journal 日志中,当 journal 日志文件大小达到或翻转(rollover)周期完成时,消息将被移动到 Ledger 中。Journal 日志和 Ledger 的磁盘在决定集群性能方面起着同样重要的作用。
我们建议将 journal 日志保存在更快的磁盘上,以在集群中获得最佳性能,因为所有的消息都首先到达 journal 日志。存储成功后,它会向 brokers 发送确认。
bookkeeper 配置journalSyncData
决定了在数据被同步到 journal 日志磁盘或存储在内存中时是否发送确认。将其设置为“false”意味着存在数据丢失的可能性,因为journal日志条目被写入操作系统页面缓存但未刷新到磁盘。在断电的情况下,受影响的 bookkeeper 可能会丢失未刷新的数据。
将journal日志和ledger数据移动到更好的磁盘(SSD/NVME)上有助于实现更低的延迟和更好的 bookkeeper 集群性能,但成本更高。
决定 bookie 配置,您需要回答几个基本问题:
我需要 journal 吗?
Journal 具有较低的发布延迟,同时不影响持久化。当 journal 中的写操作成功时, bookkeeper 仅向 bookkeeper 客户(即 Broker )发送确认。拥有独立的 journal 和 ledger 还有助于提高 bookkeeper 节点的可扩展性。我们可以在 bookkeeper 配置中使用journalWriteData
标志来启用 journal 记录(默认启用)。
Ledger 可以进行延迟写入,而 journal 日志可以存储消息以保持操作的原子性和一致性。我们建议将journal日志磁盘放在比 Ledger 磁盘更高速的磁盘上。当消息存储在 ledger 中时,journal 日志磁盘会释放那些条目,因此可以使用较小的 journal 日志磁盘。
每个 bookie 节点需要多少个 Ledger ?
bookkeeper 将消息存储为ledger文件中的一个 entry。在 Pulsar 中,我们可以使用ledgerDirectories
bookkeeper 配置来配置 ledger 目录的数量。这些ledger 目录可以位于不同的磁盘上,以减少在一个设备上发生的随机 IO 引起的延迟峰值。Ledger 磁盘可以采用更便宜的 HDD,因此IOPS非常有限。拥有多个磁盘可以减少瓶颈。
你是否想知道为什么 Ledger 虽然与生产流程没有直接关系,却如此重要?原因很简单。如果已经加载了,它们可以对 journal 日志产生反压力。然而,在已加载的 Ledger 中,增加 Ledger 的 IOPS 限制或迁移到更好的磁盘有助于缓解这个瓶颈。
Journal日志配置
调整 journal 日志很容易。只需要注意以下重要的配置项:
journalBufferedWritesThreshold
确定触发将缓冲写入刷新到磁盘的数量。默认值为64,意味着当累积了64个缓冲写入时,它们将被刷新并写入磁盘上的 journal 日志。刷新写入可以确保在系统故障时消息的持久性和耐久性。
journalMaxGroupWaitMSec
确定缓冲写入在刷新到磁盘之前可以等待的最长时间。它指定写入在写入 journal 日志之前在内存中保留的持续时间。默认值为1毫秒,确保及时持久性写入。该参数平衡性能和耐久性,将写入作为一组刷新可以优化磁盘 I/O 操作。最佳值取决于许多因素,包括系统需要多少耐久性,资源利用率等。
journalWriteBufferSizeKB
确定用于将消息写入 journal 日志的缓冲区的大小。默认情况下,它设置为4 MB。该缓冲区在将消息写入磁盘上的 journal 日志之前,临时将消息保存在内存中。缓冲区的目的是优化磁盘 I/O 操作并提高写入效率。增加缓冲区大小可以增强写入吞吐量,但也会增加内存使用量。适当的值取决于消息负载、期望的性能、内存资源和磁盘能力,我们可以调整它以实现性能和资源利用之间的平衡。
参考:bookkeeper 配置 https://github.com/apache/Pulsar/blob/master/conf/bookkeeper.conf
注意:调整 journalBufferedWritesThreshold 和 journalMaxGroupWaitMSec 会影响写入延迟和持久性,较低的值会增加持久性但可能影响性能,较高的值会提高吞吐量但可能延迟磁盘持久性。
Zookeeper
将 zookeeper 的仲裁节点数量设置为适应读取操作的规模。限制 zookeeper 的操作是最佳选择,因为增加对 zookeeper 的操作可能会导致集群中的瓶颈。
导致瓶颈的一些情景包括:
增加元数据(大量元数据或大量Topic变动):最好是重复使用主题,而不是频繁创建和删除主题。
增加订阅数量
增加QPS(导致高读写)
我们发现增加 zookeeper 节点在一定程度上有助于缓解瓶颈问题。
批量处理元数据调用
升级到 Zookeeper 3.6.0+ 有助于优化查询,因为批量提交是可用的。https://issues.apache.org/jira/browse/ZOOKEEPER-3359。Pulsar > 2.10带有此功能,因为它与 Zookeeper 3.6.3 绑定在一起。
如果配置中启用了 metadataStoreBatchingEnabled 标志, brokers 可以批量调用 zookeeper。当 brokers 进行多个zk请求时(更新 acks 上的游标信息,更新主题属性,读取在启动具有多个主题或订阅的大型应用程序时的操作等),这非常有用。该属性有助于减少批处理作业启动时的延迟峰值。
上图:禁用元数据操作批处理
下图:启用元数据操作批处理
压缩
如果元数据很大,Zookeeper 很容易成为瓶颈,因为它在配置的间隔内进行快照。巨大的数据大小意味着在快照成功存储到磁盘之前,Zookeeper 将不可用。
我们可以通过减少磁盘IO来缓解这个问题,即通过启用 Zookeeper 中的压缩来增加CPU利用率。Zookeeper 支持 Gzip 和 Snappy 压缩算法。
在我们的一个使用案例中,我们有超过 100,000 个主题生成超过 2GB 的元数据。快照写入导致集群无响应,要么在生产端导致积压,要么集群大部分时间不可用。混沌测试(Chaos tests)可能会出现这个问题,其中一个随机的 zookeeper pod 重新启动,触发快照和同步。
我们选择 Snappy 是因为它对 CPU 利用率的影响较低,并且具有实时性。启用压缩后,元数据大小减小到约 200MB,这有助于更快的同步,因为写入 200MB 要快得多。
注意:压缩功能还需要升级到 Zookeeper 3.6.0+,该版本与 Pulsar 2.10+ Bundle 在一起。
参考:https://issues.apache.org/jira/browse/ZOOKEEPER-3179[2]
Pulsar 客户端
调整 Pulsar 集群后,客户端必须以优化的方式使用集群中的服务。从 Pulsar 客户端的角度考虑,为了提高性能,需要考虑一些关键配置:
消息批处理:通过将多个消息合并成一个发送,可以减少单个消息发送的开销,提高整体吞吐量。要在 Pulsar 客户端中配置消息批处理,可以将
batchingEnabled
标志设置为true
,并设置batchingMaxMessages
和batchingMaxPublishDelay
参数来控制批处理大小和发送之间的最大延迟。压缩:Pulsar 支持消息压缩,可以减少需要传输的数据量,提高网络效率和整体吞吐量。为了启用压缩, Pulsar 提供了 lz4、zlib、zstd 和snappy 的
compressionType
选项。有时候,批处理和压缩结合在一起会导致更多的延迟,这种想法似乎与直觉相悖,但我们发现情况正好相反。较小的有效载荷大小意味着减少的IO延迟,并且与批处理结合使用可以减少往返时间(RTT),从而相比没有批处理和压缩的情况下获得更好的结果。确认类型:Pulsar 提供不同的确认模式。为了优化发送到 Pulsar 集群的确认请求的数量,使用累积确认。这可以限制发送到ZooKeeper的操作数量,有助于更好地利用资源,但有一个注意事项。我们不能在共享(shared)订阅类型中使用累积确认(这是 Pulsar 架构的限制)。
接收队列:在消费者端,要处理的消息会预先获取到一个内存队列中,其大小由receiverQueueSize控制。较大的值意味着客户端可以一次拉取更多的消息,从而减少了整体的网络往返次数,但会增加内存的使用量。
结论
优化Apache Pulsar 集群包括:
优化各个组件,如Broker、bookkeeper、ZooKeeper和 Pulsar 客户端
选择合适的硬件
根据磁盘规格实施 journal 日志和 ledger配置
利用 Pulsar 客户端中的消息批处理和压缩
随着 Apache Pulsar 的新版本添加了配置和功能,可能需要进一步调优内核参数、文件系统调优、IO 优化和网络配置以获得更好的性能。您可能需要考虑管理持久性和延迟设置、负载均衡、限流以保持稳定性,调整 JVM 垃圾回收和优化 ZooKeeper 操作。
通过 Apache Pulsar 的所有这些努力,可以在实时数据处理应用中持续实现最佳性能和低延迟。
加入 Apache Pulsar 社区,与全球的开发者一起学习、分享和成长,共同塑造云原生消息流平台的未来。期待在社区见到你的身影,一起为打造更加开放、高效的技术生态做出贡献!
点击“阅读原文”,获取 Apache Pulsar 硬核干货资料!