Pulsar客户端消费模式揭秘:Go 语言实现 ZeroQueueConsumer
前段时间在 pulsar-client-go 社区里看到这么一个 issue:

import "github.com/apache/pulsar-client-go/pulsar"
client, err := pulsar.NewClient(pulsar.ClientOptions{
URL: "pulsar://localhost:6650",
})
if err != nil {
log.Fatal(err)
}
consumer, err := client.Subscribe(pulsar.ConsumerOptions{
Topic: "persistent://public/default/mq-topic-1",
SubscriptionName: "sub-1",
Type: pulsar.Shared,
ReceiverQueueSize: 0,
})
if err != nil {
log.Fatal(err)
}
// 小于等于 0 时会设置为 1000
const (
defaultReceiverQueueSize = 1000
)
if options.ReceiverQueueSize <= 0 {
options.ReceiverQueueSize = defaultReceiverQueueSize
}
他发现手动将 pulsar-client-go 客户端的 ReceiverQueueSize 设置为 0 的时候,客户端在初始化时会再将其调整为 1000.
if options.ReceiverQueueSize < 0 {
options.ReceiverQueueSize = defaultReceiverQueueSize
}
而如果手动将源码修改为可以设置为 0 时,却不能正常消费,消费者会一直处于 waiting 状态,获取不到任何数据。
经过我的排查发现是 Pulsar 的 Go 客户端缺少了一个 ZeroQueueConsumerImpl的实现类,这个类主要用于可以精细控制消费逻辑。
If you'd like to have tight control over message dispatching across consumers, set the consumers' receiver queue size very low (potentially even to 0 if necessary). Each consumer has a receiver queue that determines how many messages the consumer attempts to fetch at a time. For example, a receiver queue of 1000 (the default) means that the consumer attempts to process 1000 messages from the topic's backlog upon connection. Setting the receiver queue to 0 essentially means ensuring that each consumer is only doing one thing at a time.
https://pulsar.apache.org/docs/next/cookbooks-message-queue/#client-configuration-changes
正如官方文档里提到的那样,可以将 ReceiverQueueSize 设置为 0;这样消费者就可以一条条的消费数据,而不会将消息堆积在客户端队列里。
客户端消费逻辑
借此机会需要再回顾下 pulsar 客户端的消费逻辑,这样才能理解 ReceiverQueueSize 的作用以及如何在 pulsar-client-go 如何实现这个 ZeroQueueConsumerImpl。
Pulsar 客户端的消费模式是基于推拉结合的:

如这张图所描述的流程,消费者在启动的时候会主动向服务端发送一个 Flow 的命令,告诉服务端需要下发多少条消息给客户端。
同时会使用刚才的那个 ReceiverQueueSize参数作为内部队列的大小,将客户端下发的消息存储在内部队列里。
然后在调用 receive 函数的时候会直接从这个队列里获取数据。


每次消费成功后都会将内部的一个 AvailablePermit+1,直到大于 MaxReceiveQueueSize / 2 就会再次向 broker 发送 flow 命令,告诉 broker 再次下发消息。
所以这里有一个很关键的事件:就是向 broker 发送 flow 命令,这样才会有新的消息下发给客户端。
之前经常都会有研发同学让我排查无法消费的问题,最终定位到的原因几乎都是消费缓慢,导致这里的 AvailablePermit 没有增长,从而也就不会触发 broker 给客户端推送新的消息。
看到的现象就是消费非常缓慢。
ZeroQueueConsumerImpl 原理
下面来看看 ZeroQueueConsumerImpl 是如何实现队列大小为 0 依然是可以消费的。

