文档章节

一个线程罢工的诡异事件

crossoverJie
 crossoverJie
发布于 03/13 08:32
字数 2068
阅读 880
收藏 14

背景

事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。

虽然是前人写的代码,但作为 Bug maker&killer 只能咬着牙上了。

因为之前没有接触过出问题这块的逻辑,所以简单理了下如图:

  1. 有一个生产线程一直源源不断的往队列写数据。
  2. 消费线程也一直不停的取出数据后写入后续的业务线程池。
  3. 业务线程池里的线程会对每个任务进行入库操作。

整个过程还是比较清晰的,就是一个典型的生产者消费者模型。

尝试定位

接下来便是尝试定位这个问题,首先例行检查了以下几项:

  • 是否内存有内存溢出?
  • 应用 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 也能看出它会一直从内置的队列取出任务。

而一旦队列是空的,它就会 waitingworkQueue.take(),也就是我们从堆栈中发现的 1067 行代码。

线程名字的变化

上文还提到了异常后的线程名称发生了改变,其实在 addWorker() 方法中可以看到 new Worker()时就会重新命名线程的名称,默认就是把后缀的计数+1。

这样一切都能解释得通了,真相只有一个:

在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 Worker; 它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。

总结

所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是:

既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。

这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。

结果发现在上文提到的众多 switch case 中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了🤬!!

这事也给我个教训,还是得眼见为实啊。

虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的:

  1. 消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。
  2. 开发规范,防御式编程大家需要养成习惯。
  3. 未知的技术栈需要谨慎,比如 disruptor,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。

实例代码:

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java

你的点赞与分享是对我最大的支持

© 著作权归作者所有

共有 人打赏支持
crossoverJie
粉丝 648
博文 83
码字总数 157830
作品 0
江北
后端工程师
私信 提问
加载中

评论(17)

f
freezingsky
一般做法,线程外层加个try catch.
三两带走
三两带走
线程池里的异常要处理好啊,像定时任务的线程如果抛异常了,你的定时任务就挂啦
y
yinheyimin
为吸引相关世界各地人才来港,我们将通过“优秀人才入境计划”按现时年度1000的配额,为人才清单下合资格人士提供入境便利。在“优秀人才入境计划”下,获批准的申请者无须在来港定居前先获得本地僱主聘任。香港公布人才清单,多项优惠就等你来!香港优才计划申请条件评估网址:(http://www.galaxy-immi.com/obscure/assessment/1.html?pla=sq&spreadword=kyzg)符合人才清单相关专业资格的申请者,可在“优秀人才入境计划”的“综合计分制”下获得额外分数。
crossoverJie
crossoverJie

引用来自“JPer”的评论

消费线程死掉了,没有重启拉起,线程池当然等待了啊;
死掉了怎么拉起,线程对象都回收了。

等待的原因和你前面说的没有因果关系。
crossoverJie
crossoverJie

引用来自“蔡晓建”的评论

没看出有什么问题,这是正常现象。工作线程在队列取不到,waiting不是很正常么
出现问题是正常,但会导致业务不正常,所有才会有后面的分析以及解决问题。
crossoverJie
crossoverJie

引用来自“开源中国首席聊天玩家”的评论

这个是正常的啊 disruptor的消费线程如果发生异常不进行捕获,会导致消费阻塞,定义异常捕获器去处理就好了
出现问题是正常,但会导致业务不正常,所有才会有后面的分析以及解决问题。
12叔
12叔
看到标题就猜到是没有捕获异常的问题
开源中国首席聊天玩家
开源中国首席聊天玩家
这个是正常的啊 disruptor的消费线程如果发生异常不进行捕获,会导致消费阻塞,定义异常捕获器去处理就好了
蔡晓建
蔡晓建
没看出有什么问题,这是正常现象。工作线程在队列取不到,waiting不是很正常么
wy65
wy65

引用来自“wy65”的评论

是不是可以这样理解,消费者线程池创建的时候,往里面放了一个Runnable对象,执行死循环,不停的取数据的,然后这个Runnable对象执行的过程中发生了异常,导致消费者线程池中没有了任务,所以一直在getTask()中等待。最后,没有consumer消费producter的数据,导致了bug

引用来自“crossoverJie”的评论

导致消费者线程池中没有了任务,所以一直在getTask()中等待。

这句话并没有因果关系,中间漏了一点。

线程池里的任务确实也没有了,异常之后会创建一个 Worker 线程,是它一直在 getTask 取不到任务,所以就是出于 waiting 状态了。
那么这个bug原因就一句话,任务执行的时候没有处理异常
JAVA多线程-基础Lock Condition 并发集合

前篇 JAVA多线程-基础Synchronized 后篇 JAVA多线程-交互计算 Future Callable Promise 后篇 JAVA多线程-线程池-实例模拟上厕所问题 跟上一篇文章比较,这次改进了之前的代码,使用了Lock Cond...

xpbug
2012/11/08
0
5
JAVA多线程-基础Synchronized

后篇: JAVA多线程-基础Lock Condition 并发集合 JAVA多线程-交互计算 Future Callable Promise 读懂代码,首先要懂得thread的几个状态,以及它们之间的转换. Java thread的状态有new, runnable...

xpbug
2012/11/07
0
10
包庇“安卓之父”性骚扰惹众怒,谷歌大罢工席卷全球!

谷歌员工说,时间到了,随即他们走上了大街,而Twitter上也“炸”了。 美国时间周四即11月1日上午11点,成千上万的谷歌工作人员走出世界各地的办公室,以“Google walkout”为代号,让这次运...

程序员之家_
2018/11/02
0
0
Windows 10 惊现尴尬 Bug!24 核竟然卡成蜗牛

很多人将 Windows 10 系统称为 “Bug10” ,虽然太言过其实,但不可否认的是,Windows 10 确实经常会出现一些很诡异的 Bug 。 来看看 Google 程序员 Bruce Dawson 的遭遇。 公司为他配备了一...

王练
2017/07/18
4.2K
58
谷歌又麻烦了,1500+ 员工罢工,抗议公司包庇安卓之父等

(给程序员的那些事加星标) BuzzFeed 在10月30日曾报道,四名知情人士透露,谷歌 200 多名工程师本周晚些时候将组织一场全公司范围的“女性游行”罢工,以抗议谷歌包庇公司前高管,安卓之父...

程序员的那些事_
2018/11/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

欧拉公式

欧拉公式表达式 欧拉公式的几何意 cosθ + j sinθ 是个复数,实数部分也就是实部为 cosθ ,虚数部分也就是虚部为 j sinθ ,对应复平面单位圆上的一个点。 根据欧拉公式和这个点可以用 复指...

sharelocked
36分钟前
2
0
burpsuite无法抓取https数据包

1.将浏览器和burpsuite的代理都设置好 2.在浏览器地址栏输入: http://burp 3.下载下面的证书,并将证书导入浏览器 cacert.der

Frost729
今天
1
0
JeeSite4.x 消息管理、消息推送、消息提醒

实现统一的消息推送接口,包含PC消息、短信消息、邮件消息、微信消息等,无需让所有开发者了解消息是怎么发送出去的,只需了解消息发送接口即可。 所有推送消息均通过 MsgPushUtils 工具类发...

ThinkGem
今天
6
0
OpenML

https://www.openml.org/search?type=data

shengjuntu
今天
2
0
java强引用,软引用,弱引用和虚引用

先来简要说一下这四种引用的特性: 强引用:如果一个对象具有强引用,那垃圾回收器绝不会回收它 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它 弱引用:在垃圾...

woshixin
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部