一次线上事故对“本地文件队列异步使用”的思考

事故描述

事故现象是部分服务http请求无响应。事故从发生到恢复,接近3个小时,事故过程中重启应用服务,只能坚持几分钟到十几分钟,在真正发现问题前通过不断重启服务实例来支撑,庆幸的是核心服务没有出现无响应的事故。

最终分析为AMQ出现故障,现象是MQ客户端sendMessage后等待响应,但一直在等待,AMQ监控端口ok,控制台也可以打开,由于紧急没有具体分析,直接重启AMQ服务,切换master,通过验证服务全部恢复。

这次故障大部分服务都使用了AMQ,但除了一个核心服务没受到明显影响外,其他使用AMQ的服务都不同程度的收到了影响,服务不可用。


事后通过分析这个核心服务正式使用了本地文件队列避免了事故放大,逃过一劫,当时如果这个核心服务也受到影响就可想而知了,事故间期正直商户业务高峰期,客户估计要炸了,公司也会受到很大的损失。

此次事故比较严重,就是因为使用了本地文件队列有效隔离故障,使得影响面不大。假设(当然不希望发生了)核心业务没有使用本地文件队列来隔离故障,整个下单、收银服务将不可用,商户无法营业,损失应该在数量级。

此次事故也证明了我当时的这个架构思路的正确性,主要体现在隔离和降级。

说说这个核心服务使用“本地文件队列”

我开发出来这个组件在这个团队使用一直很稳定效果也很好。

实际上这个团队使用“本地文件队列”的姿势并不是我期望的,本身使用方法并没有明显不妥,只是会延迟消息的消费,但这中方法可以很好的且有效的隔离故障。

就是在发送AMQ消息的方法上添加了@AsyncExecutable,所以在入AMQ前先入队“本地文件队列”,然后“本地文件队列”消费者再把消息生产到AMQ。

这里正是利用率“本地文件队列”的优点,比较可靠,只依赖于本地文件系统,不会有网络故障的特性,近似不会被阻塞。

在事故分析中,实际上AMQ对该核心服务也受到影响,但由于采用了“本地文件队列”作为一级队列,有效的隔离了对AMQ的网络依赖,所以没有放大事故。事故中“本地文件队列”中消息被积累,没有被消费,重启服务后才被消费,原因后面再分析。

再说说此次事故中服务不可用的原因

  1. AMQ出现故障,现象是MQ客户端sendMessage后等待响应,但一直在等待。
  2. AMQ Client消息的生产和Tomcat共用worker线程
  3. AMQ Client消息的生产没有超时机制
  4. AMQ Client消息的生产采用同步发送,异步发送有一些问题场景不太合适。

所以基于以上信息,AMQ消息的生产阻塞了Tomcat worker线程,最终导致worker线程被耗光而服务不可用。

在事故中通过线程堆栈信息和Tomcat线程使用数统计也确定了线程很快被耗光而请求被阻塞。

另一个服务中采用了异步线程池来生产AMQ消息,但拒绝策略采用了CallerRunsPolicy, 也是线程池线程很快被耗光而再耗光Tomcat worker线程,最终导致服务不可用。

事故产生的原因就是代码中没有有效做网路调用的隔离和降级。

上面提到的核心服务也受到影响,参考《本地文件队列-异步隔离》,也是因为“本地文件队列”拒绝策略采用了CallerRunsPolicy,最终导致线程池线程很快被耗光,而使用“本地文件队列”消费调度主线程,消费调度主线程被阻塞而无法消费“本地文件队列”消息并生产到AMQ。但这里和其他服务不同的是,主业务和AMQ的消息生产是隔离的,主业务生产消息到“本地文件队列”就返回,并不直接依赖AMQ。

本文中提到了使用本地文件队列的隔离姿势。

后面再介绍使用本地文件队列的降级姿势。