记一次消息队列堆积的问题排查

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文介绍一次线上消息队列堆积的问题排查过程

一、背景

本次问题涉及的系统链路如上图,各系统的基本职责为:

  • Proxy:提供请求代理服务。统一代理发往下游系统的各类请求,从而让上游系统无需感知下游各服务的使用差异;同时提供精细化的限流、导流等服务特性;
  • 拉图 SDK:面向各存储平台提供统一的图片下载功能。它通常与图像类算法模型被前后编排在一起;
  • 预估引擎:提供模型推理服务。支持各种机器学习、深度学习算法模型的工程部署


二、问题排查

说明:本文用到的消息队列内部称 MetaQ,外部开源版即为 RocketMQ,下文中统一用 “MQ” 指代。

2.1 问题描述

某天上午收到报警通知,提示 Proxy 系统的入口 MQ 有堆积。打开控制台,发现 500 台 Proxy 机器中,有 1 台机器堆积十分严重,而其余机器均运行正常:

image.png


2.2 一句话根因

先开门见山直接讲问题根因(详细排查流程可参考后文内容):

  • 个别机器堆积:该机器有消费线程被卡住,虽然其余线程正常消费,但 MQ 机制决定消费位点并不前进;
  • HTTP 下载卡住:所使用的 HttpClient 版本有 bug,特定情况下超时不生效,可能导致线程一直卡住


2.3 排查流程

【1、该机器消费速度太慢?】

第一反应是这台机器的消费太慢。同样的消息量,别的机器能快速消化,而它由于消费慢于是持续堆积。然而在 MQ 控制台详细对比后,发现该机器的业务处理时长、消费 TPS 都与其他机器相近,且不同机器的规格本身也是一样的:其次,观察 Arthas 画出的火焰图,右边两根比较细长的火焰即为我们系统的业务逻辑(火焰比较高是因为 RPC 的调用栈比较深)。从图中可看出,火焰图并没有明显的“平顶效应”,火焰的宽度都比较窄,也可说明该机器在执行业务逻辑时中并没有明显的耗时卡点,由此也可证明该机器并不是因为业务处理慢而造成的堆积:


【2、系统指标是否正常?】

登录机器,发现 CPU、MEM、LOAD 均正常,且与正常机器相近,没有发现明显线索:


且该机器并无明显的 Full GC:


【3、限流导致?】

Tip:Proxy 在代理请求时会有限流机制,超出限额的流量会触发阻塞等待,从而保护下游同步服务。

因此,假如今天系统流量很大,超出了下游服务能承受的限制,则超出部分的请求会触发 RateLimiter 限流而阻塞。于是大量 MQ 消费者线程被阻塞,故 Proxy 系统整体的消息消费速度会变慢,最终体现的结果即为入口 topic 堆积:然而,不管是看该机器的日志还是监控,因限流而阻塞的现象都并不严重,比较通畅:其次,如果真是因为今天系统入口流量大导致,那应该所有 Proxy 机器(MQ 消费者)都会出现程度相近的堆积。不应该全堆在同 1 台机器上,而其他 499 台机器都正常。因此,排查系统入口流量太大的可能性。


【4、MQ 数据倾斜?】

预期应该是 500 台 Proxy 机器均分入口 MQ 里面的消息,有没有可能是数据分配时出现了倾斜,导致这台机器分到的消息量太多,其他机器分到的消息很少?

查看 Proxy 上游系统的代码,该系统在向 Proxy 发消息时并没有做自定义 Shuffle,于是会使用 MQ 默认selectOneMessageQueue 策略。而该策略会把当前消息发到基于 index % queue_size出的队列中。进一步查看 index 的逻辑,发现它初始化时以一个随机数为起点,每次访问时会 +1 自增:

image.png

结合以上两点,实现的效果即为:对各个 queue 从左到右、均匀地分发消息,并通过 % 实现自动循环:

综上可知,MQ 默认 shuffle 策略为:把消息均分到各个 queue 中。因此,可排除因为 MQ 发送消息数据倾斜,导致我们那台 Proxy 机器堆积的可能性。


【5、CPU steal ?】

Tip:CPU steal 表示虚拟机中运行的进程被宿主机上的其他进程/虚拟机占用了 CPU 时间的百分比,高 CPU steal 值通常意味着虚拟机中的进程性能下降。

比如机器规格本身写的是 4C,实际因为被 steal 后能用的可能只有 2C。所以体现的结果就是单个请求的 RT 无明显变化(不同 C 之间差异不大),但 C 的量变少了,所以这台机器整体吞吐变小、消费能力变弱,导致堆积。

但排查发现,st 正常,排除这种可能:


【6、找到线索:MQ 消费位点没变!】

