摘要

本文围绕一个常见的使用场景深入分析在高吞吐场景下,使用Pulsar客户端收发消息可能会遇到的若干问题。并以此为切入点,梳理一下Pulsar客户端在内存控制上所做的优化改进。

使用场景

假设这样一个常见的场景,一个搜索类业务需要记录用户搜索请求,以便后续分析搜索热点,以及有针对性的优化搜索效果等。于是,我们有下面这段逻辑,简化后如下:

PulsarClient pulsarClient = PulsarClient.builder()
.serviceUrl("pulsar://localhost:6650")
.build();
Producer<byte[]> producer = pulsarClient.newProducer()
.topic("search-activities")
.create();
try {
MessageId messageId = producer.send(/* message payload here */);
log.debug("Search activity messageId={}", messageId);
} catch (Exception e) {
log.error("Failed to record search activity", e);
}

注意pulsarClientproducer均支持复用,并且推荐这么做,这里只是为了演示写到了一起。

producer.send是阻塞方式发送消息,换句话说就是线程会卡在这里等待发送结果返回。在现实中可以根据消息在实际业务中的需要选择阻塞非阻塞两种方式,例如这里我们的业务是在用户发起一次搜索请求时记录搜索请求和上下文信息,业务上对搜索请求事件并无强依赖,因此这里使用阻塞方式发消息就不太适合了,从性能上考虑会加长整体的搜索延迟,从稳定性上考虑会增加搜索执行过程中的不确定性,总的来说,要区分支线流程和主线流程,不应该把支线流程全部嵌套在主线流程中。

于是,我们可以优化一下,调整为非阻塞方式,将记录搜索事件放到其它线程中完成:

producer.sendAsync(/* message payload here */).whenComplete((msgId, ex) -> {
if (ex != null) {
log.error("Failed to record search activity", ex);
} else {
log.debug("Search activity messageId={}", msgId);
}
});

在现实中,若用户搜索的TPS较高,例如在单实例上可以超过1000QPS(高和低都是相对而言的,这里只是举个例子)。若恰好记录的搜索事件内容较多(例如包含了搜索请求的完整上下文和搜索结果等),序列化之后大小能达到100KB甚至1MB,那么上面代码在运行时你可以会遇到MemoryBufferIsFullError异常:

org.apache.pulsar.client.api.PulsarClientException$MemoryBufferIsFullError: Client memory buffer is full
at org.apache.pulsar.client.impl.ProducerImpl.canEnqueueRequest(ProducerImpl.java:972)
at org.apache.pulsar.client.impl.ProducerImpl.sendAsync(ProducerImpl.java:452)
at org.apache.pulsar.client.impl.ProducerImpl.internalSendAsync(ProducerImpl.java:343)
at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.sendAsync(TypedMessageBuilderImpl.java:102)
at org.apache.pulsar.client.impl.ProducerBase.sendAsync(ProducerBase.java:68)

另外若服务本身与Pulsar的broker之间出现了网络波动,或者Pulsar服务内部组件之间出现网络波动,导致整体producer写入延迟升高,亦或是短时间出现大量写入,你还可能会遇到ProducerQueueIsFullError异常:

org.apache.pulsar.client.api.PulsarClientException$ProducerQueueIsFullError: Producer send queue is full
at org.apache.pulsar.client.impl.ProducerImpl.canEnqueueRequest(ProducerImpl.java:965)
at org.apache.pulsar.client.impl.ProducerImpl.sendAsync(ProducerImpl.java:452)
at org.apache.pulsar.client.impl.ProducerImpl.internalSendAsync(ProducerImpl.java:343)
at org.apache.pulsar.client.impl.TypedMessageBuilderImpl.sendAsync(TypedMessageBuilderImpl.java:102)
at org.apache.pulsar.client.impl.ProducerBase.sendAsync(ProducerBase.java:68)

Producer的内存控制

下面我们对上面两种异常产生的原因作一下分析,我们先来看一下构建Producer时,ProducerBuilder中与内存使用有关的配置项:

