救命!Java Web项目中的MQ消息堆积让我抓狂

开发 前端
使用线程池消费 MQ 消息并不是一个通用的解决方案。它存在一些缺点,比如可能会导致消息顺序性问题,以及服务器 CPU 使用率的飙升风险。此外,如果在多线程环境中调用第三方接口,可能会造成第三方服务的超负荷甚至崩溃。

我之前参与开发了一个餐厅系统,该系统在午餐和晚餐高峰时段面临巨大的并发需求。

为了确保系统顺畅运行,公司要求各个部门在用餐时间轮流值班,以便及时解决在线问题。

我所在的团队负责厨房展示系统,这是订单系统的下游服务。

当用户下单后,订单系统会向我们的系统发送一条 Kafka 消息。我们的系统读取消息,处理业务逻辑,保存订单和菜品数据,然后在菜品管理客户端上显示出来。

通过这种方式,厨师们可以了解每个订单所需的菜品,一旦有菜品准备好,系统会通知服务员上菜。

上菜后,服务员会更新菜品状态,这样用户就能知道哪些菜已上,哪些菜还在准备中。

这个系统极大地提高了从厨房到顾客的效率。

图片图片

这其中的关键是消息中间件 Kafka。如果它出现问题,将直接影响厨房展示系统的正常运作。

在本文中,我将分享我们在处理“消息积压问题”时的经验,希望能对你有所帮助。

初次遇到消息积压问题

最初,我们的用户量较小,系统上线后的一段时间里,消息队列(MQ)的消息通信非常顺畅。

随着用户量的增长,每个商户每天都生成大量订单数据,每个订单包含多个菜品。这导致我们菜品管理系统的数据量显著增加。

某天下午,我们收到了商户的投诉,用户下单后,菜品列表在平板上出现延迟。

厨房只有在几分钟后才能看到这些菜品。

我们立即开始调查原因。

菜品展示延迟问题通常与 Kafka 有关,因此我们首先检查了 Kafka。

果不其然,存在 消息积压。

消息积压的常见原因有以下两种:

  1. MQ 消费者服务宕机。
  2. MQ 生产者产生消息的速率超过 MQ 消费者消费消息的速率。

我检查了监控系统,发现 MQ 消费者服务运行正常,没有异常。

剩下的原因可能是 MQ 消费者的消息处理速度变慢了。

接下来,我检查了菜品管理表,发现只有几十万条记录。

似乎有必要优化 MQ 消费者的处理逻辑。

我在代码中添加了一些日志,打印出 MQ 消费者中各个关键节点的耗时。

发现两处存在明显的延迟:

  1. 在 for 循环中逐条查询数据库的代码。
  2. 进行多条件数据查询的代码。

我针对性地进行了优化:

对于 for 循环逐条查询数据库的代码,我改成了使用参数集合的 批量查询。

有时,我们需要查询指定集合中的哪些用户已经存在于数据库中。实现方式如下:

publicList<User>queryUser(List<User> searchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}

List<User> result = Lists.newArrayList();
    searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}

如果有 50 个用户,这种方法需要查询数据库 50 次。众所周知,每次数据库查询都是一次远程调用。

查询数据库 50 次意味着需要进行 50 次远程调用,耗时非常长。

那么,如何优化呢?

优化后的代码如下:

publicList<User>queryUser(List<User> searchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}

这种方式提供了一个基于用户 ID 集合进行批量查询的接口,仅需一次远程调用就可以获取所有数据。

对于多条件数据查询,我添加了一个 组合索引,解决了这一问题。

经过这些优化后,MQ 消费者的消息处理速度显著提升,成功解决了消息积压问题。

再次遇到消息积压问题

几个月后,我们再次遇到了消息积压问题。

这次问题是偶发性的,只在某些时候出现,大部分时间没有问题。

积压持续时间很短,对用户的影响较小,商户也没有投诉。

我检查了菜品管理表,此时已有几百万条记录。

通过监控和 DBA 提供的每日慢查询邮件,我发现了一些异常。

我注意到有些 SQL 语句的 WHERE 条件相同,但仅参数值不同,却使用了不同的索引。

例如,order_id=123 使用索引 a,而 order_id=124 使用索引 b。

该表存在多种查询场景,为满足不同的业务需求,添加了多个组合索引。

MySQL 根据以下几种因素来选择索引:

  1. 通过数据采样估算扫描的行数。扫描的行数越多,I/O 操作和 CPU 使用率越高。
  2. 是否使用临时表,临时表会影响查询速度。
  3. 是否需要排序,排序也会影响查询速度。

综合这些因素,MySQL 优化器选择其认为最合适的索引。

MySQL 优化器通过采样估算扫描的行数,这涉及从数据页中选择一些进行统计估算,这种方法会带来一定的误差。

由于 MVCC 机制,数据页存在多个版本,例如被删除的数据在其他事务中仍然可见,因此索引并未真正删除。这可能导致统计数据不准确,影响优化器的决策。

这些因素可能导致 MySQL 在执行 SQL 语句时 选择了错误的索引。

即便使用索引 a 更为高效,MySQL 也可能使用了索引 b。

