通过 Pulsar 源码彻底解决重复消费问题

背景
最近真是和 Pulsar 杠上了,业务团队反馈说是线上有个应用消息重复消费。

而且在测试环境是可以稳定复现的,根据经验来看一般能稳定复现的都比较好解决。
定位问题
接着便是定位问题了,根据之前的经验让业务按照这几种情况先排查一下:

通过排查:1,2可以排除了。
- 没有相关日志
- 存在异常,但最外层也捕获了,所以不管有无异常都会 ACK。
第三个也在消费的入口和提交消息出计算了时间,最终发现都是在2s左右 ACK 的。
伪代码如下:
Consumer consumer = client.newConsumer()
.subscriptionType(SubscriptionType.Shared)
.enableRetry(true)
.topic(topic)
.ackTimeout(30, TimeUnit.SECONDS)
.subscriptionName("my-sub")
.messageListener(new MessageListener<byte[]>() {
@SneakyThrows
@Override
public void received(Consumer<byte[]> consumer, Message<byte[]> msg) {
log.info("msg_id{}",msg.getMessageId().toString());
TimeUnit.SECONDS.sleep(2);
consumer.acknowledge(msg);
}
})
.subscribe();
那这就很奇怪了,因为代码里配置的 ackTimeout 是 30s,理论上来说是不会存在超时导致消息重发的。
为了排除是否是超时引起的,直接将业务代码注释掉了,等于是消息收到后立即就 ACK,经过测试发现这样确实就没有重复消费了。
为了再次确认是不是和 ackTimeout 有关,直接将 .ackTimeout(30, TimeUnit.SECONDS) 注释掉后测试,发现也没有重复消费了。
确认原因
既然如此那一定是和这个配置有关了,但看代码确实没有超时,为了定位具体原因只有去看 client 的源码了。
这里简单梳理下消息的消费的流程:
- 根据
.receiverQueueSize(1000)的配置,默认情况下 broker 会直接给客户端推送 1000 条消息。 - 客户端将这 1000 条消息保存到内部队列中。
- 如果使用同步消费
receive()时,本质上就是去take这个内部队列。 - 如果是使用的是
messageListener异步消费并配置ackTimeout,每当从队列里获得一条消息后便会把这条消息加入UnAckedMessageTracker内部的一个时间轮中,定时检测顶部是否存在消息,如果存在则会触发重新投递。
4.1 加入时间轮后,异步调用我们自定义的事件,这个异步操作是提交到一个无界队列中由单个线程依次排队执行(这点是这次问题的关键) - 业务 ACK 的时候会从时间轮中删除消息,所以如果消息 ACK 的足够快,在第四步就不会获取到消息进行重新投递。

整体流程如上图,代码细节如下图:

所以问题的根本原因就是写入时间轮(UnAckedMessageTracker)开始倒计时的线程和回调业务逻辑的不是同一个线程。
如果业务执行耗时,等到消息从那个单线程的无界队列中取出来的时候很有可能已经过了 ackTimeou 的时间,从而导致了超时重发。
也就是用户所理解的 ackTimeout 周期(应该进入回调时候开始计时)和 SDK 实现的不一致造成的。
之后我再次确认同样的代码换为同步消费是没有问题的,不会导致重复消费:
while (true) {
Message msg = consumer.receive();
log.info(
"consumer Message received: " + new String(msg.getData()) + msg.getMessageId().toString());
TimeUnit.SECONDS.sleep(2);
consumer.acknowledge(msg);
}
查看代码后发现同步代码的获取消息和加入 UnAckedMessageTracker 时间轮是同步的,也就不会出现超时的问题。

总结
所以其实 是messageListener 异步消费的 ackTimeout 的语义是有问题的,需要将加入 UnAckedMessageTracker 处移动到回调函数中同步调用。
我查看了最新的 2.11.x 版本的代码依然没有修复,正准备提个 PR 切换到 master 时才发现已经有相关的 PR 了,只是还没有发版。
修复的背景和思路也是类似的,具体参考:
https://github.com/apache/pulsar/pull/18911
其实业务中并不推荐使用 ackTimeout 这个配置了,不好预估时间从而导致超时,而且我相信大部分业务配置好 ackTImeout 后直到后续出问题的时候才想起来要改。
所以干脆一开始就不要使用。
在 go 版本的 SDK 中直接废弃掉了这个参数,推荐使用 nack API 替换。

