深度解析 Apache Pulsar 内存使用原理

2022-12-13 0 931

Tessy:赵季军

编者按

Apache Pulsar 中大批采用了 Java 的堆内缓存、间接缓存和控制系统的 Page Cache 等缓存基本原理。为的是协助他们在操控性Listary和难题功能定位中更明晰地认知那些基本原理和采用各式各样缓存,责任编辑将为他们详尽如是说 Apache Pulsar 中的缓存采用基本原理、所推荐实用性并展开单纯的校正试验。

基本基本原理

Pulsar 中的 Broker,BookKeeper 和 ZooKeeper 五大模块都是如前所述 JVM(Java 软件包)运转的。JVM 在执行 Java 程序的操作过程中常把管理组织工作的缓存分割成数个地区,在每一地区放置相同类别的统计数据。右图右图为 JVM 运转时主要就的缓存南区:

深度解析 Apache Pulsar 内存使用原理

堆内缓存(Heap Memory)

在上图那些南区中,挤占缓存空间最小的一小部分叫作“堆(Heap)”,也是他们所言的堆内缓存(on-heap memory)。JVM 中的“堆”主要就放置大部份第一类的示例。而此地区在 JVM 开启的时建立,由大部份的缓存共享资源,与此同时也是废弃物过滤器的主要就组织工作地区。

JVM 便携式的废弃物拆解器给开发人员增添很大的便捷,但与此同时也会造成 GC 停滞,对 Java 插件增添很大的负面影响。

堆外缓存(Off-heap Memory)

为的是化解堆内缓存极重增添的较长时间的 GC 停滞,和作业控制系统对堆内缓存不由此可知的难题,JVM 开拓出了堆外缓存(Off-heap Memory)。堆外缓存意味著把许多第一类的示例分配在 JVM 堆内缓存以外的缓存地区,那些缓存间接受作业控制系统(而不是 JVM)管理组织工作。这样能保持一个较小的堆,以减少废弃物收集对应用的负面影响。与此同时因为这部分地区间接受作业控制系统的管理组织工作,别的进程和设备可以间接通过作业控制系统对其展开访问,减少了从 JVM 中复制缓存统计数据的操作过程。采用堆外缓存的优缺点如下:

优点:

• 方便自主开拓很大的缓存空间,对大缓存伸缩性友好;• 减少废弃物拆解增添的 GC 停滞时间;• 间接受作业控制系统控制,可以间接被其他进程和设备访问,减少了从 JVM 复制统计数据的操作过程;• 适合分配次数少、读写操作频繁的场景。

缺点:

• 需要开发人员维护缓存,容易出现缓存泄漏并且难排查;• 如果第一类的存储结构复杂,堆外缓存的统计数据结构不直观,展开串行化(Serialization)会损耗很大的操控性。

间接缓存(Direct Memory)

间接缓存(Direct Memory)属于堆外缓存的一种,既不是 JVM 运转时统计数据区的一小部分,也不是 JVM 规范中定义的缓存地区,它间接由作业控制系统分配,因此不受 Java 堆大小的限制,但是会受到所在机器总缓存的大小及处理器寻址空间的限制。在 JDK1.4 中新引入的 NIO(Non-blocking IO,即同步非阻塞 IO)机制是一种如前所述通道与缓冲区的新 I/O 方式,可以间接从作业控制系统中分配间接缓存,即在堆外分配缓存。这样的机制在许多场景中既可以避免在 Jav

Broker 中的缓存

Broker 既采用了堆内缓存,也采用了间接缓存。由于 Broker 不存储统计数据,所以对于 OS Cache 采用较少,缓存分配如右图右图:

深度解析 Apache Pulsar 内存使用原理

堆内缓存(Heap Memory)

在 Pulsar Broker 中大批采用了堆内缓存,用于记录许多处理操作过程中的缓存信息,如 Ledger、Cursor、延迟消息的时间和元统计数据等信息,下表是在某试验环境中统计出 Broker 的堆内缓存采用情况。

深度解析 Apache Pulsar 内存使用原理