/*
* Set the max size of the queue holding the messages pending to receive an acknowledgment from the broker.
*/
ProducerBuilder<T> maxPendingMessages(int maxPendingMessages); /*
* Set the number of max pending messages across all partitions.
*/
ProducerBuilder<T> maxPendingMessagesAcrossPartitions(int maxPendingMessagesAcrossPartitions);

maxPendingMessages用来控制producer内部队列中正在发送还没有接收到broker确认的消息数量,若队列大小超出了这个限制,默认的行为就是抛出ProducerQueueIsFullError异常,你可以通过修改另外一个配置blockIfQueueFull=true调整为阻塞等待队列中空出新的空间,这里还有另外需要注意的地方在下面会细说。

maxPendingMessages这个配置实际上是直接传递给底层各个分区的内部producer的,对于多分区的topic,实际处于pending状态的最大消息数量是maxPendingMessages乘以topic分区数量。由于maxPendingMessages结合可变的topic分区数量使得最终的pending消息数量变得不可控,因此还有另外一个优先级更高的配置maxPendingMessagesAcrossPartitions用来控制整个topic所有分区的总的一个pending消息数量,最终到各个分区内部producer取maxPendingMessagesmaxPendingMessagesAcrossPartitions / partitions的较小值。

然而,在现实应用场景中,不同业务的消息大小差异很大,单纯基于消息数量控制内存使用,开发者需要预估平均消息大小,这几乎不可能做到,因为消息的实际大小很可能会随着业务的变化而发生变化,因此在PIP-74中,在构建PulsarClient时,ClientBuilder提供了一个面向整个client实例统一的内存限制配置:

/*
* Configure a limit on the amount of memory that will be allocated by this client instance.
*
* Setting this to 0 will disable the limit.
*/
ClientBuilder memoryLimit(long memoryLimit, SizeUnit unit);

当客户端所有producer中所有pending的消息大小总和超过这个限制时,默认则会抛出MemoryBufferIsFullError异常,若同时配置了blockIfQueueFull=true,则当前线程会阻塞等待前面pending的消息发送完成。

前面提到关于blockIfQueueFull配置的使用有一个细节需要注意,这个配置是为了限制客户端producer内存使用的同时,让开发者简化处理队列或者内存buffer满了的情况可以继续发送消息,例如在一个后台定时任务的场景中批量发送消息。然而这里需要强调的是blockIfQueueFull一旦配置为true,不论是应用发送消息调用的是阻塞的Producer.send方法还是非阻塞的Producer.sendAsync方法都会出现阻塞等待,”卡“住当前线程,那么对于我们上面的业务来说这是不可接受的,若由于支线流程(特殊情况容忍丢失的用户搜索事件)异常抖动,阻塞了主线流程(搜索主线程)就得不偿失了。

// 注意:若producer发送队列满或者内存buffer满,当前线程将卡在sendAsync方法调用
producer.sendAsync(/* message payload here */).whenComplete((msgId, ex) -> {
if (ex != null) {
log.error("Failed to record search activity", ex);
} else {
log.debug("Search activity messageId={}", msgId);
}
});

PIP-1202.10.0以及之后版本的客户端中,默认启用了memoryLimit配置,其默认值为64MB,同时默认禁用了maxPendingMessagesmaxPendingMessagesAcrossPartitions配置(默认值修改为0),另外将maxPendingMessagesAcrossPartitions配置标记为了Deprecated,因为使用这个配置最终目的就是控制客户端producer的内存使用,现在已经有memoryLimit这个更加直接的配置可以替代了。

Consumer的内存控制

上面说的全部都是围绕着Producer侧的内存使用来讲的,其实在PIP-74中也提到了Pulsar客户端consumer侧的内存使用,只不过在实现中是分阶段进行的。

我们先来看一下Pulsar客户端的API早期在构造一个Consumer时,ConsumerBuilder提供的与内存使用有关的选项:

/*
* Sets the size of the consumer receive queue.
*
* (default: 1000)
*/
ConsumerBuilder<T> receiverQueueSize(int receiverQueueSize); /*
* Sets the max total receiver queue size across partitions.
*
* (default: 50000)
*/
ConsumerBuilder<T> maxTotalReceiverQueueSizeAcrossPartitions(int maxTotalReceiverQueueSizeAcrossPartitions);