通过 Pulsar 源码彻底解决重复消费问题的更多相关文章
- Android Studio2.x版本无法自动关联源码的解决方法
Android Studio2.x版本无法自动关联源码的解决方法 在学习android开发过程中,对于一个不熟悉的类,阅读源码是一个很好的学习方式,使用andorid studio开发工具的SDK M ...
- 清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)C#版
目录 说明 TCP与UDP通信的特点 TCP中的沾包现象 自定义应用层协议 TCPLibrary通信库介绍 Demo演示 未完成功能 源码下载 说明 我前面博客中有多篇文章讲到了.NET中的网络编程, ...
- jQuery源码研究——解决命名冲突
在项目中难免不去使用多个插件,如此一来这些插件就有可能出现一样的名称,当出现同名变量时后一个将会覆盖上一个,这样的话我们就无法同时使用多个插件了. 当遇到这种情况我们可以手动去修改插件源码把它的名字改 ...
- 使用myeclipse自动导入hibernate3的jar包,如何关联hibernate源码的解决办法
1.在网上找了好久,今天终于解决了,如果你的myeclipse自动生成的添加hibernate3jar包时,依靠通常的方法是无法关联其相应版本的源代码的,就是你在编写代码是,按住ctrl + hibe ...
- 源码分析Dubbo服务消费端启动流程
通过前面文章详解,我们知道Dubbo服务消费者标签dubbo:reference最终会在Spring容器中创建一个对应的ReferenceBean实例,而ReferenceBean实现了Spring生 ...
- 被Spring坑了一把,查看源码终于解决了DataFlow部署K8s应用的问题
1 前言 欢迎访问南瓜慢说 www.pkslow.com获取更多精彩文章! Docker & Kubernetes相关文章:容器技术 基于各种原因,团队的Kubernetes被加了限制,必须在 ...
- 探索TiDB Lightning的源码来解决发现的bug
背景 上一篇<记一次简单的Oracle离线数据迁移至TiDB过程>说到在使用Lightning导入csv文件到TiDB的时候发现了一个bug,是这样一个过程. Oracle源库中表名都是大 ...
- JGUI源码:解决手机端点击出现半透明阴影(4)
下面开始进入正题,问题发现与解决 1.According解决手机浏览器点击出现半透明阴影 手机下点击有白色蒙版,原始效果如下,看起来很不协调 2.解决办法:增加 -webkit-tap-highlig ...
- [dubbo 源码之 ]2. 服务消费方如何启动服务
启动流程 消费者在启动之后,会通过ReferenceConfig#get()来生成远程调用代理类.在get方法中,会启动一系列调用函数,我们来一个个解析. 配置同样包含2种: XML <?xml ...
- eclipse 打开其他项目的jar源码 乱码解决
步骤1.在eclipse菜单栏中,Window–>Preferences–>General–>Content types 将JAR Content , Java Class File ...
随机推荐
- vue项目中配置scss
之前创建 vue 项目的时候没有选择 scss 预编译,现在项目中要使用,不知道如何配置,网上搜了下全都是: npm install sass-loader --save-devnpm instal ...
- SpringBoot内置tomcat启动过程及原理
作者:李岩科 1 背景 SpringBoot 是一个框架,一种全新的编程规范,他的产生简化了框架的使用,同时也提供了很多便捷的功能,比如内置 tomcat 就是其中一项,他让我们省去了搭建 tomca ...
- 深度学习GPU加速配置方法
深度学习GPU加速配置方法 一.英伟达官方驱动及工具安装 首先检查自己的电脑驱动版本,未更新至最新建议先将驱动更新至最新,然后点击Nvidia控制面板 2.在如下界面中点击系统信息,点击显示可以看见当 ...
- go语言的切片特性
概述: 在使用切片进行赋值的时候,产生新的数组的指针指向原来的数组,只要修改新数组中的元素时,原来数组的元素也会改变. 测试代码: func TestSliceShareMemory(t *testi ...
- 一个简单的rust字符串时钟
1.简介 用rust写的一个简单的练手的demo,一个字符串时钟,在终端用字符串方式显示当前时间.本质是对图片取灰度,然后每个像素按灰度门限用星号代替灰度值,就把图片变为由星号组成的字符型图案.把 ...
- CH334H与GL85x功能对比(过流检测与电源控制说明)
CH334H与GL85x功能对比 CH334H是符合 USB2.0 协议规范的高性能MTT 4 端口 USB2.0 HUB 控制器芯片,高ESD特性,工业级设计,外围精简,可应用于计算机和工控机主板 ...
- 基于jQuery的三种AJAX请求
基于jQuery的三种AJAX请求 1. 介绍 get请求 通常用于 获取服务端资源(向服务器要资源) 例如:根据URL地址,从服务器获取HTML文件.CSS文件.JS文件.图片文件.数据资源等. ...
- 阿里云Imagine Computing创新技术大赛决赛启幕!
2023年1月,由阿里云与英特尔主办,阿里云天池平台.边缘云.视频云共同承办的"新算力 新体验"Imagine Computing创新技术大赛复赛圆满落幕.经过两个多月的激烈角逐, ...
- 普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单
目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...
- 二十一、B树的定义、查找、插入和删除
一.B树的定义 一棵m阶的B树,或为空树,或为满足下列特性的m叉树: (1)树中每个结点至多有m棵子树:(2)B树是所有结点的平衡因子均等于0的多路平衡查找树:(3)若根结点不是叶子结点,则至少有两棵 ...