从上表可以看出,ManagedLedgerImpl 和 ManagedCursorImp 挤占缓存比较大。ManagedLedgerImpl 主要就存储了 Topic 的 Ledger 信息,如果 Topic 数量很多或者 Topic 的保留统计数据非常长(默认最短 10 分钟并且达到 5 万条,最长 4 小时展开切换),就会挤占大批缓存;ManagedCursorImp 主要就存储了消费者的偏移量信息,Pulsar 可以支持单条消息确认和批量确认,这意味著需要记录哪些消息被确认,和连续确认的消息 ID 区间,如果消息 ID 区间中有大批的空洞信息,在缓存中记录的消息 ID 区间相应增多(当然会实时存储到 BookKeeper 中,以便中断后可以继续消费),因此消费者数量大意味著高缓存开销。

另外大批延迟消息也会挤占较大缓存,维护了 Timestamp、LedgerId、EntryId 等 3 个 Long 类别的优先级队列,关于延迟消息的最新改进可以参考 PIP-195[1]。挤占缓存较大的还有元统计数据缓存信息和必要的统计数据。

间接缓存(Direct Memory)

间接缓存存储第一类的操作相对堆内缓存中的操作更加频繁,比如消息的发送和消费需要频繁建立大批第一类,非常合适采用堆外缓存。Netty 提供了堆外缓存的建立和拆解能力能降低采用门槛。Pulsar Broker 的堆外缓存主要就用于发送和消费时的 Message 第一类构造,和追尾读的实时缓存消息。

发送第一类构造 Cache

发送时的 Message 第一类构造采用了一小部分间接缓存,主要就由 OpAddEntry 提供,采用了 Netty 的 RECYCLER。这部分间接缓存主要就缓存生产者发送来的消息,等待那些消息写入 Bookie 中。如果写入速率低于生产者发送过来的速率,等待写入的消息超过 Cache 大小,Broker 将停止读取生产者发送过来的统计数据。

在极端情况下,当生产者有较大的突增流量冲击时,Broker 会出现堆外缓存不足导致异常的情况,通常是因为 OpAddEntry 第一类缓存了大批待写入的消息而 Bookie 端写入速度跟不上,导致 Broker 端在写入 Bookie 超时前(目前为 30s)积压大批消息,而客户端继续写入新消息最终导致无法再分配更多的堆外缓存。

这部分 Cache 默认挤占到 MaxDirectMemorySize 的 1/2,代码如下:

private intmaxMessagePublishBufferSizeInMB = Math.max(64,    (int) (DirectMemoryUtils.jvmMaxDirectMemory() / 2 / (1024 * 1024)));

broker.conf 中的实用性如下:

# Max memory size for broker handling messages sending from producers. # If the processing message size exceed this value, broker will stop read data # from the connection. The processing messages means messages are sends to broker # but broker have not send response to client, usually waiting to write to bookies. # Its shared across all the topics running in the same broker. # Use -1 to disable the memory limitation. Default is 1/2 of direct memory. maxMessagePublishBufferSizeInMB=

尾部统计数据 Cache

Pulsar Broker 的堆外缓存的另外一种用处是 Topic 的尾部统计数据缓存,在追尾读消费场景中可以避免网络往返的时延和在 Bookie 上读取磁盘的时延,从而提高 Message 的读取操控性。在尾部读取消息时,消费者从服务 Broker 中读取统计数据,由于 Broker 已经将统计数据缓存有缓存中,因此无需从磁盘读取统计数据。 在追赶读(Catch-Up Read)读取消息时, Broker Cache 大概率会无法命中,大部份的读请求都会落到 Bookie 上。

具体操作过程如右图右图,首先 Broker 会判断该 Topic 是否有 Active Cursor,如果有,则将收到的消息写入该 Topic 对应的 Cache 中;否则不写入 Cache。

深度解析 Apache Pulsar 内存使用原理

判断存有 Active Cursor 需要与此同时满足以下两个条件:

1. 有 Durable Cursor;2. Cursor 的延迟在 managedLedgerCursorBackloggedThreshold 范围内,该参数在broker.conf中实用性,默认为 1000 条 entry。

