RabbitMQ 线上事故!慌的一批,脑袋一片空白。。。
前言
那天我和同事一起吃完晚饭回公司加班,然后就群里就有人@我说xxx商户说收不到推送,一开始觉得没啥。我第一反应是不是极光没注册上,就让客服通知商户,重新登录下试试。这边打开极光推送的后台进行检查。后面反应收不到推送的越来越多,我就知道这事情不简单。
事故经过
由于大量商户反应收不到推送,我第一反应是不是推送系统挂了,导致没有进行推送。于是让运维老哥检查推送系统各节点的情况,发现都正常。于是打开RabbitMQ的管控台看了一下,人都蒙了。已经有几万条消息处于ready
状态,还有几百条unacked
的消息。
我以为推送服务和MQ连接断开了,导致无法推送消息,于是让运维重启推送服务,将所有的推送服务重启完,发现unacked
的消息全部变成ready
,但是没过多久又有几百条unacked
的消息了,这个就很明显了能消费,没有进行ack
呀。
当时我以为是网络问题,导致mq无法接收到ack
,让运维老哥检查了一下,发现网络没问题。现在看是真的是傻,网络有问题连接都连不上。由于确定的是无法ack
造成的,立马将ack模式
由原来的manual
改成auto
紧急发布。将所有的节点升级好以后,发现推送正常了。
你以为这就结束了其实并没有,没过多久发现有一台MQ服务出现异常,由于生产采用了镜像队列
,立即将这台有问题的MQ从集群中移除。直接进行重置,然后加入回集群。这事情算是告一段落了。此时已经接近24:00了。
时间来到第二天上午10:00,运维那边又出现报警了,说推送系统有台机器,磁盘快被写满了,并且占用率很高。我的乖乖从昨晚到现在写了快40G的日志,一看报错信息瞬间就明白问题出在哪里了。麻溜的把bug
修了紧急发布。
事故重现-队列阻塞
MQ配置
spring:
# 消息队列
rabbitmq:
host: 10.0.0.53
username: guest
password: guest
virtual-host: local
port: 5672
# 消息发送确认
publisher-confirm-type: correlated
# 开启发送失败退回
publisher-returns: true
listener:
simple:
# 消费端最小并发数
concurrency: 1
# 消费端最大并发数
max-concurrency: 5
# 一次请求中预处理的消息数量
prefetch: 2
# 手动应答
acknowledge-mode: manual
问题代码
@RabbitListener(queues = ORDER_QUEUE)
public void receiveOrder(@Payload String encryptOrderDto,
@Headers Map<String,Object> headers,
Channel channel) throws Exception {
// 解密和解析
String decryptOrderDto = EncryptUtil.decryptByAes(encryptOrderDto);
OrderDto orderDto = JSON.parseObject(decryptOrderDto, OrderDto.class);
try {
// 模拟推送
pushMsg(orderDto);
}catch (Exception e){
log.error("推送失败-错误信息:{},消息内容:{}", e.getLocalizedMessage(), JSON.toJSONString(orderDto));
}finally {
// 消息签收
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG),false);
}
}
看起来好像没啥问题。由于和交易系统约定好,订单数据需要先转换json
串,然后再使用AES
进行加密,所以这边需要,先进行解密然后在进行解析。才能得到订单数据。
为了防止消息丢失,交易系统做了失败重发
机制,防止消息丢失,不巧的是重发的时候没有对订单数据进行加密。这就导致推送系统,在解密的时候出异常,从而无法进行ack
。默默的吐槽一句:人在家中坐,锅从天上来。
模拟推送
推送代码
发送3条正常的消息
curl http://localhost:8080/sendMsg/3
发送1条错误的消息
curl http://localhost:8080/sendErrorMsg/1
再发送3条正常的消息
curl http://localhost:8080/sendMsg/3
观察日志发下,虽然有报错,但是还能正常进行推送。但是RabbitMQ已经出现了一条unacked
的消息。
继续发送1条错误的消息
curl http://localhost:8080/sendErrorMsg/1
再发送3条正常的消息
curl http://localhost:8080/sendMsg/3
这个时候你会发现控制台报错,当然错误信息是解密失败,但是正常的消息却没有被消费,这个时候其实队列已经阻塞了。
从RabbitMQ
管控台也可以看到,刚刚发送的的3条消息处于ready
状态。这个时候就如果一直有消息进入,都会堆积在队里里面无法被消费。
再发送3条正常的消息
curl http://localhost:8080/sendMsg/3
分析原因
上面说了是由于没有进行ack
导致队里阻塞。那么问题来了,这是为什么呢?其实这是RabbitMQ
的一种保护机制。防止当消息激增的时候,海量的消息进入consumer
而引发consumer
宕机。
RabbitMQ提供了一种QOS(服务质量保证)功能,即在非自动确认的消息的前提下,限制信道上的消费者所能保持的最大未确认的数量。可以通过设置PrefetchCount
实现。
举例说明:可以理解为在consumer
前面加了一个缓冲容器,容器能容纳最大的消息数量就是PrefetchCount
。如果容器没有满RabbitMQ
就会将消息投递到容器内,如果满了就不投递了。当consumer
对消息进行ack
以后就会将此消息移除,从而放入新的消息。
listener:
simple:
# 消费端最小并发数
concurrency: 1
# 消费端最大并发数
max-concurrency: 5
# 一次处理的消息数量
prefetch: 2
# 手动应答
acknowledge-mode: manual
prefetch参数就是PrefetchCount
通过上面的配置发现prefetch
我只配置了2,并且concurrency
配置的只有1,所以当我发送了2条错误消息以后,由于解密失败这2条消息一直没有被ack
。将缓冲区沾满了,这个时候RabbitMQ
认为这个consumer
已经没有消费能力了就不继续给它推送消息了,所以就造成了队列阻塞。
判断队列是否有阻塞的风险。
当ack
模式为manual
,并且线上出现了unacked
消息,这个时候不用慌。由于QOS是限制信道channel
上的消费者所能保持的最大未确认的数量。所以允许出现unacked
的数量可以通过channelCount * prefetchCount * 节点数量
得出。
channlCount
就是由concurrency
,max-concurrency
决定的。
min
=concurrency * prefetch * 节点数量
max
=max-concurrency * prefetch * 节点数量
由此可以的出结论
unacked_msg_count
<min
队列不会阻塞。但需要及时处理unacked
的消息。unacked_msg_count
>=min
可能会出现堵塞。unacked_msg_count
>=max
队列一定阻塞。
这里需要好好理解一下。
处理方法
其实处理的方法很简单,将解密和解析的方法放入try catch
中就解决了这样不管解密正常与否,消息都会被签收。如果出错将会输出错误日志,让开发人员进行处理了。
对于这个就需要有日志监控系统,来及时告警了。
@RabbitListener(queues = ORDER_QUEUE)
public void receiveOrder(@Payload String encryptOrderDto,
@Headers Map<String,Object> headers,
Channel channel) throws Exception {
try {
// 解密和解析
String decryptOrderDto = EncryptUtil.decryptByAes(encryptOrderDto);
OrderDto orderDto = JSON.parseObject(decryptOrderDto, OrderDto.class);
// 模拟推送
pushMsg(orderDto);
}catch (Exception e){
log.error("推送失败-错误信息:{},消息内容:{}", e.getLocalizedMessage(), encryptOrderDto);
}finally {
// 消息签收
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG),false);
}
}
注意的点
unacked
的消息在consumer
切断连接后(重启),会自动回到队头。
事故重现-磁盘占用飙升
一开始我不知道代码有问题,就是以为单纯的没有进行ack
所以将ack
模式改成auto
自动,紧急升级了,这样不管正常与否,消息都会被签收,所以在当时确实是解决了问题。
其实现在回想起来是非常危险的操作的,将ack
模式改成auto
自动,这样会使QOS不生效。会出现大量消息涌入consumer
从而造成consumer
宕机,可以是因为当时在晚上,交易比较少,并且推送系统有多个节点,才没出现问题。
问题代码
@RabbitListener(queues = ORDER_QUEUE)
public void receiveOrder(@Payload String encryptOrderDto,
@Headers Map<String,Object> headers,
Channel channel) throws Exception {
// 解密和解析
String decryptOrderDto = EncryptUtil.decryptByAes(encryptOrderDto);
OrderDto orderDto = JSON.parseObject(decryptOrderDto, OrderDto.class);
try {
// 模拟推送
pushMsg(orderDto);
}catch (Exception e){
log.error("推送失败-错误信息:{},消息内容:{}", e.getLocalizedMessage(), encryptOrderDto);
}finally {
// 消息签收
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG),false);
}
}
配置文件
listener:
simple:
# 消费端最小并发数
concurrency: 1
# 消费端最大并发数
max-concurrency: 5
# 一次处理的消息数量
prefetch: 2
# 手动应答
acknowledge-mode: auto
由于当时不知道交易系统的重发机制,重发时没有对订单数据加密的bug,所以还是会发出少量有误的消息。
发送1条错误的消息
curl http://localhost:8080/sendErrorMsg/1
原因
RabbitMQ
消息监听程序异常时,consumer
会向rabbitmq server
发送Basic.Reject
,表示消息拒绝接受,由于Spring
默认requeue-rejected
配置为true
,消息会重新入队,然后rabbitmq server
重新投递。就相当于死循环了,所以控制台在疯狂刷错误日志造成磁盘利用率飙升的原因。
解决方法
将default-requeue-rejected: false
即可。
总结
- 个人建议,生产环境不建议使用自动ack,这样会QOS无法生效。
- 在使用手动ack的时候,需要非常注意消息签收。
- 其实在将有问题的MQ重置时,是将错误的消息给清除才没有问题了,相当于是消息丢失了。
try {
// 业务逻辑。
}catch (Exception e){
// 输出错误日志。
}finally {
// 消息签收。
}
参考资料
- RabbitMQ消息监听异常问题探究
代码地址
https://gitee.com/huangxunhui/rabbitmq_accdient.git
结尾
如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以随手点个关注哦,谢谢。
RabbitMQ 线上事故!慌的一批,脑袋一片空白。。。的更多相关文章
- 由定时脚本错误以及Elasticsearch配置错误引发的Flink线上事故
近期接手离职同事项目,突然遇到线上事故,Flink无法正常聚合数据生成指标. 以下是详细的排查过程: 问题复现 清晨,运维报告Flink数据分析模块无法正常生成指标数据. 赶紧登陆Flink所在机器, ...
- 记一次线上事故的JVM内存学习
今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...
- 记一次真实的线上事故:一个update引发的惨案!
目录 前言 项目背景介绍 要命的update 结语 前言 从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...
- ThreadLocal引起的一次线上事故
> 线上用户存储数据后查看提示无权限 前言 不知道什么时候年轻的我曾一度认为Java没啥难度,没有我实现不了的需求,没有我解不了的bug 直到我遇到至今难忘的一个bug . 线上用户存储数据后查 ...
- rabbitmq线上服务器与项目结合的问题总结
一.特殊字符需要转义 只需要加个\反斜杠就可以了 二.zk的connectString 在rabbit web页面上登录上去,新增queue就可以了
- 一次线上事故,让我对MySql的时间戳存char(10)还是int(10)有了全新的认识
美好的周五 周五的早晨,一切都是那么美好. 然鹅,10点多的时候,运营小哥哥突然告诉我后台打不开了,我怀着一颗"有什么大不了的,估计又是(S)(B)不会连wifi"的心情,自信的打 ...
- 线上bug分析
昨天下午大神把组内几十号人召集在一起开Online bug分析大会,主要是针对近期线上事故从事故原因和解决方案两个维度来分析. 对金融软件来说,每一次的线上事故都有可能给公司带来重大的损失,少扣了用户 ...
- 研发流程 接口定义&开发&前后端联调 线上日志观察 模型变动
阿里等大厂的研发流程,进去前先了解一下_我们一起进大厂 - SegmentFault 思否 https://segmentfault.com/a/1190000021831640 接口定义 测试用例评 ...
- 记录一次因subprocess PIPE 引起的线上故障
sence:python中使用subprocess.Popen(cmd, stdout=sys.STDOUT, stderr=sys.STDERR, shell=True) ,stdout, stde ...
随机推荐
- 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 百篇博客分析OpenHarmony源码 | v16.02
百篇博客系列篇.本篇为: v16.xx 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪 ...
- POJ3734-Blocks【EGF】
正题 题目链接:http://poj.org/problem?id=3734 题目大意 用思种颜色给\(n\)个格子染色,要求前两种颜色出现偶数次,求方案. \(1\leq T\leq 100,1\l ...
- NOI.AC#2144-子串【SAM,倍增】
正题 题目链接:http://noi.ac/problem/2144 题目大意 给出一个字符串\(s\)和一个序列\(a\).将字符串\(s\)的所有本质不同子串降序排序后,求有多少个区间\([l,r ...
- 聊聊并发(一)——初始JUC
一.volatile 1.介绍 JDK 5.0 提供了java.util.concurrent包,在此包中增加了并发编程中很常用的使用工具类,用于定义类似于线程的自定义子系统,包括线程池.异步IO和轻 ...
- C# 显示、隐藏窗口对应的任务栏
WPF中全屏窗口,会自动隐藏任务栏. 那非全屏窗口如何隐藏任务栏?甚至有没有一种场景,隐藏任务后自定义一套系统任务栏来显示? 以下会分阶段讲述一些概念 1. 主屏任务栏 任务栏,其实也是一个窗口,主屏 ...
- Vue router中携带参数与获取参数
Vue router中携带参数与获取参数 携带参数 query方式,就是?+&结构,例如/login?id=1 <router-link :to="{ name:'login' ...
- python class自身返回值研究
在python中,如果你写了这样一串代码: import datetime a = datetime.date(2021, 9, 1) b = "2021-09-01" print ...
- 简单介绍session,cookie,token以及区别
Cookie简介 ①.是由服务器发给客户端的特殊信息,以文本的形式存放在客户端 ②.客户端再次请求的时候,会把Cookie回发给服务器 ③.服务器接收到后,会解析Cookie生成与客户端相对应的内容 ...
- Tomcat 源码环境搭建
Tomcat 源码搭建 下载源码 下载地址 :https://tomcat.apache.org/download-80.cgi#8.5.35 下载之后解压缩 导入Idea 添加pom.xml文件 & ...
- JVM:内存结构
JVM:内存结构 说明:这是看了 bilibili 上 黑马程序员 的课程 JVM完整教程 后做的笔记 内容 程序计数器 虚拟机栈 本地方法栈 堆 方法区 直接内存 1. 程序计数器 1.1 定义 P ...