在构建 consumer 的时候,就会根据队列大小从而来创建普通消费者还是 ZeroQueueConsumerImpl 消费者。
@Override
protected CompletableFuture<Message<T>> internalReceiveAsync() {
CompletableFuture<Message<T>> future = super.internalReceiveAsync();
if (!future.isDone()) {
// We expect the message to be not in the queue yet
increaseAvailablePermits(cnx());
}
return future;
}
这是 ZeroQueueConsumerImpl 重写的一个消费函数,其中关键的就是 increaseAvailablePermits(cnx());.
void increaseAvailablePermits(ClientCnx currentCnx) {
increaseAvailablePermits(currentCnx, 1);
}
protected void increaseAvailablePermits(ClientCnx currentCnx, int delta) {
int available = AVAILABLE_PERMITS_UPDATER.addAndGet(this, delta);
while (available >= getCurrentReceiverQueueSize() / 2 && !paused) {
if (AVAILABLE_PERMITS_UPDATER.compareAndSet(this, available, 0)) {
sendFlowPermitsToBroker(currentCnx, available);
break;
} else {
available = AVAILABLE_PERMITS_UPDATER.get(this);
}
}
}
从源码里可以得知这里的逻辑就是将 AvailablePermit 自增,达到阈值后请求 broker 下发消息。
因为在 ZeroQueueConsumerImpl 中队列大小为 0,所以 available >= getCurrentReceiverQueueSize() / 2永远都会为 true。
也就是说每消费一条消息都会请求 broker 让它再下发一条消息,这样就达到了每一条消息都精确控制的效果。
pulsar-client-go 中的实现
为了在 pulsar-client-go 实现这个需求,我提交了一个 PR 来解决这个问题。
其实从上面的分析已经得知为啥手动将 ReceiverQueueSize 设置为 0 无法消费消息了。
根本原因还是在初始化的时候优于队列为 0,导致不会给 broker 发送 flow 命令,这样就不会有消息推送到客户端,也就无法消费到数据了。
所以我们依然得参考 Java 的 ZeroQueueConsumerImpl 在每次消费的时候都手动增加 availablePermits。
为此我也新增了一个消费者 zeroQueueConsumer。
// EnableZeroQueueConsumer, if enabled, the ReceiverQueueSize will be 0.
// Notice: only non-partitioned topic is supported.
// Default is false.
EnableZeroQueueConsumer bool
consumer, err := client.Subscribe(ConsumerOptions{
Topic: topicName,
SubscriptionName: "sub-1",
Type: Shared,
NackRedeliveryDelay: 1 * time.Second,
EnableZeroQueueConsumer: true,
})
if options.EnableZeroQueueConsumer {
options.ReceiverQueueSize = 0
}
在创建消费者的时候需要指定是否开启 ZeroQueueConsumer,当开启后会手动将 ReceiverQueueSize 设置为 0.
// 可以设置默认值。
private int receiverQueueSize = 1000;
在 Go 中无法像 Java 那样在结构体初始化化的时候就指定默认值,再加上 Go 的 int 类型具备零值(也就是0),所以无法区分出 ReceiverQueueSize=0 是用户主动设置的,还是没有传入这个参数使用的零值。
所以才需要新增一个参数来手动区分是否使用 ZeroQueueConsumer。

之后在创建 consumer 的时候进行判断,只有使用的是单分区的 topic 并且开启了 EnableZeroQueueConsumer 才能创建 zeroQueueConsumer。