这部分 Cache 默认挤占MaxDirectMemorySize 的 1/5,代码如下:

private int managedLedgerCacheSizeMB = Math.max(64,        (int) (DirectMemoryUtils.jvmMaxDirectMemory() /5 / (1024 * 1024)));

broker.conf 中实用性如下:

# Amount of memory to use for caching data payload in managed ledger. This memory # is allocated from JVM direct memory and its shared across all the topics # running  in the same broker. By default, uses 1/5th of available direct memory managedLedgerCacheSizeMB=

其他

其余的间接缓存由 Netty,和网络进来的包采用。此外,消费时的 Message 第一类构造也采用了一小部分 Direct Memory,主要就由 OpReadEntry 提供。 ##所推荐实用性 在长期实践经验中,所推荐堆内缓存和间接缓存的实用性比例是 1:2。 在conf/pulsar_env.sh 文件中实用性样例如下:

# Extra options to be passed to the jvm PULSAR_MEM=${PULSAR_MEM:-“-Xms2g -Xmx2g -XX:MaxDirectMemorySize=4g”}

Bookie 中的缓存

Bookie 由于存储统计数据,除采用了比较多的间接缓存,对 OS Cache 也有比较大的需求。

深度解析 Apache Pulsar 内存使用原理

堆内缓存(Heap Memory)

Bookie 采用的堆内缓存比较少,主要就用于元统计数据信息和必要统计数据的存储,例如 Journal 组提交的队列。下表是在某试验环境中统计 BookKeeper 的堆内缓存采用情况。

深度解析 Apache Pulsar 内存使用原理

间接缓存(Direct Memory)

Bookie 采用了大批的堆外缓存,主要就用于 Read Cache、Write Cache、Index Cache 和 Netty 的出入队列等,在 Bookie 的统计数据读写操作过程中的流程如右图右图:

深度解析 Apache Pulsar 内存使用原理

从图中可以看出 Bookie 通常采用相同的磁盘将读写 IO 展开了物理隔离,与此同时采用了 Read Cache 和 Write Cache,这两个 Cache 在统计数据读写操作过程中有非常重要的作用,下面将会详尽如是说。

写缓存(Write Cache)

Bookie 收到 Entry 写入请求后将其 Append 到 Journal 文件的与此同时(异常情况下统计数据可以恢复,保证统计数据的可靠性),也会保存到 Write Cache 中,如上图中深蓝色的两个箭头右图。Write Cache 分为两部分,一小部分是正在写入的 Write Cache, 另一小部分是正在正在刷盘的部分,两部分交替采用。

Write Cache 中有索引统计数据结构,可以通过索引查找到对应的 Entry。该索引是缓存级别的,如前所述 Bookie 定义的ConcurrentLongLongPairHashMap结构实现。消费者优先从正在写的 Write Cache 中读取消息,没有命中时,会去正在刷盘的 Write Cache 中读取,如果在 Write Cache 中没有命中时,则会请求从 Bookie 中读取。

每一 Write Cache 在增加 Entry 的时候会展开排序和去重处理,在同一个 Write Cache 中同一个 Ledger 下的统计数据是相邻有序的。在 Write Cache 中的统计数据刷盘到 Entrylog 文件时,写入的统计数据局部有序。这样的设计能够很大提高后续的读取效率。

Write Cache 大小默认为MaxDirectMemorySize 大小的 1/4,如MaxDirectMemorySize=4GB,则WriteCacheSize=1G,在多盘的情况下 Bookie 能够实用性数个目录充分利用磁盘 IO,最终的 Cache size 需要按照目录数均分,详尽代码如下:

private static final longDEFAULT_WRITE_CACHE_MAX_SIZE_MB = (long) (0.25 * PlatformDependent.maxDirectMemory())/ MB; longperDirectoryWriteCacheSize = writeCacheMaxSize / numberOfDirs;

bookkeeper.conf 中实用性如下:

