Kafka 消息积压了,同事跑路了

快到年底了,系统频繁出问题。我有正当理由怀疑老板不想发年终奖所以搞事。

这不,几年都遇不到的消息队列积压现象今晚又卷土重来了。

今晚注定是个不眠夜了,原神启动。。。

组里的小伙伴火急火燎找到我说,Kafka 的消息积压一直在涨,预览图一直出不来。我加了几个服务实例,刚开始可以消费,后面消费着也卡住了。

本来刚刚下班的我就比较疲惫,想让他撤回镜像明天再上。不成想组长不讲武德,直接开了个飞书视频。

我当时本来不想理他,我已经下班了,别人上线的功能出问题关我啥事。

后来他趁我不注意搞偷袭,给我私信了,我当时没多想就点开了飞书。

本来以传统功夫的点到为止,我进入飞书不点开他的会话,是能看他给我发的最后一句话的。

我把手放在他那个会话上就是没点开,已读不回这种事做多了不好。我笑了一下,准备洗洗睡了。

正在我收手不点的时候,他突然给我来了一个电话,我大意了啊,没有挂,还强行接了他的电话。两分多钟以后就好了,我说小伙子你不讲武德。

直接喊话,今晚必须解决,大家都点咖啡算他的。

这真没办法,都找上门来了。只能跟着查一下,早点解决早点睡觉。然后我就上 Kafka 面板一看:最初的4个分区已经积压了 1200 条,后面新加的分区也开始积压了,而且积压的速度越来越快。

搞清楚发生了什么?我们就得思考一下导致积压的原因。一般是消费者代码执行出错了,导致某条消息消费不了。

所以某个点卡住了,然后又有新的消息进来。

Kafka 是通过 offset 机制来标记消息是否消费过的,所以如果分区中有某一条消息消费失败,就会导致后面的没机会消费。

我用的是spring cloud stream 来处理消息队列的发送和监听。代码上是每次处理一条消息,而且代码还在处理的过程中加了 try-catch。监听器链路打印的日志显示执行成功了,try-catch也没有捕捉到任何的异常。

这一看,我就以为是消费者性能不足,突然想起 SpringCloudStream 好像有个多线程消费的机制。立马让开发老哥试试,看看能不能就这样解决了,我困得不行。

我半眯着眼睛被提醒吵醒了。开发老哥把多线程改成10之后,发现积压更快了,而且还有pod会挂。老哥查了一下多线程的配置 concurrency。

原来指的是消费者线程,一个消费者线程会负责处理一个分区。对于我们来说,增加之后可能会导致严重的流量倾斜,难怪pod会挂掉,赶紧恢复了回去。

看来想糊弄过去是不行了,我把pod运行的日志全部拉下来。查了一下日志,日志显示执行成功了,但同时有超时错误,这就见了鬼了。

作为一个坚定的唯物主义者,我是不信见鬼的。但此刻我汗毛倒竖,吓得不敢再看屏幕一眼。

但是内心又觉得不甘心,于是我偷偷瞄了一眼屏幕。不看还好,一喵你猜我发现了啥?

消费者组重平衡了,这就像是在黑暗中有一束光照向了我,猪八戒发现嫦娥原来一直暗恋自己,我的女神其实不需要拉屎。

有了重平衡就好说了,无非就是两种情况,一是服务挂掉了,二是服务消费者执行超时了。现在看来服务没有挂,那就是超时了,也正好和上面的日志能对上。

那怎么又看到监听器执行的结果是正常的呢?

这就得从 Kafka 的批量拉取机制说起了,这货和我们人类直觉上的的队列机制不太一样。我们一般理解的队列是发送一个消息给队列,然后队列就异步把这消息给消费者。但是这货是消费者主动去拉取一批来消费。

然后好死不死,SpringCloudStream 为了封装得好用符合人类的认知,就做成了一般理解的队列那种方式。

SpringCloudStream 一批拉了500条记录,然后提供了一个监听器接口让我们实现。入参是一个对象,也就是500条数据中的一条,而不是一个数组。

我们假设这500条数据的ID是 001-500,每一条数据对应的消费者需要执行10s。那么总共就需要500 x 10s=5000s。

再假设消费者执行的超时时间是 300s,而且消费者执行的过程是串行的。那么500条中最多只能执行30条,这就能解释为什么看消费链路是正常的,但是还超时。

因为单次消费确实成功了,但是批次消费也确实超时了。

我咧个豆,破案了。

于是我就想到了两种方式来处理这个问题:第一是改成单条消息消费完立马确认,第二是把批次拉取的数据量改小一点。

第一种方案挺好的,就是性能肯定没有批量那么好,不然你以为 Kafka 的吞吐量能薄纱ActiveMQ这些传统队列。吞吐量都是小事,这个方案胜在可以立马去睡觉了。只需要改一个配置:ack-mode: RECORD

第二种方案是后来提的,其实单单把批次拉取的数据量改小性能提升还不是很明显。不过既然我们都能拿到一批数据了,那多线程安排上就得了。

先改配置,一次只拉取50条 max.poll.records: 50。然后启用线程池处理,完美!

java 复制代码
@StreamListener("<TOPIC>")
public void consume(List<byte[]> payloads) {
    
    List<CompletableFuture<Void>> futures = payloads.stream().map(bytes -> {
        Payload payload = JacksonSnakeCaseUtils.parseJson(new String(bytes), Payload.class);

        return CompletableFuture.runAsync(() -> {
            // ........
        }, batchConsumeExecutor).exceptionally(e -> {
            log.error("Thread error {}", bytes, e);
            return null;
        });
    }).collect(Collectors.toList());

    try {
        // 等待这批消息中的所有任务全部完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        errorMessage = "OK";
    } catch (Exception e) {
        errorMessage = "Ex: " + e.getMessage();
    } finally {
        // ...
    }
}
相关推荐
00后程序员2 小时前
App Store 上架 App 的完整指南,从开发准备到 IPA 上传的跨平台实战流程
后端
武子康2 小时前
大数据-156 Apache Druid+Kafka 实时分析实战:JSON 拉平摄取与 SQL 指标全流程
大数据·后端·nosql
华仔啊2 小时前
MySql 的 VARCHAR 和 TEXT 怎么选?大厂都在用的文本存储方案
后端·mysql
脉动数据行情2 小时前
Go语言对接股票、黄金、外汇API实时数据教程
开发语言·后端·golang
kfyty7252 小时前
loveqq 作为网关框架时如何修改请求体 / 响应体,和 spring 又有什么区别?
后端·架构
aiopencode2 小时前
Swift 加密工具推荐,构建可落地的多层安全体系(源码混淆+IPA 加固+动态对抗+映射治理)
后端
Moe4883 小时前
合并Pdf、excel、图片、word为单个Pdf文件的工具类(技术点的选择与深度解析)
java·后端
又过一个秋3 小时前
CyberRT Transport传输层设计
后端
Java水解3 小时前
20个高级Java开发面试题及答案!
spring boot·后端·面试