使用 PARTITIONED_METADATA 命令可以让 broker 返回分区数量。
func (z *zeroQueueConsumer) Receive(ctx context.Context) (Message, error) {
if state := z.pc.getConsumerState(); state == consumerClosed || state == consumerClosing {
z.log.WithField("state", state).Error("Failed to ack by closing or closed consumer")
return nil, errors.New("consumer state is closed")
}
z.Lock()
defer z.Unlock()
z.pc.availablePermits.inc()
for {
select {
case <-z.closeCh:
return nil, newError(ConsumerClosed, "consumer closed")
case cm, ok := <-z.messageCh:
if !ok {
return nil, newError(ConsumerClosed, "consumer closed")
}
return cm.Message, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
其中的关键代码:z.pc.availablePermits.inc()
消费时的逻辑其实和 Java 的 ZeroQueueConsumerImpl 逻辑保持了一致,也是每消费一条数据之前就增加一次 availablePermits。
pulsar-client-go 的运行原理与 Java 客户端的类似,也是将消息存放在了一个内部队列里,所以每次消费消息只需要从这个队列 messageCh 里获取即可。
值得注意的是, pulsar-client-go 版本的 zeroQueueConsumer 就不支持直接读取内部的队列了。
func (z *zeroQueueConsumer) Chan() <-chan ConsumerMessage {
panic("zeroQueueConsumer cannot support Chan method")
}
会直接 panic,因为直接消费 channel 在客户端层面就没法帮用户主动发送 flow 命令了,所以这个功能就只能屏蔽掉了,只可以主动的 receive 消息。

许久之前我也画过一个关于 pulsar client 的消费流程图,后续考虑会再写一篇关于 pulsar client 的原理分析文章。
参考链接:
- https://github.com/apache/pulsar-client-go/issues/1223
- https://cloud.tencent.com/developer/article/2307608
- https://pulsar.apache.org/docs/next/cookbooks-message-queue/#client-configuration-changes
- https://github.com/apache/pulsar-client-go/pull/1225
Pulsar客户端消费模式揭秘:Go 语言实现 ZeroQueueConsumer的更多相关文章
- 【Apache Pulsar】Apache Pulsar单机环境及Go语言开发环境搭建
0x01 简介 Apache Pulsar是一个开源的分布式发布-订阅消息系统,与Kafka类似,但比后者更加强大.Pulsar最初由Yahoo开发并维护,目前已经成为Apache软件组织的一个孵化子 ...
- Java 获取客户端浏览器中的语言设置
获取客户端的首选语言 javax.servlet.ServletRequest.getLocale() 根据Accept-Language请求头返回客户端的首选语言.如果客户端请求没有Accept-L ...
- TCP通信聊天服务端和客户端(C/C++语言开发)附完整源码
距离上次学Python写的Python实现简单聊天室已经过去好久了,现在学c++又写了一遍,其实过程差不多,无非是语法的变化,目前仅实现最简单的一对一的通信,然后改就是了,接下来应该是多线程了,话不多 ...
- [Apache Pulsar] 企业级分布式消息系统-Pulsar快速上手
Pulsar快速上手 前言 如果你还不了解Pulsar消息系统,可以先看上一篇文章 企业级分布式消息系统-Pulsar入门基础 Pulsar客户端支持多个语言,包括Java,Go,Pytho和C++, ...
- NATS_08:NATS客户端Go语言手动编写
NATS客户端 一个NATS客户端是基于NATS服务端来说既可以是一个生产数据的也可以是消费数据的.生产数据的叫生产者英文为 publishers,消费数据的叫消费者英文为 subscriber ...
- 用c++语言socket库函数实现服务端客户端聊天室
客户端 /* * 程序名:client.cpp,此程序用于演示socket的客户端 * 作者:C语言技术网(www.freecplus.net) 日期:20190525 */ #include < ...
- Pulsar Consumer实现介绍
Pulsar-Consumer “Pulsar is a distributed pub-sub messaging platform with a very flexible messaging m ...
- [Pulsar系列] 10分钟学会Pulsar消息系统概念
Apache Pulsar Pulsar是一个支持多租户的.高性能的服务与服务之间消息通讯的解决方案,最初由雅虎开发,现在由Apache软件基金会管理. Pulsar的主要特性如下: Pulsar实例 ...
- Apache Pulsar 在能源互联网领域的落地实践
关于 Apache Pulsar Apache Pulsar 是 Apache 软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息.存储.轻量化函数式计算为一体,采用计算与存储分离架构设计,支 ...
- RocketMQ 5.0 POP 消费模式探秘
作者:凯易&耘田 审核校对:白玙 编辑&排版:雯燕 前言:随着 RocketMQ 5.0 preview 的发布,5.0 的重大特性逐步与大家见面.POP Consumer 作为 5. ...
随机推荐
- MVCC(多版本并发控制)详解
在 MySQL InnoDB存储引擎下,RC.RR 基于 MVCC 进行并发事务控制, MVCC 是基于"数据版本"对并发事务进行访问 用一个例子来解释一下,下面是一张事务执行流程 ...
- 新手入门html
网页的组成:结构 表现 行为 Web标准: 结构 表现 行为 Html css js Html和css 是w3c制定标准 js是ECMA制定标准 HTML:指的是超文本标记语言 文件命名的规范 ...
- 小米 红米 Redmi 屏幕录制默认参数设置
小米 红米 Redmi 屏幕录制默认参数设置 视频画质:16Mbps. 帧数:60fps.
- 小米 红米 Redmi MIUI14 ANDROID 系统 耗电
小米 红米 Redmi MIUI14 ANDROID 系统 耗电 在系统更新里,点右上角三点,下载完整更新包,安装好.再把电量用到关机,充电,充满开机,别拔线,继续充10分钟.我就是这么解决的,今天用 ...
- C#.NET 不可见字符DEL
不可见字符DEL .空格.替换掉. 签名时遇到了客户端发过来的数据包含一个DEL.Notepad++ 里显示为DEL. 包含这个字符签名给上游,就会报错:签名错误. 得想办法replace掉.目前方案 ...
- 透过 node-exporter 彻底弄懂机器监控:01. node-exporter 框架讲解
前言 Prometheus 生态里有很多采集器负责各类监控数据的采集,其中使用最广泛的,显然是 node-exporter,负责 Linux.BSD 等系统的常规监控指标的采集,比如 CPU.内存.硬 ...
- 记一次 .NET某工厂报警监控设置 崩溃分析
一:背景 1. 讲故事 前些天有位朋友在微信上丢了一个崩溃的dump给我,让我帮忙看下为什么出现了崩溃,在 Windows 的事件查看器上显示的是经典的 访问违例 ,即 c0000005 错误码,不管 ...
- redis 锁
demo1 public ErrorCode initDemo1(@RequestParam("orderNo") String orderNo) throws IOExcepti ...
- MySQL入门到实战详细教程
MySQL介绍 MySQL是一个开源的关系型数据库管理系统(RDBMS),由瑞典 MySQL AB 公司开发,属于 Oracle 旗下产品,它广泛应用于各种Web应用程序和网站,MySQL使用结构化查 ...
- SingletonKit单例源码阅读学习
阅读学习QFramwork中的SingletonKit源码. Singleton 普通类的单例 作为最常用的单例模块,通过继承单例泛型类来实现,需要私有构造: //使用第一种接口单例方式 intern ...