为了解决 MySQL 选择错误索引的问题,我们使用了 FORCE INDEX 关键字,强制 SQL 查询使用索引 a。

经过这一优化,消息的轻微积压问题也得到了解决。

第三次遭遇消息积压

六个月后,某天晚上大约六点,几位商家投诉菜品管理系统出现延迟。

他们反馈说下单后菜品要过几分钟才会显示。

检查监控系统后,我发现 Kafka 消息再次堆积了。

我复查了 MySQL 的索引,发现索引是正确的,但数据查询依然很慢。

接着查看了菜品管理表,发现表中的数据量在短短六个月内竟然增长到了 3000 万条记录。

通常情况下,当单表数据过多时,查询和写入性能都会下降。

这次查询变慢的原因就是因为数据量过于庞大。

为了解决这个问题,我们需要:

  1. 实施数据库和表的分区
  2. 备份历史数据

在当时,实施数据库和表分区的成本太高,而且商家数量尚不足以支持这样的解决方案。

因此,我们决定备份历史数据。

在与产品经理和 DBA 讨论后,我们决定菜品管理表仅保留最近 30 天的数据,超过 30 天的数据会被移动到一个“历史表”中。

经过这个优化后,菜品管理表在 30 天内只积累了几百万条数据,对性能的影响较小。

这样一来,消息堆积问题得以解决。

第四次遇到消息堆积问题

在上述优化之后,系统长时间内运行顺利,没有出现消息堆积的问题。

然而,一年后的一天下午,一些商家又来投诉。

我查阅了公司邮件,发现有大量关于 Kafka 消息堆积的监控告警邮件。

由于我当时正在开会,错过了这些告警。

这次问题的时间点比较奇怪。

通常高并发都是在午餐或晚餐的高峰时段,但这次消息堆积却发生在“下午”。

这很不寻常。

一开始,我没有任何线索能找到问题的原因。

于是,我询问了订单团队是否在下午发布了新版本或者执行了某些特定操作。

因为我们的菜品管理系统是他们的下游系统,直接和他们的操作相关。

一位同事提到,半小时前他们进行了一个批量更新数万个订单状态的作业。

更改订单状态会自动发送 MQ 消息。

这导致他们的程序在极短时间内产生了大量 MQ 消息。

我们的 MQ 消费端无法快速处理这些消息,因而出现了消息堆积。

我们查看了 Kafka 消息堆积情况,发现有数十万条消息在排队等待处理。

为了快速提高 MQ 消费端的处理速度,我们考虑了两个解决方案:

  1. 增加分区数量。
  2. 使用线程池处理消息。

然而,由于消息已经堆积在现有分区中,增加新的分区并不会有太大帮助。

因此,我们决定重构代码,使用线程池来处理消息。

为了解决堆积消息,我们将线程池的核心线程数和最大线程数增加到 50。

这些参数是可以动态配置的。

经过这个调整后,堆积的数十万条消息在大约 20 分钟内被处理完毕。

这次突发的消息堆积问题得到了妥善解决。

解决问题后,我们保留了线程池的消息消费逻辑,将核心线程数设置为 8,最大线程数设置为 10。

这样在遇到消息堆积问题时,我们可以临时调整线程数以快速应对,而不会对用户造成明显影响。

注意:使用线程池消费 MQ 消息并不是一个通用的解决方案。它存在一些缺点,比如可能会导致消息顺序性问题,以及服务器 CPU 使用率的飙升风险。此外,如果在多线程环境中调用第三方接口,可能会造成第三方服务的超负荷甚至崩溃。

总结来说,MQ 消息堆积不是一个简单的问题。

根本原因是 MQ 生产端的消息生产速率超过了消费端的消息消费速率,但具体原因可能有多种。

在实际场景中,我们需要根据不同的业务情况进行优化。

对 MQ 队列消息堆积的监控和告警至关重要,能够及时发现问题。

没有完美的解决方案,只有最适合当前业务场景的方案。

责任编辑:武晓燕 来源: 路条编程
相关推荐

2024-06-06 11:57:44

2024-05-14 08:20:59

线程CPU场景

2021-09-30 07:26:15

MQ消息丢失

2020-06-15 14:36:15

2023-12-21 08:01:41

RocketMQ消息堆积

2022-12-15 17:13:22

MQRocketMQ架构

2009-06-14 21:41:23

Java Web框架

2022-10-12 07:38:24

SQL语句异常

2012-09-04 09:55:22

代码抓狂的代码开发

2021-11-08 15:38:15

消息延迟堆积

2019-06-18 16:40:26

NodeJS前端项目

2009-06-29 15:51:48

Spring容器

2022-07-26 00:00:00

MQ消息中间件

2014-07-01 09:43:55

程序员算法

2021-10-26 08:22:38

消息堆积扩容RocketMQ

2021-11-23 09:00:59

消息堆积扩容RocketMQ

2021-04-14 09:24:59

App内存后台

2009-06-14 17:18:55

ibmdwWebSphereMQ

2023-06-29 10:10:06

Rocket MQ消息中间件

2023-10-24 07:50:18

消息中间件MQ
点赞
收藏

51CTO技术栈公众号