# Size of Write Cache. Memory is allocated from JVM direct memory. # Write cache is used to buffer entries before flushing into the entry log # For good performance, it should be big enough to hold a substantial amount # of entries in the flush interval #  By default it will be allocated to 1/4th of the available direct memory dbStorage_writeCacheMaxSizeMb=

读缓存(Read Cache)

当追赶读场景时,如果消息不在 Write Cache 中, Broker 会从 Bookie 中读取一小部分磁盘上的统计数据写入到 Read Cache 中。因为存储的时

右图展示了在消费者消费统计数据时,Cache 是如何加速处理的:

深度解析 Apache Pulsar 内存使用原理
1. 从 Broker 端的3. 从 Bookie 的 Write Cache 正在刷盘Entry 日志文件和文件中偏移量的位置信息,格式如: (EntryLog, Offset);6. 在指示的偏移处读取指示的 Entry 日志文件;7. 执行预读;8. 将大部份预读条目加载到 Read Cache 中;9. 返回 Entry。

他们在采用的过程中,应尽量避免或减少追赶读场景,即消费老统计数据触发读取磁盘文件中的消息,以免对整体控制系统的操控性造成负面影响。 Read Cache 大小默认为MaxDirectMemorySize 大小的 1/4,例如MaxDirectMemorySize=4GB,则ReadCacheSize=1G。在多盘的情况下 Bookie 能够实用性数个目录充分利用磁盘 IO,最终的 Cache 大小需要按照目录数均分,详尽代码如下:

private static final long DEFAULT_READ_CACHE_MAX_SIZE_MB = (long) (0.25* PlatformDependent.maxDirectMemory())/ MB;long perDirectoryReadCacheSize = readCacheMaxSize / numberOfDirs;

bookkeeper.conf 中实用性如下:

# Size of Read cache. Memory is allocated from JVM direct memory. # This read cache is pre-filled doing read-ahead whenever a cache miss happens #  By default it will be allocated to 1/4th of the available direct memory dbStorage_readAheadCacheMaxSizeMb=

索引缓存(Index Cache)

Bookie 的 IO 模式避免了随机写的情况,意味著相同 Topic 的统计数据会混在一起,在读取时需借助 index 来提升查询效率,与此同时对 Index 文件展开缓存,默认为MaxDirectMemorySize 大小的 1/10。

bookkeeper.conf中实用性如下:

## RocksDB specific configurations ## DbLedgerStorage uses RocksDB to store the indexes from ## (ledgerId, entryId) -> (entryLog, offset) # Size of RocksDB block-cache. For best performance, this cache # should be big enough to hold a significant portion of the index # database which can reach ~2GB in some cases # Default is to use 10% of the direct memory size dbStorage_rocksDB_blockCacheSize=

其他

其他一小部分间接内存用于分配对应操作第一类,例如接收 Broker 的 Entry 第一类的建立,和 Netty 的出入队列等。

Page Cache

Page Cache 对本地文件的读写非常高效。Bookie 同样采用 Page Cache 对文件展开间接操作。Pulsar 与其他中间件相同的地方在于,Pulsar 的 Bookie 只在操作时采用,当不再需要时会及时关闭,因此 Pulsar 不会借助 Page Cache 来做大批缓存操作,下面实用性可以看到 BookKeeper 关于 Page Cache 的默认行为。对于 Page Cache 大小的设置,由于 Bookie 的 Journal 和 Ledger 都会用到 Page Cache,所以建议 OS 预留总缓存一半的资源。

conf/bookkeeper.conf 中实用性:

# Should we removepages from page cache after forcewrite  journalRemoveFromPageCache=true

所推荐实用性

在长期实践经验中,所推荐堆内缓存,间接缓存和 OS PageCache 的实用性比例是 1:2:3。 在conf/bkenv.sh 文件中实用性示例如下:

# Memory size options BOOKIE_MEM=${BOOKIE_MEM:-${PULSAR_MEM:-“-Xms2g -Xmx2g -XX:MaxDirectMemorySize=4g”}}

试验