Pulsar客户端通过预接收队列临时存放broker推送过来的消息,以便应用程序调用Consumer#receive或者Consumer#receiveAsync方法时直接从内存中返回消息,这是出于消费吞吐的考虑,本质上是一种以空间换取时间的策略。上面两个选项是给这个”空间“设置一个数量上的上限,注意这里仅是数量上的上限,实际的内存空间使用还要取决于平均消息大小。receiverQueueSize控制每个分区consumer的接收队列大小,maxTotalReceiverQueueSizeAcrossPartitions来控制所有分区consumer和parent consumer的接收队列总大小。

前面提到receiverQueueSizemaxTotalReceiverQueueSizeAcrossPartitions参数是以数量的形式间接的控制Consumer预接收队列的内存使用,在[PIP-74][pip-74]中提出了整个client级别的memoryLimit,同时提出了一个新的控制Consumer内存使用的方案,就是autoScaledReceiverQueueSizeEnabled:

/*
* If this is enabled, the consumer receiver queue size is initialized as a very small value, 1 by default,
* and will double itself until it reaches either the value set by {@link #receiverQueueSize(int)} or the client
* memory limit set by {@link ClientBuilder#memoryLimit(long, SizeUnit)}.
*/
ConsumerBuilder<T> autoScaledReceiverQueueSizeEnabled(boolean enabled);

当启用了这个特性之后,receiverQueueSize会从1开始呈2的指数倍增长,直至达到receiverQueueSize的限制或达到client的memoryLimit限制,其目标是在有限制的内存使用下,达到最大的吞吐效率。

番外

除了上面说的Producer和Consumer在生产和消费过程中的内存使用之外,还有一个容易被忽视的点是创建Consumer时ackTimeoutackTimeoutTickTime的配置如果不匹配,会消耗较多堆内内存。

/*
* Sets the timeout for unacknowledged messages, truncated to the nearest millisecond. The timeout must be greater than 1 second.
*/
ConsumerBuilder<T> ackTimeout(long ackTimeout, TimeUnit timeUnit); /**
* Define the granularity of the ack-timeout redelivery.
*
* <p>By default, the tick time is set to 1 second. Using a higher tick time
* reduces the memory overhead to track messages when the ack-timeout is set to
* bigger values (e.g., 1 hour).
*/
ConsumerBuilder<T> ackTimeoutTickTime(long tickTime, TimeUnit timeUnit);

若Consumer配置了ackTimeout并且配置了较大的时间窗口(例如1小时或者更长)时,应适当的调大ackTimeoutTickTime,这是因为Consumer内部使用了一个简单时间轮的算法对消息的处理时间计时,若ackTimeout时间窗口很大,ackTimeoutTickTime仍然使用其默认值1s,时间轮本身将会占用大量堆内存空间。具体细节可参考客户端源码UnAckedMessageTracker.java

总结

  1. 使用sendAsync非阻塞方法要注意其不能保证消息一定发送成功,特别是开启了blockIfQueueFull之后,它会在特定情况下演变成阻塞方法。
  2. 对于同时使用到了Producer和Consumer的应用,推荐创建两个client,分别用来创建Producer和Consumer,做读写分离,避免由于共用memoryLimit导致相互影响。

本文最先发表于: https://aops.io/article/pulsar-client-memory-control.html

作者 萧易客 一线深耕消息中间件,RPC框架多年,欢迎评论区或通过邮件交流。

微信公众号: 萧易客

github id: shawyeok

