3个bug导致Kafka消息丢失,我人麻了

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug

发送MQ消息居然加了超时熔断

在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QPS超过20且失败率大于5%时触发熔断。这意味着当QPS=20时,只要有一条消息发送超时,整个系统就会熔断,无法继续发送MQ消息。 hystrix.command.default.circuitBreaker.errorThresholdPercentage=5

less 复制代码
HystrixCommand(
 commandProperties = {
   @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),
   @HystrixProperty(name = "execution.timeout.enabled", value = "true")})
public void doSendMessage(Message message){
   // 发送消息
}

之前系统一直运行正常,直到最近系统请求量上升才触发了这个bug。现在已经找不到是谁配置了这个过于激进的熔断策略了。真的非常气人!

一般情况下,发送MQ消息不会失败。但是在服务刚启动且未预热时,可能会有少量请求超过100毫秒,被Hystrix判断为失败。而恰好当时QPS超过了20,导致触发了熔断。

为什么发送MQ消息还需要加入熔断机制呢? 我很不理解啊

MQ(消息队列)本身就是用来削峰填谷的,可以支持非常高的并发量。无论是低峰期还是高峰期,只要给MQ发送端添加熔断机制都会导致数据严重不一致!我真的不太明白,为什么要在发送MQ消息时加入熔断机制。

另外,为什么要设定这么激进的熔断策略呢?仅有5%的失败率就导致服务100%不可用,这是哪个天才的逻辑呢?至少在失败率超过30%且QPS超过200的情况下,才需要考虑使用熔断机制吧。在QPS为20的情况下,即使100%的请求都失败了,也不会拖垮应用服务,更何况只是区区5%的失败率呢。

这是典型的为了熔断而熔断!把熔断变成政治正确的事情。不加熔断反而变成异类,会被人瞧不起!

吞掉了异常

虽然添加熔断策略,会导致发送MQ失败抛出熔断异常,但是上层代码考虑了消息发送失败的情况。流程中包含分布式重试方案,但是排查问题时我才发现,重试策略居然没有生效!这是什么原因?

在一番排查后我发现,发送MQ的代码 吞掉了异常信息,没有向上抛出!

去掉无用的业务逻辑后,我把代码粘贴到下面。

scss 复制代码
try{
    doSendMessage(msg);
}catch(Exception  e){
    log.error("发送MQ异常:{}", msg, e);
    //发送失败MQ消息到公司故障群!
}

消息发送异常后,仅仅在系统打印了ERROR日志,并将失败消息发送到了公司的IM群里。然而,这样的处理方式根本无法让上层方法意识到消息发送失败的情况,更别提察觉到由于熔断而导致的发送失败了。在熔断场景下,消息根本没有被发送给MQ,而是直接失败。因此,可以确定消息一定丢失了。

面试时我们经常会被问到"如何保证消息不丢"。大家能够滔滔不绝地说出七八个策略来确保消息的可靠性。然而当写起代码时,为什么会犯下如此低级的错误呢?

仅仅打印ERROR日志就能解决问题吗?将故障消息上报到公司的群里就有人关注吗?考虑到公司每天各种群里都会涌现成千上万条消息,谁能保证一定有人会关注到!国庆节放假八天,会有人关注公司故障群的消息吗?

很多人在处理异常时习惯性的吞掉异常,害怕把异常抛给上游处理。系统应该处理Rpc调用失败、MQ发送失败的场景,不应该吞掉异常,而是应该重试!一般流程都会有整体的分布式重试机制,出问题不怕、出异常也不怕,只要把问题抛出,由上游发起重试即可。

悄咪咪的把异常吞掉,不是处理问题的办法!

于是我只能从日志中心捞日志,然后把消息手动发送到MQ中。我真的想问,这代码是人写的吗?

服务关闭期间,生产者先于消费者关闭,导致消息发送失败

出问题的系统流程是 先消费TopicA ,然后发送消息到Topic B。但是服务实例关闭期间,发送TopicB消息时,报错 producer has closed。为什么消费者还未关闭,生产者先关闭呢?

这个问题属于服务优雅发布范畴,一般情况下都应该首先关闭消费者,切断系统流量入口,然后再关闭生产者实例。

经过排查,发现问题的原因是生产者实例注册了shutdown hook钩子程序。也就是说,只要进程收到Kill信息,生产者就会启动关闭流程。这解释了为什么会出现这个问题。

针对这个问题,我修改了策略,删除了生产者注册shutdown hook钩子的逻辑。确保消费者先关闭!生产者后关闭。

总结

如果有人问我:消息发送失败的可能原因,我是肯定想不到会有这三个原因的。也是涨见识了。

很多人滔滔不绝的谈着 消息不丢不重,背后写的代码却让人不忍直视!

相关推荐
candyTong2 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
Rust研习社4 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒4 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro5 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax6 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH6 小时前
Koa和Express的区别
后端
MariaH6 小时前
Koa框架的使用
后端
luckdewei7 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某8 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy8 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试