兜兜转转一圈都没找到异常点,由于问题现象是 MQ 堆积,所以又想到可以去查看中间件的日志寻找线索。果然,通过定向搜索卡住机器的 queueId,发现这个队列的消费位点长时间不推进,一直卡在某个位置:

image.png

Tip:MQ 拉消息的机制是,拉到的消息会先存放在内存中容量为 1000 的 cache 中,然后这些内存中的消息将被消费者线程消费。当 cache 满了时,就不会再从 queue 中拉取

image.png

由此怀疑:是否因为 Proxy 消费停止 or 消费极慢,导致本地缓存一直是满的,于是 MQ 会停止向 queue 中拉消息,于是 offset 一直没变?

但上文已提到,不管从 MQ 控制台、系统监控指标、机器日志来看,该机器都是正常的,与其余机器没什么区别。那为什么该机器的消费位点就是不动,导致堆积越来越严重?


【7、找到根因:某个消费者线程卡住了】

Tip:对于本地缓存中的消息,MQ 会开多线程(线程数量由用户指定)去拉取消费。且为了保证消息不丢失,offset 记录的只是最靠前的那条消息

首先,这个机制是合理的。因为在 At Least Once 语义下,消息允许被消费多次但不允许被遗漏。假设现在有两个线程同时拉取了前后两条消息,且后面的消息先执行完毕。由于前一条消息的执行可能会出现异常,所以不能直接用后者的偏移量去更新 offset,否则消费失败的消息就找不回了。因此,消费位点 offset 的含义是:在该位置及之前的所有消息都已经被正确消费(有点类似 Flink 的 Watermark 机制)。


根据以上机制,回到本问题,假如这台 Proxy 机器的众多 MQ 消费者线程中有任意一个被卡住,那么整个 queue 的消费位点将永远停留在被卡住的消息对应的 offset。此时虽然其他线程都在持续正常消费,但并不能往前推进 offset。另一方面,由于上游还在源源不断往队列中发消息,于是消息只进不出, 堆积量 = 最新入口消息的offset - 消费位点offset 只增不减,体现在控制台上就是堆积越来越严重


基于此分析,通过 jstack 查看所有 MQ 消费者线程的状态,的确发现第 251 号线程永远处于 runnable 状态!

image.png

有理由怀疑,它就是被卡住的消费线程。因为在 Proxy 系统的业务场景中,绝大部分耗时都在 RPC 同步调用深度学习模型这个环节(快的几百毫秒,慢的可能要几秒),因此线程大部分时间应该处于等待同步调用返回的 waiting 状态!而此处 251 线程永远是 runnable,一定有问题。

进一步打印详情,可找到线程卡住的具体代码位置:

image.png

getImageDetail 方法内部会通过 HTTP 下载图片,以便深度学习模型进行预估。其次,搜索业务日志也发现,搜不到这个线程的日志。因为从昨晚 10 点开始卡住后,该线程就不会产生任何新的日志了,随着机器日志滚动清理,现在所有日志都将不含该线程的内容:

至此,Proxy 系统个别机器 MQ 堆积严重的问题根因找到:该机器的某个消费者线程在通过 HTTP 下载图片时一直卡住,导致整个 queue 的消费位点无法往前推进,从而持续堆积。


【8、为什么 HTTP 会一直卡住?】

至此,虽然堆积的根因已经找到,且经过尝试短期内可通过应用重启/机器下线等方式临时解决。但长远来看始终存在隐患,因为近期每隔几天时不时就会出现相同问题。需要彻底查清楚 HTTP 卡住的根因,才能从根本上解决本次问题。通过几次蹲点,捞到了一些会导致线程卡住的图片 URL。发现它们都不是内部的图片地址,且地址也打不开,也不是 jpg 形式结尾的格式:

https://ju1.vmhealthy.cn
https://978.vmhealthy.cn
https://xiong.bhjgkjhhb.shop

但问题是,即便是输入这种打不开的极端 URL,由于我们对 HttpClient 设置了 5s 超时,顶多也就会卡住 5s 啊,为什么好像超时机制没生效,一直卡住了十多个小时?

Tip:HTTP 会需要先建立连接,再进行数据传输。由此对应着两种超时:1、连接超时,针对建立连接这一阶段;2、socket 超时,针对数据传输过程中的超时

image.png


经检查,目前代码中只设置了 socket timeout,没设置 connection timeout,因此怀疑上面那些请求直接卡在了前面的 connect 阶段,而又由于没设置 connect 超时,所以请求一直卡住?然而,修改代码后重新尝试请求这些地址,发现还是会一直卡住,需要进一步排查。


【9、找到 HTTP 卡住的根因】