Pulsar客户端如何控制内存使用的更多相关文章

  1. 在浏览器中将表格导入到本地的EXCEL文件,注意控制内存

    if ($export_flag == 1) { $rr = $this->mdl->test($test); header("Content-Type: application ...

  2. [C++] 重载new和delete——控制内存分配

      1.new和delete表达式的工作机理      1)new表达式实际执行了三步 string *sp=new string("aaaa"); ];//string采用默认初 ...

  3. C++之控制内存分配

    一.内存分配方式 在C++中,内存分成5个区,他们分别是堆.栈.自由存储区.全局/静态存储区和常量存储区.栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释 ...

  4. c++控制内存分配

    为了满足应用程序对内存分配的特殊需求,C++允许重载new运算符和delete运算符控制内存分配,通过定位new表达式初始化对象(好处是可以在某些场景下避免重新内存分配的消耗) 1.operate n ...

  5. IdentityServer4[3]:使用客户端认证控制API访问(客户端授权模式)

    使用客户端认证控制API访问(客户端授权模式) 场景描述 使用IdentityServer保护API的最基本场景. 我们定义一个API和要访问API的客户端.客户端从IdentityServer请求A ...

  6. SQL Server sp_configure 控制内存使用

    背景知识: sp_configure   显示或更改当前服务器的全局配置设置(使用 sp_configure 可以显示或更改服务器级别的设置.) 查看 全局配置值 方法 1.execute sp_co ...

  7. 使用cgroups来控制内存使用

    磨砺技术珠矶,践行数据之道,追求卓越价值 回到上一级页面:PostgreSQL内部结构与源代码研究索引页    回到顶级页面:PostgreSQL索引页 [作者 高健@博客园  luckyjackga ...

  8. ASP.NET Core的身份认证框架IdentityServer4(7)- 使用客户端证书控制API访问

    前言 今天(2017-9-8,写于9.8,今天才发布)一口气连续把最后几篇IdentityServer4相关理论全部翻译完了,终于可以进入写代码的过程了,比较累.目前官方的文档和Demo以及一些相关组 ...

  9. IdentityServer4(7)- 使用客户端认证控制API访问(客户端授权模式)

    一.前言 本文已更新到 .NET Core 2.2 本文包括后续的Demo都会放在github:https://github.com/stulzq/IdentityServer4.Samples (Q ...

  10. C++ Primer 笔记——控制内存分配

    1.当我们使用一条new表达式时,实际执行了三步操作. new表达式调用一个名为operator new ( 或者operator new[] ) 的标准库函数.该函数分配一块足够大的,原始的,未命名 ...

随机推荐

  1. BeautifulSoup优化测试报告

    一.是什么 Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库. 中文官方文档:https://beautifulsoup.readthedocs.io/zh_C ...

  2. Python 潮流周刊#75:用 Python 开发 NoSQL 数据库(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  3. Python不同数据结构的元素频率统计

    1.list的词频统计 这里利用Python字典的键值对来进行统计.逻辑就是,根据list的内容生成一个字典,把要统计的列表元素的值作为字典的key,而后给字典中对应的key进行赋值,赋值方法采用字典 ...

  4. Sublime装sftp远程编辑插件

    我平时很喜欢用Sublime来编辑一些脚本软件,本次需要远程编辑Linux上的脚本,需要安装一些插件来完成. Ctrl+Shift+P呼叫出命令行面板后输入 install选择 Install Pac ...

  5. 零基础入门gRPC:从 0 实现一个Hello World

    在之前讲解 Nacos 注册中心的过程中,我曾简要提到过 gRPC,主要是因为 Nacos 的最新版已经采用了 gRPC 作为其核心通信协议.这一变化带来了显著的性能优化,尤其在心跳检测.健康检查等接 ...

  6. 高性能计算-gemm-openmp效率测试(10)

    1. 目标 设计一个程序,使用OpenMP并行化实现矩阵乘法.给定两个矩阵 A 和 B,矩阵大小均为1024*1024,你的任务是计算它们的乘积 C. 要求: (1).使用循环结构体的知识点,包括fo ...

  7. flask+APScheduler定时任务的使用

    目录 APScheduler简介 安装 add_job参数详解 结合flask使用 用uwsgi启动项目 用gunicorn+gevent启动flask项目 APScheduler简介 APSched ...

  8. VUE懒加载的table前端搜索

    // 前端搜索 fliterData() { const search = this.search if (search) { this.blist = this.list.filter(item = ...

  9. python 自动下载 moudle

    import sys,re,subprocess import os from subprocess import CalledProcessError new_set = set() ls = se ...

  10. gitlab之配置文件.gitlab-ci.yml

    自动化部署給我们带来的好处 自动化部署的好处体现在几个方面 1.提高前端的开发效率和开发测试之间的协调效率 Before 如果按照传统的流程,在项目上线前的测试阶段,前端同学修复bug之后,要手动把代 ...