从以上理论分析可以看出,在最新消息的追尾读场景中,Broker Cache 或者 Bookie Write Cache 的命中概率更大;而在比较早的消息的追赶读场景中,Bookie 的磁盘和 Bookie Read Cache 命中概率更大。下面他们对追尾读和追赶读读两种场景分别展开试验,该试验仅提供一个基本思路以供参考。

实用性

• 环境基本信息:K8s 部署,其中,1 个 Broker 节点,1 个 Bookie 节点,3 个 ZooKeeper 节点。• Broker JVM 实用性:-Xms128m -Xmx256m -XX:MaxDirectMemorySize=256m• Bookie JVM 实用性:-Xms128m -Xmx256m -XX:MaxDirectMemorySize=256m• 采用 pulsar-perf 工具展开试验,采用 Grafana 展开指标观察。

场景 1:追尾读

在两个客户端上与此同时开启生产者和消费者,从客户端看到生产速率和消费速率基本持平,生产速率如下:

深度解析 Apache Pulsar 内存使用原理

消费速率如下:

深度解析 Apache Pulsar 内存使用原理

Broker 和 Bookie 的堆内缓存和间接缓存的采用如右图右图:

深度解析 Apache Pulsar 内存使用原理

BookKeeper 的监控中,Write CacheRead Cache 采用情况如下,其中Read entry from read cache为 0 表示没有从Read Cache 中读取统计数据:

深度解析 Apache Pulsar 内存使用原理

在 BookKeeper 的监控中,Bookie 的写吞吐率与客户端看到的一致,均为 31MB/s 左右;Bookie 的读吞吐率read entryEntry Rate 图中的read entry 均为 0,表示统计数据没有从 Bookie 中读取,如右图右图:

深度解析 Apache Pulsar 内存使用原理
深度解析 Apache Pulsar 内存使用原理

Broker 的读吞吐率与客户端看到的消费吞吐率一致,均为 31MB/s 左右:

深度解析 Apache Pulsar 内存使用原理

在以上试验中,从客户端看到的生产速率和消费速率基本持平,从Read Cache 中可以看到,Read entry from read cache 为 0,也是说没有统计数据是从Read Cache中命中的,从 Bookie 的吞吐率输出(read entry)为 0 可以说明统计数据不是从Write Cache 中命中的,所以推断出统计数据是在 Broker 的 Cache 命中的。

场景 2:追赶读(Catch-Up Read)

在两个客户端上分别开启生产者和消费者,暂停消费者,等待生产者生产 10 分钟后再开始消费,从右图可以看到生产速率为 33000 msg/s:

深度解析 Apache Pulsar 内存使用原理

将消费速率实用性比生产速率低许多,本试验固定在 5000 msg/s:

深度解析 Apache Pulsar 内存使用原理

Broker 和 Bookie 的堆内缓存和间接缓存采用情况如下:

深度解析 Apache Pulsar 内存使用原理

在 BookKeeper 的监控中,Write Cache 和 Read Cache 中,可以看到Read entry rate from read cache 有速率,说明有统计数据从Read Cache 中读取,如右图右图:

深度解析 Apache Pulsar 内存使用原理

Bookie 读写吞吐率与客户端看到的生产消费吞吐率一致,分别为 5.2MB/s 和 34.5MB/s:

深度解析 Apache Pulsar 内存使用原理
深度解析 Apache Pulsar 内存使用原理

Broker 读吞吐率(Out)跟客户端看到的消费吞吐量一致,均为 5.2MB/s:

深度解析 Apache Pulsar 内存使用原理

从以上试验中可以看出,在有很大数量的 Backlog 情况下,生产速率为 260Mbit/s(约 32.5MB/s),消费速率为 39Mbit/s(约 4.87MB/s),理论上应该会有大批统计数据是从磁盘读取的。从 BookKeeper 的监控中Read rate from read cacheRead Cache Size 可以看出,有一小部分统计数据是从 Read Cache 命中的,从 Bookie 的吞吐量输出为 5.2MB/s 与客户端消费速率大致相同可以看出,统计数据是从 Bookie 中命中的。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务