Tip:找到根因。在特定版本的 HttpClient 中,基于 SSL 连接的请求有 bug:实际会先尝试建立连接,再设置超时时长,先后顺序反了。因此若在连接时卡住,超时还没来得及设置,本次 HTTP 请求就会一直卡下去...

image.png

再回过头看上面那几个卡住的 URL,无一例外都是https开头的!破案了,原来是项目用的 HttpClient 有 bug。在升级了 HttpClient 版本后,重新在预发构造测试请求,线程不再卡住,且全部发到线上环境后,近期频繁出现的少量机器堆积问题再也没有出现过。

至此,经历了许多曲折,问题终于彻底解决。


2.4 通盘梳理

从最外层 Proxy 个别机器堆积的表象,不断往内层排查中经历了许多关键节点,直到找到根源。目前所有问题已全部查清,现在站在上帝视角,从内向外完整的因果链条如下:

--> Proxy 系统会基于 HttpClient 去下载图片,然后调用图像类模型做预估

--> 所使用 HttpClient 版本有 bug,在访问 https 地址时,超时不会生效

--> Proxy 系统恰好遇到了少量 https 地址,且恰好卡住(低概率事件),于是会因超时不生效而一直卡住

--> 基于 MQ 的 At Least Once 机制,消费位点将一直停留在卡住消息所对应的 offset 处(尽管其余线程都在正常消费)

--> 上游系统仍然源源不断向 Proxy 发送消息,于是消息只进不出,堆积越来越严重

--> 堆积量超过一定阈值,触发监控报警,用户察觉


三、总结

本次问题的排查过程略微曲折,但也能学到不少共性的方法论和经验教训,在本节统一提炼归纳

  • 善用排查工具:学会使用 jstack、Arthas、jprofile 等利器,在不同情景下善用合适的工具,能高效地发现异常点,找到线索,从而逐步找到问题根因;
  • 异常的敏感度:有时一些发现问题的线索其实早早就展现在了眼前,但当时因为各种原因(比如最初对问题的疑团很多,未排除的干扰因素多等)导致并不能立马发现线索,还需多积累经验;
  • 广泛查阅资料:除了内网查文档、找相关人员咨询,还要学会去外部英文网站查第一手资料;
  • 困难时要坚持:对一些隐蔽的问题,很多时候不是出现一次就能立马发现问题并解决的。可能需要前前后后经历了几轮排查,最终才能找到根因;
  • 补充底层知识:如果最开始能知道 MQ 的 offset 机制,一些问题就不会走那么多弯路了。后续在用各种中间件的过程中,要多去了解它们的底层原理;
  • 没有玄学问题:代码和机器不会骗人,一切“玄学”问题的背后,都有一套严谨又合理的成因。


四、参考

相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
目录
相关文章
|
5月前
|
消息中间件 负载均衡 开发工具
消息队列 MQ产品使用合集之当一个服务出现堆积后,为什么不把后面的流量负载到其它服务上
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
消息队列 MQ产品使用合集之当一个服务出现堆积后,为什么不把后面的流量负载到其它服务上
|
4月前
|
消息中间件 数据安全/隐私保护 RocketMQ
消息队列 MQ使用问题之遇到消费速度是固定的并且导致了堆积,该怎么办
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
4月前
|
消息中间件 监控 物联网
消息队列 MQ使用问题之如何获取和处理消息堆积数据
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
5月前
|
消息中间件 存储 负载均衡
消息队列 MQ产品使用合集之如何排查是哪个队列导致的异常TPS增加
阿里云消息队列MQ(Message Queue)是一种高可用、高性能的消息中间件服务,它允许您在分布式应用的不同组件之间异步传递消息,从而实现系统解耦、流量削峰填谷以及提高系统的可扩展性和灵活性。以下是使用阿里云消息队列MQ产品的关键点和最佳实践合集。
|
6月前
|
消息中间件 存储 监控
中间件消息队列堆积
中间件消息队列堆积是一个常见的问题
45 1
|
6月前
|
消息中间件 存储 NoSQL
消息队列之堆积问题分析
消息队列之堆积问题分析
137 1
|
消息中间件 弹性计算 运维
消息队列RocketMQ版:消费异常运维排查体验
本实验场景介绍消息队列RocketMQ版的可观测工具功能,通过示例程序模拟生产环境消费业务故障,并通过产品提供的开箱即用的可观测工具定位消费异常。
916 0
消息队列RocketMQ版:消费异常运维排查体验
|
消息中间件 存储 SQL
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(下)
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(下)
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(下)
|
消息中间件 存储 运维
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(上)
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(上)
消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?(上)
|
4月前
|
消息中间件 C语言 RocketMQ
消息队列 MQ操作报错合集之出现"Connection reset by peer"的错误,该如何处理
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。