-
前言
-
事故经过
-
事故重现-队列阻塞
-
事故重现-磁盘占用飙升
-
解决方法
-
总结
-
参考资料
-
代码地址
-
结尾
前言
那天我和同事一起吃完晚饭回公司加班,然后就群里就有人@我说xxx商户说收不到推送,一开始觉得没啥。我第一反应是不是极光没注册上,就让客服通知商户,重新登录下试试。这边打开极光推送的后台进行检查。后面反应收不到推送的越来越多,我就知道这事情不简单。
事故经过
由于大量商户反应收不到推送,我第一反应是不是推送系统挂了,导致没有进行推送。于是让运维老哥检查推送系统各节点的情况,发现都正常。于是打开RabbitMQ的管控台看了一下,人都蒙了。已经有几万条消息处于
我以为推送服务和MQ连接断开了,导致无法推送消息,于是让运维重启推送服务,将所有的推送服务重启完,发现
当时我以为是网络问题,导致mq无法接收到
你以为这就结束了其实并没有,没过多久发现有一台MQ服务出现异常,由于生产采用了
时间来到第二天上午10:00,运维那边又出现报警了,说推送系统有台机器,磁盘快被写满了,并且占用率很高。我的乖乖从昨晚到现在写了快40G的日志,一看报错信息瞬间就明白问题出在哪里了。麻溜的把
吐槽一波公司的ELK,压根就没有收集到这个报错信息,导致我没有及时发现。
事故重现-队列阻塞
MQ配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 |
问题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @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); } } |
看起来好像没啥问题。由于和交易系统约定好,订单数据需要先转换
为了防止消息丢失,交易系统做了
默默的吐槽一句:人在家中坐,锅从天上来。
模拟推送
推送代码
发送3条正常的消息
1 | curl http://localhost:8080/sendMsg/3 |
发送1条错误的消息
1 | curl http://localhost:8080/sendErrorMsg/1 |
再发送3条正常的消息
1 | curl http://localhost:8080/sendMsg/3 |
观察日志发下,虽然有报错,但是还能正常进行推送。但是RabbitMQ已经出现了一条
继续发送1条错误的消息
1 | curl http://localhost:8080/sendErrorMsg/1 |
再发送3条正常的消息
1 | curl http://localhost:8080/sendMsg/3 |
这个时候你会发现控制台报错,当然错误信息是解密失败,但是正常的消息却没有被消费,这个时候其实队列已经阻塞了。
从
再发送3条正常的消息
1 | curl http://localhost:8080/sendMsg/3 |
分析原因
上面说了是由于没有进行
RabbitMQ提供了一种QOS(服务质量保证)功能,即在非自动确认的消息的前提下,限制信道上的消费者所能保持的最大未确认的数量。可以通过设置
举例说明:可以理解为在
1 2 3 4 5 6 7 8 9 10 | listener: simple: # 消费端最小并发数 concurrency: 1 # 消费端最大并发数 max-concurrency: 5 # 一次处理的消息数量 prefetch: 2 # 手动应答 acknowledge-mode: manual |
prefetch参数就是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 队列一定阻塞。
这里需要好好理解一下。
处理方法
其实处理的方法很简单,将解密和解析的方法放入
对于这个就需要有日志监控系统,来及时告警了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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); } } |
注意的点
事故重现-磁盘占用飙升
一开始我不知道代码有问题,就是以为单纯的没有进行
其实现在回想起来是非常危险的操作的,将
问题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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); } } |
配置文件
1 2 3 4 5 6 7 8 9 10 | listener: simple: # 消费端最小并发数 concurrency: 1 # 消费端最大并发数 max-concurrency: 5 # 一次处理的消息数量 prefetch: 2 # 手动应答 acknowledge-mode: auto |
由于当时不知道交易系统的重发机制,重发时没有对订单数据加密的bug,所以还是会发出少量有误的消息。
发送1条错误的消息
1 | curl http://localhost:8080/sendErrorMsg/1 |
原因
解决方法
将
总结
-
个人建议,生产环境不建议使用自动ack,这样会QOS无法生效。
-
在使用手动ack的时候,需要非常注意消息签收。
-
其实在将有问题的MQ重置时,是将错误的消息给清除才没有问题了,相当于是消息丢失了。
1 2 3 4 5 6 7 | try { // 业务逻辑。 }catch (Exception e){ // 输出错误日志。 }finally { // 消息签收。 } |
参考资料
-
RabbitMQ消息监听异常问题探究
代码地址
https://gitee.com/huangxunhui/rabbitmq_accdient.git
结尾
如果有人告诉你遇到线上事故不要慌,除非是超级大佬久经沙场。否则就是瞎扯淡,你让他来试试,看看他会不会大脑一片空白,直冒汗。
如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。