通过 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 ...
随机推荐
- 【Shell脚本案例】案例5:找出CPU/内存率占用高的进程
一.背景 找出占用高的进程 使用脚本编写找出占用CPU的进程 二.分析 1.查看进程 top 输入后按C,就可以列出 其他: ps aux 2.思路 awk进行排序,如top10 即ps aux |a ...
- jupyter 数据显示设置
#设置显示行数pd.set_option('display.max_row',None)#设置显示列数pd.set_option('display.max_column',None)#设置显示宽度pd ...
- 持续发烧,聊聊Dart语言的静态编译,能挑战Go不?
前言 前两天写了几篇文章,谈了谈Dart做后端开发的优势,比如: <Dart开发服务端,我是不是发烧(骚)了?> <持续发烧,试试Dart语言的异步操作,效率提升500%> & ...
- windows GO语言环境配置
目录 GO语言下载 安装goland go目录简介 配置gopath goland里添加goroot和gopath GO语言下载 参考教程:https://www.cnblogs.com/Domini ...
- 通过surging的后台托管服务编写任务调度并支持规则引擎自定义脚本
简介 过去,如果在业务中需要处理任务调度的时候,大家都会使用第三方的任务调度组件,而第三方组件有一套自己的规则,在微服务的中显得那么格格不入,这样就会造成代码臃肿,耦合性高,如果有分布式还需要搭建新的 ...
- SSM基础学习笔记
SSM框架: 1.界面层---servlet类---SpringMVC 2.业务逻辑层---service类---Spring 3.数据访问层(持久层)---dao类---MyBatis 一.MyBa ...
- 网络监测工具之Zabbix的搭建与测试方法(三) ---Zabbix Agent
安装客户端 在官方网站下载最新版本zabbix agent v6.2.6,然后默认安装,其中配置服务端的界面如下图: 其他一律默认即可. 启用发现功能 如上图所示,开启发现规则,默认搜索zabbix ...
- 万字长文解析Scaled YOLOv4模型(YOLO变体模型)
一,Scaled YOLOv4 摘要 1,介绍 2,相关工作 2.1,模型缩放 3,模型缩放原则 3.1,模型缩放的常规原则 3.2,为低端设备缩放的tiny模型 3.3,为高端设备缩放的Large模 ...
- .Net引用根目录子文件夹下的dll文件
在.Net开发的时候,有时候会引用一套库,这些库是由多个dll文件.正常情况下,这些dll文件需要拷贝到运行根目录下.如果这些dll文件比较多,加上其他直接引用的dll,这样会导致根目录下非常乱.我们 ...
- 关于Token和Cookie做权限校验的区别及Token自动续期方案
title: 关于Token和Cookie做权限校验的区别及Token自动续期方案 categories: 后端 tags: - .NET Token和Cookie的区别 首先,要知道一些基本概念:h ...