生产问题:一个线程罢工的诡异事件
事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。
虽然是前人写的代码,但作为 Bugmaker&killer 只能咬着牙上了。
因为之前没有接触过出问题这块的逻辑,所以简单理了下如图:
- 有一个生产线程一直源源不断的往队列写数据。
- 消费线程也一直不停的取出数据后写入后续的业务线程池。
- 业务线程池里的线程会对每个任务进行入库操作。
整个过程还是比较清晰的,就是一个典型的生产者消费者模型。
尝试定位
接下来便是尝试定位这个问题,首先例行检查了以下几项:
- 是否内存有内存溢出?
- 应用 GC 是否有异常?
通过日志以及监控发现以上两项都是正常的。
紧接着便 dump 了线程快照查看业务线程池中的线程都在干啥。
结果发现所有业务线程池都处于 waiting 状态,队列也是空的。
同时生产者使用的队列却已经满了,没有任何消费迹象。
结合上面的流程图不难发现应该是消费队列的 Consumer 出问题了,导致上游的队列不能消费,下有的业务线程池没事可做。
review 代码
于是查看了消费代码的业务逻辑,同时也发现消费线程是一个单线程。
结合之前的线程快照,我发现这个消费线程也是处于 waiting 状态,和后面的业务线程池一模一样。
他做的事情基本上就是对消息解析,之后丢到后面的业务线程池中,没有发现什么特别的地方。
但是由于里面的分支特别多(switch case),看着有点头疼;所以我与写这个业务代码的同学沟通后他告诉我确实也只是入口处解析了一下数据,后续所有的业务逻辑都是丢到线程池中处理的,于是我便带着这个前提去排查了(埋下了伏笔)。
因为这里消费的队列其实是一个 disruptor 队列;它和我们常用的 BlockQueue不太一样,不是由开发者自定义一个消费逻辑进行处理的;而是在初始化队列时直接丢一个线程池进去,它会在内部使用这个线程池进行消费,同时回调一个方法,在这个方法里我们写自己的消费逻辑。
所以对于开发者而言,这个消费逻辑其实是一个黑盒。
于是在我反复 review 了消费代码中的数据解析逻辑发现不太可能出现问题后,便开始疯狂怀疑是不是 disruptor 自身的问题导致这个消费线程罢工了。
再翻了一阵 disruptor 的源码后依旧没发现什么问题后我咨询对 disruptor 较熟的@咖啡拿铁,在他的帮助下在本地模拟出来和生产一样的情况。
本地模拟
本地也是创建了一个单线程的线程池,分别执行了两个任务。
- 第一个任务没啥好说的,就是简单的打印。
- 第二个任务会对一个数进行累加,加到 10 之后就抛出一个未捕获的异常。
接着我们来运行一下。
发现当任务中抛出一个没有捕获的异常时,线程池中的线程就会处于 waiting 状态,同时所有的堆栈都和生产相符。
细心的朋友会发现正常运行的线程名称和异常后处于 waiting 状态的线程名称是不一样的,这个后续分析。
解决问题
当加入异常捕获后又如何呢?
程序肯定会正常运行。
同时会发现所有的任务都是由一个线程完成的。
虽说就是加了一行代码,但我们还是要搞清楚这里面的门门道道。
源码分析
于是只有直接 debug 线程池的源码最快了;
通过刚才的异常堆栈我们进入到 ThreadPoolExecutor.java:1142 处。
- 发现线程池已经帮我们做了异常捕获,但依然会往上抛。
- 在 finally 块中会执行 processWorkerExit(w,completedAbruptly) 方法。
看过之前《如何优雅的使用和理解线程池》的朋友应该还会有印象。
线程池中的任务都会被包装为一个内部 Worker 对象执行。
processWorkerExit 可以简单的理解为是把当前运行的线程销毁( workers.remove(w))、同时新增( addWorker())一个 Worker 对象接着处理;
就像是哪个零件坏掉后重新换了一个新的接着工作,但是旧零件负责的任务就没有了。
接下来看看 addWorker() 做了什么事情:
只看这次比较关心的部分;添加成功后会直接执行他的 start() 的方法。
由于 Worker 实现了 Runnable 接口,所以本质上就是调用了 runWorker() 方法。
在 runWorker() 其实就是上文 ThreadPoolExecutor 抛出异常时的那个方法。
它会从队列里一直不停的获取待执行的任务,也就是 getTask();在 getTask 也能看出它会一直从内置的队列取出任务。
而一旦队列是空的,它就会 waiting 在 workQueue.take(),也就是我们从堆栈中发现的 1067 行代码。
线程名字的变化
上文还提到了异常后的线程名称发生了改变,其实在 addWorker() 方法中可以看到 newWorker()时就会重新命名线程的名称,默认就是把后缀的计数+1。
这样一切都能解释得通了,真相只有一个:
在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 Worker;
它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。
总结
所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是:
既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。
这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。
结果发现在上文提到的众多 switchcase 中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了!!
这事也给我个教训,还是得眼见为实啊。
虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的:
- 消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。
- 开发规范,防御式编程大家需要养成习惯。
- 未知的技术栈需要谨慎,比如 disruptor,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。
转载于:https://www.cnblogs.com/CQqf2019/p/11021946.html
生产问题:一个线程罢工的诡异事件相关推荐
- 一个线程罢工的诡异事件
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 背景 事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没 ...
- 用三个线程实现生产者消费者模型,其中一个线程作为生产者,二个线程作为消费者,生产者随机生产一个时间戳或者字符串,消费者消费这个时间戳,并不能重复消费,并将其打印出来
题目要求: 用三个线程实现生产者消费者模型,其中一个线程作为生产者,二个线程作为消费者,生产者随机生产一个时间戳或者字符串,消费者消费这个时间戳,并不能重复消费,并将其打印出来.(这是一道百度面试的算 ...
- 使用python编写三个吃货吃馒头与一个伙夫生产馒头(线程与锁)
一个伙夫蒸馒头(生产者,售票),要求蒸够10个,等待,并唤醒吃馒头的人 三个吃货吃馒头(消费者,买票),要求同时吃,当任何一个人发现没馒头了,唤醒伙夫. 生产者是一堆线程,消费者是另一堆线程,内存缓冲 ...
- 面试官一个线程池问题把我问懵逼了。
你好呀,我是why哥. 前几天,有个朋友在微信上找我.他问:why哥,在吗? 我说:发生肾么事了? 他啪的一下就提了一个问题啊,很快. 我大意了,随意瞅了一眼,这题不是很简单吗? 结果没想到里面还隐藏 ...
- 如何从另一个线程更新GUI?
从另一个线程更新Label的最简单方法是什么? 我在thread1上有一个Form , thread1开始,我开始了另一个线程( thread2 ). 当thread2处理某些文件时,我想使用thre ...
- Java经典面试题:一个线程两次调用start()方法会出现什么情况?
大家好,我是 Oracle首席工程师杨晓峰. 今天想和大家深入聊聊线程,相信大家对于线程这个概念都不陌生,它是Java并发的基础元素,理解.操纵.诊断线程是Java工程师的必修课,但是你真的掌握线程了 ...
- 线程可以kill吗_我是一个线程(转)
我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长,他带着沧桑的口气对我说:"我们 ...
- java中一个线程最小优先数_Java线程的优先级
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(不完全正确,请参考下面的"线程优先级的问题"). 记住当线程的优先级没有指定时,所有线程都携带普 ...
- c语言线程不安全错误定位,C语言中的线程安全可破坏事件触发类#
最近,我被要求实现一个类作为选择过程的一部分.我按要求做了这个节目.但是,我考试不及格.我真的很想知道我的解决方案出了什么问题.任何帮助都非常感谢.问题和我的解决方案如下 实现一个线程安全类,该类在构 ...
最新文章
- 机器学习中的有监督学习,无监督学习,半监督学习
- JSON.NET 简单的使用
- ECharts-图表回执组件
- stm32串口空闲中断接收不定长数据
- Spring Cloud 与微服务学习总结(16)—— 微服务架构统一安全认证设计与实践
- php function curl token_最全PHP代码规范
- 让你每天精神都好好的方法ZT 1
- 火狐 firefox proxy moz=proxy:// 407错误 解决办法
- python实现时间戳和时间格式转换以及当前时间,特定时间的前后偏移
- 各省简称 拼音 缩写_中国各省市的简称读音
- 3月编程排行榜来了~有哪些新看点?
- SATA 模式相关概念
- win误删计算机桌面快捷方式,win10系统找回桌面被误删快捷方式的图文教程
- 腾讯云部署node mysql_【腾讯云】配置安装node
- java mifare_java – NTAG212 Mifare Ultralight与身份验证
- mysql查询专业术语,英语术语MySQL查询
- 攻防世界 转轮机加密
- 使用java实现 Excel在线预览
- RK3588参数 rk3288处理器属于什么档次
- python编程实例教程-Python程序设计实例教程
热门文章
- Delphi中TVarRec做为参数的用法
- SQL注入之时间盲注 和 报错注入(sql-lab第一关为例)
- (char*)malloc(sizeof(char))有什么用,为什么要这么写——简单介绍指针
- web.xml 中的listener、 filter、servlet 加载顺序及其详解
- ARM MOV指令对立即数的要求
- visual studio code安装shadertoy特效环境
- TCP的三次握手与四次挥手图文
- 为什么wait、notify必须在synchronized保护的同步代码中
- C语言再学习 -- 关键字volatile
- 逻辑回归的总结(详细步骤)